@open-mercato/shared 0.6.8-develop.6975.1.b21e19d444 → 0.6.8-develop.6976.1.9b79145ac4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/i18n/context.js +5 -0
- package/dist/lib/i18n/context.js.map +2 -2
- package/dist/lib/time.js +4 -9
- package/dist/lib/time.js.map +3 -3
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/__tests__/time.test.ts +47 -15
- package/src/lib/i18n/context.tsx +10 -0
- package/src/lib/time.ts +12 -13
package/dist/lib/i18n/context.js
CHANGED
|
@@ -55,6 +55,10 @@ function useLocale() {
|
|
|
55
55
|
if (!ctx) throw new Error("useLocale must be used within I18nProvider");
|
|
56
56
|
return ctx.locale;
|
|
57
57
|
}
|
|
58
|
+
function useOptionalLocale() {
|
|
59
|
+
const ctx = useContext(I18nContext);
|
|
60
|
+
return ctx?.locale;
|
|
61
|
+
}
|
|
58
62
|
function useLocaleLocked() {
|
|
59
63
|
const ctx = useContext(I18nContext);
|
|
60
64
|
return ctx?.localeLocked ?? false;
|
|
@@ -63,6 +67,7 @@ export {
|
|
|
63
67
|
I18nProvider,
|
|
64
68
|
useLocale,
|
|
65
69
|
useLocaleLocked,
|
|
70
|
+
useOptionalLocale,
|
|
66
71
|
useOptionalT,
|
|
67
72
|
useT
|
|
68
73
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/i18n/context.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { createContext, useContext, useMemo, type ReactNode } from 'react'\nimport type { Locale } from './config'\n\nexport type Dict = Record<string, string>\n\nexport type TranslateParams = Record<string, string | number>\n\nexport type TranslateFn = (\n key: string,\n fallbackOrParams?: string | TranslateParams,\n params?: TranslateParams\n) => string\n\nexport type I18nContextValue = {\n locale: Locale\n t: TranslateFn\n /** True when the locale is pinned via `OM_FORCE_LOCALE`; UI should hide switchers. */\n localeLocked: boolean\n}\n\nconst I18N_CONTEXT_KEY = '__openMercatoI18nContext'\n\ntype GlobalI18nContextStore = typeof globalThis & {\n [I18N_CONTEXT_KEY]?: ReturnType<typeof createContext<I18nContextValue | null>>\n}\n\nfunction getI18nContext() {\n const store = globalThis as GlobalI18nContextStore\n if (!store[I18N_CONTEXT_KEY]) {\n store[I18N_CONTEXT_KEY] = createContext<I18nContextValue | null>(null)\n }\n return store[I18N_CONTEXT_KEY]\n}\n\nconst I18nContext = getI18nContext()\n\nfunction format(template: string, params?: TranslateParams) {\n if (!params) return template\n return template.replace(/\\{\\{(\\w+)\\}\\}|\\{(\\w+)\\}/g, (_, doubleKey, singleKey) => {\n const key = doubleKey ?? singleKey\n if (!key) return _\n const value = params[key]\n if (value === undefined) {\n return doubleKey ? `{{${key}}}` : `{${key}}`\n }\n return String(value)\n })\n}\n\nexport function I18nProvider({ children, locale, dict, localeLocked = false }: { children: ReactNode; locale: Locale; dict: Dict; localeLocked?: boolean }) {\n const value = useMemo<I18nContextValue>(() => ({\n locale,\n localeLocked,\n t: (key, fallbackOrParams, params) => {\n let fallback: string | undefined\n let resolvedParams: TranslateParams | undefined\n\n if (typeof fallbackOrParams === 'string') {\n fallback = fallbackOrParams\n resolvedParams = params\n } else {\n resolvedParams = fallbackOrParams ?? params\n }\n\n const template = dict[key] ?? fallback ?? key\n return format(template, resolvedParams)\n },\n }), [locale, dict, localeLocked])\n return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>\n}\n\nexport function useT() {\n const ctx = useContext(I18nContext)\n if (!ctx) throw new Error('useT must be used within I18nProvider')\n return ctx.t\n}\n\n/**\n * Like `useT`, but returns `undefined` instead of throwing when no\n * `I18nProvider` is in scope. Use where a translator is desirable but not\n * guaranteed (e.g. plumbing `t` into side-effect handlers that may run before\n * the provider mounts) \u2014 callers MUST provide a fallback.\n */\nexport function useOptionalT(): TranslateFn | undefined {\n const ctx = useContext(I18nContext)\n return ctx?.t\n}\n\nexport function useLocale() {\n const ctx = useContext(I18nContext)\n if (!ctx) throw new Error('useLocale must be used within I18nProvider')\n return ctx.locale\n}\n\n/**\n * True when the active locale is pinned via `OM_FORCE_LOCALE`. Returns `false`\n * when no provider is in scope so callers can render unconditionally.\n */\nexport function useLocaleLocked() {\n const ctx = useContext(I18nContext)\n return ctx?.localeLocked ?? false\n}\n"],
|
|
5
|
-
"mappings": ";AAqES;AApET,SAAS,eAAe,YAAY,eAA+B;AAoBnE,MAAM,mBAAmB;AAMzB,SAAS,iBAAiB;AACxB,QAAM,QAAQ;AACd,MAAI,CAAC,MAAM,gBAAgB,GAAG;AAC5B,UAAM,gBAAgB,IAAI,cAAuC,IAAI;AAAA,EACvE;AACA,SAAO,MAAM,gBAAgB;AAC/B;AAEA,MAAM,cAAc,eAAe;AAEnC,SAAS,OAAO,UAAkB,QAA0B;AAC1D,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,SAAS,QAAQ,4BAA4B,CAAC,GAAG,WAAW,cAAc;AAC/E,UAAM,MAAM,aAAa;AACzB,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,QAAQ,OAAO,GAAG;AACxB,QAAI,UAAU,QAAW;AACvB,aAAO,YAAY,KAAK,GAAG,OAAO,IAAI,GAAG;AAAA,IAC3C;AACA,WAAO,OAAO,KAAK;AAAA,EACrB,CAAC;AACH;AAEO,SAAS,aAAa,EAAE,UAAU,QAAQ,MAAM,eAAe,MAAM,GAAgF;AAC1J,QAAM,QAAQ,QAA0B,OAAO;AAAA,IAC7C;AAAA,IACA;AAAA,IACA,GAAG,CAAC,KAAK,kBAAkB,WAAW;AACpC,UAAI;AACJ,UAAI;AAEJ,UAAI,OAAO,qBAAqB,UAAU;AACxC,mBAAW;AACX,yBAAiB;AAAA,MACnB,OAAO;AACL,yBAAiB,oBAAoB;AAAA,MACvC;AAEA,YAAM,WAAW,KAAK,GAAG,KAAK,YAAY;AAC1C,aAAO,OAAO,UAAU,cAAc;AAAA,IACxC;AAAA,EACF,IAAI,CAAC,QAAQ,MAAM,YAAY,CAAC;AAChC,SAAO,oBAAC,YAAY,UAAZ,EAAqB,OAAe,UAAS;AACvD;AAEO,SAAS,OAAO;AACrB,QAAM,MAAM,WAAW,WAAW;AAClC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,uCAAuC;AACjE,SAAO,IAAI;AACb;AAQO,SAAS,eAAwC;AACtD,QAAM,MAAM,WAAW,WAAW;AAClC,SAAO,KAAK;AACd;AAEO,SAAS,YAAY;AAC1B,QAAM,MAAM,WAAW,WAAW;AAClC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,4CAA4C;AACtE,SAAO,IAAI;AACb;AAMO,SAAS,kBAAkB;AAChC,QAAM,MAAM,WAAW,WAAW;AAClC,SAAO,KAAK,gBAAgB;AAC9B;",
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { createContext, useContext, useMemo, type ReactNode } from 'react'\nimport type { Locale } from './config'\n\nexport type Dict = Record<string, string>\n\nexport type TranslateParams = Record<string, string | number>\n\nexport type TranslateFn = (\n key: string,\n fallbackOrParams?: string | TranslateParams,\n params?: TranslateParams\n) => string\n\nexport type I18nContextValue = {\n locale: Locale\n t: TranslateFn\n /** True when the locale is pinned via `OM_FORCE_LOCALE`; UI should hide switchers. */\n localeLocked: boolean\n}\n\nconst I18N_CONTEXT_KEY = '__openMercatoI18nContext'\n\ntype GlobalI18nContextStore = typeof globalThis & {\n [I18N_CONTEXT_KEY]?: ReturnType<typeof createContext<I18nContextValue | null>>\n}\n\nfunction getI18nContext() {\n const store = globalThis as GlobalI18nContextStore\n if (!store[I18N_CONTEXT_KEY]) {\n store[I18N_CONTEXT_KEY] = createContext<I18nContextValue | null>(null)\n }\n return store[I18N_CONTEXT_KEY]\n}\n\nconst I18nContext = getI18nContext()\n\nfunction format(template: string, params?: TranslateParams) {\n if (!params) return template\n return template.replace(/\\{\\{(\\w+)\\}\\}|\\{(\\w+)\\}/g, (_, doubleKey, singleKey) => {\n const key = doubleKey ?? singleKey\n if (!key) return _\n const value = params[key]\n if (value === undefined) {\n return doubleKey ? `{{${key}}}` : `{${key}}`\n }\n return String(value)\n })\n}\n\nexport function I18nProvider({ children, locale, dict, localeLocked = false }: { children: ReactNode; locale: Locale; dict: Dict; localeLocked?: boolean }) {\n const value = useMemo<I18nContextValue>(() => ({\n locale,\n localeLocked,\n t: (key, fallbackOrParams, params) => {\n let fallback: string | undefined\n let resolvedParams: TranslateParams | undefined\n\n if (typeof fallbackOrParams === 'string') {\n fallback = fallbackOrParams\n resolvedParams = params\n } else {\n resolvedParams = fallbackOrParams ?? params\n }\n\n const template = dict[key] ?? fallback ?? key\n return format(template, resolvedParams)\n },\n }), [locale, dict, localeLocked])\n return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>\n}\n\nexport function useT() {\n const ctx = useContext(I18nContext)\n if (!ctx) throw new Error('useT must be used within I18nProvider')\n return ctx.t\n}\n\n/**\n * Like `useT`, but returns `undefined` instead of throwing when no\n * `I18nProvider` is in scope. Use where a translator is desirable but not\n * guaranteed (e.g. plumbing `t` into side-effect handlers that may run before\n * the provider mounts) \u2014 callers MUST provide a fallback.\n */\nexport function useOptionalT(): TranslateFn | undefined {\n const ctx = useContext(I18nContext)\n return ctx?.t\n}\n\nexport function useLocale() {\n const ctx = useContext(I18nContext)\n if (!ctx) throw new Error('useLocale must be used within I18nProvider')\n return ctx.locale\n}\n\n/**\n * Like `useLocale`, but returns `undefined` instead of throwing when no\n * `I18nProvider` is in scope. Use in shared components that receive their\n * translator as a prop and may render outside a provider (galleries, tests).\n */\nexport function useOptionalLocale(): Locale | undefined {\n const ctx = useContext(I18nContext)\n return ctx?.locale\n}\n\n/**\n * True when the active locale is pinned via `OM_FORCE_LOCALE`. Returns `false`\n * when no provider is in scope so callers can render unconditionally.\n */\nexport function useLocaleLocked() {\n const ctx = useContext(I18nContext)\n return ctx?.localeLocked ?? false\n}\n"],
|
|
5
|
+
"mappings": ";AAqES;AApET,SAAS,eAAe,YAAY,eAA+B;AAoBnE,MAAM,mBAAmB;AAMzB,SAAS,iBAAiB;AACxB,QAAM,QAAQ;AACd,MAAI,CAAC,MAAM,gBAAgB,GAAG;AAC5B,UAAM,gBAAgB,IAAI,cAAuC,IAAI;AAAA,EACvE;AACA,SAAO,MAAM,gBAAgB;AAC/B;AAEA,MAAM,cAAc,eAAe;AAEnC,SAAS,OAAO,UAAkB,QAA0B;AAC1D,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,SAAS,QAAQ,4BAA4B,CAAC,GAAG,WAAW,cAAc;AAC/E,UAAM,MAAM,aAAa;AACzB,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,QAAQ,OAAO,GAAG;AACxB,QAAI,UAAU,QAAW;AACvB,aAAO,YAAY,KAAK,GAAG,OAAO,IAAI,GAAG;AAAA,IAC3C;AACA,WAAO,OAAO,KAAK;AAAA,EACrB,CAAC;AACH;AAEO,SAAS,aAAa,EAAE,UAAU,QAAQ,MAAM,eAAe,MAAM,GAAgF;AAC1J,QAAM,QAAQ,QAA0B,OAAO;AAAA,IAC7C;AAAA,IACA;AAAA,IACA,GAAG,CAAC,KAAK,kBAAkB,WAAW;AACpC,UAAI;AACJ,UAAI;AAEJ,UAAI,OAAO,qBAAqB,UAAU;AACxC,mBAAW;AACX,yBAAiB;AAAA,MACnB,OAAO;AACL,yBAAiB,oBAAoB;AAAA,MACvC;AAEA,YAAM,WAAW,KAAK,GAAG,KAAK,YAAY;AAC1C,aAAO,OAAO,UAAU,cAAc;AAAA,IACxC;AAAA,EACF,IAAI,CAAC,QAAQ,MAAM,YAAY,CAAC;AAChC,SAAO,oBAAC,YAAY,UAAZ,EAAqB,OAAe,UAAS;AACvD;AAEO,SAAS,OAAO;AACrB,QAAM,MAAM,WAAW,WAAW;AAClC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,uCAAuC;AACjE,SAAO,IAAI;AACb;AAQO,SAAS,eAAwC;AACtD,QAAM,MAAM,WAAW,WAAW;AAClC,SAAO,KAAK;AACd;AAEO,SAAS,YAAY;AAC1B,QAAM,MAAM,WAAW,WAAW;AAClC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,4CAA4C;AACtE,SAAO,IAAI;AACb;AAOO,SAAS,oBAAwC;AACtD,QAAM,MAAM,WAAW,WAAW;AAClC,SAAO,KAAK;AACd;AAMO,SAAS,kBAAkB;AAChC,QAAM,MAAM,WAAW,WAAW;AAClC,SAAO,KAAK,gBAAgB;AAC9B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/time.js
CHANGED
|
@@ -15,18 +15,13 @@ function formatRelativeTime(value, options) {
|
|
|
15
15
|
const rtf = typeof Intl !== "undefined" && typeof Intl.RelativeTimeFormat === "function" ? new Intl.RelativeTimeFormat(options?.locale, { numeric: "auto" }) : null;
|
|
16
16
|
const format = (unit, divisor) => {
|
|
17
17
|
const valueToFormat = Math.round(diffSeconds / divisor);
|
|
18
|
-
const isPast = diffSeconds < 0;
|
|
19
|
-
if (translate) {
|
|
20
|
-
const suffixKey = isPast ? "time.relative.ago" : "time.relative.fromNow";
|
|
21
|
-
const fallbackSuffix2 = isPast ? "ago" : "from now";
|
|
22
|
-
const suffix = translate(suffixKey, fallbackSuffix2);
|
|
23
|
-
const magnitude2 = Math.abs(valueToFormat);
|
|
24
|
-
return `${magnitude2} ${unit}${magnitude2 === 1 ? "" : "s"} ${suffix}`;
|
|
25
|
-
}
|
|
26
18
|
if (rtf) return rtf.format(valueToFormat, unit);
|
|
19
|
+
const isPast = diffSeconds < 0;
|
|
27
20
|
const fallbackSuffix = isPast ? "ago" : "from now";
|
|
21
|
+
const suffixKey = isPast ? "time.relative.ago" : "time.relative.fromNow";
|
|
22
|
+
const suffix = translate ? translate(suffixKey, fallbackSuffix) : fallbackSuffix;
|
|
28
23
|
const magnitude = Math.abs(valueToFormat);
|
|
29
|
-
return `${magnitude} ${unit}${magnitude === 1 ? "" : "s"} ${
|
|
24
|
+
return `${magnitude} ${unit}${magnitude === 1 ? "" : "s"} ${suffix}`;
|
|
30
25
|
};
|
|
31
26
|
if (absSeconds < 45) return format("second", 1);
|
|
32
27
|
if (absSeconds < 45 * 60) return format("minute", 60);
|
package/dist/lib/time.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/time.ts"],
|
|
4
|
-
"sourcesContent": ["\nexport function formatDateTime(value?: string | null): string | null {\n if (!value) return null\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return null\n return date.toLocaleString()\n}\nexport type RelativeTimeTranslator = (\n key: string,\n fallback?: string,\n params?: Record<string, string | number>\n) => string\n\nexport type FormatRelativeTimeOptions = {\n translate?: RelativeTimeTranslator\n locale?: string | string[]\n}\n\ntype RelativeTimeUnit = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'\n\nexport function formatRelativeTime(\n value?: string | null,\n options?: FormatRelativeTimeOptions\n): string | null {\n if (!value) return null\n\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return null\n\n const now = Date.now()\n const diffSeconds = (date.getTime() - now) / 1000\n const absSeconds = Math.abs(diffSeconds)\n const translate = options?.translate\n\n const rtf =\n typeof Intl !== 'undefined' && typeof Intl.RelativeTimeFormat === 'function'\n ? new Intl.RelativeTimeFormat(options?.locale, { numeric: 'auto' })\n : null\n\
|
|
5
|
-
"mappings": "AACO,SAAS,eAAe,OAAsC;AACnE,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,IAAI,KAAK,KAAK;AAC3B,MAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,EAAG,QAAO;AACzC,SAAO,KAAK,eAAe;AAC7B;
|
|
6
|
-
"names": [
|
|
4
|
+
"sourcesContent": ["\nexport function formatDateTime(value?: string | null): string | null {\n if (!value) return null\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return null\n return date.toLocaleString()\n}\nexport type RelativeTimeTranslator = (\n key: string,\n fallback?: string,\n params?: Record<string, string | number>\n) => string\n\nexport type FormatRelativeTimeOptions = {\n /**\n * Supplies the suffix only when the runtime has no `Intl.RelativeTimeFormat`.\n * Otherwise the number, unit name, plural form and suffix all come from\n * `Intl` in `locale` \u2014 pass `useLocale()` client-side so the text follows the\n * application language rather than the runtime one.\n */\n translate?: RelativeTimeTranslator\n locale?: string | string[]\n}\n\ntype RelativeTimeUnit = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'\n\nexport function formatRelativeTime(\n value?: string | null,\n options?: FormatRelativeTimeOptions\n): string | null {\n if (!value) return null\n\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return null\n\n const now = Date.now()\n const diffSeconds = (date.getTime() - now) / 1000\n const absSeconds = Math.abs(diffSeconds)\n const translate = options?.translate\n\n const rtf =\n typeof Intl !== 'undefined' && typeof Intl.RelativeTimeFormat === 'function'\n ? new Intl.RelativeTimeFormat(options?.locale, { numeric: 'auto' })\n : null\n\n const format = (unit: RelativeTimeUnit, divisor: number) => {\n const valueToFormat = Math.round(diffSeconds / divisor)\n if (rtf) return rtf.format(valueToFormat, unit)\n\n const isPast = diffSeconds < 0\n const fallbackSuffix = isPast ? 'ago' : 'from now'\n const suffixKey = isPast ? 'time.relative.ago' : 'time.relative.fromNow'\n const suffix = translate ? translate(suffixKey, fallbackSuffix) : fallbackSuffix\n const magnitude = Math.abs(valueToFormat)\n return `${magnitude} ${unit}${magnitude === 1 ? '' : 's'} ${suffix}`\n }\n\n if (absSeconds < 45) return format('second', 1)\n if (absSeconds < 45 * 60) return format('minute', 60)\n if (absSeconds < 24 * 60 * 60) return format('hour', 60 * 60)\n if (absSeconds < 7 * 24 * 60 * 60) return format('day', 24 * 60 * 60)\n if (absSeconds < 30 * 24 * 60 * 60) return format('week', 7 * 24 * 60 * 60)\n if (absSeconds < 365 * 24 * 60 * 60) return format('month', 30 * 24 * 60 * 60)\n return format('year', 365 * 24 * 60 * 60)\n}\n"],
|
|
5
|
+
"mappings": "AACO,SAAS,eAAe,OAAsC;AACnE,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,IAAI,KAAK,KAAK;AAC3B,MAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,EAAG,QAAO;AACzC,SAAO,KAAK,eAAe;AAC7B;AAoBO,SAAS,mBACd,OACA,SACe;AACf,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,OAAO,IAAI,KAAK,KAAK;AAC3B,MAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,EAAG,QAAO;AAEzC,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,eAAe,KAAK,QAAQ,IAAI,OAAO;AAC7C,QAAM,aAAa,KAAK,IAAI,WAAW;AACvC,QAAM,YAAY,SAAS;AAE3B,QAAM,MACJ,OAAO,SAAS,eAAe,OAAO,KAAK,uBAAuB,aAC9D,IAAI,KAAK,mBAAmB,SAAS,QAAQ,EAAE,SAAS,OAAO,CAAC,IAChE;AAEN,QAAM,SAAS,CAAC,MAAwB,YAAoB;AAC1D,UAAM,gBAAgB,KAAK,MAAM,cAAc,OAAO;AACtD,QAAI,IAAK,QAAO,IAAI,OAAO,eAAe,IAAI;AAE9C,UAAM,SAAS,cAAc;AAC7B,UAAM,iBAAiB,SAAS,QAAQ;AACxC,UAAM,YAAY,SAAS,sBAAsB;AACjD,UAAM,SAAS,YAAY,UAAU,WAAW,cAAc,IAAI;AAClE,UAAM,YAAY,KAAK,IAAI,aAAa;AACxC,WAAO,GAAG,SAAS,IAAI,IAAI,GAAG,cAAc,IAAI,KAAK,GAAG,IAAI,MAAM;AAAA,EACpE;AAEA,MAAI,aAAa,GAAI,QAAO,OAAO,UAAU,CAAC;AAC9C,MAAI,aAAa,KAAK,GAAI,QAAO,OAAO,UAAU,EAAE;AACpD,MAAI,aAAa,KAAK,KAAK,GAAI,QAAO,OAAO,QAAQ,KAAK,EAAE;AAC5D,MAAI,aAAa,IAAI,KAAK,KAAK,GAAI,QAAO,OAAO,OAAO,KAAK,KAAK,EAAE;AACpE,MAAI,aAAa,KAAK,KAAK,KAAK,GAAI,QAAO,OAAO,QAAQ,IAAI,KAAK,KAAK,EAAE;AAC1E,MAAI,aAAa,MAAM,KAAK,KAAK,GAAI,QAAO,OAAO,SAAS,KAAK,KAAK,KAAK,EAAE;AAC7E,SAAO,OAAO,QAAQ,MAAM,KAAK,KAAK,EAAE;AAC1C;",
|
|
6
|
+
"names": []
|
|
7
7
|
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.8-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.8-develop.6976.1.9b79145ac4';\nexport const appVersion = APP_VERSION;\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.6.8-develop.
|
|
3
|
+
"version": "0.6.8-develop.6976.1.9b79145ac4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"@mikro-orm/core": "^7.1.8",
|
|
106
106
|
"@mikro-orm/decorators": "^7.1.8",
|
|
107
107
|
"@mikro-orm/postgresql": "^7.1.8",
|
|
108
|
-
"@open-mercato/cache": "0.6.8-develop.
|
|
108
|
+
"@open-mercato/cache": "0.6.8-develop.6976.1.9b79145ac4",
|
|
109
109
|
"@types/sanitize-html": "^2.16.1",
|
|
110
110
|
"dotenv": "^17.4.2",
|
|
111
111
|
"pino": "^10.3.1",
|
|
@@ -82,22 +82,54 @@ describe('formatRelativeTime', () => {
|
|
|
82
82
|
expect(/year|month/.test(future!)).toBeTruthy()
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
-
it('uses fallback if Intl.RelativeTimeFormat is not available', () => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
})
|
|
85
|
+
it('uses fallback if Intl.RelativeTimeFormat is not available', () => {
|
|
86
|
+
const descriptor = Object.getOwnPropertyDescriptor(Intl, 'RelativeTimeFormat')
|
|
87
|
+
Object.defineProperty(Intl, 'RelativeTimeFormat', { value: undefined, configurable: true })
|
|
88
|
+
try {
|
|
89
|
+
expect(formatRelativeTime(sub(3_600_000))).toMatch(/ago/)
|
|
90
|
+
expect(formatRelativeTime(add(3_600_000))).toMatch(/from now/)
|
|
91
|
+
} finally {
|
|
92
|
+
Object.defineProperty(Intl, 'RelativeTimeFormat', descriptor!)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
95
|
|
|
96
|
-
it('uses custom translate
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
it('uses custom translate for the fallback suffix when Intl.RelativeTimeFormat is not available', () => {
|
|
97
|
+
const translate = (key: string) => `T(${key})`
|
|
98
|
+
const descriptor = Object.getOwnPropertyDescriptor(Intl, 'RelativeTimeFormat')
|
|
99
|
+
Object.defineProperty(Intl, 'RelativeTimeFormat', { value: undefined, configurable: true })
|
|
100
|
+
try {
|
|
101
|
+
expect(formatRelativeTime(sub(3_600_000), { translate })).toMatch(/T\(time\.relative\.ago\)/)
|
|
102
|
+
expect(formatRelativeTime(add(3_600_000), { translate })).toMatch(/T\(time\.relative\.fromNow\)/)
|
|
103
|
+
} finally {
|
|
104
|
+
Object.defineProperty(Intl, 'RelativeTimeFormat', descriptor!)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('localizes unit, number and suffix in a non-English locale', () => {
|
|
109
|
+
expect(formatRelativeTime(sub(3_600_000), { locale: 'pl' })).toBe('1 godzinę temu')
|
|
110
|
+
expect(formatRelativeTime(sub(600_000), { locale: 'pl' })).toBe('10 minut temu')
|
|
111
|
+
expect(formatRelativeTime(add(18_000_000), { locale: 'pl' })).toBe('za 5 godzin')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('localizes in the requested locale even when a translate function is supplied', () => {
|
|
115
|
+
const translate = (_key: string, fallback?: string) => fallback ?? _key
|
|
116
|
+
const polish = formatRelativeTime(sub(600_000), { locale: 'pl', translate })
|
|
117
|
+
const english = formatRelativeTime(sub(600_000), { locale: 'en', translate })
|
|
118
|
+
expect(polish).toBe('10 minut temu')
|
|
119
|
+
expect(polish).not.toMatch(/minutes|ago/)
|
|
120
|
+
expect(english).not.toEqual(polish)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('applies the locale plural rules instead of English pluralization', () => {
|
|
124
|
+
expect(formatRelativeTime(sub(120_000), { locale: 'pl' })).toBe('2 minuty temu')
|
|
125
|
+
expect(formatRelativeTime(sub(300_000), { locale: 'pl' })).toBe('5 minut temu')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('renders differently per locale for the same instant', () => {
|
|
129
|
+
const value = sub(172_800_000)
|
|
130
|
+
const rendered = ['en', 'pl', 'de', 'es'].map((locale) => formatRelativeTime(value, { locale }))
|
|
131
|
+
expect(new Set(rendered).size).toBe(rendered.length)
|
|
132
|
+
})
|
|
101
133
|
})
|
|
102
134
|
describe('formatDateTime', () => {
|
|
103
135
|
it('returns null for invalid or missing values', () => {
|
package/src/lib/i18n/context.tsx
CHANGED
|
@@ -93,6 +93,16 @@ export function useLocale() {
|
|
|
93
93
|
return ctx.locale
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Like `useLocale`, but returns `undefined` instead of throwing when no
|
|
98
|
+
* `I18nProvider` is in scope. Use in shared components that receive their
|
|
99
|
+
* translator as a prop and may render outside a provider (galleries, tests).
|
|
100
|
+
*/
|
|
101
|
+
export function useOptionalLocale(): Locale | undefined {
|
|
102
|
+
const ctx = useContext(I18nContext)
|
|
103
|
+
return ctx?.locale
|
|
104
|
+
}
|
|
105
|
+
|
|
96
106
|
/**
|
|
97
107
|
* True when the active locale is pinned via `OM_FORCE_LOCALE`. Returns `false`
|
|
98
108
|
* when no provider is in scope so callers can render unconditionally.
|
package/src/lib/time.ts
CHANGED
|
@@ -12,6 +12,12 @@ export type RelativeTimeTranslator = (
|
|
|
12
12
|
) => string
|
|
13
13
|
|
|
14
14
|
export type FormatRelativeTimeOptions = {
|
|
15
|
+
/**
|
|
16
|
+
* Supplies the suffix only when the runtime has no `Intl.RelativeTimeFormat`.
|
|
17
|
+
* Otherwise the number, unit name, plural form and suffix all come from
|
|
18
|
+
* `Intl` in `locale` — pass `useLocale()` client-side so the text follows the
|
|
19
|
+
* application language rather than the runtime one.
|
|
20
|
+
*/
|
|
15
21
|
translate?: RelativeTimeTranslator
|
|
16
22
|
locale?: string | string[]
|
|
17
23
|
}
|
|
@@ -37,25 +43,18 @@ export function formatRelativeTime(
|
|
|
37
43
|
? new Intl.RelativeTimeFormat(options?.locale, { numeric: 'auto' })
|
|
38
44
|
: null
|
|
39
45
|
|
|
40
|
-
const format = (unit: RelativeTimeUnit, divisor: number) => {
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
const format = (unit: RelativeTimeUnit, divisor: number) => {
|
|
47
|
+
const valueToFormat = Math.round(diffSeconds / divisor)
|
|
48
|
+
if (rtf) return rtf.format(valueToFormat, unit)
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
const suffixKey = isPast ? 'time.relative.ago' : 'time.relative.fromNow'
|
|
50
|
+
const isPast = diffSeconds < 0
|
|
46
51
|
const fallbackSuffix = isPast ? 'ago' : 'from now'
|
|
47
|
-
const
|
|
52
|
+
const suffixKey = isPast ? 'time.relative.ago' : 'time.relative.fromNow'
|
|
53
|
+
const suffix = translate ? translate(suffixKey, fallbackSuffix) : fallbackSuffix
|
|
48
54
|
const magnitude = Math.abs(valueToFormat)
|
|
49
55
|
return `${magnitude} ${unit}${magnitude === 1 ? '' : 's'} ${suffix}`
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
if (rtf) return rtf.format(valueToFormat, unit)
|
|
53
|
-
|
|
54
|
-
const fallbackSuffix = isPast ? 'ago' : 'from now'
|
|
55
|
-
const magnitude = Math.abs(valueToFormat)
|
|
56
|
-
return `${magnitude} ${unit}${magnitude === 1 ? '' : 's'} ${fallbackSuffix}`
|
|
57
|
-
}
|
|
58
|
-
|
|
59
58
|
if (absSeconds < 45) return format('second', 1)
|
|
60
59
|
if (absSeconds < 45 * 60) return format('minute', 60)
|
|
61
60
|
if (absSeconds < 24 * 60 * 60) return format('hour', 60 * 60)
|