@rimelight/i18n 0.0.4 → 0.0.5
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/README.md +19 -0
- package/package.json +30 -6
- package/src/env.d.ts +13 -1
- package/src/index.ts +37 -14
- package/src/integration.ts +28 -19
- package/src/middleware/routing.ts +13 -2
- package/src/runtime.ts +176 -9
- package/src/virtual.d.ts +0 -1
- package/LICENSE +0 -21
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Rimelight Entertainment Workspace
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
|
|
5
|
+
### Apps (`/packages`)
|
|
6
|
+
|
|
7
|
+
- **`rimelight.com`**: The main company website.
|
|
8
|
+
- **`starter.rimelight.com`**: Our standardized starter template for Astro websites.
|
|
9
|
+
|
|
10
|
+
### Packages (`/packages`)
|
|
11
|
+
|
|
12
|
+
- **`@rimelight/auth`**: Authentication and authorization utilities for the Rimelight ecosystem.
|
|
13
|
+
- **`@rimelight/cli`**: The command line interface for managing Rimelight projects.
|
|
14
|
+
- **`@rimelight/cms`**: Enterprise content management, block rendering, and wiki engine.
|
|
15
|
+
- **`@rimelight/docs`**: Documentation components and utilities.
|
|
16
|
+
- **`@rimelight/i18n`**: Internationalization and localization tools.
|
|
17
|
+
- **`@rimelight/security`**: Astro security integration (CSP, SRI, and more).
|
|
18
|
+
- **`@rimelight/seo`**: SEO utilities including sitemap, robots, and meta components.
|
|
19
|
+
- **`@rimelight/ui`**: Our component library used in all our web projects.
|
package/package.json
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimelight/i18n",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "Rimelight
|
|
5
|
+
"description": "Rimelight Entertainment's Internationalization Package",
|
|
6
|
+
"homepage": "https://rimelight.com/docs",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/Rimelight-Entertainment/rimelight/issues"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"author": {
|
|
12
|
+
"name": "Rimelight Entertainment"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/Rimelight-Entertainment/rimelight.git"
|
|
17
|
+
},
|
|
6
18
|
"files": [
|
|
7
19
|
"src"
|
|
8
20
|
],
|
|
@@ -15,19 +27,31 @@
|
|
|
15
27
|
"./types": "./src/types.ts",
|
|
16
28
|
"./utils": "./src/utils.ts"
|
|
17
29
|
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
18
33
|
"scripts": {
|
|
19
|
-
"check": "
|
|
34
|
+
"check": "pnpm audit --audit-level=moderate && vp check --fix"
|
|
20
35
|
},
|
|
21
36
|
"dependencies": {
|
|
22
37
|
"@nanostores/i18n": "1.3.3",
|
|
23
|
-
"nanostores": "1.
|
|
38
|
+
"nanostores": "1.5.1"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@rimelight/config": "workspace:*",
|
|
42
|
+
"astro": "7.2.2",
|
|
43
|
+
"typescript": "6.0.3"
|
|
24
44
|
},
|
|
25
45
|
"peerDependencies": {
|
|
26
|
-
"astro": ">=
|
|
46
|
+
"astro": ">=7.0.0"
|
|
27
47
|
},
|
|
28
48
|
"peerDependenciesMeta": {
|
|
29
49
|
"astro": {
|
|
30
50
|
"optional": true
|
|
31
51
|
}
|
|
32
|
-
}
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=26.7.0"
|
|
55
|
+
},
|
|
56
|
+
"packageManager": "pnpm@11.22.0"
|
|
33
57
|
}
|
package/src/env.d.ts
CHANGED
|
@@ -1 +1,13 @@
|
|
|
1
|
-
|
|
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
CHANGED
|
@@ -3,6 +3,7 @@ import type { ComponentsJSON, KVNamespaceBinding } from "./types"
|
|
|
3
3
|
import {
|
|
4
4
|
useI18n as baseUseI18n,
|
|
5
5
|
useI18nAsync as baseUseI18nAsync,
|
|
6
|
+
t,
|
|
6
7
|
currentLocale,
|
|
7
8
|
useFormat,
|
|
8
9
|
clearCache,
|
|
@@ -11,55 +12,77 @@ import {
|
|
|
11
12
|
} from "@rimelight/i18n:runtime"
|
|
12
13
|
import type { Translations } from "@nanostores/i18n"
|
|
13
14
|
|
|
14
|
-
export { currentLocale, useFormat, clearCache, getI18nInstance, getFormatterInstance }
|
|
15
|
+
export { t, currentLocale, useFormat, clearCache, getI18nInstance, getFormatterInstance }
|
|
15
16
|
|
|
17
|
+
export interface ComponentMessages {
|
|
18
|
+
[key: string]: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useI18n(componentName: string): ComponentMessages
|
|
16
22
|
export function useI18n<Body extends Translations>(
|
|
17
23
|
componentName: string,
|
|
18
24
|
baseTranslations: Body
|
|
19
|
-
): Body
|
|
25
|
+
): ComponentMessages & Body
|
|
26
|
+
export function useI18n(
|
|
27
|
+
astro: { currentLocale?: string | undefined; params?: Record<string, any> } | undefined | null,
|
|
28
|
+
componentName: string
|
|
29
|
+
): ComponentMessages
|
|
20
30
|
export function useI18n<Body extends Translations>(
|
|
21
|
-
astro: { currentLocale?: string | undefined } | undefined | null,
|
|
31
|
+
astro: { currentLocale?: string | undefined; params?: Record<string, any> } | undefined | null,
|
|
22
32
|
componentName: string,
|
|
23
33
|
baseTranslations: Body
|
|
24
|
-
): Body
|
|
25
|
-
export function useI18n(arg1: any, arg2
|
|
34
|
+
): ComponentMessages & Body
|
|
35
|
+
export function useI18n(arg1: any, arg2?: any, arg3?: any): any {
|
|
26
36
|
if (typeof arg1 === "string") {
|
|
27
37
|
return baseUseI18n(arg1, arg2)
|
|
28
38
|
} else {
|
|
29
|
-
const
|
|
30
|
-
|
|
39
|
+
const locale = arg1?.currentLocale || arg1?.params?.locale
|
|
40
|
+
if (locale) {
|
|
41
|
+
currentLocale.set(locale)
|
|
42
|
+
}
|
|
31
43
|
return baseUseI18n(arg2, arg3)
|
|
32
44
|
}
|
|
33
45
|
}
|
|
34
46
|
|
|
47
|
+
export function useI18nAsync(componentName: string): Promise<Record<string, string>>
|
|
35
48
|
export function useI18nAsync<Body extends Translations>(
|
|
36
49
|
componentName: string,
|
|
37
50
|
baseTranslations: Body
|
|
38
|
-
): Promise<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>>
|
|
39
56
|
export function useI18nAsync<Body extends Translations>(
|
|
40
|
-
astro: { currentLocale?: string | undefined } | undefined | null,
|
|
57
|
+
astro: { currentLocale?: string | undefined; params?: Record<string, any> } | undefined | null,
|
|
41
58
|
componentName: string,
|
|
42
59
|
baseTranslations: Body
|
|
43
|
-
): Promise<Body>
|
|
44
|
-
export function useI18nAsync(arg1: any, arg2
|
|
60
|
+
): Promise<Record<string, string> & Body>
|
|
61
|
+
export function useI18nAsync(arg1: any, arg2?: any, arg3?: any): any {
|
|
45
62
|
if (typeof arg1 === "string") {
|
|
46
63
|
return baseUseI18nAsync(arg1, arg2)
|
|
47
64
|
} else {
|
|
48
|
-
const
|
|
49
|
-
|
|
65
|
+
const locale = arg1?.currentLocale || arg1?.params?.locale
|
|
66
|
+
if (locale) {
|
|
67
|
+
currentLocale.set(locale)
|
|
68
|
+
}
|
|
50
69
|
return baseUseI18nAsync(arg2, arg3)
|
|
51
70
|
}
|
|
52
71
|
}
|
|
53
72
|
|
|
54
73
|
import { getRelativeLocaleUrl as astroGetRelativeLocaleUrl } from "astro:i18n"
|
|
55
74
|
|
|
75
|
+
export function getLocale(): string {
|
|
76
|
+
return currentLocale.get() || "en"
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
export function getRelativeLocaleUrl(path: string): string
|
|
57
80
|
export function getRelativeLocaleUrl(locale: string, path: string): string
|
|
58
81
|
export function getRelativeLocaleUrl(arg1: string, arg2?: string): string {
|
|
59
82
|
if (arg2 !== undefined) {
|
|
60
83
|
return astroGetRelativeLocaleUrl(arg1, arg2)
|
|
61
84
|
} else {
|
|
62
|
-
const locale =
|
|
85
|
+
const locale = getLocale()
|
|
63
86
|
return astroGetRelativeLocaleUrl(locale, arg1)
|
|
64
87
|
}
|
|
65
88
|
}
|
package/src/integration.ts
CHANGED
|
@@ -15,11 +15,11 @@ export function rimelightI18n(options?: RimelightI18nOptions): AstroIntegration[
|
|
|
15
15
|
const kvBinding = options?.kvBinding ?? "TRANSLATIONS_KV"
|
|
16
16
|
const translationLoader = options?.translationLoader
|
|
17
17
|
|
|
18
|
-
// Normalise
|
|
18
|
+
// Normalise translations passed in options
|
|
19
19
|
const normalisedTranslations: Record<string, any> = {}
|
|
20
20
|
if (translations) {
|
|
21
21
|
for (const [locale, val] of Object.entries(translations)) {
|
|
22
|
-
normalisedTranslations[locale
|
|
22
|
+
normalisedTranslations[locale] = val
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -49,7 +49,7 @@ export function rimelightI18n(options?: RimelightI18nOptions): AstroIntegration[
|
|
|
49
49
|
const defaultLocale = config.i18n?.defaultLocale ?? "en"
|
|
50
50
|
|
|
51
51
|
const virtualModuleContent = `\
|
|
52
|
-
import { initializeI18n, useFormat, useI18n, useI18nAsync,
|
|
52
|
+
import { initializeI18n, useFormat, useI18n, useI18nAsync, t,
|
|
53
53
|
currentLocale, getI18nInstance, getFormatterInstance, clearCache }
|
|
54
54
|
from ${JSON.stringify(runtimeSpecifier)};
|
|
55
55
|
${loaderImport}
|
|
@@ -59,7 +59,7 @@ initializeI18n({
|
|
|
59
59
|
translations: ${JSON.stringify(normalisedTranslations)}${loaderOption}
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
export { useFormat, useI18n, useI18nAsync, currentLocale,
|
|
62
|
+
export { useFormat, useI18n, useI18nAsync, t, currentLocale,
|
|
63
63
|
getI18nInstance, getFormatterInstance, clearCache };
|
|
64
64
|
`
|
|
65
65
|
|
|
@@ -124,8 +124,14 @@ declare module "@rimelight/i18n:runtime" {
|
|
|
124
124
|
export const currentLocale: import('nanostores').PreinitializedWritableAtom<string> & object;
|
|
125
125
|
export declare function initializeI18n(options: import('@rimelight/i18n/runtime').InitializeI18nOptions): void;
|
|
126
126
|
export declare function useFormat(): import('@nanostores/i18n').Formatter;
|
|
127
|
-
export
|
|
128
|
-
export declare function
|
|
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;
|
|
129
135
|
export declare function getI18nInstance(): ReturnType<typeof import('@nanostores/i18n').createI18n>;
|
|
130
136
|
export declare function getFormatterInstance(): ReturnType<typeof import('@nanostores/i18n').formatter>;
|
|
131
137
|
export declare function clearCache(locale?: string): void;
|
|
@@ -151,19 +157,22 @@ declare module "@rimelight/i18n:runtime" {
|
|
|
151
157
|
fs.writeFileSync(seedFile, JSON.stringify(seedData, null, 2))
|
|
152
158
|
logger.info(`translations seed file written to ${seedFile}`)
|
|
153
159
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
`
|
|
166
|
-
|
|
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
|
+
}
|
|
167
176
|
}
|
|
168
177
|
} else {
|
|
169
178
|
logger.info("i18n build complete (no translations to seed)")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { locales, defaultLocale } from "virtual:rimelight-i18n-config"
|
|
2
|
+
import { currentLocale } from "../runtime"
|
|
2
3
|
|
|
3
4
|
const SUPPORTED_LOCALES = new Set(locales)
|
|
4
5
|
const DEFAULT_LOCALE = defaultLocale
|
|
@@ -41,6 +42,17 @@ export const i18n = async (context: any, next: any) => {
|
|
|
41
42
|
const url = new URL(context.request.url)
|
|
42
43
|
const pathname = url.pathname
|
|
43
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
|
+
|
|
44
56
|
// Only handle GET/HEAD requests
|
|
45
57
|
if (context.request.method !== "GET" && context.request.method !== "HEAD") {
|
|
46
58
|
return next()
|
|
@@ -52,8 +64,6 @@ export const i18n = async (context: any, next: any) => {
|
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
// Check if pathname starts with a supported locale prefix
|
|
55
|
-
const firstSegment = pathname.split("/").find(Boolean)
|
|
56
|
-
|
|
57
67
|
if (firstSegment && SUPPORTED_LOCALES.has(firstSegment)) {
|
|
58
68
|
return next()
|
|
59
69
|
}
|
|
@@ -61,6 +71,7 @@ export const i18n = async (context: any, next: any) => {
|
|
|
61
71
|
// Redirect to the preferred locale
|
|
62
72
|
const acceptLanguage = context.request.headers.get("accept-language")
|
|
63
73
|
const locale = getPreferredLocale(acceptLanguage)
|
|
74
|
+
currentLocale.set(locale)
|
|
64
75
|
const targetPath = `/${locale}${pathname}${url.search}`
|
|
65
76
|
|
|
66
77
|
return context.redirect(targetPath, 302)
|
package/src/runtime.ts
CHANGED
|
@@ -19,6 +19,8 @@ type FormatterInstance = ReturnType<typeof formatter>
|
|
|
19
19
|
|
|
20
20
|
let i18nInstance: I18nInstance | undefined
|
|
21
21
|
let formatterInstance: FormatterInstance | undefined
|
|
22
|
+
let baseLocaleDefault: string = "en"
|
|
23
|
+
let rawTranslationsDict: Record<string, Components> = {}
|
|
22
24
|
|
|
23
25
|
function throwNotInitialized(): never {
|
|
24
26
|
throw new Error("i18n not initialized. Call initializeI18n first.")
|
|
@@ -70,12 +72,34 @@ export interface InitializeI18nOptions {
|
|
|
70
72
|
*/
|
|
71
73
|
export function initializeI18n(options: InitializeI18nOptions): void {
|
|
72
74
|
const { defaultLocale, translations, get } = options
|
|
75
|
+
baseLocaleDefault = defaultLocale
|
|
76
|
+
rawTranslationsDict = translations || {}
|
|
77
|
+
|
|
73
78
|
if (!i18nInstance) {
|
|
74
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
|
+
|
|
75
99
|
i18nInstance = createI18n(currentLocale, {
|
|
76
100
|
baseLocale: defaultLocale,
|
|
77
101
|
get: wrapLoader(get ?? (async (): Promise<ComponentsJSON> => ({}))),
|
|
78
|
-
cache:
|
|
102
|
+
cache: formattedCache,
|
|
79
103
|
isSSR: true
|
|
80
104
|
})
|
|
81
105
|
}
|
|
@@ -107,32 +131,175 @@ export function useFormat(): ReturnType<FormatterInstance["get"]> {
|
|
|
107
131
|
}
|
|
108
132
|
|
|
109
133
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
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.
|
|
112
188
|
*/
|
|
189
|
+
export interface ComponentMessages {
|
|
190
|
+
[key: string]: string
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function useI18n(componentName: string): ComponentMessages
|
|
113
194
|
export function useI18n<Body extends Translations>(
|
|
114
195
|
componentName: string,
|
|
115
196
|
baseTranslations: Body
|
|
116
|
-
): Body
|
|
117
|
-
|
|
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
|
+
})
|
|
118
222
|
}
|
|
119
223
|
|
|
120
224
|
/**
|
|
121
|
-
* Async version of useI18n that waits for translations to finish loading.
|
|
122
|
-
* dynamic translationLoader (e.g. KV-backed loading).
|
|
225
|
+
* Async version of useI18n that waits for translations to finish loading.
|
|
123
226
|
*/
|
|
227
|
+
export async function useI18nAsync(componentName: string): Promise<ComponentMessages>
|
|
124
228
|
export async function useI18nAsync<Body extends Translations>(
|
|
125
229
|
componentName: string,
|
|
126
230
|
baseTranslations: Body
|
|
127
|
-
): Promise<Body>
|
|
231
|
+
): Promise<ComponentMessages & Body>
|
|
232
|
+
export async function useI18nAsync(
|
|
233
|
+
componentName: string,
|
|
234
|
+
baseTranslations?: Translations
|
|
235
|
+
): Promise<ComponentMessages> {
|
|
128
236
|
const i18n = getI18nInstance()
|
|
129
|
-
const
|
|
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)
|
|
130
246
|
const unsubscribe = store.listen(() => {})
|
|
131
247
|
await translationsLoading(i18n)
|
|
132
248
|
unsubscribe()
|
|
133
249
|
return store.get()
|
|
134
250
|
}
|
|
135
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
|
+
|
|
136
303
|
/**
|
|
137
304
|
* Clears the translation cache.
|
|
138
305
|
*
|
package/src/virtual.d.ts
CHANGED
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
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.
|