From f8e9bcbb26a67a804798ca04a5e5315278a1c84a Mon Sep 17 00:00:00 2001
From: Volker Schukai <volker.schukai@schukai.com>
Date: Sat, 21 Dec 2024 21:08:50 +0100
Subject: [PATCH] feat(log): update the apearance of the log #270

---
 development/issues/open/270.html              |   24 +
 development/issues/open/270.mjs               |   46 +
 .../components/datatable/datasource/rest.mjs  |   17 +
 source/components/state/log.mjs               |   89 +-
 source/components/state/log/entry.mjs         |    8 +-
 source/components/state/style/log.pcss        |   85 +-
 source/components/state/stylesheet/log.mjs    |   21 +-
 source/data/transformer.mjs                   | 1595 +++++++++--------
 source/i18n/time-ago.mjs                      |  671 +++++++
 test/cases/i18n/time-ago.mjs                  |   33 +
 10 files changed, 1672 insertions(+), 917 deletions(-)
 create mode 100644 development/issues/open/270.html
 create mode 100644 development/issues/open/270.mjs
 create mode 100644 source/i18n/time-ago.mjs
 create mode 100644 test/cases/i18n/time-ago.mjs

diff --git a/development/issues/open/270.html b/development/issues/open/270.html
new file mode 100644
index 000000000..7b0659cde
--- /dev/null
+++ b/development/issues/open/270.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="de">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>update and optimize data-bind #270</title>
+    <script src="./270.mjs" type="module"></script>
+</head>
+<body>
+    <h1>update and optimize data-bind #270</h1>
+    <p></p>
+    <ul>
+        <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/270">Issue #270</a></li>
+        <li><a href="/">Back to overview</a></li>
+    </ul>
+    <main>
+
+<monster-log id="Bai3A">
+
+</monster-log>
+
+    </main>
+</body>
+</html>
diff --git a/development/issues/open/270.mjs b/development/issues/open/270.mjs
new file mode 100644
index 000000000..53d2603d1
--- /dev/null
+++ b/development/issues/open/270.mjs
@@ -0,0 +1,46 @@
+/**
+* @file development/issues/open/270.mjs
+* @url https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/270
+* @description update and optimize data-bind
+* @issue 270
+*/
+
+import "../../../source/components/style/property.pcss";
+import "../../../source/components/style/link.pcss";
+import "../../../source/components/style/color.pcss";
+import "../../../source/components/style/theme.pcss";
+import "../../../source/components/style/normalize.pcss";
+import "../../../source/components/style/typography.pcss";
+import "../../../source/components/state/log.mjs";
+import {Entry} from "../../../source/components/state/log/entry.mjs";
+
+
+
+const element = document.getElementById('Bai3A')
+
+element.addEntry(new Entry({title: "Test", message: "Test", user: "Administrator", date: new Date()}))
+
+const for10MinutesDate = new Date();
+for10MinutesDate.setMinutes(for10MinutesDate.getMinutes() - 10);
+
+element.addEntry(new Entry({title: "Test", message: "Test" , user: "Hans Meier", date: for10MinutesDate}))
+
+const for1HourAnd16MinutesDate = new Date();
+for1HourAnd16MinutesDate.setHours(for1HourAnd16MinutesDate.getHours() - 1);
+for1HourAnd16MinutesDate.setMinutes(for1HourAnd16MinutesDate.getMinutes() - 16);
+
+element.addEntry(new Entry({title: "Test", message: "Test", user: "Administrator", date: for1HourAnd16MinutesDate}))
+
+const for10Days5HoursAnd30MinutesDate = new Date();
+for10Days5HoursAnd30MinutesDate.setDate(for10Days5HoursAnd30MinutesDate.getDate() - 10);
+for10Days5HoursAnd30MinutesDate.setHours(for10Days5HoursAnd30MinutesDate.getHours() - 5);
+for10Days5HoursAnd30MinutesDate.setMinutes(for10Days5HoursAnd30MinutesDate.getMinutes() - 30);
+
+element.addEntry(new Entry({title: "Test", message: "Test", user: "Fritz Müller", date: for10Days5HoursAnd30MinutesDate}))
+
+const for1Year2MonthsAnd3DaysDate = new Date();
+for1Year2MonthsAnd3DaysDate.setFullYear(for1Year2MonthsAnd3DaysDate.getFullYear() - 1);
+for1Year2MonthsAnd3DaysDate.setMonth(for1Year2MonthsAnd3DaysDate.getMonth() - 2);
+for1Year2MonthsAnd3DaysDate.setDate(for1Year2MonthsAnd3DaysDate.getDate() - 3);
+
+element.addEntry(new Entry({title: "Test", message: "Test", user: "Administrator", date: for1Year2MonthsAnd3DaysDate}))
diff --git a/source/components/datatable/datasource/rest.mjs b/source/components/datatable/datasource/rest.mjs
index da49bbc39..cb0deebc0 100644
--- a/source/components/datatable/datasource/rest.mjs
+++ b/source/components/datatable/datasource/rest.mjs
@@ -81,6 +81,9 @@ const filterObserverSymbol = Symbol("filterObserver");
  * @copyright schukai GmbH
  * @summary A rest api datasource
  */
+
+
+
 class Rest extends Datasource {
 	/**
 	 * the constructor of the class
@@ -453,6 +456,13 @@ function initFilter() {
 	filterControl.attachObserver(this[filterObserverSymbol]);
 }
 
+/**
+ * @private
+ * @param json
+ * @param response
+ * @param filterControl
+ * @returns {Promise<never>|Promise<Awaited<unknown>>}
+ */
 function handleIntersectionObserver(json, response, filterControl) {
 	const path = new Pathfinder(json);
 
@@ -495,6 +505,9 @@ function initAutoInit() {
 	});
 }
 
+/**
+ * @private
+ */
 function initEventHandler() {
 	this[intersectionObserverHandlerSymbol] = (entries) => {
 		entries.forEach((entry) => {
@@ -515,6 +528,9 @@ function initEventHandler() {
 	};
 }
 
+/**
+ * @private
+ */
 function initIntersectionObserver() {
 	this.classList.add("intersection-observer");
 
@@ -528,6 +544,7 @@ function initIntersectionObserver() {
 		this[intersectionObserverHandlerSymbol],
 		options,
 	);
+
 	this[intersectionObserverObserverSymbol].observe(this);
 }
 
diff --git a/source/components/state/log.mjs b/source/components/state/log.mjs
index a1f257663..2d9b15fd3 100644
--- a/source/components/state/log.mjs
+++ b/source/components/state/log.mjs
@@ -21,7 +21,7 @@ import {
 } from "../../dom/customelement.mjs";
 import { LogStyleSheet } from "./stylesheet/log.mjs";
 import { Entry } from "./log/entry.mjs";
-import { validateInstance } from "../../types/validate.mjs";
+import {validateInstance, validateString} from "../../types/validate.mjs";
 import "./state.mjs";
 
 export { Log };
@@ -39,14 +39,16 @@ const logElementSymbol = Symbol("logElement");
 const emptyStateElementSymbol = Symbol("emptyStateElement");
 
 /**
- * A Log component
+ * A log entry
  *
- * @fragments /fragments/components/layout/collapse/
+ * @fragments /fragments/components/state/log
+ *
+ * @example /examples/components/state/log-simple
  *
  * @since 3.74.0
  * @copyright schukai GmbH
- * @summary A Log component to show a log message.
- */
+ * @summary The log entry is a single entry in the log.
+ **/
 class Log extends CustomElement {
 	/**
 	 * @return {void}
@@ -116,7 +118,7 @@ class Log extends CustomElement {
 
 	/**
 	 * Add an entry to the log
-	 * @param entry
+	 * @param {Entry} entry
 	 * @return {Log}
 	 */
 	addEntry(entry) {
@@ -133,15 +135,19 @@ class Log extends CustomElement {
 
 	/**
 	 * Add a log message
-	 * @param message
-	 * @param date
+	 *
+	 * @param {string} message
+	 * @param {Date} date
 	 * @return {Log}
+	 * @throws {TypeError} message is not a string
 	 */
 	addMessage(message, date) {
 		if (!date) {
 			date = new Date();
 		}
 
+		validateString(message);
+
 		this.addEntry(
 			new Entry({
 				message: message,
@@ -213,43 +219,40 @@ function initEventHandler() {
  */
 function getTemplate() {
 	// language=HTML
-	return `
-        <template id="entry">
-            <li data-monster-role="entry">
-                <span></span>
-                <div data-monster-replace="path:entry.title"
-                     data-monster-attributes="class path:entry.title | ?:title:hidden"></div>
-                <div data-monster-replace="path:entry.message"
-                     data-monster-attributes="class path:entry.message | ?:message:hidden"></div>
-                <div data-monster-replace="path:entry.user"
-                     data-monster-attributes="class path:entry.user | ?:user:hidden"></div>
-                <div data-monster-attributes="class path:entry.date | is-set | ?:datetime:hidden">
-                    <span data-monster-replace="path:entry.date | date"></span>
-                    <span data-monster-replace="path:entry.date | time"></span>
-                </div>
-            </li>
-        </template>
-        <div part="control" data-monster-role="control">
-            <div data-monster-role="empty-state" data-monster-attributes="class path:entries | has-entries | ?:hidden:">
-                <monster-state>
-                    <div part="visual">
-                        <svg width="4rem" height="4rem" viewBox="0 -12 512.00032 512"
-                             xmlns="http://www.w3.org/2000/svg">
-                            <path d="m455.074219 172.613281 53.996093-53.996093c2.226563-2.222657 3.273438-5.367188 2.828126-8.480469-.441407-3.113281-2.328126-5.839844-5.085938-7.355469l-64.914062-35.644531c-4.839844-2.65625-10.917969-.886719-13.578126 3.953125-2.65625 4.84375-.890624 10.921875 3.953126 13.578125l53.234374 29.230469-46.339843 46.335937-166.667969-91.519531 46.335938-46.335938 46.839843 25.722656c4.839844 2.65625 10.921875.890626 13.578125-3.953124 2.660156-4.839844.890625-10.921876-3.953125-13.578126l-53.417969-29.335937c-3.898437-2.140625-8.742187-1.449219-11.882812 1.695313l-54 54-54-54c-3.144531-3.144532-7.988281-3.832032-11.882812-1.695313l-184.929688 101.546875c-2.757812 1.515625-4.644531 4.238281-5.085938 7.355469-.445312 3.113281.601563 6.257812 2.828126 8.480469l53.996093 53.996093-53.996093 53.992188c-2.226563 2.226562-3.273438 5.367187-2.828126 8.484375.441407 3.113281 2.328126 5.839844 5.085938 7.351562l55.882812 30.6875v102.570313c0 3.652343 1.988282 7.011719 5.1875 8.769531l184.929688 101.542969c1.5.824219 3.15625 1.234375 4.8125 1.234375s3.3125-.410156 4.8125-1.234375l184.929688-101.542969c3.199218-1.757812 5.1875-5.117188 5.1875-8.769531v-102.570313l55.882812-30.683594c2.757812-1.515624 4.644531-4.242187 5.085938-7.355468.445312-3.113282-.601563-6.257813-2.828126-8.480469zm-199.074219 90.132813-164.152344-90.136719 164.152344-90.140625 164.152344 90.140625zm-62.832031-240.367188 46.332031 46.335938-166.667969 91.519531-46.335937-46.335937zm-120.328125 162.609375 166.667968 91.519531-46.339843 46.339844-166.671875-91.519531zm358.089844 184.796875-164.929688 90.5625v-102.222656c0-5.523438-4.476562-10-10-10s-10 4.476562-10 10v102.222656l-164.929688-90.5625v-85.671875l109.046876 59.878907c1.511718.828124 3.167968 1.234374 4.808593 1.234374 2.589844 0 5.152344-1.007812 7.074219-2.929687l54-54 54 54c1.921875 1.925781 4.484375 2.929687 7.074219 2.929687 1.640625 0 3.296875-.40625 4.808593-1.234374l109.046876-59.878907zm-112.09375-46.9375-46.339844-46.34375 166.667968-91.515625 46.34375 46.335938zm0 0"/>
-                            <path d="m404.800781 68.175781c2.628907 0 5.199219-1.070312 7.070313-2.933593 1.859375-1.859376 2.929687-4.4375 2.929687-7.066407 0-2.632812-1.070312-5.210937-2.929687-7.070312-1.859375-1.863281-4.441406-2.929688-7.070313-2.929688-2.640625 0-5.210937 1.066407-7.070312 2.929688-1.871094 1.859375-2.929688 4.4375-2.929688 7.070312 0 2.628907 1.058594 5.207031 2.929688 7.066407 1.859375 1.863281 4.441406 2.933593 7.070312 2.933593zm0 0"/>
-                            <path d="m256 314.925781c-2.628906 0-5.210938 1.066407-7.070312 2.929688-1.859376 1.867187-2.929688 4.4375-2.929688 7.070312 0 2.636719 1.070312 5.207031 2.929688 7.078125 1.859374 1.859375 4.441406 2.921875 7.070312 2.921875s5.210938-1.0625 7.070312-2.921875c1.859376-1.871094 2.929688-4.441406 2.929688-7.078125 0-2.632812-1.070312-5.203125-2.929688-7.070312-1.859374-1.863281-4.441406-2.929688-7.070312-2.929688zm0 0"/>
-                        </svg>
-                    </div>
-                    <div part="content" monster-replace="path:labels.nothingToReport">
-                        There is nothing to report yet.
-                    </div>
-                </monster-state>
+	return `<template id="entry">
+    <li data-monster-role="entry">
+        <span data-monster-replace="path:entry.user"
+              data-monster-attributes="class path:entry.user | ?:user:hidden"></span>
+        <span data-monster-replace="path:entry.title"
+              data-monster-attributes="class path:entry.title | ?:title:hidden"></span>
+        <span data-monster-replace="path:entry.message"
+              data-monster-attributes="class path:entry.message | ?:message:hidden"></span>
+        <span data-monster-replace="path:entry.date | time-ago" data-monster-attributes="title path:entry.date | datetime"></span>
+        
+    </li>
+</template>
+
+<div part="control" data-monster-role="control">
+    <div data-monster-role="empty-state" data-monster-attributes="class path:entries | has-entries | ?:hidden:">
+        <monster-state>
+            <div part="visual">
+                <svg width="4rem" height="4rem" viewBox="0 -12 512.00032 512"
+                     xmlns="http://www.w3.org/2000/svg">
+                    <path d="m455.074219 172.613281 53.996093-53.996093c2.226563-2.222657 3.273438-5.367188 2.828126-8.480469-.441407-3.113281-2.328126-5.839844-5.085938-7.355469l-64.914062-35.644531c-4.839844-2.65625-10.917969-.886719-13.578126 3.953125-2.65625 4.84375-.890624 10.921875 3.953126 13.578125l53.234374 29.230469-46.339843 46.335937-166.667969-91.519531 46.335938-46.335938 46.839843 25.722656c4.839844 2.65625 10.921875.890626 13.578125-3.953124 2.660156-4.839844.890625-10.921876-3.953125-13.578126l-53.417969-29.335937c-3.898437-2.140625-8.742187-1.449219-11.882812 1.695313l-54 54-54-54c-3.144531-3.144532-7.988281-3.832032-11.882812-1.695313l-184.929688 101.546875c-2.757812 1.515625-4.644531 4.238281-5.085938 7.355469-.445312 3.113281.601563 6.257812 2.828126 8.480469l53.996093 53.996093-53.996093 53.992188c-2.226563 2.226562-3.273438 5.367187-2.828126 8.484375.441407 3.113281 2.328126 5.839844 5.085938 7.351562l55.882812 30.6875v102.570313c0 3.652343 1.988282 7.011719 5.1875 8.769531l184.929688 101.542969c1.5.824219 3.15625 1.234375 4.8125 1.234375s3.3125-.410156 4.8125-1.234375l184.929688-101.542969c3.199218-1.757812 5.1875-5.117188 5.1875-8.769531v-102.570313l55.882812-30.683594c2.757812-1.515624 4.644531-4.242187 5.085938-7.355468.445312-3.113282-.601563-6.257813-2.828126-8.480469zm-199.074219 90.132813-164.152344-90.136719 164.152344-90.140625 164.152344 90.140625zm-62.832031-240.367188 46.332031 46.335938-166.667969 91.519531-46.335937-46.335937zm-120.328125 162.609375 166.667968 91.519531-46.339843 46.339844-166.671875-91.519531zm358.089844 184.796875-164.929688 90.5625v-102.222656c0-5.523438-4.476562-10-10-10s-10 4.476562-10 10v102.222656l-164.929688-90.5625v-85.671875l109.046876 59.878907c1.511718.828124 3.167968 1.234374 4.808593 1.234374 2.589844 0 5.152344-1.007812 7.074219-2.929687l54-54 54 54c1.921875 1.925781 4.484375 2.929687 7.074219 2.929687 1.640625 0 3.296875-.40625 4.808593-1.234374l109.046876-59.878907zm-112.09375-46.9375-46.339844-46.34375 166.667968-91.515625 46.34375 46.335938zm0 0"/>
+                    <path d="m404.800781 68.175781c2.628907 0 5.199219-1.070312 7.070313-2.933593 1.859375-1.859376 2.929687-4.4375 2.929687-7.066407 0-2.632812-1.070312-5.210937-2.929687-7.070312-1.859375-1.863281-4.441406-2.929688-7.070313-2.929688-2.640625 0-5.210937 1.066407-7.070312 2.929688-1.871094 1.859375-2.929688 4.4375-2.929688 7.070312 0 2.628907 1.058594 5.207031 2.929688 7.066407 1.859375 1.863281 4.441406 2.933593 7.070312 2.933593zm0 0"/>
+                    <path d="m256 314.925781c-2.628906 0-5.210938 1.066407-7.070312 2.929688-1.859376 1.867187-2.929688 4.4375-2.929688 7.070312 0 2.636719 1.070312 5.207031 2.929688 7.078125 1.859374 1.859375 4.441406 2.921875 7.070312 2.921875s5.210938-1.0625 7.070312-2.921875c1.859376-1.871094 2.929688-4.441406 2.929688-7.078125 0-2.632812-1.070312-5.203125-2.929688-7.070312-1.859374-1.863281-4.441406-2.929688-7.070312-2.929688zm0 0"/>
+                </svg>
             </div>
-            <div data-monster-role="entries">
-                <ul data-monster-insert="entry path:entries">
-                </ul>
+            <div part="content" data-monster-replace="path:labels.nothingToReport">
+                There is nothing to report yet.
             </div>
-        </div>
+        </monster-state>
+    </div>
+    <div data-monster-role="entries">
+        <ul data-monster-insert="entry path:entries">
+        </ul>
+    </div>
+</div>
     `;
 }
 
diff --git a/source/components/state/log/entry.mjs b/source/components/state/log/entry.mjs
index b0f7109f8..a8b94337d 100644
--- a/source/components/state/log/entry.mjs
+++ b/source/components/state/log/entry.mjs
@@ -66,10 +66,10 @@ class Entry extends Base {
 	 */
 	get internalDefaults() {
 		return {
-			title: undefined,
-			message: undefined,
-			user: undefined,
-			date: undefined,
+			title: null,
+			message: null,
+			user: null,
+			date: null,
 		};
 	}
 
diff --git a/source/components/state/style/log.pcss b/source/components/state/style/log.pcss
index e93a0e53d..9fb652e60 100644
--- a/source/components/state/style/log.pcss
+++ b/source/components/state/style/log.pcss
@@ -8,21 +8,16 @@
 @import "../../style/mixin/hover.pcss";
 
 [data-monster-role=entries] {
-    overflow: hidden;
-    padding: 10px 0 40px 60px;
+
 
     & ul {
         list-style-type: none;
         margin: 0;
-        padding: 0;
+        padding: 0 0 0 1.8rem;
         position: relative;
         top: 0
     }
 
-    & ul:last-of-type {
-        top: 2rem;
-    }
-
     & ul:before {
         content: "";
         display: block;
@@ -31,81 +26,37 @@
         border: 1px dashed var(--monster-color-primary-2);
         position: absolute;
         top: 0;
-        left: 30px
+        left: 1rem;
+    }
+
+    .title {
+        font-weight: bold;
     }
 
     & ul li {
-        margin: 20px 60px 60px;
+        margin: 0;
         position: relative;
-        padding: 10px 20px;
-        color: var(--monster-color-primary-2);
-        background-color: var(--monster-bg-color-primary-2);
+        padding: 0.1rem 0.3rem;
+        color: var(--monster-color-primary-1);
+        background-color: var(--monster-bg-color-primary-1);
         border-radius: 5px;
     }
 
 
-    & ul li > span {
-        content: "";
-        display: block;
-        width: 0;
-        height: 100%;
-        border: 1px solid var(--monster-color-primary-2);
-        position: absolute;
-        top: 0;
-        left: -30px
-    }
-
-    & ul li > span:before, & ul li > span:after {
+    & ul li:before {
         content: "";
         display: block;
-        width: 10px;
-        height: 10px;
+        width: 5px;
+        height: 5px;
         border-radius: 50%;
-        background: var(--monster-bg-color-primary-2);
-        border: 2px solid var(--monster-color-primary-2);
-        position: absolute;
-        left: -7.5px
-    }
-
-    & ul li > span:before {
-        top: -10px
-    }
-
-    & ul li > span:after {
-        top: 95%
-    }
-
-    & .title {
-        text-transform: uppercase;
-        margin-bottom: 5px
-    }
-
-    & .message:first-letter {
-    }
-
-    & .user {
-        margin-top: 10px;
-        font-style: italic;
-        text-align: right;
-        margin-right: 20px;
-        font-size: 0.8rem;
-    }
-
-    & .datetime span {
+        background: var(--monster-bg-color-primary-3);
+        border: 1px solid var(--monster-color-primary-2);
         position: absolute;
-        left: -100px;
-        color: var(--monster-color-primary-1);
-        background-color: var(--monster-bg-color-primary-1);
-        font-size: 0.5rem;
+        left: -0.9rem;
+        top: 0.6rem;
     }
 
-    & .datetime span:first-child {
-        top: -16px
-    }
 
-    & .datetime span:last-child {
-        top: 94%
-    }
 
 
 }
\ No newline at end of file
diff --git a/source/components/state/stylesheet/log.mjs b/source/components/state/stylesheet/log.mjs
index 6d8ac2f09..fcf5bbfb4 100644
--- a/source/components/state/stylesheet/log.mjs
+++ b/source/components/state/stylesheet/log.mjs
@@ -10,10 +10,10 @@
  * For more information about purchasing a commercial license, please contact schukai GmbH.
  */
 
-import { addAttributeToken } from "../../../dom/attributes.mjs";
-import { ATTRIBUTE_ERRORMESSAGE } from "../../../dom/constants.mjs";
+import {addAttributeToken} from "../../../dom/attributes.mjs";
+import {ATTRIBUTE_ERRORMESSAGE} from "../../../dom/constants.mjs";
 
-export { LogStyleSheet };
+export {LogStyleSheet}
 
 /**
  * @private
@@ -22,17 +22,10 @@ export { LogStyleSheet };
 const LogStyleSheet = new CSSStyleSheet();
 
 try {
-	LogStyleSheet.insertRule(
-		`
+  LogStyleSheet.insertRule(`
 @layer log { 
-[data-monster-role=control]{box-sizing:border-box;outline:none;width:100%}[data-monster-role=control].flex{align-items:center;display:flex;flex-direction:row}:host{box-sizing:border-box;display:block}.block{display:block}.inline{display:inline}.inline-block{display:inline-block}.grid{display:grid}.inline-grid{display:inline-grid}.flex{display:flex}.inline-flex{display:inline-flex}.hidden,.hide,.none{display:none}.visible{visibility:visible}.invisible{visibility:hidden}.monster-border-primary-1,.monster-border-primary-2,.monster-border-primary-3,.monster-border-primary-4{border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width)}.monster-border-0{border-radius:0;border-style:none;border-width:0}.monster-border-primary-1{border-color:var(--monster-bg-color-primary-1)}.monster-border-primary-2{border-color:var(--monster-bg-color-primary-2)}.monster-border-primary-3{border-color:var(--monster-bg-color-primary-3)}.monster-border-primary-4{border-color:var(--monster-bg-color-primary-4)}.monster-border-secondary-1,.monster-border-secondary-2,.monster-border-secondary-3,.monster-border-secondary-4{border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width)}.monster-border-secondary-1{border-color:var(--monster-bg-color-secondary-1)}.monster-border-secondary-2{border-color:var(--monster-bg-color-secondary-2)}.monster-border-secondary-3{border-color:var(--monster-bg-color-secondary-3)}.monster-border-secondary-4{border-color:var(--monster-bg-color-secondary-4)}.monster-border-tertiary-1,.monster-border-tertiary-2,.monster-border-tertiary-3,.monster-border-tertiary-4{border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width)}.monster-border-tertiary-1{border-color:var(--monster-bg-color-tertiary-1)}.monster-border-tertiary-2{border-color:var(--monster-bg-color-tertiary-2)}.monster-border-tertiary-3{border-color:var(--monster-bg-color-tertiary-3)}.monster-border-tertiary-4{border-color:var(--monster-bg-color-tertiary-4)}[data-monster-role=entries]{overflow:hidden;padding:10px 0 40px 60px}[data-monster-role=entries] ul{list-style-type:none;margin:0;padding:0;position:relative;top:0}[data-monster-role=entries] ul:last-of-type{top:2rem}[data-monster-role=entries] ul:before{border:1px dashed var(--monster-color-primary-2);content:\"\";display:block;height:100%;left:30px;position:absolute;top:0;width:0}[data-monster-role=entries] ul li{background-color:var(--monster-bg-color-primary-2);border-radius:5px;color:var(--monster-color-primary-2);margin:20px 60px 60px;padding:10px 20px;position:relative}[data-monster-role=entries] ul li>span{border:1px solid var(--monster-color-primary-2);content:\"\";display:block;height:100%;left:-30px;position:absolute;top:0;width:0}[data-monster-role=entries] ul li>span:after,[data-monster-role=entries] ul li>span:before{background:var(--monster-bg-color-primary-2);border:2px solid var(--monster-color-primary-2);border-radius:50%;content:\"\";display:block;height:10px;left:-7.5px;position:absolute;width:10px}[data-monster-role=entries] ul li>span:before{top:-10px}[data-monster-role=entries] ul li>span:after{top:95%}[data-monster-role=entries] .title{margin-bottom:5px;text-transform:uppercase}[data-monster-role=entries] .user{font-size:.8rem;font-style:italic;margin-right:20px;margin-top:10px;text-align:right}[data-monster-role=entries] .datetime span{background-color:var(--monster-bg-color-primary-1);color:var(--monster-color-primary-1);font-size:.5rem;left:-100px;position:absolute}[data-monster-role=entries] .datetime span:first-child{top:-16px}[data-monster-role=entries] .datetime span:last-child{top:94%} 
-}`,
-		0,
-	);
+[data-monster-role=control]{box-sizing:border-box;outline:none;width:100%}[data-monster-role=control].flex{align-items:center;display:flex;flex-direction:row}:host{box-sizing:border-box;display:block}.block{display:block}.inline{display:inline}.inline-block{display:inline-block}.grid{display:grid}.inline-grid{display:inline-grid}.flex{display:flex}.inline-flex{display:inline-flex}.hidden,.hide,.none{display:none}.visible{visibility:visible}.invisible{visibility:hidden}.monster-border-primary-1,.monster-border-primary-2,.monster-border-primary-3,.monster-border-primary-4{border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width)}.monster-border-0{border-radius:0;border-style:none;border-width:0}.monster-border-primary-1{border-color:var(--monster-bg-color-primary-1)}.monster-border-primary-2{border-color:var(--monster-bg-color-primary-2)}.monster-border-primary-3{border-color:var(--monster-bg-color-primary-3)}.monster-border-primary-4{border-color:var(--monster-bg-color-primary-4)}.monster-border-secondary-1,.monster-border-secondary-2,.monster-border-secondary-3,.monster-border-secondary-4{border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width)}.monster-border-secondary-1{border-color:var(--monster-bg-color-secondary-1)}.monster-border-secondary-2{border-color:var(--monster-bg-color-secondary-2)}.monster-border-secondary-3{border-color:var(--monster-bg-color-secondary-3)}.monster-border-secondary-4{border-color:var(--monster-bg-color-secondary-4)}.monster-border-tertiary-1,.monster-border-tertiary-2,.monster-border-tertiary-3,.monster-border-tertiary-4{border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width)}.monster-border-tertiary-1{border-color:var(--monster-bg-color-tertiary-1)}.monster-border-tertiary-2{border-color:var(--monster-bg-color-tertiary-2)}.monster-border-tertiary-3{border-color:var(--monster-bg-color-tertiary-3)}.monster-border-tertiary-4{border-color:var(--monster-bg-color-tertiary-4)}[data-monster-role=entries] ul{list-style-type:none;margin:0;padding:0 0 0 1.8rem;position:relative;top:0}[data-monster-role=entries] ul:before{border:1px dashed var(--monster-color-primary-2);content:\"\";display:block;height:100%;left:1rem;position:absolute;top:0;width:0}[data-monster-role=entries] .title{font-weight:700}[data-monster-role=entries] ul li{background-color:var(--monster-bg-color-primary-1);border-radius:5px;color:var(--monster-color-primary-1);margin:0;padding:.1rem .3rem;position:relative}[data-monster-role=entries] ul li:before{background:var(--monster-bg-color-primary-3);border:1px solid var(--monster-color-primary-2);border-radius:50%;content:\"\";display:block;height:5px;left:-.9rem;position:absolute;top:.6rem;width:5px} 
+}`, 0);
 } catch (e) {
-	addAttributeToken(
-		document.getRootNode().querySelector("html"),
-		ATTRIBUTE_ERRORMESSAGE,
-		e + "",
-	);
+  addAttributeToken(document.getRootNode().querySelector('html'), ATTRIBUTE_ERRORMESSAGE, e + "");
 }
diff --git a/source/data/transformer.mjs b/source/data/transformer.mjs
index ff4f31c45..3b679c963 100644
--- a/source/data/transformer.mjs
+++ b/source/data/transformer.mjs
@@ -12,27 +12,28 @@
  * SPDX-License-Identifier: AGPL-3.0
  */
 
-import { getLocaleOfDocument } from "../dom/locale.mjs";
-import { Base } from "../types/base.mjs";
-import { getGlobal, getGlobalObject } from "../types/global.mjs";
-import { ID } from "../types/id.mjs";
-import { isArray, isObject, isString, isPrimitive } from "../types/is.mjs";
+import {getLocaleOfDocument} from "../dom/locale.mjs";
+import {Base} from "../types/base.mjs";
+import {getGlobal, getGlobalObject} from "../types/global.mjs";
+import {ID} from "../types/id.mjs";
+import {isArray, isObject, isString, isPrimitive} from "../types/is.mjs";
 import {
-	getDocumentTranslations,
-	Translations,
+    getDocumentTranslations,
+    Translations,
 } from "../i18n/translations.mjs";
 import {
-	validateFunction,
-	validateInteger,
-	validateObject,
-	validatePrimitive,
-	validateString,
-	validateBoolean,
+    validateFunction,
+    validateInteger,
+    validateObject,
+    validatePrimitive,
+    validateString,
+    validateBoolean,
 } from "../types/validate.mjs";
-import { clone } from "../util/clone.mjs";
-import { Pathfinder } from "./pathfinder.mjs";
+import {clone} from "../util/clone.mjs";
+import {Pathfinder} from "./pathfinder.mjs";
+import {formatTimeAgo} from "../i18n/time-ago.mjs";
 
-export { Transformer };
+export {Transformer};
 
 /**
  * The transformer class is a swiss army knife for manipulating values.
@@ -52,53 +53,53 @@ export { Transformer };
  * @summary The transformer class is a swiss army knife for manipulating values. especially in combination with the pipe, processing chains can be built up.
  */
 class Transformer extends Base {
-	/**
-	 *
-	 * @param {string} definition
-	 */
-	constructor(definition) {
-		super();
-		this.args = disassemble(definition);
-		this.command = this.args.shift();
-		this.callbacks = new Map();
-	}
-
-	/**
-	 *
-	 * @param {string} name
-	 * @param {function} callback
-	 * @param {object} context
-	 * @return {Transformer}
-	 * @throws {TypeError} value is not a string
-	 * @throws {TypeError} value is not a function
-	 */
-	setCallback(name, callback, context) {
-		validateString(name);
-		validateFunction(callback);
-
-		if (context !== undefined) {
-			validateObject(context);
-		}
-
-		this.callbacks.set(name, {
-			callback: callback,
-			context: context,
-		});
-
-		return this;
-	}
-
-	/**
-	 *
-	 * @param {*} value
-	 * @return {*}
-	 * @throws {Error} unknown command
-	 * @throws {TypeError} unsupported type
-	 * @throws {Error} type not supported
-	 */
-	run(value) {
-		return transform.apply(this, [value]);
-	}
+    /**
+     *
+     * @param {string} definition
+     */
+    constructor(definition) {
+        super();
+        this.args = disassemble(definition);
+        this.command = this.args.shift();
+        this.callbacks = new Map();
+    }
+
+    /**
+     *
+     * @param {string} name
+     * @param {function} callback
+     * @param {object} context
+     * @return {Transformer}
+     * @throws {TypeError} value is not a string
+     * @throws {TypeError} value is not a function
+     */
+    setCallback(name, callback, context) {
+        validateString(name);
+        validateFunction(callback);
+
+        if (context !== undefined) {
+            validateObject(context);
+        }
+
+        this.callbacks.set(name, {
+            callback: callback,
+            context: context,
+        });
+
+        return this;
+    }
+
+    /**
+     *
+     * @param {*} value
+     * @return {*}
+     * @throws {Error} unknown command
+     * @throws {TypeError} unsupported type
+     * @throws {Error} type not supported
+     */
+    run(value) {
+        return transform.apply(this, [value]);
+    }
 }
 
 /**
@@ -108,41 +109,41 @@ class Transformer extends Base {
  * @private
  */
 function disassemble(command) {
-	validateString(command);
-
-	const placeholder = new Map();
-	const regex = /((?<pattern>\\(?<char>.)){1})/gim;
-
-	// The separator for args must be escaped
-	// undefined string which should not occur normally and is also not a regex
-	const result = command.matchAll(regex);
-
-	for (const m of result) {
-		const g = m?.["groups"];
-		if (!isObject(g)) {
-			continue;
-		}
-
-		const p = g?.["pattern"];
-		const c = g?.["char"];
-
-		if (p && c) {
-			const r = `__${new ID().toString()}__`;
-			placeholder.set(r, c);
-			command = command.replace(p, r);
-		}
-	}
-	let parts = command.split(":");
-
-	parts = parts.map(function (value) {
-		let v = value.trim();
-		for (const k of placeholder) {
-			v = v.replace(k[0], k[1]);
-		}
-		return v;
-	});
-
-	return parts;
+    validateString(command);
+
+    const placeholder = new Map();
+    const regex = /((?<pattern>\\(?<char>.)){1})/gim;
+
+    // The separator for args must be escaped
+    // undefined string which should not occur normally and is also not a regex
+    const result = command.matchAll(regex);
+
+    for (const m of result) {
+        const g = m?.["groups"];
+        if (!isObject(g)) {
+            continue;
+        }
+
+        const p = g?.["pattern"];
+        const c = g?.["char"];
+
+        if (p && c) {
+            const r = `__${new ID().toString()}__`;
+            placeholder.set(r, c);
+            command = command.replace(p, r);
+        }
+    }
+    let parts = command.split(":");
+
+    parts = parts.map(function (value) {
+        let v = value.trim();
+        for (const k of placeholder) {
+            v = v.replace(k[0], k[1]);
+        }
+        return v;
+    });
+
+    return parts;
 }
 
 /**
@@ -153,12 +154,12 @@ function disassemble(command) {
  * @private
  */
 function convertToString(value) {
-	if (isObject(value) && value.hasOwnProperty("toString")) {
-		value = value.toString();
-	}
+    if (isObject(value) && value.hasOwnProperty("toString")) {
+        value = value.toString();
+    }
 
-	validateString(value);
-	return value;
+    validateString(value);
+    return value;
 }
 
 /**
@@ -172,667 +173,680 @@ function convertToString(value) {
  * @throws {Error} missing key parameter
  */
 function transform(value) {
-	const console = getGlobalObject("console");
-
-	const args = clone(this.args);
-	let key;
-	let defaultValue;
-
-	let translations;
-	let date;
-	let locale;
-	let timestamp;
-	let map;
-	let keyValue;
-
-	switch (this.command) {
-		case "static":
-			return this.args.join(":");
-
-		case "tolower":
-		case "strtolower":
-		case "tolowercase":
-			validateString(value);
-			return value.toLowerCase();
-
-		case "contains":
-			if (isString(value)) {
-				return value.includes(args[0]);
-			}
-
-			if (isArray(value)) {
-				return value.includes(args[0]);
-			}
-
-			if (isObject(value)) {
-				return value.hasOwnProperty(args[0]);
-			}
-
-			return false;
-
-		case "has-entries":
-		case "hasentries":
-			if (isObject(value)) {
-				return Object.keys(value).length > 0;
-			}
-
-			if (isArray(value)) {
-				return value.length > 0;
-			}
-
-			return false;
-
-		case "isundefined":
-		case "is-undefined":
-			return value === undefined;
-
-		case "isnull":
-		case "is-null":
-			return value === null;
-
-		case "isset":
-		case "is-set":
-			return value !== undefined && value !== null;
-
-		case "isnumber":
-		case "is-number":
-			return isPrimitive(value) && !isNaN(value);
-
-		case "isinteger":
-		case "is-integer":
-			return isPrimitive(value) && !isNaN(value) && value % 1 === 0;
-
-		case "isfloat":
-		case "is-float":
-			return isPrimitive(value) && !isNaN(value) && value % 1 !== 0;
-
-		case "isobject":
-		case "is-object":
-			return isObject(value);
-
-		case "isarray":
-		case "is-array":
-			return Array.isArray(value);
-
-		case "not":
-			validateBoolean(value);
-			return !value;
-
-		case "toupper":
-		case "strtoupper":
-		case "touppercase":
-			validateString(value);
-			return value.toUpperCase();
-
-		case "tostring":
-			return `${value}`;
-
-		case "tointeger":
-			const n = parseInt(value);
-			validateInteger(n);
-			return n;
-
-		case "to-json":
-		case "tojson":
-			return JSON.stringify(value);
-
-		case "from-json":
-		case "fromjson":
-			return JSON.parse(value);
-
-		case "trim":
-			validateString(value);
-			return value.trim();
-
-		case "rawurlencode":
-			validateString(value);
-			return encodeURIComponent(value)
-				.replace(/!/g, "%21")
-				.replace(/'/g, "%27")
-				.replace(/\(/g, "%28")
-				.replace(/\)/g, "%29")
-				.replace(/\*/g, "%2A");
-
-		case "call":
-			/**
-			 * callback-definition
-			 * function callback(value, ...args) {
-			 *   return value;
-			 * }
-			 */
-
-			let callback;
-			const callbackName = args.shift();
-			let context = getGlobal();
-
-			if (isObject(value) && value.hasOwnProperty(callbackName)) {
-				callback = value[callbackName];
-			} else if (this.callbacks.has(callbackName)) {
-				const s = this.callbacks.get(callbackName);
-				callback = s?.["callback"];
-				context = s?.["context"];
-			} else if (
-				typeof window === "object" &&
-				window.hasOwnProperty(callbackName)
-			) {
-				callback = window[callbackName];
-			}
-			validateFunction(callback);
-
-			args.unshift(value);
-			return callback.call(context, ...args);
-
-		case "plain":
-		case "plaintext":
-			validateString(value);
-			const doc = new DOMParser().parseFromString(value, "text/html");
-			return doc.body.textContent || "";
-
-		case "if":
-		case "?":
-			validatePrimitive(value);
-
-			let trueStatement = args.shift() || undefined;
-			let falseStatement = args.shift() || undefined;
-
-			trueStatement = convertSpecialStrings(trueStatement, value);
-			falseStatement = convertSpecialStrings(falseStatement, value);
-
-			const condition = evaluateCondition(value);
-			return condition ? trueStatement : falseStatement;
-
-		case "ucfirst":
-			validateString(value);
-
-			const firstchar = value.charAt(0).toUpperCase();
-			return firstchar + value.substr(1);
-		case "ucwords":
-			validateString(value);
-
-			return value.replace(
-				/^([a-z\u00E0-\u00FC])|\s+([a-z\u00E0-\u00FC])/g,
-				function (v) {
-					return v.toUpperCase();
-				},
-			);
-
-		case "count":
-		case "length":
-			if (
-				(isString(value) || isObject(value) || isArray(value)) &&
-				value.hasOwnProperty("length")
-			) {
-				return value.length;
-			}
-
-			throw new TypeError(`unsupported type ${typeof value}`);
-
-		case "to-base64":
-		case "btoa":
-		case "base64":
-			return btoa(convertToString(value));
-
-		case "atob":
-		case "from-base64":
-			return atob(convertToString(value));
-
-		case "empty":
-			return "";
-
-		case "undefined":
-			return undefined;
-
-		case "debug":
-			if (isObject(console)) {
-				console.log(value);
-			}
-
-			return value;
-
-		case "prefix":
-			validateString(value);
-			const prefix = args?.[0];
-			return prefix + value;
-
-		case "suffix":
-			validateString(value);
-			const suffix = args?.[0];
-			return value + suffix;
-
-		case "uniqid":
-			return new ID().toString();
-
-		case "first-key":
-		case "last-key":
-		case "nth-last-key":
-		case "nth-key":
-			if (!isObject(value)) {
-				throw new Error("type not supported");
-			}
-
-			const keys = Object.keys(value).sort();
-
-			if (this.command === "first-key") {
-				key = 0;
-			} else if (this.command === "last-key") {
-				key = keys.length - 1;
-			} else {
-				key = validateInteger(parseInt(args.shift()));
-
-				if (this.command === "nth-last-key") {
-					key = keys.length - key - 1;
-				}
-			}
-
-			defaultValue = args.shift() || "";
-
-			const useKey = keys?.[key];
-
-			if (value?.[useKey]) {
-				return value?.[useKey];
-			}
-
-			return defaultValue;
-
-		case "key":
-		case "property":
-		case "index":
-			key = args.shift() || undefined;
-
-			if (key === undefined) {
-				throw new Error("missing key parameter");
-			}
-
-			defaultValue = args.shift() || undefined;
-
-			if (value instanceof Map) {
-				if (!value.has(key)) {
-					return defaultValue;
-				}
-				return value.get(key);
-			}
-
-			if (isObject(value) || isArray(value)) {
-				if (value?.[key]) {
-					return value?.[key];
-				}
-
-				return defaultValue;
-			}
-
-			throw new Error("type not supported");
-
-		case "path-exists":
-			key = args.shift();
-			if (key === undefined) {
-				throw new Error("missing key parameter");
-			}
-
-			return new Pathfinder(value).exists(key);
-
-		case "concat":
-			const pf2 = new Pathfinder(value);
-			let concat = "";
-			while (args.length > 0) {
-				key = args.shift();
-				if (key === undefined) {
-					throw new Error("missing key parameter");
-				}
-
-				// add empty strings
-				if (isString(key) && key.trim() === "") {
-					concat += key;
-					continue;
-				}
-
-				if (!pf2.exists(key)) {
-					concat += key;
-					continue;
-				}
-				const v = pf2.getVia(key);
-				if (!isPrimitive(v)) {
-					throw new Error("value is not primitive");
-				}
-
-				concat += v;
-			}
-
-			return concat;
-		case "path":
-			key = args.shift();
-			if (key === undefined) {
-				throw new Error("missing key parameter");
-			}
-
-			const pf = new Pathfinder(value);
-
-			if (!pf.exists(key)) {
-				return undefined;
-			}
-
-			return pf.getVia(key);
-
-		case "substring":
-			validateString(value);
-
-			const start = parseInt(args[0]) || 0;
-			const end = (parseInt(args[1]) || 0) + start;
-
-			return value.substring(start, end);
-
-		case "nop":
-			return value;
-
-		case "??":
-		case "default":
-			if (value !== undefined && value !== null) {
-				return value;
-			}
-
-			defaultValue = args.shift();
-			let defaultType = args.shift();
-			if (defaultType === undefined) {
-				defaultType = "string";
-			}
-
-			switch (defaultType) {
-				case "int":
-				case "integer":
-					return parseInt(defaultValue);
-				case "float":
-					return parseFloat(defaultValue);
-				case "undefined":
-					return undefined;
-				case "bool":
-				case "boolean":
-					defaultValue = defaultValue.toLowerCase();
-					return (
-						(defaultValue !== "undefined" &&
-							defaultValue !== "" &&
-							defaultValue !== "off" &&
-							defaultValue !== "false" &&
-							defaultValue !== "false") ||
-						defaultValue === "on" ||
-						defaultValue === "true" ||
-						defaultValue === "true"
-					);
-				case "string":
-					return `${defaultValue}`;
-				case "object":
-					return JSON.parse(atob(defaultValue));
-			}
-
-			throw new Error("type not supported");
-
-		case "map":
-			map = new Map();
-			while (args.length > 0) {
-				keyValue = args.shift();
-				if (keyValue === undefined) {
-					throw new Error("missing key parameter");
-				}
-
-				keyValue = keyValue.split("=");
-				map.set(keyValue[0], keyValue[1]);
-			}
-
-			return map.get(value);
-
-		case "equals":
-			if (args.length === 0) {
-				throw new Error("missing value parameter");
-			}
-
-			validatePrimitive(value);
-
-			const equalsValue = args.shift();
-
-			/**
-			 * The history of “typeof null”
-			 * https://2ality.com/2013/10/typeof-null.html
-			 * In JavaScript, typeof null is 'object', which incorrectly suggests
-			 * that null is an object.
-			 */
-			if (value === null) {
-				return equalsValue === "null";
-			}
-
-			const typeOfValue = typeof value;
-
-			switch (typeOfValue) {
-				case "string":
-					return value === equalsValue;
-				case "number":
-					return value === parseFloat(equalsValue);
-				case "boolean":
-					return value === (equalsValue === "true" || equalsValue === "on");
-				case "undefined":
-					return equalsValue === "undefined";
-				default:
-					throw new Error("type not supported");
-			}
-
-		case "money":
-		case "currency":
-			try {
-				locale = getLocaleOfDocument();
-			} catch (e) {
-				throw new Error(`unsupported locale or missing format (${e.message})`);
-			}
-
-			// Verwenden von RegExp, um Währung und Betrag zu extrahieren
-			const match = value.match(/^([A-Z]{3})[\s-]*(\d+(\.\d+)?)$/);
-			if (!match) {
-				throw new Error("invalid currency format");
-			}
-
-			const currency = match[1];
-			const amount = match[2];
-
-			const maximumFractionDigits = args?.[0] || 2;
-			const roundingIncrement = args?.[1] || 5;
-
-			const nf = new Intl.NumberFormat(locale.toString(), {
-				style: "currency",
-				currency: currency,
-				maximumFractionDigits: maximumFractionDigits,
-				roundingIncrement: roundingIncrement,
-			});
-
-			return nf.format(amount);
-
-		case "timestamp":
-			date = new Date(value);
-			timestamp = date.getTime();
-			if (isNaN(timestamp)) {
-				throw new Error("invalid date");
-			}
-			return timestamp;
-
-		case "time":
-			date = new Date(value);
-			if (isNaN(date.getTime())) {
-				throw new Error("invalid date");
-			}
-
-			try {
-				locale = getLocaleOfDocument();
-				return date.toLocaleTimeString(locale.toString(), {
-					hour12: false,
-				});
-			} catch (e) {
-				throw new Error(`unsupported locale or missing format (${e.message})`);
-			}
-
-		case "datetimeformat":
-			date = new Date(value);
-			if (isNaN(date.getTime())) {
-				throw new Error("invalid date");
-			}
-
-			const options = {
-				dateStyle: "medium",
-				timeStyle: "medium",
-				hour12: false,
-			};
-
-			if (args.length > 0) {
-				options.dateStyle = args.shift();
-			}
-
-			if (args.length > 0) {
-				options.timeStyle = args.shift();
-			}
-
-			try {
-				locale = getLocaleOfDocument().toString();
-				return new Intl.DateTimeFormat(locale, options).format(date);
-			} catch (e) {
-				throw new Error(`unsupported locale or missing format (${e.message})`);
-			}
-
-		case "datetime":
-			date = new Date(value);
-			if (isNaN(date.getTime())) {
-				throw new Error("invalid date");
-			}
-
-			try {
-				locale = getLocaleOfDocument();
-				return date.toLocaleString(locale.toString(), {
-					hour12: false,
-				});
-			} catch (e) {
-				throw new Error(`unsupported locale or missing format (${e.message})`);
-			}
-
-		case "date":
-			date = new Date(value);
-			if (isNaN(date.getTime())) {
-				throw new Error("invalid date");
-			}
-
-			try {
-				locale = getLocaleOfDocument();
-				return date.toLocaleDateString(locale.toString(), {
-					year: "numeric",
-					month: "2-digit",
-					day: "2-digit",
-				});
-			} catch (e) {
-				throw new Error(`unsupported locale or missing format (${e.message})`);
-			}
-
-		case "year":
-			date = new Date(value);
-			if (isNaN(date.getTime())) {
-				throw new Error("invalid date");
-			}
-
-			return date.getFullYear();
-
-		case "month":
-			date = new Date(value);
-			if (isNaN(date.getTime())) {
-				throw new Error("invalid date");
-			}
-
-			return date.getMonth() + 1;
-
-		case "day":
-			date = new Date(value);
-			if (isNaN(date.getTime())) {
-				throw new Error("invalid date");
-			}
-
-			return date.getDate();
-
-		case "weekday":
-			date = new Date(value);
-			if (isNaN(date.getTime())) {
-				throw new Error("invalid date");
-			}
-
-			return date.getDay();
-
-		case "hour":
-		case "hours":
-			date = new Date(value);
-			if (isNaN(date.getTime())) {
-				throw new Error("invalid date");
-			}
-
-			return date.getHours();
-
-		case "minute":
-		case "minutes":
-			date = new Date(value);
-			if (isNaN(date.getTime())) {
-				throw new Error("invalid date");
-			}
-
-			return date.getMinutes();
-
-		case "second":
-		case "seconds":
-			date = new Date(value);
-			if (isNaN(date.getTime())) {
-				throw new Error("invalid date");
-			}
-
-			return date.getSeconds();
-
-		case "i18n":
-		case "translation":
-			translations = getDocumentTranslations();
-			if (!(translations instanceof Translations)) {
-				throw new Error("missing translations");
-			}
-
-			key = args.shift() || undefined;
-			if (key === undefined) {
-				key = value;
-			}
-
-			defaultValue = args.shift() || undefined;
-
-			defaultValue = convertSpecialStrings(defaultValue, value);
-
-			return translations.getText(key, defaultValue);
-
-		case "set-toggle":
-		case "set-set":
-		case "set-remove":
-			const modifier = args.shift();
-			let delimiter = args.shift();
-			if (delimiter === undefined) {
-				delimiter = " ";
-			}
-
-			const set = new Set(value.split(delimiter));
-			const toggle = new Set(modifier.split(delimiter));
-			if (this.command === "set-toggle") {
-				for (const t of toggle) {
-					if (set.has(t)) {
-						set.delete(t);
-					} else {
-						set.add(t);
-					}
-				}
-			} else if (this.command === "set-set") {
-				for (const t of toggle) {
-					set.add(t);
-				}
-			} else if (this.command === "set-remove") {
-				for (const t of toggle) {
-					set.delete(t);
-				}
-			}
-			return Array.from(set).join(delimiter);
-
-		default:
-			throw new Error(`unknown command ${this.command}`);
-	}
+    const console = getGlobalObject("console");
+
+    const args = clone(this.args);
+    let key;
+    let defaultValue;
+
+    let translations;
+    let date;
+    let locale;
+    let timestamp;
+    let map;
+    let keyValue;
+
+    switch (this.command) {
+        case "static":
+            return this.args.join(":");
+
+        case "tolower":
+        case "strtolower":
+        case "tolowercase":
+            validateString(value);
+            return value.toLowerCase();
+
+        case "contains":
+            if (isString(value)) {
+                return value.includes(args[0]);
+            }
+
+            if (isArray(value)) {
+                return value.includes(args[0]);
+            }
+
+            if (isObject(value)) {
+                return value.hasOwnProperty(args[0]);
+            }
+
+            return false;
+
+        case "has-entries":
+        case "hasentries":
+            if (isObject(value)) {
+                return Object.keys(value).length > 0;
+            }
+
+            if (isArray(value)) {
+                return value.length > 0;
+            }
+
+            return false;
+
+        case "isundefined":
+        case "is-undefined":
+            return value === undefined;
+
+        case "isnull":
+        case "is-null":
+            return value === null;
+
+        case "isset":
+        case "is-set":
+            return value !== undefined && value !== null;
+
+        case "isnumber":
+        case "is-number":
+            return isPrimitive(value) && !isNaN(value);
+
+        case "isinteger":
+        case "is-integer":
+            return isPrimitive(value) && !isNaN(value) && value % 1 === 0;
+
+        case "isfloat":
+        case "is-float":
+            return isPrimitive(value) && !isNaN(value) && value % 1 !== 0;
+
+        case "isobject":
+        case "is-object":
+            return isObject(value);
+
+        case "isarray":
+        case "is-array":
+            return Array.isArray(value);
+
+        case "not":
+            validateBoolean(value);
+            return !value;
+
+        case "toupper":
+        case "strtoupper":
+        case "touppercase":
+            validateString(value);
+            return value.toUpperCase();
+
+        case "tostring":
+            return `${value}`;
+
+        case "tointeger":
+            const n = parseInt(value);
+            validateInteger(n);
+            return n;
+
+        case "to-json":
+        case "tojson":
+            return JSON.stringify(value);
+
+        case "from-json":
+        case "fromjson":
+            return JSON.parse(value);
+
+        case "trim":
+            validateString(value);
+            return value.trim();
+
+        case "rawurlencode":
+            validateString(value);
+            return encodeURIComponent(value)
+                .replace(/!/g, "%21")
+                .replace(/'/g, "%27")
+                .replace(/\(/g, "%28")
+                .replace(/\)/g, "%29")
+                .replace(/\*/g, "%2A");
+
+        case "call":
+            /**
+             * callback-definition
+             * function callback(value, ...args) {
+             *   return value;
+             * }
+             */
+
+            let callback;
+            const callbackName = args.shift();
+            let context = getGlobal();
+
+            if (isObject(value) && value.hasOwnProperty(callbackName)) {
+                callback = value[callbackName];
+            } else if (this.callbacks.has(callbackName)) {
+                const s = this.callbacks.get(callbackName);
+                callback = s?.["callback"];
+                context = s?.["context"];
+            } else if (
+                typeof window === "object" &&
+                window.hasOwnProperty(callbackName)
+            ) {
+                callback = window[callbackName];
+            }
+            validateFunction(callback);
+
+            args.unshift(value);
+            return callback.call(context, ...args);
+
+        case "plain":
+        case "plaintext":
+            validateString(value);
+            const doc = new DOMParser().parseFromString(value, "text/html");
+            return doc.body.textContent || "";
+
+        case "if":
+        case "?":
+            validatePrimitive(value);
+
+            let trueStatement = args.shift() || undefined;
+            let falseStatement = args.shift() || undefined;
+
+            trueStatement = convertSpecialStrings(trueStatement, value);
+            falseStatement = convertSpecialStrings(falseStatement, value);
+
+            const condition = evaluateCondition(value);
+            return condition ? trueStatement : falseStatement;
+
+        case "ucfirst":
+            validateString(value);
+
+            const firstchar = value.charAt(0).toUpperCase();
+            return firstchar + value.substr(1);
+        case "ucwords":
+            validateString(value);
+
+            return value.replace(
+                /^([a-z\u00E0-\u00FC])|\s+([a-z\u00E0-\u00FC])/g,
+                function (v) {
+                    return v.toUpperCase();
+                },
+            );
+
+        case "count":
+        case "length":
+            if (
+                (isString(value) || isObject(value) || isArray(value)) &&
+                value.hasOwnProperty("length")
+            ) {
+                return value.length;
+            }
+
+            throw new TypeError(`unsupported type ${typeof value}`);
+
+        case "to-base64":
+        case "btoa":
+        case "base64":
+            return btoa(convertToString(value));
+
+        case "atob":
+        case "from-base64":
+            return atob(convertToString(value));
+
+        case "empty":
+            return "";
+
+        case "undefined":
+            return undefined;
+
+        case "debug":
+            if (isObject(console)) {
+                console.log(value);
+            }
+
+            return value;
+
+        case "prefix":
+            validateString(value);
+            const prefix = args?.[0];
+            return prefix + value;
+
+        case "suffix":
+            validateString(value);
+            const suffix = args?.[0];
+            return value + suffix;
+
+        case "uniqid":
+            return new ID().toString();
+
+        case "first-key":
+        case "last-key":
+        case "nth-last-key":
+        case "nth-key":
+            if (!isObject(value)) {
+                throw new Error("type not supported");
+            }
+
+            const keys = Object.keys(value).sort();
+
+            if (this.command === "first-key") {
+                key = 0;
+            } else if (this.command === "last-key") {
+                key = keys.length - 1;
+            } else {
+                key = validateInteger(parseInt(args.shift()));
+
+                if (this.command === "nth-last-key") {
+                    key = keys.length - key - 1;
+                }
+            }
+
+            defaultValue = args.shift() || "";
+
+            const useKey = keys?.[key];
+
+            if (value?.[useKey]) {
+                return value?.[useKey];
+            }
+
+            return defaultValue;
+
+        case "key":
+        case "property":
+        case "index":
+            key = args.shift() || undefined;
+
+            if (key === undefined) {
+                throw new Error("missing key parameter");
+            }
+
+            defaultValue = args.shift() || undefined;
+
+            if (value instanceof Map) {
+                if (!value.has(key)) {
+                    return defaultValue;
+                }
+                return value.get(key);
+            }
+
+            if (isObject(value) || isArray(value)) {
+                if (value?.[key]) {
+                    return value?.[key];
+                }
+
+                return defaultValue;
+            }
+
+            throw new Error("type not supported");
+
+        case "path-exists":
+            key = args.shift();
+            if (key === undefined) {
+                throw new Error("missing key parameter");
+            }
+
+            return new Pathfinder(value).exists(key);
+
+        case "concat":
+            const pf2 = new Pathfinder(value);
+            let concat = "";
+            while (args.length > 0) {
+                key = args.shift();
+                if (key === undefined) {
+                    throw new Error("missing key parameter");
+                }
+
+                // add empty strings
+                if (isString(key) && key.trim() === "") {
+                    concat += key;
+                    continue;
+                }
+
+                if (!pf2.exists(key)) {
+                    concat += key;
+                    continue;
+                }
+                const v = pf2.getVia(key);
+                if (!isPrimitive(v)) {
+                    throw new Error("value is not primitive");
+                }
+
+                concat += v;
+            }
+
+            return concat;
+        case "path":
+            key = args.shift();
+            if (key === undefined) {
+                throw new Error("missing key parameter");
+            }
+
+            const pf = new Pathfinder(value);
+
+            if (!pf.exists(key)) {
+                return undefined;
+            }
+
+            return pf.getVia(key);
+
+        case "substring":
+            validateString(value);
+
+            const start = parseInt(args[0]) || 0;
+            const end = (parseInt(args[1]) || 0) + start;
+
+            return value.substring(start, end);
+
+        case "nop":
+            return value;
+
+        case "??":
+        case "default":
+            if (value !== undefined && value !== null) {
+                return value;
+            }
+
+            defaultValue = args.shift();
+            let defaultType = args.shift();
+            if (defaultType === undefined) {
+                defaultType = "string";
+            }
+
+            switch (defaultType) {
+                case "int":
+                case "integer":
+                    return parseInt(defaultValue);
+                case "float":
+                    return parseFloat(defaultValue);
+                case "undefined":
+                    return undefined;
+                case "bool":
+                case "boolean":
+                    defaultValue = defaultValue.toLowerCase();
+                    return (
+                        (defaultValue !== "undefined" &&
+                            defaultValue !== "" &&
+                            defaultValue !== "off" &&
+                            defaultValue !== "false" &&
+                            defaultValue !== "false") ||
+                        defaultValue === "on" ||
+                        defaultValue === "true" ||
+                        defaultValue === "true"
+                    );
+                case "string":
+                    return `${defaultValue}`;
+                case "object":
+                    return JSON.parse(atob(defaultValue));
+            }
+
+            throw new Error("type not supported");
+
+        case "map":
+            map = new Map();
+            while (args.length > 0) {
+                keyValue = args.shift();
+                if (keyValue === undefined) {
+                    throw new Error("missing key parameter");
+                }
+
+                keyValue = keyValue.split("=");
+                map.set(keyValue[0], keyValue[1]);
+            }
+
+            return map.get(value);
+
+        case "equals":
+            if (args.length === 0) {
+                throw new Error("missing value parameter");
+            }
+
+            validatePrimitive(value);
+
+            const equalsValue = args.shift();
+
+            /**
+             * The history of “typeof null”
+             * https://2ality.com/2013/10/typeof-null.html
+             * In JavaScript, typeof null is 'object', which incorrectly suggests
+             * that null is an object.
+             */
+            if (value === null) {
+                return equalsValue === "null";
+            }
+
+            const typeOfValue = typeof value;
+
+            switch (typeOfValue) {
+                case "string":
+                    return value === equalsValue;
+                case "number":
+                    return value === parseFloat(equalsValue);
+                case "boolean":
+                    return value === (equalsValue === "true" || equalsValue === "on");
+                case "undefined":
+                    return equalsValue === "undefined";
+                default:
+                    throw new Error("type not supported");
+            }
+
+        case "money":
+        case "currency":
+            try {
+                locale = getLocaleOfDocument();
+            } catch (e) {
+                throw new Error(`unsupported locale or missing format (${e.message})`);
+            }
+
+            // Verwenden von RegExp, um Währung und Betrag zu extrahieren
+            const match = value.match(/^([A-Z]{3})[\s-]*(\d+(\.\d+)?)$/);
+            if (!match) {
+                throw new Error("invalid currency format");
+            }
+
+            const currency = match[1];
+            const amount = match[2];
+
+            const maximumFractionDigits = args?.[0] || 2;
+            const roundingIncrement = args?.[1] || 5;
+
+            const nf = new Intl.NumberFormat(locale.toString(), {
+                style: "currency",
+                currency: currency,
+                maximumFractionDigits: maximumFractionDigits,
+                roundingIncrement: roundingIncrement,
+            });
+
+            return nf.format(amount);
+
+        case "timestamp":
+            date = new Date(value);
+            timestamp = date.getTime();
+            if (isNaN(timestamp)) {
+                throw new Error("invalid date");
+            }
+            return timestamp;
+
+        case "time":
+            date = new Date(value);
+            if (isNaN(date.getTime())) {
+                throw new Error("invalid date");
+            }
+
+            try {
+                locale = getLocaleOfDocument();
+                return date.toLocaleTimeString(locale.toString(), {
+                    hour12: false,
+                });
+            } catch (e) {
+                throw new Error(`unsupported locale or missing format (${e.message})`);
+            }
+
+        case "datetimeformat":
+            date = new Date(value);
+            if (isNaN(date.getTime())) {
+                throw new Error("invalid date");
+            }
+
+            const options = {
+                dateStyle: "medium",
+                timeStyle: "medium",
+                hour12: false,
+            };
+
+            if (args.length > 0) {
+                options.dateStyle = args.shift();
+            }
+
+            if (args.length > 0) {
+                options.timeStyle = args.shift();
+            }
+
+            try {
+                locale = getLocaleOfDocument().toString();
+                return new Intl.DateTimeFormat(locale, options).format(date);
+            } catch (e) {
+                throw new Error(`unsupported locale or missing format (${e.message})`);
+            }
+
+        case "datetime":
+            date = new Date(value);
+            if (isNaN(date.getTime())) {
+                throw new Error("invalid date");
+            }
+
+            try {
+                locale = getLocaleOfDocument();
+                return date.toLocaleString(locale.toString(), {
+                    hour12: false,
+                });
+            } catch (e) {
+                throw new Error(`unsupported locale or missing format (${e.message})`);
+            }
+
+        case "date":
+            date = new Date(value);
+            if (isNaN(date.getTime())) {
+                throw new Error("invalid date");
+            }
+
+            try {
+                locale = getLocaleOfDocument();
+                return date.toLocaleDateString(locale.toString(), {
+                    year: "numeric",
+                    month: "2-digit",
+                    day: "2-digit",
+                });
+            } catch (e) {
+                throw new Error(`unsupported locale or missing format (${e.message})`);
+            }
+
+        case "time-ago":
+            date = new Date(value);
+            if (isNaN(date.getTime())) {
+                throw new Error("invalid date");
+            }
+
+            try {
+                locale = getLocaleOfDocument();
+                return formatTimeAgo(date, locale.toString());
+            } catch (e) {
+                throw new Error(`unsupported locale or missing format (${e.message})`);
+            }
+
+        case "year":
+            date = new Date(value);
+            if (isNaN(date.getTime())) {
+                throw new Error("invalid date");
+            }
+
+            return date.getFullYear();
+
+        case "month":
+            date = new Date(value);
+            if (isNaN(date.getTime())) {
+                throw new Error("invalid date");
+            }
+
+            return date.getMonth() + 1;
+
+        case "day":
+            date = new Date(value);
+            if (isNaN(date.getTime())) {
+                throw new Error("invalid date");
+            }
+
+            return date.getDate();
+
+        case "weekday":
+            date = new Date(value);
+            if (isNaN(date.getTime())) {
+                throw new Error("invalid date");
+            }
+
+            return date.getDay();
+
+        case "hour":
+        case "hours":
+            date = new Date(value);
+            if (isNaN(date.getTime())) {
+                throw new Error("invalid date");
+            }
+
+            return date.getHours();
+
+        case "minute":
+        case "minutes":
+            date = new Date(value);
+            if (isNaN(date.getTime())) {
+                throw new Error("invalid date");
+            }
+
+            return date.getMinutes();
+
+        case "second":
+        case "seconds":
+            date = new Date(value);
+            if (isNaN(date.getTime())) {
+                throw new Error("invalid date");
+            }
+
+            return date.getSeconds();
+
+        case "i18n":
+        case "translation":
+            translations = getDocumentTranslations();
+            if (!(translations instanceof Translations)) {
+                throw new Error("missing translations");
+            }
+
+            key = args.shift() || undefined;
+            if (key === undefined) {
+                key = value;
+            }
+
+            defaultValue = args.shift() || undefined;
+
+            defaultValue = convertSpecialStrings(defaultValue, value);
+
+            return translations.getText(key, defaultValue);
+
+        case "set-toggle":
+        case "set-set":
+        case "set-remove":
+            const modifier = args.shift();
+            let delimiter = args.shift();
+            if (delimiter === undefined) {
+                delimiter = " ";
+            }
+
+            const set = new Set(value.split(delimiter));
+            const toggle = new Set(modifier.split(delimiter));
+            if (this.command === "set-toggle") {
+                for (const t of toggle) {
+                    if (set.has(t)) {
+                        set.delete(t);
+                    } else {
+                        set.add(t);
+                    }
+                }
+            } else if (this.command === "set-set") {
+                for (const t of toggle) {
+                    set.add(t);
+                }
+            } else if (this.command === "set-remove") {
+                for (const t of toggle) {
+                    set.delete(t);
+                }
+            }
+            return Array.from(set).join(delimiter);
+
+        default:
+            throw new Error(`unknown command ${this.command}`);
+    }
 }
 
 /**
@@ -843,18 +857,18 @@ function transform(value) {
  * @return {undefined|*|null|string}
  */
 function convertSpecialStrings(input, value) {
-	switch (input) {
-		case "value":
-			return value;
-		case "\\value":
-			return "value";
-		case "\\undefined":
-			return undefined;
-		case "\\null":
-			return null;
-		default:
-			return input;
-	}
+    switch (input) {
+        case "value":
+            return value;
+        case "\\value":
+            return "value";
+        case "\\undefined":
+            return undefined;
+        case "\\null":
+            return null;
+        default:
+            return input;
+    }
 }
 
 /**
@@ -863,17 +877,20 @@ function convertSpecialStrings(input, value) {
  * @return {boolean}
  */
 function evaluateCondition(value) {
-	const lowerValue = typeof value === "string" ? value.toLowerCase() : value;
-
-	return (
-		(value !== undefined &&
-			value !== null &&
-			value !== "" &&
-			lowerValue !== "off" &&
-			lowerValue !== "false" &&
-			value !== false) ||
-		lowerValue === "on" ||
-		lowerValue === "true" ||
-		value === true
-	);
+    const lowerValue = typeof value === "string" ? value.toLowerCase() : value;
+
+    return (
+        (value !== undefined &&
+            value !== null &&
+            value !== "" &&
+            lowerValue !== "off" &&
+            lowerValue !== "false" &&
+            value !== false) ||
+        lowerValue === "on" ||
+        lowerValue === "true" ||
+        value === true
+    );
 }
+
+
+
diff --git a/source/i18n/time-ago.mjs b/source/i18n/time-ago.mjs
new file mode 100644
index 000000000..0fb58a240
--- /dev/null
+++ b/source/i18n/time-ago.mjs
@@ -0,0 +1,671 @@
+/**
+ * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
+ * Node module: @schukai/monster
+ *
+ * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
+ * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
+ *
+ * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
+ * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
+ * For more information about purchasing a commercial license, please contact schukai GmbH.
+ *
+ * SPDX-License-Identifier: AGPL-3.0
+ */
+
+export {formatTimeAgo};
+
+/**
+ * Format a date as a relative time string with multiple units.
+ *
+ * @param date
+ * @param locale
+ * @returns {string}
+ */
+function formatTimeAgo(date, locale) {
+    const now = new Date(Math.floor(new Date().getTime() / 1000) * 1000);
+    const roundedDate = new Date(Math.floor(date.getTime() / 1000) * 1000);
+    const diffInSeconds = Math.floor((roundedDate - now) / 1000);
+    const absDiff = Math.abs(diffInSeconds);
+
+    const translation = translations[normalizeLocale(locale)];
+
+    if (absDiff === 0) {
+        return translation.template.justNow();
+    }
+
+    if (absDiff > 31536000) {
+        return translation.template.on(new Intl.DateTimeFormat(locale, {
+            dateStyle: 'full',
+            timeStyle: 'short'
+        }).format(date));
+    }
+
+    const units = [
+        {name: 'year', length: 31536000},  // Hinzugefügt für Konsistenz, auch wenn nicht genutzt in dieser spezifischen Logik
+        {name: 'day', length: 86400},
+        {name: 'hour', length: 3600},
+        {name: 'minute', length: 60},
+        {name: 'second', length: 1},
+    ];
+
+    let remainder = absDiff;
+    const parts = [];
+
+    for (const unit of units) {
+        if (remainder >= unit.length) {
+            const count = Math.floor(remainder / unit.length);
+            remainder %= unit.length;
+            const formatted = formatUnitCount(count, unit.name, locale);
+            parts.push(formatted);
+        }
+    }
+
+    if (parts.length === 0) {
+        return translation.template.justNow();
+    }
+
+    const joined = new Intl.ListFormat(locale, {style: 'long', type: 'conjunction'}).format(parts);
+    if (diffInSeconds > 0) {
+        return translation.template.future(joined);
+    } else {
+        return translation.template.past(joined);
+    }
+}
+
+function formatUnitCount(count, unit, locale) {
+    const lang = normalizeLocale(locale);
+    const fallback = translations[lang] || translations['en'];
+    const dictForUnit = fallback.units[unit];
+    const pr = new Intl.PluralRules(lang);
+    const category = pr.select(count);
+    const phrase = dictForUnit[category] || dictForUnit.other;
+    return `${count} ${phrase}`;
+}
+
+function normalizeLocale(locale) {
+    return locale.split('-')[0].toLowerCase();
+}
+
+
+
+/**
+ * @private
+ */
+const translations = {
+    en: {
+        template: {
+            on: (value) => `on ${value}`,
+            past: (value) => `${value} ago`,
+            future: (value) => `in ${value}`,
+            justNow: () => `just now`,
+        },
+        units: {
+            day: {zero: 'days', one: 'day', two: 'days', few: 'days', many: 'days', other: 'days'},
+            hour: {zero: 'hours', one: 'hour', two: 'hours', few: 'hours', many: 'hours', other: 'hours'},
+            minute: {zero: 'minutes', one: 'minute', two: 'minutes', few: 'minutes', many: 'minutes', other: 'minutes'},
+            second: {zero: 'seconds', one: 'second', two: 'seconds', few: 'seconds', many: 'seconds', other: 'seconds'},
+        }
+    },
+
+    zh: {
+        template: {
+            on: (value) => `在${value}`,
+            past: (value) => `${value}前`,
+            future: (value) => `${value}后`,
+            justNow: () => `刚刚`,
+        },
+        units: {
+            day: {zero: '天', one: '天', two: '天', few: '天', many: '天', other: '天'},
+            hour: {zero: '小时', one: '小时', two: '小时', few: '小时', many: '小时', other: '小时'},
+            minute: {zero: '分钟', one: '分钟', two: '分钟', few: '分钟', many: '分钟', other: '分钟'},
+            second: {zero: '秒', one: '秒', two: '秒', few: '秒', many: '秒', other: '秒'},
+        }
+    },
+
+    es: {
+        template: {
+            on: (value) => `el ${value}`,
+            past: (value) => `hace ${value}`,
+            future: (value) => `en ${value}`,
+            justNow: () => `justo ahora`,
+        },
+        units: {
+            day: {zero: 'días', one: 'día', two: 'días', few: 'días', many: 'días', other: 'días'},
+            hour: {zero: 'horas', one: 'hora', two: 'horas', few: 'horas', many: 'horas', other: 'horas'},
+            minute: {zero: 'minutos', one: 'minuto', two: 'minutos', few: 'minutos', many: 'minutos', other: 'minutos'},
+            second: {
+                zero: 'segundos',
+                one: 'segundo',
+                two: 'segundos',
+                few: 'segundos',
+                many: 'segundos',
+                other: 'segundos'
+            },
+        }
+    },
+
+    de: {
+        template: {
+            on: (value) => `am ${value}`,
+            past: (value) => `vor ${value}`,
+            future: (value) => `in ${value}`,
+            justNow: () => `gerade eben`,
+        },
+        units: {
+            day: {zero: 'Tagen', one: 'Tag', two: 'Tagen', few: 'Tagen', many: 'Tagen', other: 'Tagen'},
+            hour: {zero: 'Stunden', one: 'Stunde', two: 'Stunden', few: 'Stunden', many: 'Stunden', other: 'Stunden'},
+            minute: {zero: 'Minuten', one: 'Minute', two: 'Minuten', few: 'Minuten', many: 'Minuten', other: 'Minuten'},
+            second: {
+                zero: 'Sekunden',
+                one: 'Sekunde',
+                two: 'Sekunden',
+                few: 'Sekunden',
+                many: 'Sekunden',
+                other: 'Sekunden'
+            },
+        }
+    },
+
+    fr: {
+        template: {
+            on: (value) => `le ${value}`,
+            past: (value) => `il y a ${value}`,
+            future: (value) => `dans ${value}`,
+            justNow: () => `à l'instant`,
+        },
+        units: {
+            day: {zero: 'jours', one: 'jour', two: 'jours', few: 'jours', many: 'jours', other: 'jours'},
+            hour: {zero: 'heures', one: 'heure', two: 'heures', few: 'heures', many: 'heures', other: 'heures'},
+            minute: {zero: 'minutes', one: 'minute', two: 'minutes', few: 'minutes', many: 'minutes', other: 'minutes'},
+            second: {
+                zero: 'secondes',
+                one: 'seconde',
+                two: 'secondes',
+                few: 'secondes',
+                many: 'secondes',
+                other: 'secondes'
+            },
+        }
+    },
+
+    ru: {
+        template: {
+            on: (value) => `на ${value}`,
+            past: (value) => `${value} назад`,
+            future: (value) => `через ${value}`,
+            justNow: () => `только что`,
+        },
+        units: {
+            day: {zero: 'дней', one: 'день', two: 'дня', few: 'дня', many: 'дней', other: 'дней'},
+            hour: {zero: 'часов', one: 'час', two: 'часа', few: 'часа', many: 'часов', other: 'часов'},
+            minute: {zero: 'минут', one: 'минута', two: 'минуты', few: 'минуты', many: 'минут', other: 'минут'},
+            second: {zero: 'секунд', one: 'секунда', two: 'секунды', few: 'секунды', many: 'секунд', other: 'секунд'},
+        }
+    },
+
+    ar: {
+        template: {
+            on: (value) => `في ${value}`,
+            past: (value) => `منذ ${value}`,
+            future: (value) => `بعد ${value}`,
+            justNow: () => `الآن`,
+        },
+        units: {
+            day: {zero: 'أيام', one: 'يوم', two: 'يومين', few: 'أيام', many: 'أيام', other: 'أيام'},
+            hour: {zero: 'ساعات', one: 'ساعة', two: 'ساعتين', few: 'ساعات', many: 'ساعات', other: 'ساعات'},
+            minute: {zero: 'دقائق', one: 'دقيقة', two: 'دقيقتين', few: 'دقائق', many: 'دقائق', other: 'دقائق'},
+            second: {zero: 'ثواني', one: 'ثانية', two: 'ثانيتين', few: 'ثواني', many: 'ثواني', other: 'ثواني'},
+        }
+    },
+
+    hi: {
+        template: {
+            on: (value) => `${value} को`,
+            past: (value) => `${value} पहले`,
+            future: (value) => `${value} में`,
+            justNow: () => `अभी`,
+        },
+        units: {
+            day: {zero: 'दिन', one: 'दिन', two: 'दिन', few: 'दिन', many: 'दिन', other: 'दिन'},
+            hour: {zero: 'घंटे', one: 'घंटा', two: 'घंटे', few: 'घंटे', many: 'घंटे', other: 'घंटे'},
+            minute: {zero: 'मिनट', one: 'मिनट', two: 'मिनट', few: 'मिनट', many: 'मिनट', other: 'मिनट'},
+            second: {zero: 'सेकंड', one: 'सेकंड', two: 'सेकंड', few: 'सेकंड', many: 'सेकंड', other: 'सेकंड'},
+        }
+    },
+
+    pt: {
+        template: {
+            on: (value) => `em ${value}`,
+            past: (value) => `há ${value}`,
+            future: (value) => `em ${value}`,
+            justNow: () => `agora mesmo`,
+        },
+        units: {
+            day: {zero: 'dias', one: 'dia', two: 'dias', few: 'dias', many: 'dias', other: 'dias'},
+            hour: {zero: 'horas', one: 'hora', two: 'horas', few: 'horas', many: 'horas', other: 'horas'},
+            minute: {zero: 'minutos', one: 'minuto', two: 'minutos', few: 'minutos', many: 'minutos', other: 'minutos'},
+            second: {
+                zero: 'segundos',
+                one: 'segundo',
+                two: 'segundos',
+                few: 'segundos',
+                many: 'segundos',
+                other: 'segundos'
+            },
+        }
+    },
+
+    ja: {
+        template: {
+            on: (value) => `${value}に`,
+            past: (value) => `${value}前`,
+            future: (value) => `${value}後`,
+            justNow: () => `たった今`,
+        },
+        units: {
+            day: {zero: '日', one: '日', two: '日', few: '日', many: '日', other: '日'},
+            hour: {zero: '時間', one: '時間', two: '時間', few: '時間', many: '時間', other: '時間'},
+            minute: {zero: '分', one: '分', two: '分', few: '分', many: '分', other: '分'},
+            second: {zero: '秒', one: '秒', two: '秒', few: '秒', many: '秒', other: '秒'},
+        }
+    },
+
+    it: {
+        template: {
+            on: (value) => `il ${value}`,
+            past: (value) => `${value} fa`,
+            future: (value) => `in ${value}`,
+            justNow: () => `proprio ora`,
+        },
+        units: {
+            day: {zero: 'giorni', one: 'giorno', two: 'giorni', few: 'giorni', many: 'giorni', other: 'giorni'},
+            hour: {zero: 'ore', one: 'ora', two: 'ore', few: 'ore', many: 'ore', other: 'ore'},
+            minute: {zero: 'minuti', one: 'minuto', two: 'minuti', few: 'minuti', many: 'minuti', other: 'minuti'},
+            second: {
+                zero: 'secondi',
+                one: 'secondo',
+                two: 'secondi',
+                few: 'secondi',
+                many: 'secondi',
+                other: 'secondi'
+            },
+        }
+    },
+
+    ko: {
+        template: {
+            on: (value) => `${value}에`,
+            past: (value) => `${value} 전`,
+            future: (value) => `${value} 후`,
+            justNow: () => `방금`,
+        },
+        units: {
+            day: {zero: '일', one: '일', two: '일', few: '일', many: '일', other: '일'},
+            hour: {zero: '시간', one: '시간', two: '시간', few: '시간', many: '시간', other: '시간'},
+            minute: {zero: '분', one: '분', two: '분', few: '분', many: '분', other: '분'},
+            second: {zero: '초', one: '초', two: '초', few: '초', many: '초', other: '초'},
+        }
+    },
+
+    sw: {
+        template: {
+            on: (value) => `kwa ${value}`,
+            past: (value) => `${value} iliyopita`,
+            future: (value) => `katika ${value}`,
+            justNow: () => `hivi punde`,
+        },
+        units: {
+            day: {zero: 'siku', one: 'siku', two: 'siku', few: 'siku', many: 'siku', other: 'siku'},
+            hour: {zero: 'masaa', one: 'saa', two: 'masaa', few: 'masaa', many: 'masaa', other: 'masaa'},
+            minute: {zero: 'dakika', one: 'dakika', two: 'dakika', few: 'dakika', many: 'dakika', other: 'dakika'},
+            second: {
+                zero: 'sekunde',
+                one: 'sekunde',
+                two: 'sekunde',
+                few: 'sekunde',
+                many: 'sekunde',
+                other: 'sekunde'
+            },
+        }
+    },
+
+    nl: {
+        template: {
+            on: (value) => `op ${value}`,
+            past: (value) => `${value} geleden`,
+            future: (value) => `over ${value}`,
+            justNow: () => `net`,
+        },
+        units: {
+            day: {zero: 'dagen', one: 'dag', two: 'dagen', few: 'dagen', many: 'dagen', other: 'dagen'},
+            hour: {zero: 'uren', one: 'uur', two: 'uren', few: 'uren', many: 'uren', other: 'uren'},
+            minute: {zero: 'minuten', one: 'minuut', two: 'minuten', few: 'minuten', many: 'minuten', other: 'minuten'},
+            second: {
+                zero: 'seconden',
+                one: 'seconde',
+                two: 'seconden',
+                few: 'seconden',
+                many: 'seconden',
+                other: 'seconden'
+            },
+        }
+    },
+
+    ms: {
+        template: {
+            on: (value) => `pada ${value}`,
+            past: (value) => `${value} yang lalu`,
+            future: (value) => `${value} lagi`,
+            justNow: () => `tadi`,
+        },
+        units: {
+            day: {zero: 'hari', one: 'hari', two: 'hari', few: 'hari', many: 'hari', other: 'hari'},
+            hour: {zero: 'jam', one: 'jam', two: 'jam', few: 'jam', many: 'jam', other: 'jam'},
+            minute: {zero: 'minit', one: 'minit', two: 'minit', few: 'minit', many: 'minit', other: 'minit'},
+            second: {zero: 'saat', one: 'saat', two: 'saat', few: 'saat', many: 'saat', other: 'saat'},
+        }
+    },
+
+    id: {
+        template: {
+            on: (value) => `pada ${value}`,
+            past: (value) => `${value} yang lalu`,
+            future: (value) => `dalam ${value}`,
+            justNow: () => `tadi`,
+        },
+        units: {
+            day: {zero: 'hari', one: 'hari', two: 'hari', few: 'hari', many: 'hari', other: 'hari'},
+            hour: {zero: 'jam', one: 'jam', two: 'jam', few: 'jam', many: 'jam', other: 'jam'},
+            minute: {zero: 'menit', one: 'menit', two: 'menit', few: 'menit', many: 'menit', other: 'menit'},
+            second: {zero: 'detik', one: 'detik', two: 'detik', few: 'detik', many: 'detik', other: 'detik'},
+        }
+    },
+
+    tr: {
+        template: {
+            on: (value) => `${value} tarihinde`,
+            past: (value) => `${value} önce`,
+            future: (value) => `${value} sonra`,
+            justNow: () => `az önce`,
+        },
+        units: {
+            day: {zero: 'günler', one: 'gün', two: 'gün', few: 'gün', many: 'gün', other: 'gün'},
+            hour: {zero: 'saatler', one: 'saat', two: 'saat', few: 'saat', many: 'saat', other: 'saat'},
+            minute: {zero: 'dakikalar', one: 'dakika', two: 'dakika', few: 'dakika', many: 'dakika', other: 'dakika'},
+            second: {zero: 'saniyeler', one: 'saniye', two: 'saniye', few: 'saniye', many: 'saniye', other: 'saniye'},
+        }
+    },
+
+    pl: {
+        template: {
+            on: (value) => `w dniu ${value}`,
+            past: (value) => `${value} temu`,
+            future: (value) => `za ${value}`,
+            justNow: () => `przed chwilą`,
+        },
+        units: {
+            day: {zero: 'dni', one: 'dzień', two: 'dni', few: 'dni', many: 'dni', other: 'dni'},
+            hour: {zero: 'godzin', one: 'godzina', two: 'godziny', few: 'godziny', many: 'godzin', other: 'godzin'},
+            minute: {zero: 'minut', one: 'minuta', two: 'minuty', few: 'minuty', many: 'minut', other: 'minut'},
+            second: {zero: 'sekund', one: 'sekunda', two: 'sekundy', few: 'sekundy', many: 'sekund', other: 'sekund'},
+        }
+    },
+
+    sv: {
+        template: {
+            on: (value) => `den ${value}`,
+            past: (value) => `för ${value} sedan`,
+            future: (value) => `om ${value}`,
+            justNow: () => `nyss`,
+        },
+        units: {
+            day: {zero: 'dagar', one: 'dag', two: 'dagar', few: 'dagar', many: 'dagar', other: 'dagar'},
+            hour: {zero: 'timmar', one: 'timme', two: 'timmar', few: 'timmar', many: 'timmar', other: 'timmar'},
+            minute: {zero: 'minuter', one: 'minut', two: 'minuter', few: 'minuter', many: 'minuter', other: 'minuter'},
+            second: {
+                zero: 'sekunder',
+                one: 'sekund',
+                two: 'sekunder',
+                few: 'sekunder',
+                many: 'sekunder',
+                other: 'sekunder'
+            },
+        }
+    },
+
+
+    ro: {
+        template: {
+            on: (value) => `pe ${value}`,
+            past: (value) => `acum ${value}`,
+            future: (value) => `peste ${value}`,
+            justNow: () => `chiar acum`,
+        },
+        units: {
+            day: {zero: 'zile', one: 'zi', two: 'zile', few: 'zile', many: 'zile', other: 'zile'},
+            hour: {zero: 'ore', one: 'oră', two: 'ore', few: 'ore', many: 'ore', other: 'ore'},
+            minute: {zero: 'minute', one: 'minut', two: 'minute', few: 'minute', many: 'minute', other: 'minute'},
+            second: {
+                zero: 'secunde',
+                one: 'secundă',
+                two: 'secunde',
+                few: 'secunde',
+                many: 'secunde',
+                other: 'secunde'
+            },
+        }
+    },
+
+    bg: {
+        template: {
+            on: (value) => `на ${value}`,
+            past: (value) => `преди ${value}`,
+            future: (value) => `след ${value}`,
+            justNow: () => `току-що`,
+        },
+        units: {
+            day: {zero: 'дни', one: 'ден', two: 'дни', few: 'дни', many: 'дни', other: 'дни'},
+            hour: {zero: 'часа', one: 'час', two: 'часа', few: 'часа', many: 'часа', other: 'часа'},
+            minute: {zero: 'минути', one: 'минута', two: 'минути', few: 'минути', many: 'минути', other: 'минути'},
+            second: {
+                zero: 'секунди',
+                one: 'секунда',
+                two: 'секунди',
+                few: 'секунди',
+                many: 'секунди',
+                other: 'секунди'
+            },
+        }
+    },
+
+    da: {
+        template: {
+            on: (value) => `den ${value}`,
+            past: (value) => `for ${value} siden`,
+            future: (value) => `om ${value}`,
+            justNow: () => `lige nu`,
+        },
+        units: {
+            day: {zero: 'dage', one: 'dag', two: 'dage', few: 'dage', many: 'dage', other: 'dage'},
+            hour: {zero: 'timer', one: 'time', two: 'timer', few: 'timer', many: 'timer', other: 'timer'},
+            minute: {
+                zero: 'minutter',
+                one: 'minut',
+                two: 'minutter',
+                few: 'minutter',
+                many: 'minutter',
+                other: 'minutter'
+            },
+            second: {
+                zero: 'sekunder',
+                one: 'sekund',
+                two: 'sekunder',
+                few: 'sekunder',
+                many: 'sekunder',
+                other: 'sekunder'
+            },
+        }
+    },
+
+    fi: {
+        template: {
+            on: (value) => `päivänä ${value}`,
+            past: (value) => `${value} sitten`,
+            future: (value) => `${value} kuluttua`,
+            justNow: () => `juuri nyt`,
+        },
+        units: {
+            day: {zero: 'päivää', one: 'päivä', two: 'päivää', few: 'päivää', many: 'päivää', other: 'päivää'},
+            hour: {zero: 'tuntia', one: 'tunti', two: 'tuntia', few: 'tuntia', many: 'tuntia', other: 'tuntia'},
+            minute: {
+                zero: 'minuuttia',
+                one: 'minuutti',
+                two: 'minuuttia',
+                few: 'minuuttia',
+                many: 'minuuttia',
+                other: 'minuuttia'
+            },
+            second: {
+                zero: 'sekuntia',
+                one: 'sekunti',
+                two: 'sekuntia',
+                few: 'sekuntia',
+                many: 'sekuntia',
+                other: 'sekuntia'
+            },
+        }
+    },
+
+    cs: {
+        template: {
+            on: (value) => `dne ${value}`,
+            past: (value) => `před ${value}`,
+            future: (value) => `za ${value}`,
+            justNow: () => `právě teď`,
+        },
+        units: {
+            day: {zero: 'dny', one: 'den', two: 'dny', few: 'dny', many: 'dnů', other: 'dny'},
+            hour: {zero: 'hodin', one: 'hodina', two: 'hodiny', few: 'hodiny', many: 'hodin', other: 'hodin'},
+            minute: {zero: 'minut', one: 'minuta', two: 'minuty', few: 'minuty', many: 'minut', other: 'minut'},
+            second: {zero: 'sekund', one: 'sekunda', two: 'sekundy', few: 'sekundy', many: 'sekund', other: 'sekund'},
+        }
+    },
+
+    el: {
+        template: {
+            on: (value) => `στις ${value}`,
+            past: (value) => `πριν ${value}`,
+            future: (value) => `σε ${value}`,
+            justNow: () => `μόλις τώρα`,
+        },
+        units: {
+            day: {zero: 'ημέρες', one: 'ημέρα', two: 'ημέρες', few: 'ημέρες', many: 'ημέρες', other: 'ημέρες'},
+            hour: {zero: 'ώρες', one: 'ώρα', two: 'ώρες', few: 'ώρες', many: 'ώρες', other: 'ώρες'},
+            minute: {zero: 'λεπτά', one: 'λεπτό', two: 'λεπτά', few: 'λεπτά', many: 'λεπτά', other: 'λεπτά'},
+            second: {
+                zero: 'δευτερόλεπτα',
+                one: 'δευτερόλεπτο',
+                two: 'δευτερόλεπτα',
+                few: 'δευτερόλεπτα',
+                many: 'δευτερόλεπτα',
+                other: 'δευτερόλεπτα'
+            },
+        }
+    },
+
+    hr: {
+        template: {
+            on: (value) => `na ${value}`,
+            past: (value) => `prije ${value}`,
+            future: (value) => `za ${value}`,
+            justNow: () => `prije trenutka`,
+        },
+        units: {
+            day: {zero: 'dana', one: 'dan', two: 'dana', few: 'dana', many: 'dana', other: 'dana'},
+            hour: {zero: 'sati', one: 'sat', two: 'sata', few: 'sata', many: 'sati', other: 'sati'},
+            minute: {zero: 'minuta', one: 'minuta', two: 'minute', few: 'minute', many: 'minuta', other: 'minuta'},
+            second: {
+                zero: 'sekundi',
+                one: 'sekunda',
+                two: 'sekunde',
+                few: 'sekunde',
+                many: 'sekundi',
+                other: 'sekundi'
+            },
+        }
+    },
+
+    sk: {
+        template: {
+            on: (value) => `v deň ${value}`,
+            past: (value) => `pred ${value}`,
+            future: (value) => `za ${value}`,
+            justNow: () => `práve teraz`,
+        },
+        units: {
+            day: {zero: 'dni', one: 'deň', two: 'dni', few: 'dni', many: 'dní', other: 'dni'},
+            hour: {zero: 'hodiny', one: 'hodina', two: 'hodiny', few: 'hodiny', many: 'hodín', other: 'hodiny'},
+            minute: {zero: 'minúty', one: 'minúta', two: 'minúty', few: 'minúty', many: 'minút', other: 'minúty'},
+            second: {zero: 'sekundy', one: 'sekunda', two: 'sekundy', few: 'sekundy', many: 'sekúnd', other: 'sekundy'},
+        }
+    },
+
+    no: {
+        template: {
+            on: (value) => `på ${value}`,
+            past: (value) => `${value} siden`,
+            future: (value) => `om ${value}`,
+            justNow: () => `nettopp`,
+        },
+        units: {
+            day: {zero: 'dager', one: 'dag', two: 'dager', few: 'dager', many: 'dager', other: 'dager'},
+            hour: {zero: 'timer', one: 'time', two: 'timer', few: 'timer', many: 'timer', other: 'timer'},
+            minute: {
+                zero: 'minutter',
+                one: 'minutt',
+                two: 'minutter',
+                few: 'minutter',
+                many: 'minutter',
+                other: 'minutter'
+            },
+            second: {
+                zero: 'sekunder',
+                one: 'sekund',
+                two: 'sekunder',
+                few: 'sekunder',
+                many: 'sekunder',
+                other: 'sekunder'
+            },
+        }
+    },
+
+    sl: {
+        template: {
+            on: (value) => `na ${value}`,
+            past: (value) => `pred ${value}`,
+            future: (value) => `čez ${value}`,
+            justNow: () => `ravno zdaj`,
+        },
+        units: {
+            day: {zero: 'dni', one: 'dan', two: 'dni', few: 'dni', many: 'dni', other: 'dni'},
+            hour: {zero: 'ure', one: 'ura', two: 'ure', few: 'ure', many: 'ure', other: 'ure'},
+            minute: {zero: 'minute', one: 'minuta', two: 'minute', few: 'minute', many: 'minute', other: 'minute'},
+            second: {zero: 'sekunde', one: 'sekunda', two: 'sekunde', few: 'sekunde', many: 'sekund', other: 'sekund'},
+        }
+    },
+
+    vi: {
+        template: {
+            on: (value) => `vào ${value}`,
+            past: (value) => `${value} trước`,
+            future: (value) => `${value} sau`,
+            justNow: () => `vừa mới`,
+        },
+        units: {
+            day: {zero: 'ngày', one: 'ngày', two: 'ngày', few: 'ngày', many: 'ngày', other: 'ngày'},
+            hour: {zero: 'giờ', one: 'giờ', two: 'giờ', few: 'giờ', many: 'giờ', other: 'giờ'},
+            minute: {zero: 'phút', one: 'phút', two: 'phút', few: 'phút', many: 'phút', other: 'phút'},
+            second: {zero: 'giây', one: 'giây', two: 'giây', few: 'giây', many: 'giây', other: 'giây'},
+        }
+    },
+};
\ No newline at end of file
diff --git a/test/cases/i18n/time-ago.mjs b/test/cases/i18n/time-ago.mjs
new file mode 100644
index 000000000..2e736a387
--- /dev/null
+++ b/test/cases/i18n/time-ago.mjs
@@ -0,0 +1,33 @@
+
+
+import {expect} from "chai"
+import {formatTimeAgo} from "../../../source/i18n/time-ago.mjs";
+
+describe('formatTimeAgo', () => {
+    it('returns "just now" for times less than a second ago', () => {
+        const now = new Date();
+        expect(formatTimeAgo(now, 'en')).to.equal('just now');
+    });
+
+    it('returns "in 1 second" for one second in the future', () => {
+        const oneSecondFuture = new Date(Date.now() + 1000);
+        expect(formatTimeAgo(oneSecondFuture, 'en')).to.equal('in 1 second');
+    });
+
+    it('returns "1 second ago" for one second in the past', () => {
+        const oneSecondPast = new Date(Date.now() - 1000);
+        expect(formatTimeAgo(oneSecondPast, 'en')).to.equal('1 second ago');
+    });
+
+    it('returns "in 1 minute, 30 seconds" for 90 seconds in the future', () => {
+        const ninetySecondsFuture = new Date(Date.now() + 90000);
+        expect(formatTimeAgo(ninetySecondsFuture, 'en')).to.equal('in 1 minute and 30 seconds');
+    });
+
+    it('returns "1 minute, 30 seconds ago" for 90 seconds in the past', () => {
+        const ninetySecondsPast = new Date(Date.now() - 90000);
+        expect(formatTimeAgo(ninetySecondsPast, 'en')).to.equal('1 minute and 30 seconds ago');
+    });
+
+    // Weitere Tests können hinzugefügt werden
+});
-- 
GitLab