@rimelight/i18n 0.0.6 → 0.0.8

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/utils.mjs ADDED
@@ -0,0 +1,43 @@
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
+ /**
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelight/i18n",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment's Internationalization Package",
6
6
  "homepage": "https://rimelight.com/docs",
@@ -16,41 +16,51 @@
16
16
  "url": "git+https://github.com/Rimelight-Entertainment/rimelight.git"
17
17
  },
18
18
  "files": [
19
- "src"
19
+ "dist"
20
20
  ],
21
21
  "type": "module",
22
22
  "exports": {
23
- ".": "./src/index.ts",
24
- "./integration": "./src/integration.ts",
25
- "./middleware": "./src/middleware.ts",
26
- "./runtime": "./src/runtime.ts",
27
- "./types": "./src/types.ts",
28
- "./utils": "./src/utils.ts"
23
+ ".": {
24
+ "types": "./dist/index.d.mts",
25
+ "import": "./dist/index.mjs"
26
+ },
27
+ "./plugin": {
28
+ "types": "./dist/plugin.d.mts",
29
+ "import": "./dist/plugin.mjs"
30
+ },
31
+ "./hono": {
32
+ "types": "./dist/hono.d.mts",
33
+ "import": "./dist/hono.mjs"
34
+ },
35
+ "./runtime": {
36
+ "types": "./dist/runtime.d.mts",
37
+ "import": "./dist/runtime.mjs"
38
+ },
39
+ "./types": {
40
+ "types": "./dist/types.d.mts",
41
+ "import": "./dist/types.mjs"
42
+ },
43
+ "./utils": {
44
+ "types": "./dist/utils.d.mts",
45
+ "import": "./dist/utils.mjs"
46
+ }
29
47
  },
30
48
  "publishConfig": {
31
49
  "access": "public"
32
50
  },
33
51
  "dependencies": {
34
52
  "@nanostores/i18n": "1.3.3",
35
- "nanostores": "1.5.2"
53
+ "nanostores": "1.5.3"
36
54
  },
37
55
  "devDependencies": {
38
- "@rimelight/config": "0.0.3",
39
- "astro": "7.2.7",
56
+ "@rimelight/config": "0.0.4",
40
57
  "typescript": "6.0.3"
41
58
  },
42
- "peerDependencies": {
43
- "astro": ">=7.0.0"
44
- },
45
- "peerDependenciesMeta": {
46
- "astro": {
47
- "optional": true
48
- }
49
- },
50
59
  "engines": {
51
60
  "node": ">=26.7.0"
52
61
  },
53
62
  "scripts": {
63
+ "build": "vp pack",
54
64
  "check": "vp check --fix"
55
65
  }
56
66
  }
package/src/env.d.ts DELETED
@@ -1,13 +0,0 @@
1
- // Enables importing .astro components inside .ts files (e.g., email rendering)
2
- declare module "*.astro" {
3
- export default {} as import("astro/runtime/server").AstroComponentFactory
4
- }
5
-
6
- declare module "astro:i18n" {
7
- export function getRelativeLocaleUrl(locale: string, path?: string): string
8
- export function getAbsoluteLocaleUrl(locale: string, path?: string): string
9
- }
10
-
11
- declare module "@rimelight/i18n:runtime" {
12
- export * from "./runtime"
13
- }
package/src/index.ts DELETED
@@ -1,143 +0,0 @@
1
- import type { TranslationLoader } from "@nanostores/i18n"
2
- import type { ComponentsJSON, KVNamespaceBinding } from "./types"
3
- import {
4
- useI18n as baseUseI18n,
5
- useI18nAsync as baseUseI18nAsync,
6
- t,
7
- currentLocale,
8
- useFormat,
9
- clearCache,
10
- getI18nInstance,
11
- getFormatterInstance
12
- } from "@rimelight/i18n:runtime"
13
- import type { Translations } from "@nanostores/i18n"
14
-
15
- export { t, currentLocale, useFormat, clearCache, getI18nInstance, getFormatterInstance }
16
-
17
- export interface ComponentMessages {
18
- [key: string]: string
19
- }
20
-
21
- export function useI18n(componentName: string): ComponentMessages
22
- export function useI18n<Body extends Translations>(
23
- componentName: string,
24
- baseTranslations: Body
25
- ): ComponentMessages & Body
26
- export function useI18n(
27
- astro: { currentLocale?: string | undefined; params?: Record<string, any> } | undefined | null,
28
- componentName: string
29
- ): ComponentMessages
30
- export function useI18n<Body extends Translations>(
31
- astro: { currentLocale?: string | undefined; params?: Record<string, any> } | undefined | null,
32
- componentName: string,
33
- baseTranslations: Body
34
- ): ComponentMessages & Body
35
- export function useI18n(arg1: any, arg2?: any, arg3?: any): any {
36
- if (typeof arg1 === "string") {
37
- return baseUseI18n(arg1, arg2)
38
- } else {
39
- const locale = arg1?.currentLocale || arg1?.params?.locale
40
- if (locale) {
41
- currentLocale.set(locale)
42
- }
43
- return baseUseI18n(arg2, arg3)
44
- }
45
- }
46
-
47
- export function useI18nAsync(componentName: string): Promise<Record<string, string>>
48
- export function useI18nAsync<Body extends Translations>(
49
- componentName: string,
50
- baseTranslations: Body
51
- ): Promise<Record<string, string> & Body>
52
- export function useI18nAsync(
53
- astro: { currentLocale?: string | undefined; params?: Record<string, any> } | undefined | null,
54
- componentName: string
55
- ): Promise<Record<string, string>>
56
- export function useI18nAsync<Body extends Translations>(
57
- astro: { currentLocale?: string | undefined; params?: Record<string, any> } | undefined | null,
58
- componentName: string,
59
- baseTranslations: Body
60
- ): Promise<Record<string, string> & Body>
61
- export function useI18nAsync(arg1: any, arg2?: any, arg3?: any): any {
62
- if (typeof arg1 === "string") {
63
- return baseUseI18nAsync(arg1, arg2)
64
- } else {
65
- const locale = arg1?.currentLocale || arg1?.params?.locale
66
- if (locale) {
67
- currentLocale.set(locale)
68
- }
69
- return baseUseI18nAsync(arg2, arg3)
70
- }
71
- }
72
-
73
- import { getRelativeLocaleUrl as astroGetRelativeLocaleUrl } from "astro:i18n"
74
-
75
- export function getLocale(): string {
76
- return currentLocale.get() || "en"
77
- }
78
-
79
- export function getRelativeLocaleUrl(path: string): string
80
- export function getRelativeLocaleUrl(locale: string, path: string): string
81
- export function getRelativeLocaleUrl(arg1: string, arg2?: string): string {
82
- if (arg2 !== undefined) {
83
- return astroGetRelativeLocaleUrl(arg1, arg2)
84
- } else {
85
- const locale = getLocale()
86
- return astroGetRelativeLocaleUrl(locale, arg1)
87
- }
88
- }
89
-
90
- let kvBinding: KVNamespaceBinding | null = null
91
-
92
- export function setKVBinding(kv: KVNamespaceBinding) {
93
- kvBinding = kv
94
- }
95
-
96
- export function createTranslationLoader(
97
- translations: Record<string, ComponentsJSON>
98
- ): TranslationLoader {
99
- return async (locale, components) => {
100
- const localeTranslations = translations[locale] || {}
101
-
102
- // If components is not provided, is not an array, or is empty, return the entire locale's translations.
103
- if (!components || !Array.isArray(components) || components.length === 0) {
104
- return localeTranslations
105
- }
106
-
107
- const activeKv = kvBinding
108
- if (activeKv) {
109
- try {
110
- const results = await Promise.all(
111
- components.map(async (name) => {
112
- const key = `locale:${locale}:${name}`
113
- const data = await activeKv.get(key, "json")
114
- // Fall back to bundled translation for this component if missing in KV
115
- if (data == null) {
116
- return { [name]: localeTranslations[name] ?? {} }
117
- }
118
- return { [name]: data }
119
- })
120
- )
121
- return Object.assign({}, ...results)
122
- } catch {
123
- // KV read failed — fall through to static
124
- }
125
- }
126
-
127
- const results = await Promise.all(
128
- components.map(async (name) => {
129
- const data = localeTranslations[name]
130
- return { [name]: data ?? {} }
131
- })
132
- )
133
- return Object.assign({}, ...results)
134
- }
135
- }
136
-
137
- /**
138
- * Returns the plural category (e.g. 'one', 'few', 'many', 'other') for a given number and locale
139
- * using the native browser Intl.PluralRules API.
140
- */
141
- export function getPluralCategory(count: number, locale: string = "en"): Intl.LDMLPluralRule {
142
- return new Intl.PluralRules(locale).select(count)
143
- }
@@ -1,185 +0,0 @@
1
- import type { AstroIntegration } from "astro"
2
- import fs from "node:fs"
3
- import path from "node:path"
4
-
5
- export interface RimelightI18nOptions {
6
- validateExtraction?: boolean
7
- translations?: Record<string, Record<string, any>>
8
- kvBinding?: string
9
- translationLoader?: string
10
- }
11
-
12
- export function rimelightI18n(options?: RimelightI18nOptions): AstroIntegration[] {
13
- const validateExtraction = options?.validateExtraction ?? true
14
- const translations = options?.translations
15
- const kvBinding = options?.kvBinding ?? "TRANSLATIONS_KV"
16
- const translationLoader = options?.translationLoader
17
-
18
- // Normalise translations passed in options
19
- const normalisedTranslations: Record<string, any> = {}
20
- if (translations) {
21
- for (const [locale, val] of Object.entries(translations)) {
22
- normalisedTranslations[locale] = val
23
- }
24
- }
25
-
26
- // ---------- Vite virtual module: @rimelight/i18n:runtime ----------
27
- // This replicates what astro-nanostores-i18n integration.js used to do:
28
- // it creates a virtual module that calls initializeI18n() once at module
29
- // evaluation time and re-exports all runtime helpers.
30
- const VIRTUAL_MODULE_ID = "@rimelight/i18n:runtime"
31
- const RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID
32
-
33
- // Use the package export specifier so Vite resolves it through normal
34
- // module resolution rather than an absolute filesystem path.
35
- const runtimeSpecifier = "@rimelight/i18n/runtime"
36
-
37
- const loaderImport = translationLoader
38
- ? `import translationLoader from ${JSON.stringify(translationLoader)};`
39
- : ""
40
- const loaderOption = translationLoader ? ", get: translationLoader" : ""
41
-
42
- // ----------------------------------------------------------------
43
-
44
- const rimelightI18nCore: AstroIntegration = {
45
- name: "@rimelight/i18n",
46
- hooks: {
47
- "astro:config:setup": ({ config, addMiddleware, updateConfig, logger }) => {
48
- const locales = config.i18n?.locales ?? ["en"]
49
- const defaultLocale = config.i18n?.defaultLocale ?? "en"
50
-
51
- const virtualModuleContent = `\
52
- import { initializeI18n, useFormat, useI18n, useI18nAsync, t,
53
- currentLocale, getI18nInstance, getFormatterInstance, clearCache }
54
- from ${JSON.stringify(runtimeSpecifier)};
55
- ${loaderImport}
56
-
57
- initializeI18n({
58
- defaultLocale: ${JSON.stringify(defaultLocale)},
59
- translations: ${JSON.stringify(normalisedTranslations)}${loaderOption}
60
- });
61
-
62
- export { useFormat, useI18n, useI18nAsync, t, currentLocale,
63
- getI18nInstance, getFormatterInstance, clearCache };
64
- `
65
-
66
- addMiddleware({
67
- entrypoint: "@rimelight/i18n/middleware",
68
- order: "pre"
69
- })
70
-
71
- updateConfig({
72
- vite: {
73
- ssr: {
74
- noExternal: ["@rimelight/i18n"]
75
- },
76
- plugins: [
77
- // Virtual module: @rimelight/i18n:runtime
78
- {
79
- name: "vite-plugin-rimelight-i18n-runtime",
80
- resolveId(id) {
81
- if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID
82
- return null
83
- },
84
- load(id) {
85
- if (id === RESOLVED_VIRTUAL_MODULE_ID) return virtualModuleContent
86
- return null
87
- }
88
- },
89
- // Virtual module: virtual:rimelight-i18n-config (locale metadata)
90
- {
91
- name: "vite-plugin-rimelight-i18n-config",
92
- resolveId(id) {
93
- if (id === "virtual:rimelight-i18n-config") return "\0" + id
94
- return null
95
- },
96
- load(id) {
97
- if (id === "\0virtual:rimelight-i18n-config") {
98
- return `
99
- export const locales = ${JSON.stringify(locales)};
100
- export const defaultLocale = ${JSON.stringify(defaultLocale)};
101
- `
102
- }
103
- return null
104
- }
105
- }
106
- ]
107
- }
108
- })
109
-
110
- if (validateExtraction) {
111
- logger.info("i18n extraction validation active")
112
- }
113
- },
114
-
115
- "astro:config:done": ({ injectTypes }) => {
116
- // Inject ambient type declarations for the virtual module so TypeScript
117
- // and editor tooling know the shape of @rimelight/i18n:runtime.
118
- injectTypes({
119
- filename: "rimelight-i18n.d.ts",
120
- content: `\
121
- declare module "@rimelight/i18n:runtime" {
122
- import type { Translations } from '@nanostores/i18n';
123
- export type { InitializeI18nOptions } from '@rimelight/i18n/runtime';
124
- export const currentLocale: import('nanostores').PreinitializedWritableAtom<string> & object;
125
- export declare function initializeI18n(options: import('@rimelight/i18n/runtime').InitializeI18nOptions): void;
126
- export declare function useFormat(): import('@nanostores/i18n').Formatter;
127
- export type ComponentMessages = Record<string, string>;
128
- export declare function useI18n(componentName: string): ComponentMessages;
129
- export declare function useI18n<Body extends Translations>(componentName: string, baseTranslations: Body): ComponentMessages & Body;
130
- export declare function useI18nAsync(componentName: string): Promise<Record<string, string>>;
131
- export declare function useI18nAsync<Body extends Translations>(componentName: string, baseTranslations: Body): Promise<Record<string, string> & Body>;
132
- export declare function t(key: string, params?: Record<string, any>): string;
133
- export declare function t(astro: { currentLocale?: string | undefined; params?: Record<string, any> } | undefined | null, key: string, params?: Record<string, any>): string;
134
- export declare function getLocale(): string;
135
- export declare function getI18nInstance(): ReturnType<typeof import('@nanostores/i18n').createI18n>;
136
- export declare function getFormatterInstance(): ReturnType<typeof import('@nanostores/i18n').formatter>;
137
- export declare function clearCache(locale?: string): void;
138
- }
139
- `
140
- })
141
- },
142
-
143
- "astro:build:done": async ({ logger }) => {
144
- if (translations) {
145
- const seedData: { keys: { key: string; value: string }[] } = { keys: [] }
146
- for (const [locale, localeData] of Object.entries(translations)) {
147
- for (const [component, componentData] of Object.entries(localeData)) {
148
- seedData.keys.push({
149
- key: `locale:${locale}:${component}`,
150
- value: JSON.stringify(componentData)
151
- })
152
- }
153
- }
154
- const outDir = process.env.WRANGLER_OUT_DIR ?? "dist"
155
- const seedFile = path.join(outDir, "_translations-seed.json")
156
- fs.mkdirSync(path.dirname(seedFile), { recursive: true })
157
- fs.writeFileSync(seedFile, JSON.stringify(seedData, null, 2))
158
- logger.info(`translations seed file written to ${seedFile}`)
159
-
160
- const autoSeed = process.env.WRANGLER_SEED_KV === "true" || process.env.CI === "true"
161
- if (autoSeed) {
162
- try {
163
- const { execSync } = await import("node:child_process")
164
- execSync(`npx wrangler kv:bulk put --binding "${kvBinding}" "${seedFile}"`, {
165
- stdio: "pipe",
166
- timeout: 30_000
167
- })
168
- logger.info(`KV "${kvBinding}" seeded with translations`)
169
- } catch {
170
- logger.warn(
171
- `could not seed KV automatically — run ` +
172
- `"npx wrangler kv:bulk put --binding "${kvBinding}" "${seedFile}"" ` +
173
- `to seed translations into KV`
174
- )
175
- }
176
- }
177
- } else {
178
- logger.info("i18n build complete (no translations to seed)")
179
- }
180
- }
181
- }
182
- }
183
-
184
- return [rimelightI18nCore]
185
- }
@@ -1,10 +0,0 @@
1
- import { setKVBinding } from "../index"
2
- import { env } from "cloudflare:workers"
3
-
4
- export const onRequest = async (_context: unknown, next: () => Promise<Response>) => {
5
- const kv = Reflect.get(env, "TRANSLATIONS_KV")
6
- if (kv) {
7
- setKVBinding(kv)
8
- }
9
- return next()
10
- }
@@ -1,78 +0,0 @@
1
- import { locales, defaultLocale } from "virtual:rimelight-i18n-config"
2
- import { currentLocale } from "../runtime"
3
-
4
- const SUPPORTED_LOCALES = new Set(locales)
5
- const DEFAULT_LOCALE = defaultLocale
6
-
7
- function getPreferredLocale(acceptLanguage: string | null): string {
8
- if (!acceptLanguage) return DEFAULT_LOCALE
9
-
10
- const parsed = acceptLanguage
11
- .split(",")
12
- .map((lang) => {
13
- const parts = lang.split(";")
14
- const code = (parts[0] ?? "").trim().toLowerCase()
15
- const base = code.split("-")[0] ?? ""
16
- let q = 1.0
17
- const qualityPart = parts[1]
18
- if (qualityPart) {
19
- const qMatch = qualityPart.match(/q=([0-9.]+)/)
20
- if (qMatch) {
21
- q = parseFloat(qMatch[1] ?? "1")
22
- }
23
- }
24
- return { code, base, q }
25
- })
26
- .toSorted((a, b) => b.q - a.q)
27
-
28
- for (const item of parsed) {
29
- if (SUPPORTED_LOCALES.has(item.code)) {
30
- return item.code
31
- }
32
- const base: string = item.base
33
- if (SUPPORTED_LOCALES.has(base)) {
34
- return base
35
- }
36
- }
37
-
38
- return DEFAULT_LOCALE
39
- }
40
-
41
- export const i18n = async (context: any, next: any) => {
42
- const url = new URL(context.request.url)
43
- const pathname = url.pathname
44
-
45
- // Extract active locale from request context
46
- const paramLocale = context.params?.locale
47
- const firstSegment = pathname.split("/").find(Boolean)
48
- const activeLocale =
49
- (paramLocale && SUPPORTED_LOCALES.has(paramLocale) ? paramLocale : null) ||
50
- (firstSegment && SUPPORTED_LOCALES.has(firstSegment) ? firstSegment : null) ||
51
- context.currentLocale ||
52
- DEFAULT_LOCALE
53
-
54
- currentLocale.set(activeLocale)
55
-
56
- // Only handle GET/HEAD requests
57
- if (context.request.method !== "GET" && context.request.method !== "HEAD") {
58
- return next()
59
- }
60
-
61
- // Skip API routes, internal Astro routes, and files with extensions
62
- if (pathname.startsWith("/api") || pathname.startsWith("/_") || pathname.includes(".")) {
63
- return next()
64
- }
65
-
66
- // Check if pathname starts with a supported locale prefix
67
- if (firstSegment && SUPPORTED_LOCALES.has(firstSegment)) {
68
- return next()
69
- }
70
-
71
- // Redirect to the preferred locale
72
- const acceptLanguage = context.request.headers.get("accept-language")
73
- const locale = getPreferredLocale(acceptLanguage)
74
- currentLocale.set(locale)
75
- const targetPath = `/${locale}${pathname}${url.search}`
76
-
77
- return context.redirect(targetPath, 302)
78
- }
package/src/middleware.ts DELETED
@@ -1,12 +0,0 @@
1
- import { env } from "cloudflare:workers"
2
- import { setKVBinding } from "./index"
3
-
4
- export const onRequest = async (_context: unknown, next: () => Promise<Response>) => {
5
- const kv = Reflect.get(env, "TRANSLATIONS_KV")
6
- if (kv) {
7
- setKVBinding(kv)
8
- }
9
- return next()
10
- }
11
-
12
- export { i18n } from "./middleware/routing"