@fluenti/solid 0.1.2 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluenti/solid",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "SolidJS compile-time i18n — Trans/Plural/Select components, I18nProvider, useI18n",
6
6
  "homepage": "https://fluenti.dev",
@@ -43,6 +43,16 @@
43
43
  "types": "./dist/index.d.ts",
44
44
  "default": "./dist/index.cjs"
45
45
  }
46
+ },
47
+ "./vite-plugin": {
48
+ "import": {
49
+ "types": "./dist/vite-plugin.d.ts",
50
+ "default": "./dist/vite-plugin.js"
51
+ },
52
+ "require": {
53
+ "types": "./dist/vite-plugin.d.ts",
54
+ "default": "./dist/vite-plugin.cjs"
55
+ }
46
56
  }
47
57
  },
48
58
  "files": [
@@ -50,10 +60,17 @@
50
60
  "src"
51
61
  ],
52
62
  "peerDependencies": {
53
- "solid-js": "^1.8"
63
+ "solid-js": "^1.8",
64
+ "vite": "^5 || ^6 || ^8"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "vite": {
68
+ "optional": true
69
+ }
54
70
  },
55
71
  "dependencies": {
56
- "@fluenti/core": "0.1.2"
72
+ "@fluenti/core": "0.2.0",
73
+ "@fluenti/vite-plugin": "0.2.0"
57
74
  },
58
75
  "devDependencies": {
59
76
  "@solidjs/testing-library": "^0.8",
package/src/context.ts CHANGED
@@ -223,6 +223,7 @@ export function createI18nContext(config: FluentConfig | I18nConfig): I18nContex
223
223
  }
224
224
 
225
225
  const loadMessages = (loc: Locale, msgs: Messages): void => {
226
+ // Intentional mutation: messages record is locally scoped to this context closure
226
227
  messages[loc] = { ...messages[loc], ...msgs }
227
228
  loadedLocalesSet.add(loc)
228
229
  setLoadedLocales(new Set(loadedLocalesSet))
@@ -247,6 +248,7 @@ export function createI18nContext(config: FluentConfig | I18nConfig): I18nContex
247
248
  setIsLoading(true)
248
249
  try {
249
250
  const loaded = resolveChunkMessages(await i18nConfig.chunkLoader(newLocale))
251
+ // Intentional mutation: messages record is locally scoped to this context closure
250
252
  messages[newLocale] = { ...messages[newLocale], ...loaded }
251
253
  loadedLocalesSet.add(newLocale)
252
254
  setLoadedLocales(new Set(loadedLocalesSet))
@@ -264,14 +266,15 @@ export function createI18nContext(config: FluentConfig | I18nConfig): I18nContex
264
266
  const splitRuntime = getSplitRuntimeModule()
265
267
  i18nConfig.chunkLoader(loc).then(async (loaded) => {
266
268
  const resolved = resolveChunkMessages(loaded)
269
+ // Intentional mutation: messages record is locally scoped to this context closure
267
270
  messages[loc] = { ...messages[loc], ...resolved }
268
271
  loadedLocalesSet.add(loc)
269
272
  setLoadedLocales(new Set(loadedLocalesSet))
270
273
  if (splitRuntime?.__preloadLocale) {
271
274
  await splitRuntime.__preloadLocale(loc)
272
275
  }
273
- }).catch(() => {
274
- // Silent failure for preload
276
+ }).catch((e: unknown) => {
277
+ console.warn('[fluenti] preload failed:', loc, e)
275
278
  })
276
279
  }
277
280
 
@@ -314,8 +317,6 @@ export function createI18n(config: FluentConfig | I18nConfig): I18nContext {
314
317
  '[fluenti] createI18n() detected SSR environment. ' +
315
318
  'Use <I18nProvider> for per-request isolation in SSR.',
316
319
  )
317
- // Still set globalCtx as fallback, but document the risk
318
- globalCtx = ctx
319
320
  }
320
321
 
321
322
  return ctx
package/src/server.ts CHANGED
@@ -72,6 +72,13 @@ export interface ServerI18n {
72
72
  * your `resolveLocale` callback, or call `setLocale()` in your
73
73
  * entry-server middleware.
74
74
  *
75
+ * **⚠️ SSR Concurrency Warning**: This function uses module-level state for locale
76
+ * and cached instance. In concurrent SSR environments (e.g. multiple simultaneous
77
+ * requests), this can cause cross-request locale leakage. For per-request isolation:
78
+ * - Use `getRequestEvent()` in SolidStart to scope locale per request
79
+ * - Or create a separate `createServerI18n()` per request context
80
+ * - Consider using AsyncLocalStorage for true per-request isolation (future)
81
+ *
75
82
  * @example
76
83
  * ```ts
77
84
  * // lib/i18n.server.ts
@@ -90,6 +97,7 @@ export function createServerI18n(config: ServerI18nConfig): ServerI18n {
90
97
 
91
98
  function setLocale(locale: string): void {
92
99
  currentLocale = locale
100
+ cachedInstance = null
93
101
  }
94
102
 
95
103
  async function loadLocaleMessages(locale: string): Promise<Messages> {
@@ -0,0 +1,134 @@
1
+ import { resolve } from 'node:path'
2
+ import type { RuntimeGenerator, RuntimeGeneratorOptions } from '@fluenti/vite-plugin'
3
+
4
+ export const solidRuntimeGenerator: RuntimeGenerator = {
5
+ generateRuntime(options: RuntimeGeneratorOptions): string {
6
+ const { catalogDir, locales, sourceLocale, defaultBuildLocale } = options
7
+ const defaultLocale = defaultBuildLocale || sourceLocale
8
+ const absoluteCatalogDir = resolve(process.cwd(), catalogDir)
9
+ const runtimeKey = 'fluenti.runtime.solid'
10
+ const lazyLocales = locales.filter((locale) => locale !== defaultLocale)
11
+
12
+ return `
13
+ import { createSignal } from 'solid-js'
14
+ import { createStore, reconcile } from 'solid-js/store'
15
+ import __defaultMsgs from '${absoluteCatalogDir}/${defaultLocale}.js'
16
+
17
+ const [__catalog, __setCatalog] = createStore({ ...__defaultMsgs })
18
+ const [__currentLocale, __setCurrentLocale] = createSignal('${defaultLocale}')
19
+ const __loadedLocales = new Set(['${defaultLocale}'])
20
+ const [__loading, __setLoading] = createSignal(false)
21
+ const __cache = new Map()
22
+ const __normalizeMessages = (mod) => mod.default ?? mod
23
+
24
+ const __loaders = {
25
+ ${lazyLocales.map((l) => ` '${l}': () => import('${absoluteCatalogDir}/${l}.js'),`).join('\n')}
26
+ }
27
+
28
+ async function __switchLocale(locale) {
29
+ if (__loadedLocales.has(locale)) {
30
+ __setCatalog(reconcile(__cache.get(locale) || __defaultMsgs))
31
+ __setCurrentLocale(locale)
32
+ return
33
+ }
34
+ __setLoading(true)
35
+ try {
36
+ const mod = __normalizeMessages(await __loaders[locale]())
37
+ __cache.set(locale, mod)
38
+ __loadedLocales.add(locale)
39
+ __setCatalog(reconcile(mod))
40
+ __setCurrentLocale(locale)
41
+ } finally {
42
+ __setLoading(false)
43
+ }
44
+ }
45
+
46
+ async function __preloadLocale(locale) {
47
+ if (__loadedLocales.has(locale) || !__loaders[locale]) return
48
+ try {
49
+ const mod = __normalizeMessages(await __loaders[locale]())
50
+ __cache.set(locale, mod)
51
+ __loadedLocales.add(locale)
52
+ } catch (e) { console.warn('[fluenti] preload failed:', locale, e) }
53
+ }
54
+
55
+ globalThis[Symbol.for('${runtimeKey}')] = { __switchLocale, __preloadLocale }
56
+
57
+ export { __catalog, __switchLocale, __preloadLocale, __currentLocale, __loading, __loadedLocales }
58
+ `
59
+ },
60
+
61
+ generateRouteRuntime(options: RuntimeGeneratorOptions): string {
62
+ const { catalogDir, locales, sourceLocale, defaultBuildLocale } = options
63
+ const defaultLocale = defaultBuildLocale || sourceLocale
64
+ const absoluteCatalogDir = resolve(process.cwd(), catalogDir)
65
+ const runtimeKey = 'fluenti.runtime.solid'
66
+ const lazyLocales = locales.filter((locale) => locale !== defaultLocale)
67
+
68
+ return `
69
+ import { createSignal } from 'solid-js'
70
+ import { createStore, reconcile } from 'solid-js/store'
71
+ import __defaultMsgs from '${absoluteCatalogDir}/${defaultLocale}.js'
72
+
73
+ const [__catalog, __setCatalog] = createStore({ ...__defaultMsgs })
74
+ const [__currentLocale, __setCurrentLocale] = createSignal('${defaultLocale}')
75
+ const __loadedLocales = new Set(['${defaultLocale}'])
76
+ const [__loading, __setLoading] = createSignal(false)
77
+ const __cache = new Map()
78
+ const __loadedRoutes = new Set()
79
+ const __normalizeMessages = (mod) => mod.default ?? mod
80
+
81
+ const __loaders = {
82
+ ${lazyLocales.map((l) => ` '${l}': () => import('${absoluteCatalogDir}/${l}.js'),`).join('\n')}
83
+ }
84
+
85
+ const __routeLoaders = {}
86
+
87
+ function __registerRouteLoader(routeId, locale, loader) {
88
+ const key = routeId + ':' + locale
89
+ __routeLoaders[key] = loader
90
+ }
91
+
92
+ async function __loadRoute(routeId, locale) {
93
+ const key = routeId + ':' + (locale || __currentLocale())
94
+ if (__loadedRoutes.has(key)) return
95
+ const loader = __routeLoaders[key]
96
+ if (!loader) return
97
+ const mod = __normalizeMessages(await loader())
98
+ __setCatalog(reconcile({ ...__catalog, ...mod }))
99
+ __loadedRoutes.add(key)
100
+ }
101
+
102
+ async function __switchLocale(locale) {
103
+ if (locale === __currentLocale()) return
104
+ __setLoading(true)
105
+ try {
106
+ if (__cache.has(locale)) {
107
+ __setCatalog(reconcile(__cache.get(locale)))
108
+ } else {
109
+ const mod = __normalizeMessages(await __loaders[locale]())
110
+ __cache.set(locale, mod)
111
+ __setCatalog(reconcile(mod))
112
+ }
113
+ __loadedLocales.add(locale)
114
+ __setCurrentLocale(locale)
115
+ } finally {
116
+ __setLoading(false)
117
+ }
118
+ }
119
+
120
+ async function __preloadLocale(locale) {
121
+ if (__cache.has(locale) || !__loaders[locale]) return
122
+ try {
123
+ const mod = __normalizeMessages(await __loaders[locale]())
124
+ __cache.set(locale, mod)
125
+ __loadedLocales.add(locale)
126
+ } catch (e) { console.warn('[fluenti] preload failed:', locale, e) }
127
+ }
128
+
129
+ globalThis[Symbol.for('${runtimeKey}')] = { __switchLocale, __preloadLocale }
130
+
131
+ export { __catalog, __switchLocale, __preloadLocale, __loadRoute, __registerRouteLoader, __currentLocale, __loading, __loadedLocales }
132
+ `
133
+ },
134
+ }
@@ -0,0 +1,14 @@
1
+ import type { Plugin } from 'vite'
2
+ import type { FluentiPluginOptions } from '@fluenti/vite-plugin'
3
+ import { createFluentiPlugins } from '@fluenti/vite-plugin'
4
+ import { solidRuntimeGenerator } from './solid-runtime'
5
+
6
+ export type { FluentiPluginOptions as FluentiSolidOptions } from '@fluenti/vite-plugin'
7
+
8
+ export default function fluentiSolid(options?: FluentiPluginOptions): Plugin[] {
9
+ return createFluentiPlugins(
10
+ { ...options, framework: 'solid' },
11
+ [],
12
+ solidRuntimeGenerator,
13
+ )
14
+ }