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