@fluenti/solid 0.3.3 → 0.4.0-rc.0
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 +2 -2
- package/dist/components/DateTime.d.ts +3 -3
- package/dist/components/DateTime.d.ts.map +1 -1
- package/dist/components/NumberFormat.d.ts +3 -3
- package/dist/components/NumberFormat.d.ts.map +1 -1
- 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 -404
- 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/trans.d.ts.map +1 -1
- package/dist/use-i18n-Bb-Ivibx.js +190 -0
- package/dist/use-i18n-Bb-Ivibx.js.map +1 -0
- package/dist/use-i18n-CooxLxjG.cjs +2 -0
- package/dist/use-i18n-CooxLxjG.cjs.map +1 -0
- package/llms.txt +44 -0
- package/package.json +18 -3
- package/src/components/DateTime.tsx +4 -4
- package/src/components/NumberFormat.tsx +4 -4
- package/src/components-entry.ts +13 -0
- package/src/context.ts +116 -142
- 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/trans.tsx +8 -2
|
@@ -3,8 +3,8 @@ import { useI18n } from '../use-i18n'
|
|
|
3
3
|
export interface DateTimeProps {
|
|
4
4
|
/** Date value to format */
|
|
5
5
|
value: Date | number
|
|
6
|
-
/** Named format
|
|
7
|
-
|
|
6
|
+
/** Named format key defined in dateFormats config */
|
|
7
|
+
format?: string
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/** @alias DateTimeProps */
|
|
@@ -15,10 +15,10 @@ export type FluentiDateTimeProps = DateTimeProps
|
|
|
15
15
|
*
|
|
16
16
|
* @example
|
|
17
17
|
* ```tsx
|
|
18
|
-
* <DateTime value={new Date()}
|
|
18
|
+
* <DateTime value={new Date()} format="long" />
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
export function DateTime(props: DateTimeProps) {
|
|
22
22
|
const { d } = useI18n()
|
|
23
|
-
return <>{d(props.value, props.
|
|
23
|
+
return <>{d(props.value, props.format)}</>
|
|
24
24
|
}
|
|
@@ -3,8 +3,8 @@ import { useI18n } from '../use-i18n'
|
|
|
3
3
|
export interface NumberProps {
|
|
4
4
|
/** Number value to format */
|
|
5
5
|
value: number
|
|
6
|
-
/** Named format
|
|
7
|
-
|
|
6
|
+
/** Named format key defined in numberFormats config */
|
|
7
|
+
format?: string
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/** @alias NumberProps */
|
|
@@ -15,10 +15,10 @@ export type FluentiNumberFormatProps = NumberProps
|
|
|
15
15
|
*
|
|
16
16
|
* @example
|
|
17
17
|
* ```tsx
|
|
18
|
-
* <NumberFormat value={1234.56}
|
|
18
|
+
* <NumberFormat value={1234.56} format="currency" />
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
export function NumberFormat(props: NumberProps) {
|
|
22
22
|
const { n } = useI18n()
|
|
23
|
-
return <>{n(props.value, props.
|
|
23
|
+
return <>{n(props.value, props.format)}</>
|
|
24
24
|
}
|
|
@@ -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,157 +148,50 @@ 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
|
-
const result = config.missing(loc, id)
|
|
170
|
-
if (result !== undefined) {
|
|
171
|
-
return result as LocalizedString
|
|
172
|
-
}
|
|
173
|
-
return undefined
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function resolveMessage(
|
|
177
|
-
id: string,
|
|
178
|
-
loc: Locale,
|
|
179
|
-
values?: Record<string, unknown>,
|
|
180
|
-
): LocalizedString {
|
|
181
|
-
const catalogResult = lookupWithFallbacks(id, loc, values)
|
|
182
|
-
if (catalogResult !== undefined) {
|
|
183
|
-
return catalogResult
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
diagnostics?.missingKey(loc, id)
|
|
187
|
-
|
|
188
|
-
const missingResult = resolveMissing(id, loc)
|
|
189
|
-
if (missingResult !== undefined) {
|
|
190
|
-
return missingResult
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (id.includes('{')) {
|
|
194
|
-
return coreInterpolate(id, values, loc) as LocalizedString
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
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 },
|
|
198
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)
|
|
199
171
|
|
|
200
172
|
function t(strings: TemplateStringsArray, ...exprs: unknown[]): LocalizedString
|
|
201
173
|
function t(id: string | MessageDescriptor, values?: Record<string, unknown>): LocalizedString
|
|
202
174
|
function t(idOrStrings: string | MessageDescriptor | TemplateStringsArray, ...rest: unknown[]): LocalizedString {
|
|
203
|
-
//
|
|
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
|
|
204
178
|
if (Array.isArray(idOrStrings) && 'raw' in idOrStrings) {
|
|
205
|
-
|
|
206
|
-
const icu = buildICUMessage(strings, rest)
|
|
207
|
-
const values = Object.fromEntries(rest.map((v, i) => [String(i), v]))
|
|
208
|
-
return t(icu, values)
|
|
179
|
+
return i18n.t(idOrStrings as TemplateStringsArray, ...rest) as LocalizedString
|
|
209
180
|
}
|
|
210
|
-
|
|
211
|
-
const id = idOrStrings as string | MessageDescriptor
|
|
212
|
-
const values = rest[0] as Record<string, unknown> | undefined
|
|
213
|
-
const currentLocale = locale() // reactive dependency
|
|
214
|
-
if (typeof id === 'object' && id !== null) {
|
|
215
|
-
const messageId = resolveDescriptorId(id)
|
|
216
|
-
if (messageId) {
|
|
217
|
-
const catalogResult = lookupWithFallbacks(messageId, currentLocale, values)
|
|
218
|
-
if (catalogResult !== undefined) {
|
|
219
|
-
return catalogResult
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const missingResult = resolveMissing(messageId, currentLocale)
|
|
223
|
-
if (missingResult !== undefined) {
|
|
224
|
-
return missingResult
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (id.message !== undefined) {
|
|
229
|
-
return coreInterpolate(id.message, values, currentLocale) as LocalizedString
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return (messageId ?? '') as LocalizedString
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return resolveMessage(id, currentLocale, values)
|
|
181
|
+
return i18n.t(idOrStrings as string | MessageDescriptor, rest[0] as Record<string, unknown> | undefined) as LocalizedString
|
|
236
182
|
}
|
|
237
183
|
|
|
238
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
|
|
239
187
|
// Intentional mutation: messages record is locally scoped to this context closure
|
|
240
188
|
messages[loc] = { ...messages[loc], ...msgs }
|
|
241
189
|
loadedLocalesSet.add(loc)
|
|
242
190
|
setLoadedLocales(new Set(loadedLocalesSet))
|
|
243
191
|
}
|
|
244
192
|
|
|
193
|
+
let _localeRequestId = 0
|
|
194
|
+
|
|
245
195
|
const setLocale = async (newLocale: Locale): Promise<void> => {
|
|
246
196
|
if (!lazyLocaleLoading || !i18nConfig.chunkLoader) {
|
|
247
197
|
setLocaleSignal(newLocale)
|
|
@@ -258,27 +208,41 @@ export function createFluentiContext(config: FluentiCoreConfig | FluentiConfig):
|
|
|
258
208
|
return
|
|
259
209
|
}
|
|
260
210
|
|
|
211
|
+
// Race-condition protection: track request ID
|
|
212
|
+
const thisRequest = ++_localeRequestId
|
|
261
213
|
setIsLoading(true)
|
|
262
214
|
try {
|
|
263
215
|
const loaded = resolveChunkMessages(await i18nConfig.chunkLoader(newLocale))
|
|
216
|
+
// Always store loaded messages — they may be needed if locale is switched back
|
|
217
|
+
i18n.loadMessages(newLocale, loaded)
|
|
264
218
|
// Intentional mutation: messages record is locally scoped to this context closure
|
|
265
219
|
messages[newLocale] = { ...messages[newLocale], ...loaded }
|
|
266
220
|
loadedLocalesSet.add(newLocale)
|
|
267
221
|
setLoadedLocales(new Set(loadedLocalesSet))
|
|
222
|
+
// Stale request — a newer setLocale call superseded this one; don't switch locale
|
|
223
|
+
if (thisRequest !== _localeRequestId) return
|
|
268
224
|
if (splitRuntime?.__switchLocale) {
|
|
269
225
|
await splitRuntime.__switchLocale(newLocale)
|
|
270
226
|
}
|
|
227
|
+
// Re-check after async __switchLocale — a newer setLocale() may have superseded this one
|
|
228
|
+
if (thisRequest !== _localeRequestId) return
|
|
271
229
|
setLocaleSignal(newLocale)
|
|
272
230
|
} finally {
|
|
273
|
-
|
|
231
|
+
if (thisRequest === _localeRequestId) {
|
|
232
|
+
setIsLoading(false)
|
|
233
|
+
}
|
|
274
234
|
}
|
|
275
235
|
}
|
|
276
236
|
|
|
237
|
+
const _preloadInFlight = new Set<string>()
|
|
238
|
+
|
|
277
239
|
const preloadLocale = (loc: string): void => {
|
|
278
|
-
if (!lazyLocaleLoading || loadedLocalesSet.has(loc) || !i18nConfig.chunkLoader) return
|
|
240
|
+
if (!lazyLocaleLoading || loadedLocalesSet.has(loc) || !i18nConfig.chunkLoader || _preloadInFlight.has(loc)) return
|
|
241
|
+
_preloadInFlight.add(loc)
|
|
279
242
|
const splitRuntime = getSplitRuntimeModule()
|
|
280
243
|
i18nConfig.chunkLoader(loc).then(async (loaded) => {
|
|
281
244
|
const resolved = resolveChunkMessages(loaded)
|
|
245
|
+
i18n.loadMessages(loc, resolved)
|
|
282
246
|
// Intentional mutation: messages record is locally scoped to this context closure
|
|
283
247
|
messages[loc] = { ...messages[loc], ...resolved }
|
|
284
248
|
loadedLocalesSet.add(loc)
|
|
@@ -288,19 +252,29 @@ export function createFluentiContext(config: FluentiCoreConfig | FluentiConfig):
|
|
|
288
252
|
}
|
|
289
253
|
}).catch((e: unknown) => {
|
|
290
254
|
console.warn('[fluenti] preload failed:', loc, e)
|
|
255
|
+
}).finally(() => {
|
|
256
|
+
_preloadInFlight.delete(loc)
|
|
291
257
|
})
|
|
292
258
|
}
|
|
293
259
|
|
|
294
|
-
const getLocales = (): Locale[] =>
|
|
260
|
+
const getLocales = (): Locale[] => i18n.getLocales()
|
|
295
261
|
|
|
296
|
-
const d = (value: Date | number, style?: string): LocalizedString =>
|
|
297
|
-
|
|
262
|
+
const d = (value: Date | number, style?: string): LocalizedString => {
|
|
263
|
+
const current = locale() // READ SIGNAL → reactive dependency
|
|
264
|
+
if (i18n.locale !== current) i18n.locale = current
|
|
265
|
+
return i18n.d(value, style) as LocalizedString
|
|
266
|
+
}
|
|
298
267
|
|
|
299
|
-
const n = (value: number, style?: string): LocalizedString =>
|
|
300
|
-
|
|
268
|
+
const n = (value: number, style?: string): LocalizedString => {
|
|
269
|
+
const current = locale() // READ SIGNAL → reactive dependency
|
|
270
|
+
if (i18n.locale !== current) i18n.locale = current
|
|
271
|
+
return i18n.n(value, style) as LocalizedString
|
|
272
|
+
}
|
|
301
273
|
|
|
302
274
|
const format = (message: string, values?: Record<string, unknown>): LocalizedString => {
|
|
303
|
-
|
|
275
|
+
const current = locale() // READ SIGNAL → reactive dependency
|
|
276
|
+
if (i18n.locale !== current) i18n.locale = current
|
|
277
|
+
return i18n.format(message, values) as LocalizedString
|
|
304
278
|
}
|
|
305
279
|
|
|
306
280
|
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/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
|
}
|