@fluenti/solid 0.1.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/LICENSE +21 -0
  2. package/README.md +317 -0
  3. package/dist/compile-time-t.d.ts +3 -0
  4. package/dist/compile-time-t.d.ts.map +1 -0
  5. package/dist/components/DateTime.d.ts +16 -0
  6. package/dist/components/DateTime.d.ts.map +1 -0
  7. package/dist/components/NumberFormat.d.ts +16 -0
  8. package/dist/components/NumberFormat.d.ts.map +1 -0
  9. package/dist/context.d.ts +69 -0
  10. package/dist/context.d.ts.map +1 -0
  11. package/dist/hooks/__useI18n.d.ts +12 -0
  12. package/dist/hooks/__useI18n.d.ts.map +1 -0
  13. package/dist/index.cjs +2 -0
  14. package/dist/index.cjs.map +1 -0
  15. package/dist/index.d.ts +15 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +446 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/msg.d.ts +2 -0
  20. package/dist/msg.d.ts.map +1 -0
  21. package/dist/plural.d.ts +55 -0
  22. package/dist/plural.d.ts.map +1 -0
  23. package/dist/provider.d.ts +10 -0
  24. package/dist/provider.d.ts.map +1 -0
  25. package/dist/rich-dom.d.ts +17 -0
  26. package/dist/rich-dom.d.ts.map +1 -0
  27. package/dist/select.d.ts +49 -0
  28. package/dist/select.d.ts.map +1 -0
  29. package/dist/server.d.ts +77 -0
  30. package/dist/server.d.ts.map +1 -0
  31. package/dist/trans.d.ts +46 -0
  32. package/dist/trans.d.ts.map +1 -0
  33. package/dist/types.d.ts +45 -0
  34. package/dist/types.d.ts.map +1 -0
  35. package/dist/use-i18n.d.ts +12 -0
  36. package/dist/use-i18n.d.ts.map +1 -0
  37. package/package.json +75 -0
  38. package/src/compile-time-t.ts +9 -0
  39. package/src/components/DateTime.tsx +21 -0
  40. package/src/components/NumberFormat.tsx +21 -0
  41. package/src/context.ts +337 -0
  42. package/src/hooks/__useI18n.ts +15 -0
  43. package/src/index.ts +14 -0
  44. package/src/msg.ts +4 -0
  45. package/src/plural.tsx +136 -0
  46. package/src/provider.tsx +16 -0
  47. package/src/rich-dom.tsx +170 -0
  48. package/src/select.tsx +90 -0
  49. package/src/server.ts +153 -0
  50. package/src/trans.tsx +243 -0
  51. package/src/types.ts +55 -0
  52. package/src/use-i18n.ts +30 -0
  53. package/src/vite-runtime.d.ts +4 -0
package/src/select.tsx ADDED
@@ -0,0 +1,90 @@
1
+ import type { Component, JSX } from 'solid-js'
2
+ import { Dynamic } from 'solid-js/web'
3
+ import { hashMessage } from '@fluenti/core'
4
+ import { useI18n } from './use-i18n'
5
+ import { buildICUSelectMessage, normalizeSelectForms, reconstruct, serializeRichForms } from './rich-dom'
6
+
7
+ /** Props for the `<Select>` component */
8
+ export interface SelectProps {
9
+ /** The value to match against prop keys */
10
+ value: string
11
+ /** Override the auto-generated synthetic ICU message id */
12
+ id?: string
13
+ /** Message context used for identity and translator disambiguation */
14
+ context?: string
15
+ /** Translator-facing note preserved in extraction catalogs */
16
+ comment?: string
17
+ /** Fallback message when no key matches */
18
+ other: string | JSX.Element
19
+ /**
20
+ * Named options map. Keys are match values, values are display strings or JSX.
21
+ * Takes precedence over dynamic attrs when both are provided.
22
+ *
23
+ * @example `{ male: 'He', female: 'She' }`
24
+ */
25
+ options?: Record<string, string | JSX.Element>
26
+ /** Wrapper element tag name (default: `span`) */
27
+ tag?: string
28
+ /** Additional key/message pairs for matching (attrs fallback) */
29
+ [key: string]: unknown
30
+ }
31
+
32
+ /**
33
+ * Render a message selected by matching `value` against prop keys.
34
+ *
35
+ * Options can be provided via the type-safe `options` prop (recommended)
36
+ * or as direct attrs (convenience). When both are present, `options` takes
37
+ * precedence.
38
+ *
39
+ * Rich text is supported via JSX element values in the `options` prop or
40
+ * as direct JSX element props:
41
+ * ```tsx
42
+ * <Select
43
+ * value={gender()}
44
+ * options={{
45
+ * male: <><strong>He</strong> liked this</>,
46
+ * female: <><strong>She</strong> liked this</>,
47
+ * }}
48
+ * other={<><em>They</em> liked this</>}
49
+ * />
50
+ * ```
51
+ *
52
+ * Falls back to the `other` prop when no key matches.
53
+ */
54
+ export const SelectComp: Component<SelectProps> = (props) => {
55
+ const { t } = useI18n()
56
+
57
+ const resolvedTag = () => props.tag ?? 'span'
58
+
59
+ const content = () => {
60
+ const forms: Record<string, unknown> = props.options !== undefined
61
+ ? { ...props.options, other: props.other }
62
+ : {
63
+ ...Object.fromEntries(
64
+ Object.entries(props).filter(([key]) => !['value', 'id', 'context', 'comment', 'options', 'other', 'tag'].includes(key)),
65
+ ),
66
+ other: props.other,
67
+ }
68
+
69
+ const orderedKeys = [...Object.keys(forms).filter(key => key !== 'other'), 'other'] as const
70
+ const { messages, components } = serializeRichForms(orderedKeys, forms)
71
+ const normalized = normalizeSelectForms(
72
+ Object.fromEntries([...orderedKeys].map((key) => [key, messages[key] ?? ''])),
73
+ )
74
+ const translated = t(
75
+ {
76
+ id: props.id ?? (props.context === undefined
77
+ ? buildICUSelectMessage(normalized.forms)
78
+ : hashMessage(buildICUSelectMessage(normalized.forms), props.context)),
79
+ message: buildICUSelectMessage(normalized.forms),
80
+ ...(props.context !== undefined ? { context: props.context } : {}),
81
+ ...(props.comment !== undefined ? { comment: props.comment } : {}),
82
+ },
83
+ { value: normalized.valueMap[props.value] ?? 'other' },
84
+ )
85
+
86
+ return components.length > 0 ? reconstruct(translated, components) : translated
87
+ }
88
+
89
+ return (<Dynamic component={resolvedTag()}>{content()}</Dynamic>) as JSX.Element
90
+ }
package/src/server.ts ADDED
@@ -0,0 +1,153 @@
1
+ import { createFluent } from '@fluenti/core'
2
+ import type {
3
+ FluentInstanceExtended,
4
+ FluentConfigExtended,
5
+ Locale,
6
+ Messages,
7
+ DateFormatOptions,
8
+ NumberFormatOptions,
9
+ } from '@fluenti/core'
10
+
11
+ // Re-export SSR utilities from core for convenience
12
+ export { detectLocale, getSSRLocaleScript, getHydratedLocale, isRTL, getDirection } from '@fluenti/core'
13
+ export type { DetectLocaleOptions } from '@fluenti/core'
14
+
15
+ /**
16
+ * Configuration for `createServerI18n`.
17
+ */
18
+ export interface ServerI18nConfig {
19
+ /** Load messages for a given locale. Called once per locale per request. */
20
+ loadMessages: (locale: string) => Promise<Messages | { default: Messages }>
21
+ /** Fallback locale when a translation is missing */
22
+ fallbackLocale?: string
23
+ /**
24
+ * Auto-resolve locale when `setLocale()` was not called.
25
+ *
26
+ * This is the fallback for contexts where the layout doesn't run — most
27
+ * notably `"use server"` functions, which are independent requests
28
+ * that skip the layout tree entirely.
29
+ *
30
+ * Common patterns:
31
+ * - Read from a cookie via `getRequestEvent()`
32
+ * - Read from a request header set by middleware
33
+ *
34
+ * If omitted and `setLocale()` was not called, `getI18n()` will throw.
35
+ */
36
+ resolveLocale?: () => string | Promise<string>
37
+ /** Custom fallback chains per locale */
38
+ fallbackChain?: Record<string, Locale[]>
39
+ /** Custom date format styles */
40
+ dateFormats?: DateFormatOptions
41
+ /** Custom number format styles */
42
+ numberFormats?: NumberFormatOptions
43
+ /** Handler for missing translation keys */
44
+ missing?: (locale: Locale, id: string) => string | undefined
45
+ }
46
+
47
+ /**
48
+ * The object returned by `createServerI18n`.
49
+ */
50
+ export interface ServerI18n {
51
+ /**
52
+ * Set the locale for the current server request.
53
+ * Call this once in your entry-server or root layout before any `getI18n()` calls.
54
+ */
55
+ setLocale: (locale: string) => void
56
+
57
+ /**
58
+ * Get a fully configured i18n instance for the current request.
59
+ * Messages are loaded lazily and cached.
60
+ */
61
+ getI18n: () => Promise<FluentInstanceExtended & { locale: string }>
62
+ }
63
+
64
+ /**
65
+ * Create server-side i18n utilities for SolidStart.
66
+ *
67
+ * Unlike React's `createServerI18n` which uses `React.cache()` for RSC
68
+ * per-request isolation, this version uses a simple module-level store
69
+ * matching SolidStart's synchronous rendering model.
70
+ *
71
+ * For per-request isolation in SolidStart, use `getRequestEvent()` in
72
+ * your `resolveLocale` callback, or call `setLocale()` in your
73
+ * entry-server middleware.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * // lib/i18n.server.ts
78
+ * import { createServerI18n } from '@fluenti/solid/server'
79
+ *
80
+ * export const { setLocale, getI18n } = createServerI18n({
81
+ * loadMessages: (locale) => import(`../locales/compiled/${locale}.ts`),
82
+ * fallbackLocale: 'en',
83
+ * })
84
+ * ```
85
+ */
86
+ export function createServerI18n(config: ServerI18nConfig): ServerI18n {
87
+ let currentLocale: string | null = null
88
+ let cachedInstance: (FluentInstanceExtended & { locale: string }) | null = null
89
+ const messageCache = new Map<string, Messages>()
90
+
91
+ function setLocale(locale: string): void {
92
+ currentLocale = locale
93
+ }
94
+
95
+ async function loadLocaleMessages(locale: string): Promise<Messages> {
96
+ const cached = messageCache.get(locale)
97
+ if (cached) return cached
98
+
99
+ const raw = await config.loadMessages(locale)
100
+ const messages: Messages =
101
+ typeof raw === 'object' && raw !== null && 'default' in raw
102
+ ? (raw as { default: Messages }).default
103
+ : (raw as Messages)
104
+
105
+ messageCache.set(locale, messages)
106
+ return messages
107
+ }
108
+
109
+ async function getI18n(): Promise<FluentInstanceExtended & { locale: string }> {
110
+ // If setLocale() was never called, try the resolveLocale fallback.
111
+ if (!currentLocale && config.resolveLocale) {
112
+ currentLocale = await config.resolveLocale()
113
+ }
114
+
115
+ const locale = currentLocale
116
+
117
+ if (!locale) {
118
+ throw new Error(
119
+ '[fluenti] No locale set. Call setLocale(locale) in your entry-server or layout before using getI18n(), ' +
120
+ 'or provide a resolveLocale function in createServerI18n config to auto-detect locale ' +
121
+ 'in server functions and other contexts where the layout does not run.',
122
+ )
123
+ }
124
+
125
+ // Return cached instance if locale hasn't changed
126
+ if (cachedInstance && cachedInstance.locale === locale) {
127
+ return cachedInstance
128
+ }
129
+
130
+ // Load messages for current locale (and fallback if configured)
131
+ const allMessages: Record<string, Messages> = {}
132
+ allMessages[locale] = await loadLocaleMessages(locale)
133
+
134
+ if (config.fallbackLocale && config.fallbackLocale !== locale) {
135
+ allMessages[config.fallbackLocale] = await loadLocaleMessages(config.fallbackLocale)
136
+ }
137
+
138
+ const fluentConfig: FluentConfigExtended = {
139
+ locale,
140
+ messages: allMessages,
141
+ }
142
+ if (config.fallbackLocale !== undefined) fluentConfig.fallbackLocale = config.fallbackLocale
143
+ if (config.fallbackChain !== undefined) fluentConfig.fallbackChain = config.fallbackChain
144
+ if (config.dateFormats !== undefined) fluentConfig.dateFormats = config.dateFormats
145
+ if (config.numberFormats !== undefined) fluentConfig.numberFormats = config.numberFormats
146
+ if (config.missing !== undefined) fluentConfig.missing = config.missing
147
+
148
+ cachedInstance = createFluent(fluentConfig)
149
+ return cachedInstance
150
+ }
151
+
152
+ return { setLocale, getI18n }
153
+ }
package/src/trans.tsx ADDED
@@ -0,0 +1,243 @@
1
+ import { Dynamic } from 'solid-js/web'
2
+ import { children as resolveChildren, createMemo } from 'solid-js'
3
+ import type { Component, JSX } from 'solid-js'
4
+ import { useI18n } from './use-i18n'
5
+ import { extractMessage as extractDomMessage, reconstruct as reconstructDomMessage } from './rich-dom'
6
+
7
+ /** A Solid component that accepts children */
8
+ export type RichComponent = Component<{ children?: JSX.Element }>
9
+
10
+ /** Props for the `<Trans>` component */
11
+ export interface TransProps {
12
+ /** Override auto-generated hash ID */
13
+ id?: string
14
+ /** Message context used for identity and translator disambiguation */
15
+ context?: string
16
+ /** Translator-facing note preserved in extraction catalogs */
17
+ comment?: string
18
+ /** Wrapper element tag name (default: `'span'`) — used in children-only mode */
19
+ tag?: string
20
+ /** Children — the content to translate (legacy API) */
21
+ children?: JSX.Element
22
+ /** Translated message string with XML-like tags (e.g. `<bold>text</bold>`) */
23
+ message?: string
24
+ /** Map of tag names to Solid components */
25
+ components?: Record<string, RichComponent>
26
+ /** @internal Pre-computed message from build plugin */
27
+ __message?: string
28
+ /** @internal Pre-computed component map from build plugin */
29
+ __components?: Record<string, RichComponent>
30
+ }
31
+
32
+ /**
33
+ * A token from parsing the message string.
34
+ * Either a plain text segment or a tag with inner content.
35
+ */
36
+ interface TextToken {
37
+ readonly type: 'text'
38
+ readonly value: string
39
+ }
40
+
41
+ interface TagToken {
42
+ readonly type: 'tag'
43
+ readonly name: string
44
+ readonly children: readonly Token[]
45
+ }
46
+
47
+ type Token = TextToken | TagToken
48
+
49
+ /**
50
+ * Parse a message string containing XML-like tags into a token tree.
51
+ *
52
+ * Supports:
53
+ * - Named tags: `<bold>content</bold>`
54
+ * - Self-closing tags: `<br/>`
55
+ * - Nested tags: `<bold>hello <italic>world</italic></bold>`
56
+ */
57
+ function parseTokens(input: string): readonly Token[] {
58
+ const tokens: Token[] = []
59
+ let pos = 0
60
+
61
+ while (pos < input.length) {
62
+ const openIdx = input.indexOf('<', pos)
63
+
64
+ if (openIdx === -1) {
65
+ // No more tags — rest is plain text
66
+ tokens.push({ type: 'text', value: input.slice(pos) })
67
+ break
68
+ }
69
+
70
+ // Push any text before this tag
71
+ if (openIdx > pos) {
72
+ tokens.push({ type: 'text', value: input.slice(pos, openIdx) })
73
+ }
74
+
75
+ // Check for self-closing tag: <tagName/>
76
+ const selfCloseMatch = input.slice(openIdx).match(/^<(\w+)\s*\/>/)
77
+ if (selfCloseMatch) {
78
+ tokens.push({ type: 'tag', name: selfCloseMatch[1]!, children: [] })
79
+ pos = openIdx + selfCloseMatch[0].length
80
+ continue
81
+ }
82
+
83
+ // Check for opening tag: <tagName>
84
+ const openMatch = input.slice(openIdx).match(/^<(\w+)>/)
85
+ if (!openMatch) {
86
+ // Not a valid tag — treat '<' as text
87
+ tokens.push({ type: 'text', value: '<' })
88
+ pos = openIdx + 1
89
+ continue
90
+ }
91
+
92
+ const tagName = openMatch[1]!
93
+ const contentStart = openIdx + openMatch[0].length
94
+
95
+ // Find the matching closing tag, respecting nesting
96
+ const innerEnd = findClosingTag(input, tagName, contentStart)
97
+ if (innerEnd === -1) {
98
+ // No closing tag found — treat as plain text
99
+ tokens.push({ type: 'text', value: input.slice(openIdx, contentStart) })
100
+ pos = contentStart
101
+ continue
102
+ }
103
+
104
+ const innerContent = input.slice(contentStart, innerEnd)
105
+ const closingTag = `</${tagName}>`
106
+ tokens.push({
107
+ type: 'tag',
108
+ name: tagName,
109
+ children: parseTokens(innerContent),
110
+ })
111
+ pos = innerEnd + closingTag.length
112
+ }
113
+
114
+ return tokens
115
+ }
116
+
117
+ /**
118
+ * Find the position of the matching closing tag, accounting for nesting
119
+ * of the same tag name.
120
+ *
121
+ * Returns the index of the start of the closing tag, or -1 if not found.
122
+ */
123
+ function findClosingTag(input: string, tagName: string, startPos: number): number {
124
+ const openTag = `<${tagName}>`
125
+ const closeTag = `</${tagName}>`
126
+ let depth = 1
127
+ let pos = startPos
128
+
129
+ while (pos < input.length && depth > 0) {
130
+ const nextOpen = input.indexOf(openTag, pos)
131
+ const nextClose = input.indexOf(closeTag, pos)
132
+
133
+ if (nextClose === -1) return -1
134
+
135
+ if (nextOpen !== -1 && nextOpen < nextClose) {
136
+ depth++
137
+ pos = nextOpen + openTag.length
138
+ } else {
139
+ depth--
140
+ if (depth === 0) return nextClose
141
+ pos = nextClose + closeTag.length
142
+ }
143
+ }
144
+
145
+ return -1
146
+ }
147
+
148
+ /**
149
+ * Render a token tree into Solid JSX elements using the components map.
150
+ */
151
+ function renderTokens(
152
+ tokens: readonly Token[],
153
+ components: Record<string, RichComponent>,
154
+ ): JSX.Element {
155
+ const elements = tokens.map((token): JSX.Element => {
156
+ if (token.type === 'text') {
157
+ return token.value as unknown as JSX.Element
158
+ }
159
+
160
+ const Comp = components[token.name]
161
+ if (!Comp) {
162
+ // Unknown component — render inner content as plain text
163
+ return renderTokens(token.children, components)
164
+ }
165
+
166
+ const innerContent = token.children.length > 0
167
+ ? renderTokens(token.children, components)
168
+ : undefined
169
+
170
+ return (<Dynamic component={Comp}>{innerContent}</Dynamic>) as JSX.Element
171
+ })
172
+
173
+ if (elements.length === 1) return elements[0]!
174
+ return (<>{elements}</>) as JSX.Element
175
+ }
176
+
177
+ /**
178
+ * Render translated content with inline components.
179
+ *
180
+ * Supports two APIs:
181
+ *
182
+ * 1. **message + components** (recommended for rich text):
183
+ * ```tsx
184
+ * <Trans
185
+ * message={t`Welcome to <bold>Fluenti</bold>!`}
186
+ * components={{ bold: (props) => <strong>{props.children}</strong> }}
187
+ * />
188
+ * ```
189
+ *
190
+ * 2. **children** (legacy / simple passthrough):
191
+ * ```tsx
192
+ * <Trans>Click <a href="/next">here</a> to continue</Trans>
193
+ * ```
194
+ */
195
+ export const Trans: Component<TransProps> = (props) => {
196
+ const { t } = useI18n()
197
+ const resolvedChildren = resolveChildren(() => props.children)
198
+ // message + components API (including build-time __message/__components)
199
+ // Note: the vite-plugin tagged-template transform wraps Solid expressions in
200
+ // createMemo(), so props.message may be a memo accessor (function) instead of
201
+ // a string. We unwrap it here to handle both cases.
202
+ const message = createMemo(() => {
203
+ const raw = props.__message ?? props.message
204
+ return typeof raw === 'function' ? (raw as () => string)() : raw
205
+ })
206
+ const components = createMemo(() => props.__components ?? props.components)
207
+
208
+ return (() => {
209
+ const msg = message()
210
+ const comps = components()
211
+
212
+ if (msg !== undefined && comps) {
213
+ const translated = t({
214
+ ...(props.id !== undefined ? { id: props.id } : {}),
215
+ message: msg,
216
+ ...(props.context !== undefined ? { context: props.context } : {}),
217
+ ...(props.comment !== undefined ? { comment: props.comment } : {}),
218
+ })
219
+ const tokens = parseTokens(translated)
220
+ return renderTokens(tokens, comps)
221
+ }
222
+
223
+ // Fallback: children-only API with runtime extraction/reconstruction
224
+ const children = resolvedChildren.toArray()
225
+ if (children.length === 0) return null
226
+ const extracted = extractDomMessage(children)
227
+ const translated = t({
228
+ ...(props.id !== undefined ? { id: props.id } : {}),
229
+ message: extracted.message,
230
+ ...(props.context !== undefined ? { context: props.context } : {}),
231
+ ...(props.comment !== undefined ? { comment: props.comment } : {}),
232
+ })
233
+ const result = extracted.components.length > 0
234
+ ? reconstructDomMessage(translated, extracted.components)
235
+ : translated
236
+
237
+ if (Array.isArray(result) && result.length > 1) {
238
+ return (<Dynamic component={props.tag ?? 'span'}>{result}</Dynamic>) as JSX.Element
239
+ }
240
+
241
+ return result as JSX.Element
242
+ }) as unknown as JSX.Element
243
+ }
package/src/types.ts ADDED
@@ -0,0 +1,55 @@
1
+ import type { Accessor } from 'solid-js'
2
+ import type {
3
+ FluentConfig,
4
+ Locale,
5
+ Messages,
6
+ CompiledMessage,
7
+ MessageDescriptor,
8
+ DateFormatOptions,
9
+ NumberFormatOptions,
10
+ } from '@fluenti/core'
11
+
12
+ /** Chunk loader for lazy locale loading */
13
+ export type ChunkLoader = (
14
+ locale: string,
15
+ ) => Promise<Record<string, CompiledMessage> | { default: Record<string, CompiledMessage> }>
16
+
17
+ /** Extended config with lazy locale loading support */
18
+ export interface I18nConfig extends FluentConfig {
19
+ /** Async chunk loader for lazy locale loading */
20
+ chunkLoader?: ChunkLoader
21
+ /** Enable lazy locale loading through chunkLoader */
22
+ lazyLocaleLoading?: boolean
23
+ /** Named date format styles */
24
+ dateFormats?: DateFormatOptions
25
+ /** Named number format styles */
26
+ numberFormats?: NumberFormatOptions
27
+ }
28
+
29
+ /** Reactive i18n context holding locale signal and translation utilities */
30
+ export interface I18nContext {
31
+ /** Reactive accessor for the current locale */
32
+ locale(): Locale
33
+ /** Set the active locale (async when lazy locale loading is enabled) */
34
+ setLocale(locale: Locale): Promise<void>
35
+ /** Translate a message by id with optional interpolation values */
36
+ t(id: string | MessageDescriptor, values?: Record<string, unknown>): string
37
+ /** Tagged template form: t`Hello ${name}` */
38
+ t(strings: TemplateStringsArray, ...exprs: unknown[]): string
39
+ /** Merge additional messages into a locale catalog at runtime */
40
+ loadMessages(locale: Locale, messages: Messages): void
41
+ /** Return all locale codes that have loaded messages */
42
+ getLocales(): Locale[]
43
+ /** Format a date value for the current locale */
44
+ d(value: Date | number, style?: string): string
45
+ /** Format a number value for the current locale */
46
+ n(value: number, style?: string): string
47
+ /** Format an ICU message string directly (no catalog lookup) */
48
+ format(message: string, values?: Record<string, unknown>): string
49
+ /** Whether a locale chunk is currently being loaded */
50
+ isLoading: Accessor<boolean>
51
+ /** Set of locales whose messages have been loaded */
52
+ loadedLocales: Accessor<Set<string>>
53
+ /** Preload a locale in the background without switching to it */
54
+ preloadLocale(locale: string): void
55
+ }
@@ -0,0 +1,30 @@
1
+ import { useContext } from 'solid-js'
2
+ import { I18nCtx } from './provider'
3
+ import { getGlobalI18nContext } from './context'
4
+ import type { I18nContext } from './context'
5
+
6
+ /**
7
+ * Access the i18n context.
8
+ *
9
+ * Resolution order:
10
+ * 1. Nearest `<I18nProvider>` in the component tree
11
+ * 2. Module-level singleton created by `createI18n()`
12
+ *
13
+ * Throws if neither is available.
14
+ */
15
+ export function useI18n(): I18nContext {
16
+ const ctx = useContext(I18nCtx)
17
+ if (ctx) {
18
+ return ctx
19
+ }
20
+
21
+ const global = getGlobalI18nContext()
22
+ if (global) {
23
+ return global
24
+ }
25
+
26
+ throw new Error(
27
+ 'useI18n requires either createI18n() to be called at startup, ' +
28
+ 'or the component to be inside an <I18nProvider>.',
29
+ )
30
+ }
@@ -0,0 +1,4 @@
1
+ declare module 'virtual:fluenti/runtime' {
2
+ export const __switchLocale: ((locale: string) => Promise<void>) | undefined
3
+ export const __preloadLocale: ((locale: string) => Promise<void>) | undefined
4
+ }