@rimelight/i18n 0.0.6 → 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/src/runtime.ts DELETED
@@ -1,316 +0,0 @@
1
- import { createI18n, formatter, translationsLoading } from "@nanostores/i18n"
2
- import type {
3
- TranslationLoader,
4
- Translations,
5
- I18n,
6
- Components,
7
- ComponentsJSON
8
- } from "@nanostores/i18n"
9
- import { atom } from "nanostores"
10
-
11
- /**
12
- * A reactive store containing the current locale code. Set by middleware on each request, or
13
- * manually via `currentLocale.set(locale)`.
14
- */
15
- export const currentLocale = atom("")
16
-
17
- type I18nInstance = I18n
18
- type FormatterInstance = ReturnType<typeof formatter>
19
-
20
- let i18nInstance: I18nInstance | undefined
21
- let formatterInstance: FormatterInstance | undefined
22
- let baseLocaleDefault: string = "en"
23
- let rawTranslationsDict: Record<string, Components> = {}
24
-
25
- function throwNotInitialized(): never {
26
- throw new Error("i18n not initialized. Call initializeI18n first.")
27
- }
28
-
29
- /**
30
- * Wraps a TranslationLoader so that the returned object is guaranteed to contain a key for every
31
- * requested component.
32
- *
33
- * The nanostores i18n library uses the keys of the object returned from `get` to clear its internal
34
- * "requested" set. If a requested component is missing from the result (e.g. because the backend
35
- * has no translations for it in the given locale yet), the internal loading atom is never set back
36
- * to `false`, which causes useI18nAsync / translationsLoading to hang forever.
37
- *
38
- * This wrapper normalises the loader output so missing components are filled in with empty
39
- * translation objects, falling back to the base translations defined at the call site.
40
- */
41
- function wrapLoader(loader: TranslationLoader): TranslationLoader {
42
- return async (code, components): Promise<ComponentsJSON> => {
43
- const raw = await loader(code, components)
44
- const normalised: ComponentsJSON = Array.isArray(raw)
45
- ? Object.assign({} as ComponentsJSON, ...raw)
46
- : { ...raw }
47
- for (const component of components) {
48
- if (!(component in normalised)) normalised[component] = {}
49
- }
50
- return normalised
51
- }
52
- }
53
-
54
- export interface InitializeI18nOptions {
55
- /**
56
- * The default locale code (e.g. 'en').
57
- */
58
- defaultLocale: string
59
- /**
60
- * Pre-loaded translations keyed by locale then component.
61
- */
62
- translations: Record<string, Components>
63
- /**
64
- * Optional dynamic loader called when a locale is not in cache.
65
- */
66
- get?: TranslationLoader
67
- }
68
-
69
- /**
70
- * Initializes the i18n system. Must be called once before any other i18n functions are used (the
71
- * integration virtual module does this automatically).
72
- */
73
- export function initializeI18n(options: InitializeI18nOptions): void {
74
- const { defaultLocale, translations, get } = options
75
- baseLocaleDefault = defaultLocale
76
- rawTranslationsDict = translations || {}
77
-
78
- if (!i18nInstance) {
79
- currentLocale.set(defaultLocale)
80
-
81
- // Convert plain JSON dictionaries to nanostores atoms expected by nanostores/i18n cache
82
- const formattedCache: Record<string, Components> = {}
83
- for (const [locale, components] of Object.entries(translations)) {
84
- formattedCache[locale] = {}
85
- for (const [compName, compBody] of Object.entries(components)) {
86
- let bodyObj: Record<string, string> = {}
87
- if (isStringRecord(compBody)) {
88
- bodyObj = compBody
89
- } else if (isStoreWithGet(compBody)) {
90
- const res: unknown = compBody.get()
91
- if (isStringRecord(res)) {
92
- bodyObj = res
93
- }
94
- }
95
- formattedCache[locale][compName] = atom(bodyObj)
96
- }
97
- }
98
-
99
- i18nInstance = createI18n(currentLocale, {
100
- baseLocale: defaultLocale,
101
- get: wrapLoader(get ?? (async (): Promise<ComponentsJSON> => ({}))),
102
- cache: formattedCache,
103
- isSSR: true
104
- })
105
- }
106
- formatterInstance = formatter(currentLocale)
107
- }
108
-
109
- /**
110
- * Returns the underlying nanostores/i18n instance. Throws if not initialized.
111
- */
112
- export function getI18nInstance(): I18nInstance {
113
- if (!i18nInstance) throwNotInitialized()
114
- return i18nInstance
115
- }
116
-
117
- /**
118
- * Returns the formatter instance. Throws if not initialized.
119
- */
120
- export function getFormatterInstance(): FormatterInstance {
121
- if (!formatterInstance) throwNotInitialized()
122
- return formatterInstance
123
- }
124
-
125
- /**
126
- * Returns a Formatter object for the current locale with methods for formatting numbers, dates, and
127
- * relative times using the native Intl API.
128
- */
129
- export function useFormat(): ReturnType<FormatterInstance["get"]> {
130
- return getFormatterInstance().get()
131
- }
132
-
133
- /**
134
- * Helper to safely extract raw translation JSON object from cache regardless of whether it is an
135
- * atom or object. Checks exact locale code (e.g. 'zh-CN'), base language code (e.g. 'zh'), and
136
- * default locale fallback.
137
- */
138
- interface StoreWithGet {
139
- get: () => unknown
140
- }
141
-
142
- function isStoreWithGet(obj: unknown): obj is StoreWithGet {
143
- if (typeof obj !== "object" || obj === null) return false
144
- const getFn = Reflect.get(obj, "get")
145
- return typeof getFn === "function"
146
- }
147
-
148
- function isStringRecord(obj: unknown): obj is Record<string, string> {
149
- return typeof obj === "object" && obj !== null
150
- }
151
-
152
- function getCachedComponent(
153
- locale: string,
154
- componentName: string
155
- ): Record<string, string> | undefined {
156
- const normalizedLocale = locale.toLowerCase()
157
- const baseLanguage = normalizedLocale.split("-")[0] ?? normalizedLocale
158
-
159
- const candidates = [locale, normalizedLocale, baseLanguage, baseLocaleDefault]
160
-
161
- for (const code of candidates) {
162
- const rawComp: unknown = rawTranslationsDict[code]?.[componentName]
163
- if (isStoreWithGet(rawComp)) {
164
- const res: unknown = rawComp.get()
165
- if (isStringRecord(res)) return res
166
- } else if (isStringRecord(rawComp)) {
167
- return rawComp
168
- }
169
- }
170
-
171
- if (!i18nInstance) return undefined
172
- for (const code of candidates) {
173
- const rawComp: unknown = i18nInstance.cache[code]?.[componentName]
174
- if (isStoreWithGet(rawComp)) {
175
- const res: unknown = rawComp.get()
176
- if (isStringRecord(res)) return res
177
- } else if (isStringRecord(rawComp)) {
178
- return rawComp
179
- }
180
- }
181
-
182
- return undefined
183
- }
184
-
185
- /**
186
- * Returns the translated strings for a component in the current locale. Falls back to defaultLocale
187
- * loaded translations or `baseTranslations` if provided.
188
- */
189
- export interface ComponentMessages {
190
- [key: string]: string
191
- }
192
-
193
- export function useI18n(componentName: string): ComponentMessages
194
- export function useI18n<Body extends Translations>(
195
- componentName: string,
196
- baseTranslations: Body
197
- ): ComponentMessages & Body
198
- export function useI18n(componentName: string, baseTranslations?: Translations): ComponentMessages {
199
- const i18n = getI18nInstance()
200
- const activeLocale = currentLocale.get() || baseLocaleDefault
201
- const fallbackDict: Record<string, string> = isStringRecord(baseTranslations)
202
- ? baseTranslations
203
- : {}
204
- const baseDict =
205
- getCachedComponent(activeLocale, componentName) ??
206
- getCachedComponent(baseLocaleDefault, componentName) ??
207
- fallbackDict
208
-
209
- const store = i18n(componentName, baseDict)
210
- const targetObj = store.get()
211
-
212
- return new Proxy(targetObj, {
213
- get(target: Record<string, string>, prop: string | symbol): string {
214
- if (typeof prop === "symbol" || prop in Object.prototype) {
215
- const val: unknown = Reflect.get(target, prop)
216
- return typeof val === "string" ? val : ""
217
- }
218
- const val = target[prop] ?? baseDict[prop]
219
- return val ?? prop
220
- }
221
- })
222
- }
223
-
224
- /**
225
- * Async version of useI18n that waits for translations to finish loading.
226
- */
227
- export async function useI18nAsync(componentName: string): Promise<ComponentMessages>
228
- export async function useI18nAsync<Body extends Translations>(
229
- componentName: string,
230
- baseTranslations: Body
231
- ): Promise<ComponentMessages & Body>
232
- export async function useI18nAsync(
233
- componentName: string,
234
- baseTranslations?: Translations
235
- ): Promise<ComponentMessages> {
236
- const i18n = getI18nInstance()
237
- const activeLocale = currentLocale.get() || baseLocaleDefault
238
- const cachedActive = getCachedComponent(activeLocale, componentName)
239
- const cachedDefault = getCachedComponent(baseLocaleDefault, componentName)
240
- let baseDict: Record<string, string> = cachedActive ?? cachedDefault ?? {}
241
- if (!cachedActive && !cachedDefault && isStringRecord(baseTranslations)) {
242
- baseDict = baseTranslations
243
- }
244
-
245
- const store = i18n(componentName, baseDict)
246
- const unsubscribe = store.listen(() => {})
247
- await translationsLoading(i18n)
248
- unsubscribe()
249
- return store.get()
250
- }
251
-
252
- /**
253
- * Translates a single string key using dot-notation (e.g. `t("page_home.section_hero_title")`).
254
- */
255
- export function t(key: string, params?: Record<string, any>): string
256
- export function t(
257
- astro: { currentLocale?: string | undefined; params?: Record<string, any> } | undefined | null,
258
- key: string,
259
- params?: Record<string, any>
260
- ): string
261
- export function t(arg1: any, arg2?: any, arg3?: any): string {
262
- let key: string
263
- let params: Record<string, any> | undefined
264
-
265
- if (typeof arg1 === "string") {
266
- key = arg1
267
- params = arg2
268
- } else {
269
- const locale = arg1?.currentLocale || arg1?.params?.locale
270
- if (locale) {
271
- currentLocale.set(locale)
272
- }
273
- key = arg2
274
- params = arg3
275
- }
276
-
277
- const dotIndex = key.indexOf(".")
278
- if (dotIndex === -1) {
279
- return key
280
- }
281
- const componentName = key.slice(0, dotIndex)
282
- const keyName = key.slice(dotIndex + 1)
283
-
284
- const activeLocale = currentLocale.get() || baseLocaleDefault
285
- const componentDict =
286
- getCachedComponent(activeLocale, componentName) ??
287
- getCachedComponent(baseLocaleDefault, componentName) ??
288
- {}
289
-
290
- const value = componentDict[keyName] ?? key
291
-
292
- if (typeof value === "function") {
293
- return (value as Function)(params)
294
- }
295
-
296
- if (params && typeof value === "string") {
297
- return value.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? `{${k}}`))
298
- }
299
-
300
- return value
301
- }
302
-
303
- /**
304
- * Clears the translation cache.
305
- *
306
- * @param locale - If provided, clears only that locale's cache. Otherwise clears all.
307
- */
308
- export function clearCache(locale?: string): void {
309
- if (!i18nInstance) throwNotInitialized()
310
- const cache = i18nInstance.cache
311
- if (locale) {
312
- cache[locale] = {}
313
- } else {
314
- for (const key in cache) cache[key] = {}
315
- }
316
- }
package/src/types.ts DELETED
@@ -1,18 +0,0 @@
1
- import type { ComponentsJSON } from "@nanostores/i18n"
2
-
3
- export type { ComponentsJSON }
4
-
5
- export interface FlattenedTranslations {
6
- [key: string]: string
7
- }
8
-
9
- export interface LocaleFile {
10
- [component: string]: {
11
- [key: string]: string
12
- }
13
- }
14
-
15
- export interface KVNamespaceBinding {
16
- get(key: string, type: "json"): Promise<Record<string, string> | null>
17
- put(key: string, value: string): Promise<void>
18
- }
package/src/utils.ts DELETED
@@ -1,44 +0,0 @@
1
- import type { ComponentsJSON, FlattenedTranslations } from "./types"
2
-
3
- export function flatten(obj: ComponentsJSON): FlattenedTranslations {
4
- const result: FlattenedTranslations = {}
5
- for (const [component, translations] of Object.entries(obj)) {
6
- if (translations && typeof translations === "object") {
7
- for (const [key, value] of Object.entries(translations)) {
8
- if (typeof value === "string") {
9
- result[`${component}.${key}`] = value
10
- }
11
- }
12
- }
13
- }
14
- return result
15
- }
16
-
17
- export function unflatten(flat: FlattenedTranslations): ComponentsJSON {
18
- const result: ComponentsJSON = {}
19
- for (const [key, value] of Object.entries(flat)) {
20
- const parts = key.split(".")
21
- if (parts.length < 2) continue
22
- const component: string = parts[0] ?? ""
23
- if (!component) continue
24
- const translationKey = parts.slice(1).join(".")
25
-
26
- if (!(component in result)) {
27
- result[component] = {}
28
- }
29
- result[component]![translationKey] = value
30
- }
31
- return result
32
- }
33
-
34
- export function extractKeys(source: ComponentsJSON): string[] {
35
- const keys: string[] = []
36
- for (const [component, translations] of Object.entries(source)) {
37
- if (translations && typeof translations === "object") {
38
- for (const key of Object.keys(translations)) {
39
- keys.push(`${component}.${key}`)
40
- }
41
- }
42
- }
43
- return keys
44
- }
package/src/virtual.d.ts DELETED
@@ -1,15 +0,0 @@
1
- declare module "@rimelight/i18n:runtime" {
2
- export * from "@rimelight/i18n/runtime"
3
- }
4
-
5
- declare module "virtual:rimelight-i18n-config" {
6
- export const locales: string[]
7
- export const defaultLocale: string
8
- }
9
-
10
- declare module "cloudflare:workers" {
11
- /**
12
- * Ambient Cloudflare Workers env — populated at runtime by the CF runtime.
13
- */
14
- export const env: Record<string, any>
15
- }