@bquery/bquery 1.7.0 → 1.8.1
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/README.md +760 -716
- package/dist/{a11y-C5QOVvRn.js → a11y-DVBCy09c.js} +3 -3
- package/dist/a11y-DVBCy09c.js.map +1 -0
- package/dist/a11y.es.mjs +1 -1
- package/dist/component/library.d.ts.map +1 -1
- package/dist/{component-CuuTijA6.js → component-L3-JfOFz.js} +5 -5
- package/dist/component-L3-JfOFz.js.map +1 -0
- package/dist/component.es.mjs +1 -1
- package/dist/{config-BW35FKuA.js → config-DhT9auRm.js} +1 -1
- package/dist/{config-BW35FKuA.js.map → config-DhT9auRm.js.map} +1 -1
- package/dist/{constraints-3lV9yyBw.js → constraints-D5RHQLmP.js} +1 -1
- package/dist/constraints-D5RHQLmP.js.map +1 -0
- package/dist/core/collection.d.ts +86 -0
- package/dist/core/collection.d.ts.map +1 -1
- package/dist/core/element.d.ts +28 -0
- package/dist/core/element.d.ts.map +1 -1
- package/dist/core/shared.d.ts +6 -0
- package/dist/core/shared.d.ts.map +1 -1
- package/dist/core-DdtZHzsS.js +168 -0
- package/dist/core-DdtZHzsS.js.map +1 -0
- package/dist/{core-Cjl7GUu8.js → core-EMYSLzaT.js} +289 -259
- package/dist/core-EMYSLzaT.js.map +1 -0
- package/dist/core.es.mjs +48 -47
- package/dist/{custom-directives-7wAShnnd.js → custom-directives-Dr4C5lVV.js} +1 -1
- package/dist/custom-directives-Dr4C5lVV.js.map +1 -0
- package/dist/{devtools-D2fQLhDN.js → devtools-BhB2iDPT.js} +2 -2
- package/dist/devtools-BhB2iDPT.js.map +1 -0
- package/dist/devtools.es.mjs +1 -1
- package/dist/{dnd-B8EgyzaI.js → dnd-NwZBYh4l.js} +1 -1
- package/dist/dnd-NwZBYh4l.js.map +1 -0
- package/dist/dnd.es.mjs +1 -1
- package/dist/{env-NeVmr4Gf.js → env-CTdvLaH2.js} +1 -1
- package/dist/env-CTdvLaH2.js.map +1 -0
- package/dist/forms/create-form.d.ts.map +1 -1
- package/dist/forms/index.d.ts +3 -2
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/types.d.ts +46 -0
- package/dist/forms/types.d.ts.map +1 -1
- package/dist/forms/use-field.d.ts +34 -0
- package/dist/forms/use-field.d.ts.map +1 -0
- package/dist/forms/validators.d.ts +25 -0
- package/dist/forms/validators.d.ts.map +1 -1
- package/dist/forms-UcRHsYxC.js +227 -0
- package/dist/forms-UcRHsYxC.js.map +1 -0
- package/dist/forms.es.mjs +14 -12
- package/dist/full.d.ts +17 -26
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +206 -181
- package/dist/full.iife.js +33 -33
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +33 -33
- package/dist/full.umd.js.map +1 -1
- package/dist/function-Cybd57JV.js +33 -0
- package/dist/function-Cybd57JV.js.map +1 -0
- package/dist/{i18n-BnnhTFOS.js → i18n-kuF6Ekj6.js} +3 -3
- package/dist/i18n-kuF6Ekj6.js.map +1 -0
- package/dist/i18n.es.mjs +1 -1
- package/dist/index.es.mjs +251 -228
- package/dist/media/breakpoints.d.ts.map +1 -1
- package/dist/media/types.d.ts +2 -2
- package/dist/media/types.d.ts.map +1 -1
- package/dist/{media-Di2Ta22s.js → media-i-fB5WxI.js} +3 -3
- package/dist/media-i-fB5WxI.js.map +1 -0
- package/dist/media.es.mjs +1 -1
- package/dist/{motion-qPj_TYGv.js → motion-BJsAuULb.js} +2 -2
- package/dist/motion-BJsAuULb.js.map +1 -0
- package/dist/motion.es.mjs +1 -1
- package/dist/{mount-SM07RUa6.js → mount-B4Y8bk8Z.js} +5 -5
- package/dist/mount-B4Y8bk8Z.js.map +1 -0
- package/dist/{platform-CPbCprb6.js → platform-Dw2gE3zI.js} +3 -3
- package/dist/{platform-CPbCprb6.js.map → platform-Dw2gE3zI.js.map} +1 -1
- package/dist/platform.es.mjs +2 -2
- package/dist/plugin/registry.d.ts.map +1 -1
- package/dist/{plugin-cPoOHFLY.js → plugin-C2WuC8SF.js} +20 -18
- package/dist/plugin-C2WuC8SF.js.map +1 -0
- package/dist/plugin.es.mjs +1 -1
- package/dist/reactive/async-data.d.ts +28 -3
- package/dist/reactive/async-data.d.ts.map +1 -1
- package/dist/reactive/computed.d.ts +3 -0
- package/dist/reactive/computed.d.ts.map +1 -1
- package/dist/reactive/effect.d.ts +3 -0
- package/dist/reactive/effect.d.ts.map +1 -1
- package/dist/reactive/http.d.ts +194 -0
- package/dist/reactive/http.d.ts.map +1 -0
- package/dist/reactive/index.d.ts +2 -2
- package/dist/reactive/index.d.ts.map +1 -1
- package/dist/reactive/pagination.d.ts +126 -0
- package/dist/reactive/pagination.d.ts.map +1 -0
- package/dist/reactive/polling.d.ts +55 -0
- package/dist/reactive/polling.d.ts.map +1 -0
- package/dist/reactive/readonly.d.ts +20 -1
- package/dist/reactive/readonly.d.ts.map +1 -1
- package/dist/reactive/rest.d.ts +293 -0
- package/dist/reactive/rest.d.ts.map +1 -0
- package/dist/reactive/scope.d.ts +140 -0
- package/dist/reactive/scope.d.ts.map +1 -0
- package/dist/reactive/signal.d.ts +16 -2
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive/to-value.d.ts +57 -0
- package/dist/reactive/to-value.d.ts.map +1 -0
- package/dist/reactive/websocket.d.ts +285 -0
- package/dist/reactive/websocket.d.ts.map +1 -0
- package/dist/reactive-DwkhUJfP.js +1148 -0
- package/dist/reactive-DwkhUJfP.js.map +1 -0
- package/dist/reactive.es.mjs +38 -19
- package/dist/{registry-CWf368tT.js → registry-B08iilIh.js} +1 -1
- package/dist/{registry-CWf368tT.js.map → registry-B08iilIh.js.map} +1 -1
- package/dist/router/constraints.d.ts.map +1 -1
- package/dist/router/index.d.ts +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/state.d.ts +25 -2
- package/dist/router/state.d.ts.map +1 -1
- package/dist/router-CQikC9Ed.js +492 -0
- package/dist/router-CQikC9Ed.js.map +1 -0
- package/dist/router.es.mjs +9 -8
- package/dist/ssr/hydrate.d.ts.map +1 -1
- package/dist/{ssr-B2qd_WBB.js → ssr-_dAcGdzu.js} +4 -4
- package/dist/ssr-_dAcGdzu.js.map +1 -0
- package/dist/ssr.es.mjs +1 -1
- package/dist/store/persisted.d.ts.map +1 -1
- package/dist/{store-DWpyH6p5.js → store-Cb3gPRve.js} +7 -7
- package/dist/store-Cb3gPRve.js.map +1 -0
- package/dist/store.es.mjs +2 -2
- package/dist/storybook.es.mjs.map +1 -1
- package/dist/{testing-CsqjNUyy.js → testing-C5Sjfsna.js} +8 -8
- package/dist/testing-C5Sjfsna.js.map +1 -0
- package/dist/testing.es.mjs +1 -1
- package/dist/{type-guards-Do9DWgNp.js → type-guards-BMX2c0LP.js} +1 -1
- package/dist/{type-guards-Do9DWgNp.js.map → type-guards-BMX2c0LP.js.map} +1 -1
- package/dist/untrack-D0fnO5k2.js +36 -0
- package/dist/untrack-D0fnO5k2.js.map +1 -0
- package/dist/view/custom-directives.d.ts.map +1 -1
- package/dist/view.es.mjs +4 -4
- package/package.json +177 -177
- package/src/a11y/announce.ts +131 -131
- package/src/a11y/audit.ts +314 -314
- package/src/a11y/index.ts +68 -68
- package/src/a11y/media-preferences.ts +255 -255
- package/src/a11y/roving-tab-index.ts +164 -164
- package/src/a11y/skip-link.ts +255 -255
- package/src/a11y/trap-focus.ts +184 -184
- package/src/a11y/types.ts +183 -183
- package/src/component/component.ts +599 -599
- package/src/component/html.ts +153 -153
- package/src/component/index.ts +52 -52
- package/src/component/library.ts +540 -542
- package/src/component/scope.ts +212 -212
- package/src/component/types.ts +310 -310
- package/src/core/collection.ts +876 -707
- package/src/core/element.ts +1015 -981
- package/src/core/env.ts +60 -60
- package/src/core/index.ts +49 -49
- package/src/core/shared.ts +77 -62
- package/src/core/utils/index.ts +148 -148
- package/src/devtools/devtools.ts +410 -410
- package/src/devtools/index.ts +48 -48
- package/src/devtools/types.ts +104 -104
- package/src/dnd/draggable.ts +296 -296
- package/src/dnd/droppable.ts +228 -228
- package/src/dnd/index.ts +62 -62
- package/src/dnd/sortable.ts +307 -307
- package/src/dnd/types.ts +293 -293
- package/src/forms/create-form.ts +320 -278
- package/src/forms/index.ts +70 -65
- package/src/forms/types.ts +203 -154
- package/src/forms/use-field.ts +231 -0
- package/src/forms/validators.ts +294 -265
- package/src/full.ts +554 -480
- package/src/i18n/formatting.ts +67 -67
- package/src/i18n/i18n.ts +200 -200
- package/src/i18n/index.ts +67 -67
- package/src/i18n/translate.ts +182 -182
- package/src/i18n/types.ts +171 -171
- package/src/index.ts +108 -108
- package/src/media/battery.ts +116 -116
- package/src/media/breakpoints.ts +129 -131
- package/src/media/clipboard.ts +80 -80
- package/src/media/device-sensors.ts +158 -158
- package/src/media/geolocation.ts +119 -119
- package/src/media/index.ts +76 -76
- package/src/media/media-query.ts +92 -92
- package/src/media/network.ts +115 -115
- package/src/media/types.ts +177 -177
- package/src/media/viewport.ts +84 -84
- package/src/motion/index.ts +57 -57
- package/src/motion/morph.ts +151 -151
- package/src/motion/parallax.ts +120 -120
- package/src/motion/reduced-motion.ts +66 -66
- package/src/motion/types.ts +271 -271
- package/src/motion/typewriter.ts +164 -164
- package/src/plugin/index.ts +37 -37
- package/src/plugin/registry.ts +284 -269
- package/src/plugin/types.ts +137 -137
- package/src/reactive/async-data.ts +250 -29
- package/src/reactive/computed.ts +144 -130
- package/src/reactive/effect.ts +29 -6
- package/src/reactive/http.ts +790 -0
- package/src/reactive/index.ts +60 -0
- package/src/reactive/pagination.ts +317 -0
- package/src/reactive/polling.ts +179 -0
- package/src/reactive/readonly.ts +52 -8
- package/src/reactive/rest.ts +859 -0
- package/src/reactive/scope.ts +276 -0
- package/src/reactive/signal.ts +61 -1
- package/src/reactive/to-value.ts +71 -0
- package/src/reactive/websocket.ts +849 -0
- package/src/router/bq-link.ts +279 -279
- package/src/router/constraints.ts +204 -201
- package/src/router/index.ts +49 -49
- package/src/router/match.ts +312 -312
- package/src/router/path-pattern.ts +52 -52
- package/src/router/query.ts +38 -38
- package/src/router/router.ts +421 -402
- package/src/router/state.ts +51 -3
- package/src/router/types.ts +139 -139
- package/src/router/use-route.ts +68 -68
- package/src/router/utils.ts +157 -157
- package/src/security/index.ts +12 -12
- package/src/ssr/hydrate.ts +84 -82
- package/src/ssr/index.ts +70 -70
- package/src/ssr/render.ts +508 -508
- package/src/ssr/serialize.ts +296 -296
- package/src/ssr/types.ts +81 -81
- package/src/store/create-store.ts +467 -467
- package/src/store/index.ts +27 -27
- package/src/store/persisted.ts +245 -249
- package/src/store/types.ts +247 -247
- package/src/store/utils.ts +135 -135
- package/src/storybook/index.ts +480 -480
- package/src/testing/index.ts +42 -42
- package/src/testing/testing.ts +593 -593
- package/src/testing/types.ts +170 -170
- package/src/view/custom-directives.ts +28 -30
- package/src/view/evaluate.ts +292 -292
- package/src/view/process.ts +108 -108
- package/dist/a11y-C5QOVvRn.js.map +0 -1
- package/dist/component-CuuTijA6.js.map +0 -1
- package/dist/constraints-3lV9yyBw.js.map +0 -1
- package/dist/core-Cjl7GUu8.js.map +0 -1
- package/dist/core-DnlyjbF2.js +0 -112
- package/dist/core-DnlyjbF2.js.map +0 -1
- package/dist/custom-directives-7wAShnnd.js.map +0 -1
- package/dist/devtools-D2fQLhDN.js.map +0 -1
- package/dist/dnd-B8EgyzaI.js.map +0 -1
- package/dist/env-NeVmr4Gf.js.map +0 -1
- package/dist/forms-C3yovgH9.js +0 -141
- package/dist/forms-C3yovgH9.js.map +0 -1
- package/dist/i18n-BnnhTFOS.js.map +0 -1
- package/dist/media-Di2Ta22s.js.map +0 -1
- package/dist/motion-qPj_TYGv.js.map +0 -1
- package/dist/mount-SM07RUa6.js.map +0 -1
- package/dist/plugin-cPoOHFLY.js.map +0 -1
- package/dist/reactive-Cfv0RK6x.js +0 -233
- package/dist/reactive-Cfv0RK6x.js.map +0 -1
- package/dist/router-BrthaP_z.js +0 -473
- package/dist/router-BrthaP_z.js.map +0 -1
- package/dist/ssr-B2qd_WBB.js.map +0 -1
- package/dist/store-DWpyH6p5.js.map +0 -1
- package/dist/testing-CsqjNUyy.js.map +0 -1
- package/dist/untrack-DJVQQ2WM.js +0 -33
- package/dist/untrack-DJVQQ2WM.js.map +0 -1
package/src/i18n/translate.ts
CHANGED
|
@@ -1,182 +1,182 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Translation resolution helpers.
|
|
3
|
-
* @module bquery/i18n
|
|
4
|
-
* @internal
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { isPlainObject, isPrototypePollutionKey, merge } from '../core/utils/object';
|
|
8
|
-
import type { LocaleMessages, TranslateParams } from './types';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Resolves a dot-delimited key path against a messages object.
|
|
12
|
-
*
|
|
13
|
-
* @param messages - The locale messages
|
|
14
|
-
* @param key - Dot-delimited key (e.g. 'user.welcome')
|
|
15
|
-
* @returns The resolved string, or `undefined` if not found
|
|
16
|
-
*
|
|
17
|
-
* @internal
|
|
18
|
-
*/
|
|
19
|
-
export const resolveKey = (messages: LocaleMessages, key: string): string | undefined => {
|
|
20
|
-
const parts = key.split('.');
|
|
21
|
-
let current: LocaleMessages | string = messages;
|
|
22
|
-
|
|
23
|
-
for (const part of parts) {
|
|
24
|
-
if (typeof current === 'string') return undefined;
|
|
25
|
-
if (current[part] === undefined) return undefined;
|
|
26
|
-
current = current[part];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return typeof current === 'string' ? current : undefined;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Interpolates `{param}` placeholders in a string.
|
|
34
|
-
*
|
|
35
|
-
* @param template - The template string with `{key}` placeholders
|
|
36
|
-
* @param params - Key-value pairs for replacement
|
|
37
|
-
* @returns The interpolated string
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```ts
|
|
41
|
-
* interpolate('Hello, {name}!', { name: 'Ada' });
|
|
42
|
-
* // → 'Hello, Ada!'
|
|
43
|
-
* ```
|
|
44
|
-
*
|
|
45
|
-
* @internal
|
|
46
|
-
*/
|
|
47
|
-
export const interpolate = (template: string, params: TranslateParams): string => {
|
|
48
|
-
return template.replace(/\{(\w+)\}/g, (match, key: string) => {
|
|
49
|
-
if (key in params) {
|
|
50
|
-
return String(params[key]);
|
|
51
|
-
}
|
|
52
|
-
return match; // Leave unmatched placeholders as-is
|
|
53
|
-
});
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Selects the correct plural form from a pipe-delimited string.
|
|
58
|
-
*
|
|
59
|
-
* Supports two formats:
|
|
60
|
-
* - **Two forms:** `"singular | plural"` — singular when count === 1
|
|
61
|
-
* - **Three forms:** `"zero | one | many"` — zero when count === 0,
|
|
62
|
-
* one when count === 1, many otherwise
|
|
63
|
-
*
|
|
64
|
-
* The `count` parameter must be present in `params` for pluralization.
|
|
65
|
-
* If no `count` param exists or the string has no pipes, the string is
|
|
66
|
-
* returned as-is.
|
|
67
|
-
*
|
|
68
|
-
* @param template - Pipe-delimited plural forms
|
|
69
|
-
* @param params - Must include a `count` key for plural selection
|
|
70
|
-
* @returns The selected form
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* ```ts
|
|
74
|
-
* pluralize('{count} item | {count} items', { count: 1 });
|
|
75
|
-
* // → '{count} item'
|
|
76
|
-
*
|
|
77
|
-
* pluralize('no items | {count} item | {count} items', { count: 0 });
|
|
78
|
-
* // → 'no items'
|
|
79
|
-
* ```
|
|
80
|
-
*
|
|
81
|
-
* @internal
|
|
82
|
-
*/
|
|
83
|
-
export const pluralize = (template: string, params: TranslateParams): string => {
|
|
84
|
-
if (!template.includes('|')) return template;
|
|
85
|
-
if (!('count' in params)) return template;
|
|
86
|
-
|
|
87
|
-
const count = Number(params.count);
|
|
88
|
-
const forms = template.split('|').map((s) => s.trim());
|
|
89
|
-
|
|
90
|
-
if (forms.length === 3) {
|
|
91
|
-
// zero | one | many
|
|
92
|
-
if (count === 0) return forms[0];
|
|
93
|
-
if (count === 1) return forms[1];
|
|
94
|
-
return forms[2];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (forms.length === 2) {
|
|
98
|
-
// singular | plural
|
|
99
|
-
return count === 1 ? forms[0] : forms[1];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// More than 3 forms: use last form for "many"
|
|
103
|
-
if (count === 0 && forms.length > 0) return forms[0];
|
|
104
|
-
if (count === 1 && forms.length > 1) return forms[1];
|
|
105
|
-
return forms[forms.length - 1];
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Full translation pipeline: resolve → pluralize → interpolate.
|
|
110
|
-
*
|
|
111
|
-
* @param messages - Locale messages
|
|
112
|
-
* @param key - Dot-delimited key path
|
|
113
|
-
* @param params - Interpolation + pluralization params
|
|
114
|
-
* @param fallbackMessages - Optional fallback locale messages
|
|
115
|
-
* @returns The translated string, or the key if not found
|
|
116
|
-
*
|
|
117
|
-
* @internal
|
|
118
|
-
*/
|
|
119
|
-
export const translate = (
|
|
120
|
-
messages: LocaleMessages | undefined,
|
|
121
|
-
key: string,
|
|
122
|
-
params: TranslateParams,
|
|
123
|
-
fallbackMessages?: LocaleMessages
|
|
124
|
-
): string => {
|
|
125
|
-
let template: string | undefined;
|
|
126
|
-
|
|
127
|
-
// Try current locale
|
|
128
|
-
if (messages) {
|
|
129
|
-
template = resolveKey(messages, key);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Fallback locale
|
|
133
|
-
if (template === undefined && fallbackMessages) {
|
|
134
|
-
template = resolveKey(fallbackMessages, key);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Key not found — return key as-is
|
|
138
|
-
if (template === undefined) {
|
|
139
|
-
return key;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Pluralize first, then interpolate
|
|
143
|
-
const pluralized = pluralize(template, params);
|
|
144
|
-
return interpolate(pluralized, params);
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Deep merges source into target and returns a sanitized, prototype-safe copy.
|
|
149
|
-
*
|
|
150
|
-
* @param target - Target messages object
|
|
151
|
-
* @param source - Source messages to merge
|
|
152
|
-
* @returns A new merged, sanitized messages object
|
|
153
|
-
*
|
|
154
|
-
* @internal
|
|
155
|
-
*/
|
|
156
|
-
export const deepMerge = (target: LocaleMessages, source: LocaleMessages): LocaleMessages => {
|
|
157
|
-
const merged = merge(
|
|
158
|
-
target as Record<string, unknown>,
|
|
159
|
-
source as Record<string, unknown>
|
|
160
|
-
) as LocaleMessages;
|
|
161
|
-
|
|
162
|
-
const cloneSafeMessages = (value: unknown): unknown => {
|
|
163
|
-
if (Array.isArray(value)) {
|
|
164
|
-
return value.map((entry) => cloneSafeMessages(entry));
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (!isPlainObject(value)) {
|
|
168
|
-
return value;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const safeObject = Object.create(null) as Record<string, unknown>;
|
|
172
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
173
|
-
if (isPrototypePollutionKey(key)) {
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
safeObject[key] = cloneSafeMessages(entry);
|
|
177
|
-
}
|
|
178
|
-
return safeObject;
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
return cloneSafeMessages(merged) as LocaleMessages;
|
|
182
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Translation resolution helpers.
|
|
3
|
+
* @module bquery/i18n
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { isPlainObject, isPrototypePollutionKey, merge } from '../core/utils/object';
|
|
8
|
+
import type { LocaleMessages, TranslateParams } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolves a dot-delimited key path against a messages object.
|
|
12
|
+
*
|
|
13
|
+
* @param messages - The locale messages
|
|
14
|
+
* @param key - Dot-delimited key (e.g. 'user.welcome')
|
|
15
|
+
* @returns The resolved string, or `undefined` if not found
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export const resolveKey = (messages: LocaleMessages, key: string): string | undefined => {
|
|
20
|
+
const parts = key.split('.');
|
|
21
|
+
let current: LocaleMessages | string = messages;
|
|
22
|
+
|
|
23
|
+
for (const part of parts) {
|
|
24
|
+
if (typeof current === 'string') return undefined;
|
|
25
|
+
if (current[part] === undefined) return undefined;
|
|
26
|
+
current = current[part];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return typeof current === 'string' ? current : undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Interpolates `{param}` placeholders in a string.
|
|
34
|
+
*
|
|
35
|
+
* @param template - The template string with `{key}` placeholders
|
|
36
|
+
* @param params - Key-value pairs for replacement
|
|
37
|
+
* @returns The interpolated string
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* interpolate('Hello, {name}!', { name: 'Ada' });
|
|
42
|
+
* // → 'Hello, Ada!'
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
export const interpolate = (template: string, params: TranslateParams): string => {
|
|
48
|
+
return template.replace(/\{(\w+)\}/g, (match, key: string) => {
|
|
49
|
+
if (key in params) {
|
|
50
|
+
return String(params[key]);
|
|
51
|
+
}
|
|
52
|
+
return match; // Leave unmatched placeholders as-is
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Selects the correct plural form from a pipe-delimited string.
|
|
58
|
+
*
|
|
59
|
+
* Supports two formats:
|
|
60
|
+
* - **Two forms:** `"singular | plural"` — singular when count === 1
|
|
61
|
+
* - **Three forms:** `"zero | one | many"` — zero when count === 0,
|
|
62
|
+
* one when count === 1, many otherwise
|
|
63
|
+
*
|
|
64
|
+
* The `count` parameter must be present in `params` for pluralization.
|
|
65
|
+
* If no `count` param exists or the string has no pipes, the string is
|
|
66
|
+
* returned as-is.
|
|
67
|
+
*
|
|
68
|
+
* @param template - Pipe-delimited plural forms
|
|
69
|
+
* @param params - Must include a `count` key for plural selection
|
|
70
|
+
* @returns The selected form
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* pluralize('{count} item | {count} items', { count: 1 });
|
|
75
|
+
* // → '{count} item'
|
|
76
|
+
*
|
|
77
|
+
* pluralize('no items | {count} item | {count} items', { count: 0 });
|
|
78
|
+
* // → 'no items'
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
export const pluralize = (template: string, params: TranslateParams): string => {
|
|
84
|
+
if (!template.includes('|')) return template;
|
|
85
|
+
if (!('count' in params)) return template;
|
|
86
|
+
|
|
87
|
+
const count = Number(params.count);
|
|
88
|
+
const forms = template.split('|').map((s) => s.trim());
|
|
89
|
+
|
|
90
|
+
if (forms.length === 3) {
|
|
91
|
+
// zero | one | many
|
|
92
|
+
if (count === 0) return forms[0];
|
|
93
|
+
if (count === 1) return forms[1];
|
|
94
|
+
return forms[2];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (forms.length === 2) {
|
|
98
|
+
// singular | plural
|
|
99
|
+
return count === 1 ? forms[0] : forms[1];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// More than 3 forms: use last form for "many"
|
|
103
|
+
if (count === 0 && forms.length > 0) return forms[0];
|
|
104
|
+
if (count === 1 && forms.length > 1) return forms[1];
|
|
105
|
+
return forms[forms.length - 1];
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Full translation pipeline: resolve → pluralize → interpolate.
|
|
110
|
+
*
|
|
111
|
+
* @param messages - Locale messages
|
|
112
|
+
* @param key - Dot-delimited key path
|
|
113
|
+
* @param params - Interpolation + pluralization params
|
|
114
|
+
* @param fallbackMessages - Optional fallback locale messages
|
|
115
|
+
* @returns The translated string, or the key if not found
|
|
116
|
+
*
|
|
117
|
+
* @internal
|
|
118
|
+
*/
|
|
119
|
+
export const translate = (
|
|
120
|
+
messages: LocaleMessages | undefined,
|
|
121
|
+
key: string,
|
|
122
|
+
params: TranslateParams,
|
|
123
|
+
fallbackMessages?: LocaleMessages
|
|
124
|
+
): string => {
|
|
125
|
+
let template: string | undefined;
|
|
126
|
+
|
|
127
|
+
// Try current locale
|
|
128
|
+
if (messages) {
|
|
129
|
+
template = resolveKey(messages, key);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Fallback locale
|
|
133
|
+
if (template === undefined && fallbackMessages) {
|
|
134
|
+
template = resolveKey(fallbackMessages, key);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Key not found — return key as-is
|
|
138
|
+
if (template === undefined) {
|
|
139
|
+
return key;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Pluralize first, then interpolate
|
|
143
|
+
const pluralized = pluralize(template, params);
|
|
144
|
+
return interpolate(pluralized, params);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Deep merges source into target and returns a sanitized, prototype-safe copy.
|
|
149
|
+
*
|
|
150
|
+
* @param target - Target messages object
|
|
151
|
+
* @param source - Source messages to merge
|
|
152
|
+
* @returns A new merged, sanitized messages object
|
|
153
|
+
*
|
|
154
|
+
* @internal
|
|
155
|
+
*/
|
|
156
|
+
export const deepMerge = (target: LocaleMessages, source: LocaleMessages): LocaleMessages => {
|
|
157
|
+
const merged = merge(
|
|
158
|
+
target as Record<string, unknown>,
|
|
159
|
+
source as Record<string, unknown>
|
|
160
|
+
) as LocaleMessages;
|
|
161
|
+
|
|
162
|
+
const cloneSafeMessages = (value: unknown): unknown => {
|
|
163
|
+
if (Array.isArray(value)) {
|
|
164
|
+
return value.map((entry) => cloneSafeMessages(entry));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!isPlainObject(value)) {
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const safeObject = Object.create(null) as Record<string, unknown>;
|
|
172
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
173
|
+
if (isPrototypePollutionKey(key)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
safeObject[key] = cloneSafeMessages(entry);
|
|
177
|
+
}
|
|
178
|
+
return safeObject;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return cloneSafeMessages(merged) as LocaleMessages;
|
|
182
|
+
};
|