@fluenti/solid 0.3.4 → 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/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/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-entry.ts +13 -0
- package/src/context.ts +98 -144
- 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
|
@@ -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)
|
|
@@ -270,6 +214,7 @@ export function createFluentiContext(config: FluentiCoreConfig | FluentiConfig):
|
|
|
270
214
|
try {
|
|
271
215
|
const loaded = resolveChunkMessages(await i18nConfig.chunkLoader(newLocale))
|
|
272
216
|
// Always store loaded messages — they may be needed if locale is switched back
|
|
217
|
+
i18n.loadMessages(newLocale, loaded)
|
|
273
218
|
// Intentional mutation: messages record is locally scoped to this context closure
|
|
274
219
|
messages[newLocale] = { ...messages[newLocale], ...loaded }
|
|
275
220
|
loadedLocalesSet.add(newLocale)
|
|
@@ -297,6 +242,7 @@ export function createFluentiContext(config: FluentiCoreConfig | FluentiConfig):
|
|
|
297
242
|
const splitRuntime = getSplitRuntimeModule()
|
|
298
243
|
i18nConfig.chunkLoader(loc).then(async (loaded) => {
|
|
299
244
|
const resolved = resolveChunkMessages(loaded)
|
|
245
|
+
i18n.loadMessages(loc, resolved)
|
|
300
246
|
// Intentional mutation: messages record is locally scoped to this context closure
|
|
301
247
|
messages[loc] = { ...messages[loc], ...resolved }
|
|
302
248
|
loadedLocalesSet.add(loc)
|
|
@@ -311,16 +257,24 @@ export function createFluentiContext(config: FluentiCoreConfig | FluentiConfig):
|
|
|
311
257
|
})
|
|
312
258
|
}
|
|
313
259
|
|
|
314
|
-
const getLocales = (): Locale[] =>
|
|
260
|
+
const getLocales = (): Locale[] => i18n.getLocales()
|
|
315
261
|
|
|
316
|
-
const d = (value: Date | number, style?: string): LocalizedString =>
|
|
317
|
-
|
|
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
|
+
}
|
|
318
267
|
|
|
319
|
-
const n = (value: number, style?: string): LocalizedString =>
|
|
320
|
-
|
|
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
|
+
}
|
|
321
273
|
|
|
322
274
|
const format = (message: string, values?: Record<string, unknown>): LocalizedString => {
|
|
323
|
-
|
|
275
|
+
const current = locale() // READ SIGNAL → reactive dependency
|
|
276
|
+
if (i18n.locale !== current) i18n.locale = current
|
|
277
|
+
return i18n.format(message, values) as LocalizedString
|
|
324
278
|
}
|
|
325
279
|
|
|
326
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
|
}
|