@rimelight/i18n 0.0.5 → 0.0.7

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rimelight Entertainment
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,39 @@
1
+ import { ComponentsJSON, KVNamespaceBinding } from "./types.mjs";
2
+ import { clearCache, currentLocale, getFormatterInstance, getI18nInstance, t, useFormat } from "@rimelight/i18n:runtime";
3
+ import { TranslationLoader, Translations } from "@nanostores/i18n";
4
+ //#region src/index.d.ts
5
+ interface ComponentMessages {
6
+ [key: string]: string;
7
+ }
8
+ declare function useI18n(componentName: string): ComponentMessages;
9
+ declare function useI18n<Body extends Translations>(componentName: string, baseTranslations: Body): ComponentMessages & Body;
10
+ declare function useI18n(astro: {
11
+ currentLocale?: string | undefined;
12
+ params?: Record<string, any>;
13
+ } | undefined | null, componentName: string): ComponentMessages;
14
+ declare function useI18n<Body extends Translations>(astro: {
15
+ currentLocale?: string | undefined;
16
+ params?: Record<string, any>;
17
+ } | undefined | null, componentName: string, baseTranslations: Body): ComponentMessages & Body;
18
+ declare function useI18nAsync(componentName: string): Promise<Record<string, string>>;
19
+ declare function useI18nAsync<Body extends Translations>(componentName: string, baseTranslations: Body): Promise<Record<string, string> & Body>;
20
+ declare function useI18nAsync(astro: {
21
+ currentLocale?: string | undefined;
22
+ params?: Record<string, any>;
23
+ } | undefined | null, componentName: string): Promise<Record<string, string>>;
24
+ declare function useI18nAsync<Body extends Translations>(astro: {
25
+ currentLocale?: string | undefined;
26
+ params?: Record<string, any>;
27
+ } | undefined | null, componentName: string, baseTranslations: Body): Promise<Record<string, string> & Body>;
28
+ declare function getLocale(): string;
29
+ declare function getRelativeLocaleUrl(path: string): string;
30
+ declare function getRelativeLocaleUrl(locale: string, path: string): string;
31
+ declare function setKVBinding(kv: KVNamespaceBinding): void;
32
+ declare function createTranslationLoader(translations: Record<string, ComponentsJSON>): TranslationLoader;
33
+ /**
34
+ * Returns the plural category (e.g. 'one', 'few', 'many', 'other') for a given number and locale
35
+ * using the native browser Intl.PluralRules API.
36
+ */
37
+ declare function getPluralCategory(count: number, locale?: string): Intl.LDMLPluralRule;
38
+ //#endregion
39
+ export { ComponentMessages, clearCache, createTranslationLoader, currentLocale, getFormatterInstance, getI18nInstance, getLocale, getPluralCategory, getRelativeLocaleUrl, setKVBinding, t, useFormat, useI18n, useI18nAsync };
package/dist/index.mjs ADDED
@@ -0,0 +1,63 @@
1
+ import { clearCache, currentLocale, getFormatterInstance, getI18nInstance, t, useFormat, useI18n as useI18n$1, useI18nAsync as useI18nAsync$1 } from "@rimelight/i18n:runtime";
2
+ import { getRelativeLocaleUrl as getRelativeLocaleUrl$1 } from "astro:i18n";
3
+ //#region src/index.ts
4
+ function useI18n(arg1, arg2, arg3) {
5
+ if (typeof arg1 === "string") return useI18n$1(arg1, arg2);
6
+ else {
7
+ const locale = arg1?.currentLocale || arg1?.params?.locale;
8
+ if (locale) currentLocale.set(locale);
9
+ return useI18n$1(arg2, arg3);
10
+ }
11
+ }
12
+ function useI18nAsync(arg1, arg2, arg3) {
13
+ if (typeof arg1 === "string") return useI18nAsync$1(arg1, arg2);
14
+ else {
15
+ const locale = arg1?.currentLocale || arg1?.params?.locale;
16
+ if (locale) currentLocale.set(locale);
17
+ return useI18nAsync$1(arg2, arg3);
18
+ }
19
+ }
20
+ function getLocale() {
21
+ return currentLocale.get() || "en";
22
+ }
23
+ function getRelativeLocaleUrl(arg1, arg2) {
24
+ if (arg2 !== void 0) return getRelativeLocaleUrl$1(arg1, arg2);
25
+ else {
26
+ const locale = getLocale();
27
+ return getRelativeLocaleUrl$1(locale, arg1);
28
+ }
29
+ }
30
+ let kvBinding = null;
31
+ function setKVBinding(kv) {
32
+ kvBinding = kv;
33
+ }
34
+ function createTranslationLoader(translations) {
35
+ return async (locale, components) => {
36
+ const localeTranslations = translations[locale] || {};
37
+ if (!components || !Array.isArray(components) || components.length === 0) return localeTranslations;
38
+ const activeKv = kvBinding;
39
+ if (activeKv) try {
40
+ const results = await Promise.all(components.map(async (name) => {
41
+ const key = `locale:${locale}:${name}`;
42
+ const data = await activeKv.get(key, "json");
43
+ if (data == null) return { [name]: localeTranslations[name] ?? {} };
44
+ return { [name]: data };
45
+ }));
46
+ return Object.assign({}, ...results);
47
+ } catch {}
48
+ const results = await Promise.all(components.map(async (name) => {
49
+ const data = localeTranslations[name];
50
+ return { [name]: data ?? {} };
51
+ }));
52
+ return Object.assign({}, ...results);
53
+ };
54
+ }
55
+ /**
56
+ * Returns the plural category (e.g. 'one', 'few', 'many', 'other') for a given number and locale
57
+ * using the native browser Intl.PluralRules API.
58
+ */
59
+ function getPluralCategory(count, locale = "en") {
60
+ return new Intl.PluralRules(locale).select(count);
61
+ }
62
+ //#endregion
63
+ export { clearCache, createTranslationLoader, currentLocale, getFormatterInstance, getI18nInstance, getLocale, getPluralCategory, getRelativeLocaleUrl, setKVBinding, t, useFormat, useI18n, useI18nAsync };
@@ -0,0 +1,11 @@
1
+ import { AstroIntegration } from "astro";
2
+ //#region src/integration.d.ts
3
+ interface RimelightI18nOptions {
4
+ validateExtraction?: boolean;
5
+ translations?: Record<string, Record<string, any>>;
6
+ kvBinding?: string;
7
+ translationLoader?: string;
8
+ }
9
+ declare function rimelightI18n(options?: RimelightI18nOptions): AstroIntegration[];
10
+ //#endregion
11
+ export { RimelightI18nOptions, rimelightI18n };
@@ -0,0 +1,122 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ //#region src/integration.ts
4
+ function rimelightI18n(options) {
5
+ const validateExtraction = options?.validateExtraction ?? true;
6
+ const translations = options?.translations;
7
+ const kvBinding = options?.kvBinding ?? "TRANSLATIONS_KV";
8
+ const translationLoader = options?.translationLoader;
9
+ const normalisedTranslations = {};
10
+ if (translations) for (const [locale, val] of Object.entries(translations)) normalisedTranslations[locale] = val;
11
+ const VIRTUAL_MODULE_ID = "@rimelight/i18n:runtime";
12
+ const RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
13
+ const runtimeSpecifier = "@rimelight/i18n/runtime";
14
+ const loaderImport = translationLoader ? `import translationLoader from ${JSON.stringify(translationLoader)};` : "";
15
+ const loaderOption = translationLoader ? ", get: translationLoader" : "";
16
+ return [{
17
+ name: "@rimelight/i18n",
18
+ hooks: {
19
+ "astro:config:setup": ({ config, addMiddleware, updateConfig, logger }) => {
20
+ const locales = config.i18n?.locales ?? ["en"];
21
+ const defaultLocale = config.i18n?.defaultLocale ?? "en";
22
+ const virtualModuleContent = `\
23
+ import { initializeI18n, useFormat, useI18n, useI18nAsync, t,
24
+ currentLocale, getI18nInstance, getFormatterInstance, clearCache }
25
+ from ${JSON.stringify(runtimeSpecifier)};
26
+ ${loaderImport}
27
+
28
+ initializeI18n({
29
+ defaultLocale: ${JSON.stringify(defaultLocale)},
30
+ translations: ${JSON.stringify(normalisedTranslations)}${loaderOption}
31
+ });
32
+
33
+ export { useFormat, useI18n, useI18nAsync, t, currentLocale,
34
+ getI18nInstance, getFormatterInstance, clearCache };
35
+ `;
36
+ addMiddleware({
37
+ entrypoint: "@rimelight/i18n/middleware",
38
+ order: "pre"
39
+ });
40
+ updateConfig({ vite: {
41
+ ssr: { noExternal: ["@rimelight/i18n"] },
42
+ plugins: [{
43
+ name: "vite-plugin-rimelight-i18n-runtime",
44
+ resolveId(id) {
45
+ if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID;
46
+ return null;
47
+ },
48
+ load(id) {
49
+ if (id === RESOLVED_VIRTUAL_MODULE_ID) return virtualModuleContent;
50
+ return null;
51
+ }
52
+ }, {
53
+ name: "vite-plugin-rimelight-i18n-config",
54
+ resolveId(id) {
55
+ if (id === "virtual:rimelight-i18n-config") return "\0" + id;
56
+ return null;
57
+ },
58
+ load(id) {
59
+ if (id === "\0virtual:rimelight-i18n-config") return `
60
+ export const locales = ${JSON.stringify(locales)};
61
+ export const defaultLocale = ${JSON.stringify(defaultLocale)};
62
+ `;
63
+ return null;
64
+ }
65
+ }]
66
+ } });
67
+ if (validateExtraction) logger.info("i18n extraction validation active");
68
+ },
69
+ "astro:config:done": ({ injectTypes }) => {
70
+ injectTypes({
71
+ filename: "rimelight-i18n.d.ts",
72
+ content: `\
73
+ declare module "@rimelight/i18n:runtime" {
74
+ import type { Translations } from '@nanostores/i18n';
75
+ export type { InitializeI18nOptions } from '@rimelight/i18n/runtime';
76
+ export const currentLocale: import('nanostores').PreinitializedWritableAtom<string> & object;
77
+ export declare function initializeI18n(options: import('@rimelight/i18n/runtime').InitializeI18nOptions): void;
78
+ export declare function useFormat(): import('@nanostores/i18n').Formatter;
79
+ export type ComponentMessages = Record<string, string>;
80
+ export declare function useI18n(componentName: string): ComponentMessages;
81
+ export declare function useI18n<Body extends Translations>(componentName: string, baseTranslations: Body): ComponentMessages & Body;
82
+ export declare function useI18nAsync(componentName: string): Promise<Record<string, string>>;
83
+ export declare function useI18nAsync<Body extends Translations>(componentName: string, baseTranslations: Body): Promise<Record<string, string> & Body>;
84
+ export declare function t(key: string, params?: Record<string, any>): string;
85
+ export declare function t(astro: { currentLocale?: string | undefined; params?: Record<string, any> } | undefined | null, key: string, params?: Record<string, any>): string;
86
+ export declare function getLocale(): string;
87
+ export declare function getI18nInstance(): ReturnType<typeof import('@nanostores/i18n').createI18n>;
88
+ export declare function getFormatterInstance(): ReturnType<typeof import('@nanostores/i18n').formatter>;
89
+ export declare function clearCache(locale?: string): void;
90
+ }
91
+ `
92
+ });
93
+ },
94
+ "astro:build:done": async ({ logger }) => {
95
+ if (translations) {
96
+ const seedData = { keys: [] };
97
+ for (const [locale, localeData] of Object.entries(translations)) for (const [component, componentData] of Object.entries(localeData)) seedData.keys.push({
98
+ key: `locale:${locale}:${component}`,
99
+ value: JSON.stringify(componentData)
100
+ });
101
+ const outDir = process.env.WRANGLER_OUT_DIR ?? "dist";
102
+ const seedFile = path.join(outDir, "_translations-seed.json");
103
+ fs.mkdirSync(path.dirname(seedFile), { recursive: true });
104
+ fs.writeFileSync(seedFile, JSON.stringify(seedData, null, 2));
105
+ logger.info(`translations seed file written to ${seedFile}`);
106
+ if (process.env.WRANGLER_SEED_KV === "true" || process.env.CI === "true") try {
107
+ const { execSync } = await import("node:child_process");
108
+ execSync(`npx wrangler kv:bulk put --binding "${kvBinding}" "${seedFile}"`, {
109
+ stdio: "pipe",
110
+ timeout: 3e4
111
+ });
112
+ logger.info(`KV "${kvBinding}" seeded with translations`);
113
+ } catch {
114
+ logger.warn(`could not seed KV automatically — run "npx wrangler kv:bulk put --binding "${kvBinding}" "${seedFile}"" to seed translations into KV`);
115
+ }
116
+ } else logger.info("i18n build complete (no translations to seed)");
117
+ }
118
+ }
119
+ }];
120
+ }
121
+ //#endregion
122
+ export { rimelightI18n };
@@ -0,0 +1,7 @@
1
+ //#region src/middleware/routing.d.ts
2
+ declare const i18n: (context: any, next: any) => Promise<any>;
3
+ //#endregion
4
+ //#region src/middleware.d.ts
5
+ declare const onRequest: (_context: unknown, next: () => Promise<Response>) => Promise<Response>;
6
+ //#endregion
7
+ export { i18n, onRequest };
@@ -0,0 +1,56 @@
1
+ import { setKVBinding } from "./index.mjs";
2
+ import { currentLocale } from "./runtime.mjs";
3
+ import { env } from "cloudflare:workers";
4
+ import { defaultLocale, locales } from "virtual:rimelight-i18n-config";
5
+ //#region src/middleware/routing.ts
6
+ const SUPPORTED_LOCALES = new Set(locales);
7
+ const DEFAULT_LOCALE = defaultLocale;
8
+ function getPreferredLocale(acceptLanguage) {
9
+ if (!acceptLanguage) return DEFAULT_LOCALE;
10
+ const parsed = acceptLanguage.split(",").map((lang) => {
11
+ const parts = lang.split(";");
12
+ const code = (parts[0] ?? "").trim().toLowerCase();
13
+ const base = code.split("-")[0] ?? "";
14
+ let q = 1;
15
+ const qualityPart = parts[1];
16
+ if (qualityPart) {
17
+ const qMatch = qualityPart.match(/q=([0-9.]+)/);
18
+ if (qMatch) q = parseFloat(qMatch[1] ?? "1");
19
+ }
20
+ return {
21
+ code,
22
+ base,
23
+ q
24
+ };
25
+ }).toSorted((a, b) => b.q - a.q);
26
+ for (const item of parsed) {
27
+ if (SUPPORTED_LOCALES.has(item.code)) return item.code;
28
+ const base = item.base;
29
+ if (SUPPORTED_LOCALES.has(base)) return base;
30
+ }
31
+ return DEFAULT_LOCALE;
32
+ }
33
+ const i18n = async (context, next) => {
34
+ const url = new URL(context.request.url);
35
+ const pathname = url.pathname;
36
+ const paramLocale = context.params?.locale;
37
+ const firstSegment = pathname.split("/").find(Boolean);
38
+ const activeLocale = (paramLocale && SUPPORTED_LOCALES.has(paramLocale) ? paramLocale : null) || (firstSegment && SUPPORTED_LOCALES.has(firstSegment) ? firstSegment : null) || context.currentLocale || DEFAULT_LOCALE;
39
+ currentLocale.set(activeLocale);
40
+ if (context.request.method !== "GET" && context.request.method !== "HEAD") return next();
41
+ if (pathname.startsWith("/api") || pathname.startsWith("/_") || pathname.includes(".")) return next();
42
+ if (firstSegment && SUPPORTED_LOCALES.has(firstSegment)) return next();
43
+ const locale = getPreferredLocale(context.request.headers.get("accept-language"));
44
+ currentLocale.set(locale);
45
+ const targetPath = `/${locale}${pathname}${url.search}`;
46
+ return context.redirect(targetPath, 302);
47
+ };
48
+ //#endregion
49
+ //#region src/middleware.ts
50
+ const onRequest = async (_context, next) => {
51
+ const kv = Reflect.get(env, "TRANSLATIONS_KV");
52
+ if (kv) setKVBinding(kv);
53
+ return next();
54
+ };
55
+ //#endregion
56
+ export { i18n, onRequest };
@@ -0,0 +1,71 @@
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
+ declare function t(astro: {
61
+ currentLocale?: string | undefined;
62
+ params?: Record<string, any>;
63
+ } | undefined | null, key: string, params?: Record<string, any>): string;
64
+ /**
65
+ * Clears the translation cache.
66
+ *
67
+ * @param locale - If provided, clears only that locale's cache. Otherwise clears all.
68
+ */
69
+ declare function clearCache(locale?: string): void;
70
+ //#endregion
71
+ export { ComponentMessages, InitializeI18nOptions, clearCache, currentLocale, getFormatterInstance, getI18nInstance, initializeI18n, t, useFormat, useI18n, useI18nAsync };
@@ -0,0 +1,179 @@
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
+ function t(arg1, arg2, arg3) {
147
+ let key;
148
+ let params;
149
+ if (typeof arg1 === "string") {
150
+ key = arg1;
151
+ params = arg2;
152
+ } else {
153
+ const locale = arg1?.currentLocale || arg1?.params?.locale;
154
+ if (locale) currentLocale.set(locale);
155
+ key = arg2;
156
+ params = arg3;
157
+ }
158
+ const dotIndex = key.indexOf(".");
159
+ if (dotIndex === -1) return key;
160
+ const componentName = key.slice(0, dotIndex);
161
+ const keyName = key.slice(dotIndex + 1);
162
+ const value = (getCachedComponent(currentLocale.get() || baseLocaleDefault, componentName) ?? getCachedComponent(baseLocaleDefault, componentName) ?? {})[keyName] ?? key;
163
+ if (typeof value === "function") return value(params);
164
+ if (params && typeof value === "string") return value.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? `{${k}}`));
165
+ return value;
166
+ }
167
+ /**
168
+ * Clears the translation cache.
169
+ *
170
+ * @param locale - If provided, clears only that locale's cache. Otherwise clears all.
171
+ */
172
+ function clearCache(locale) {
173
+ if (!i18nInstance) throwNotInitialized();
174
+ const cache = i18nInstance.cache;
175
+ if (locale) cache[locale] = {};
176
+ else for (const key in cache) cache[key] = {};
177
+ }
178
+ //#endregion
179
+ export { clearCache, currentLocale, getFormatterInstance, getI18nInstance, initializeI18n, t, useFormat, useI18n, useI18nAsync };
@@ -0,0 +1,16 @@
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
+ //#endregion
16
+ export { type ComponentsJSON, FlattenedTranslations, KVNamespaceBinding, LocaleFile };
package/dist/types.mjs ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import { ComponentsJSON, FlattenedTranslations } from "./types.mjs";
2
+ //#region src/utils.d.ts
3
+ declare function flatten(obj: ComponentsJSON): FlattenedTranslations;
4
+ declare function unflatten(flat: FlattenedTranslations): ComponentsJSON;
5
+ declare function extractKeys(source: ComponentsJSON): string[];
6
+ //#endregion
7
+ export { extractKeys, flatten, unflatten };
package/dist/utils.mjs ADDED
@@ -0,0 +1,28 @@
1
+ //#region src/utils.ts
2
+ function flatten(obj) {
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
+ //#endregion
28
+ export { extractKeys, flatten, unflatten };