@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.
Files changed (42) hide show
  1. package/dist/components-entry.cjs +2 -0
  2. package/dist/components-entry.cjs.map +1 -0
  3. package/dist/components-entry.d.ts +12 -0
  4. package/dist/components-entry.d.ts.map +1 -0
  5. package/dist/components-entry.js +283 -0
  6. package/dist/components-entry.js.map +1 -0
  7. package/dist/context.d.ts +23 -1
  8. package/dist/context.d.ts.map +1 -1
  9. package/dist/index.cjs +1 -1
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.ts +1 -6
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +4 -414
  14. package/dist/index.js.map +1 -1
  15. package/dist/plural.d.ts +9 -0
  16. package/dist/plural.d.ts.map +1 -1
  17. package/dist/provider.d.ts +19 -1
  18. package/dist/provider.d.ts.map +1 -1
  19. package/dist/rich-dom.d.ts.map +1 -1
  20. package/dist/select.d.ts +15 -0
  21. package/dist/select.d.ts.map +1 -1
  22. package/dist/solid-runtime.d.ts.map +1 -1
  23. package/dist/trans.d.ts.map +1 -1
  24. package/dist/use-i18n-DPb98Dw1.js +201 -0
  25. package/dist/use-i18n-DPb98Dw1.js.map +1 -0
  26. package/dist/use-i18n-oAO3vYS7.cjs +2 -0
  27. package/dist/use-i18n-oAO3vYS7.cjs.map +1 -0
  28. package/dist/vite-plugin.cjs +1 -1
  29. package/dist/vite-plugin.cjs.map +1 -1
  30. package/dist/vite-plugin.js +0 -1
  31. package/dist/vite-plugin.js.map +1 -1
  32. package/llms.txt +44 -0
  33. package/package.json +18 -3
  34. package/src/components-entry.ts +13 -0
  35. package/src/context.ts +110 -148
  36. package/src/index.ts +5 -6
  37. package/src/plural.tsx +9 -0
  38. package/src/provider.tsx +19 -1
  39. package/src/rich-dom.tsx +10 -0
  40. package/src/select.tsx +15 -0
  41. package/src/solid-runtime.ts +0 -1
  42. 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.4",
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.3.4",
84
- "@fluenti/vite-plugin": "0.3.4"
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 { 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'
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
- function lookupCatalog(
100
- id: string,
101
- loc: Locale,
102
- values?: Record<string, unknown>,
103
- ): LocalizedString | undefined {
104
- const catalog = messages[loc]
105
- if (!catalog) {
106
- return undefined
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
- // Tagged template form: t`Hello ${name}`
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
- const strings = idOrStrings as TemplateStringsArray
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
- if (splitRuntime?.__switchLocale) {
261
- await splitRuntime.__switchLocale(newLocale)
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
- if (splitRuntime?.__switchLocale) {
280
- await splitRuntime.__switchLocale(newLocale)
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[] => Object.keys(messages)
268
+ const getLocales = (): Locale[] => i18n.getLocales()
315
269
 
316
- const d = (value: Date | number, style?: string): LocalizedString =>
317
- formatDate(value, locale(), style, i18nConfig.dateFormats) as LocalizedString
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
- formatNumber(value, locale(), style, i18nConfig.numberFormats) as LocalizedString
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
- return coreInterpolate(message, values, locale()) as LocalizedString
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 { Trans } from './trans'
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
- * Provide i18n context to the component tree.
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()
@@ -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
  }