@moraya/core 0.1.0 → 0.3.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/dist/i18n/index.d.ts +231 -0
- package/dist/i18n/index.js +301 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/locales/ar.json +3873 -0
- package/dist/i18n/locales/de.json +3873 -0
- package/dist/i18n/locales/en.json +3943 -0
- package/dist/i18n/locales/es.json +3871 -0
- package/dist/i18n/locales/fr.json +3871 -0
- package/dist/i18n/locales/hi.json +3873 -0
- package/dist/i18n/locales/ja.json +3873 -0
- package/dist/i18n/locales/ko.json +3873 -0
- package/dist/i18n/locales/pt.json +3873 -0
- package/dist/i18n/locales/ru.json +3873 -0
- package/dist/i18n/locales/zh-CN.json +3943 -0
- package/dist/i18n/locales/zh-Hant.json +3873 -0
- package/package.json +7 -2
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared i18n type surface.
|
|
3
|
+
*
|
|
4
|
+
* This module MUST stay free of any framework dependency (no svelte, no react,
|
|
5
|
+
* no prosemirror, no markdown-it). It is the contract that consumers — moraya
|
|
6
|
+
* desktop, moraya-web, future @moraya/i18n extraction — all agree on.
|
|
7
|
+
*/
|
|
8
|
+
type SupportedLocale = 'en' | 'zh-CN' | 'zh-Hant' | 'ar' | 'de' | 'es' | 'fr' | 'hi' | 'ja' | 'ko' | 'pt' | 'ru';
|
|
9
|
+
/** Selection a user can make in UI; `'system'` means follow OS / browser locale. */
|
|
10
|
+
type LocaleSelection = SupportedLocale | 'system';
|
|
11
|
+
interface LocaleOption {
|
|
12
|
+
code: LocaleSelection;
|
|
13
|
+
/** Native-script label, shown unchanged regardless of active UI locale. */
|
|
14
|
+
label: string;
|
|
15
|
+
}
|
|
16
|
+
/** All 12 locales we support today, in label-alphabetical order. */
|
|
17
|
+
declare const SUPPORTED_LOCALES: LocaleOption[];
|
|
18
|
+
/** Locales that render right-to-left. */
|
|
19
|
+
declare const RTL_LOCALES: readonly SupportedLocale[];
|
|
20
|
+
/** A nested locale message bundle as authored in `locales/<loc>.json`. */
|
|
21
|
+
type MessageBundle = {
|
|
22
|
+
[k: string]: string | MessageBundle;
|
|
23
|
+
};
|
|
24
|
+
/** Flat key → string form, produced by `flattenMessages`. */
|
|
25
|
+
type FlatMessages = Record<string, string>;
|
|
26
|
+
/**
|
|
27
|
+
* Persistence callbacks for `initLocale`. Each consumer wires these to its
|
|
28
|
+
* host environment — Tauri plugin-store for desktop, localStorage for web.
|
|
29
|
+
* Pass `undefined` to skip persistence (locale resets to detected on each load).
|
|
30
|
+
*/
|
|
31
|
+
interface PersistenceAdapter {
|
|
32
|
+
get(): LocaleSelection | null | Promise<LocaleSelection | null>;
|
|
33
|
+
set(selection: LocaleSelection): void | Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Minimal pub/sub primitive compatible with Svelte's `Readable<T>` contract
|
|
38
|
+
* (`{ subscribe(run) => unsub }`) — without importing svelte/store.
|
|
39
|
+
*
|
|
40
|
+
* Why a custom primitive: this package must stay framework-agnostic so it
|
|
41
|
+
* can be lifted to a standalone `@moraya/i18n` package later without
|
|
42
|
+
* dragging a svelte peer-dep along. Consumers that DO use Svelte can pass
|
|
43
|
+
* the returned store straight into `derived()` from `svelte/store` because
|
|
44
|
+
* the subscribe signature matches.
|
|
45
|
+
*/
|
|
46
|
+
type Subscriber<T> = (value: T) => void;
|
|
47
|
+
type Unsubscriber = () => void;
|
|
48
|
+
interface Readable<T> {
|
|
49
|
+
subscribe(run: Subscriber<T>): Unsubscriber;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Locale detection helpers. Pure / framework-agnostic — uses only standard
|
|
54
|
+
* browser APIs (`navigator.language`) with defensive guards for SSR / Node /
|
|
55
|
+
* environments where `navigator` is absent.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Best-effort guess of the user's preferred locale from the browser /
|
|
60
|
+
* navigator. Returns `'en'` if no match (also when `navigator` is unavailable
|
|
61
|
+
* — SSR, Node, Worker). Chinese is split into `zh-CN` (Simplified) vs
|
|
62
|
+
* `zh-Hant` (Traditional, including TW/HK/MO).
|
|
63
|
+
*/
|
|
64
|
+
declare function detectSystemLocale(): SupportedLocale;
|
|
65
|
+
/** Whether a locale renders right-to-left. */
|
|
66
|
+
declare function isRTL(loc: SupportedLocale): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Apply the locale's writing direction to `document.documentElement.dir`.
|
|
69
|
+
* No-op if `document` is unavailable (SSR / Worker / Node).
|
|
70
|
+
*/
|
|
71
|
+
declare function applyDocumentDirection(loc: SupportedLocale): void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Pure message helpers — flatten / interpolate / merge / lookup. No side
|
|
75
|
+
* effects, no I/O, no global state. Used by `index.ts` to back the active
|
|
76
|
+
* locale and by `ai-utils.ts` for cross-locale lookups.
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Recursively flatten a nested bundle into a flat `'a.b.c' → string` map.
|
|
81
|
+
* Non-string leaves (numbers, booleans) are coerced to `String(v)`; non-leaf
|
|
82
|
+
* non-objects (e.g. arrays) are dropped silently — i18n payloads should be
|
|
83
|
+
* pure nested string maps.
|
|
84
|
+
*/
|
|
85
|
+
declare function flattenMessages(obj: unknown, prefix?: string): FlatMessages;
|
|
86
|
+
/**
|
|
87
|
+
* Interpolate `{name}` placeholders in a message template.
|
|
88
|
+
*
|
|
89
|
+
* - Unknown placeholders pass through unchanged: `{notInVars}` stays literal.
|
|
90
|
+
* This mirrors the existing moraya & moraya-web behaviour so callers that
|
|
91
|
+
* embed literal `{x}` in copy don't break.
|
|
92
|
+
*/
|
|
93
|
+
declare function interpolate(template: string, vars?: Record<string, string>): string;
|
|
94
|
+
/**
|
|
95
|
+
* Look up `key` in `messages` with a fallback table. Returns:
|
|
96
|
+
* 1. `messages[key]` if present
|
|
97
|
+
* 2. `fallback[key]` if present
|
|
98
|
+
* 3. the literal key itself (debug-friendly — missing keys are visible)
|
|
99
|
+
*
|
|
100
|
+
* `key` is the dot-joined path produced by `flattenMessages`.
|
|
101
|
+
*/
|
|
102
|
+
declare function lookup(key: string, messages: FlatMessages, fallback?: FlatMessages): string;
|
|
103
|
+
/**
|
|
104
|
+
* Deep merge two nested bundles. Right side wins on leaf conflict. Used when
|
|
105
|
+
* a host wants to overlay app-specific strings on top of the shipped bundle
|
|
106
|
+
* without forking the JSON. NOT used by the dynamic locale loader (which
|
|
107
|
+
* just replaces the active bundle wholesale).
|
|
108
|
+
*/
|
|
109
|
+
declare function mergeBundles(base: MessageBundle, overlay: MessageBundle): MessageBundle;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Force every supported locale's bundle into the cache. Call once at boot if
|
|
113
|
+
* the app expects to use `resolveForLocale` / `resolveAllLocales` sync.
|
|
114
|
+
*/
|
|
115
|
+
declare function preloadAllLocales(): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Sync lookup against a specific (non-active) locale's bundle. Returns the
|
|
118
|
+
* key itself if the locale isn't loaded yet — call `preloadAllLocales()` (or
|
|
119
|
+
* `loadBundle(loc)`) first if you need real translations.
|
|
120
|
+
*/
|
|
121
|
+
declare function resolveForLocale(key: string, loc: SupportedLocale, vars?: Record<string, string>): string;
|
|
122
|
+
/**
|
|
123
|
+
* Async variant: ensures the requested locale is loaded before resolving.
|
|
124
|
+
* Preferred when the caller is already in an async path (workflow steps,
|
|
125
|
+
* AI prompt generation, etc.).
|
|
126
|
+
*/
|
|
127
|
+
declare function resolveForLocaleAsync(key: string, loc: SupportedLocale, vars?: Record<string, string>): Promise<string>;
|
|
128
|
+
/**
|
|
129
|
+
* Sync: returns one translation per supported locale, in `ALL_LOCALES` order.
|
|
130
|
+
* Locales whose bundle isn't loaded yet contribute the key itself — meaning a
|
|
131
|
+
* pre-load is essential before relying on this for pattern matching.
|
|
132
|
+
*
|
|
133
|
+
* Use case: matching user-generated speaker names like "Passerby 1" /
|
|
134
|
+
* "路人 1" / "通行人 1" across 12 locales — the caller takes the union of
|
|
135
|
+
* all returned strings as a regex alternation source.
|
|
136
|
+
*/
|
|
137
|
+
declare function resolveAllLocales(key: string, vars?: Record<string, string>): string[];
|
|
138
|
+
/** Async variant — loads every locale first, then resolves. */
|
|
139
|
+
declare function resolveAllLocalesAsync(key: string, vars?: Record<string, string>): Promise<string[]>;
|
|
140
|
+
/**
|
|
141
|
+
* Escape hatch for tests / advanced consumers: returns the cached nested
|
|
142
|
+
* bundle for a locale (read-only). Returns `undefined` if not loaded.
|
|
143
|
+
* Most callers should NOT need this — prefer `resolveForLocale`.
|
|
144
|
+
*/
|
|
145
|
+
declare function getNestedBundle(loc: SupportedLocale): MessageBundle | undefined;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Dynamic locale-bundle loader with module-scoped cache.
|
|
149
|
+
*
|
|
150
|
+
* Each `loadBundle(loc)` call returns the same flat-messages map on repeat
|
|
151
|
+
* calls (memoized). The underlying `import('./locales/<loc>.json')` is what
|
|
152
|
+
* tsup / vite turn into a code-split chunk — only locales the user actually
|
|
153
|
+
* picks reach the consumer's bundle.
|
|
154
|
+
*
|
|
155
|
+
* Test injection: `__setLoader(fn)` lets unit tests swap the dynamic-import
|
|
156
|
+
* function with a synchronous fixture-returning stub.
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
type LoaderFn = (loc: SupportedLocale) => Promise<MessageBundle>;
|
|
160
|
+
/**
|
|
161
|
+
* Resolve and cache a locale's flat-messages bundle. Subsequent calls return
|
|
162
|
+
* the cached map without re-importing.
|
|
163
|
+
*/
|
|
164
|
+
declare function loadBundle(loc: SupportedLocale): Promise<FlatMessages>;
|
|
165
|
+
/** Synchronous cache read — returns `undefined` if the locale isn't loaded yet. */
|
|
166
|
+
declare function peekBundle(loc: SupportedLocale): FlatMessages | undefined;
|
|
167
|
+
/** Preload several locales in parallel. Returns once all are cached. */
|
|
168
|
+
declare function preloadBundles(locales: readonly SupportedLocale[]): Promise<void>;
|
|
169
|
+
/** @internal — for unit tests only. */
|
|
170
|
+
declare function __setLoader(fn: LoaderFn | null): void;
|
|
171
|
+
/** @internal — for unit tests only. */
|
|
172
|
+
declare function __resetCache(): void;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Public API for the Moraya unified i18n module.
|
|
176
|
+
*
|
|
177
|
+
* Imported as `@moraya/core/i18n` once `package.json` / `tsup.config.ts` are
|
|
178
|
+
* wired (Phase 1c). Designed to be lifted into a standalone `@moraya/i18n`
|
|
179
|
+
* package with a path-only rename — see plan v0.96.0.
|
|
180
|
+
*
|
|
181
|
+
* Framework coupling: NONE in this module. The `locale` export is a custom
|
|
182
|
+
* `Readable<T>` whose `subscribe` signature is compatible with Svelte's
|
|
183
|
+
* `Readable<T>`, so a Svelte consumer can pass it straight into `derived()`
|
|
184
|
+
* without wrapping. See `consumer-svelte-shim` example in the iteration doc.
|
|
185
|
+
*/
|
|
186
|
+
|
|
187
|
+
/** Read-only view of the active locale. Compatible with Svelte's `Readable`. */
|
|
188
|
+
declare const locale: Readable<SupportedLocale>;
|
|
189
|
+
/**
|
|
190
|
+
* Translate a flat dot-joined key. Falls back to English, then to the literal
|
|
191
|
+
* key (so missing translations are visible in dev).
|
|
192
|
+
*/
|
|
193
|
+
declare function t(key: string, vars?: Record<string, string>): string;
|
|
194
|
+
/**
|
|
195
|
+
* Switch the active locale. Loads the bundle on demand (memoized), updates
|
|
196
|
+
* the active-messages map, applies document direction (for RTL), and notifies
|
|
197
|
+
* subscribers of the `locale` store.
|
|
198
|
+
*
|
|
199
|
+
* If the bundle fails to load (e.g. network error in a future remote-locale
|
|
200
|
+
* mode), the active locale is updated anyway so the UI doesn't get stuck —
|
|
201
|
+
* but translations will fall through to English / key.
|
|
202
|
+
*/
|
|
203
|
+
declare function setLocale(loc: SupportedLocale): Promise<void>;
|
|
204
|
+
/**
|
|
205
|
+
* Options for `initLocale`. All fields are optional:
|
|
206
|
+
* - `preferred`: explicit user choice. Wins over persisted + detected.
|
|
207
|
+
* - `persistence`: read/write hooks for remembering the user's choice.
|
|
208
|
+
* Pass localStorage-backed callbacks on web, Tauri plugin-store on PC.
|
|
209
|
+
*/
|
|
210
|
+
interface InitLocaleOptions {
|
|
211
|
+
preferred?: LocaleSelection | null;
|
|
212
|
+
persistence?: PersistenceAdapter;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Bootstrap the i18n module: resolve which locale to use, load its bundle,
|
|
216
|
+
* set it active. Resolution order:
|
|
217
|
+
*
|
|
218
|
+
* 1. `opts.preferred` if it names a concrete locale (not `'system'`)
|
|
219
|
+
* 2. `opts.persistence.get()` if it returns a concrete locale
|
|
220
|
+
* 3. `detectSystemLocale()` (navigator-based fallback)
|
|
221
|
+
*
|
|
222
|
+
* The chosen locale is `await setLocale(...)` so the active messages are
|
|
223
|
+
* loaded before this Promise resolves. Safe to call multiple times — each
|
|
224
|
+
* call simply re-runs the resolution.
|
|
225
|
+
*/
|
|
226
|
+
declare function initLocale(opts?: InitLocaleOptions): Promise<void>;
|
|
227
|
+
|
|
228
|
+
/** @internal — reset the active-locale state. Tests only. */
|
|
229
|
+
declare function __resetState(): void;
|
|
230
|
+
|
|
231
|
+
export { type FlatMessages, type InitLocaleOptions, type LocaleOption, type LocaleSelection, type MessageBundle, type PersistenceAdapter, RTL_LOCALES, type Readable, SUPPORTED_LOCALES, type Subscriber, type SupportedLocale, type Unsubscriber, __resetCache, __resetState, __setLoader, applyDocumentDirection, detectSystemLocale, flattenMessages, getNestedBundle, initLocale, interpolate, isRTL, loadBundle, locale, lookup, mergeBundles, peekBundle, preloadAllLocales, preloadBundles, resolveAllLocales, resolveAllLocalesAsync, resolveForLocale, resolveForLocaleAsync, setLocale, t };
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// src/i18n/messages.ts
|
|
2
|
+
function flattenMessages(obj, prefix = "") {
|
|
3
|
+
if (typeof obj !== "object" || obj === null) return {};
|
|
4
|
+
const out = {};
|
|
5
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
6
|
+
const key = prefix ? `${prefix}.${k}` : k;
|
|
7
|
+
if (typeof v === "string") {
|
|
8
|
+
out[key] = v;
|
|
9
|
+
} else if (typeof v === "number" || typeof v === "boolean") {
|
|
10
|
+
out[key] = String(v);
|
|
11
|
+
} else if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
12
|
+
Object.assign(out, flattenMessages(v, key));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
function interpolate(template, vars) {
|
|
18
|
+
if (!vars) return template;
|
|
19
|
+
return template.replace(/\{(\w+)\}/g, (m, k) => {
|
|
20
|
+
const v = vars[k];
|
|
21
|
+
return v === void 0 ? m : v;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function lookup(key, messages, fallback) {
|
|
25
|
+
const direct = messages[key];
|
|
26
|
+
if (direct !== void 0) return direct;
|
|
27
|
+
if (fallback) {
|
|
28
|
+
const fb = fallback[key];
|
|
29
|
+
if (fb !== void 0) return fb;
|
|
30
|
+
}
|
|
31
|
+
return key;
|
|
32
|
+
}
|
|
33
|
+
function mergeBundles(base, overlay) {
|
|
34
|
+
const out = { ...base };
|
|
35
|
+
for (const [k, v] of Object.entries(overlay)) {
|
|
36
|
+
const existing = out[k];
|
|
37
|
+
if (v && typeof v === "object" && !Array.isArray(v) && existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
38
|
+
out[k] = mergeBundles(existing, v);
|
|
39
|
+
} else {
|
|
40
|
+
out[k] = v;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/i18n/types.ts
|
|
47
|
+
var SUPPORTED_LOCALES = [
|
|
48
|
+
{ code: "system", label: "System" },
|
|
49
|
+
{ code: "ar", label: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629" },
|
|
50
|
+
{ code: "de", label: "Deutsch" },
|
|
51
|
+
{ code: "en", label: "English" },
|
|
52
|
+
{ code: "es", label: "Espa\xF1ol" },
|
|
53
|
+
{ code: "fr", label: "Fran\xE7ais" },
|
|
54
|
+
{ code: "hi", label: "\u0939\u093F\u0928\u094D\u0926\u0940" },
|
|
55
|
+
{ code: "ja", label: "\u65E5\u672C\u8A9E" },
|
|
56
|
+
{ code: "ko", label: "\uD55C\uAD6D\uC5B4" },
|
|
57
|
+
{ code: "pt", label: "Portugu\xEAs" },
|
|
58
|
+
{ code: "ru", label: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439" },
|
|
59
|
+
{ code: "zh-CN", label: "\u7B80\u4F53\u4E2D\u6587" },
|
|
60
|
+
{ code: "zh-Hant", label: "\u7E41\u9AD4\u4E2D\u6587" }
|
|
61
|
+
];
|
|
62
|
+
var RTL_LOCALES = ["ar"];
|
|
63
|
+
|
|
64
|
+
// src/i18n/detect.ts
|
|
65
|
+
var PREFIX_MAP = [
|
|
66
|
+
["ja", "ja"],
|
|
67
|
+
["ko", "ko"],
|
|
68
|
+
["ar", "ar"],
|
|
69
|
+
["hi", "hi"],
|
|
70
|
+
["ru", "ru"],
|
|
71
|
+
["de", "de"],
|
|
72
|
+
["fr", "fr"],
|
|
73
|
+
["es", "es"],
|
|
74
|
+
["pt", "pt"]
|
|
75
|
+
];
|
|
76
|
+
function detectSystemLocale() {
|
|
77
|
+
try {
|
|
78
|
+
const nav = globalThis.navigator;
|
|
79
|
+
if (!nav) return "en";
|
|
80
|
+
const lang = nav.language ?? nav.languages?.[0] ?? "en";
|
|
81
|
+
if (lang.startsWith("zh")) {
|
|
82
|
+
if (/zh-(TW|HK|MO|Hant)/i.test(lang)) return "zh-Hant";
|
|
83
|
+
return "zh-CN";
|
|
84
|
+
}
|
|
85
|
+
for (const [prefix, locale2] of PREFIX_MAP) {
|
|
86
|
+
if (lang.startsWith(prefix)) return locale2;
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
return "en";
|
|
91
|
+
}
|
|
92
|
+
function isRTL(loc) {
|
|
93
|
+
return RTL_LOCALES.includes(loc);
|
|
94
|
+
}
|
|
95
|
+
function applyDocumentDirection(loc) {
|
|
96
|
+
try {
|
|
97
|
+
const doc = globalThis.document;
|
|
98
|
+
if (doc) doc.documentElement.dir = isRTL(loc) ? "rtl" : "ltr";
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/i18n/loader.ts
|
|
104
|
+
var defaultLoader = async (loc) => {
|
|
105
|
+
switch (loc) {
|
|
106
|
+
case "en":
|
|
107
|
+
return (await import("./locales/en.json")).default;
|
|
108
|
+
case "zh-CN":
|
|
109
|
+
return (await import("./locales/zh-CN.json")).default;
|
|
110
|
+
case "zh-Hant":
|
|
111
|
+
return (await import("./locales/zh-Hant.json")).default;
|
|
112
|
+
case "ar":
|
|
113
|
+
return (await import("./locales/ar.json")).default;
|
|
114
|
+
case "de":
|
|
115
|
+
return (await import("./locales/de.json")).default;
|
|
116
|
+
case "es":
|
|
117
|
+
return (await import("./locales/es.json")).default;
|
|
118
|
+
case "fr":
|
|
119
|
+
return (await import("./locales/fr.json")).default;
|
|
120
|
+
case "hi":
|
|
121
|
+
return (await import("./locales/hi.json")).default;
|
|
122
|
+
case "ja":
|
|
123
|
+
return (await import("./locales/ja.json")).default;
|
|
124
|
+
case "ko":
|
|
125
|
+
return (await import("./locales/ko.json")).default;
|
|
126
|
+
case "pt":
|
|
127
|
+
return (await import("./locales/pt.json")).default;
|
|
128
|
+
case "ru":
|
|
129
|
+
return (await import("./locales/ru.json")).default;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
var activeLoader = defaultLoader;
|
|
133
|
+
var flatCache = /* @__PURE__ */ new Map();
|
|
134
|
+
var nestedCache = /* @__PURE__ */ new Map();
|
|
135
|
+
async function loadBundle(loc) {
|
|
136
|
+
const cached = flatCache.get(loc);
|
|
137
|
+
if (cached) return cached;
|
|
138
|
+
const nested = await activeLoader(loc);
|
|
139
|
+
const flat = flattenMessages(nested);
|
|
140
|
+
flatCache.set(loc, flat);
|
|
141
|
+
nestedCache.set(loc, nested);
|
|
142
|
+
return flat;
|
|
143
|
+
}
|
|
144
|
+
function peekBundle(loc) {
|
|
145
|
+
return flatCache.get(loc);
|
|
146
|
+
}
|
|
147
|
+
function peekNested(loc) {
|
|
148
|
+
return nestedCache.get(loc);
|
|
149
|
+
}
|
|
150
|
+
async function preloadBundles(locales) {
|
|
151
|
+
await Promise.all(locales.map((l) => loadBundle(l)));
|
|
152
|
+
}
|
|
153
|
+
function __setLoader(fn) {
|
|
154
|
+
activeLoader = fn ?? defaultLoader;
|
|
155
|
+
flatCache.clear();
|
|
156
|
+
nestedCache.clear();
|
|
157
|
+
}
|
|
158
|
+
function __resetCache() {
|
|
159
|
+
flatCache.clear();
|
|
160
|
+
nestedCache.clear();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/i18n/store.ts
|
|
164
|
+
function createWritable(initial) {
|
|
165
|
+
let value = initial;
|
|
166
|
+
const subs = /* @__PURE__ */ new Set();
|
|
167
|
+
return {
|
|
168
|
+
subscribe(run) {
|
|
169
|
+
run(value);
|
|
170
|
+
subs.add(run);
|
|
171
|
+
return () => {
|
|
172
|
+
subs.delete(run);
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
set(v) {
|
|
176
|
+
value = v;
|
|
177
|
+
for (const fn of subs) fn(value);
|
|
178
|
+
},
|
|
179
|
+
setIfChanged(v) {
|
|
180
|
+
if (Object.is(value, v)) return;
|
|
181
|
+
value = v;
|
|
182
|
+
for (const fn of subs) fn(value);
|
|
183
|
+
},
|
|
184
|
+
get() {
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function asReadable(w) {
|
|
190
|
+
return { subscribe: w.subscribe.bind(w) };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/i18n/ai-utils.ts
|
|
194
|
+
var ALL_LOCALES = [
|
|
195
|
+
"en",
|
|
196
|
+
"zh-CN",
|
|
197
|
+
"zh-Hant",
|
|
198
|
+
"ar",
|
|
199
|
+
"de",
|
|
200
|
+
"es",
|
|
201
|
+
"fr",
|
|
202
|
+
"hi",
|
|
203
|
+
"ja",
|
|
204
|
+
"ko",
|
|
205
|
+
"pt",
|
|
206
|
+
"ru"
|
|
207
|
+
];
|
|
208
|
+
async function preloadAllLocales() {
|
|
209
|
+
await preloadBundles(ALL_LOCALES);
|
|
210
|
+
}
|
|
211
|
+
function resolveForLocale(key, loc, vars) {
|
|
212
|
+
const bundle = peekBundle(loc);
|
|
213
|
+
const fallback = peekBundle("en");
|
|
214
|
+
if (!bundle) {
|
|
215
|
+
if (fallback) return interpolate(lookup(key, fallback), vars);
|
|
216
|
+
return key;
|
|
217
|
+
}
|
|
218
|
+
return interpolate(lookup(key, bundle, fallback), vars);
|
|
219
|
+
}
|
|
220
|
+
async function resolveForLocaleAsync(key, loc, vars) {
|
|
221
|
+
await loadBundle(loc);
|
|
222
|
+
if (loc !== "en") await loadBundle("en");
|
|
223
|
+
return resolveForLocale(key, loc, vars);
|
|
224
|
+
}
|
|
225
|
+
function resolveAllLocales(key, vars) {
|
|
226
|
+
return ALL_LOCALES.map((loc) => resolveForLocale(key, loc, vars));
|
|
227
|
+
}
|
|
228
|
+
async function resolveAllLocalesAsync(key, vars) {
|
|
229
|
+
await preloadAllLocales();
|
|
230
|
+
return resolveAllLocales(key, vars);
|
|
231
|
+
}
|
|
232
|
+
function getNestedBundle(loc) {
|
|
233
|
+
return peekNested(loc);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/i18n/index.ts
|
|
237
|
+
var localeStore = createWritable("en");
|
|
238
|
+
var activeMessages = {};
|
|
239
|
+
var englishFallback = {};
|
|
240
|
+
var locale = asReadable(localeStore);
|
|
241
|
+
function t(key, vars) {
|
|
242
|
+
return interpolate(lookup(key, activeMessages, englishFallback), vars);
|
|
243
|
+
}
|
|
244
|
+
async function setLocale(loc) {
|
|
245
|
+
try {
|
|
246
|
+
const messages = await loadBundle(loc);
|
|
247
|
+
activeMessages = messages;
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
if (loc !== "en" && Object.keys(englishFallback).length === 0) {
|
|
251
|
+
try {
|
|
252
|
+
englishFallback = await loadBundle("en");
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
} else if (loc === "en") {
|
|
256
|
+
englishFallback = activeMessages;
|
|
257
|
+
}
|
|
258
|
+
localeStore.setIfChanged(loc);
|
|
259
|
+
applyDocumentDirection(loc);
|
|
260
|
+
}
|
|
261
|
+
async function initLocale(opts = {}) {
|
|
262
|
+
const persisted = opts.persistence ? await opts.persistence.get() : null;
|
|
263
|
+
const chosen = (opts.preferred && opts.preferred !== "system" ? opts.preferred : null) ?? (persisted && persisted !== "system" ? persisted : null) ?? detectSystemLocale();
|
|
264
|
+
await setLocale(chosen);
|
|
265
|
+
if (opts.persistence && opts.preferred && opts.preferred !== "system") {
|
|
266
|
+
await opts.persistence.set(opts.preferred);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function __resetState() {
|
|
270
|
+
activeMessages = {};
|
|
271
|
+
englishFallback = {};
|
|
272
|
+
localeStore.setIfChanged("en");
|
|
273
|
+
}
|
|
274
|
+
export {
|
|
275
|
+
RTL_LOCALES,
|
|
276
|
+
SUPPORTED_LOCALES,
|
|
277
|
+
__resetCache,
|
|
278
|
+
__resetState,
|
|
279
|
+
__setLoader,
|
|
280
|
+
applyDocumentDirection,
|
|
281
|
+
detectSystemLocale,
|
|
282
|
+
flattenMessages,
|
|
283
|
+
getNestedBundle,
|
|
284
|
+
initLocale,
|
|
285
|
+
interpolate,
|
|
286
|
+
isRTL,
|
|
287
|
+
loadBundle,
|
|
288
|
+
locale,
|
|
289
|
+
lookup,
|
|
290
|
+
mergeBundles,
|
|
291
|
+
peekBundle,
|
|
292
|
+
preloadAllLocales,
|
|
293
|
+
preloadBundles,
|
|
294
|
+
resolveAllLocales,
|
|
295
|
+
resolveAllLocalesAsync,
|
|
296
|
+
resolveForLocale,
|
|
297
|
+
resolveForLocaleAsync,
|
|
298
|
+
setLocale,
|
|
299
|
+
t
|
|
300
|
+
};
|
|
301
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/i18n/messages.ts","../../src/i18n/types.ts","../../src/i18n/detect.ts","../../src/i18n/loader.ts","../../src/i18n/store.ts","../../src/i18n/ai-utils.ts","../../src/i18n/index.ts"],"sourcesContent":["/**\n * Pure message helpers — flatten / interpolate / merge / lookup. No side\n * effects, no I/O, no global state. Used by `index.ts` to back the active\n * locale and by `ai-utils.ts` for cross-locale lookups.\n */\n\nimport type { FlatMessages, MessageBundle } from './types.js'\n\n/**\n * Recursively flatten a nested bundle into a flat `'a.b.c' → string` map.\n * Non-string leaves (numbers, booleans) are coerced to `String(v)`; non-leaf\n * non-objects (e.g. arrays) are dropped silently — i18n payloads should be\n * pure nested string maps.\n */\nexport function flattenMessages(obj: unknown, prefix = ''): FlatMessages {\n if (typeof obj !== 'object' || obj === null) return {}\n const out: FlatMessages = {}\n for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {\n const key = prefix ? `${prefix}.${k}` : k\n if (typeof v === 'string') {\n out[key] = v\n } else if (typeof v === 'number' || typeof v === 'boolean') {\n out[key] = String(v)\n } else if (v && typeof v === 'object' && !Array.isArray(v)) {\n Object.assign(out, flattenMessages(v, key))\n }\n // arrays / null / undefined: dropped\n }\n return out\n}\n\n/**\n * Interpolate `{name}` placeholders in a message template.\n *\n * - Unknown placeholders pass through unchanged: `{notInVars}` stays literal.\n * This mirrors the existing moraya & moraya-web behaviour so callers that\n * embed literal `{x}` in copy don't break.\n */\nexport function interpolate(template: string, vars?: Record<string, string>): string {\n if (!vars) return template\n return template.replace(/\\{(\\w+)\\}/g, (m, k: string) => {\n const v = vars[k]\n return v === undefined ? m : v\n })\n}\n\n/**\n * Look up `key` in `messages` with a fallback table. Returns:\n * 1. `messages[key]` if present\n * 2. `fallback[key]` if present\n * 3. the literal key itself (debug-friendly — missing keys are visible)\n *\n * `key` is the dot-joined path produced by `flattenMessages`.\n */\nexport function lookup(\n key: string,\n messages: FlatMessages,\n fallback?: FlatMessages,\n): string {\n const direct = messages[key]\n if (direct !== undefined) return direct\n if (fallback) {\n const fb = fallback[key]\n if (fb !== undefined) return fb\n }\n return key\n}\n\n/**\n * Deep merge two nested bundles. Right side wins on leaf conflict. Used when\n * a host wants to overlay app-specific strings on top of the shipped bundle\n * without forking the JSON. NOT used by the dynamic locale loader (which\n * just replaces the active bundle wholesale).\n */\nexport function mergeBundles(base: MessageBundle, overlay: MessageBundle): MessageBundle {\n const out: MessageBundle = { ...base }\n for (const [k, v] of Object.entries(overlay)) {\n const existing = out[k]\n if (\n v && typeof v === 'object' && !Array.isArray(v) &&\n existing && typeof existing === 'object' && !Array.isArray(existing)\n ) {\n out[k] = mergeBundles(existing as MessageBundle, v as MessageBundle)\n } else {\n out[k] = v\n }\n }\n return out\n}\n","/**\n * Shared i18n type surface.\n *\n * This module MUST stay free of any framework dependency (no svelte, no react,\n * no prosemirror, no markdown-it). It is the contract that consumers — moraya\n * desktop, moraya-web, future @moraya/i18n extraction — all agree on.\n */\n\nexport type SupportedLocale =\n | 'en' | 'zh-CN' | 'zh-Hant'\n | 'ar' | 'de' | 'es' | 'fr' | 'hi' | 'ja' | 'ko' | 'pt' | 'ru'\n\n/** Selection a user can make in UI; `'system'` means follow OS / browser locale. */\nexport type LocaleSelection = SupportedLocale | 'system'\n\nexport interface LocaleOption {\n code: LocaleSelection\n /** Native-script label, shown unchanged regardless of active UI locale. */\n label: string\n}\n\n/** All 12 locales we support today, in label-alphabetical order. */\nexport const SUPPORTED_LOCALES: LocaleOption[] = [\n { code: 'system', label: 'System' },\n { code: 'ar', label: 'العربية' },\n { code: 'de', label: 'Deutsch' },\n { code: 'en', label: 'English' },\n { code: 'es', label: 'Español' },\n { code: 'fr', label: 'Français' },\n { code: 'hi', label: 'हिन्दी' },\n { code: 'ja', label: '日本語' },\n { code: 'ko', label: '한국어' },\n { code: 'pt', label: 'Português' },\n { code: 'ru', label: 'Русский' },\n { code: 'zh-CN', label: '简体中文' },\n { code: 'zh-Hant', label: '繁體中文' },\n]\n\n/** Locales that render right-to-left. */\nexport const RTL_LOCALES: readonly SupportedLocale[] = ['ar'] as const\n\n/** A nested locale message bundle as authored in `locales/<loc>.json`. */\nexport type MessageBundle = { [k: string]: string | MessageBundle }\n\n/** Flat key → string form, produced by `flattenMessages`. */\nexport type FlatMessages = Record<string, string>\n\n/**\n * Persistence callbacks for `initLocale`. Each consumer wires these to its\n * host environment — Tauri plugin-store for desktop, localStorage for web.\n * Pass `undefined` to skip persistence (locale resets to detected on each load).\n */\nexport interface PersistenceAdapter {\n get(): LocaleSelection | null | Promise<LocaleSelection | null>\n set(selection: LocaleSelection): void | Promise<void>\n}\n","/**\n * Locale detection helpers. Pure / framework-agnostic — uses only standard\n * browser APIs (`navigator.language`) with defensive guards for SSR / Node /\n * environments where `navigator` is absent.\n */\n\nimport type { SupportedLocale } from './types.js'\nimport { RTL_LOCALES } from './types.js'\n\nconst PREFIX_MAP: ReadonlyArray<readonly [string, SupportedLocale]> = [\n ['ja', 'ja'],\n ['ko', 'ko'],\n ['ar', 'ar'],\n ['hi', 'hi'],\n ['ru', 'ru'],\n ['de', 'de'],\n ['fr', 'fr'],\n ['es', 'es'],\n ['pt', 'pt'],\n]\n\n/**\n * Best-effort guess of the user's preferred locale from the browser /\n * navigator. Returns `'en'` if no match (also when `navigator` is unavailable\n * — SSR, Node, Worker). Chinese is split into `zh-CN` (Simplified) vs\n * `zh-Hant` (Traditional, including TW/HK/MO).\n */\nexport function detectSystemLocale(): SupportedLocale {\n try {\n const nav = (globalThis as { navigator?: { language?: string; languages?: readonly string[] } }).navigator\n if (!nav) return 'en'\n const lang = nav.language ?? nav.languages?.[0] ?? 'en'\n\n if (lang.startsWith('zh')) {\n if (/zh-(TW|HK|MO|Hant)/i.test(lang)) return 'zh-Hant'\n return 'zh-CN'\n }\n for (const [prefix, locale] of PREFIX_MAP) {\n if (lang.startsWith(prefix)) return locale\n }\n } catch {\n // navigator unavailable — fall through to 'en'\n }\n return 'en'\n}\n\n/** Whether a locale renders right-to-left. */\nexport function isRTL(loc: SupportedLocale): boolean {\n return RTL_LOCALES.includes(loc)\n}\n\n/**\n * Apply the locale's writing direction to `document.documentElement.dir`.\n * No-op if `document` is unavailable (SSR / Worker / Node).\n */\nexport function applyDocumentDirection(loc: SupportedLocale): void {\n try {\n const doc = (globalThis as { document?: { documentElement: { dir: string } } }).document\n if (doc) doc.documentElement.dir = isRTL(loc) ? 'rtl' : 'ltr'\n } catch {\n // document unavailable — no-op\n }\n}\n","/**\n * Dynamic locale-bundle loader with module-scoped cache.\n *\n * Each `loadBundle(loc)` call returns the same flat-messages map on repeat\n * calls (memoized). The underlying `import('./locales/<loc>.json')` is what\n * tsup / vite turn into a code-split chunk — only locales the user actually\n * picks reach the consumer's bundle.\n *\n * Test injection: `__setLoader(fn)` lets unit tests swap the dynamic-import\n * function with a synchronous fixture-returning stub.\n */\n\nimport type { FlatMessages, SupportedLocale, MessageBundle } from './types.js'\nimport { flattenMessages } from './messages.js'\n\ntype LoaderFn = (loc: SupportedLocale) => Promise<MessageBundle>\n\n/**\n * Default loader uses `import()` against the colocated `./locales/<loc>.json`\n * files. The `with { type: 'json' }` assertion is omitted intentionally —\n * tsup/vite both handle bare JSON imports, and adding the assertion would\n * break Vite < 5.\n */\nconst defaultLoader: LoaderFn = async (loc) => {\n // The switch keeps each branch a static literal so bundlers can analyze\n // and code-split per locale. A computed `import('./locales/' + loc + '.json')`\n // would either bundle them all eagerly or fail to resolve.\n switch (loc) {\n case 'en': return (await import('./locales/en.json')).default as unknown as MessageBundle\n case 'zh-CN': return (await import('./locales/zh-CN.json')).default as unknown as MessageBundle\n case 'zh-Hant': return (await import('./locales/zh-Hant.json')).default as unknown as MessageBundle\n case 'ar': return (await import('./locales/ar.json')).default as unknown as MessageBundle\n case 'de': return (await import('./locales/de.json')).default as unknown as MessageBundle\n case 'es': return (await import('./locales/es.json')).default as unknown as MessageBundle\n case 'fr': return (await import('./locales/fr.json')).default as unknown as MessageBundle\n case 'hi': return (await import('./locales/hi.json')).default as unknown as MessageBundle\n case 'ja': return (await import('./locales/ja.json')).default as unknown as MessageBundle\n case 'ko': return (await import('./locales/ko.json')).default as unknown as MessageBundle\n case 'pt': return (await import('./locales/pt.json')).default as unknown as MessageBundle\n case 'ru': return (await import('./locales/ru.json')).default as unknown as MessageBundle\n }\n}\n\nlet activeLoader: LoaderFn = defaultLoader\nconst flatCache = new Map<SupportedLocale, FlatMessages>()\nconst nestedCache = new Map<SupportedLocale, MessageBundle>()\n\n/**\n * Resolve and cache a locale's flat-messages bundle. Subsequent calls return\n * the cached map without re-importing.\n */\nexport async function loadBundle(loc: SupportedLocale): Promise<FlatMessages> {\n const cached = flatCache.get(loc)\n if (cached) return cached\n const nested = await activeLoader(loc)\n const flat = flattenMessages(nested)\n flatCache.set(loc, flat)\n nestedCache.set(loc, nested)\n return flat\n}\n\n/** Synchronous cache read — returns `undefined` if the locale isn't loaded yet. */\nexport function peekBundle(loc: SupportedLocale): FlatMessages | undefined {\n return flatCache.get(loc)\n}\n\n/** Synchronous cache read of the nested form (for AI utilities). */\nexport function peekNested(loc: SupportedLocale): MessageBundle | undefined {\n return nestedCache.get(loc)\n}\n\n/** Preload several locales in parallel. Returns once all are cached. */\nexport async function preloadBundles(locales: readonly SupportedLocale[]): Promise<void> {\n await Promise.all(locales.map((l) => loadBundle(l)))\n}\n\n/* ─────────────────────────────────────────────────────────────────────────\n * Test injection. Not part of the public API. Resets caches as a side effect\n * to keep tests deterministic.\n * ────────────────────────────────────────────────────────────────────── */\n\n/** @internal — for unit tests only. */\nexport function __setLoader(fn: LoaderFn | null): void {\n activeLoader = fn ?? defaultLoader\n flatCache.clear()\n nestedCache.clear()\n}\n\n/** @internal — for unit tests only. */\nexport function __resetCache(): void {\n flatCache.clear()\n nestedCache.clear()\n}\n","/**\n * Minimal pub/sub primitive compatible with Svelte's `Readable<T>` contract\n * (`{ subscribe(run) => unsub }`) — without importing svelte/store.\n *\n * Why a custom primitive: this package must stay framework-agnostic so it\n * can be lifted to a standalone `@moraya/i18n` package later without\n * dragging a svelte peer-dep along. Consumers that DO use Svelte can pass\n * the returned store straight into `derived()` from `svelte/store` because\n * the subscribe signature matches.\n */\n\nexport type Subscriber<T> = (value: T) => void\nexport type Unsubscriber = () => void\n\nexport interface Readable<T> {\n subscribe(run: Subscriber<T>): Unsubscriber\n}\n\nexport interface Writable<T> extends Readable<T> {\n set(value: T): void\n /** Like `set` but only fires subscribers when the value actually changed. */\n setIfChanged(value: T): void\n /** Sync snapshot — escape hatch for code paths that can't subscribe. */\n get(): T\n}\n\nexport function createWritable<T>(initial: T): Writable<T> {\n let value = initial\n const subs = new Set<Subscriber<T>>()\n return {\n subscribe(run) {\n run(value)\n subs.add(run)\n return () => { subs.delete(run) }\n },\n set(v) {\n value = v\n for (const fn of subs) fn(value)\n },\n setIfChanged(v) {\n if (Object.is(value, v)) return\n value = v\n for (const fn of subs) fn(value)\n },\n get() { return value },\n }\n}\n\n/** Read-only view of a writable; hides `set` from public exports. */\nexport function asReadable<T>(w: Writable<T>): Readable<T> {\n return { subscribe: w.subscribe.bind(w) }\n}\n","/**\n * AI-prompt helpers. Originally lived in `moraya/src/lib/i18n/i18n.ts` — used\n * when an AI conversation runs in a locale different from the active UI\n * locale, or when matching user-generated data that could be in any locale.\n *\n * Both helpers require the relevant locale bundles to already be loaded.\n * Callers should `await preloadAllLocales()` once at app boot before relying\n * on the sync read.\n */\n\nimport type { FlatMessages, SupportedLocale } from './types.js'\nimport { interpolate, lookup, flattenMessages } from './messages.js'\nimport { loadBundle, peekBundle, peekNested, preloadBundles } from './loader.js'\n\nconst ALL_LOCALES: readonly SupportedLocale[] = [\n 'en', 'zh-CN', 'zh-Hant',\n 'ar', 'de', 'es', 'fr', 'hi', 'ja', 'ko', 'pt', 'ru',\n] as const\n\n/**\n * Force every supported locale's bundle into the cache. Call once at boot if\n * the app expects to use `resolveForLocale` / `resolveAllLocales` sync.\n */\nexport async function preloadAllLocales(): Promise<void> {\n await preloadBundles(ALL_LOCALES)\n}\n\n/**\n * Sync lookup against a specific (non-active) locale's bundle. Returns the\n * key itself if the locale isn't loaded yet — call `preloadAllLocales()` (or\n * `loadBundle(loc)`) first if you need real translations.\n */\nexport function resolveForLocale(\n key: string,\n loc: SupportedLocale,\n vars?: Record<string, string>,\n): string {\n const bundle = peekBundle(loc)\n const fallback = peekBundle('en')\n if (!bundle) {\n // Bundle not loaded — last-ditch: try English fallback then key\n if (fallback) return interpolate(lookup(key, fallback), vars)\n return key\n }\n return interpolate(lookup(key, bundle, fallback), vars)\n}\n\n/**\n * Async variant: ensures the requested locale is loaded before resolving.\n * Preferred when the caller is already in an async path (workflow steps,\n * AI prompt generation, etc.).\n */\nexport async function resolveForLocaleAsync(\n key: string,\n loc: SupportedLocale,\n vars?: Record<string, string>,\n): Promise<string> {\n await loadBundle(loc)\n if (loc !== 'en') await loadBundle('en')\n return resolveForLocale(key, loc, vars)\n}\n\n/**\n * Sync: returns one translation per supported locale, in `ALL_LOCALES` order.\n * Locales whose bundle isn't loaded yet contribute the key itself — meaning a\n * pre-load is essential before relying on this for pattern matching.\n *\n * Use case: matching user-generated speaker names like \"Passerby 1\" /\n * \"路人 1\" / \"通行人 1\" across 12 locales — the caller takes the union of\n * all returned strings as a regex alternation source.\n */\nexport function resolveAllLocales(\n key: string,\n vars?: Record<string, string>,\n): string[] {\n return ALL_LOCALES.map((loc) => resolveForLocale(key, loc, vars))\n}\n\n/** Async variant — loads every locale first, then resolves. */\nexport async function resolveAllLocalesAsync(\n key: string,\n vars?: Record<string, string>,\n): Promise<string[]> {\n await preloadAllLocales()\n return resolveAllLocales(key, vars)\n}\n\n/**\n * Escape hatch for tests / advanced consumers: returns the cached nested\n * bundle for a locale (read-only). Returns `undefined` if not loaded.\n * Most callers should NOT need this — prefer `resolveForLocale`.\n */\nexport function getNestedBundle(loc: SupportedLocale) {\n return peekNested(loc)\n}\n\n/** Re-exported for callers that need to flatten an arbitrary bundle. */\nexport { flattenMessages }\n","/**\n * Public API for the Moraya unified i18n module.\n *\n * Imported as `@moraya/core/i18n` once `package.json` / `tsup.config.ts` are\n * wired (Phase 1c). Designed to be lifted into a standalone `@moraya/i18n`\n * package with a path-only rename — see plan v0.96.0.\n *\n * Framework coupling: NONE in this module. The `locale` export is a custom\n * `Readable<T>` whose `subscribe` signature is compatible with Svelte's\n * `Readable<T>`, so a Svelte consumer can pass it straight into `derived()`\n * without wrapping. See `consumer-svelte-shim` example in the iteration doc.\n */\n\nimport type { FlatMessages, PersistenceAdapter, SupportedLocale, LocaleSelection } from './types.js'\nimport { interpolate, lookup } from './messages.js'\nimport { applyDocumentDirection, detectSystemLocale } from './detect.js'\nimport { loadBundle, peekBundle } from './loader.js'\nimport { createWritable, asReadable, type Readable } from './store.js'\n\n/* ─────────────────────────────────────────────────────────────────────────\n * Internal state — single-process / per-bundle singleton. Consumers expect\n * one active locale at a time per host. (PC: Tauri webview; Web: each tab.)\n * ────────────────────────────────────────────────────────────────────── */\n\nconst localeStore = createWritable<SupportedLocale>('en')\nlet activeMessages: FlatMessages = {}\nlet englishFallback: FlatMessages = {}\n\n/** Read-only view of the active locale. Compatible with Svelte's `Readable`. */\nexport const locale: Readable<SupportedLocale> = asReadable(localeStore)\n\n/* ─────────────────────────────────────────────────────────────────────────\n * Translation lookup.\n *\n * Plain function (not a derived store) — this keeps the package framework-\n * agnostic. Svelte consumers that want auto re-render on locale change wrap\n * `t` in a `derived` store inside their thin shim (see iteration doc). Non-\n * Svelte consumers just call `t('foo.bar')` synchronously.\n * ────────────────────────────────────────────────────────────────────── */\n\n/**\n * Translate a flat dot-joined key. Falls back to English, then to the literal\n * key (so missing translations are visible in dev).\n */\nexport function t(key: string, vars?: Record<string, string>): string {\n return interpolate(lookup(key, activeMessages, englishFallback), vars)\n}\n\n/* ─────────────────────────────────────────────────────────────────────────\n * Locale switching + initialization.\n * ────────────────────────────────────────────────────────────────────── */\n\n/**\n * Switch the active locale. Loads the bundle on demand (memoized), updates\n * the active-messages map, applies document direction (for RTL), and notifies\n * subscribers of the `locale` store.\n *\n * If the bundle fails to load (e.g. network error in a future remote-locale\n * mode), the active locale is updated anyway so the UI doesn't get stuck —\n * but translations will fall through to English / key.\n */\nexport async function setLocale(loc: SupportedLocale): Promise<void> {\n try {\n const messages = await loadBundle(loc)\n activeMessages = messages\n } catch {\n // Bundle load failed — keep prior messages but still flip the locale so\n // RTL direction + subscribers stay accurate.\n }\n // Always keep English in the fallback slot, loaded lazily.\n if (loc !== 'en' && Object.keys(englishFallback).length === 0) {\n try {\n englishFallback = await loadBundle('en')\n } catch { /* fallback unavailable — accept */ }\n } else if (loc === 'en') {\n englishFallback = activeMessages\n }\n localeStore.setIfChanged(loc)\n applyDocumentDirection(loc)\n}\n\n/**\n * Options for `initLocale`. All fields are optional:\n * - `preferred`: explicit user choice. Wins over persisted + detected.\n * - `persistence`: read/write hooks for remembering the user's choice.\n * Pass localStorage-backed callbacks on web, Tauri plugin-store on PC.\n */\nexport interface InitLocaleOptions {\n preferred?: LocaleSelection | null\n persistence?: PersistenceAdapter\n}\n\n/**\n * Bootstrap the i18n module: resolve which locale to use, load its bundle,\n * set it active. Resolution order:\n *\n * 1. `opts.preferred` if it names a concrete locale (not `'system'`)\n * 2. `opts.persistence.get()` if it returns a concrete locale\n * 3. `detectSystemLocale()` (navigator-based fallback)\n *\n * The chosen locale is `await setLocale(...)` so the active messages are\n * loaded before this Promise resolves. Safe to call multiple times — each\n * call simply re-runs the resolution.\n */\nexport async function initLocale(opts: InitLocaleOptions = {}): Promise<void> {\n const persisted = opts.persistence ? await opts.persistence.get() : null\n\n const chosen: SupportedLocale =\n (opts.preferred && opts.preferred !== 'system' ? opts.preferred : null)\n ?? (persisted && persisted !== 'system' ? persisted : null)\n ?? detectSystemLocale()\n\n await setLocale(chosen)\n\n if (opts.persistence && opts.preferred && opts.preferred !== 'system') {\n // Caller asserted a preference at init — write it through.\n await opts.persistence.set(opts.preferred)\n }\n}\n\n/* ─────────────────────────────────────────────────────────────────────────\n * Re-exports for the public surface.\n * ────────────────────────────────────────────────────────────────────── */\n\nexport {\n detectSystemLocale,\n isRTL,\n applyDocumentDirection,\n} from './detect.js'\n\nexport {\n resolveForLocale,\n resolveForLocaleAsync,\n resolveAllLocales,\n resolveAllLocalesAsync,\n preloadAllLocales,\n getNestedBundle,\n} from './ai-utils.js'\n\nexport {\n flattenMessages,\n interpolate,\n lookup,\n mergeBundles,\n} from './messages.js'\n\nexport {\n loadBundle,\n peekBundle,\n preloadBundles,\n} from './loader.js'\n\nexport {\n type Readable,\n type Subscriber,\n type Unsubscriber,\n} from './store.js'\n\nexport {\n type SupportedLocale,\n type LocaleSelection,\n type LocaleOption,\n type MessageBundle,\n type FlatMessages,\n type PersistenceAdapter,\n SUPPORTED_LOCALES,\n RTL_LOCALES,\n} from './types.js'\n\n/* ─────────────────────────────────────────────────────────────────────────\n * Test-only injection points. Intentionally not part of the public type\n * surface (they're stripped from .d.ts via the @internal marker). Used by\n * unit tests in `__tests__/` to install fixture loaders.\n * ────────────────────────────────────────────────────────────────────── */\n\n/** @internal */\nexport { __setLoader, __resetCache } from './loader.js'\n\n/** @internal — reset the active-locale state. Tests only. */\nexport function __resetState(): void {\n activeMessages = {}\n englishFallback = {}\n localeStore.setIfChanged('en')\n}\n"],"mappings":"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","names":["locale"]}
|