@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/dist/index.d.mts +39 -0
- package/dist/index.mjs +63 -0
- package/dist/integration.d.mts +11 -0
- package/dist/integration.mjs +122 -0
- package/dist/middleware.d.mts +7 -0
- package/dist/middleware.mjs +56 -0
- package/dist/runtime.d.mts +71 -0
- package/dist/runtime.mjs +179 -0
- package/dist/types.d.mts +16 -0
- package/dist/types.mjs +1 -0
- package/dist/utils.d.mts +7 -0
- package/dist/utils.mjs +28 -0
- package/package.json +30 -11
- package/src/env.d.ts +0 -13
- package/src/index.ts +0 -143
- package/src/integration.ts +0 -185
- package/src/middleware/kv.ts +0 -10
- package/src/middleware/routing.ts +0 -78
- package/src/middleware.ts +0 -12
- package/src/runtime.ts +0 -316
- package/src/types.ts +0 -18
- package/src/utils.ts +0 -44
- package/src/virtual.d.ts +0 -15
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
|
-
}
|