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