@rimelight/i18n 0.0.17 → 0.0.20
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/cli.d.mts +3 -0
- package/dist/cli.mjs +137 -0
- package/dist/hono.d.mts +19 -1
- package/dist/hono.mjs +22 -22
- package/dist/index.d.mts +11 -6
- package/dist/index.mjs +3 -3
- package/dist/runtime-B2W6s31-.d.mts +60 -0
- package/dist/runtime-D8XFXD5B.mjs +164 -0
- package/dist/runtime.d.mts +2 -2
- package/dist/runtime.mjs +2 -2
- package/dist/{src-BDG_duDk.mjs → src--oNux7GS.mjs} +9 -5
- package/dist/types-PBnVVzPm.d.mts +64 -0
- package/dist/types.d.mts +2 -2
- package/dist/utils-WDscZN9H.mjs +202 -0
- package/dist/utils.d.mts +37 -6
- package/dist/utils.mjs +2 -43
- package/package.json +10 -7
- package/dist/runtime-Bc8I8sA4.d.mts +0 -72
- package/dist/runtime-LitYNVlO.mjs +0 -183
- package/dist/types-dxt4IMnV.d.mts +0 -32
package/dist/cli.d.mts
ADDED
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as fixTranslations, l as verifyTranslations, s as formatAuditReport } from "./utils-WDscZN9H.mjs";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
//#region src/cli.ts
|
|
8
|
+
const HELP_TEXT = `
|
|
9
|
+
Usage:
|
|
10
|
+
rimelight-i18n [action] [options]
|
|
11
|
+
|
|
12
|
+
Actions:
|
|
13
|
+
check (default) Verify translation files across locales for missing, stale, or invalid keys
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
-d, --dir <path> Directory containing <locale>.json files (auto-discovered if omitted)
|
|
17
|
+
-b, --base <locale> Base/default locale code to compare against (default: "en")
|
|
18
|
+
-f, --fix Auto-prune stale keys and generate missing key skeletons
|
|
19
|
+
--allow-empty Do not treat empty strings as missing/invalid translations
|
|
20
|
+
-h, --help Show this help message
|
|
21
|
+
`;
|
|
22
|
+
function findLocalesDir(customDir) {
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
if (customDir) {
|
|
25
|
+
const resolved = path.resolve(cwd, customDir);
|
|
26
|
+
return fs.existsSync(resolved) ? resolved : null;
|
|
27
|
+
}
|
|
28
|
+
for (const rel of [
|
|
29
|
+
"src/i18n",
|
|
30
|
+
"i18n",
|
|
31
|
+
"src/locales",
|
|
32
|
+
"locales",
|
|
33
|
+
"src/assets/i18n"
|
|
34
|
+
]) {
|
|
35
|
+
const resolved = path.join(cwd, rel);
|
|
36
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) return resolved;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
async function runCli(argv = process.argv.slice(2)) {
|
|
41
|
+
const { values, positionals } = parseArgs({
|
|
42
|
+
args: argv,
|
|
43
|
+
options: {
|
|
44
|
+
"dir": {
|
|
45
|
+
type: "string",
|
|
46
|
+
short: "d"
|
|
47
|
+
},
|
|
48
|
+
"base": {
|
|
49
|
+
type: "string",
|
|
50
|
+
short: "b",
|
|
51
|
+
default: "en"
|
|
52
|
+
},
|
|
53
|
+
"fix": {
|
|
54
|
+
type: "boolean",
|
|
55
|
+
short: "f",
|
|
56
|
+
default: false
|
|
57
|
+
},
|
|
58
|
+
"allow-empty": {
|
|
59
|
+
type: "boolean",
|
|
60
|
+
default: false
|
|
61
|
+
},
|
|
62
|
+
"help": {
|
|
63
|
+
type: "boolean",
|
|
64
|
+
short: "h",
|
|
65
|
+
default: false
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
allowPositionals: true
|
|
69
|
+
});
|
|
70
|
+
if (values.help) {
|
|
71
|
+
console.log(HELP_TEXT);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
const action = positionals[0] || "check";
|
|
75
|
+
if (action !== "check") {
|
|
76
|
+
console.error(`Unknown action: '${action}'. Run 'rimelight-i18n --help' for usage.`);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
const localesDir = findLocalesDir(values.dir);
|
|
80
|
+
if (!localesDir) {
|
|
81
|
+
console.error(`✖ [i18n] Could not find translation directory. Pass --dir <path> (e.g. --dir src/i18n)`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
const files = fs.readdirSync(localesDir).filter((f) => f.endsWith(".json"));
|
|
85
|
+
if (files.length === 0) {
|
|
86
|
+
console.error(`✖ [i18n] No .json translation files found in ${localesDir}`);
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
const translations = {};
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
const locale = path.basename(file, ".json");
|
|
92
|
+
const filePath = path.join(localesDir, file);
|
|
93
|
+
try {
|
|
94
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
95
|
+
translations[locale] = JSON.parse(content);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error(`✖ [i18n] Failed to parse ${file}: ${err?.message || err}`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const baseLocale = values.base || "en";
|
|
102
|
+
if (!translations[baseLocale]) {
|
|
103
|
+
console.error(`✖ [i18n] Base locale '${baseLocale}' not found in ${localesDir} (found: ${Object.keys(translations).join(", ")})`);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
let report = verifyTranslations(translations, {
|
|
107
|
+
defaultLocale: baseLocale,
|
|
108
|
+
allowEmpty: values["allow-empty"]
|
|
109
|
+
});
|
|
110
|
+
if (!report.isValid && values.fix) {
|
|
111
|
+
console.log(`ℹ [i18n] Fixing translation files in ${localesDir}...`);
|
|
112
|
+
const fixed = fixTranslations(translations, baseLocale);
|
|
113
|
+
for (const [locale, data] of Object.entries(fixed)) {
|
|
114
|
+
const filePath = path.join(localesDir, `${locale}.json`);
|
|
115
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
116
|
+
}
|
|
117
|
+
report = verifyTranslations(fixed, {
|
|
118
|
+
defaultLocale: baseLocale,
|
|
119
|
+
allowEmpty: values["allow-empty"]
|
|
120
|
+
});
|
|
121
|
+
console.log(`✔ [i18n] Successfully updated translation files.`);
|
|
122
|
+
}
|
|
123
|
+
const formatted = formatAuditReport(report);
|
|
124
|
+
if (report.isValid) {
|
|
125
|
+
console.log(formatted);
|
|
126
|
+
return 0;
|
|
127
|
+
} else {
|
|
128
|
+
console.error(formatted);
|
|
129
|
+
if (!values.fix) console.error(`\nRun with --fix to automatically prune stale keys and generate missing skeletons.`);
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (process.argv[1] && (import.meta.url === pathToFileURL(process.argv[1]).href || import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/")))) runCli().then((code) => {
|
|
134
|
+
if (code !== 0) process.exit(code);
|
|
135
|
+
});
|
|
136
|
+
//#endregion
|
|
137
|
+
export { runCli };
|
package/dist/hono.d.mts
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
//#region src/hono.d.ts
|
|
2
|
+
export interface HonoI18nOptions {
|
|
3
|
+
/**
|
|
4
|
+
* List of supported locale codes. Defaults to configured Vite plugin locales.
|
|
5
|
+
*/
|
|
6
|
+
locales?: string[];
|
|
7
|
+
/**
|
|
8
|
+
* The default fallback locale code. Defaults to configured Vite plugin defaultLocale.
|
|
9
|
+
*/
|
|
10
|
+
defaultLocale?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Whether the default locale is prefixed in URLs. Defaults to configured Vite plugin setting.
|
|
13
|
+
*/
|
|
14
|
+
prefixDefaultLocale?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The Cloudflare KV namespace binding name (default: "TRANSLATIONS_KV").
|
|
17
|
+
*/
|
|
18
|
+
kvBinding?: string;
|
|
19
|
+
}
|
|
2
20
|
/**
|
|
3
21
|
* Hono middleware for Rimelight i18n. Handles locale routing, redirection, currentLocale store
|
|
4
22
|
* updates, and Cloudflare KV binding setup.
|
|
5
23
|
*/
|
|
6
|
-
export declare function i18n(): (c: any, next: any) => Promise<any>;
|
|
24
|
+
export declare function i18n(options?: HonoI18nOptions): (c: any, next: any) => Promise<any>;
|
|
7
25
|
//#endregion
|
package/dist/hono.mjs
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import { n as currentLocale } from "./runtime-
|
|
2
|
-
import { d as setKVBinding } from "./src
|
|
3
|
-
import { env } from "cloudflare:workers";
|
|
4
|
-
import { defaultLocale, locales, prefixDefaultLocale } from "virtual:rimelight-i18n-config";
|
|
1
|
+
import { n as currentLocale } from "./runtime-D8XFXD5B.mjs";
|
|
2
|
+
import { d as setKVBinding, l as locales, n as defaultLocale, u as prefixDefaultLocale } from "./src--oNux7GS.mjs";
|
|
5
3
|
//#region src/hono.ts
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
function getPreferredLocale(acceptLanguage) {
|
|
9
|
-
if (!acceptLanguage) return DEFAULT_LOCALE;
|
|
4
|
+
function getPreferredLocale(acceptLanguage, supportedLocales, defaultLocale) {
|
|
5
|
+
if (!acceptLanguage) return defaultLocale;
|
|
10
6
|
const parsed = acceptLanguage.split(",").map((lang) => {
|
|
11
7
|
const parts = lang.split(";");
|
|
12
8
|
const code = (parts[0] ?? "").trim().toLowerCase();
|
|
@@ -24,20 +20,24 @@ function getPreferredLocale(acceptLanguage) {
|
|
|
24
20
|
};
|
|
25
21
|
}).toSorted((a, b) => b.q - a.q);
|
|
26
22
|
for (const item of parsed) {
|
|
27
|
-
if (
|
|
23
|
+
if (supportedLocales.has(item.code)) return item.code;
|
|
28
24
|
const base = item.base;
|
|
29
|
-
if (
|
|
25
|
+
if (supportedLocales.has(base)) return base;
|
|
30
26
|
}
|
|
31
|
-
return
|
|
27
|
+
return defaultLocale;
|
|
32
28
|
}
|
|
33
29
|
/**
|
|
34
30
|
* Hono middleware for Rimelight i18n. Handles locale routing, redirection, currentLocale store
|
|
35
31
|
* updates, and Cloudflare KV binding setup.
|
|
36
32
|
*/
|
|
37
|
-
function i18n() {
|
|
33
|
+
function i18n(options) {
|
|
34
|
+
const supportedLocales = new Set(options?.locales ?? locales);
|
|
35
|
+
const defaultLocale$1 = options?.defaultLocale ?? defaultLocale;
|
|
36
|
+
const prefixDefaultLocale$1 = options?.prefixDefaultLocale ?? prefixDefaultLocale;
|
|
37
|
+
const kvKey = options?.kvBinding ?? "TRANSLATIONS_KV";
|
|
38
38
|
return async (c, next) => {
|
|
39
39
|
try {
|
|
40
|
-
const kv =
|
|
40
|
+
const kv = c.env?.[kvKey];
|
|
41
41
|
if (kv) setKVBinding(kv);
|
|
42
42
|
} catch {}
|
|
43
43
|
const url = new URL(c.req.url);
|
|
@@ -45,33 +45,33 @@ function i18n() {
|
|
|
45
45
|
if (c.req.method !== "GET" && c.req.method !== "HEAD") {
|
|
46
46
|
const paramLocale = c.req.param("locale");
|
|
47
47
|
const firstSegment = pathname.split("/").find(Boolean);
|
|
48
|
-
const activeLocale = (paramLocale &&
|
|
48
|
+
const activeLocale = (paramLocale && supportedLocales.has(paramLocale) ? paramLocale : null) || (firstSegment && supportedLocales.has(firstSegment) ? firstSegment : null) || defaultLocale$1;
|
|
49
49
|
currentLocale.set(activeLocale);
|
|
50
50
|
return next();
|
|
51
51
|
}
|
|
52
52
|
if (pathname.startsWith("/api") || pathname.startsWith("/_") || pathname.includes(".")) return next();
|
|
53
53
|
const firstSegment = pathname.split("/").find(Boolean);
|
|
54
|
-
const firstIsLocale = firstSegment !== void 0 &&
|
|
55
|
-
if (prefixDefaultLocale) {
|
|
54
|
+
const firstIsLocale = firstSegment !== void 0 && supportedLocales.has(firstSegment);
|
|
55
|
+
if (prefixDefaultLocale$1) {
|
|
56
56
|
const paramLocale = c.req.param("locale");
|
|
57
|
-
const activeLocale = (paramLocale &&
|
|
57
|
+
const activeLocale = (paramLocale && supportedLocales.has(paramLocale) ? paramLocale : null) || (firstIsLocale ? firstSegment : null) || defaultLocale$1;
|
|
58
58
|
currentLocale.set(activeLocale);
|
|
59
59
|
if (firstIsLocale) return next();
|
|
60
|
-
const locale = getPreferredLocale(c.req.header("accept-language") || null);
|
|
60
|
+
const locale = getPreferredLocale(c.req.header("accept-language") || null, supportedLocales, defaultLocale$1);
|
|
61
61
|
currentLocale.set(locale);
|
|
62
62
|
const targetPath = `/${locale}${pathname}${url.search}`;
|
|
63
63
|
return c.redirect(targetPath, 302);
|
|
64
64
|
} else {
|
|
65
65
|
if (firstIsLocale) {
|
|
66
|
-
if (firstSegment ===
|
|
67
|
-
const rest = pathname.slice(
|
|
68
|
-
currentLocale.set(
|
|
66
|
+
if (firstSegment === defaultLocale$1) {
|
|
67
|
+
const rest = pathname.slice(defaultLocale$1.length + 1) || "/";
|
|
68
|
+
currentLocale.set(defaultLocale$1);
|
|
69
69
|
return c.redirect(`${rest}${url.search}`, 302);
|
|
70
70
|
}
|
|
71
71
|
currentLocale.set(firstSegment);
|
|
72
72
|
return next();
|
|
73
73
|
}
|
|
74
|
-
currentLocale.set(
|
|
74
|
+
currentLocale.set(defaultLocale$1);
|
|
75
75
|
return next();
|
|
76
76
|
}
|
|
77
77
|
};
|
package/dist/index.d.mts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import { i as KVNamespaceBinding, n as CustomTranslations, o as NestedTranslationKeys,
|
|
2
|
-
import { a as
|
|
1
|
+
import { c as ReadableAtom, d as TranslationLoader, i as KVNamespaceBinding, m as WritableAtom, n as CustomTranslations, o as NestedTranslationKeys, t as ComponentsJSON, u as TranslationKey } from "./types-PBnVVzPm.mjs";
|
|
2
|
+
import { a as getRawTranslation, c as tArray, i as getPluralCategory, l as tRaw, n as atom, o as initializeI18n, r as currentLocale } from "./runtime-B2W6s31-.mjs";
|
|
3
3
|
import { i as i18n, n as RimelightI18nPlugins, t as RimelightI18nOptions } from "./plugin-CFEm0e4U.mjs";
|
|
4
|
-
import { TranslationLoader } from "@nanostores/i18n";
|
|
5
4
|
//#region src/index.d.ts
|
|
6
5
|
declare let locales: string[];
|
|
7
6
|
declare let defaultLocale: string;
|
|
8
7
|
declare let prefixDefaultLocale: boolean;
|
|
8
|
+
export interface TFunction {
|
|
9
|
+
<K extends string = TranslationKey>(key: K | TranslationKey, params?: Record<string, any>): string;
|
|
10
|
+
raw: <T = unknown>(key: string, fallback?: T) => T;
|
|
11
|
+
array: <T = string>(key: string, fallback?: T[]) => T[];
|
|
12
|
+
}
|
|
9
13
|
/**
|
|
10
14
|
* Translates a single string key using dot-notation (e.g. `t("page_home.section_hero_title")`).
|
|
11
|
-
* When augmented via CustomTranslations, keys are strictly type-checked and autocompleted.
|
|
15
|
+
* When augmented via CustomTranslations, keys are strictly type-checked and autocompleted. Also
|
|
16
|
+
* provides `t.raw()` and `t.array()` methods to retrieve structured data or string arrays.
|
|
12
17
|
*/
|
|
13
|
-
export declare
|
|
18
|
+
export declare const t: TFunction;
|
|
14
19
|
export declare function getLocale(): string;
|
|
15
20
|
/**
|
|
16
21
|
* Returns a standard BCP 47 / RFC 5646 language tag suitable for HTML `<html lang="...">`. Maps
|
|
@@ -36,4 +41,4 @@ export declare function getLanguageAlternates(pathname: string, siteUrl?: string
|
|
|
36
41
|
export declare function setKVBinding(kv: KVNamespaceBinding): void;
|
|
37
42
|
export declare function createTranslationLoader(translations: Record<string, ComponentsJSON>): TranslationLoader;
|
|
38
43
|
//#endregion
|
|
39
|
-
export { type CustomTranslations, type NestedTranslationKeys, type RimelightI18nOptions, type RimelightI18nPlugins, type TranslationKey,
|
|
44
|
+
export { type CustomTranslations, type NestedTranslationKeys, type ReadableAtom, type RimelightI18nOptions, type RimelightI18nPlugins, type TranslationKey, type WritableAtom, atom, currentLocale, defaultLocale, getPluralCategory, getRawTranslation, i18n, i18n as rimelightI18n, initializeI18n, locales, prefixDefaultLocale, tArray, tRaw };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as initializeI18n, c as tRaw, i as getRawTranslation, n as currentLocale, r as getPluralCategory, s as tArray, t as atom } from "./runtime-D8XFXD5B.mjs";
|
|
2
2
|
import { t as i18n } from "./plugin-wd6n-2-C.mjs";
|
|
3
|
-
import { a as getLocale, c as getRelativeLocaleUrlList, d as setKVBinding, f as t, i as getLanguageAlternates, l as locales, n as defaultLocale, o as getOgLocale, r as getHtmlLang, s as getRelativeLocaleUrl, t as createTranslationLoader, u as prefixDefaultLocale } from "./src
|
|
4
|
-
export {
|
|
3
|
+
import { a as getLocale, c as getRelativeLocaleUrlList, d as setKVBinding, f as t, i as getLanguageAlternates, l as locales, n as defaultLocale, o as getOgLocale, r as getHtmlLang, s as getRelativeLocaleUrl, t as createTranslationLoader, u as prefixDefaultLocale } from "./src--oNux7GS.mjs";
|
|
4
|
+
export { atom, createTranslationLoader, currentLocale, defaultLocale, getHtmlLang, getLanguageAlternates, getLocale, getOgLocale, getPluralCategory, getRawTranslation, getRelativeLocaleUrl, getRelativeLocaleUrlList, i18n, i18n as rimelightI18n, initializeI18n, locales, prefixDefaultLocale, setKVBinding, t, tArray, tRaw };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { d as TranslationLoader, m as WritableAtom } from "./types-PBnVVzPm.mjs";
|
|
2
|
+
//#region src/runtime.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Creates a lightweight reactive atom store compatible with Nanostores / Svelte store contracts.
|
|
5
|
+
*/
|
|
6
|
+
declare function atom<T>(initialValue: T): WritableAtom<T>;
|
|
7
|
+
/**
|
|
8
|
+
* A reactive store containing the current locale code. Set by middleware on each request, or
|
|
9
|
+
* manually via `currentLocale.set(locale)`.
|
|
10
|
+
*/
|
|
11
|
+
declare const currentLocale: WritableAtom<string>;
|
|
12
|
+
interface InitializeI18nOptions {
|
|
13
|
+
/**
|
|
14
|
+
* The default locale code (e.g. 'en').
|
|
15
|
+
*/
|
|
16
|
+
defaultLocale: string;
|
|
17
|
+
/**
|
|
18
|
+
* Pre-loaded translations keyed by locale then component.
|
|
19
|
+
*/
|
|
20
|
+
translations: Record<string, Record<string, any>>;
|
|
21
|
+
/**
|
|
22
|
+
* Optional dynamic loader called when a locale or component is not in cache.
|
|
23
|
+
*/
|
|
24
|
+
get?: TranslationLoader | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Initializes the i18n system. Must be called once before any other i18n functions are used (the
|
|
28
|
+
* integration virtual module does this automatically).
|
|
29
|
+
*/
|
|
30
|
+
declare function initializeI18n(options: InitializeI18nOptions): void;
|
|
31
|
+
/**
|
|
32
|
+
* Safely retrieves the raw translation entry (string, array, or object) for a given dot-notation
|
|
33
|
+
* key.
|
|
34
|
+
*/
|
|
35
|
+
declare function getRawTranslation(key: string): unknown;
|
|
36
|
+
/**
|
|
37
|
+
* Returns raw translation data (such as arrays or nested objects) typed as T.
|
|
38
|
+
*/
|
|
39
|
+
declare function tRaw<T = unknown>(key: string, fallback?: T): T;
|
|
40
|
+
/**
|
|
41
|
+
* Returns an array of translations (e.g. lists of strings or structured objects). Falls back to an
|
|
42
|
+
* empty array or the provided fallback if not found or not an array.
|
|
43
|
+
*/
|
|
44
|
+
declare function tArray<T = string>(key: string, fallback?: T[]): T[];
|
|
45
|
+
/**
|
|
46
|
+
* Translates a single string key using dot-notation (e.g. `t("page_home.section_hero_title")` or
|
|
47
|
+
* nested/array paths like `t("page_resume.experiences.0.role")`).
|
|
48
|
+
*/
|
|
49
|
+
declare function t(key: string, params?: Record<string, any>): string;
|
|
50
|
+
declare namespace t {
|
|
51
|
+
var raw: typeof tRaw;
|
|
52
|
+
var array: typeof tArray;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Returns the plural category (e.g. 'one', 'few', 'many', 'other') for a given number and locale
|
|
56
|
+
* using the native browser Intl.PluralRules API.
|
|
57
|
+
*/
|
|
58
|
+
declare function getPluralCategory(count: number, locale?: string): Intl.LDMLPluralRule;
|
|
59
|
+
//#endregion
|
|
60
|
+
export { getRawTranslation as a, tArray as c, getPluralCategory as i, tRaw as l, atom as n, initializeI18n as o, currentLocale as r, t as s, InitializeI18nOptions as t };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
//#region src/runtime.ts
|
|
2
|
+
/**
|
|
3
|
+
* Creates a lightweight reactive atom store compatible with Nanostores / Svelte store contracts.
|
|
4
|
+
*/
|
|
5
|
+
function atom(initialValue) {
|
|
6
|
+
let value = initialValue;
|
|
7
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
8
|
+
return {
|
|
9
|
+
get: () => value,
|
|
10
|
+
set: (nextValue) => {
|
|
11
|
+
if (value !== nextValue) {
|
|
12
|
+
value = nextValue;
|
|
13
|
+
for (const listener of listeners) listener(value);
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
subscribe: (listener) => {
|
|
17
|
+
listeners.add(listener);
|
|
18
|
+
listener(value);
|
|
19
|
+
return () => {
|
|
20
|
+
listeners.delete(listener);
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
listen: (listener) => {
|
|
24
|
+
listeners.add(listener);
|
|
25
|
+
return () => {
|
|
26
|
+
listeners.delete(listener);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A reactive store containing the current locale code. Set by middleware on each request, or
|
|
33
|
+
* manually via `currentLocale.set(locale)`.
|
|
34
|
+
*/
|
|
35
|
+
const currentLocale = atom("");
|
|
36
|
+
let baseLocaleDefault = "en";
|
|
37
|
+
let rawTranslationsDict = {};
|
|
38
|
+
function isObjectRecord(obj) {
|
|
39
|
+
return typeof obj === "object" && obj !== null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Initializes the i18n system. Must be called once before any other i18n functions are used (the
|
|
43
|
+
* integration virtual module does this automatically).
|
|
44
|
+
*/
|
|
45
|
+
function initializeI18n(options) {
|
|
46
|
+
const { defaultLocale, translations } = options;
|
|
47
|
+
baseLocaleDefault = defaultLocale;
|
|
48
|
+
rawTranslationsDict = translations ? { ...translations } : {};
|
|
49
|
+
if (!currentLocale.get()) currentLocale.set(defaultLocale);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Helper to safely resolve nested dot-notation and array indices while guarding against prototype
|
|
53
|
+
* pollution.
|
|
54
|
+
*/
|
|
55
|
+
function resolvePath(obj, path) {
|
|
56
|
+
if (obj == null || typeof obj !== "object") return void 0;
|
|
57
|
+
if (Object.prototype.hasOwnProperty.call(obj, path)) return obj[path];
|
|
58
|
+
const segments = path.split(".");
|
|
59
|
+
let current = obj;
|
|
60
|
+
for (let i = 0; i < segments.length; i++) {
|
|
61
|
+
if (current == null || typeof current !== "object") return;
|
|
62
|
+
const seg = segments[i];
|
|
63
|
+
if (seg === "__proto__" || seg === "constructor" || seg === "prototype") return;
|
|
64
|
+
if (Object.prototype.hasOwnProperty.call(current, seg) || Array.isArray(current) && seg in current) current = current[seg];
|
|
65
|
+
else {
|
|
66
|
+
const remainingKey = segments.slice(i).join(".");
|
|
67
|
+
if (Object.prototype.hasOwnProperty.call(current, remainingKey)) return current[remainingKey];
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return current;
|
|
72
|
+
}
|
|
73
|
+
function getLocaleCandidateChain(activeLocale, defaultLocale) {
|
|
74
|
+
const chain = [];
|
|
75
|
+
function addCandidates(code) {
|
|
76
|
+
if (!code) return;
|
|
77
|
+
const exact = code;
|
|
78
|
+
const normalized = code.toLowerCase();
|
|
79
|
+
const baseLang = normalized.split("-")[0] ?? normalized;
|
|
80
|
+
if (!chain.includes(exact)) chain.push(exact);
|
|
81
|
+
if (!chain.includes(normalized)) chain.push(normalized);
|
|
82
|
+
if (!chain.includes(baseLang)) chain.push(baseLang);
|
|
83
|
+
}
|
|
84
|
+
addCandidates(activeLocale);
|
|
85
|
+
addCandidates(defaultLocale);
|
|
86
|
+
addCandidates("en");
|
|
87
|
+
return chain;
|
|
88
|
+
}
|
|
89
|
+
function getCachedComponent(locale, componentName) {
|
|
90
|
+
const candidates = getLocaleCandidateChain(locale, baseLocaleDefault);
|
|
91
|
+
for (const code of candidates) {
|
|
92
|
+
const rawComp = rawTranslationsDict[code]?.[componentName];
|
|
93
|
+
if (isObjectRecord(rawComp)) return rawComp;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function resolveKeyInLocales(locale, componentName, keyName) {
|
|
97
|
+
const candidates = getLocaleCandidateChain(locale, baseLocaleDefault);
|
|
98
|
+
for (const code of candidates) {
|
|
99
|
+
const rawComp = rawTranslationsDict[code]?.[componentName];
|
|
100
|
+
if (isObjectRecord(rawComp)) {
|
|
101
|
+
const val = resolvePath(rawComp, keyName);
|
|
102
|
+
if (val !== void 0) return val;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Safely retrieves the raw translation entry (string, array, or object) for a given dot-notation
|
|
108
|
+
* key.
|
|
109
|
+
*/
|
|
110
|
+
function getRawTranslation(key) {
|
|
111
|
+
const dotIndex = key.indexOf(".");
|
|
112
|
+
const activeLocale = currentLocale.get() || baseLocaleDefault;
|
|
113
|
+
if (dotIndex === -1) return getCachedComponent(activeLocale, key) ?? getCachedComponent(baseLocaleDefault, key);
|
|
114
|
+
return resolveKeyInLocales(activeLocale, key.slice(0, dotIndex), key.slice(dotIndex + 1));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Returns raw translation data (such as arrays or nested objects) typed as T.
|
|
118
|
+
*/
|
|
119
|
+
function tRaw(key, fallback) {
|
|
120
|
+
const val = getRawTranslation(key);
|
|
121
|
+
if (val !== void 0) return val;
|
|
122
|
+
return fallback;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Returns an array of translations (e.g. lists of strings or structured objects). Falls back to an
|
|
126
|
+
* empty array or the provided fallback if not found or not an array.
|
|
127
|
+
*/
|
|
128
|
+
function tArray(key, fallback = []) {
|
|
129
|
+
const val = getRawTranslation(key);
|
|
130
|
+
if (Array.isArray(val)) return val;
|
|
131
|
+
return fallback;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Translates a single string key using dot-notation (e.g. `t("page_home.section_hero_title")` or
|
|
135
|
+
* nested/array paths like `t("page_resume.experiences.0.role")`).
|
|
136
|
+
*/
|
|
137
|
+
function t(key, params) {
|
|
138
|
+
const dotIndex = key.indexOf(".");
|
|
139
|
+
if (dotIndex === -1) return key;
|
|
140
|
+
const componentName = key.slice(0, dotIndex);
|
|
141
|
+
const keyName = key.slice(dotIndex + 1);
|
|
142
|
+
const activeLocale = currentLocale.get() || baseLocaleDefault;
|
|
143
|
+
let value = resolveKeyInLocales(activeLocale, componentName, keyName) ?? key;
|
|
144
|
+
if (params && typeof params["count"] === "number" && typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
145
|
+
const pluralCategory = getPluralCategory(params["count"], activeLocale);
|
|
146
|
+
value = value[pluralCategory] ?? value["other"] ?? key;
|
|
147
|
+
}
|
|
148
|
+
if (typeof value === "function") return value(params);
|
|
149
|
+
if (params && typeof value === "string") return value.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? `{${k}}`));
|
|
150
|
+
if (typeof value === "string") return value;
|
|
151
|
+
if (value !== void 0 && value !== null && typeof value === "object") return JSON.stringify(value);
|
|
152
|
+
return value != null ? `${value}` : "";
|
|
153
|
+
}
|
|
154
|
+
t.raw = tRaw;
|
|
155
|
+
t.array = tArray;
|
|
156
|
+
/**
|
|
157
|
+
* Returns the plural category (e.g. 'one', 'few', 'many', 'other') for a given number and locale
|
|
158
|
+
* using the native browser Intl.PluralRules API.
|
|
159
|
+
*/
|
|
160
|
+
function getPluralCategory(count, locale = "en") {
|
|
161
|
+
return new Intl.PluralRules(locale).select(count);
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
export { initializeI18n as a, tRaw as c, getRawTranslation as i, currentLocale as n, t as o, getPluralCategory as r, tArray as s, atom as t };
|
package/dist/runtime.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export {
|
|
1
|
+
import { a as getRawTranslation, c as tArray, i as getPluralCategory, l as tRaw, n as atom, o as initializeI18n, r as currentLocale, s as t, t as InitializeI18nOptions } from "./runtime-B2W6s31-.mjs";
|
|
2
|
+
export { InitializeI18nOptions, atom, currentLocale, getPluralCategory, getRawTranslation, initializeI18n, t, tArray, tRaw };
|
package/dist/runtime.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export {
|
|
1
|
+
import { a as initializeI18n, c as tRaw, i as getRawTranslation, n as currentLocale, o as t, r as getPluralCategory, s as tArray, t as atom } from "./runtime-D8XFXD5B.mjs";
|
|
2
|
+
export { atom, currentLocale, getPluralCategory, getRawTranslation, initializeI18n, t, tArray, tRaw };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as currentLocale, o as
|
|
1
|
+
import { a as initializeI18n, c as tRaw, n as currentLocale, o as t$1, s as tArray } from "./runtime-D8XFXD5B.mjs";
|
|
2
2
|
//#region src/index.ts
|
|
3
3
|
let locales = ["en"];
|
|
4
4
|
let defaultLocale = "en";
|
|
@@ -20,11 +20,15 @@ if (translations && Object.keys(translations).length > 0) initializeI18n({
|
|
|
20
20
|
});
|
|
21
21
|
/**
|
|
22
22
|
* Translates a single string key using dot-notation (e.g. `t("page_home.section_hero_title")`).
|
|
23
|
-
* When augmented via CustomTranslations, keys are strictly type-checked and autocompleted.
|
|
23
|
+
* When augmented via CustomTranslations, keys are strictly type-checked and autocompleted. Also
|
|
24
|
+
* provides `t.raw()` and `t.array()` methods to retrieve structured data or string arrays.
|
|
24
25
|
*/
|
|
25
|
-
|
|
26
|
+
const t = Object.assign(function(key, params) {
|
|
26
27
|
return t$1(key, params);
|
|
27
|
-
}
|
|
28
|
+
}, {
|
|
29
|
+
raw: tRaw,
|
|
30
|
+
array: tArray
|
|
31
|
+
});
|
|
28
32
|
function getLocale() {
|
|
29
33
|
return currentLocale.get() || defaultLocale || "en";
|
|
30
34
|
}
|
|
@@ -48,7 +52,7 @@ function getOgLocale() {
|
|
|
48
52
|
return {
|
|
49
53
|
en: "en_US",
|
|
50
54
|
pt: "pt_BR",
|
|
51
|
-
es: "
|
|
55
|
+
es: "es-ES"
|
|
52
56
|
}[loc] || `${loc}_${loc.toUpperCase()}`;
|
|
53
57
|
}
|
|
54
58
|
function getRelativeLocaleUrl(arg1, arg2) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
type ComponentsJSON = Record<string, Record<string, any>>;
|
|
3
|
+
type Translations = Record<string, any>;
|
|
4
|
+
type TranslationLoader = (locale: string, components: string[]) => Promise<ComponentsJSON | Record<string, any>>;
|
|
5
|
+
interface ReadableAtom<T> {
|
|
6
|
+
get(): T;
|
|
7
|
+
subscribe(listener: (value: T) => void): () => void;
|
|
8
|
+
listen(listener: (value: T) => void): () => void;
|
|
9
|
+
}
|
|
10
|
+
interface WritableAtom<T> extends ReadableAtom<T> {
|
|
11
|
+
set(value: T): void;
|
|
12
|
+
}
|
|
13
|
+
interface FlattenedTranslations {
|
|
14
|
+
[key: string]: string;
|
|
15
|
+
}
|
|
16
|
+
interface PlaceholderMismatch {
|
|
17
|
+
key: string;
|
|
18
|
+
basePlaceholders: string[];
|
|
19
|
+
targetPlaceholders: string[];
|
|
20
|
+
}
|
|
21
|
+
interface TranslationAuditReport {
|
|
22
|
+
isValid: boolean;
|
|
23
|
+
totalKeys: number;
|
|
24
|
+
locales: string[];
|
|
25
|
+
defaultLocale: string;
|
|
26
|
+
missingKeys: Record<string, string[]>;
|
|
27
|
+
staleKeys: Record<string, string[]>;
|
|
28
|
+
emptyKeys: Record<string, string[]>;
|
|
29
|
+
placeholderMismatches: Record<string, PlaceholderMismatch[]>;
|
|
30
|
+
stats: {
|
|
31
|
+
completion: Record<string, string>;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
interface VerifyTranslationsOptions {
|
|
35
|
+
defaultLocale?: string;
|
|
36
|
+
allowEmpty?: boolean;
|
|
37
|
+
}
|
|
38
|
+
interface LocaleFile {
|
|
39
|
+
[component: string]: {
|
|
40
|
+
[key: string]: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
interface KVNamespaceBinding {
|
|
44
|
+
get(key: string, type: "json"): Promise<Record<string, string> | null>;
|
|
45
|
+
put(key: string, value: string): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Derives dot-notated nested translation keys (e.g. "page_home.section_hero_title" or
|
|
49
|
+
* "page_resume.experiences.0.role") from a translation schema type.
|
|
50
|
+
*/
|
|
51
|
+
type NestedTranslationKeys<T> = T extends (infer U)[] ? `${number}` | (U extends object ? `${number}.${NestedTranslationKeys<U>}` : never) : T extends object ? { [K in keyof T & (string | number)]: T[K] extends (infer U)[] ? `${K}` | `${K}.${number}` | (U extends object ? `${K}.${number}.${NestedTranslationKeys<U>}` : never) : T[K] extends object ? `${K}` | `${K}.${NestedTranslationKeys<T[K]>}` : `${K}`; }[keyof T & (string | number)] : never;
|
|
52
|
+
/**
|
|
53
|
+
* Augmented interface for app-level strongly typed translation keys. Applications can declare:
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* declare module "@rimelight/i18n" {
|
|
57
|
+
* interface CustomTranslations extends typeof import("./i18n/en.json") {}
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
interface CustomTranslations {}
|
|
62
|
+
type TranslationKey = [keyof CustomTranslations] extends [never] ? string : NestedTranslationKeys<CustomTranslations>;
|
|
63
|
+
//#endregion
|
|
64
|
+
export { LocaleFile as a, ReadableAtom as c, TranslationLoader as d, Translations as f, KVNamespaceBinding as i, TranslationAuditReport as l, WritableAtom as m, CustomTranslations as n, NestedTranslationKeys as o, VerifyTranslationsOptions as p, FlattenedTranslations as r, PlaceholderMismatch as s, ComponentsJSON as t, TranslationKey as u };
|
package/dist/types.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as LocaleFile, i as KVNamespaceBinding, n as CustomTranslations, o as NestedTranslationKeys, r as FlattenedTranslations, s as
|
|
2
|
-
export {
|
|
1
|
+
import { a as LocaleFile, c as ReadableAtom, d as TranslationLoader, f as Translations, i as KVNamespaceBinding, l as TranslationAuditReport, m as WritableAtom, n as CustomTranslations, o as NestedTranslationKeys, p as VerifyTranslationsOptions, r as FlattenedTranslations, s as PlaceholderMismatch, t as ComponentsJSON, u as TranslationKey } from "./types-PBnVVzPm.mjs";
|
|
2
|
+
export { ComponentsJSON, CustomTranslations, FlattenedTranslations, KVNamespaceBinding, LocaleFile, NestedTranslationKeys, PlaceholderMismatch, ReadableAtom, TranslationAuditReport, TranslationKey, TranslationLoader, Translations, VerifyTranslationsOptions, WritableAtom };
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
//#region src/utils.ts
|
|
2
|
+
/**
|
|
3
|
+
* Flattens deeply nested translation objects or arrays into dot-notated paths.
|
|
4
|
+
*/
|
|
5
|
+
function flatten(obj, prefix = "", result = {}) {
|
|
6
|
+
if (obj == null || typeof obj !== "object") return result;
|
|
7
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
8
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
9
|
+
const fullPath = prefix ? `${prefix}.${key}` : key;
|
|
10
|
+
if (value != null && typeof value === "object" && !Array.isArray(value)) flatten(value, fullPath, result);
|
|
11
|
+
else if (Array.isArray(value)) for (let i = 0; i < value.length; i++) {
|
|
12
|
+
const item = value[i];
|
|
13
|
+
const arrayPath = `${fullPath}.${i}`;
|
|
14
|
+
if (item != null && typeof item === "object") flatten(item, arrayPath, result);
|
|
15
|
+
else result[arrayPath] = String(item ?? "");
|
|
16
|
+
}
|
|
17
|
+
else result[fullPath] = value != null ? String(value) : "";
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Unflattens a dot-notated translation map back into a nested object structure.
|
|
23
|
+
*/
|
|
24
|
+
function unflatten(flat) {
|
|
25
|
+
const result = {};
|
|
26
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
27
|
+
const parts = key.split(".");
|
|
28
|
+
let current = result;
|
|
29
|
+
for (let i = 0; i < parts.length; i++) {
|
|
30
|
+
const part = parts[i];
|
|
31
|
+
if (part === "__proto__" || part === "constructor" || part === "prototype") break;
|
|
32
|
+
const isLast = i === parts.length - 1;
|
|
33
|
+
const nextPart = parts[i + 1];
|
|
34
|
+
const nextIsNum = nextPart !== void 0 && /^\d+$/.test(nextPart);
|
|
35
|
+
if (isLast) current[part] = value;
|
|
36
|
+
else {
|
|
37
|
+
if (current[part] == null || typeof current[part] !== "object") current[part] = nextIsNum ? [] : {};
|
|
38
|
+
current = current[part];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Extracts all dotted translation keys from a translation dictionary.
|
|
46
|
+
*/
|
|
47
|
+
function extractKeys(source) {
|
|
48
|
+
return Object.keys(flatten(source));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Extracts all interpolation variables like `{name}` or `{count}` from a string.
|
|
52
|
+
*/
|
|
53
|
+
function extractPlaceholders(str) {
|
|
54
|
+
const matches = str.match(/\{(\w+)\}/g);
|
|
55
|
+
if (!matches) return [];
|
|
56
|
+
return Array.from(new Set(matches.map((m) => m.slice(1, -1))));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Compares a target translation dictionary against a base translation dictionary, returning any
|
|
60
|
+
* missing deep dotted keys.
|
|
61
|
+
*/
|
|
62
|
+
function findMissingKeys(base, target) {
|
|
63
|
+
const baseFlat = flatten(base);
|
|
64
|
+
const targetFlat = flatten(target);
|
|
65
|
+
const missing = [];
|
|
66
|
+
for (const key of Object.keys(baseFlat)) if (!(key in targetFlat)) missing.push(key);
|
|
67
|
+
return missing;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Compares a target translation dictionary against a base translation dictionary, returning any
|
|
71
|
+
* stale/orphaned keys present in target but absent in base.
|
|
72
|
+
*/
|
|
73
|
+
function findStaleKeys(base, target) {
|
|
74
|
+
const baseFlat = flatten(base);
|
|
75
|
+
const targetFlat = flatten(target);
|
|
76
|
+
const stale = [];
|
|
77
|
+
for (const key of Object.keys(targetFlat)) if (!(key in baseFlat)) stale.push(key);
|
|
78
|
+
return stale;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Runs a comprehensive audit across all translation dictionaries comparing against
|
|
82
|
+
* base/defaultLocale.
|
|
83
|
+
*/
|
|
84
|
+
function verifyTranslations(translations, options = {}) {
|
|
85
|
+
const defaultLocale = options.defaultLocale || "en";
|
|
86
|
+
const allowEmpty = options.allowEmpty ?? false;
|
|
87
|
+
const locales = Object.keys(translations);
|
|
88
|
+
const baseDict = translations[defaultLocale] || {};
|
|
89
|
+
const baseFlat = flatten(baseDict);
|
|
90
|
+
const totalKeys = Object.keys(baseFlat).length;
|
|
91
|
+
const missingKeys = {};
|
|
92
|
+
const staleKeys = {};
|
|
93
|
+
const emptyKeys = {};
|
|
94
|
+
const placeholderMismatches = {};
|
|
95
|
+
const completion = {};
|
|
96
|
+
let isValid = true;
|
|
97
|
+
for (const locale of locales) {
|
|
98
|
+
const targetDict = translations[locale] || {};
|
|
99
|
+
const targetFlat = flatten(targetDict);
|
|
100
|
+
const missing = findMissingKeys(baseDict, targetDict);
|
|
101
|
+
const stale = findStaleKeys(baseDict, targetDict);
|
|
102
|
+
const empty = [];
|
|
103
|
+
const mismatches = [];
|
|
104
|
+
if (locale !== defaultLocale) {
|
|
105
|
+
if (missing.length > 0) {
|
|
106
|
+
missingKeys[locale] = missing;
|
|
107
|
+
isValid = false;
|
|
108
|
+
}
|
|
109
|
+
if (stale.length > 0) {
|
|
110
|
+
staleKeys[locale] = stale;
|
|
111
|
+
isValid = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const [key, val] of Object.entries(targetFlat)) {
|
|
115
|
+
if (!allowEmpty && typeof val === "string" && val.trim() === "") {
|
|
116
|
+
empty.push(key);
|
|
117
|
+
isValid = false;
|
|
118
|
+
}
|
|
119
|
+
if (key in baseFlat) {
|
|
120
|
+
const baseHolders = extractPlaceholders(baseFlat[key] ?? "").sort();
|
|
121
|
+
const targetHolders = extractPlaceholders(val).sort();
|
|
122
|
+
if (JSON.stringify(baseHolders) !== JSON.stringify(targetHolders)) {
|
|
123
|
+
mismatches.push({
|
|
124
|
+
key,
|
|
125
|
+
basePlaceholders: baseHolders,
|
|
126
|
+
targetPlaceholders: targetHolders
|
|
127
|
+
});
|
|
128
|
+
isValid = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (empty.length > 0) emptyKeys[locale] = empty;
|
|
133
|
+
if (mismatches.length > 0) placeholderMismatches[locale] = mismatches;
|
|
134
|
+
const matchedCount = totalKeys - missing.length;
|
|
135
|
+
completion[locale] = `${totalKeys > 0 ? Math.round(matchedCount / totalKeys * 100) : 100}%`;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
isValid,
|
|
139
|
+
totalKeys,
|
|
140
|
+
locales,
|
|
141
|
+
defaultLocale,
|
|
142
|
+
missingKeys,
|
|
143
|
+
staleKeys,
|
|
144
|
+
emptyKeys,
|
|
145
|
+
placeholderMismatches,
|
|
146
|
+
stats: { completion }
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Formats a TranslationAuditReport into a human-readable diagnostic message.
|
|
151
|
+
*/
|
|
152
|
+
function formatAuditReport(report) {
|
|
153
|
+
if (report.isValid) return `✔ [i18n] Verified ${report.locales.length} locales (${report.locales.join(", ")}) — ${report.totalKeys} keys synchronized with 0 issues.`;
|
|
154
|
+
const lines = [];
|
|
155
|
+
lines.push(`✖ [i18n] Translation verification failed:`);
|
|
156
|
+
for (const locale of report.locales) {
|
|
157
|
+
const missing = report.missingKeys[locale];
|
|
158
|
+
const stale = report.staleKeys[locale];
|
|
159
|
+
const empty = report.emptyKeys[locale];
|
|
160
|
+
const mismatches = report.placeholderMismatches[locale];
|
|
161
|
+
if (!missing && !stale && !empty && !mismatches) continue;
|
|
162
|
+
lines.push(`\n [${locale}] (${report.stats.completion[locale]} complete):`);
|
|
163
|
+
if (missing && missing.length > 0) {
|
|
164
|
+
lines.push(` Missing Keys (${missing.length}):`);
|
|
165
|
+
for (const k of missing) lines.push(` - ${k}`);
|
|
166
|
+
}
|
|
167
|
+
if (stale && stale.length > 0) {
|
|
168
|
+
lines.push(` Stale Keys (${stale.length}, not in '${report.defaultLocale}'):`);
|
|
169
|
+
for (const k of stale) lines.push(` - ${k}`);
|
|
170
|
+
}
|
|
171
|
+
if (empty && empty.length > 0) {
|
|
172
|
+
lines.push(` Empty Keys (${empty.length}):`);
|
|
173
|
+
for (const k of empty) lines.push(` - ${k}`);
|
|
174
|
+
}
|
|
175
|
+
if (mismatches && mismatches.length > 0) {
|
|
176
|
+
lines.push(` Placeholder Mismatches (${mismatches.length}):`);
|
|
177
|
+
for (const m of mismatches) lines.push(` - ${m.key}: expected {${m.basePlaceholders.join(", ")}}, found {${m.targetPlaceholders.join(", ")}}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return lines.join("\n");
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Automatically prunes stale keys and injects missing key skeletons from the base locale.
|
|
184
|
+
*/
|
|
185
|
+
function fixTranslations(translations, defaultLocale = "en") {
|
|
186
|
+
const baseFlat = flatten(translations[defaultLocale] || {});
|
|
187
|
+
const fixed = {};
|
|
188
|
+
for (const [locale, dict] of Object.entries(translations)) {
|
|
189
|
+
if (locale === defaultLocale) {
|
|
190
|
+
fixed[locale] = dict;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const targetFlat = flatten(dict);
|
|
194
|
+
const newFlat = {};
|
|
195
|
+
for (const [baseKey, baseVal] of Object.entries(baseFlat)) if (baseKey in targetFlat) newFlat[baseKey] = targetFlat[baseKey];
|
|
196
|
+
else newFlat[baseKey] = baseVal;
|
|
197
|
+
fixed[locale] = unflatten(newFlat);
|
|
198
|
+
}
|
|
199
|
+
return fixed;
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
export { fixTranslations as a, unflatten as c, findStaleKeys as i, verifyTranslations as l, extractPlaceholders as n, flatten as o, findMissingKeys as r, formatAuditReport as s, extractKeys as t };
|
package/dist/utils.d.mts
CHANGED
|
@@ -1,11 +1,42 @@
|
|
|
1
|
-
import { r as FlattenedTranslations, t as ComponentsJSON } from "./types-
|
|
1
|
+
import { l as TranslationAuditReport, p as VerifyTranslationsOptions, r as FlattenedTranslations, t as ComponentsJSON } from "./types-PBnVVzPm.mjs";
|
|
2
2
|
//#region src/utils.d.ts
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Flattens deeply nested translation objects or arrays into dot-notated paths.
|
|
5
|
+
*/
|
|
6
|
+
export declare function flatten(obj: Record<string, any>, prefix?: string, result?: FlattenedTranslations): FlattenedTranslations;
|
|
7
|
+
/**
|
|
8
|
+
* Unflattens a dot-notated translation map back into a nested object structure.
|
|
9
|
+
*/
|
|
4
10
|
export declare function unflatten(flat: FlattenedTranslations): ComponentsJSON;
|
|
5
|
-
export declare function extractKeys(source: ComponentsJSON): string[];
|
|
6
11
|
/**
|
|
7
|
-
*
|
|
8
|
-
|
|
12
|
+
* Extracts all dotted translation keys from a translation dictionary.
|
|
13
|
+
*/
|
|
14
|
+
export declare function extractKeys(source: Record<string, any>): string[];
|
|
15
|
+
/**
|
|
16
|
+
* Extracts all interpolation variables like `{name}` or `{count}` from a string.
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractPlaceholders(str: string): string[];
|
|
19
|
+
/**
|
|
20
|
+
* Compares a target translation dictionary against a base translation dictionary, returning any
|
|
21
|
+
* missing deep dotted keys.
|
|
22
|
+
*/
|
|
23
|
+
export declare function findMissingKeys(base: Record<string, any>, target: Record<string, any>): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Compares a target translation dictionary against a base translation dictionary, returning any
|
|
26
|
+
* stale/orphaned keys present in target but absent in base.
|
|
27
|
+
*/
|
|
28
|
+
export declare function findStaleKeys(base: Record<string, any>, target: Record<string, any>): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Runs a comprehensive audit across all translation dictionaries comparing against
|
|
31
|
+
* base/defaultLocale.
|
|
32
|
+
*/
|
|
33
|
+
export declare function verifyTranslations(translations: Record<string, Record<string, any>>, options?: VerifyTranslationsOptions): TranslationAuditReport;
|
|
34
|
+
/**
|
|
35
|
+
* Formats a TranslationAuditReport into a human-readable diagnostic message.
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatAuditReport(report: TranslationAuditReport): string;
|
|
38
|
+
/**
|
|
39
|
+
* Automatically prunes stale keys and injects missing key skeletons from the base locale.
|
|
9
40
|
*/
|
|
10
|
-
export declare function
|
|
41
|
+
export declare function fixTranslations(translations: Record<string, Record<string, any>>, defaultLocale?: string): Record<string, Record<string, any>>;
|
|
11
42
|
//#endregion
|
package/dist/utils.mjs
CHANGED
|
@@ -1,43 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const result = {};
|
|
4
|
-
for (const [component, translations] of Object.entries(obj)) if (translations && typeof translations === "object") {
|
|
5
|
-
for (const [key, value] of Object.entries(translations)) if (typeof value === "string") result[`${component}.${key}`] = value;
|
|
6
|
-
}
|
|
7
|
-
return result;
|
|
8
|
-
}
|
|
9
|
-
function unflatten(flat) {
|
|
10
|
-
const result = {};
|
|
11
|
-
for (const [key, value] of Object.entries(flat)) {
|
|
12
|
-
const parts = key.split(".");
|
|
13
|
-
if (parts.length < 2) continue;
|
|
14
|
-
const component = parts[0] ?? "";
|
|
15
|
-
if (!component) continue;
|
|
16
|
-
const translationKey = parts.slice(1).join(".");
|
|
17
|
-
if (!(component in result)) result[component] = {};
|
|
18
|
-
result[component][translationKey] = value;
|
|
19
|
-
}
|
|
20
|
-
return result;
|
|
21
|
-
}
|
|
22
|
-
function extractKeys(source) {
|
|
23
|
-
const keys = [];
|
|
24
|
-
for (const [component, translations] of Object.entries(source)) if (translations && typeof translations === "object") for (const key of Object.keys(translations)) keys.push(`${component}.${key}`);
|
|
25
|
-
return keys;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Compares a target translation dictionary against a base/source translation dictionary, returning
|
|
29
|
-
* any missing component keys.
|
|
30
|
-
*/
|
|
31
|
-
function findMissingKeys(base, target) {
|
|
32
|
-
const missing = [];
|
|
33
|
-
for (const [component, keys] of Object.entries(base)) {
|
|
34
|
-
if (!target[component] || typeof target[component] !== "object") {
|
|
35
|
-
missing.push(component);
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
for (const key of Object.keys(keys)) if (target[component][key] === void 0) missing.push(`${component}.${key}`);
|
|
39
|
-
}
|
|
40
|
-
return missing;
|
|
41
|
-
}
|
|
42
|
-
//#endregion
|
|
43
|
-
export { extractKeys, findMissingKeys, flatten, unflatten };
|
|
1
|
+
import { a as fixTranslations, c as unflatten, i as findStaleKeys, l as verifyTranslations, n as extractPlaceholders, o as flatten, r as findMissingKeys, s as formatAuditReport, t as extractKeys } from "./utils-WDscZN9H.mjs";
|
|
2
|
+
export { extractKeys, extractPlaceholders, findMissingKeys, findStaleKeys, fixTranslations, flatten, formatAuditReport, unflatten, verifyTranslations };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimelight/i18n",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Rimelight Entertainment's Internationalization Package",
|
|
6
6
|
"homepage": "https://rimelight.com/docs",
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"type": "git",
|
|
16
16
|
"url": "git+https://github.com/Rimelight-Entertainment/rimelight.git"
|
|
17
17
|
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"rimelight-i18n": "./dist/cli.mjs"
|
|
20
|
+
},
|
|
18
21
|
"files": [
|
|
19
22
|
"dist"
|
|
20
23
|
],
|
|
@@ -43,21 +46,21 @@
|
|
|
43
46
|
"./utils": {
|
|
44
47
|
"types": "./dist/utils.d.mts",
|
|
45
48
|
"import": "./dist/utils.mjs"
|
|
49
|
+
},
|
|
50
|
+
"./cli": {
|
|
51
|
+
"types": "./dist/cli.d.mts",
|
|
52
|
+
"import": "./dist/cli.mjs"
|
|
46
53
|
}
|
|
47
54
|
},
|
|
48
55
|
"publishConfig": {
|
|
49
56
|
"access": "public"
|
|
50
57
|
},
|
|
51
|
-
"dependencies": {
|
|
52
|
-
"@nanostores/i18n": "1.3.3",
|
|
53
|
-
"nanostores": "1.5.3"
|
|
54
|
-
},
|
|
55
58
|
"devDependencies": {
|
|
56
|
-
"@rimelight/config": "0.0.
|
|
59
|
+
"@rimelight/config": "0.0.20",
|
|
57
60
|
"typescript": "6.0.3"
|
|
58
61
|
},
|
|
59
62
|
"engines": {
|
|
60
|
-
"node": ">=26.
|
|
63
|
+
"node": ">=26.9.0"
|
|
61
64
|
},
|
|
62
65
|
"scripts": {
|
|
63
66
|
"build": "vp pack",
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { Components, I18n, TranslationLoader, Translations, formatter } from "@nanostores/i18n";
|
|
2
|
-
//#region src/runtime.d.ts
|
|
3
|
-
/**
|
|
4
|
-
* A reactive store containing the current locale code. Set by middleware on each request, or
|
|
5
|
-
* manually via `currentLocale.set(locale)`.
|
|
6
|
-
*/
|
|
7
|
-
declare const currentLocale: import("nanostores").PreinitializedWritableAtom<string> & object;
|
|
8
|
-
type I18nInstance = I18n;
|
|
9
|
-
type FormatterInstance = ReturnType<typeof formatter>;
|
|
10
|
-
interface InitializeI18nOptions {
|
|
11
|
-
/**
|
|
12
|
-
* The default locale code (e.g. 'en').
|
|
13
|
-
*/
|
|
14
|
-
defaultLocale: string;
|
|
15
|
-
/**
|
|
16
|
-
* Pre-loaded translations keyed by locale then component.
|
|
17
|
-
*/
|
|
18
|
-
translations: Record<string, Components>;
|
|
19
|
-
/**
|
|
20
|
-
* Optional dynamic loader called when a locale is not in cache.
|
|
21
|
-
*/
|
|
22
|
-
get?: TranslationLoader;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Initializes the i18n system. Must be called once before any other i18n functions are used (the
|
|
26
|
-
* integration virtual module does this automatically).
|
|
27
|
-
*/
|
|
28
|
-
declare function initializeI18n(options: InitializeI18nOptions): void;
|
|
29
|
-
/**
|
|
30
|
-
* Returns the underlying nanostores/i18n instance. Throws if not initialized.
|
|
31
|
-
*/
|
|
32
|
-
declare function getI18nInstance(): I18nInstance;
|
|
33
|
-
/**
|
|
34
|
-
* Returns the formatter instance. Throws if not initialized.
|
|
35
|
-
*/
|
|
36
|
-
declare function getFormatterInstance(): FormatterInstance;
|
|
37
|
-
/**
|
|
38
|
-
* Returns a Formatter object for the current locale with methods for formatting numbers, dates, and
|
|
39
|
-
* relative times using the native Intl API.
|
|
40
|
-
*/
|
|
41
|
-
declare function useFormat(): ReturnType<FormatterInstance["get"]>;
|
|
42
|
-
/**
|
|
43
|
-
* Returns the translated strings for a component in the current locale. Falls back to defaultLocale
|
|
44
|
-
* loaded translations or `baseTranslations` if provided.
|
|
45
|
-
*/
|
|
46
|
-
interface ComponentMessages {
|
|
47
|
-
[key: string]: string;
|
|
48
|
-
}
|
|
49
|
-
declare function useI18n(componentName: string): ComponentMessages;
|
|
50
|
-
declare function useI18n<Body extends Translations>(componentName: string, baseTranslations: Body): ComponentMessages & Body;
|
|
51
|
-
/**
|
|
52
|
-
* Async version of useI18n that waits for translations to finish loading.
|
|
53
|
-
*/
|
|
54
|
-
declare function useI18nAsync(componentName: string): Promise<ComponentMessages>;
|
|
55
|
-
declare function useI18nAsync<Body extends Translations>(componentName: string, baseTranslations: Body): Promise<ComponentMessages & Body>;
|
|
56
|
-
/**
|
|
57
|
-
* Translates a single string key using dot-notation (e.g. `t("page_home.section_hero_title")`).
|
|
58
|
-
*/
|
|
59
|
-
declare function t(key: string, params?: Record<string, any>): string;
|
|
60
|
-
/**
|
|
61
|
-
* Returns the plural category (e.g. 'one', 'few', 'many', 'other') for a given number and locale
|
|
62
|
-
* using the native browser Intl.PluralRules API.
|
|
63
|
-
*/
|
|
64
|
-
declare function getPluralCategory(count: number, locale?: string): Intl.LDMLPluralRule;
|
|
65
|
-
/**
|
|
66
|
-
* Clears the translation cache.
|
|
67
|
-
*
|
|
68
|
-
* @param locale - If provided, clears only that locale's cache. Otherwise clears all.
|
|
69
|
-
*/
|
|
70
|
-
declare function clearCache(locale?: string): void;
|
|
71
|
-
//#endregion
|
|
72
|
-
export { getFormatterInstance as a, initializeI18n as c, useI18n as d, useI18nAsync as f, currentLocale as i, t as l, InitializeI18nOptions as n, getI18nInstance as o, clearCache as r, getPluralCategory as s, ComponentMessages as t, useFormat as u };
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import { createI18n, formatter, translationsLoading } from "@nanostores/i18n";
|
|
2
|
-
import { atom } from "nanostores";
|
|
3
|
-
//#region src/runtime.ts
|
|
4
|
-
/**
|
|
5
|
-
* A reactive store containing the current locale code. Set by middleware on each request, or
|
|
6
|
-
* manually via `currentLocale.set(locale)`.
|
|
7
|
-
*/
|
|
8
|
-
const currentLocale = atom("");
|
|
9
|
-
let i18nInstance;
|
|
10
|
-
let formatterInstance;
|
|
11
|
-
let baseLocaleDefault = "en";
|
|
12
|
-
let rawTranslationsDict = {};
|
|
13
|
-
function throwNotInitialized() {
|
|
14
|
-
throw new Error("i18n not initialized. Call initializeI18n first.");
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Wraps a TranslationLoader so that the returned object is guaranteed to contain a key for every
|
|
18
|
-
* requested component.
|
|
19
|
-
*
|
|
20
|
-
* The nanostores i18n library uses the keys of the object returned from `get` to clear its internal
|
|
21
|
-
* "requested" set. If a requested component is missing from the result (e.g. because the backend
|
|
22
|
-
* has no translations for it in the given locale yet), the internal loading atom is never set back
|
|
23
|
-
* to `false`, which causes useI18nAsync / translationsLoading to hang forever.
|
|
24
|
-
*
|
|
25
|
-
* This wrapper normalises the loader output so missing components are filled in with empty
|
|
26
|
-
* translation objects, falling back to the base translations defined at the call site.
|
|
27
|
-
*/
|
|
28
|
-
function wrapLoader(loader) {
|
|
29
|
-
return async (code, components) => {
|
|
30
|
-
const raw = await loader(code, components);
|
|
31
|
-
const normalised = Array.isArray(raw) ? Object.assign({}, ...raw) : { ...raw };
|
|
32
|
-
for (const component of components) if (!(component in normalised)) normalised[component] = {};
|
|
33
|
-
return normalised;
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Initializes the i18n system. Must be called once before any other i18n functions are used (the
|
|
38
|
-
* integration virtual module does this automatically).
|
|
39
|
-
*/
|
|
40
|
-
function initializeI18n(options) {
|
|
41
|
-
const { defaultLocale, translations, get } = options;
|
|
42
|
-
baseLocaleDefault = defaultLocale;
|
|
43
|
-
rawTranslationsDict = translations || {};
|
|
44
|
-
if (!i18nInstance) {
|
|
45
|
-
currentLocale.set(defaultLocale);
|
|
46
|
-
const formattedCache = {};
|
|
47
|
-
for (const [locale, components] of Object.entries(translations)) {
|
|
48
|
-
formattedCache[locale] = {};
|
|
49
|
-
for (const [compName, compBody] of Object.entries(components)) {
|
|
50
|
-
let bodyObj = {};
|
|
51
|
-
if (isStringRecord(compBody)) bodyObj = compBody;
|
|
52
|
-
else if (isStoreWithGet(compBody)) {
|
|
53
|
-
const res = compBody.get();
|
|
54
|
-
if (isStringRecord(res)) bodyObj = res;
|
|
55
|
-
}
|
|
56
|
-
formattedCache[locale][compName] = atom(bodyObj);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
i18nInstance = createI18n(currentLocale, {
|
|
60
|
-
baseLocale: defaultLocale,
|
|
61
|
-
get: wrapLoader(get ?? (async () => ({}))),
|
|
62
|
-
cache: formattedCache,
|
|
63
|
-
isSSR: true
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
formatterInstance = formatter(currentLocale);
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Returns the underlying nanostores/i18n instance. Throws if not initialized.
|
|
70
|
-
*/
|
|
71
|
-
function getI18nInstance() {
|
|
72
|
-
if (!i18nInstance) throwNotInitialized();
|
|
73
|
-
return i18nInstance;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Returns the formatter instance. Throws if not initialized.
|
|
77
|
-
*/
|
|
78
|
-
function getFormatterInstance() {
|
|
79
|
-
if (!formatterInstance) throwNotInitialized();
|
|
80
|
-
return formatterInstance;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Returns a Formatter object for the current locale with methods for formatting numbers, dates, and
|
|
84
|
-
* relative times using the native Intl API.
|
|
85
|
-
*/
|
|
86
|
-
function useFormat() {
|
|
87
|
-
return getFormatterInstance().get();
|
|
88
|
-
}
|
|
89
|
-
function isStoreWithGet(obj) {
|
|
90
|
-
if (typeof obj !== "object" || obj === null) return false;
|
|
91
|
-
return typeof Reflect.get(obj, "get") === "function";
|
|
92
|
-
}
|
|
93
|
-
function isStringRecord(obj) {
|
|
94
|
-
return typeof obj === "object" && obj !== null;
|
|
95
|
-
}
|
|
96
|
-
function getCachedComponent(locale, componentName) {
|
|
97
|
-
const normalizedLocale = locale.toLowerCase();
|
|
98
|
-
const candidates = [
|
|
99
|
-
locale,
|
|
100
|
-
normalizedLocale,
|
|
101
|
-
normalizedLocale.split("-")[0] ?? normalizedLocale,
|
|
102
|
-
baseLocaleDefault
|
|
103
|
-
];
|
|
104
|
-
for (const code of candidates) {
|
|
105
|
-
const rawComp = rawTranslationsDict[code]?.[componentName];
|
|
106
|
-
if (isStoreWithGet(rawComp)) {
|
|
107
|
-
const res = rawComp.get();
|
|
108
|
-
if (isStringRecord(res)) return res;
|
|
109
|
-
} else if (isStringRecord(rawComp)) return rawComp;
|
|
110
|
-
}
|
|
111
|
-
if (!i18nInstance) return void 0;
|
|
112
|
-
for (const code of candidates) {
|
|
113
|
-
const rawComp = i18nInstance.cache[code]?.[componentName];
|
|
114
|
-
if (isStoreWithGet(rawComp)) {
|
|
115
|
-
const res = rawComp.get();
|
|
116
|
-
if (isStringRecord(res)) return res;
|
|
117
|
-
} else if (isStringRecord(rawComp)) return rawComp;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
function useI18n(componentName, baseTranslations) {
|
|
121
|
-
const i18n = getI18nInstance();
|
|
122
|
-
const activeLocale = currentLocale.get() || baseLocaleDefault;
|
|
123
|
-
const fallbackDict = isStringRecord(baseTranslations) ? baseTranslations : {};
|
|
124
|
-
const baseDict = getCachedComponent(activeLocale, componentName) ?? getCachedComponent(baseLocaleDefault, componentName) ?? fallbackDict;
|
|
125
|
-
const targetObj = i18n(componentName, baseDict).get();
|
|
126
|
-
return new Proxy(targetObj, { get(target, prop) {
|
|
127
|
-
if (typeof prop === "symbol" || prop in Object.prototype) {
|
|
128
|
-
const val = Reflect.get(target, prop);
|
|
129
|
-
return typeof val === "string" ? val : "";
|
|
130
|
-
}
|
|
131
|
-
return target[prop] ?? baseDict[prop] ?? prop;
|
|
132
|
-
} });
|
|
133
|
-
}
|
|
134
|
-
async function useI18nAsync(componentName, baseTranslations) {
|
|
135
|
-
const i18n = getI18nInstance();
|
|
136
|
-
const cachedActive = getCachedComponent(currentLocale.get() || baseLocaleDefault, componentName);
|
|
137
|
-
const cachedDefault = getCachedComponent(baseLocaleDefault, componentName);
|
|
138
|
-
let baseDict = cachedActive ?? cachedDefault ?? {};
|
|
139
|
-
if (!cachedActive && !cachedDefault && isStringRecord(baseTranslations)) baseDict = baseTranslations;
|
|
140
|
-
const store = i18n(componentName, baseDict);
|
|
141
|
-
const unsubscribe = store.listen(() => {});
|
|
142
|
-
await translationsLoading(i18n);
|
|
143
|
-
unsubscribe();
|
|
144
|
-
return store.get();
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Translates a single string key using dot-notation (e.g. `t("page_home.section_hero_title")`).
|
|
148
|
-
*/
|
|
149
|
-
function t(key, params) {
|
|
150
|
-
const dotIndex = key.indexOf(".");
|
|
151
|
-
if (dotIndex === -1) return key;
|
|
152
|
-
const componentName = key.slice(0, dotIndex);
|
|
153
|
-
const keyName = key.slice(dotIndex + 1);
|
|
154
|
-
const activeLocale = currentLocale.get() || baseLocaleDefault;
|
|
155
|
-
let value = (getCachedComponent(activeLocale, componentName) ?? getCachedComponent(baseLocaleDefault, componentName) ?? {})[keyName] ?? key;
|
|
156
|
-
if (params && typeof params["count"] === "number" && typeof value === "object" && value !== null) {
|
|
157
|
-
const pluralCategory = getPluralCategory(params["count"], activeLocale);
|
|
158
|
-
value = value[pluralCategory] ?? value["other"] ?? key;
|
|
159
|
-
}
|
|
160
|
-
if (typeof value === "function") return value(params);
|
|
161
|
-
if (params && typeof value === "string") return value.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? `{${k}}`));
|
|
162
|
-
return typeof value === "string" ? value : String(value);
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Returns the plural category (e.g. 'one', 'few', 'many', 'other') for a given number and locale
|
|
166
|
-
* using the native browser Intl.PluralRules API.
|
|
167
|
-
*/
|
|
168
|
-
function getPluralCategory(count, locale = "en") {
|
|
169
|
-
return new Intl.PluralRules(locale).select(count);
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Clears the translation cache.
|
|
173
|
-
*
|
|
174
|
-
* @param locale - If provided, clears only that locale's cache. Otherwise clears all.
|
|
175
|
-
*/
|
|
176
|
-
function clearCache(locale) {
|
|
177
|
-
if (!i18nInstance) throwNotInitialized();
|
|
178
|
-
const cache = i18nInstance.cache;
|
|
179
|
-
if (locale) cache[locale] = {};
|
|
180
|
-
else for (const key in cache) cache[key] = {};
|
|
181
|
-
}
|
|
182
|
-
//#endregion
|
|
183
|
-
export { getPluralCategory as a, useFormat as c, getI18nInstance as i, useI18n as l, currentLocale as n, initializeI18n as o, getFormatterInstance as r, t as s, clearCache as t, useI18nAsync as u };
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { ComponentsJSON } from "@nanostores/i18n";
|
|
2
|
-
//#region src/types.d.ts
|
|
3
|
-
interface FlattenedTranslations {
|
|
4
|
-
[key: string]: string;
|
|
5
|
-
}
|
|
6
|
-
interface LocaleFile {
|
|
7
|
-
[component: string]: {
|
|
8
|
-
[key: string]: string;
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
interface KVNamespaceBinding {
|
|
12
|
-
get(key: string, type: "json"): Promise<Record<string, string> | null>;
|
|
13
|
-
put(key: string, value: string): Promise<void>;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Derives dot-notated nested translation keys (e.g. "page_home.section_hero_title") from a
|
|
17
|
-
* translation schema type.
|
|
18
|
-
*/
|
|
19
|
-
type NestedTranslationKeys<T> = T extends object ? { [K in keyof T & (string | number)]: T[K] extends object ? `${K}.${NestedTranslationKeys<T[K]>}` : `${K}`; }[keyof T & (string | number)] : never;
|
|
20
|
-
/**
|
|
21
|
-
* Augmented interface for app-level strongly typed translation keys. Applications can declare:
|
|
22
|
-
*
|
|
23
|
-
* ```ts
|
|
24
|
-
* declare module "@rimelight/i18n" {
|
|
25
|
-
* interface CustomTranslations extends typeof import("./i18n/en.json") {}
|
|
26
|
-
* }
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
interface CustomTranslations {}
|
|
30
|
-
type TranslationKey = [keyof CustomTranslations] extends [never] ? string : NestedTranslationKeys<CustomTranslations>;
|
|
31
|
-
//#endregion
|
|
32
|
-
export { LocaleFile as a, KVNamespaceBinding as i, CustomTranslations as n, NestedTranslationKeys as o, FlattenedTranslations as r, TranslationKey as s, ComponentsJSON as t };
|