@fluenti/solid 0.2.1 → 0.3.1
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 +5 -5
- package/dist/context.d.ts +15 -24
- package/dist/context.d.ts.map +1 -1
- package/dist/hooks/__useI18n.d.ts +2 -2
- package/dist/hooks/__useI18n.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +129 -167
- package/dist/index.js.map +1 -1
- package/dist/plural.d.ts +3 -3
- package/dist/plural.d.ts.map +1 -1
- package/dist/provider.d.ts +3 -3
- package/dist/provider.d.ts.map +1 -1
- package/dist/rich-dom.d.ts +0 -6
- package/dist/rich-dom.d.ts.map +1 -1
- package/dist/select.d.ts +3 -3
- package/dist/select.d.ts.map +1 -1
- package/dist/server.cjs +2 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.ts +33 -17
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +54 -0
- package/dist/server.js.map +1 -0
- package/dist/solid-runtime.d.ts.map +1 -1
- package/dist/trans.d.ts +3 -3
- package/dist/trans.d.ts.map +1 -1
- package/dist/types.d.ts +8 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/use-i18n.d.ts +4 -8
- package/dist/use-i18n.d.ts.map +1 -1
- package/dist/vite-plugin.cjs +2 -113
- package/dist/vite-plugin.cjs.map +1 -1
- package/dist/vite-plugin.js +14 -123
- package/dist/vite-plugin.js.map +1 -1
- package/llms-full.txt +186 -0
- package/llms-migration.txt +176 -0
- package/llms.txt +64 -0
- package/package.json +17 -5
- package/src/context.ts +56 -77
- package/src/hooks/__useI18n.ts +2 -2
- package/src/index.ts +6 -6
- package/src/plural.tsx +9 -38
- package/src/provider.tsx +5 -5
- package/src/rich-dom.tsx +25 -47
- package/src/select.tsx +11 -8
- package/src/server.ts +94 -49
- package/src/solid-runtime.ts +15 -134
- package/src/trans.tsx +7 -4
- package/src/types.ts +9 -8
- package/src/use-i18n.ts +5 -16
- package/src/vite-plugin.ts +1 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Migrating to @fluenti/solid from @solid-primitives/i18n
|
|
2
|
+
|
|
3
|
+
> Step-by-step guide to migrate a SolidJS app from @solid-primitives/i18n (or manual i18n) to Fluenti.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Fluenti is a **compile-time** i18n library — translations are compiled to optimized JS at build time. @solid-primitives/i18n provides a runtime dictionary-based approach. Fluenti offers ICU MessageFormat support, PO/JSON catalog management, and zero runtime overhead.
|
|
8
|
+
|
|
9
|
+
Key differences:
|
|
10
|
+
- Compile-time compilation vs runtime dictionary lookup
|
|
11
|
+
- ICU MessageFormat (plurals, selects, date/number formatting) vs flat key-value strings
|
|
12
|
+
- CLI-managed extraction and catalog workflow
|
|
13
|
+
- Vite plugin for build-time transforms
|
|
14
|
+
|
|
15
|
+
## Step-by-step Migration
|
|
16
|
+
|
|
17
|
+
### 1. Install Fluenti
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add @fluenti/core @fluenti/solid
|
|
21
|
+
pnpm add -D @fluenti/cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 2. Replace provider setup
|
|
25
|
+
|
|
26
|
+
Before (@solid-primitives/i18n):
|
|
27
|
+
```tsx
|
|
28
|
+
import { I18nContext, createFluenti } from '@solid-primitives/i18n'
|
|
29
|
+
|
|
30
|
+
const dict = { en: { hello: 'Hello!' }, ja: { hello: 'こんにちは!' } }
|
|
31
|
+
const value = createFluenti(dict, 'en')
|
|
32
|
+
|
|
33
|
+
<I18nContext.Provider value={value}>
|
|
34
|
+
<App />
|
|
35
|
+
</I18nContext.Provider>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
After (Fluenti):
|
|
39
|
+
```tsx
|
|
40
|
+
import { I18nProvider } from '@fluenti/solid'
|
|
41
|
+
import en from './locales/compiled/en'
|
|
42
|
+
import ja from './locales/compiled/ja'
|
|
43
|
+
|
|
44
|
+
<I18nProvider locale="en" fallbackLocale="en" messages={{ en, ja }}>
|
|
45
|
+
<App />
|
|
46
|
+
</I18nProvider>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Update translation usage
|
|
50
|
+
|
|
51
|
+
Before (@solid-primitives/i18n):
|
|
52
|
+
```tsx
|
|
53
|
+
import { useI18n } from '@solid-primitives/i18n'
|
|
54
|
+
const [t, { locale, add }] = useI18n()
|
|
55
|
+
t('hello')
|
|
56
|
+
locale('ja') // switch locale
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
After (Fluenti):
|
|
60
|
+
```tsx
|
|
61
|
+
import { useI18n } from '@fluenti/solid'
|
|
62
|
+
const { i18n, locale, setLocale } = useI18n()
|
|
63
|
+
i18n.t('hello')
|
|
64
|
+
setLocale('ja')
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 4. Add rich formatting
|
|
68
|
+
|
|
69
|
+
@solid-primitives/i18n only supports flat strings. Fluenti adds ICU MessageFormat:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// Pluralization
|
|
73
|
+
<Plural value={count} one="# item" other="# items" />
|
|
74
|
+
|
|
75
|
+
// Gender select
|
|
76
|
+
<Select value={gender} male="He" female="She" other="They" />
|
|
77
|
+
|
|
78
|
+
// Date/number formatting
|
|
79
|
+
<DateTime value={new Date()} style="long" />
|
|
80
|
+
<NumberFormat value={1234.5} style="currency" />
|
|
81
|
+
|
|
82
|
+
// Rich text
|
|
83
|
+
<Trans message="Read the {0}docs{1}">
|
|
84
|
+
{(text) => <a href="/docs">{text}</a>}
|
|
85
|
+
</Trans>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 5. Set up Vite plugin
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// vite.config.ts
|
|
92
|
+
import solid from 'vite-plugin-solid'
|
|
93
|
+
import fluentiSolid from '@fluenti/solid/vite-plugin'
|
|
94
|
+
|
|
95
|
+
export default {
|
|
96
|
+
plugins: [solid(), fluentiSolid()],
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 6. Set up CLI config and catalogs
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
// fluenti.config.ts
|
|
104
|
+
export default {
|
|
105
|
+
sourceLocale: 'en',
|
|
106
|
+
locales: ['en', 'ja'],
|
|
107
|
+
catalogDir: './locales',
|
|
108
|
+
format: 'po',
|
|
109
|
+
include: ['./src/**/*.{tsx,jsx,ts,js}'],
|
|
110
|
+
compileOutDir: './locales/compiled',
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npx fluenti extract # Extract messages from source
|
|
116
|
+
npx fluenti compile # Compile to JS modules
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 7. Remove old library
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pnpm remove @solid-primitives/i18n
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## API Mapping Table
|
|
126
|
+
|
|
127
|
+
| @solid-primitives/i18n | Fluenti |
|
|
128
|
+
|------------------------|---------|
|
|
129
|
+
| `createFluenti(dict, locale)` | `createFluenti(config)` or `<I18nProvider>` |
|
|
130
|
+
| `I18nContext.Provider` | `I18nProvider` |
|
|
131
|
+
| `useI18n()` → `[t, { locale, add }]` | `useI18n()` → `{ i18n, locale, setLocale, isLoading }` |
|
|
132
|
+
| `t('key')` | `i18n.t('key')` |
|
|
133
|
+
| `t('key', { name: 'X' })` | `i18n.t('key', { name: 'X' })` |
|
|
134
|
+
| `locale('ja')` (setter) | `setLocale('ja')` |
|
|
135
|
+
| `locale()` (getter) | `locale` (signal) |
|
|
136
|
+
| `add(locale, dict)` | `loadMessages` callback on provider |
|
|
137
|
+
| N/A | `<Trans>` component |
|
|
138
|
+
| N/A | `<Plural>` component |
|
|
139
|
+
| N/A | `<Select>` component |
|
|
140
|
+
| N/A | `<DateTime>` component |
|
|
141
|
+
| N/A | `<NumberFormat>` component |
|
|
142
|
+
| N/A | `i18n.d(date, style)` — date formatting |
|
|
143
|
+
| N/A | `i18n.n(number, style)` — number formatting |
|
|
144
|
+
| N/A | `msg()` — lazy message descriptor |
|
|
145
|
+
|
|
146
|
+
## Message Format Conversion
|
|
147
|
+
|
|
148
|
+
@solid-primitives/i18n uses flat key-value dictionaries:
|
|
149
|
+
```ts
|
|
150
|
+
// Before: flat dictionary
|
|
151
|
+
const dict = {
|
|
152
|
+
en: {
|
|
153
|
+
hello: 'Hello, {{name}}!',
|
|
154
|
+
items: '{{count}} items',
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Fluenti uses ICU MessageFormat in PO or JSON catalogs:
|
|
160
|
+
```
|
|
161
|
+
# PO format (locales/en.po)
|
|
162
|
+
msgid "hello"
|
|
163
|
+
msgstr "Hello, {name}!"
|
|
164
|
+
|
|
165
|
+
msgid "items"
|
|
166
|
+
msgstr "{count, plural, one {{count} item} other {{count} items}}"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Key Behavioral Differences
|
|
170
|
+
|
|
171
|
+
1. **Compile-time** — messages compiled to JS functions at build time, no runtime parser
|
|
172
|
+
2. **ICU MessageFormat** — standard format for plurals, selects, date/number formatting
|
|
173
|
+
3. **CLI workflow** — extract → translate → compile pipeline
|
|
174
|
+
4. **Signals-based reactivity** — Fluenti uses SolidJS signals internally for locale state
|
|
175
|
+
5. **PO file support** — compatible with professional translation tools (Poedit, Crowdin, Weblate)
|
|
176
|
+
6. **AI translation** — `npx fluenti translate --provider claude` for automated translation
|
package/llms.txt
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @fluenti/solid
|
|
2
|
+
|
|
3
|
+
> SolidJS bindings for Fluenti — compile-time `t`, runtime-capable components, and signal-based locale switching.
|
|
4
|
+
|
|
5
|
+
@fluenti/solid provides Solid-specific runtime APIs for Fluenti. The recommended public path is:
|
|
6
|
+
|
|
7
|
+
- compile-time authoring: `import { t } from '@fluenti/solid'`
|
|
8
|
+
- runtime / imperative access: `useI18n()`
|
|
9
|
+
- runtime-capable components: `Trans`, `Plural`, `Select`, `DateTime`, `NumberFormat`
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @fluenti/core @fluenti/solid @fluenti/vite-plugin
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Main Exports
|
|
18
|
+
|
|
19
|
+
- `createFluentiContext(config)` — context factory (creates a reactive i18n context)
|
|
20
|
+
- `I18nProvider` — provider component
|
|
21
|
+
- `useI18n()` — access the nearest `<I18nProvider>` context
|
|
22
|
+
- `t` — compile-time-only authoring API
|
|
23
|
+
- `Trans`, `Plural`, `Select`, `DateTime`, `NumberFormat`
|
|
24
|
+
- `msg`
|
|
25
|
+
|
|
26
|
+
### Type Exports
|
|
27
|
+
|
|
28
|
+
- `FluentiContext`, `FluentiConfig` — context value and config types
|
|
29
|
+
- `FluentiTransProps`, `FluentiPluralProps`, `FluentiSelectProps` — component prop types
|
|
30
|
+
|
|
31
|
+
## FluentiContext Methods
|
|
32
|
+
|
|
33
|
+
The context returned by `useI18n()` provides:
|
|
34
|
+
|
|
35
|
+
- `locale()`, `setLocale(locale)`, `t(id, values?)`, `d(value, style?)`, `n(value, style?)`
|
|
36
|
+
- `format(message, values?)`, `loadMessages(locale, messages)`, `getLocales()`
|
|
37
|
+
- `te(key, locale?)` — check if a translation key exists
|
|
38
|
+
- `tm(key, locale?)` — get the raw compiled message without interpolation
|
|
39
|
+
- `isLoading()`, `loadedLocales()`, `preloadLocale(locale)`
|
|
40
|
+
|
|
41
|
+
## Config Extensions
|
|
42
|
+
|
|
43
|
+
- `lazyLocaleLoading?: boolean`
|
|
44
|
+
- `chunkLoader?: (locale) => Promise<Messages | { default: Messages }>`
|
|
45
|
+
|
|
46
|
+
## Important Boundaries
|
|
47
|
+
|
|
48
|
+
- imported `t` is compile-time only and requires the Fluenti Vite plugin
|
|
49
|
+
- `useI18n().t()` is the full runtime API
|
|
50
|
+
- `Trans / Plural / Select / DateTime / NumberFormat` remain runtime-capable without the build plugin
|
|
51
|
+
|
|
52
|
+
## Preferred Usage (compile-time first)
|
|
53
|
+
|
|
54
|
+
1. t\`\` tagged template — t\`Hello {name}\` (primary compile-time API)
|
|
55
|
+
2. `<Trans>` / `<Plural>` / `<Select>` — rich text with inline markup
|
|
56
|
+
3. `msg` tagged template — outside components (route meta, stores)
|
|
57
|
+
4. `useI18n().t()` — runtime fallback for dynamic keys only
|
|
58
|
+
|
|
59
|
+
❌ AVOID: `t('some.key')` as the default — this is a legacy i18n pattern.
|
|
60
|
+
|
|
61
|
+
## Docs
|
|
62
|
+
|
|
63
|
+
- Full docs: https://fluenti.dev
|
|
64
|
+
- Source: https://github.com/usefluenti/fluenti
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluenti/solid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SolidJS compile-time i18n — Trans/Plural/Select components, I18nProvider, useI18n",
|
|
6
6
|
"homepage": "https://fluenti.dev",
|
|
@@ -44,6 +44,16 @@
|
|
|
44
44
|
"default": "./dist/index.cjs"
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
|
+
"./server": {
|
|
48
|
+
"import": {
|
|
49
|
+
"types": "./dist/server.d.ts",
|
|
50
|
+
"default": "./dist/server.js"
|
|
51
|
+
},
|
|
52
|
+
"require": {
|
|
53
|
+
"types": "./dist/server.d.ts",
|
|
54
|
+
"default": "./dist/server.cjs"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
47
57
|
"./vite-plugin": {
|
|
48
58
|
"import": {
|
|
49
59
|
"types": "./dist/vite-plugin.d.ts",
|
|
@@ -57,7 +67,8 @@
|
|
|
57
67
|
},
|
|
58
68
|
"files": [
|
|
59
69
|
"dist",
|
|
60
|
-
"src"
|
|
70
|
+
"src",
|
|
71
|
+
"llms*.txt"
|
|
61
72
|
],
|
|
62
73
|
"peerDependencies": {
|
|
63
74
|
"solid-js": "^1.8",
|
|
@@ -69,8 +80,8 @@
|
|
|
69
80
|
}
|
|
70
81
|
},
|
|
71
82
|
"dependencies": {
|
|
72
|
-
"@fluenti/core": "0.
|
|
73
|
-
"@fluenti/vite-plugin": "0.
|
|
83
|
+
"@fluenti/core": "0.3.1",
|
|
84
|
+
"@fluenti/vite-plugin": "0.3.1"
|
|
74
85
|
},
|
|
75
86
|
"devDependencies": {
|
|
76
87
|
"@solidjs/testing-library": "^0.8",
|
|
@@ -87,6 +98,7 @@
|
|
|
87
98
|
"build": "vite build",
|
|
88
99
|
"dev": "vite build --watch",
|
|
89
100
|
"test": "vitest run",
|
|
90
|
-
"typecheck": "tsc --noEmit"
|
|
101
|
+
"typecheck": "tsc --noEmit",
|
|
102
|
+
"bench": "vitest bench"
|
|
91
103
|
}
|
|
92
104
|
}
|
package/src/context.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { createSignal,
|
|
2
|
-
import { formatDate, formatNumber
|
|
3
|
-
import type {
|
|
1
|
+
import { createSignal, type Accessor } from 'solid-js'
|
|
2
|
+
import { createDiagnostics, formatDate, formatNumber } from '@fluenti/core'
|
|
3
|
+
import type { FluentiCoreConfig, Locale, LocalizedString, Messages, CompiledMessage, MessageDescriptor, DateFormatOptions, NumberFormatOptions, DiagnosticsConfig } from '@fluenti/core'
|
|
4
|
+
import { interpolate as coreInterpolate, buildICUMessage, resolveDescriptorId } from '@fluenti/core/internal'
|
|
4
5
|
|
|
5
6
|
/** Chunk loader for lazy locale loading */
|
|
6
7
|
export type ChunkLoader = (
|
|
@@ -12,7 +13,7 @@ interface SplitRuntimeModule {
|
|
|
12
13
|
__preloadLocale?: (locale: string) => Promise<void>
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
const SPLIT_RUNTIME_KEY = Symbol.for('fluenti.runtime.solid')
|
|
16
|
+
const SPLIT_RUNTIME_KEY = Symbol.for('fluenti.runtime.solid.v1')
|
|
16
17
|
|
|
17
18
|
function getSplitRuntimeModule(): SplitRuntimeModule | null {
|
|
18
19
|
const runtime = (globalThis as Record<PropertyKey, unknown>)[SPLIT_RUNTIME_KEY]
|
|
@@ -30,7 +31,7 @@ function resolveChunkMessages(
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
/** Extended config with lazy locale loading support */
|
|
33
|
-
export interface
|
|
34
|
+
export interface FluentiConfig extends FluentiCoreConfig {
|
|
34
35
|
/** Async chunk loader for lazy locale loading */
|
|
35
36
|
chunkLoader?: ChunkLoader
|
|
36
37
|
/** Enable lazy locale loading through chunkLoader */
|
|
@@ -41,34 +42,40 @@ export interface I18nConfig extends FluentConfig {
|
|
|
41
42
|
dateFormats?: DateFormatOptions
|
|
42
43
|
/** Named number format styles */
|
|
43
44
|
numberFormats?: NumberFormatOptions
|
|
45
|
+
/** Runtime diagnostics configuration */
|
|
46
|
+
diagnostics?: DiagnosticsConfig
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
/** Reactive i18n context holding locale signal and translation utilities */
|
|
47
|
-
export interface
|
|
50
|
+
export interface FluentiContext {
|
|
48
51
|
/** Reactive accessor for the current locale */
|
|
49
52
|
locale(): Locale
|
|
50
53
|
/** Set the active locale (async when lazy locale loading is enabled) */
|
|
51
54
|
setLocale(locale: Locale): Promise<void>
|
|
52
55
|
/** Translate a message by id with optional interpolation values */
|
|
53
|
-
t(id: string | MessageDescriptor, values?: Record<string, unknown>):
|
|
56
|
+
t(id: string | MessageDescriptor, values?: Record<string, unknown>): LocalizedString
|
|
54
57
|
/** Tagged template form: t`Hello ${name}` */
|
|
55
|
-
t(strings: TemplateStringsArray, ...exprs: unknown[]):
|
|
58
|
+
t(strings: TemplateStringsArray, ...exprs: unknown[]): LocalizedString
|
|
56
59
|
/** Merge additional messages into a locale catalog at runtime */
|
|
57
60
|
loadMessages(locale: Locale, messages: Messages): void
|
|
58
61
|
/** Return all locale codes that have loaded messages */
|
|
59
62
|
getLocales(): Locale[]
|
|
60
63
|
/** Format a date value for the current locale */
|
|
61
|
-
d(value: Date | number, style?: string):
|
|
64
|
+
d(value: Date | number, style?: string): LocalizedString
|
|
62
65
|
/** Format a number value for the current locale */
|
|
63
|
-
n(value: number, style?: string):
|
|
66
|
+
n(value: number, style?: string): LocalizedString
|
|
64
67
|
/** Format an ICU message string directly (no catalog lookup) */
|
|
65
|
-
format(message: string, values?: Record<string, unknown>):
|
|
68
|
+
format(message: string, values?: Record<string, unknown>): LocalizedString
|
|
66
69
|
/** Whether a locale chunk is currently being loaded */
|
|
67
70
|
isLoading: Accessor<boolean>
|
|
68
71
|
/** Set of locales whose messages have been loaded */
|
|
69
72
|
loadedLocales: Accessor<Set<string>>
|
|
70
73
|
/** Preload a locale in the background without switching to it */
|
|
71
74
|
preloadLocale(locale: string): void
|
|
75
|
+
/** Check if a translation key exists for the given or current locale */
|
|
76
|
+
te(key: string, loc?: string): boolean
|
|
77
|
+
/** Get the raw compiled message for a key without interpolation */
|
|
78
|
+
tm(key: string, loc?: string): CompiledMessage | undefined
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
/**
|
|
@@ -77,22 +84,23 @@ export interface I18nContext {
|
|
|
77
84
|
* The returned `t()` reads the internal `locale()` signal, so any
|
|
78
85
|
* Solid computation that calls `t()` will re-run when the locale changes.
|
|
79
86
|
*/
|
|
80
|
-
export function
|
|
87
|
+
export function createFluentiContext(config: FluentiCoreConfig | FluentiConfig): FluentiContext {
|
|
81
88
|
const [locale, setLocaleSignal] = createSignal<Locale>(config.locale)
|
|
82
89
|
const [isLoading, setIsLoading] = createSignal(false)
|
|
83
90
|
const loadedLocalesSet = new Set<string>([config.locale])
|
|
84
91
|
const [loadedLocales, setLoadedLocales] = createSignal(new Set(loadedLocalesSet))
|
|
85
92
|
const messages: Record<string, Messages> = { ...config.messages }
|
|
86
|
-
const i18nConfig = config as
|
|
93
|
+
const i18nConfig = config as FluentiConfig
|
|
94
|
+
const diagnostics = i18nConfig.diagnostics ? createDiagnostics(i18nConfig.diagnostics) : undefined
|
|
87
95
|
const lazyLocaleLoading = i18nConfig.lazyLocaleLoading
|
|
88
|
-
?? (config as
|
|
96
|
+
?? (config as FluentiConfig & { splitting?: boolean }).splitting
|
|
89
97
|
?? false
|
|
90
98
|
|
|
91
99
|
function lookupCatalog(
|
|
92
100
|
id: string,
|
|
93
101
|
loc: Locale,
|
|
94
102
|
values?: Record<string, unknown>,
|
|
95
|
-
):
|
|
103
|
+
): LocalizedString | undefined {
|
|
96
104
|
const catalog = messages[loc]
|
|
97
105
|
if (!catalog) {
|
|
98
106
|
return undefined
|
|
@@ -104,21 +112,21 @@ export function createI18nContext(config: FluentConfig | I18nConfig): I18nContex
|
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
if (typeof msg === 'function') {
|
|
107
|
-
return msg(values)
|
|
115
|
+
return msg(values) as LocalizedString
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
if (typeof msg === 'string' && values) {
|
|
111
|
-
return coreInterpolate(msg, values, loc)
|
|
119
|
+
return coreInterpolate(msg, values, loc) as LocalizedString
|
|
112
120
|
}
|
|
113
121
|
|
|
114
|
-
return String(msg)
|
|
122
|
+
return String(msg) as LocalizedString
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
function lookupWithFallbacks(
|
|
118
126
|
id: string,
|
|
119
127
|
loc: Locale,
|
|
120
128
|
values?: Record<string, unknown>,
|
|
121
|
-
):
|
|
129
|
+
): LocalizedString | undefined {
|
|
122
130
|
const localesToTry: Locale[] = [loc]
|
|
123
131
|
const seen = new Set(localesToTry)
|
|
124
132
|
|
|
@@ -140,6 +148,9 @@ export function createI18nContext(config: FluentConfig | I18nConfig): I18nContex
|
|
|
140
148
|
for (const targetLocale of localesToTry) {
|
|
141
149
|
const result = lookupCatalog(id, targetLocale, values)
|
|
142
150
|
if (result !== undefined) {
|
|
151
|
+
if (targetLocale !== loc) {
|
|
152
|
+
diagnostics?.fallbackUsed(loc, targetLocale, id)
|
|
153
|
+
}
|
|
143
154
|
return result
|
|
144
155
|
}
|
|
145
156
|
}
|
|
@@ -150,14 +161,14 @@ export function createI18nContext(config: FluentConfig | I18nConfig): I18nContex
|
|
|
150
161
|
function resolveMissing(
|
|
151
162
|
id: string,
|
|
152
163
|
loc: Locale,
|
|
153
|
-
):
|
|
164
|
+
): LocalizedString | undefined {
|
|
154
165
|
if (!config.missing) {
|
|
155
166
|
return undefined
|
|
156
167
|
}
|
|
157
168
|
|
|
158
169
|
const result = config.missing(loc, id)
|
|
159
170
|
if (result !== undefined) {
|
|
160
|
-
return result
|
|
171
|
+
return result as LocalizedString
|
|
161
172
|
}
|
|
162
173
|
return undefined
|
|
163
174
|
}
|
|
@@ -166,27 +177,29 @@ export function createI18nContext(config: FluentConfig | I18nConfig): I18nContex
|
|
|
166
177
|
id: string,
|
|
167
178
|
loc: Locale,
|
|
168
179
|
values?: Record<string, unknown>,
|
|
169
|
-
):
|
|
180
|
+
): LocalizedString {
|
|
170
181
|
const catalogResult = lookupWithFallbacks(id, loc, values)
|
|
171
182
|
if (catalogResult !== undefined) {
|
|
172
183
|
return catalogResult
|
|
173
184
|
}
|
|
174
185
|
|
|
186
|
+
diagnostics?.missingKey(loc, id)
|
|
187
|
+
|
|
175
188
|
const missingResult = resolveMissing(id, loc)
|
|
176
189
|
if (missingResult !== undefined) {
|
|
177
190
|
return missingResult
|
|
178
191
|
}
|
|
179
192
|
|
|
180
193
|
if (id.includes('{')) {
|
|
181
|
-
return coreInterpolate(id, values, loc)
|
|
194
|
+
return coreInterpolate(id, values, loc) as LocalizedString
|
|
182
195
|
}
|
|
183
196
|
|
|
184
|
-
return id
|
|
197
|
+
return id as LocalizedString
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
function t(strings: TemplateStringsArray, ...exprs: unknown[]):
|
|
188
|
-
function t(id: string | MessageDescriptor, values?: Record<string, unknown>):
|
|
189
|
-
function t(idOrStrings: string | MessageDescriptor | TemplateStringsArray, ...rest: unknown[]):
|
|
200
|
+
function t(strings: TemplateStringsArray, ...exprs: unknown[]): LocalizedString
|
|
201
|
+
function t(id: string | MessageDescriptor, values?: Record<string, unknown>): LocalizedString
|
|
202
|
+
function t(idOrStrings: string | MessageDescriptor | TemplateStringsArray, ...rest: unknown[]): LocalizedString {
|
|
190
203
|
// Tagged template form: t`Hello ${name}`
|
|
191
204
|
if (Array.isArray(idOrStrings) && 'raw' in idOrStrings) {
|
|
192
205
|
const strings = idOrStrings as TemplateStringsArray
|
|
@@ -213,10 +226,10 @@ export function createI18nContext(config: FluentConfig | I18nConfig): I18nContex
|
|
|
213
226
|
}
|
|
214
227
|
|
|
215
228
|
if (id.message !== undefined) {
|
|
216
|
-
return coreInterpolate(id.message, values, currentLocale)
|
|
229
|
+
return coreInterpolate(id.message, values, currentLocale) as LocalizedString
|
|
217
230
|
}
|
|
218
231
|
|
|
219
|
-
return messageId ?? ''
|
|
232
|
+
return (messageId ?? '') as LocalizedString
|
|
220
233
|
}
|
|
221
234
|
|
|
222
235
|
return resolveMessage(id, currentLocale, values)
|
|
@@ -280,59 +293,25 @@ export function createI18nContext(config: FluentConfig | I18nConfig): I18nContex
|
|
|
280
293
|
|
|
281
294
|
const getLocales = (): Locale[] => Object.keys(messages)
|
|
282
295
|
|
|
283
|
-
const d = (value: Date | number, style?: string):
|
|
284
|
-
formatDate(value, locale(), style, i18nConfig.dateFormats)
|
|
296
|
+
const d = (value: Date | number, style?: string): LocalizedString =>
|
|
297
|
+
formatDate(value, locale(), style, i18nConfig.dateFormats) as LocalizedString
|
|
285
298
|
|
|
286
|
-
const n = (value: number, style?: string):
|
|
287
|
-
formatNumber(value, locale(), style, i18nConfig.numberFormats)
|
|
299
|
+
const n = (value: number, style?: string): LocalizedString =>
|
|
300
|
+
formatNumber(value, locale(), style, i18nConfig.numberFormats) as LocalizedString
|
|
288
301
|
|
|
289
|
-
const format = (message: string, values?: Record<string, unknown>):
|
|
290
|
-
return coreInterpolate(message, values, locale())
|
|
302
|
+
const format = (message: string, values?: Record<string, unknown>): LocalizedString => {
|
|
303
|
+
return coreInterpolate(message, values, locale()) as LocalizedString
|
|
291
304
|
}
|
|
292
305
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
// ─── Module-level singleton ─────────────────────────────────────────────────
|
|
297
|
-
|
|
298
|
-
let globalCtx: I18nContext | undefined
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Initialize the global i18n singleton.
|
|
302
|
-
*
|
|
303
|
-
* Call once at app startup (e.g. in your entry file) before any `useI18n()`.
|
|
304
|
-
* Signals are created inside a `createRoot` so they outlive any component scope.
|
|
305
|
-
*
|
|
306
|
-
* Returns the context for convenience, but `useI18n()` will also find it.
|
|
307
|
-
*/
|
|
308
|
-
export function createI18n(config: FluentConfig | I18nConfig): I18nContext {
|
|
309
|
-
const ctx = createRoot(() => createI18nContext(config))
|
|
310
|
-
|
|
311
|
-
// Only set global singleton in browser (client-side).
|
|
312
|
-
// In SSR, each request should use <I18nProvider> for per-request isolation.
|
|
313
|
-
if (typeof window !== 'undefined') {
|
|
314
|
-
globalCtx = ctx
|
|
315
|
-
} else {
|
|
316
|
-
console.warn(
|
|
317
|
-
'[fluenti] createI18n() detected SSR environment. ' +
|
|
318
|
-
'Use <I18nProvider> for per-request isolation in SSR.',
|
|
319
|
-
)
|
|
306
|
+
const te = (key: string, loc?: string): boolean => {
|
|
307
|
+
const msgs = messages[loc ?? locale()]
|
|
308
|
+
return msgs !== undefined && key in msgs
|
|
320
309
|
}
|
|
321
310
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
export function getGlobalI18nContext(): I18nContext | undefined {
|
|
327
|
-
return globalCtx
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/** @internal — used by I18nProvider to set context without createRoot wrapper */
|
|
331
|
-
export function setGlobalI18nContext(ctx: I18nContext): void {
|
|
332
|
-
globalCtx = ctx
|
|
333
|
-
}
|
|
311
|
+
const tm = (key: string, loc?: string): CompiledMessage | undefined => {
|
|
312
|
+
const msgs = messages[loc ?? locale()]
|
|
313
|
+
return msgs ? msgs[key] : undefined
|
|
314
|
+
}
|
|
334
315
|
|
|
335
|
-
|
|
336
|
-
export function resetGlobalI18nContext(): void {
|
|
337
|
-
globalCtx = undefined
|
|
316
|
+
return { locale, setLocale, t, loadMessages, getLocales, d, n, format, isLoading, loadedLocales, preloadLocale, te, tm }
|
|
338
317
|
}
|
package/src/hooks/__useI18n.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useI18n } from '../use-i18n'
|
|
2
|
-
import type {
|
|
2
|
+
import type { FluentiContext } from '../types'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Internal hook used by the Vite plugin's compiled output.
|
|
@@ -10,6 +10,6 @@ import type { I18nContext } from '../types'
|
|
|
10
10
|
*
|
|
11
11
|
* @internal
|
|
12
12
|
*/
|
|
13
|
-
export function __useI18n():
|
|
13
|
+
export function __useI18n(): FluentiContext {
|
|
14
14
|
return useI18n()
|
|
15
15
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type {
|
|
3
|
-
export { I18nProvider
|
|
1
|
+
export { createFluentiContext } from './context'
|
|
2
|
+
export type { FluentiContext, FluentiConfig } from './context'
|
|
3
|
+
export { I18nProvider } from './provider'
|
|
4
4
|
export { useI18n } from './use-i18n'
|
|
5
5
|
export { t } from './compile-time-t'
|
|
6
6
|
export { Trans } from './trans'
|
|
7
|
-
export type {
|
|
7
|
+
export type { FluentiTransProps } from './trans'
|
|
8
8
|
export { Plural } from './plural'
|
|
9
|
-
export type {
|
|
9
|
+
export type { FluentiPluralProps } from './plural'
|
|
10
10
|
export { SelectComp as Select } from './select'
|
|
11
|
-
export type {
|
|
11
|
+
export type { FluentiSelectProps } from './select'
|
|
12
12
|
export { msg } from './msg'
|
|
13
13
|
export { DateTime } from './components/DateTime'
|
|
14
14
|
export { NumberFormat } from './components/NumberFormat'
|