@vielzeug/i18nit 2.1.0 → 3.0.3

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/i18n.js CHANGED
@@ -1,181 +1,172 @@
1
- import { BoundedMap as e, deepMerge as t, isMessageValue as n, resolvePath as r } from "./helpers.js";
2
- import { formatDate as i, formatList as a, formatNumber as o, formatRelative as s, getPluralForm as c, makeIntlCaches as l } from "./intl.js";
3
- import { interpolate as u } from "./interpolate.js";
4
1
  //#region src/i18n.ts
5
- function d(d = {}) {
6
- let f = d.locale ?? "en", p = d.switchMode ?? "strict", m = Array.isArray(d.fallback) ? d.fallback : d.fallback ? [d.fallback] : [], h = /* @__PURE__ */ new Map(), g = /* @__PURE__ */ new Map(), _ = /* @__PURE__ */ new Map(), v = /* @__PURE__ */ new Set(), y = new e(128), b = l(), x = null, S = null, C = !1, w = 0, T = null, E = d.onMissing, D = d.onDiagnostic;
7
- if (d.messages) for (let [e, t] of Object.entries(d.messages)) h.set(e, structuredClone(t));
8
- if (d.loaders) for (let [e, t] of Object.entries(d.loaders)) g.set(e, t);
9
- function O(e) {
10
- D ? D({
11
- error: e,
12
- kind: "subscriber-error"
13
- }) : console.error("[i18nit] Subscriber threw:", e);
2
+ var e = /\{([\p{ID_Continue}\-.]+)\}/gu;
3
+ function t(e) {
4
+ try {
5
+ return Intl.getCanonicalLocales(e)[0] ?? e;
6
+ } catch {
7
+ return e;
14
8
  }
15
- function k(e, t) {
16
- D ? D({
17
- error: e,
18
- kind: "loader-error",
19
- locale: t
20
- }) : console.warn("[i18nit] Loader error:", e);
9
+ }
10
+ function n(e, t) {
11
+ let n = e;
12
+ for (let e of t.split(".")) {
13
+ if (typeof n != "object" || !n || !Object.hasOwn(n, e)) return;
14
+ n = n[e];
21
15
  }
22
- function A(e) {
23
- if (w > 0) {
24
- T !== "locale-change" && (T = e);
25
- return;
16
+ return n;
17
+ }
18
+ function r(e, t) {
19
+ let n = /* @__PURE__ */ new Set();
20
+ for (let r of [e, ...t]) {
21
+ n.add(r);
22
+ let e = r.split("-");
23
+ for (let t = e.length - 1; t > 0; t--) n.add(e.slice(0, t).join("-"));
24
+ }
25
+ return [...n];
26
+ }
27
+ function i(e, t, n, r) {
28
+ let i = `${t}:${r ? "ordinal" : "cardinal"}`, a = e.get(i);
29
+ if (!a) {
30
+ try {
31
+ a = new Intl.PluralRules(t, { type: r ? "ordinal" : "cardinal" });
32
+ } catch {
33
+ a = { select: (e) => e === 1 ? "one" : "other" };
26
34
  }
27
- let t = {
28
- locale: f,
29
- reason: e
35
+ e.set(i, a);
36
+ }
37
+ return a.select(n);
38
+ }
39
+ function a(t, r, i, a, o) {
40
+ return t.includes("{") ? t.replace(e, (e, t) => {
41
+ let s = r == null ? void 0 : n(r, t);
42
+ return s == null ? o({
43
+ key: i,
44
+ locale: a,
45
+ type: "var",
46
+ varName: t
47
+ }) : String(s);
48
+ }) : t;
49
+ }
50
+ function o(e) {
51
+ let o = e ?? {}, s = t(o.locale ?? "en"), c = Array.isArray(o.fallback) ? o.fallback.map(t) : o.fallback ? [t(o.fallback)] : [], l = /* @__PURE__ */ new Map(), u = /* @__PURE__ */ new Map(), d = /* @__PURE__ */ new Set(), f = /* @__PURE__ */ new Map(), p = o.onMissing ?? ((e) => e.type === "key" ? e.key : `{${e.varName}}`), m = o.onSubscriberError ?? (() => {}), h = 0, g = {
52
+ locale: s,
53
+ version: h
54
+ }, _ = r(s, c), v = 0, y = () => {
55
+ h++, g = {
56
+ locale: s,
57
+ version: h
30
58
  };
31
- for (let e of v) try {
32
- e(t);
59
+ let e = [...d];
60
+ for (let t of e) try {
61
+ t(g);
33
62
  } catch (e) {
34
- O(e);
63
+ m(e);
35
64
  }
36
- }
37
- function j(e) {
38
- let t = y.get(e);
39
- if (t) return t;
40
- let n = /* @__PURE__ */ new Set(), r = (e) => {
41
- n.add(e);
42
- let t = e.split("-");
43
- for (let e = t.length - 1; e > 0; e--) n.add(t.slice(0, e).join("-"));
44
- };
45
- r(e);
46
- for (let e of m) r(e);
47
- let i = [...n];
48
- return y.set(e, i), i;
49
- }
50
- function M(e, t) {
51
- let i = h.get(t);
52
- if (!i) return !1;
53
- let a = r(i, e);
54
- return a !== void 0 && n(a);
55
- }
56
- function N(e, t) {
57
- for (let i of j(t)) {
58
- let t = h.get(i);
59
- if (!t) continue;
60
- let a = r(t, e);
61
- if (a !== void 0 && n(a)) return a;
65
+ }, b = (e, t = !1) => {
66
+ if (d.add(e), t) try {
67
+ e(g);
68
+ } catch (e) {
69
+ m(e);
62
70
  }
63
- }
64
- function P(e, t, n) {
65
- let r = N(e, n);
66
- if (r === void 0) return E?.(e, n) ?? e;
67
- if (typeof r == "string") return u(r, t ?? {}, n, b);
68
- let i = t ?? {}, a = Number(i.count ?? 0);
69
- return u(r[a === 0 && r.zero !== void 0 ? "zero" : c(b, n, a)] ?? r.other, i, n, b);
70
- }
71
- function F(e, t) {
72
- if (_.has(e)) return _.get(e);
73
- if (h.has(e)) return Promise.resolve();
74
- let n = g.get(e);
75
- if (!n) return t === "strict" ? Promise.reject(/* @__PURE__ */ Error(`[i18nit] Missing loader for locale "${e}".`)) : Promise.resolve();
76
- let r = (async () => {
77
- try {
78
- let t = await n(e);
79
- C || R.replace(e, t);
80
- } catch (t) {
81
- throw k(t, e), t;
82
- } finally {
83
- _.delete(e);
84
- }
71
+ return () => d.delete(e);
72
+ }, x = (e) => {
73
+ for (let t of _) {
74
+ let r = l.get(t)?.messages;
75
+ if (!r) continue;
76
+ let i = n(r, e);
77
+ if (typeof i == "string") return i;
78
+ }
79
+ }, S = (e, t) => {
80
+ if (typeof t == "function") {
81
+ l.set(e, {
82
+ kind: "dynamic",
83
+ loader: t
84
+ });
85
+ return;
86
+ }
87
+ l.set(e, {
88
+ kind: "static",
89
+ messages: t
90
+ });
91
+ }, C = (e, n) => {
92
+ let r = t(e);
93
+ S(r, n), _.includes(r) && y();
94
+ };
95
+ if (o.catalogs) for (let [e, n] of Object.entries(o.catalogs)) S(t(e), n);
96
+ let w = async (e) => {
97
+ let n = t(e), r = l.get(n);
98
+ if (!r) throw Error(`Missing locale source for "${n}".`);
99
+ if (r.kind === "static" || r.messages) return;
100
+ let i = u.get(n);
101
+ if (i?.entry === r) {
102
+ await i.task;
103
+ return;
104
+ }
105
+ let a = (async () => {
106
+ let e = await r.loader(n);
107
+ l.get(n) === r && (r.messages = e, _.includes(n) && y());
85
108
  })();
86
- return _.set(e, r), r;
87
- }
88
- function I(e, t) {
89
- let n = () => e ?? f, r = (e) => t ? `${t}.${e}` : e;
90
- return {
91
- currency(e, t, r) {
92
- return o(b, e, {
93
- ...r,
94
- currency: t,
95
- style: "currency"
96
- }, n());
97
- },
98
- date(e, t) {
99
- return i(b, e, t, n());
100
- },
101
- has(e) {
102
- return N(r(e), n()) !== void 0;
103
- },
104
- hasOwn(e) {
105
- return M(r(e), n());
106
- },
107
- list(e, t = "and") {
108
- return a(b, e, n(), t);
109
- },
110
- get locale() {
111
- return n();
112
- },
113
- number(e, t) {
114
- return o(b, e, t, n());
115
- },
116
- relative(e, t, r) {
117
- return s(b, e, t, r, n());
118
- },
119
- scope(n) {
120
- return I(e, t ? `${t}.${String(n)}` : String(n));
121
- },
122
- t: (e, t) => P(r(e), t, n()),
123
- withLocale(e) {
124
- return I(e, t);
125
- }
126
- };
127
- }
128
- let L = I(null), R = Object.create(L);
129
- return R.add = (e, n) => {
130
- let r = h.get(e) ?? {};
131
- h.set(e, t(r, n)), x = null, j(f).includes(e) && A("catalog-update");
132
- }, R.batch = (e) => {
133
- w++;
109
+ u.set(n, {
110
+ entry: r,
111
+ task: a
112
+ });
134
113
  try {
135
- e();
114
+ await a;
136
115
  } finally {
137
- if (w--, w === 0 && T !== null) {
138
- let e = T;
139
- T = null, A(e);
140
- }
116
+ u.get(n)?.task === a && u.delete(n);
141
117
  }
142
- }, R.dispose = () => {
143
- C = !0, v.clear(), h.clear(), g.clear(), _.clear(), y.clear(), x = null, S = null;
144
- }, R.ensureLocale = async (e, t = p) => {
145
- await F(e, t);
146
- }, R.hasLocale = (e) => h.has(e), R.isReady = (e) => h.has(e), Object.defineProperties(R, {
147
- loadableLocales: { get() {
148
- return S ??= [...g.keys()], S;
149
- } },
150
- locales: { get() {
151
- return x ??= [...h.keys()], x;
152
- } }
153
- }), R.registerLoader = (e, t) => {
154
- g.set(e, t), S = null;
155
- }, R.reload = async (e) => {
156
- if (!g.has(e)) throw Error(`[i18nit] Cannot reload locale "${e}" without a registered loader.`);
157
- h.delete(e), x = null, await F(e, "strict");
158
- }, R.replace = (e, t) => {
159
- h.set(e, structuredClone(t)), x = null, j(f).includes(e) && A("catalog-update");
160
- }, R.subscribe = (e, t) => {
161
- if (v.add(e), t) try {
162
- e({
163
- locale: f,
164
- reason: "locale-change"
165
- });
166
- } catch (e) {
167
- O(e);
168
- }
169
- return () => v.delete(e);
170
- }, R.switchLocale = async (e, t = p) => {
171
- e !== f && (await F(e, t), f = e, A("locale-change"));
172
- }, R[Symbol.asyncDispose] = async () => {
173
- await Promise.allSettled([..._.values()]), R.dispose();
174
- }, R[Symbol.dispose] = () => {
175
- R.dispose();
176
- }, R;
118
+ };
119
+ function T(e, t) {
120
+ let n = String(e), r = x(n);
121
+ return r === void 0 ? p({
122
+ key: n,
123
+ locale: s,
124
+ type: "key"
125
+ }) : a(r, t, n, s, p);
126
+ }
127
+ function E(e, t, n) {
128
+ if (!Number.isFinite(t)) throw TypeError("`count` must be a finite number.");
129
+ if (n?.vars && Object.hasOwn(n.vars, "count")) throw Error("`tp` does not allow `vars.count`; `count` is injected automatically.");
130
+ let r = String(e), o = n?.ordinal === !0, c = i(f, s, t, o), l = x(!o && t === 0 ? `${r}.zero` : `${r}.${c}`) ?? x(`${r}.other`);
131
+ return l === void 0 ? p({
132
+ key: r,
133
+ locale: s,
134
+ type: "key"
135
+ }) : a(l, {
136
+ ...n?.vars ?? {},
137
+ count: t
138
+ }, r, s, p);
139
+ }
140
+ return {
141
+ getSnapshot() {
142
+ return g;
143
+ },
144
+ getSupportedLocales(e) {
145
+ let t = [...l.keys()];
146
+ return e?.sorted === !0 ? t.sort() : t;
147
+ },
148
+ has(e) {
149
+ return x(String(e)) !== void 0;
150
+ },
151
+ get locale() {
152
+ return s;
153
+ },
154
+ preload: w,
155
+ register: C,
156
+ async setLocale(e) {
157
+ let n = t(e);
158
+ if (s === n) return;
159
+ let i = ++v;
160
+ await w(n), i === v && (s = n, _ = r(s, c), y());
161
+ },
162
+ subscribe(e, t) {
163
+ return b(e, t?.immediate === !0);
164
+ },
165
+ t: T,
166
+ tp: E
167
+ };
177
168
  }
178
169
  //#endregion
179
- export { d as createI18n };
170
+ export { o as createI18n };
180
171
 
181
172
  //# sourceMappingURL=i18n.js.map
package/dist/i18n.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"i18n.js","names":[],"sources":["../src/i18n.ts"],"sourcesContent":["import type {\n BoundI18n,\n I18n,\n I18nOptions,\n Loader,\n Locale,\n LocaleChangeEvent,\n LocaleChangeReason,\n Messages,\n MessageValue,\n NamespaceKeys,\n SwitchMode,\n Unsubscribe,\n Vars,\n} from './types';\n\nimport { BoundedMap, deepMerge, isMessageValue, resolvePath } from './helpers';\nimport { interpolate } from './interpolate';\nimport {\n formatDate,\n formatList,\n formatNumber,\n formatRelative,\n getPluralForm,\n type IntlCaches,\n makeIntlCaches,\n} from './intl';\n\nexport function createI18n<T extends Messages = Messages>(config: I18nOptions<T> = {}): I18n<T> {\n let locale = config.locale ?? 'en';\n const defaultSwitchMode: SwitchMode = config.switchMode ?? 'strict';\n const fallbacks = Array.isArray(config.fallback) ? config.fallback : config.fallback ? [config.fallback] : [];\n const catalogs = new Map<Locale, Messages>();\n const loaders = new Map<Locale, Loader>();\n const loading = new Map<Locale, Promise<void>>();\n const subscribers = new Set<(event: LocaleChangeEvent) => void>();\n const chainCache = new BoundedMap<Locale, Locale[]>(128);\n const caches: IntlCaches = makeIntlCaches();\n\n let localesCache: Locale[] | null = null;\n let loadersCache: Locale[] | null = null;\n let disposed = false;\n let batchDepth = 0;\n let pendingNotify: LocaleChangeReason | null = null;\n\n const onMissing = config.onMissing;\n const onDiagnostic = config.onDiagnostic;\n\n if (config.messages) {\n for (const [loc, messages] of Object.entries(config.messages)) {\n catalogs.set(loc, structuredClone(messages) as Messages);\n }\n }\n\n if (config.loaders) {\n for (const [loc, loader] of Object.entries(config.loaders)) {\n loaders.set(loc, loader);\n }\n }\n\n function diagnoseSubscriber(error: unknown): void {\n if (onDiagnostic) {\n onDiagnostic({ error, kind: 'subscriber-error' });\n } else {\n console.error('[i18nit] Subscriber threw:', error);\n }\n }\n\n function diagnoseLoader(error: unknown, loc: Locale): void {\n if (onDiagnostic) {\n onDiagnostic({ error, kind: 'loader-error', locale: loc });\n } else {\n console.warn('[i18nit] Loader error:', error);\n }\n }\n\n function notify(reason: LocaleChangeReason): void {\n if (batchDepth > 0) {\n if (pendingNotify !== 'locale-change') pendingNotify = reason;\n\n return;\n }\n\n const event: LocaleChangeEvent = { locale, reason };\n\n for (const listener of subscribers) {\n try {\n listener(event);\n } catch (error) {\n diagnoseSubscriber(error);\n }\n }\n }\n\n function getLocaleChain(loc: Locale): Locale[] {\n const cached = chainCache.get(loc);\n\n if (cached) return cached;\n\n const seen = new Set<Locale>();\n\n const push = (value: Locale) => {\n seen.add(value);\n\n const parts = value.split('-');\n\n for (let i = parts.length - 1; i > 0; i--) {\n seen.add(parts.slice(0, i).join('-'));\n }\n };\n\n push(loc);\n for (const fallback of fallbacks) push(fallback);\n\n const chain = [...seen];\n\n chainCache.set(loc, chain);\n\n return chain;\n }\n\n function checkOwn(key: string, loc: Locale): boolean {\n const catalog = catalogs.get(loc);\n\n if (!catalog) return false;\n\n const value = resolvePath(catalog, key);\n\n return value !== undefined && isMessageValue(value);\n }\n\n function findMessage(key: string, loc: Locale): MessageValue | undefined {\n for (const localeInChain of getLocaleChain(loc)) {\n const messages = catalogs.get(localeInChain);\n\n if (!messages) continue;\n\n const value = resolvePath(messages, key);\n\n if (value !== undefined && isMessageValue(value)) return value;\n }\n\n return undefined;\n }\n\n function translate(key: string, vars: Vars | undefined, loc: Locale): string {\n const message = findMessage(key, loc);\n\n if (message === undefined) return onMissing?.(key, loc) ?? key;\n\n if (typeof message === 'string') {\n return interpolate(message, vars ?? {}, loc, caches);\n }\n\n const context = vars ?? {};\n\n if (import.meta.env?.DEV && context.count === undefined) {\n console.warn(`[i18nit] Key \"${key}\" is a plural message but vars.count is missing. Defaulting to 0.`);\n }\n\n const count = Number(context.count ?? 0);\n const form = count === 0 && message.zero !== undefined ? 'zero' : getPluralForm(caches, loc, count);\n\n return interpolate(message[form] ?? message.other, context, loc, caches);\n }\n\n function loadOne(loc: Locale, mode: SwitchMode): Promise<void> {\n if (loading.has(loc)) return loading.get(loc)!;\n\n if (catalogs.has(loc)) return Promise.resolve();\n\n const loader = loaders.get(loc);\n\n if (!loader) {\n if (mode === 'strict') {\n return Promise.reject(new Error(`[i18nit] Missing loader for locale \"${loc}\".`));\n }\n\n return Promise.resolve();\n }\n\n const promise = (async () => {\n try {\n const messages = await loader(loc);\n\n if (!disposed) api.replace(loc, messages);\n } catch (error) {\n diagnoseLoader(error, loc);\n throw error;\n } finally {\n loading.delete(loc);\n }\n })();\n\n loading.set(loc, promise);\n\n return promise;\n }\n\n function createView<U extends Messages = Messages>(fixedLocale: Locale | null, prefix?: string): BoundI18n<U> {\n const activeLocale = (): Locale => fixedLocale ?? locale;\n const keyWithPrefix = (key: string): string => (prefix ? `${prefix}.${key}` : key);\n const t: BoundI18n<U>['t'] = (key: NamespaceKeys<U>, vars?: Record<string, unknown>) =>\n translate(keyWithPrefix(key as string), vars, activeLocale());\n\n const view = {\n currency(\n value: number,\n currency: string,\n options?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>,\n ): string {\n return formatNumber(caches, value, { ...options, currency, style: 'currency' }, activeLocale());\n },\n date(value: Date | number, options?: Intl.DateTimeFormatOptions): string {\n return formatDate(caches, value, options, activeLocale());\n },\n has(key: string): boolean {\n return findMessage(keyWithPrefix(key), activeLocale()) !== undefined;\n },\n hasOwn(key: string): boolean {\n return checkOwn(keyWithPrefix(key), activeLocale());\n },\n list(items: unknown[], type: 'and' | 'or' = 'and'): string {\n return formatList(caches, items, activeLocale(), type);\n },\n get locale(): Locale {\n return activeLocale();\n },\n number(value: number, options?: Intl.NumberFormatOptions): string {\n return formatNumber(caches, value, options, activeLocale());\n },\n relative(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string {\n return formatRelative(caches, value, unit, options, activeLocale());\n },\n scope<K extends NamespaceKeys<U>>(ns: K): BoundI18n<U[K] & Messages> {\n const nextPrefix = prefix ? `${prefix}.${String(ns)}` : String(ns);\n\n return createView<U[K] & Messages>(fixedLocale, nextPrefix);\n },\n t,\n withLocale(nextLocale: Locale): BoundI18n<U> {\n return createView<U>(nextLocale, prefix);\n },\n } satisfies BoundI18n<U>;\n\n return view;\n }\n\n const rootView = createView<T>(null);\n\n const api = Object.create(rootView) as I18n<T>;\n\n api.add = (loc: Locale, messages: Messages): void => {\n const existing = catalogs.get(loc) ?? {};\n\n catalogs.set(loc, deepMerge(existing, messages));\n localesCache = null;\n\n if (getLocaleChain(locale).includes(loc)) notify('catalog-update');\n };\n\n api.batch = (fn: () => void): void => {\n batchDepth++;\n\n try {\n fn();\n } finally {\n batchDepth--;\n\n if (batchDepth === 0 && pendingNotify !== null) {\n const reason = pendingNotify;\n\n pendingNotify = null;\n notify(reason);\n }\n }\n };\n\n api.dispose = (): void => {\n disposed = true;\n subscribers.clear();\n catalogs.clear();\n loaders.clear();\n loading.clear();\n chainCache.clear();\n localesCache = null;\n loadersCache = null;\n };\n\n api.ensureLocale = async (loc: Locale, mode: SwitchMode = defaultSwitchMode): Promise<void> => {\n await loadOne(loc, mode);\n };\n\n api.hasLocale = (loc: Locale): boolean => catalogs.has(loc);\n api.isReady = (loc: Locale): boolean => catalogs.has(loc);\n\n Object.defineProperties(api, {\n loadableLocales: {\n get(): Locale[] {\n loadersCache ??= [...loaders.keys()];\n\n return loadersCache;\n },\n },\n locales: {\n get(): Locale[] {\n localesCache ??= [...catalogs.keys()];\n\n return localesCache;\n },\n },\n });\n\n api.registerLoader = (loc: Locale, loader: Loader): void => {\n loaders.set(loc, loader);\n loadersCache = null;\n };\n\n api.reload = async (loc: Locale): Promise<void> => {\n if (!loaders.has(loc)) {\n throw new Error(`[i18nit] Cannot reload locale \"${loc}\" without a registered loader.`);\n }\n\n catalogs.delete(loc);\n localesCache = null;\n await loadOne(loc, 'strict');\n };\n\n api.replace = (loc: Locale, messages: Messages): void => {\n catalogs.set(loc, structuredClone(messages));\n localesCache = null;\n\n if (getLocaleChain(locale).includes(loc)) notify('catalog-update');\n };\n\n api.subscribe = (listener: (event: LocaleChangeEvent) => void, immediate?: boolean): Unsubscribe => {\n subscribers.add(listener);\n\n if (immediate) {\n try {\n listener({ locale, reason: 'locale-change' });\n } catch (error) {\n diagnoseSubscriber(error);\n }\n }\n\n return () => subscribers.delete(listener);\n };\n\n api.switchLocale = async (nextLocale: Locale, mode: SwitchMode = defaultSwitchMode): Promise<void> => {\n if (nextLocale === locale) return;\n\n await loadOne(nextLocale, mode);\n locale = nextLocale;\n notify('locale-change');\n };\n\n api[Symbol.asyncDispose] = async (): Promise<void> => {\n await Promise.allSettled([...loading.values()]);\n api.dispose();\n };\n\n api[Symbol.dispose] = (): void => {\n api.dispose();\n };\n\n return api;\n}\n\nexport type { I18n };\n"],"mappings":"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"}
1
+ {"version":3,"file":"i18n.js","names":[],"sources":["../src/i18n.ts"],"sourcesContent":["import type {\n AnyKey,\n I18n,\n I18nOptions,\n I18nSnapshot,\n Loader,\n Locale,\n LocaleSource,\n MessageBranchKeys,\n MessageLeafKeys,\n Messages,\n MissingInfo,\n PluralTranslateOptions,\n SubscribeOptions,\n TranslateVars,\n Unsubscribe,\n} from './types';\n\ntype PluralRuleSelector = { select: (count: number) => string };\ntype PluralCaches = Map<string, PluralRuleSelector>;\ntype LocaleRecord<M extends Messages> =\n | { kind: 'dynamic'; loader: Loader<M>; messages?: M }\n | { kind: 'static'; messages: M };\ntype LoadingRecord<M extends Messages> = {\n entry: Extract<LocaleRecord<M>, { kind: 'dynamic' }>;\n task: Promise<void>;\n};\n\nconst INTERPOLATION_PATTERN = /\\{([\\p{ID_Continue}\\-.]+)\\}/gu;\n\nfunction canon(locale: string): string {\n try {\n return Intl.getCanonicalLocales(locale)[0] ?? locale;\n } catch {\n return locale;\n }\n}\n\nfunction resolvePath(obj: Record<string, unknown>, path: string): unknown {\n let value: unknown = obj;\n\n for (const part of path.split('.')) {\n if (value == null || typeof value !== 'object') return undefined;\n\n if (!Object.hasOwn(value as object, part)) return undefined;\n\n value = (value as Record<string, unknown>)[part];\n }\n\n return value;\n}\n\nfunction buildLocaleChain(locale: Locale, fallback: Locale[]): Locale[] {\n const seen = new Set<Locale>();\n\n for (const value of [locale, ...fallback]) {\n seen.add(value);\n\n const parts = value.split('-');\n\n for (let i = parts.length - 1; i > 0; i--) {\n seen.add(parts.slice(0, i).join('-'));\n }\n }\n\n return [...seen];\n}\n\nfunction selectPluralForm(cache: PluralCaches, locale: Locale, count: number, ordinal: boolean): string {\n const key = `${locale}:${ordinal ? 'ordinal' : 'cardinal'}`;\n let rules = cache.get(key);\n\n if (!rules) {\n try {\n rules = new Intl.PluralRules(locale, { type: ordinal ? 'ordinal' : 'cardinal' });\n } catch {\n rules = { select: (value: number) => (value === 1 ? 'one' : 'other') };\n }\n\n cache.set(key, rules);\n }\n\n return rules.select(count);\n}\n\nfunction interpolate(\n template: string,\n vars: TranslateVars | undefined,\n key: string,\n locale: Locale,\n onMissing: (info: MissingInfo) => string,\n): string {\n if (!template.includes('{')) return template;\n\n return template.replace(INTERPOLATION_PATTERN, (_match, varName: string) => {\n const value = vars != null ? resolvePath(vars, varName) : undefined;\n\n if (value == null) {\n return onMissing({ key, locale, type: 'var', varName });\n }\n\n return String(value);\n });\n}\n\n/** Overload: explicit type parameter (strict typing) */\nexport function createI18n<M extends Messages>(config: I18nOptions<M>): I18n<M>;\n/** Overload: no type parameter (loose typing, allows heterogeneous catalogs) */\nexport function createI18n(config?: I18nOptions<Messages>): I18n<Messages>;\nexport function createI18n<M extends Messages = Messages>(config?: I18nOptions<M>): I18n<M> {\n const cfg = (config ?? {}) as I18nOptions<M>;\n let locale = canon(cfg.locale ?? 'en');\n const fallback = Array.isArray(cfg.fallback) ? cfg.fallback.map(canon) : cfg.fallback ? [canon(cfg.fallback)] : [];\n\n const registry = new Map<Locale, LocaleRecord<M>>();\n const loading = new Map<Locale, LoadingRecord<M>>();\n const subscribers = new Set<(snapshot: I18nSnapshot) => void>();\n const pluralCache: PluralCaches = new Map();\n const onMissing =\n cfg.onMissing ??\n ((info: MissingInfo) => {\n if (info.type === 'key') return info.key;\n\n return `{${info.varName}}`;\n });\n const onSubscriberError = cfg.onSubscriberError ?? (() => {});\n\n let version = 0;\n let snapshot: I18nSnapshot = { locale, version };\n let activeChain = buildLocaleChain(locale, fallback);\n let switchId = 0;\n\n const bump = (): void => {\n version++;\n snapshot = { locale, version };\n\n const listeners = [...subscribers];\n\n for (const listener of listeners) {\n try {\n listener(snapshot);\n } catch (error) {\n onSubscriberError(error);\n }\n }\n };\n\n const addListener = (callback: (snapshot: I18nSnapshot) => void, immediate = false): Unsubscribe => {\n subscribers.add(callback);\n\n if (immediate) {\n try {\n callback(snapshot);\n } catch (error) {\n onSubscriberError(error);\n }\n }\n\n return () => subscribers.delete(callback);\n };\n\n const findMessage = (key: string): string | undefined => {\n for (const candidate of activeChain) {\n const messages = registry.get(candidate)?.messages;\n\n if (!messages) continue;\n\n const value = resolvePath(messages, key);\n\n if (typeof value === 'string') return value;\n }\n\n return undefined;\n };\n\n const setLocaleSource = (normalized: Locale, source: LocaleSource<M>): void => {\n if (typeof source === 'function') {\n registry.set(normalized, { kind: 'dynamic', loader: source as Loader<M> });\n\n return;\n }\n\n registry.set(normalized, { kind: 'static', messages: source as M });\n };\n\n const register = (loc: Locale, source: LocaleSource<M>): void => {\n const normalized = canon(loc);\n\n setLocaleSource(normalized, source);\n\n if (activeChain.includes(normalized)) bump();\n };\n\n if (cfg.catalogs) {\n for (const [loc, source] of Object.entries(cfg.catalogs)) {\n setLocaleSource(canon(loc), source as LocaleSource<M>);\n }\n }\n\n const preload = async (loc: Locale): Promise<void> => {\n const normalized = canon(loc);\n const entry = registry.get(normalized);\n\n if (!entry) {\n throw new Error(`Missing locale source for \"${normalized}\".`);\n }\n\n if (entry.kind === 'static') return;\n\n if (entry.messages) return;\n\n const active = loading.get(normalized);\n\n if (active?.entry === entry) {\n await active.task;\n\n return;\n }\n\n const task = (async () => {\n const messages = await entry.loader(normalized);\n\n // Only apply if the registry entry is still the exact object we started\n // loading from. Any call to register() replaces the entry with a new\n // object, so a stale loader result from a superseded source is discarded.\n if (registry.get(normalized) !== entry) return;\n\n entry.messages = messages;\n\n if (activeChain.includes(normalized)) bump();\n })();\n\n loading.set(normalized, { entry, task });\n\n try {\n await task;\n } finally {\n if (loading.get(normalized)?.task === task) {\n loading.delete(normalized);\n }\n }\n };\n\n function translate(key: MessageLeafKeys<M> | AnyKey, vars?: TranslateVars): string {\n const base = String(key);\n const message = findMessage(base);\n\n return message === undefined\n ? onMissing({ key: base, locale, type: 'key' })\n : interpolate(message, vars, base, locale, onMissing);\n }\n\n function translatePlural(\n key: MessageBranchKeys<M> | AnyKey,\n count: number,\n options?: PluralTranslateOptions,\n ): string {\n if (!Number.isFinite(count)) {\n throw new TypeError('`count` must be a finite number.');\n }\n\n if (options?.vars && Object.hasOwn(options.vars, 'count')) {\n throw new Error('`tp` does not allow `vars.count`; `count` is injected automatically.');\n }\n\n const base = String(key);\n const ordinal = options?.ordinal === true;\n const form = selectPluralForm(pluralCache, locale, count, ordinal);\n const selectedKey = !ordinal && count === 0 ? `${base}.zero` : `${base}.${form}`;\n const message = findMessage(selectedKey) ?? findMessage(`${base}.other`);\n\n if (message === undefined) {\n return onMissing({ key: base, locale, type: 'key' });\n }\n\n return interpolate(message, { ...(options?.vars ?? {}), count }, base, locale, onMissing);\n }\n\n return {\n getSnapshot() {\n return snapshot;\n },\n\n getSupportedLocales(options): Locale[] {\n const locales = [...registry.keys()];\n\n // Code-point sort: deterministic across all environments and locales.\n return options?.sorted === true ? locales.sort() : locales;\n },\n\n has(key: MessageLeafKeys<M> | AnyKey): boolean {\n return findMessage(String(key)) !== undefined;\n },\n\n get locale(): Locale {\n return locale;\n },\n\n preload,\n\n register,\n\n async setLocale(next: Locale): Promise<void> {\n const normalized = canon(next);\n\n if (locale === normalized) return;\n\n const id = ++switchId;\n\n await preload(normalized);\n\n if (id !== switchId) return;\n\n locale = normalized;\n activeChain = buildLocaleChain(locale, fallback);\n bump();\n },\n\n subscribe(callback: (snapshot: I18nSnapshot) => void, options?: SubscribeOptions): Unsubscribe {\n return addListener(callback, options?.immediate === true);\n },\n\n t: translate,\n tp: translatePlural,\n };\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** @vielzeug/i18nit — Lightweight, type-safe i18n library. */
2
- export type { BoundI18n, DeepPartialMessages, DiagnosticEvent, I18n, I18nOptions, Loader, Locale, LocaleChangeEvent, LocaleChangeListener, LocaleChangeReason, Messages, MessageValue, NamespaceKeys, PluralForm, PluralKeys, PluralMessages, SwitchMode, TranslationKey, TranslationKeyParam, Unsubscribe, Vars, } from './types';
1
+ /** @vielzeug/i18nit — Lightweight, type-safe i18n runtime. */
2
+ export type { AnyKey, I18n, I18nOptions, I18nSnapshot, Loader, Locale, LocaleSource, MessageBranchKeys, MessageLeafKeys, Messages, MissingInfo, PluralTranslateOptions, SupportedLocalesOptions, SubscribeOptions, TranslateVars, Unsubscribe, } from './types';
3
3
  export { createI18n } from './i18n';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAG9D,YAAY,EACV,SAAS,EACT,mBAAmB,EACnB,eAAe,EACf,IAAI,EACJ,WAAW,EACX,MAAM,EACN,MAAM,EACN,iBAAiB,EACjB,oBAAoB,EACpB,kBAAkB,EAClB,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,UAAU,EACV,UAAU,EACV,cAAc,EACd,UAAU,EACV,cAAc,EACd,mBAAmB,EACnB,WAAW,EACX,IAAI,GACL,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAE9D,YAAY,EACV,MAAM,EACN,IAAI,EACJ,WAAW,EACX,YAAY,EACZ,MAAM,EACN,MAAM,EACN,YAAY,EACZ,iBAAiB,EACjB,eAAe,EACf,QAAQ,EACR,WAAW,EACX,sBAAsB,EACtB,uBAAuB,EACvB,gBAAgB,EAChB,aAAa,EACb,WAAW,GACZ,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC"}
package/dist/types.d.ts CHANGED
@@ -1,117 +1,76 @@
1
1
  export type Locale = string;
2
2
  export type Unsubscribe = () => void;
3
- export type PluralForm = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
4
- export type PluralMessages = Partial<Record<PluralForm, string>> & {
5
- other: string;
3
+ export interface Messages {
4
+ [key: string]: string | Messages;
5
+ }
6
+ export type TranslateVars = Record<string, unknown>;
7
+ export type Loader<M extends Messages = Messages> = (locale: Locale) => Promise<M>;
8
+ export type LocaleSource<M extends Messages = Messages> = M | Loader<M>;
9
+ export type PluralTranslateOptions = {
10
+ ordinal?: boolean;
11
+ vars?: TranslateVars;
6
12
  };
7
- export type MessageValue = string | PluralMessages;
8
- export type Messages = {
9
- [key: string]: MessageValue | Messages;
13
+ export type SupportedLocalesOptions = {
14
+ sorted?: boolean;
10
15
  };
11
- export type Vars = Record<string, unknown>;
12
- /**
13
- * Recursively makes all message keys optional, allowing partial locale catalogs.
14
- * Use when a secondary locale only translates a subset of the primary locale's messages.
15
- */
16
- export type DeepPartialMessages<T extends Messages> = {
17
- [K in keyof T]?: T[K] extends MessageValue ? MessageValue : T[K] extends Messages ? DeepPartialMessages<T[K]> : MessageValue;
18
- };
19
- /**
20
- * Recursive type that extracts all dot-notation keys from a Messages shape for type-safe translation.
21
- * Type-safe resolution is provided up to 8 levels of nesting; deeper paths resolve to `string`.
22
- */
23
- export type TranslationKey<T extends Messages, P extends string = '', D extends readonly 0[] = []> = D['length'] extends 8 ? string : {
24
- [K in keyof T & string]: T[K] extends MessageValue ? P extends '' ? K : `${P}.${K}` : T[K] extends Messages ? TranslationKey<T[K], P extends '' ? K : `${P}.${K}`, [...D, 0]> : never;
25
- }[keyof T & string];
26
- /** The `key` parameter type for `t()` — enforces known dot-notation paths when `T` is concrete, allows any string otherwise. */
27
- export type TranslationKeyParam<T extends Messages> = [TranslationKey<T>] extends [never] ? string : TranslationKey<T> | (string & {});
28
- /** Extracts dot-notation keys from `T` whose final value is `PluralMessages`, for compile-time `count` enforcement. */
29
- export type PluralKeys<T extends Messages, P extends string = '', D extends readonly 0[] = []> = D['length'] extends 8 ? never : {
30
- [K in keyof T & string]: T[K] extends PluralMessages ? P extends '' ? K : `${P}.${K}` : T[K] extends Messages ? PluralKeys<T[K], P extends '' ? K : `${P}.${K}`, [...D, 0]> : never;
31
- }[keyof T & string];
32
- export type Loader = (locale: Locale) => Promise<Messages>;
33
- export type SwitchMode = 'strict' | 'best-effort';
34
- /** The reason a `subscribe()` listener was notified. */
35
- export type LocaleChangeReason = 'locale-change' | 'catalog-update';
36
- /** Payload passed to every `subscribe()` listener. */
37
- export type LocaleChangeEvent = {
16
+ export type MissingInfo = {
17
+ key: string;
38
18
  locale: Locale;
39
- reason: LocaleChangeReason;
40
- };
41
- /** Type alias for a `subscribe()` listener function. */
42
- export type LocaleChangeListener = (event: LocaleChangeEvent) => void;
43
- /**
44
- * Diagnostic event passed to `onDiagnostic`. Each `kind` carries relevant context:
45
- * - `'subscriber-error'` — a subscriber callback threw; treat as a programming error.
46
- * - `'loader-error'` — a locale loader rejected; treat as a recoverable I/O failure.
47
- */
48
- export type DiagnosticEvent = {
49
- error: unknown;
50
- kind: 'subscriber-error';
19
+ type: 'key';
51
20
  } | {
52
- error: unknown;
53
- kind: 'loader-error';
21
+ key: string;
54
22
  locale: Locale;
23
+ type: 'var';
24
+ varName: string;
25
+ };
26
+ export type I18nSnapshot = {
27
+ readonly locale: Locale;
28
+ readonly version: number;
55
29
  };
56
- /** Keys of `T` whose values are nested `Messages` objects (i.e. valid scope targets). */
57
- export type NamespaceKeys<T extends Messages> = string extends keyof T ? string : {
58
- [K in keyof T & string]: T[K] extends Messages ? K : never;
59
- }[keyof T & string];
60
- export type I18nOptions<T extends Messages = Messages> = {
30
+ export type AnyKey = string & {};
31
+ export type SubscribeOptions = {
32
+ immediate?: boolean;
33
+ };
34
+ type Depth = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8];
35
+ export type MessageLeafKeys<T, P extends string = '', D extends number = 7> = [D] extends [0] ? never : T extends string ? P : T extends Record<string, unknown> ? {
36
+ [K in string & keyof T]: MessageLeafKeys<T[K], P extends '' ? K : `${P}.${K}`, Depth[D]>;
37
+ }[string & keyof T] : never;
38
+ export type MessageBranchKeys<T, P extends string = '', D extends number = 7> = [D] extends [0] ? never : T extends Record<string, unknown> ? {
39
+ [K in string & keyof T]: T[K] extends string ? never : (P extends '' ? K : `${P}.${K}`) | MessageBranchKeys<T[K], P extends '' ? K : `${P}.${K}`, Depth[D]>;
40
+ }[string & keyof T] : never;
41
+ /**
42
+ * Configuration for i18n instances.
43
+ * When `M` is not explicitly provided, it defaults to the broad `Messages` type,
44
+ * allowing heterogeneous catalogs. When `M` is explicit, strict type checking applies.
45
+ */
46
+ export type I18nOptions<M extends Messages = Messages> = {
47
+ /** Locale registry. Values can be static messages or async loaders. */
48
+ catalogs?: Record<Locale, LocaleSource<M>>;
61
49
  fallback?: Locale | Locale[];
62
- loaders?: Record<Locale, Loader>;
63
50
  locale?: Locale;
64
- /**
65
- * Static message bundles. Each locale can be a full or partial catalog.
66
- * For a partial secondary locale, annotate it with `DeepPartialMessages<M>` to get compile-time
67
- * checks that the subset matches the primary locale's shape.
68
- */
69
- messages?: Record<string, T | DeepPartialMessages<T>>;
70
- /**
71
- * Receives diagnostic events for both subscriber errors and loader failures.
72
- * `'subscriber-error'` events indicate a programming error in a listener.
73
- * `'loader-error'` events are recoverable I/O failures and include the failing `locale`.
74
- * Defaults to `console.error` for subscriber errors and `console.warn` for loader errors.
75
- */
76
- onDiagnostic?: (event: DiagnosticEvent) => void;
77
- onMissing?: (key: string, locale: Locale) => string | undefined;
78
- /** Default mode for `ensureLocale()` and `switchLocale()`. */
79
- switchMode?: SwitchMode;
51
+ onMissing?: (info: MissingInfo) => string;
52
+ onSubscriberError?: (error: unknown) => void;
80
53
  };
81
- export type BoundI18n<T extends Messages = Messages> = {
82
- currency(value: number, currency: string, options?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>): string;
83
- date(value: Date | number, options?: Intl.DateTimeFormatOptions): string;
84
- has(key: string): boolean;
85
- /** Like `has()`, but only checks the exact locale without walking the fallback chain. */
86
- hasOwn(key: string): boolean;
87
- list(items: unknown[], type?: 'and' | 'or'): string;
54
+ export type I18n<M extends Messages = Messages> = {
55
+ /** Snapshot version starts at 0 and increments by 1 per observable change. */
56
+ getSnapshot(): I18nSnapshot;
57
+ getSupportedLocales(options?: SupportedLocalesOptions): Locale[];
58
+ /** Check if a key exists. Accepts literal keys from M, or any dynamic string. */
59
+ has(key: MessageLeafKeys<M> | AnyKey): boolean;
88
60
  readonly locale: Locale;
89
- number(value: number, options?: Intl.NumberFormatOptions): string;
90
- relative(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string;
91
- /** Returns a translator scoped to a namespace key. Only keys whose values are nested message objects are valid. */
92
- scope<K extends NamespaceKeys<T>>(ns: K): BoundI18n<T[K] & Messages>;
93
- /** Translates a plural key — requires `{ count: number }` when `T` is concrete and the key resolves to `PluralMessages`. */
94
- t<K extends PluralKeys<T>>(key: K, vars: {
95
- count: number;
96
- } & Vars): string;
97
- t(key: TranslationKeyParam<T>, vars?: Vars): string;
98
- withLocale(locale: Locale): BoundI18n<T>;
99
- };
100
- export type I18n<T extends Messages = Messages> = BoundI18n<T> & {
101
- [Symbol.asyncDispose](): Promise<void>;
102
- [Symbol.dispose](): void;
103
- add(locale: Locale, messages: Messages): void;
104
- batch(fn: () => void): void;
105
- dispose(): void;
106
- ensureLocale(locale: Locale, mode?: SwitchMode): Promise<void>;
107
- hasLocale(locale: Locale): boolean;
108
- isReady(locale: Locale): boolean;
109
- readonly loadableLocales: Locale[];
110
- readonly locales: Locale[];
111
- registerLoader(locale: Locale, loader: Loader): void;
112
- reload(locale: Locale): Promise<void>;
113
- replace(locale: Locale, messages: Messages): void;
114
- subscribe(listener: (event: LocaleChangeEvent) => void, immediate?: boolean): Unsubscribe;
115
- switchLocale(locale: Locale, mode?: SwitchMode): Promise<void>;
61
+ preload(locale: Locale): Promise<void>;
62
+ register(locale: Locale, source: LocaleSource<M>): void;
63
+ setLocale(locale: Locale): Promise<void>;
64
+ /**
65
+ * Subscribes to locale/catalog changes.
66
+ * - Default: callback runs only on changes.
67
+ * - `{ immediate: true }`: callback runs immediately with current snapshot and on every change.
68
+ */
69
+ subscribe(callback: (snapshot: I18nSnapshot) => void, options?: SubscribeOptions): Unsubscribe;
70
+ /** Get a translation. Accepts literal keys from M, or any dynamic string. */
71
+ t(key: MessageLeafKeys<M> | AnyKey, vars?: TranslateVars): string;
72
+ /** Get a pluralized translation from branch keys. */
73
+ tp(key: MessageBranchKeys<M> | AnyKey, count: number, options?: PluralTranslateOptions): string;
116
74
  };
75
+ export {};
117
76
  //# sourceMappingURL=types.d.ts.map