@fluenti/solid 0.3.4 → 0.4.0-rc.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/dist/components-entry.cjs +2 -0
- package/dist/components-entry.cjs.map +1 -0
- package/dist/components-entry.d.ts +12 -0
- package/dist/components-entry.d.ts.map +1 -0
- package/dist/components-entry.js +283 -0
- package/dist/components-entry.js.map +1 -0
- package/dist/context.d.ts +23 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -414
- package/dist/index.js.map +1 -1
- package/dist/plural.d.ts +9 -0
- package/dist/plural.d.ts.map +1 -1
- package/dist/provider.d.ts +19 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/rich-dom.d.ts.map +1 -1
- package/dist/select.d.ts +15 -0
- package/dist/select.d.ts.map +1 -1
- package/dist/solid-runtime.d.ts.map +1 -1
- package/dist/trans.d.ts.map +1 -1
- package/dist/use-i18n-DPb98Dw1.js +201 -0
- package/dist/use-i18n-DPb98Dw1.js.map +1 -0
- package/dist/use-i18n-oAO3vYS7.cjs +2 -0
- package/dist/use-i18n-oAO3vYS7.cjs.map +1 -0
- package/dist/vite-plugin.cjs +1 -1
- package/dist/vite-plugin.cjs.map +1 -1
- package/dist/vite-plugin.js +0 -1
- package/dist/vite-plugin.js.map +1 -1
- package/llms.txt +44 -0
- package/package.json +18 -3
- package/src/components-entry.ts +13 -0
- package/src/context.ts +110 -148
- package/src/index.ts +5 -6
- package/src/plural.tsx +9 -0
- package/src/provider.tsx +19 -1
- package/src/rich-dom.tsx +10 -0
- package/src/select.tsx +15 -0
- package/src/solid-runtime.ts +0 -1
- package/src/trans.tsx +8 -2
package/llms.txt
CHANGED
|
@@ -58,6 +58,50 @@ The context returned by `useI18n()` provides:
|
|
|
58
58
|
|
|
59
59
|
❌ AVOID: `t('some.key')` as the default — this is a legacy i18n pattern.
|
|
60
60
|
|
|
61
|
+
## Code Examples — SolidJS
|
|
62
|
+
|
|
63
|
+
### Tagged template (PREFERRED)
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { useI18n } from '@fluenti/solid'
|
|
67
|
+
|
|
68
|
+
function MyComponent() {
|
|
69
|
+
const { t } = useI18n()
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div>
|
|
73
|
+
<h1>{t\`Welcome to our app\`}</h1>
|
|
74
|
+
<p>{t\`Hello, ${name()}!\`}</p>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Rich text with \<Trans\>
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { Trans } from '@fluenti/solid/components'
|
|
84
|
+
|
|
85
|
+
function Terms() {
|
|
86
|
+
return (
|
|
87
|
+
<Trans>
|
|
88
|
+
Read our <a href="/terms">terms of service</a>
|
|
89
|
+
</Trans>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### What NOT to write
|
|
95
|
+
|
|
96
|
+
❌ Do not use t() with manual key strings:
|
|
97
|
+
```tsx
|
|
98
|
+
// WRONG
|
|
99
|
+
const msg = t('welcome_message')
|
|
100
|
+
|
|
101
|
+
// CORRECT
|
|
102
|
+
const msg = t\`Welcome to our app\`
|
|
103
|
+
```
|
|
104
|
+
|
|
61
105
|
## Docs
|
|
62
106
|
|
|
63
107
|
- Full docs: https://fluenti.dev
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluenti/solid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-rc.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SolidJS compile-time i18n — Trans/Plural/Select components, I18nProvider, useI18n",
|
|
6
6
|
"homepage": "https://fluenti.dev",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
|
+
"sideEffects": false,
|
|
19
20
|
"keywords": [
|
|
20
21
|
"i18n",
|
|
21
22
|
"internationalization",
|
|
@@ -54,6 +55,20 @@
|
|
|
54
55
|
"default": "./dist/server.cjs"
|
|
55
56
|
}
|
|
56
57
|
},
|
|
58
|
+
"./components": {
|
|
59
|
+
"solid": {
|
|
60
|
+
"types": "./dist/components-entry.d.ts",
|
|
61
|
+
"default": "./src/components-entry.ts"
|
|
62
|
+
},
|
|
63
|
+
"import": {
|
|
64
|
+
"types": "./dist/components-entry.d.ts",
|
|
65
|
+
"default": "./dist/components-entry.js"
|
|
66
|
+
},
|
|
67
|
+
"require": {
|
|
68
|
+
"types": "./dist/components-entry.d.ts",
|
|
69
|
+
"default": "./dist/components-entry.cjs"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
57
72
|
"./vite-plugin": {
|
|
58
73
|
"import": {
|
|
59
74
|
"types": "./dist/vite-plugin.d.ts",
|
|
@@ -80,8 +95,8 @@
|
|
|
80
95
|
}
|
|
81
96
|
},
|
|
82
97
|
"dependencies": {
|
|
83
|
-
"@fluenti/core": "0.
|
|
84
|
-
"@fluenti/vite-plugin": "0.
|
|
98
|
+
"@fluenti/core": "0.4.0-rc.1",
|
|
99
|
+
"@fluenti/vite-plugin": "0.4.0-rc.1"
|
|
85
100
|
},
|
|
86
101
|
"devDependencies": {
|
|
87
102
|
"@solidjs/testing-library": "^0.8",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { Trans } from './trans'
|
|
2
|
+
export type { FluentiTransProps } from './trans'
|
|
3
|
+
export { Plural } from './plural'
|
|
4
|
+
export type { FluentiPluralProps } from './plural'
|
|
5
|
+
export { SelectComp as Select } from './select'
|
|
6
|
+
export type { FluentiSelectProps } from './select'
|
|
7
|
+
export { DateTime } from './components/DateTime'
|
|
8
|
+
export type { DateTimeProps, FluentiDateTimeProps } from './components/DateTime'
|
|
9
|
+
export { NumberFormat } from './components/NumberFormat'
|
|
10
|
+
export type { NumberProps, FluentiNumberFormatProps } from './components/NumberFormat'
|
|
11
|
+
|
|
12
|
+
// Re-export interpolate for apps that use <Plural>/<Select> at runtime
|
|
13
|
+
export { interpolate } from '@fluenti/core/internal'
|
package/src/context.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createSignal, type Accessor } from 'solid-js'
|
|
2
|
-
import {
|
|
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'
|
|
2
|
+
import { createFluentiCore } from '@fluenti/core'
|
|
3
|
+
import type { FluentiCoreConfig, FluentiCoreConfigFull, Locale, LocalizedString, Messages, CompiledMessage, MessageDescriptor, DateFormatOptions, NumberFormatOptions, DiagnosticsConfig, CustomFormatter } from '@fluenti/core'
|
|
5
4
|
|
|
6
5
|
/** Chunk loader for lazy locale loading */
|
|
7
6
|
export type ChunkLoader = (
|
|
@@ -30,6 +29,37 @@ function resolveChunkMessages(
|
|
|
30
29
|
: loaded
|
|
31
30
|
}
|
|
32
31
|
|
|
32
|
+
/** @internal Map locale → default currency code */
|
|
33
|
+
const LOCALE_CURRENCY_MAP: Record<string, string> = {
|
|
34
|
+
'en': 'USD', 'en-US': 'USD', 'en-GB': 'GBP', 'en-AU': 'AUD', 'en-CA': 'CAD',
|
|
35
|
+
'zh-CN': 'CNY', 'zh-TW': 'TWD', 'zh-HK': 'HKD',
|
|
36
|
+
'ja': 'JPY', 'ja-JP': 'JPY',
|
|
37
|
+
'ko': 'KRW', 'ko-KR': 'KRW',
|
|
38
|
+
'de': 'EUR', 'de-DE': 'EUR', 'de-AT': 'EUR',
|
|
39
|
+
'fr': 'EUR', 'fr-FR': 'EUR', 'fr-CA': 'CAD',
|
|
40
|
+
'es': 'EUR', 'es-ES': 'EUR', 'es-MX': 'MXN',
|
|
41
|
+
'pt': 'EUR', 'pt-BR': 'BRL', 'pt-PT': 'EUR',
|
|
42
|
+
'it': 'EUR', 'ru': 'RUB', 'ar': 'SAR', 'hi': 'INR',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @internal Built-in date format styles (merged under user-provided dateFormats) */
|
|
46
|
+
const DEFAULT_DATE_FORMATS: Record<string, Intl.DateTimeFormatOptions> = {
|
|
47
|
+
short: { year: 'numeric', month: 'numeric', day: 'numeric' },
|
|
48
|
+
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' },
|
|
49
|
+
time: { hour: 'numeric', minute: 'numeric' },
|
|
50
|
+
datetime: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' },
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @internal Built-in number format styles (merged under user-provided numberFormats) */
|
|
54
|
+
const DEFAULT_NUMBER_FORMATS: Record<string, Intl.NumberFormatOptions | ((locale: Locale) => Intl.NumberFormatOptions)> = {
|
|
55
|
+
currency: (locale: string) => ({
|
|
56
|
+
style: 'currency',
|
|
57
|
+
currency: LOCALE_CURRENCY_MAP[locale] ?? LOCALE_CURRENCY_MAP[locale.split('-')[0]!] ?? 'USD',
|
|
58
|
+
}),
|
|
59
|
+
percent: { style: 'percent' },
|
|
60
|
+
decimal: { minimumFractionDigits: 2, maximumFractionDigits: 2 },
|
|
61
|
+
}
|
|
62
|
+
|
|
33
63
|
/** Extended config with lazy locale loading support */
|
|
34
64
|
export interface FluentiConfig extends FluentiCoreConfig {
|
|
35
65
|
/** Async chunk loader for lazy locale loading */
|
|
@@ -44,6 +74,19 @@ export interface FluentiConfig extends FluentiCoreConfig {
|
|
|
44
74
|
numberFormats?: NumberFormatOptions
|
|
45
75
|
/** Runtime diagnostics configuration */
|
|
46
76
|
diagnostics?: DiagnosticsConfig
|
|
77
|
+
/**
|
|
78
|
+
* Custom message interpolation function.
|
|
79
|
+
*
|
|
80
|
+
* By default, the runtime uses a lightweight `{key}` replacer.
|
|
81
|
+
* Pass the full `interpolate` from `@fluenti/core/internal` for
|
|
82
|
+
* runtime ICU MessageFormat support (plurals, selects, nested arguments).
|
|
83
|
+
*/
|
|
84
|
+
interpolate?: (
|
|
85
|
+
message: string,
|
|
86
|
+
values: Record<string, unknown> | undefined,
|
|
87
|
+
locale: string,
|
|
88
|
+
formatters?: Record<string, CustomFormatter>,
|
|
89
|
+
) => string
|
|
47
90
|
}
|
|
48
91
|
|
|
49
92
|
/** Reactive i18n context holding locale signal and translation utilities */
|
|
@@ -83,6 +126,20 @@ export interface FluentiContext {
|
|
|
83
126
|
*
|
|
84
127
|
* The returned `t()` reads the internal `locale()` signal, so any
|
|
85
128
|
* Solid computation that calls `t()` will re-run when the locale changes.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```tsx
|
|
132
|
+
* import { createFluentiContext } from '@fluenti/solid'
|
|
133
|
+
* import messages from './locales/compiled/en.js'
|
|
134
|
+
*
|
|
135
|
+
* const ctx = createFluentiContext({
|
|
136
|
+
* locale: 'en',
|
|
137
|
+
* messages: { en: messages },
|
|
138
|
+
* })
|
|
139
|
+
*
|
|
140
|
+
* // Use t`` tagged template (preferred)
|
|
141
|
+
* const greeting = ctx.t`Hello, {name}!`
|
|
142
|
+
* ```
|
|
86
143
|
*/
|
|
87
144
|
export function createFluentiContext(config: FluentiCoreConfig | FluentiConfig): FluentiContext {
|
|
88
145
|
const [locale, setLocaleSignal] = createSignal<Locale>(config.locale)
|
|
@@ -91,155 +148,42 @@ export function createFluentiContext(config: FluentiCoreConfig | FluentiConfig):
|
|
|
91
148
|
const [loadedLocales, setLoadedLocales] = createSignal(new Set(loadedLocalesSet))
|
|
92
149
|
const messages: Record<string, Messages> = { ...config.messages }
|
|
93
150
|
const i18nConfig = config as FluentiConfig
|
|
94
|
-
const diagnostics = i18nConfig.diagnostics ? createDiagnostics(i18nConfig.diagnostics) : undefined
|
|
95
151
|
const lazyLocaleLoading = i18nConfig.lazyLocaleLoading
|
|
96
152
|
?? (config as FluentiConfig & { splitting?: boolean }).splitting
|
|
97
153
|
?? false
|
|
98
154
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const msg = catalog[id]
|
|
110
|
-
if (msg === undefined) {
|
|
111
|
-
return undefined
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (typeof msg === 'function') {
|
|
115
|
-
return msg(values) as LocalizedString
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (typeof msg === 'string' && values) {
|
|
119
|
-
return coreInterpolate(msg, values, loc) as LocalizedString
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return String(msg) as LocalizedString
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function lookupWithFallbacks(
|
|
126
|
-
id: string,
|
|
127
|
-
loc: Locale,
|
|
128
|
-
values?: Record<string, unknown>,
|
|
129
|
-
): LocalizedString | undefined {
|
|
130
|
-
const localesToTry: Locale[] = [loc]
|
|
131
|
-
const seen = new Set(localesToTry)
|
|
132
|
-
|
|
133
|
-
if (config.fallbackLocale && !seen.has(config.fallbackLocale)) {
|
|
134
|
-
localesToTry.push(config.fallbackLocale)
|
|
135
|
-
seen.add(config.fallbackLocale)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const chainLocales = i18nConfig.fallbackChain?.[loc] ?? i18nConfig.fallbackChain?.['*']
|
|
139
|
-
if (chainLocales) {
|
|
140
|
-
for (const chainLocale of chainLocales) {
|
|
141
|
-
if (!seen.has(chainLocale)) {
|
|
142
|
-
localesToTry.push(chainLocale)
|
|
143
|
-
seen.add(chainLocale)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
for (const targetLocale of localesToTry) {
|
|
149
|
-
const result = lookupCatalog(id, targetLocale, values)
|
|
150
|
-
if (result !== undefined) {
|
|
151
|
-
if (targetLocale !== loc) {
|
|
152
|
-
diagnostics?.fallbackUsed(loc, targetLocale, id)
|
|
153
|
-
}
|
|
154
|
-
return result
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return undefined
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function resolveMissing(
|
|
162
|
-
id: string,
|
|
163
|
-
loc: Locale,
|
|
164
|
-
): LocalizedString | undefined {
|
|
165
|
-
if (!config.missing) {
|
|
166
|
-
return undefined
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
const result = config.missing(loc, id)
|
|
171
|
-
if (result !== undefined) {
|
|
172
|
-
return result as LocalizedString
|
|
173
|
-
}
|
|
174
|
-
} catch {
|
|
175
|
-
// Missing handler threw — fall through to next resolution path
|
|
176
|
-
}
|
|
177
|
-
return undefined
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function resolveMessage(
|
|
181
|
-
id: string,
|
|
182
|
-
loc: Locale,
|
|
183
|
-
values?: Record<string, unknown>,
|
|
184
|
-
): LocalizedString {
|
|
185
|
-
const catalogResult = lookupWithFallbacks(id, loc, values)
|
|
186
|
-
if (catalogResult !== undefined) {
|
|
187
|
-
return catalogResult
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
diagnostics?.missingKey(loc, id)
|
|
191
|
-
|
|
192
|
-
const missingResult = resolveMissing(id, loc)
|
|
193
|
-
if (missingResult !== undefined) {
|
|
194
|
-
return missingResult
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (id.includes('{')) {
|
|
198
|
-
return coreInterpolate(id, values, loc) as LocalizedString
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return id as LocalizedString
|
|
155
|
+
// Create a core instance that handles all translation, lookup, fallback, and formatting logic.
|
|
156
|
+
// Merge built-in date/number format styles under user-provided overrides.
|
|
157
|
+
// Build config incrementally to satisfy exactOptionalPropertyTypes —
|
|
158
|
+
// optional properties must not receive `undefined` as a value.
|
|
159
|
+
const coreConfig: FluentiCoreConfigFull = {
|
|
160
|
+
locale: config.locale,
|
|
161
|
+
messages: config.messages ?? {},
|
|
162
|
+
dateFormats: { ...DEFAULT_DATE_FORMATS, ...i18nConfig.dateFormats },
|
|
163
|
+
numberFormats: { ...DEFAULT_NUMBER_FORMATS, ...i18nConfig.numberFormats },
|
|
202
164
|
}
|
|
165
|
+
if (config.fallbackLocale !== undefined) coreConfig.fallbackLocale = config.fallbackLocale
|
|
166
|
+
if (i18nConfig.fallbackChain !== undefined) coreConfig.fallbackChain = i18nConfig.fallbackChain
|
|
167
|
+
if (config.missing !== undefined) coreConfig.missing = config.missing
|
|
168
|
+
if (i18nConfig.diagnostics !== undefined) coreConfig.diagnostics = i18nConfig.diagnostics as FluentiCoreConfigFull['diagnostics']
|
|
169
|
+
if (i18nConfig.interpolate !== undefined) coreConfig.interpolate = i18nConfig.interpolate
|
|
170
|
+
const i18n = createFluentiCore(coreConfig)
|
|
203
171
|
|
|
204
172
|
function t(strings: TemplateStringsArray, ...exprs: unknown[]): LocalizedString
|
|
205
173
|
function t(id: string | MessageDescriptor, values?: Record<string, unknown>): LocalizedString
|
|
206
174
|
function t(idOrStrings: string | MessageDescriptor | TemplateStringsArray, ...rest: unknown[]): LocalizedString {
|
|
207
|
-
//
|
|
175
|
+
const current = locale() // READ SIGNAL → reactive dependency for Solid re-renders
|
|
176
|
+
if (i18n.locale !== current) i18n.locale = current
|
|
177
|
+
// Dispatch to the correct overload based on input type
|
|
208
178
|
if (Array.isArray(idOrStrings) && 'raw' in idOrStrings) {
|
|
209
|
-
|
|
210
|
-
const icu = buildICUMessage(strings, rest)
|
|
211
|
-
const values = Object.fromEntries(rest.map((v, i) => [`arg${i}`, v]))
|
|
212
|
-
return resolveMessage(icu, locale(), values)
|
|
179
|
+
return i18n.t(idOrStrings as TemplateStringsArray, ...rest) as LocalizedString
|
|
213
180
|
}
|
|
214
|
-
|
|
215
|
-
const id = idOrStrings as string | MessageDescriptor
|
|
216
|
-
const values = rest[0] as Record<string, unknown> | undefined
|
|
217
|
-
const currentLocale = locale() // reactive dependency
|
|
218
|
-
if (typeof id === 'object' && id !== null) {
|
|
219
|
-
const messageId = resolveDescriptorId(id)
|
|
220
|
-
if (messageId) {
|
|
221
|
-
const catalogResult = lookupWithFallbacks(messageId, currentLocale, values)
|
|
222
|
-
if (catalogResult !== undefined) {
|
|
223
|
-
return catalogResult
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const missingResult = resolveMissing(messageId, currentLocale)
|
|
227
|
-
if (missingResult !== undefined) {
|
|
228
|
-
return missingResult
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (id.message !== undefined) {
|
|
233
|
-
return coreInterpolate(id.message, values, currentLocale) as LocalizedString
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return (messageId ?? '') as LocalizedString
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return resolveMessage(id, currentLocale, values)
|
|
181
|
+
return i18n.t(idOrStrings as string | MessageDescriptor, rest[0] as Record<string, unknown> | undefined) as LocalizedString
|
|
240
182
|
}
|
|
241
183
|
|
|
242
184
|
const loadMessages = (loc: Locale, msgs: Messages): void => {
|
|
185
|
+
i18n.loadMessages(loc, msgs)
|
|
186
|
+
// Keep local messages in sync for te/tm which check the local object
|
|
243
187
|
// Intentional mutation: messages record is locally scoped to this context closure
|
|
244
188
|
messages[loc] = { ...messages[loc], ...msgs }
|
|
245
189
|
loadedLocalesSet.add(loc)
|
|
@@ -257,8 +201,12 @@ export function createFluentiContext(config: FluentiCoreConfig | FluentiConfig):
|
|
|
257
201
|
const splitRuntime = getSplitRuntimeModule()
|
|
258
202
|
|
|
259
203
|
if (loadedLocalesSet.has(newLocale)) {
|
|
260
|
-
|
|
261
|
-
|
|
204
|
+
try {
|
|
205
|
+
if (splitRuntime?.__switchLocale) {
|
|
206
|
+
await splitRuntime.__switchLocale(newLocale)
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
console.warn(`[fluenti] split runtime switch failed for locale "${newLocale}"`, e)
|
|
262
210
|
}
|
|
263
211
|
setLocaleSignal(newLocale)
|
|
264
212
|
return
|
|
@@ -270,14 +218,19 @@ export function createFluentiContext(config: FluentiCoreConfig | FluentiConfig):
|
|
|
270
218
|
try {
|
|
271
219
|
const loaded = resolveChunkMessages(await i18nConfig.chunkLoader(newLocale))
|
|
272
220
|
// Always store loaded messages — they may be needed if locale is switched back
|
|
221
|
+
i18n.loadMessages(newLocale, loaded)
|
|
273
222
|
// Intentional mutation: messages record is locally scoped to this context closure
|
|
274
223
|
messages[newLocale] = { ...messages[newLocale], ...loaded }
|
|
275
224
|
loadedLocalesSet.add(newLocale)
|
|
276
225
|
setLoadedLocales(new Set(loadedLocalesSet))
|
|
277
226
|
// Stale request — a newer setLocale call superseded this one; don't switch locale
|
|
278
227
|
if (thisRequest !== _localeRequestId) return
|
|
279
|
-
|
|
280
|
-
|
|
228
|
+
try {
|
|
229
|
+
if (splitRuntime?.__switchLocale) {
|
|
230
|
+
await splitRuntime.__switchLocale(newLocale)
|
|
231
|
+
}
|
|
232
|
+
} catch (e) {
|
|
233
|
+
console.warn(`[fluenti] split runtime switch failed for locale "${newLocale}"`, e)
|
|
281
234
|
}
|
|
282
235
|
// Re-check after async __switchLocale — a newer setLocale() may have superseded this one
|
|
283
236
|
if (thisRequest !== _localeRequestId) return
|
|
@@ -297,6 +250,7 @@ export function createFluentiContext(config: FluentiCoreConfig | FluentiConfig):
|
|
|
297
250
|
const splitRuntime = getSplitRuntimeModule()
|
|
298
251
|
i18nConfig.chunkLoader(loc).then(async (loaded) => {
|
|
299
252
|
const resolved = resolveChunkMessages(loaded)
|
|
253
|
+
i18n.loadMessages(loc, resolved)
|
|
300
254
|
// Intentional mutation: messages record is locally scoped to this context closure
|
|
301
255
|
messages[loc] = { ...messages[loc], ...resolved }
|
|
302
256
|
loadedLocalesSet.add(loc)
|
|
@@ -311,16 +265,24 @@ export function createFluentiContext(config: FluentiCoreConfig | FluentiConfig):
|
|
|
311
265
|
})
|
|
312
266
|
}
|
|
313
267
|
|
|
314
|
-
const getLocales = (): Locale[] =>
|
|
268
|
+
const getLocales = (): Locale[] => i18n.getLocales()
|
|
315
269
|
|
|
316
|
-
const d = (value: Date | number, style?: string): LocalizedString =>
|
|
317
|
-
|
|
270
|
+
const d = (value: Date | number, style?: string): LocalizedString => {
|
|
271
|
+
const current = locale() // READ SIGNAL → reactive dependency
|
|
272
|
+
if (i18n.locale !== current) i18n.locale = current
|
|
273
|
+
return i18n.d(value, style) as LocalizedString
|
|
274
|
+
}
|
|
318
275
|
|
|
319
|
-
const n = (value: number, style?: string): LocalizedString =>
|
|
320
|
-
|
|
276
|
+
const n = (value: number, style?: string): LocalizedString => {
|
|
277
|
+
const current = locale() // READ SIGNAL → reactive dependency
|
|
278
|
+
if (i18n.locale !== current) i18n.locale = current
|
|
279
|
+
return i18n.n(value, style) as LocalizedString
|
|
280
|
+
}
|
|
321
281
|
|
|
322
282
|
const format = (message: string, values?: Record<string, unknown>): LocalizedString => {
|
|
323
|
-
|
|
283
|
+
const current = locale() // READ SIGNAL → reactive dependency
|
|
284
|
+
if (i18n.locale !== current) i18n.locale = current
|
|
285
|
+
return i18n.format(message, values) as LocalizedString
|
|
324
286
|
}
|
|
325
287
|
|
|
326
288
|
const te = (key: string, loc?: string): boolean => {
|
package/src/index.ts
CHANGED
|
@@ -3,14 +3,13 @@ export type { FluentiContext, FluentiConfig } from './context'
|
|
|
3
3
|
export { I18nProvider } from './provider'
|
|
4
4
|
export { useI18n } from './use-i18n'
|
|
5
5
|
export { t } from './compile-time-t'
|
|
6
|
-
export {
|
|
6
|
+
export { msg } from './msg'
|
|
7
|
+
|
|
8
|
+
// Components moved to @fluenti/solid/components:
|
|
9
|
+
// import { Trans, Plural, Select, DateTime, NumberFormat } from '@fluenti/solid/components'
|
|
7
10
|
export type { FluentiTransProps } from './trans'
|
|
8
|
-
export { Plural } from './plural'
|
|
9
11
|
export type { FluentiPluralProps } from './plural'
|
|
10
|
-
export { SelectComp as Select } from './select'
|
|
11
12
|
export type { FluentiSelectProps } from './select'
|
|
12
|
-
export { msg } from './msg'
|
|
13
|
-
export { DateTime } from './components/DateTime'
|
|
14
13
|
export type { DateTimeProps, FluentiDateTimeProps } from './components/DateTime'
|
|
15
|
-
export { NumberFormat } from './components/NumberFormat'
|
|
16
14
|
export type { NumberProps, FluentiNumberFormatProps } from './components/NumberFormat'
|
|
15
|
+
|
package/src/plural.tsx
CHANGED
|
@@ -56,6 +56,15 @@ export interface FluentiPluralProps {
|
|
|
56
56
|
* ```tsx
|
|
57
57
|
* <Plural value={count()} zero="No items" one="# item" other="# items" />
|
|
58
58
|
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* import { Plural } from '@fluenti/solid'
|
|
63
|
+
*
|
|
64
|
+
* function ItemCount(props: { count: number }) {
|
|
65
|
+
* return <Plural value={props.count} one="# item" other="# items" />
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
59
68
|
*/
|
|
60
69
|
export const Plural: Component<FluentiPluralProps> = (props) => {
|
|
61
70
|
const { t } = useI18n()
|
package/src/provider.tsx
CHANGED
|
@@ -7,8 +7,26 @@ import type { FluentiConfig, FluentiContext } from './context'
|
|
|
7
7
|
export const I18nCtx = createContext<FluentiContext>()
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Provides the Fluenti i18n context to the Solid component tree.
|
|
11
11
|
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* import { I18nProvider, useI18n } from '@fluenti/solid'
|
|
15
|
+
* import messages from './locales/compiled/en.js'
|
|
16
|
+
*
|
|
17
|
+
* function App() {
|
|
18
|
+
* return (
|
|
19
|
+
* <I18nProvider locale="en" messages={{ en: messages }}>
|
|
20
|
+
* <Content />
|
|
21
|
+
* </I18nProvider>
|
|
22
|
+
* )
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* function Content() {
|
|
26
|
+
* const { t } = useI18n()
|
|
27
|
+
* return <h1>{t`Welcome to our app`}</h1>
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
12
30
|
*/
|
|
13
31
|
export const I18nProvider: ParentComponent<FluentiConfig> = (props) => {
|
|
14
32
|
const ctx = createFluentiContext(props)
|
package/src/rich-dom.tsx
CHANGED
|
@@ -90,6 +90,16 @@ export function reconstruct(
|
|
|
90
90
|
|
|
91
91
|
const idx = Number(match[1])
|
|
92
92
|
const isSelfClosing = match[2] === undefined
|
|
93
|
+
|
|
94
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= components.length) {
|
|
95
|
+
if (!isSelfClosing) {
|
|
96
|
+
result.push(match[3] ?? '')
|
|
97
|
+
}
|
|
98
|
+
lastIndex = combinedRe.lastIndex
|
|
99
|
+
match = combinedRe.exec(translated)
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
93
103
|
const template = components[idx]
|
|
94
104
|
|
|
95
105
|
if (isSelfClosing) {
|
package/src/select.tsx
CHANGED
|
@@ -50,6 +50,21 @@ export interface FluentiSelectProps {
|
|
|
50
50
|
* ```
|
|
51
51
|
*
|
|
52
52
|
* Falls back to the `other` prop when no key matches.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* import { Select } from '@fluenti/solid'
|
|
57
|
+
*
|
|
58
|
+
* function Greeting(props: { gender: string }) {
|
|
59
|
+
* return (
|
|
60
|
+
* <Select value={props.gender}
|
|
61
|
+
* male="He liked your post"
|
|
62
|
+
* female="She liked your post"
|
|
63
|
+
* other="They liked your post"
|
|
64
|
+
* />
|
|
65
|
+
* )
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
53
68
|
*/
|
|
54
69
|
export const SelectComp: Component<FluentiSelectProps> = (props) => {
|
|
55
70
|
const { t } = useI18n()
|
package/src/solid-runtime.ts
CHANGED
|
@@ -7,7 +7,6 @@ export const solidRuntimeGenerator: RuntimeGenerator = createRuntimeGenerator({
|
|
|
7
7
|
localeInit: (defaultLocale) => `const [__currentLocale, __setCurrentLocale] = createSignal('${defaultLocale}')`,
|
|
8
8
|
loadingInit: 'const [__loading, __setLoading] = createSignal(false)',
|
|
9
9
|
catalogUpdate: (msgs) => `__setCatalog(reconcile(${msgs}))`,
|
|
10
|
-
catalogMerge: (msgs) => `__setCatalog(reconcile({ ...__catalog, ...${msgs} }))`,
|
|
11
10
|
localeUpdate: (locale) => `__setCurrentLocale(${locale})`,
|
|
12
11
|
loadingUpdate: (value) => `__setLoading(${value})`,
|
|
13
12
|
localeRead: '__currentLocale()',
|
package/src/trans.tsx
CHANGED
|
@@ -46,6 +46,8 @@ interface TagToken {
|
|
|
46
46
|
|
|
47
47
|
type Token = TextToken | TagToken
|
|
48
48
|
|
|
49
|
+
const MAX_TOKEN_DEPTH = 100
|
|
50
|
+
|
|
49
51
|
/**
|
|
50
52
|
* Parse a message string containing XML-like tags into a token tree.
|
|
51
53
|
*
|
|
@@ -54,7 +56,11 @@ type Token = TextToken | TagToken
|
|
|
54
56
|
* - Self-closing tags: `<br/>`
|
|
55
57
|
* - Nested tags: `<bold>hello <italic>world</italic></bold>`
|
|
56
58
|
*/
|
|
57
|
-
function parseTokens(input: string): readonly Token[] {
|
|
59
|
+
function parseTokens(input: string, depth: number = 0): readonly Token[] {
|
|
60
|
+
if (depth > MAX_TOKEN_DEPTH) {
|
|
61
|
+
// Bail out as plain text to prevent stack overflow
|
|
62
|
+
return [{ type: 'text', value: input }]
|
|
63
|
+
}
|
|
58
64
|
const tokens: Token[] = []
|
|
59
65
|
let pos = 0
|
|
60
66
|
|
|
@@ -106,7 +112,7 @@ function parseTokens(input: string): readonly Token[] {
|
|
|
106
112
|
tokens.push({
|
|
107
113
|
type: 'tag',
|
|
108
114
|
name: tagName,
|
|
109
|
-
children: parseTokens(innerContent),
|
|
115
|
+
children: parseTokens(innerContent, depth + 1),
|
|
110
116
|
})
|
|
111
117
|
pos = innerEnd + closingTag.length
|
|
112
118
|
}
|