solid-translate 1.4.0 → 1.4.2
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 +20 -0
- package/dist/cli.js +141 -6
- package/dist/index.d.ts +7 -0
- package/dist/index.js +33 -15
- package/dist/index.js.map +1 -1
- package/dist/vite.d.ts +7 -0
- package/dist/vite.js +146 -8
- package/dist/vite.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -350,9 +350,29 @@ solidTranslate({
|
|
|
350
350
|
batchSize: 50, // Keys per API call (default: 50)
|
|
351
351
|
autoExtract: true, // Auto-extract <T> and msg() strings (default: false)
|
|
352
352
|
include: ["src/**/*.tsx"], // Files to scan for extraction
|
|
353
|
+
extractImportSources: ["@/i18n"], // Extra module specifiers whose msg/<T> imports count as markers (optional)
|
|
353
354
|
})
|
|
354
355
|
```
|
|
355
356
|
|
|
357
|
+
### What extraction considers a marker
|
|
358
|
+
|
|
359
|
+
Extraction only honors `msg()` calls and `<T>`/`<Plural>` elements whose
|
|
360
|
+
identifier actually refers to solid-translate:
|
|
361
|
+
|
|
362
|
+
- Imported bindings must come from `"solid-translate"` or an accepted
|
|
363
|
+
re-export wrapper. By default any specifier whose final path segment is
|
|
364
|
+
`solid-translate` or `i18n` (e.g. `@/i18n`, `../lib/i18n`) is accepted;
|
|
365
|
+
set `extractImportSources` (plugin config) / `"extractImportSources"`
|
|
366
|
+
(CLI config) to an explicit list to override the `i18n` heuristic
|
|
367
|
+
(`"solid-translate"` itself is always accepted).
|
|
368
|
+
- Aliased imports work: `import { msg as m } from "solid-translate"`
|
|
369
|
+
extracts `m("...")`.
|
|
370
|
+
- Locally bound identifiers are never extracted — a callback parameter
|
|
371
|
+
named `msg`, a local `const msg = ...`, or a local component named `T`
|
|
372
|
+
will not pollute the catalog or emit warnings.
|
|
373
|
+
- Identifiers with no binding at all are still treated as markers by name,
|
|
374
|
+
so snippet-style sources keep working.
|
|
375
|
+
|
|
356
376
|
## CLI
|
|
357
377
|
|
|
358
378
|
For translating locale files, JSON, Markdown, and MDX outside of the Vite build.
|
package/dist/cli.js
CHANGED
|
@@ -14599,7 +14599,7 @@ function hashContent(content) {
|
|
|
14599
14599
|
|
|
14600
14600
|
// src/extract.ts
|
|
14601
14601
|
var import_parser = __toESM(require_lib(), 1);
|
|
14602
|
-
function extractStringsFromSource(code, filePath, warnings) {
|
|
14602
|
+
function extractStringsFromSource(code, filePath, warnings, options) {
|
|
14603
14603
|
const results = [];
|
|
14604
14604
|
const seen = /* @__PURE__ */ new Set();
|
|
14605
14605
|
const warn = (line, message) => {
|
|
@@ -14621,26 +14621,158 @@ function extractStringsFromSource(code, filePath, warnings) {
|
|
|
14621
14621
|
seen.add(entry.key);
|
|
14622
14622
|
results.push(entry);
|
|
14623
14623
|
};
|
|
14624
|
+
const bindings = collectModuleBindings(ast);
|
|
14625
|
+
const shadowStack = [];
|
|
14626
|
+
const resolveMarker = (localName) => {
|
|
14627
|
+
for (let i = shadowStack.length - 1; i >= 0; i--) {
|
|
14628
|
+
if (shadowStack[i].has(localName)) return null;
|
|
14629
|
+
}
|
|
14630
|
+
const imported = bindings.imports.get(localName);
|
|
14631
|
+
if (imported) {
|
|
14632
|
+
if (!isAcceptedImportSource(imported.source, options?.importSources)) {
|
|
14633
|
+
return null;
|
|
14634
|
+
}
|
|
14635
|
+
return MARKER_NAMES.has(imported.imported) ? imported.imported : null;
|
|
14636
|
+
}
|
|
14637
|
+
if (bindings.moduleLocals.has(localName)) return null;
|
|
14638
|
+
return MARKER_NAMES.has(localName) ? localName : null;
|
|
14639
|
+
};
|
|
14624
14640
|
const visit = (node) => {
|
|
14641
|
+
const scopeBindings = collectScopeBindings(node);
|
|
14642
|
+
if (scopeBindings) shadowStack.push(scopeBindings);
|
|
14625
14643
|
if (node.type === "JSXElement") {
|
|
14626
14644
|
const name = jsxName(node);
|
|
14627
|
-
|
|
14645
|
+
const marker = name ? resolveMarker(name) : null;
|
|
14646
|
+
if (marker === "T") {
|
|
14628
14647
|
const entry = processT(node, filePath, warn);
|
|
14629
14648
|
if (entry) push(entry);
|
|
14630
|
-
} else if (
|
|
14649
|
+
} else if (marker === "Plural") {
|
|
14631
14650
|
for (const entry of processPlural(node, filePath, warn)) {
|
|
14632
14651
|
push(entry);
|
|
14633
14652
|
}
|
|
14634
14653
|
}
|
|
14635
|
-
} else if (node.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "msg") {
|
|
14654
|
+
} else if (node.type === "CallExpression" && node.callee?.type === "Identifier" && resolveMarker(node.callee.name) === "msg") {
|
|
14636
14655
|
const entry = processMsg(node, filePath, warn);
|
|
14637
14656
|
if (entry) push(entry);
|
|
14638
14657
|
}
|
|
14639
14658
|
walkChildren(node, visit);
|
|
14659
|
+
if (scopeBindings) shadowStack.pop();
|
|
14640
14660
|
};
|
|
14641
14661
|
visit(ast);
|
|
14642
14662
|
return results;
|
|
14643
14663
|
}
|
|
14664
|
+
var MARKER_NAMES = /* @__PURE__ */ new Set([
|
|
14665
|
+
"msg",
|
|
14666
|
+
"T",
|
|
14667
|
+
"Var",
|
|
14668
|
+
"Num",
|
|
14669
|
+
"Currency",
|
|
14670
|
+
"DateTime",
|
|
14671
|
+
"Plural"
|
|
14672
|
+
]);
|
|
14673
|
+
var DEFAULT_IMPORT_SOURCE_RE = /(^|\/)(solid-translate|i18n)(\.[cm]?[jt]sx?)?$/;
|
|
14674
|
+
function isAcceptedImportSource(source, importSources) {
|
|
14675
|
+
if (source === "solid-translate") return true;
|
|
14676
|
+
if (importSources) return importSources.includes(source);
|
|
14677
|
+
return DEFAULT_IMPORT_SOURCE_RE.test(source);
|
|
14678
|
+
}
|
|
14679
|
+
function collectModuleBindings(ast) {
|
|
14680
|
+
const imports = /* @__PURE__ */ new Map();
|
|
14681
|
+
const moduleLocals = /* @__PURE__ */ new Set();
|
|
14682
|
+
const body = ast.program?.body ?? [];
|
|
14683
|
+
for (const stmt of body) {
|
|
14684
|
+
if (stmt.type === "ImportDeclaration") {
|
|
14685
|
+
const source = String(stmt.source?.value ?? "");
|
|
14686
|
+
for (const spec of stmt.specifiers ?? []) {
|
|
14687
|
+
const local = spec.local?.name;
|
|
14688
|
+
if (typeof local !== "string") continue;
|
|
14689
|
+
if (spec.type === "ImportSpecifier") {
|
|
14690
|
+
const imported = spec.imported?.type === "Identifier" ? spec.imported.name : String(spec.imported?.value ?? "");
|
|
14691
|
+
imports.set(local, { imported, source });
|
|
14692
|
+
} else {
|
|
14693
|
+
imports.set(local, { imported: "*", source });
|
|
14694
|
+
}
|
|
14695
|
+
}
|
|
14696
|
+
} else {
|
|
14697
|
+
collectDeclaredNames(stmt, moduleLocals);
|
|
14698
|
+
}
|
|
14699
|
+
}
|
|
14700
|
+
return { imports, moduleLocals };
|
|
14701
|
+
}
|
|
14702
|
+
function collectDeclaredNames(stmt, into) {
|
|
14703
|
+
if (stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportDefaultDeclaration") {
|
|
14704
|
+
if (stmt.declaration) collectDeclaredNames(stmt.declaration, into);
|
|
14705
|
+
return;
|
|
14706
|
+
}
|
|
14707
|
+
if (stmt.type === "VariableDeclaration") {
|
|
14708
|
+
for (const decl of stmt.declarations ?? []) {
|
|
14709
|
+
if (decl.id) collectPatternNames(decl.id, into);
|
|
14710
|
+
}
|
|
14711
|
+
return;
|
|
14712
|
+
}
|
|
14713
|
+
if ((stmt.type === "FunctionDeclaration" || stmt.type === "ClassDeclaration" || stmt.type === "TSEnumDeclaration") && stmt.id?.type === "Identifier") {
|
|
14714
|
+
addMarkerName(stmt.id.name, into);
|
|
14715
|
+
}
|
|
14716
|
+
}
|
|
14717
|
+
function collectPatternNames(pattern, into) {
|
|
14718
|
+
switch (pattern.type) {
|
|
14719
|
+
case "Identifier":
|
|
14720
|
+
addMarkerName(pattern.name, into);
|
|
14721
|
+
break;
|
|
14722
|
+
case "AssignmentPattern":
|
|
14723
|
+
collectPatternNames(pattern.left, into);
|
|
14724
|
+
break;
|
|
14725
|
+
case "RestElement":
|
|
14726
|
+
collectPatternNames(pattern.argument, into);
|
|
14727
|
+
break;
|
|
14728
|
+
case "ObjectPattern":
|
|
14729
|
+
for (const prop of pattern.properties ?? []) {
|
|
14730
|
+
if (prop.type === "ObjectProperty") {
|
|
14731
|
+
collectPatternNames(prop.value, into);
|
|
14732
|
+
} else if (prop.type === "RestElement") {
|
|
14733
|
+
collectPatternNames(prop.argument, into);
|
|
14734
|
+
}
|
|
14735
|
+
}
|
|
14736
|
+
break;
|
|
14737
|
+
case "ArrayPattern":
|
|
14738
|
+
for (const el of pattern.elements ?? []) {
|
|
14739
|
+
if (el) collectPatternNames(el, into);
|
|
14740
|
+
}
|
|
14741
|
+
break;
|
|
14742
|
+
}
|
|
14743
|
+
}
|
|
14744
|
+
function addMarkerName(name, into) {
|
|
14745
|
+
if (typeof name === "string" && MARKER_NAMES.has(name)) into.add(name);
|
|
14746
|
+
}
|
|
14747
|
+
var FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
14748
|
+
"ArrowFunctionExpression",
|
|
14749
|
+
"FunctionExpression",
|
|
14750
|
+
"FunctionDeclaration",
|
|
14751
|
+
"ObjectMethod",
|
|
14752
|
+
"ClassMethod",
|
|
14753
|
+
"ClassPrivateMethod"
|
|
14754
|
+
]);
|
|
14755
|
+
function collectScopeBindings(node) {
|
|
14756
|
+
const bound = /* @__PURE__ */ new Set();
|
|
14757
|
+
if (FUNCTION_TYPES.has(node.type)) {
|
|
14758
|
+
if (node.id?.type === "Identifier") addMarkerName(node.id.name, bound);
|
|
14759
|
+
for (const param of node.params ?? []) {
|
|
14760
|
+
collectPatternNames(param, bound);
|
|
14761
|
+
}
|
|
14762
|
+
} else if (node.type === "CatchClause" && node.param) {
|
|
14763
|
+
collectPatternNames(node.param, bound);
|
|
14764
|
+
} else if (node.type === "BlockStatement") {
|
|
14765
|
+
for (const stmt of node.body ?? []) {
|
|
14766
|
+
collectDeclaredNames(stmt, bound);
|
|
14767
|
+
}
|
|
14768
|
+
} else if (node.type === "ForStatement" || node.type === "ForOfStatement" || node.type === "ForInStatement") {
|
|
14769
|
+
const init = node.init ?? node.left;
|
|
14770
|
+
if (init?.type === "VariableDeclaration") {
|
|
14771
|
+
collectDeclaredNames(init, bound);
|
|
14772
|
+
}
|
|
14773
|
+
}
|
|
14774
|
+
return bound.size > 0 ? bound : null;
|
|
14775
|
+
}
|
|
14644
14776
|
function walkChildren(node, visit) {
|
|
14645
14777
|
for (const key of Object.keys(node)) {
|
|
14646
14778
|
if (key === "loc" || key === "leadingComments" || key === "trailingComments" || key === "innerComments") {
|
|
@@ -15229,7 +15361,8 @@ async function runExtract() {
|
|
|
15229
15361
|
const extracted = extractStringsFromSource(
|
|
15230
15362
|
code,
|
|
15231
15363
|
relative(root, file),
|
|
15232
|
-
warnings
|
|
15364
|
+
warnings,
|
|
15365
|
+
{ importSources: config.extractImportSources }
|
|
15233
15366
|
);
|
|
15234
15367
|
for (const entry of extracted) {
|
|
15235
15368
|
strings[entry.key] = entry.source;
|
|
@@ -15286,7 +15419,9 @@ async function runCheck(jsonOutput) {
|
|
|
15286
15419
|
const code = readFileSync2(file, "utf-8");
|
|
15287
15420
|
const entries = extractStringsFromSource(
|
|
15288
15421
|
code,
|
|
15289
|
-
relative(root, file)
|
|
15422
|
+
relative(root, file),
|
|
15423
|
+
void 0,
|
|
15424
|
+
{ importSources: config.extractImportSources }
|
|
15290
15425
|
);
|
|
15291
15426
|
for (const entry of entries) {
|
|
15292
15427
|
extracted[entry.key] = entry.source;
|
package/dist/index.d.ts
CHANGED
|
@@ -36,6 +36,13 @@ interface SolidTranslatePluginConfig {
|
|
|
36
36
|
* Default: `["src/**\/*.tsx", "src/**\/*.ts", "src/**\/*.jsx"]`
|
|
37
37
|
*/
|
|
38
38
|
include?: string[];
|
|
39
|
+
/**
|
|
40
|
+
* Module specifiers accepted as sources of the extraction markers
|
|
41
|
+
* (`msg`, `<T>`, `<Plural>`, ...). `"solid-translate"` is always
|
|
42
|
+
* accepted. When omitted, any specifier whose final path segment is
|
|
43
|
+
* `solid-translate` or `i18n` (e.g. `"@/i18n"`) is accepted.
|
|
44
|
+
*/
|
|
45
|
+
extractImportSources?: string[];
|
|
39
46
|
}
|
|
40
47
|
/** A flat dictionary mapping keys to translated strings */
|
|
41
48
|
type TranslationDictionary = Record<string, string>;
|
package/dist/index.js
CHANGED
|
@@ -38,9 +38,16 @@ function normalizeLocale(locale) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// src/components.tsx
|
|
41
|
+
import { template as _$template } from "solid-js/web";
|
|
42
|
+
import { className as _$className } from "solid-js/web";
|
|
43
|
+
import { insert as _$insert } from "solid-js/web";
|
|
44
|
+
import { createComponent as _$createComponent } from "solid-js/web";
|
|
45
|
+
import { effect as _$effect } from "solid-js/web";
|
|
41
46
|
import { useContext, For, createMemo } from "solid-js";
|
|
47
|
+
var _tmpl$ = /* @__PURE__ */ _$template(`<select>`);
|
|
48
|
+
var _tmpl$2 = /* @__PURE__ */ _$template(`<option>`);
|
|
42
49
|
function Var(props) {
|
|
43
|
-
return (
|
|
50
|
+
return () => props.children;
|
|
44
51
|
}
|
|
45
52
|
Var.__st_var = true;
|
|
46
53
|
function Num(props) {
|
|
@@ -85,7 +92,9 @@ function Plural(props) {
|
|
|
85
92
|
};
|
|
86
93
|
const form = forms[category] ?? props.other;
|
|
87
94
|
if (ctx && typeof form === "string") {
|
|
88
|
-
return ctx.t(form, {
|
|
95
|
+
return ctx.t(form, {
|
|
96
|
+
n: props.n
|
|
97
|
+
});
|
|
89
98
|
}
|
|
90
99
|
return form;
|
|
91
100
|
});
|
|
@@ -93,29 +102,38 @@ function Plural(props) {
|
|
|
93
102
|
function LocaleSelector(props) {
|
|
94
103
|
const ctx = useContext(TranslationContext);
|
|
95
104
|
if (!ctx) {
|
|
96
|
-
throw new Error(
|
|
97
|
-
"<LocaleSelector> must be used within a <TranslationProvider>"
|
|
98
|
-
);
|
|
105
|
+
throw new Error("<LocaleSelector> must be used within a <TranslationProvider>");
|
|
99
106
|
}
|
|
100
107
|
const locales = createMemo(() => props.locales || ctx.availableLocales());
|
|
101
108
|
const displayName = (code) => {
|
|
102
109
|
if (props.labels?.[code]) return props.labels[code];
|
|
103
110
|
try {
|
|
104
|
-
const dn = new Intl.DisplayNames([code], {
|
|
111
|
+
const dn = new Intl.DisplayNames([code], {
|
|
112
|
+
type: "language"
|
|
113
|
+
});
|
|
105
114
|
return dn.of(code) || code;
|
|
106
115
|
} catch {
|
|
107
116
|
return code;
|
|
108
117
|
}
|
|
109
118
|
};
|
|
110
|
-
return
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
return (() => {
|
|
120
|
+
var _el$ = _tmpl$();
|
|
121
|
+
_el$.addEventListener("change", (e) => ctx.setLocale(e.currentTarget.value));
|
|
122
|
+
_$insert(_el$, _$createComponent(For, {
|
|
123
|
+
get each() {
|
|
124
|
+
return locales();
|
|
125
|
+
},
|
|
126
|
+
children: (code) => (() => {
|
|
127
|
+
var _el$2 = _tmpl$2();
|
|
128
|
+
_el$2.value = code;
|
|
129
|
+
_$insert(_el$2, () => displayName(code));
|
|
130
|
+
return _el$2;
|
|
131
|
+
})()
|
|
132
|
+
}));
|
|
133
|
+
_$effect(() => _$className(_el$, props.class));
|
|
134
|
+
_$effect(() => _el$.value = ctx.locale());
|
|
135
|
+
return _el$;
|
|
136
|
+
})();
|
|
119
137
|
}
|
|
120
138
|
|
|
121
139
|
// src/msg.ts
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/context.ts","../src/locale-detect.ts","../src/components.tsx","../src/msg.ts"],"sourcesContent":["import {\n createComponent,\n useContext,\n createSignal,\n createMemo,\n type JSX,\n} from \"solid-js\";\nimport {\n TranslationContext,\n type TranslationContextValue,\n} from \"./context.js\";\nimport { detectLocale } from \"./locale-detect.js\";\nimport type {\n LazyTranslations,\n TranslationDictionary,\n Translations,\n TranslationsInput,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { TranslationContextValue } from \"./context.js\";\nexport type {\n LazyTranslations,\n TranslationDictionary,\n Translations,\n TranslationsInput,\n} from \"./types.js\";\nexport type { SolidTranslatePluginConfig } from \"./types.js\";\nexport { Var, Num, Currency, DateTime, Plural, LocaleSelector } from \"./components.js\";\nexport type {\n VarProps,\n NumProps,\n CurrencyProps,\n DateTimeProps,\n PluralProps,\n LocaleSelectorProps,\n} from \"./components.js\";\nexport { msg } from \"./msg.js\";\nexport { detectLocale } from \"./locale-detect.js\";\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\nexport interface TranslationProviderProps {\n /**\n * Initial locale. If omitted, auto-detects from the browser's\n * `navigator.languages` header, falling back to `sourceLocale`.\n */\n locale?: string;\n /** Source locale code (default: \"en\") */\n sourceLocale?: string;\n /**\n * Translation dictionaries keyed by locale (from `virtual:solid-translate`),\n * or a lazy manifest (from `virtual:solid-translate/lazy`) whose per-locale\n * dictionaries are loaded on demand via dynamic import.\n */\n translations: TranslationsInput;\n /**\n * Persist the active locale to `localStorage` (default: false).\n * When enabled, the initial locale is read from storage (if still valid)\n * before falling back to browser detection, and `setLocale` writes through.\n * Pass `{ key: \"...\" }` to customize the storage key.\n */\n persistLocale?: boolean | { key?: string };\n children: JSX.Element;\n}\n\nconst DEFAULT_PERSIST_KEY = \"solid-translate:locale\";\n\nfunction isLazyTranslations(\n input: TranslationsInput,\n): input is LazyTranslations {\n return (\n typeof input === \"object\" &&\n input !== null &&\n Array.isArray((input as LazyTranslations).locales) &&\n typeof (input as LazyTranslations).loaders === \"object\" &&\n (input as LazyTranslations).loaders !== null\n );\n}\n\nfunction readPersistedLocale(key: string): string | undefined {\n try {\n if (typeof localStorage === \"undefined\") return undefined;\n return localStorage.getItem(key) ?? undefined;\n } catch {\n // SSR / storage disabled\n return undefined;\n }\n}\n\nfunction writePersistedLocale(key: string, locale: string): void {\n try {\n if (typeof localStorage === \"undefined\") return;\n localStorage.setItem(key, locale);\n } catch {\n // SSR / storage disabled / quota exceeded — ignore\n }\n}\n\nexport function TranslationProvider(props: TranslationProviderProps) {\n const lazy = isLazyTranslations(props.translations)\n ? props.translations\n : undefined;\n const sourceLocale = props.sourceLocale || lazy?.sourceLocale || \"en\";\n const availableLocales = createMemo(() =>\n lazy ? lazy.locales : Object.keys(props.translations),\n );\n\n const persistKey = props.persistLocale\n ? (typeof props.persistLocale === \"object\"\n ? props.persistLocale.key\n : undefined) || DEFAULT_PERSIST_KEY\n : undefined;\n\n // Initial locale: explicit prop > persisted value (if valid) > detection\n const persisted = persistKey ? readPersistedLocale(persistKey) : undefined;\n const persistedValid =\n persisted !== undefined &&\n (persisted === sourceLocale || availableLocales().includes(persisted));\n const initialLocale =\n props.locale ||\n (persistedValid ? persisted : undefined) ||\n detectLocale(availableLocales()) ||\n sourceLocale;\n const [locale, setLocaleSignal] = createSignal(initialLocale);\n\n // Lazily loaded dictionaries, keyed by locale (lazy manifest mode only).\n // Loading NEVER throws or suspends — while a dictionary is in flight,\n // t() falls back to the source text.\n const [loadedDicts, setLoadedDicts] = createSignal<Translations>({});\n const pendingLoads = new Set<string>();\n\n const loadLocale = (target: string): void => {\n if (!lazy) return;\n const loader = lazy.loaders[target];\n if (!loader) return;\n if (target in loadedDicts() || pendingLoads.has(target)) return;\n pendingLoads.add(target);\n loader()\n .then((dict) => {\n setLoadedDicts((prev) => ({ ...prev, [target]: dict }));\n })\n .catch((err) => {\n console.warn(\n `[solid-translate] Failed to load locale \"${target}\":`,\n err,\n );\n })\n .finally(() => {\n pendingLoads.delete(target);\n });\n };\n\n const setLocale = (next: string): void => {\n loadLocale(next);\n setLocaleSignal(next);\n if (persistKey) writePersistedLocale(persistKey, next);\n };\n\n // Kick off loading for the initial locale (no-op in eager mode, or when\n // the locale has no loader — e.g. the source locale without a dict).\n loadLocale(initialLocale);\n\n const t = (\n key: string,\n params?: Record<string, string | number>,\n ): string => {\n const cur = locale();\n let text = key;\n\n // Look up in translation dictionary (works for both source and target locales)\n const dict = lazy\n ? loadedDicts()[cur]\n : (props.translations as Translations)[cur];\n if (dict && key in dict) {\n text = dict[key]!;\n }\n\n // Interpolate {{variable}} and {variable} placeholders\n if (params) {\n for (const [k, v] of Object.entries(params)) {\n text = text.replace(\n new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}|\\\\{${k}\\\\}`, \"g\"),\n String(v),\n );\n }\n }\n\n return text;\n };\n\n const value: TranslationContextValue = {\n locale,\n setLocale,\n t,\n sourceLocale,\n availableLocales,\n translations: props.translations,\n };\n\n return createComponent(TranslationContext.Provider, {\n value,\n get children() {\n return props.children;\n },\n });\n}\n\n// ---------------------------------------------------------------------------\n// Hooks\n// ---------------------------------------------------------------------------\n\n/** Access the full translation context. Must be inside a TranslationProvider. */\nexport function useTranslation(): TranslationContextValue {\n const ctx = useContext(TranslationContext);\n if (!ctx) {\n throw new Error(\n \"useTranslation() must be used within a <TranslationProvider>\",\n );\n }\n return ctx;\n}\n\n/** Access just the current locale and setter. */\nexport function useLocale(): {\n locale: () => string;\n setLocale: (locale: string) => void;\n sourceLocale: string;\n availableLocales: () => string[];\n} {\n const ctx = useTranslation();\n return {\n locale: ctx.locale,\n setLocale: ctx.setLocale,\n sourceLocale: ctx.sourceLocale,\n availableLocales: ctx.availableLocales,\n };\n}\n\n// ---------------------------------------------------------------------------\n// <T> Component\n// ---------------------------------------------------------------------------\n\nexport interface TProps {\n /** Explicit translation key. If omitted, children text is used as the key. */\n id?: string;\n /** Interpolation parameters */\n params?: Record<string, string | number>;\n /**\n * AI context hint — tells the AI translator about the meaning of this text.\n * Only used at build time for disambiguation; has no runtime effect.\n *\n * ```tsx\n * <T context=\"Button to save a document, not save money\">Save</T>\n * ```\n */\n context?: string;\n /** Source text / JSX content */\n children?: JSX.Element;\n}\n\n/**\n * Translatable content component.\n *\n * ```tsx\n * <T>Hello world</T>\n * <T id=\"greeting\" params={{ name: \"Alice\" }}>Hello {{name}}</T>\n * <T context=\"the physical bank\">Bank</T>\n * <T>Welcome <Var>{userName()}</Var>, you have <Num>{count()}</Num> items</T>\n * ```\n */\nexport function T(props: TProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n // IMPORTANT: children are read raw, WITHOUT resolveChildren(). The Solid\n // compiler passes static text as plain strings and wraps every dynamic\n // part (expressions, <Var>, <Num>, elements) in a function or object —\n // that boundary is exactly what separates translatable text from {n}\n // slots. Resolving children first would collapse dynamic strings into\n // text and destroy the key.\n const kids = flattenChildren(props.children);\n\n // No context — just render children\n if (!ctx) return kids.length === 1 ? kids[0] : kids;\n\n // Build the template key + ordered slots from the raw children\n const slots: JSX.Element[] = [];\n let template = \"\";\n for (const kid of kids) {\n if (typeof kid === \"string\") {\n template += kid;\n } else if (typeof kid === \"number\") {\n template += String(kid);\n } else if (kid == null || typeof kid === \"boolean\") {\n // {null} / {undefined} / booleans render nothing\n } else {\n template += `{${slots.length}}`;\n slots.push(kid as JSX.Element);\n }\n }\n\n // Leading/trailing whitespace is layout, not copy — keep it out of the\n // key, restore it around the translation.\n const lead = /^\\s*/.exec(template)![0];\n const rest = template.slice(lead.length);\n const trail = /\\s*$/.exec(rest)![0];\n const body = rest.slice(0, rest.length - trail.length);\n\n const key = props.id || body;\n const translated = lead + ctx.t(key, props.params) + trail;\n\n // If translation has no slot placeholders, return it as plain text\n if (slots.length === 0 || !/{(\\d+)}/.test(translated)) return translated;\n\n return interpolateSlots(translated, slots);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Flatten (possibly nested) children arrays WITHOUT resolving functions */\nfunction flattenChildren(child: unknown, out: unknown[] = []): unknown[] {\n if (Array.isArray(child)) {\n for (const c of child) flattenChildren(c, out);\n } else {\n out.push(child);\n }\n return out;\n}\n\n/** Split a translated string by `{0}`, `{1}`, etc. and interleave with slots */\nfunction interpolateSlots(\n text: string,\n slots: JSX.Element[],\n): (string | JSX.Element)[] {\n const parts = text.split(/\\{(\\d+)\\}/);\n const result: (string | JSX.Element)[] = [];\n for (let i = 0; i < parts.length; i++) {\n if (i % 2 === 0) {\n if (parts[i]) result.push(parts[i]!);\n } else {\n const idx = parseInt(parts[i]!, 10);\n if (slots[idx] !== undefined) result.push(slots[idx]!);\n }\n }\n return result;\n}\n","import { createContext } from \"solid-js\";\nimport type { TranslationsInput } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Context value type\n// ---------------------------------------------------------------------------\n\nexport interface TranslationContextValue {\n /** Current locale as a reactive signal */\n locale: () => string;\n /** Switch to a different locale */\n setLocale: (locale: string) => void;\n /** Translate a key with optional interpolation params */\n t: (key: string, params?: Record<string, string | number>) => string;\n /** The source locale code */\n sourceLocale: string;\n /** All available locale codes (reactive) */\n availableLocales: () => string[];\n /** Raw translations object (eager record or lazy manifest) */\n translations: TranslationsInput;\n}\n\n// ---------------------------------------------------------------------------\n// Shared context instance\n// ---------------------------------------------------------------------------\n\nexport const TranslationContext = createContext<TranslationContextValue>();\n","/**\n * Detect the user's preferred locale from browser settings.\n *\n * Checks `navigator.languages` (and falls back to `navigator.language`)\n * then matches against the list of available locales. Tries exact match\n * first, then language-only match (e.g. \"en-US\" → \"en\").\n */\nexport function detectLocale(availableLocales?: string[]): string {\n if (typeof navigator === \"undefined\") return \"en\";\n\n const browserLocales = navigator.languages\n ? [...navigator.languages]\n : [navigator.language || \"en\"];\n\n if (!availableLocales || availableLocales.length === 0) {\n return normalizeLocale(browserLocales[0] || \"en\");\n }\n\n // Map normalized available locales back to their canonical casing so a\n // browser \"pt-br\" can match an available \"pt-BR\" (and return \"pt-BR\").\n const canonical = new Map<string, string>();\n for (const al of availableLocales) {\n const normalized = normalizeLocale(al);\n if (!canonical.has(normalized)) canonical.set(normalized, al);\n }\n\n // Exact match (case-insensitive)\n for (const bl of browserLocales) {\n const match = canonical.get(normalizeLocale(bl));\n if (match) return match;\n }\n\n // Language-only match (e.g. \"en-US\" → \"en\")\n for (const bl of browserLocales) {\n const lang = normalizeLocale(bl).split(\"-\")[0]!;\n const match = canonical.get(lang);\n if (match) return match;\n }\n\n return availableLocales[0] || \"en\";\n}\n\nfunction normalizeLocale(locale: string): string {\n return locale.toLowerCase().replace(\"_\", \"-\");\n}\n","import { useContext, type JSX, For, createMemo } from \"solid-js\";\nimport { TranslationContext } from \"./context.js\";\n\n// ---------------------------------------------------------------------------\n// <Var> — protect dynamic content from translation\n// ---------------------------------------------------------------------------\n\nexport interface VarProps {\n /** Optional name for the variable (used as placeholder in templates) */\n name?: string;\n children: JSX.Element;\n}\n\n/**\n * Marks content as untranslatable. When used inside `<T>`, the content\n * is preserved as-is while surrounding text is translated.\n *\n * ```tsx\n * <T>Hello <Var>{userName()}</Var>, welcome!</T>\n * ```\n */\nexport function Var(props: VarProps): JSX.Element {\n return (() => props.children) as unknown as JSX.Element;\n}\n\n// Mark Var for identification by T component\n(Var as any).__st_var = true;\n\n// ---------------------------------------------------------------------------\n// <Num> — locale-aware number formatting\n// ---------------------------------------------------------------------------\n\nexport interface NumProps {\n /** The number to format */\n children: number;\n /** Intl.NumberFormat options */\n options?: Intl.NumberFormatOptions;\n}\n\n/**\n * Formats a number according to the current locale using `Intl.NumberFormat`.\n *\n * ```tsx\n * <Num>{1000000}</Num> // \"1,000,000\" in en, \"1.000.000\" in de\n * <Num options={{ style: \"percent\" }}>{0.42}</Num> // \"42%\"\n * ```\n */\nexport function Num(props: NumProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n return new Intl.NumberFormat(locale, props.options).format(props.children);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <Currency> — locale-aware currency formatting\n// ---------------------------------------------------------------------------\n\nexport interface CurrencyProps {\n /** The numeric value */\n children: number;\n /** ISO 4217 currency code (e.g. \"USD\", \"EUR\") */\n currency: string;\n /** Additional Intl.NumberFormat options */\n options?: Intl.NumberFormatOptions;\n}\n\n/**\n * Formats a number as currency according to the current locale.\n *\n * ```tsx\n * <Currency currency=\"USD\">{29.99}</Currency> // \"$29.99\" in en-US\n * <Currency currency=\"EUR\">{29.99}</Currency> // \"29,99 €\" in de\n * ```\n */\nexport function Currency(props: CurrencyProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n return new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: props.currency,\n ...props.options,\n }).format(props.children);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <DateTime> — locale-aware date/time formatting\n// ---------------------------------------------------------------------------\n\nexport interface DateTimeProps {\n /** The date to format (Date object, timestamp, or ISO string) */\n children: Date | number | string;\n /** Intl.DateTimeFormat options */\n options?: Intl.DateTimeFormatOptions;\n}\n\n/**\n * Formats a date/time according to the current locale using `Intl.DateTimeFormat`.\n *\n * ```tsx\n * <DateTime>{new Date()}</DateTime>\n * <DateTime options={{ dateStyle: \"long\" }}>{new Date()}</DateTime>\n * ```\n */\nexport function DateTime(props: DateTimeProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n const date =\n props.children instanceof Date\n ? props.children\n : new Date(props.children);\n return new Intl.DateTimeFormat(locale, props.options).format(date);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <Plural> — CLDR plural rules\n// ---------------------------------------------------------------------------\n\nexport interface PluralProps {\n /** The count value to determine which plural form to use */\n n: number;\n /** Form for zero items */\n zero?: JSX.Element;\n /** Form for exactly one item */\n one?: JSX.Element;\n /** Form for exactly two items */\n two?: JSX.Element;\n /** Form for \"few\" items (language-dependent) */\n few?: JSX.Element;\n /** Form for \"many\" items (language-dependent) */\n many?: JSX.Element;\n /** Default/fallback form */\n other: JSX.Element;\n}\n\n/**\n * Renders the appropriate plural form based on CLDR plural rules for the current locale.\n *\n * String forms are translated through the translation dictionary (the source\n * string is the key — matching extraction) and support an `{n}` placeholder\n * interpolated with the count. Non-string forms render as-is, untranslated.\n *\n * ```tsx\n * <Plural n={count()}\n * zero=\"No items\"\n * one=\"1 item\"\n * other=\"{n} items\"\n * />\n * ```\n */\nexport function Plural(props: PluralProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n const rules = new Intl.PluralRules(locale);\n const category = rules.select(props.n);\n\n const forms: Record<string, JSX.Element | undefined> = {\n zero: props.zero,\n one: props.one,\n two: props.two,\n few: props.few,\n many: props.many,\n other: props.other,\n };\n\n const form = forms[category] ?? props.other;\n\n // Translate string forms through the dictionary, keyed by source string\n if (ctx && typeof form === \"string\") {\n return ctx.t(form, { n: props.n });\n }\n\n return form;\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <LocaleSelector> — drop-in locale picker\n// ---------------------------------------------------------------------------\n\nexport interface LocaleSelectorProps {\n /** Override which locales to show (defaults to all available) */\n locales?: string[];\n /** Map locale codes to display names, e.g. { en: \"English\", es: \"Español\" } */\n labels?: Record<string, string>;\n /** Additional CSS class */\n class?: string;\n}\n\n/**\n * A ready-to-use locale selector dropdown.\n *\n * ```tsx\n * <LocaleSelector labels={{ en: \"English\", es: \"Español\", fr: \"Français\" }} />\n * ```\n */\nexport function LocaleSelector(props: LocaleSelectorProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n if (!ctx) {\n throw new Error(\n \"<LocaleSelector> must be used within a <TranslationProvider>\",\n );\n }\n\n const locales = createMemo(() => props.locales || ctx.availableLocales());\n\n const displayName = (code: string): string => {\n if (props.labels?.[code]) return props.labels[code]!;\n try {\n const dn = new Intl.DisplayNames([code], { type: \"language\" });\n return dn.of(code) || code;\n } catch {\n return code;\n }\n };\n\n return (\n <select\n class={props.class}\n value={ctx.locale()}\n onChange={(e) => ctx.setLocale(e.currentTarget.value)}\n >\n <For each={locales()}>\n {(code) => <option value={code}>{displayName(code)}</option>}\n </For>\n </select>\n ) as JSX.Element;\n}\n","/**\n * Mark a string for translation extraction.\n *\n * At build time, the Vite plugin and CLI scan for `msg()` calls and add\n * the strings to the source locale file for AI translation.\n *\n * At runtime, `msg()` is a no-op — it returns the source text as-is.\n * Use `t()` from `useTranslation()` for runtime translation.\n *\n * ```ts\n * // Marks \"Save changes\" for extraction\n * const label = msg(\"Save changes\");\n *\n * // With interpolation template\n * const greeting = msg(\"Hello {{name}}\", { name: \"World\" });\n *\n * // In a component, translate at runtime:\n * const { t } = useTranslation();\n * <button>{t(label)}</button>\n * ```\n */\nexport function msg(\n text: string,\n _params?: Record<string, string | number>,\n): string {\n return text;\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA,cAAAA;AAAA,EACA;AAAA,EACA,cAAAC;AAAA,OAEK;;;ACNP,SAAS,qBAAqB;AA0BvB,IAAM,qBAAqB,cAAuC;;;ACnBlE,SAAS,aAAa,kBAAqC;AAChE,MAAI,OAAO,cAAc,YAAa,QAAO;AAE7C,QAAM,iBAAiB,UAAU,YAC7B,CAAC,GAAG,UAAU,SAAS,IACvB,CAAC,UAAU,YAAY,IAAI;AAE/B,MAAI,CAAC,oBAAoB,iBAAiB,WAAW,GAAG;AACtD,WAAO,gBAAgB,eAAe,CAAC,KAAK,IAAI;AAAA,EAClD;AAIA,QAAM,YAAY,oBAAI,IAAoB;AAC1C,aAAW,MAAM,kBAAkB;AACjC,UAAM,aAAa,gBAAgB,EAAE;AACrC,QAAI,CAAC,UAAU,IAAI,UAAU,EAAG,WAAU,IAAI,YAAY,EAAE;AAAA,EAC9D;AAGA,aAAW,MAAM,gBAAgB;AAC/B,UAAM,QAAQ,UAAU,IAAI,gBAAgB,EAAE,CAAC;AAC/C,QAAI,MAAO,QAAO;AAAA,EACpB;AAGA,aAAW,MAAM,gBAAgB;AAC/B,UAAM,OAAO,gBAAgB,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AAC7C,UAAM,QAAQ,UAAU,IAAI,IAAI;AAChC,QAAI,MAAO,QAAO;AAAA,EACpB;AAEA,SAAO,iBAAiB,CAAC,KAAK;AAChC;AAEA,SAAS,gBAAgB,QAAwB;AAC/C,SAAO,OAAO,YAAY,EAAE,QAAQ,KAAK,GAAG;AAC9C;;;AC5CA,SAAS,YAAsB,KAAK,kBAAkB;AAqB/C,SAAS,IAAI,OAA8B;AAChD,UAAQ,MAAM,MAAM;AACtB;AAGC,IAAY,WAAW;AAqBjB,SAAS,IAAI,OAA8B;AAChD,QAAM,MAAM,WAAW,kBAAkB;AAEzC,SAAO,WAAW,MAAM;AACtB,UAAM,SAAS,KAAK,OAAO,KAAK;AAChC,WAAO,IAAI,KAAK,aAAa,QAAQ,MAAM,OAAO,EAAE,OAAO,MAAM,QAAQ;AAAA,EAC3E,CAAC;AACH;AAuBO,SAAS,SAAS,OAAmC;AAC1D,QAAM,MAAM,WAAW,kBAAkB;AAEzC,SAAO,WAAW,MAAM;AACtB,UAAM,SAAS,KAAK,OAAO,KAAK;AAChC,WAAO,IAAI,KAAK,aAAa,QAAQ;AAAA,MACnC,OAAO;AAAA,MACP,UAAU,MAAM;AAAA,MAChB,GAAG,MAAM;AAAA,IACX,CAAC,EAAE,OAAO,MAAM,QAAQ;AAAA,EAC1B,CAAC;AACH;AAqBO,SAAS,SAAS,OAAmC;AAC1D,QAAM,MAAM,WAAW,kBAAkB;AAEzC,SAAO,WAAW,MAAM;AACtB,UAAM,SAAS,KAAK,OAAO,KAAK;AAChC,UAAM,OACJ,MAAM,oBAAoB,OACtB,MAAM,WACN,IAAI,KAAK,MAAM,QAAQ;AAC7B,WAAO,IAAI,KAAK,eAAe,QAAQ,MAAM,OAAO,EAAE,OAAO,IAAI;AAAA,EACnE,CAAC;AACH;AAsCO,SAAS,OAAO,OAAiC;AACtD,QAAM,MAAM,WAAW,kBAAkB;AAEzC,SAAO,WAAW,MAAM;AACtB,UAAM,SAAS,KAAK,OAAO,KAAK;AAChC,UAAM,QAAQ,IAAI,KAAK,YAAY,MAAM;AACzC,UAAM,WAAW,MAAM,OAAO,MAAM,CAAC;AAErC,UAAM,QAAiD;AAAA,MACrD,MAAM,MAAM;AAAA,MACZ,KAAK,MAAM;AAAA,MACX,KAAK,MAAM;AAAA,MACX,KAAK,MAAM;AAAA,MACX,MAAM,MAAM;AAAA,MACZ,OAAO,MAAM;AAAA,IACf;AAEA,UAAM,OAAO,MAAM,QAAQ,KAAK,MAAM;AAGtC,QAAI,OAAO,OAAO,SAAS,UAAU;AACnC,aAAO,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;AAAA,IACnC;AAEA,WAAO;AAAA,EACT,CAAC;AACH;AAsBO,SAAS,eAAe,OAAyC;AACtE,QAAM,MAAM,WAAW,kBAAkB;AACzC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAU,WAAW,MAAM,MAAM,WAAW,IAAI,iBAAiB,CAAC;AAExE,QAAM,cAAc,CAAC,SAAyB;AAC5C,QAAI,MAAM,SAAS,IAAI,EAAG,QAAO,MAAM,OAAO,IAAI;AAClD,QAAI;AACF,YAAM,KAAK,IAAI,KAAK,aAAa,CAAC,IAAI,GAAG,EAAE,MAAM,WAAW,CAAC;AAC7D,aAAO,GAAG,GAAG,IAAI,KAAK;AAAA,IACxB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SACE,CAAC;AAAA,IACC,OAAO,MAAM;AAAA,IACb,OAAO,IAAI,OAAO;AAAA,IAClB,UAAU,CAAC,MAAM,IAAI,UAAU,EAAE,cAAc,KAAK;AAAA,GACrD;AAAA,MACC,CAAC,IAAI,MAAM,QAAQ,GAAG;AAAA,SACnB,CAAC,SAAS,CAAC,OAAO,OAAO,OAAO,YAAY,IAAI,EAAE,EAAvC,QAAiD;AAAA,MAC/D,EAFC,IAEK;AAAA,IACR,EARC;AAUL;;;ACxNO,SAAS,IACd,MACA,SACQ;AACR,SAAO;AACT;;;AJ6CA,IAAM,sBAAsB;AAE5B,SAAS,mBACP,OAC2B;AAC3B,SACE,OAAO,UAAU,YACjB,UAAU,QACV,MAAM,QAAS,MAA2B,OAAO,KACjD,OAAQ,MAA2B,YAAY,YAC9C,MAA2B,YAAY;AAE5C;AAEA,SAAS,oBAAoB,KAAiC;AAC5D,MAAI;AACF,QAAI,OAAO,iBAAiB,YAAa,QAAO;AAChD,WAAO,aAAa,QAAQ,GAAG,KAAK;AAAA,EACtC,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,qBAAqB,KAAa,QAAsB;AAC/D,MAAI;AACF,QAAI,OAAO,iBAAiB,YAAa;AACzC,iBAAa,QAAQ,KAAK,MAAM;AAAA,EAClC,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,oBAAoB,OAAiC;AACnE,QAAM,OAAO,mBAAmB,MAAM,YAAY,IAC9C,MAAM,eACN;AACJ,QAAM,eAAe,MAAM,gBAAgB,MAAM,gBAAgB;AACjE,QAAM,mBAAmBC;AAAA,IAAW,MAClC,OAAO,KAAK,UAAU,OAAO,KAAK,MAAM,YAAY;AAAA,EACtD;AAEA,QAAM,aAAa,MAAM,iBACpB,OAAO,MAAM,kBAAkB,WAC5B,MAAM,cAAc,MACpB,WAAc,sBAClB;AAGJ,QAAM,YAAY,aAAa,oBAAoB,UAAU,IAAI;AACjE,QAAM,iBACJ,cAAc,WACb,cAAc,gBAAgB,iBAAiB,EAAE,SAAS,SAAS;AACtE,QAAM,gBACJ,MAAM,WACL,iBAAiB,YAAY,WAC9B,aAAa,iBAAiB,CAAC,KAC/B;AACF,QAAM,CAAC,QAAQ,eAAe,IAAI,aAAa,aAAa;AAK5D,QAAM,CAAC,aAAa,cAAc,IAAI,aAA2B,CAAC,CAAC;AACnE,QAAM,eAAe,oBAAI,IAAY;AAErC,QAAM,aAAa,CAAC,WAAyB;AAC3C,QAAI,CAAC,KAAM;AACX,UAAM,SAAS,KAAK,QAAQ,MAAM;AAClC,QAAI,CAAC,OAAQ;AACb,QAAI,UAAU,YAAY,KAAK,aAAa,IAAI,MAAM,EAAG;AACzD,iBAAa,IAAI,MAAM;AACvB,WAAO,EACJ,KAAK,CAAC,SAAS;AACd,qBAAe,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,MAAM,GAAG,KAAK,EAAE;AAAA,IACxD,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,cAAQ;AAAA,QACN,4CAA4C,MAAM;AAAA,QAClD;AAAA,MACF;AAAA,IACF,CAAC,EACA,QAAQ,MAAM;AACb,mBAAa,OAAO,MAAM;AAAA,IAC5B,CAAC;AAAA,EACL;AAEA,QAAM,YAAY,CAAC,SAAuB;AACxC,eAAW,IAAI;AACf,oBAAgB,IAAI;AACpB,QAAI,WAAY,sBAAqB,YAAY,IAAI;AAAA,EACvD;AAIA,aAAW,aAAa;AAExB,QAAM,IAAI,CACR,KACA,WACW;AACX,UAAM,MAAM,OAAO;AACnB,QAAI,OAAO;AAGX,UAAM,OAAO,OACT,YAAY,EAAE,GAAG,IAChB,MAAM,aAA8B,GAAG;AAC5C,QAAI,QAAQ,OAAO,MAAM;AACvB,aAAO,KAAK,GAAG;AAAA,IACjB;AAGA,QAAI,QAAQ;AACV,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC3C,eAAO,KAAK;AAAA,UACV,IAAI,OAAO,SAAS,CAAC,aAAa,CAAC,OAAO,GAAG;AAAA,UAC7C,OAAO,CAAC;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAEA,QAAM,QAAiC;AAAA,IACrC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc,MAAM;AAAA,EACtB;AAEA,SAAO,gBAAgB,mBAAmB,UAAU;AAAA,IAClD;AAAA,IACA,IAAI,WAAW;AACb,aAAO,MAAM;AAAA,IACf;AAAA,EACF,CAAC;AACH;AAOO,SAAS,iBAA0C;AACxD,QAAM,MAAMC,YAAW,kBAAkB;AACzC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,YAKd;AACA,QAAM,MAAM,eAAe;AAC3B,SAAO;AAAA,IACL,QAAQ,IAAI;AAAA,IACZ,WAAW,IAAI;AAAA,IACf,cAAc,IAAI;AAAA,IAClB,kBAAkB,IAAI;AAAA,EACxB;AACF;AAkCO,SAAS,EAAE,OAA4B;AAC5C,QAAM,MAAMA,YAAW,kBAAkB;AAEzC,SAAOD,YAAW,MAAM;AAOtB,UAAM,OAAO,gBAAgB,MAAM,QAAQ;AAG3C,QAAI,CAAC,IAAK,QAAO,KAAK,WAAW,IAAI,KAAK,CAAC,IAAI;AAG/C,UAAM,QAAuB,CAAC;AAC9B,QAAI,WAAW;AACf,eAAW,OAAO,MAAM;AACtB,UAAI,OAAO,QAAQ,UAAU;AAC3B,oBAAY;AAAA,MACd,WAAW,OAAO,QAAQ,UAAU;AAClC,oBAAY,OAAO,GAAG;AAAA,MACxB,WAAW,OAAO,QAAQ,OAAO,QAAQ,WAAW;AAAA,MAEpD,OAAO;AACL,oBAAY,IAAI,MAAM,MAAM;AAC5B,cAAM,KAAK,GAAkB;AAAA,MAC/B;AAAA,IACF;AAIA,UAAM,OAAO,OAAO,KAAK,QAAQ,EAAG,CAAC;AACrC,UAAM,OAAO,SAAS,MAAM,KAAK,MAAM;AACvC,UAAM,QAAQ,OAAO,KAAK,IAAI,EAAG,CAAC;AAClC,UAAM,OAAO,KAAK,MAAM,GAAG,KAAK,SAAS,MAAM,MAAM;AAErD,UAAM,MAAM,MAAM,MAAM;AACxB,UAAM,aAAa,OAAO,IAAI,EAAE,KAAK,MAAM,MAAM,IAAI;AAGrD,QAAI,MAAM,WAAW,KAAK,CAAC,UAAU,KAAK,UAAU,EAAG,QAAO;AAE9D,WAAO,iBAAiB,YAAY,KAAK;AAAA,EAC3C,CAAC;AACH;AAOA,SAAS,gBAAgB,OAAgB,MAAiB,CAAC,GAAc;AACvE,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,eAAW,KAAK,MAAO,iBAAgB,GAAG,GAAG;AAAA,EAC/C,OAAO;AACL,QAAI,KAAK,KAAK;AAAA,EAChB;AACA,SAAO;AACT;AAGA,SAAS,iBACP,MACA,OAC0B;AAC1B,QAAM,QAAQ,KAAK,MAAM,WAAW;AACpC,QAAM,SAAmC,CAAC;AAC1C,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,QAAI,IAAI,MAAM,GAAG;AACf,UAAI,MAAM,CAAC,EAAG,QAAO,KAAK,MAAM,CAAC,CAAE;AAAA,IACrC,OAAO;AACL,YAAM,MAAM,SAAS,MAAM,CAAC,GAAI,EAAE;AAClC,UAAI,MAAM,GAAG,MAAM,OAAW,QAAO,KAAK,MAAM,GAAG,CAAE;AAAA,IACvD;AAAA,EACF;AACA,SAAO;AACT;","names":["useContext","createMemo","createMemo","useContext"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/context.ts","../src/locale-detect.ts","../src/components.tsx","../src/msg.ts"],"sourcesContent":["import {\n createComponent,\n useContext,\n createSignal,\n createMemo,\n type JSX,\n} from \"solid-js\";\nimport {\n TranslationContext,\n type TranslationContextValue,\n} from \"./context.js\";\nimport { detectLocale } from \"./locale-detect.js\";\nimport type {\n LazyTranslations,\n TranslationDictionary,\n Translations,\n TranslationsInput,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { TranslationContextValue } from \"./context.js\";\nexport type {\n LazyTranslations,\n TranslationDictionary,\n Translations,\n TranslationsInput,\n} from \"./types.js\";\nexport type { SolidTranslatePluginConfig } from \"./types.js\";\nexport { Var, Num, Currency, DateTime, Plural, LocaleSelector } from \"./components.js\";\nexport type {\n VarProps,\n NumProps,\n CurrencyProps,\n DateTimeProps,\n PluralProps,\n LocaleSelectorProps,\n} from \"./components.js\";\nexport { msg } from \"./msg.js\";\nexport { detectLocale } from \"./locale-detect.js\";\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\nexport interface TranslationProviderProps {\n /**\n * Initial locale. If omitted, auto-detects from the browser's\n * `navigator.languages` header, falling back to `sourceLocale`.\n */\n locale?: string;\n /** Source locale code (default: \"en\") */\n sourceLocale?: string;\n /**\n * Translation dictionaries keyed by locale (from `virtual:solid-translate`),\n * or a lazy manifest (from `virtual:solid-translate/lazy`) whose per-locale\n * dictionaries are loaded on demand via dynamic import.\n */\n translations: TranslationsInput;\n /**\n * Persist the active locale to `localStorage` (default: false).\n * When enabled, the initial locale is read from storage (if still valid)\n * before falling back to browser detection, and `setLocale` writes through.\n * Pass `{ key: \"...\" }` to customize the storage key.\n */\n persistLocale?: boolean | { key?: string };\n children: JSX.Element;\n}\n\nconst DEFAULT_PERSIST_KEY = \"solid-translate:locale\";\n\nfunction isLazyTranslations(\n input: TranslationsInput,\n): input is LazyTranslations {\n return (\n typeof input === \"object\" &&\n input !== null &&\n Array.isArray((input as LazyTranslations).locales) &&\n typeof (input as LazyTranslations).loaders === \"object\" &&\n (input as LazyTranslations).loaders !== null\n );\n}\n\nfunction readPersistedLocale(key: string): string | undefined {\n try {\n if (typeof localStorage === \"undefined\") return undefined;\n return localStorage.getItem(key) ?? undefined;\n } catch {\n // SSR / storage disabled\n return undefined;\n }\n}\n\nfunction writePersistedLocale(key: string, locale: string): void {\n try {\n if (typeof localStorage === \"undefined\") return;\n localStorage.setItem(key, locale);\n } catch {\n // SSR / storage disabled / quota exceeded — ignore\n }\n}\n\nexport function TranslationProvider(props: TranslationProviderProps) {\n const lazy = isLazyTranslations(props.translations)\n ? props.translations\n : undefined;\n const sourceLocale = props.sourceLocale || lazy?.sourceLocale || \"en\";\n const availableLocales = createMemo(() =>\n lazy ? lazy.locales : Object.keys(props.translations),\n );\n\n const persistKey = props.persistLocale\n ? (typeof props.persistLocale === \"object\"\n ? props.persistLocale.key\n : undefined) || DEFAULT_PERSIST_KEY\n : undefined;\n\n // Initial locale: explicit prop > persisted value (if valid) > detection\n const persisted = persistKey ? readPersistedLocale(persistKey) : undefined;\n const persistedValid =\n persisted !== undefined &&\n (persisted === sourceLocale || availableLocales().includes(persisted));\n const initialLocale =\n props.locale ||\n (persistedValid ? persisted : undefined) ||\n detectLocale(availableLocales()) ||\n sourceLocale;\n const [locale, setLocaleSignal] = createSignal(initialLocale);\n\n // Lazily loaded dictionaries, keyed by locale (lazy manifest mode only).\n // Loading NEVER throws or suspends — while a dictionary is in flight,\n // t() falls back to the source text.\n const [loadedDicts, setLoadedDicts] = createSignal<Translations>({});\n const pendingLoads = new Set<string>();\n\n const loadLocale = (target: string): void => {\n if (!lazy) return;\n const loader = lazy.loaders[target];\n if (!loader) return;\n if (target in loadedDicts() || pendingLoads.has(target)) return;\n pendingLoads.add(target);\n loader()\n .then((dict) => {\n setLoadedDicts((prev) => ({ ...prev, [target]: dict }));\n })\n .catch((err) => {\n console.warn(\n `[solid-translate] Failed to load locale \"${target}\":`,\n err,\n );\n })\n .finally(() => {\n pendingLoads.delete(target);\n });\n };\n\n const setLocale = (next: string): void => {\n loadLocale(next);\n setLocaleSignal(next);\n if (persistKey) writePersistedLocale(persistKey, next);\n };\n\n // Kick off loading for the initial locale (no-op in eager mode, or when\n // the locale has no loader — e.g. the source locale without a dict).\n loadLocale(initialLocale);\n\n const t = (\n key: string,\n params?: Record<string, string | number>,\n ): string => {\n const cur = locale();\n let text = key;\n\n // Look up in translation dictionary (works for both source and target locales)\n const dict = lazy\n ? loadedDicts()[cur]\n : (props.translations as Translations)[cur];\n if (dict && key in dict) {\n text = dict[key]!;\n }\n\n // Interpolate {{variable}} and {variable} placeholders\n if (params) {\n for (const [k, v] of Object.entries(params)) {\n text = text.replace(\n new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}|\\\\{${k}\\\\}`, \"g\"),\n String(v),\n );\n }\n }\n\n return text;\n };\n\n const value: TranslationContextValue = {\n locale,\n setLocale,\n t,\n sourceLocale,\n availableLocales,\n translations: props.translations,\n };\n\n return createComponent(TranslationContext.Provider, {\n value,\n get children() {\n return props.children;\n },\n });\n}\n\n// ---------------------------------------------------------------------------\n// Hooks\n// ---------------------------------------------------------------------------\n\n/** Access the full translation context. Must be inside a TranslationProvider. */\nexport function useTranslation(): TranslationContextValue {\n const ctx = useContext(TranslationContext);\n if (!ctx) {\n throw new Error(\n \"useTranslation() must be used within a <TranslationProvider>\",\n );\n }\n return ctx;\n}\n\n/** Access just the current locale and setter. */\nexport function useLocale(): {\n locale: () => string;\n setLocale: (locale: string) => void;\n sourceLocale: string;\n availableLocales: () => string[];\n} {\n const ctx = useTranslation();\n return {\n locale: ctx.locale,\n setLocale: ctx.setLocale,\n sourceLocale: ctx.sourceLocale,\n availableLocales: ctx.availableLocales,\n };\n}\n\n// ---------------------------------------------------------------------------\n// <T> Component\n// ---------------------------------------------------------------------------\n\nexport interface TProps {\n /** Explicit translation key. If omitted, children text is used as the key. */\n id?: string;\n /** Interpolation parameters */\n params?: Record<string, string | number>;\n /**\n * AI context hint — tells the AI translator about the meaning of this text.\n * Only used at build time for disambiguation; has no runtime effect.\n *\n * ```tsx\n * <T context=\"Button to save a document, not save money\">Save</T>\n * ```\n */\n context?: string;\n /** Source text / JSX content */\n children?: JSX.Element;\n}\n\n/**\n * Translatable content component.\n *\n * ```tsx\n * <T>Hello world</T>\n * <T id=\"greeting\" params={{ name: \"Alice\" }}>Hello {{name}}</T>\n * <T context=\"the physical bank\">Bank</T>\n * <T>Welcome <Var>{userName()}</Var>, you have <Num>{count()}</Num> items</T>\n * ```\n */\nexport function T(props: TProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n // IMPORTANT: children are read raw, WITHOUT resolveChildren(). The Solid\n // compiler passes static text as plain strings and wraps every dynamic\n // part (expressions, <Var>, <Num>, elements) in a function or object —\n // that boundary is exactly what separates translatable text from {n}\n // slots. Resolving children first would collapse dynamic strings into\n // text and destroy the key.\n const kids = flattenChildren(props.children);\n\n // No context — just render children\n if (!ctx) return kids.length === 1 ? kids[0] : kids;\n\n // Build the template key + ordered slots from the raw children\n const slots: JSX.Element[] = [];\n let template = \"\";\n for (const kid of kids) {\n if (typeof kid === \"string\") {\n template += kid;\n } else if (typeof kid === \"number\") {\n template += String(kid);\n } else if (kid == null || typeof kid === \"boolean\") {\n // {null} / {undefined} / booleans render nothing\n } else {\n template += `{${slots.length}}`;\n slots.push(kid as JSX.Element);\n }\n }\n\n // Leading/trailing whitespace is layout, not copy — keep it out of the\n // key, restore it around the translation.\n const lead = /^\\s*/.exec(template)![0];\n const rest = template.slice(lead.length);\n const trail = /\\s*$/.exec(rest)![0];\n const body = rest.slice(0, rest.length - trail.length);\n\n const key = props.id || body;\n const translated = lead + ctx.t(key, props.params) + trail;\n\n // If translation has no slot placeholders, return it as plain text\n if (slots.length === 0 || !/{(\\d+)}/.test(translated)) return translated;\n\n return interpolateSlots(translated, slots);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Flatten (possibly nested) children arrays WITHOUT resolving functions */\nfunction flattenChildren(child: unknown, out: unknown[] = []): unknown[] {\n if (Array.isArray(child)) {\n for (const c of child) flattenChildren(c, out);\n } else {\n out.push(child);\n }\n return out;\n}\n\n/** Split a translated string by `{0}`, `{1}`, etc. and interleave with slots */\nfunction interpolateSlots(\n text: string,\n slots: JSX.Element[],\n): (string | JSX.Element)[] {\n const parts = text.split(/\\{(\\d+)\\}/);\n const result: (string | JSX.Element)[] = [];\n for (let i = 0; i < parts.length; i++) {\n if (i % 2 === 0) {\n if (parts[i]) result.push(parts[i]!);\n } else {\n const idx = parseInt(parts[i]!, 10);\n if (slots[idx] !== undefined) result.push(slots[idx]!);\n }\n }\n return result;\n}\n","import { createContext } from \"solid-js\";\nimport type { TranslationsInput } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Context value type\n// ---------------------------------------------------------------------------\n\nexport interface TranslationContextValue {\n /** Current locale as a reactive signal */\n locale: () => string;\n /** Switch to a different locale */\n setLocale: (locale: string) => void;\n /** Translate a key with optional interpolation params */\n t: (key: string, params?: Record<string, string | number>) => string;\n /** The source locale code */\n sourceLocale: string;\n /** All available locale codes (reactive) */\n availableLocales: () => string[];\n /** Raw translations object (eager record or lazy manifest) */\n translations: TranslationsInput;\n}\n\n// ---------------------------------------------------------------------------\n// Shared context instance\n// ---------------------------------------------------------------------------\n\nexport const TranslationContext = createContext<TranslationContextValue>();\n","/**\n * Detect the user's preferred locale from browser settings.\n *\n * Checks `navigator.languages` (and falls back to `navigator.language`)\n * then matches against the list of available locales. Tries exact match\n * first, then language-only match (e.g. \"en-US\" → \"en\").\n */\nexport function detectLocale(availableLocales?: string[]): string {\n if (typeof navigator === \"undefined\") return \"en\";\n\n const browserLocales = navigator.languages\n ? [...navigator.languages]\n : [navigator.language || \"en\"];\n\n if (!availableLocales || availableLocales.length === 0) {\n return normalizeLocale(browserLocales[0] || \"en\");\n }\n\n // Map normalized available locales back to their canonical casing so a\n // browser \"pt-br\" can match an available \"pt-BR\" (and return \"pt-BR\").\n const canonical = new Map<string, string>();\n for (const al of availableLocales) {\n const normalized = normalizeLocale(al);\n if (!canonical.has(normalized)) canonical.set(normalized, al);\n }\n\n // Exact match (case-insensitive)\n for (const bl of browserLocales) {\n const match = canonical.get(normalizeLocale(bl));\n if (match) return match;\n }\n\n // Language-only match (e.g. \"en-US\" → \"en\")\n for (const bl of browserLocales) {\n const lang = normalizeLocale(bl).split(\"-\")[0]!;\n const match = canonical.get(lang);\n if (match) return match;\n }\n\n return availableLocales[0] || \"en\";\n}\n\nfunction normalizeLocale(locale: string): string {\n return locale.toLowerCase().replace(\"_\", \"-\");\n}\n","import { useContext, type JSX, For, createMemo } from \"solid-js\";\nimport { TranslationContext } from \"./context.js\";\n\n// ---------------------------------------------------------------------------\n// <Var> — protect dynamic content from translation\n// ---------------------------------------------------------------------------\n\nexport interface VarProps {\n /** Optional name for the variable (used as placeholder in templates) */\n name?: string;\n children: JSX.Element;\n}\n\n/**\n * Marks content as untranslatable. When used inside `<T>`, the content\n * is preserved as-is while surrounding text is translated.\n *\n * ```tsx\n * <T>Hello <Var>{userName()}</Var>, welcome!</T>\n * ```\n */\nexport function Var(props: VarProps): JSX.Element {\n return (() => props.children) as unknown as JSX.Element;\n}\n\n// Mark Var for identification by T component\n(Var as any).__st_var = true;\n\n// ---------------------------------------------------------------------------\n// <Num> — locale-aware number formatting\n// ---------------------------------------------------------------------------\n\nexport interface NumProps {\n /** The number to format */\n children: number;\n /** Intl.NumberFormat options */\n options?: Intl.NumberFormatOptions;\n}\n\n/**\n * Formats a number according to the current locale using `Intl.NumberFormat`.\n *\n * ```tsx\n * <Num>{1000000}</Num> // \"1,000,000\" in en, \"1.000.000\" in de\n * <Num options={{ style: \"percent\" }}>{0.42}</Num> // \"42%\"\n * ```\n */\nexport function Num(props: NumProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n return new Intl.NumberFormat(locale, props.options).format(props.children);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <Currency> — locale-aware currency formatting\n// ---------------------------------------------------------------------------\n\nexport interface CurrencyProps {\n /** The numeric value */\n children: number;\n /** ISO 4217 currency code (e.g. \"USD\", \"EUR\") */\n currency: string;\n /** Additional Intl.NumberFormat options */\n options?: Intl.NumberFormatOptions;\n}\n\n/**\n * Formats a number as currency according to the current locale.\n *\n * ```tsx\n * <Currency currency=\"USD\">{29.99}</Currency> // \"$29.99\" in en-US\n * <Currency currency=\"EUR\">{29.99}</Currency> // \"29,99 €\" in de\n * ```\n */\nexport function Currency(props: CurrencyProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n return new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: props.currency,\n ...props.options,\n }).format(props.children);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <DateTime> — locale-aware date/time formatting\n// ---------------------------------------------------------------------------\n\nexport interface DateTimeProps {\n /** The date to format (Date object, timestamp, or ISO string) */\n children: Date | number | string;\n /** Intl.DateTimeFormat options */\n options?: Intl.DateTimeFormatOptions;\n}\n\n/**\n * Formats a date/time according to the current locale using `Intl.DateTimeFormat`.\n *\n * ```tsx\n * <DateTime>{new Date()}</DateTime>\n * <DateTime options={{ dateStyle: \"long\" }}>{new Date()}</DateTime>\n * ```\n */\nexport function DateTime(props: DateTimeProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n const date =\n props.children instanceof Date\n ? props.children\n : new Date(props.children);\n return new Intl.DateTimeFormat(locale, props.options).format(date);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <Plural> — CLDR plural rules\n// ---------------------------------------------------------------------------\n\nexport interface PluralProps {\n /** The count value to determine which plural form to use */\n n: number;\n /** Form for zero items */\n zero?: JSX.Element;\n /** Form for exactly one item */\n one?: JSX.Element;\n /** Form for exactly two items */\n two?: JSX.Element;\n /** Form for \"few\" items (language-dependent) */\n few?: JSX.Element;\n /** Form for \"many\" items (language-dependent) */\n many?: JSX.Element;\n /** Default/fallback form */\n other: JSX.Element;\n}\n\n/**\n * Renders the appropriate plural form based on CLDR plural rules for the current locale.\n *\n * String forms are translated through the translation dictionary (the source\n * string is the key — matching extraction) and support an `{n}` placeholder\n * interpolated with the count. Non-string forms render as-is, untranslated.\n *\n * ```tsx\n * <Plural n={count()}\n * zero=\"No items\"\n * one=\"1 item\"\n * other=\"{n} items\"\n * />\n * ```\n */\nexport function Plural(props: PluralProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n const rules = new Intl.PluralRules(locale);\n const category = rules.select(props.n);\n\n const forms: Record<string, JSX.Element | undefined> = {\n zero: props.zero,\n one: props.one,\n two: props.two,\n few: props.few,\n many: props.many,\n other: props.other,\n };\n\n const form = forms[category] ?? props.other;\n\n // Translate string forms through the dictionary, keyed by source string\n if (ctx && typeof form === \"string\") {\n return ctx.t(form, { n: props.n });\n }\n\n return form;\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <LocaleSelector> — drop-in locale picker\n// ---------------------------------------------------------------------------\n\nexport interface LocaleSelectorProps {\n /** Override which locales to show (defaults to all available) */\n locales?: string[];\n /** Map locale codes to display names, e.g. { en: \"English\", es: \"Español\" } */\n labels?: Record<string, string>;\n /** Additional CSS class */\n class?: string;\n}\n\n/**\n * A ready-to-use locale selector dropdown.\n *\n * ```tsx\n * <LocaleSelector labels={{ en: \"English\", es: \"Español\", fr: \"Français\" }} />\n * ```\n */\nexport function LocaleSelector(props: LocaleSelectorProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n if (!ctx) {\n throw new Error(\n \"<LocaleSelector> must be used within a <TranslationProvider>\",\n );\n }\n\n const locales = createMemo(() => props.locales || ctx.availableLocales());\n\n const displayName = (code: string): string => {\n if (props.labels?.[code]) return props.labels[code]!;\n try {\n const dn = new Intl.DisplayNames([code], { type: \"language\" });\n return dn.of(code) || code;\n } catch {\n return code;\n }\n };\n\n return (\n <select\n class={props.class}\n value={ctx.locale()}\n onChange={(e) => ctx.setLocale(e.currentTarget.value)}\n >\n <For each={locales()}>\n {(code) => <option value={code}>{displayName(code)}</option>}\n </For>\n </select>\n ) as JSX.Element;\n}\n","/**\n * Mark a string for translation extraction.\n *\n * At build time, the Vite plugin and CLI scan for `msg()` calls and add\n * the strings to the source locale file for AI translation.\n *\n * At runtime, `msg()` is a no-op — it returns the source text as-is.\n * Use `t()` from `useTranslation()` for runtime translation.\n *\n * ```ts\n * // Marks \"Save changes\" for extraction\n * const label = msg(\"Save changes\");\n *\n * // With interpolation template\n * const greeting = msg(\"Hello {{name}}\", { name: \"World\" });\n *\n * // In a component, translate at runtime:\n * const { t } = useTranslation();\n * <button>{t(label)}</button>\n * ```\n */\nexport function msg(\n text: string,\n _params?: Record<string, string | number>,\n): string {\n return text;\n}\n"],"mappings":"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","names":["useContext","createMemo","useContext","For","createMemo","Var","props","children","__st_var","Num","ctx","useContext","TranslationContext","createMemo","locale","Intl","NumberFormat","options","format","Currency","style","currency","DateTime","date","Date","DateTimeFormat","Plural","rules","PluralRules","category","select","n","forms","zero","one","two","few","many","other","form","t","LocaleSelector","Error","locales","availableLocales","displayName","code","labels","dn","DisplayNames","type","of","_el$","_tmpl$","addEventListener","e","setLocale","currentTarget","value","_$insert","_$createComponent","For","each","_el$2","_tmpl$2","_$effect","_$className","class","createMemo","useContext"]}
|
package/dist/vite.d.ts
CHANGED
|
@@ -36,6 +36,13 @@ interface SolidTranslatePluginConfig {
|
|
|
36
36
|
* Default: `["src/**\/*.tsx", "src/**\/*.ts", "src/**\/*.jsx"]`
|
|
37
37
|
*/
|
|
38
38
|
include?: string[];
|
|
39
|
+
/**
|
|
40
|
+
* Module specifiers accepted as sources of the extraction markers
|
|
41
|
+
* (`msg`, `<T>`, `<Plural>`, ...). `"solid-translate"` is always
|
|
42
|
+
* accepted. When omitted, any specifier whose final path segment is
|
|
43
|
+
* `solid-translate` or `i18n` (e.g. `"@/i18n"`) is accepted.
|
|
44
|
+
*/
|
|
45
|
+
extractImportSources?: string[];
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
/**
|