@rimelight/ui 0.0.2 → 0.0.4

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 (105) hide show
  1. package/package.json +6 -3
  2. package/src/components/carousel/carousel-script.ts +188 -188
  3. package/src/components/carousel/index.ts +1 -1
  4. package/src/components/input-otp/InputOtpTypes.ts +6 -6
  5. package/src/components/radio-group/RadioGroupTypes.ts +6 -6
  6. package/src/components/select/SelectTypes.ts +13 -13
  7. package/src/components/switch/SwitchTypes.ts +6 -6
  8. package/src/components/toast/index.ts +1 -1
  9. package/src/components/toast/toast-manager.ts +234 -234
  10. package/src/components/toggle/ToggleTypes.ts +14 -14
  11. package/src/config/cookies.ts +5 -5
  12. package/src/config/index.ts +3 -3
  13. package/src/config/links.ts +4 -4
  14. package/src/config/site.config.ts +27 -45
  15. package/src/domain/cms/index.ts +1 -1
  16. package/src/domain/cms/types.ts +20 -20
  17. package/src/domain/i18n/constants.ts +21 -21
  18. package/src/domain/i18n/cookie-storage.ts +13 -13
  19. package/src/domain/i18n/country-to-locale-map.ts +67 -67
  20. package/src/domain/i18n/fetcher.ts +278 -268
  21. package/src/domain/i18n/flags.ts +16 -16
  22. package/src/domain/i18n/format.ts +90 -90
  23. package/src/domain/i18n/index.ts +10 -10
  24. package/src/domain/i18n/middleware/i18n.ts +161 -160
  25. package/src/domain/i18n/resolve-locale.ts +55 -55
  26. package/src/domain/i18n/schema.ts +29 -29
  27. package/src/domain/index.ts +4 -4
  28. package/src/domain/seo/constants.ts +6 -20
  29. package/src/domain/seo/index.ts +3 -3
  30. package/src/domain/seo/services/index.ts +3 -3
  31. package/src/domain/seo/services/llms.ts +56 -53
  32. package/src/domain/seo/services/robots.ts +12 -12
  33. package/src/domain/seo/services/sitemap.ts +56 -56
  34. package/src/domain/seo/utils.ts +36 -37
  35. package/src/domain/theme/constants.ts +6 -6
  36. package/src/domain/theme/index.ts +2 -2
  37. package/src/domain/theme/schema.ts +10 -10
  38. package/src/env.d.ts +49 -49
  39. package/src/integrations/index.ts +1 -0
  40. package/src/integrations/sri.ts +92 -0
  41. package/src/lib/component-preview.ts +73 -73
  42. package/src/lib/index.ts +6 -6
  43. package/src/lib/remark/remark-modified-time.ts +13 -13
  44. package/src/lib/showcase-preview-elements.ts +408 -408
  45. package/src/lib/showcase-renderer.ts +218 -218
  46. package/src/lib/showcase-utils.ts +189 -189
  47. package/src/lib/turnstile.ts +35 -35
  48. package/src/lib/utils/starwind/positioning.ts +318 -318
  49. package/src/locales/de/blog.json +9 -9
  50. package/src/locales/de/common.json +19 -19
  51. package/src/locales/de/errors.json +6 -6
  52. package/src/locales/de/home.json +20 -20
  53. package/src/locales/de/messages.json +8 -8
  54. package/src/locales/de/nav.json +13 -13
  55. package/src/locales/de/projects.json +9 -9
  56. package/src/locales/de/services.json +43 -43
  57. package/src/locales/en/blog.json +9 -9
  58. package/src/locales/en/chat.json +29 -29
  59. package/src/locales/en/common.json +19 -19
  60. package/src/locales/en/cookie_notice.json +11 -11
  61. package/src/locales/en/editor.json +24 -24
  62. package/src/locales/en/errors.json +6 -6
  63. package/src/locales/en/home.json +20 -20
  64. package/src/locales/en/messages.json +8 -8
  65. package/src/locales/en/nav.json +13 -13
  66. package/src/locales/en/projects.json +9 -9
  67. package/src/locales/en/services.json +43 -43
  68. package/src/locales/es/blog.json +9 -9
  69. package/src/locales/es/common.json +19 -19
  70. package/src/locales/es/errors.json +6 -6
  71. package/src/locales/es/home.json +20 -20
  72. package/src/locales/es/messages.json +8 -8
  73. package/src/locales/es/nav.json +13 -13
  74. package/src/locales/es/projects.json +9 -9
  75. package/src/locales/es/services.json +43 -43
  76. package/src/locales/ja/blog.json +9 -9
  77. package/src/locales/ja/common.json +19 -19
  78. package/src/locales/ja/errors.json +6 -6
  79. package/src/locales/ja/home.json +20 -20
  80. package/src/locales/ja/messages.json +8 -8
  81. package/src/locales/ja/nav.json +13 -13
  82. package/src/locales/ja/projects.json +9 -9
  83. package/src/locales/ja/services.json +43 -43
  84. package/src/locales/pt/blog.json +9 -9
  85. package/src/locales/pt/chat.json +29 -29
  86. package/src/locales/pt/common.json +19 -19
  87. package/src/locales/pt/cookie_notice.json +11 -11
  88. package/src/locales/pt/editor.json +24 -24
  89. package/src/locales/pt/errors.json +6 -6
  90. package/src/locales/pt/home.json +20 -20
  91. package/src/locales/pt/messages.json +8 -8
  92. package/src/locales/pt/nav.json +13 -13
  93. package/src/locales/pt/projects.json +9 -9
  94. package/src/locales/pt/services.json +43 -43
  95. package/src/middleware/index.ts +2 -0
  96. package/src/middleware/security.ts +15 -0
  97. package/src/middleware/sri.ts +78 -0
  98. package/src/utils/index.ts +2 -2
  99. package/src/utils/server/cookies.ts +98 -98
  100. package/src/utils/server/index.ts +1 -1
  101. package/src/utils/shared/deep-merge.ts +36 -36
  102. package/src/utils/shared/embed.ts +105 -105
  103. package/src/utils/shared/index.ts +3 -3
  104. package/src/utils/shared/parse-boolean.ts +14 -14
  105. package/worker-configuration.d.ts +0 -14791
@@ -1,268 +1,278 @@
1
- import { SITE_CONFIG } from "../../config"
2
- import { TRANSLATIONS_VERSION } from "./runtime-constants"
3
- let env: any = {}
4
- try {
5
- // @ts-ignore
6
- const workers = await import(/* @vite-ignore */ "cloudflare:workers")
7
- env = workers.env
8
- } catch {
9
- // Safe fallback for Node.js environments (Astro config, local scripts)
10
- env = {}
11
- }
12
- import type { Locale, Namespace, PickSchema } from "./schema"
13
- import { deepMerge } from "../../utils/shared/deep-merge"
14
- import { parseBooleanFlag } from "../../utils/shared/parse-boolean"
15
-
16
- // Optional fallbacks (auto-loaded if generated)
17
- let FALLBACKS: Record<string, unknown> | null = null
18
-
19
- try {
20
- FALLBACKS = await import("./fallbacks.generated.ts")
21
- } catch {
22
- FALLBACKS = null
23
- }
24
-
25
- const KV_JSON_OPTIONS = { type: "json" as const, cacheTtl: 3600 }
26
-
27
- function assertRecord(obj: unknown): asserts obj is Record<string, unknown> {
28
- if (!obj || typeof obj !== "object") throw new Error("Expected record")
29
- }
30
-
31
- function assertDefaultCache(obj: unknown): asserts obj is {
32
- match: (r: Request) => Promise<Response | undefined>
33
- put: (r: Request, res: Response) => Promise<void>
34
- } {
35
- if (!obj || typeof obj !== "object") throw new Error("Expected cache")
36
- }
37
-
38
- function assertSchema<N extends Namespace>(obj: unknown): asserts obj is PickSchema<N> {
39
- if (!obj) throw new Error("Expected schema")
40
- }
41
-
42
- function assertA<N extends Namespace>(obj: unknown): asserts obj is PickSchema<N> {
43
- if (!obj) throw new Error("Expected result")
44
- }
45
-
46
- /**
47
- * Runtime object provided by Astro via Astro.locals.
48
- */
49
- export interface Runtime {
50
- cf?: unknown
51
- cfContext: { waitUntil: (p: Promise<unknown>) => void }
52
- }
53
-
54
- /**
55
- * Fetch translations from KV for a given locale and list of namespaces.
56
- *
57
- * Responsibilities:
58
- *
59
- * - Build KV keys using SITE_CONFIG.id, namespace and locale
60
- * - Optionally use edge cache (controlled by env flag I18N_CACHE)
61
- * - On cache miss, read from KV and merge with optional FALLBACK_* dictionaries
62
- * - Persist the merged payload back to the edge cache
63
- * - If KV is unavailable, fall back to static FALLBACK_* dictionaries only
64
- */
65
- export async function fetchTranslations<N extends Namespace>(
66
- runtime: Runtime,
67
- lang: Locale,
68
- namespaces: readonly N[]
69
- ): Promise<PickSchema<N>> {
70
- const useCache = isCacheEnabled(env)
71
-
72
- if (!useCache) {
73
- debug(env, "i18n cache DISABLED, using KV only", { lang, namespaces })
74
- }
75
-
76
- const { cache, waitUntil } = useCache ? getEdgeCache(runtime) : { cache: null, waitUntil: null }
77
-
78
- const keys = namespaces.map((ns) => `${SITE_CONFIG.id}:${ns}:${lang}`)
79
-
80
- const { cacheRequest } = buildCacheKey(lang, namespaces)
81
-
82
- // 1. Try to serve from edge cache if available and enabled
83
- if (useCache && cache) {
84
- try {
85
- const cached = await cache.match(cacheRequest)
86
-
87
- if (cached) {
88
- const json = await cached.json()
89
- assertSchema<N>(json)
90
- debug(env, "i18n cache HIT", { lang, namespaces })
91
- return json
92
- }
93
- debug(env, "i18n cache MISS (no entry)", { lang, namespaces })
94
- } catch (error) {
95
- debug(env, "i18n cache READ error, falling back to KV", error)
96
- }
97
- }
98
-
99
- // 2. Cache is missing, disabled, or unavailable → read from KV
100
- try {
101
- const data = await loadTranslationsFromKV(env, keys, namespaces)
102
- debug(env, "i18n KV OK", { lang, namespaces })
103
-
104
- // 3. Store the resulting payload in the edge cache asynchronously (if enabled)
105
- if (useCache && cache && waitUntil) {
106
- try {
107
- const response = new Response(JSON.stringify(data), {
108
- headers: {
109
- "Content-Type": "application/json; charset=utf-8",
110
- "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400"
111
- }
112
- })
113
-
114
- const putPromise = cache.put(cacheRequest, response.clone())
115
-
116
- // In real Workers: schedule as a background task.
117
- // In dev / non-Workers: just await so cache is actually written.
118
- if (waitUntil) {
119
- waitUntil(putPromise)
120
- } else {
121
- await putPromise
122
- }
123
-
124
- debug(env, "i18n cache PUT completed", { lang, namespaces })
125
- } catch (error) {
126
- debug(env, "i18n cache WRITE error", error)
127
- }
128
- }
129
-
130
- return data
131
- } catch (error) {
132
- debug(env, "i18n KV ERROR, using FALLBACK_* only", error)
133
-
134
- // 4. Hard fallback to static FALLBACK_* constants if KV is unavailable
135
- return buildFallbackOnly(namespaces)
136
- }
137
- }
138
-
139
- /**
140
- * Narrow runtime.caches and runtime.ctx to the minimal shape needed for using the Cache API and
141
- * waitUntil in this module.
142
- */
143
- function getEdgeCache(runtime: Runtime): {
144
- cache: {
145
- match: (r: Request) => Promise<Response | undefined>
146
- put: (r: Request, res: Response) => Promise<void>
147
- } | null
148
- waitUntil: ((p: Promise<unknown>) => void) | null
149
- } {
150
- return {
151
- cache: getCacheFromStorage(),
152
- waitUntil: runtime.cfContext?.waitUntil ?? null
153
- }
154
- }
155
-
156
- function getCacheFromStorage(): {
157
- match: (r: Request) => Promise<Response | undefined>
158
- put: (r: Request, res: Response) => Promise<void>
159
- } | null {
160
- const c = Reflect.get(Object(caches), "default")
161
- try {
162
- assertDefaultCache(c)
163
- return c
164
- } catch {
165
- return null
166
- }
167
- }
168
-
169
- /**
170
- * Build a stable cache key and synthetic Request object used with the Cache API. The URL origin is
171
- * irrelevant; only the string identity matters.
172
- */
173
- function buildCacheKey<N extends Namespace>(
174
- lang: Locale,
175
- namespaces: readonly N[]
176
- ): { cacheId: string; cacheRequest: Request } {
177
- const cacheId = `${SITE_CONFIG.id}:i18n:v${TRANSLATIONS_VERSION}:${lang}:${namespaces.join(",")}`
178
-
179
- const cacheRequest = new Request(`https://i18n-cache/${encodeURIComponent(cacheId)}`)
180
-
181
- return { cacheId, cacheRequest }
182
- }
183
-
184
- /**
185
- * Read bulk translations from KV and merge with optional FALLBACK_* dictionaries. This is the main
186
- * "happy-path" loader.
187
- */
188
- async function loadTranslationsFromKV<N extends Namespace>(
189
- environment: Env,
190
- keys: string[],
191
- namespaces: readonly N[]
192
- ): Promise<PickSchema<N>> {
193
- const resultMap = await environment.TRANSLATIONS.get(keys, KV_JSON_OPTIONS)
194
-
195
- const data: Partial<PickSchema<N>> = {}
196
-
197
- for (let i = 0; i < namespaces.length; i++) {
198
- const ns = namespaces[i]!
199
- const key = keys[i]!
200
- const value = resultMap.get(key) ?? {}
201
- assertRecord(value)
202
-
203
- const fallbackConstName = `FALLBACK_${String(ns).toUpperCase()}`
204
- const fallback = FALLBACKS?.[fallbackConstName]
205
-
206
- if (fallback) {
207
- assertRecord(fallback)
208
- Reflect.set(data, ns, deepMerge(fallback, value))
209
- } else {
210
- Reflect.set(data, ns, value)
211
- }
212
- }
213
-
214
- assertA<N>(data)
215
- return data
216
- }
217
-
218
- /**
219
- * Build a payload using only FALLBACK_* dictionaries. Used when KV is unavailable or throws
220
- * unexpectedly.
221
- */
222
- function buildFallbackOnly<N extends Namespace>(namespaces: readonly N[]): PickSchema<N> {
223
- const empty: Partial<PickSchema<N>> = {}
224
-
225
- for (let i = 0; i < namespaces.length; i++) {
226
- const ns = namespaces[i]!
227
- const fallbackConstName = `FALLBACK_${String(ns).toUpperCase()}`
228
- const fallback = FALLBACKS?.[fallbackConstName]
229
-
230
- ;(empty as Record<string, unknown>)[ns] = fallback ?? {}
231
- }
232
-
233
- assertSchema<N>(empty)
234
- return empty
235
- }
236
-
237
- /**
238
- * Determine whether edge caching is enabled.
239
- *
240
- * Wrangler var: I18N_CACHE = "on" | "off" | "true" | "false" | "1" | "0"
241
- *
242
- * After adding the variable:
243
- *
244
- * - Update wrangler.jsonc / wrangler.toml or .dev.vars
245
- * - Run: npm run typegen
246
- */
247
- function isCacheEnabled(environment: Env): boolean {
248
- // env.I18N_CACHE is generated by `wrangler types`
249
- return parseBooleanFlag(environment.I18N_CACHE, /* defaultValue */ true)
250
- }
251
-
252
- /**
253
- * Debug logger controlled by a Wrangler variable.
254
- *
255
- * Wrangler var: DEBUG_I18N = "on" | "off" | "true" | "false" | "1" | "0"
256
- *
257
- * After adding the variable:
258
- *
259
- * - Update wrangler.jsonc / wrangler.toml or .dev.vars
260
- * - Run: npm run typegen
261
- */
262
- function debug(environment: Env, ...args: unknown[]) {
263
- const enabled = parseBooleanFlag(environment.DEBUG_I18N, /* defaultValue */ false)
264
- if (!enabled) return
265
-
266
- // eslint-disable-next-line no-console
267
- console.log(...args)
268
- }
1
+ import { TRANSLATIONS_VERSION } from "./runtime-constants"
2
+ let env: any = {}
3
+ try {
4
+ // @ts-ignore
5
+ const workers = await import(/* @vite-ignore */ "cloudflare:workers")
6
+ env = workers.env
7
+ } catch {
8
+ // Safe fallback for Node.js environments (Astro config, local scripts)
9
+ env = {}
10
+ }
11
+ import type { Locale, Namespace, PickSchema } from "./schema"
12
+ import { deepMerge } from "../../utils/shared/deep-merge"
13
+ import { parseBooleanFlag } from "../../utils/shared/parse-boolean"
14
+
15
+ /**
16
+ * Minimal environment interface for i18n operations. Usually provided by Cloudflare Workers.
17
+ */
18
+ interface Env {
19
+ TRANSLATIONS: {
20
+ get: (keys: string[], options: { type: "json"; cacheTtl?: number }) => Promise<Map<string, any>>
21
+ }
22
+ I18N_CACHE?: string
23
+ DEBUG_I18N?: string
24
+ }
25
+
26
+ // Optional fallbacks (auto-loaded if generated)
27
+ let FALLBACKS: Record<string, unknown> | null = null
28
+
29
+ try {
30
+ FALLBACKS = await import("./fallbacks.generated.ts")
31
+ } catch {
32
+ FALLBACKS = null
33
+ }
34
+
35
+ const KV_JSON_OPTIONS = { type: "json" as const, cacheTtl: 3600 }
36
+
37
+ function assertRecord(obj: unknown): asserts obj is Record<string, unknown> {
38
+ if (!obj || typeof obj !== "object") throw new Error("Expected record")
39
+ }
40
+
41
+ function assertDefaultCache(obj: unknown): asserts obj is {
42
+ match: (r: Request) => Promise<Response | undefined>
43
+ put: (r: Request, res: Response) => Promise<void>
44
+ } {
45
+ if (!obj || typeof obj !== "object") throw new Error("Expected cache")
46
+ }
47
+
48
+ function assertSchema<N extends Namespace>(obj: unknown): asserts obj is PickSchema<N> {
49
+ if (!obj) throw new Error("Expected schema")
50
+ }
51
+
52
+ function assertA<N extends Namespace>(obj: unknown): asserts obj is PickSchema<N> {
53
+ if (!obj) throw new Error("Expected result")
54
+ }
55
+
56
+ /**
57
+ * Runtime object provided by Astro via Astro.locals.
58
+ */
59
+ export interface Runtime {
60
+ cf?: unknown
61
+ cfContext: { waitUntil: (p: Promise<unknown>) => void }
62
+ }
63
+
64
+ /**
65
+ * Fetch translations from KV for a given locale and list of namespaces.
66
+ *
67
+ * Responsibilities:
68
+ *
69
+ * - Build KV keys using namespace and locale (isolated by KV namespace binding)
70
+ * - Optionally use edge cache (controlled by env flag I18N_CACHE)
71
+ * - On cache miss, read from KV and merge with optional FALLBACK_* dictionaries
72
+ * - Persist the merged payload back to the edge cache
73
+ * - If KV is unavailable, fall back to static FALLBACK_* dictionaries only
74
+ */
75
+ export async function fetchTranslations<N extends Namespace>(
76
+ runtime: Runtime,
77
+ lang: Locale,
78
+ namespaces: readonly N[]
79
+ ): Promise<PickSchema<N>> {
80
+ const useCache = isCacheEnabled(env)
81
+
82
+ if (!useCache) {
83
+ debug(env, "i18n cache DISABLED, using KV only", { lang, namespaces })
84
+ }
85
+
86
+ const { cache, waitUntil } = useCache ? getEdgeCache(runtime) : { cache: null, waitUntil: null }
87
+
88
+ const keys = namespaces.map((ns) => `${ns}:${lang}`)
89
+
90
+ const { cacheRequest } = buildCacheKey(lang, namespaces)
91
+
92
+ // 1. Try to serve from edge cache if available and enabled
93
+ if (useCache && cache) {
94
+ try {
95
+ const cached = await cache.match(cacheRequest)
96
+
97
+ if (cached) {
98
+ const json = await cached.json()
99
+ assertSchema<N>(json)
100
+ debug(env, "i18n cache HIT", { lang, namespaces })
101
+ return json
102
+ }
103
+ debug(env, "i18n cache MISS (no entry)", { lang, namespaces })
104
+ } catch (error) {
105
+ debug(env, "i18n cache READ error, falling back to KV", error)
106
+ }
107
+ }
108
+
109
+ // 2. Cache is missing, disabled, or unavailable → read from KV
110
+ try {
111
+ const data = await loadTranslationsFromKV(env, keys, namespaces)
112
+ debug(env, "i18n KV OK", { lang, namespaces })
113
+
114
+ // 3. Store the resulting payload in the edge cache asynchronously (if enabled)
115
+ if (useCache && cache && waitUntil) {
116
+ try {
117
+ const response = new Response(JSON.stringify(data), {
118
+ headers: {
119
+ "Content-Type": "application/json; charset=utf-8",
120
+ "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400"
121
+ }
122
+ })
123
+
124
+ const putPromise = cache.put(cacheRequest, response.clone())
125
+
126
+ // In real Workers: schedule as a background task.
127
+ // In dev / non-Workers: just await so cache is actually written.
128
+ if (waitUntil) {
129
+ waitUntil(putPromise)
130
+ } else {
131
+ await putPromise
132
+ }
133
+
134
+ debug(env, "i18n cache PUT completed", { lang, namespaces })
135
+ } catch (error) {
136
+ debug(env, "i18n cache WRITE error", error)
137
+ }
138
+ }
139
+
140
+ return data
141
+ } catch (error) {
142
+ debug(env, "i18n KV ERROR, using FALLBACK_* only", error)
143
+
144
+ // 4. Hard fallback to static FALLBACK_* constants if KV is unavailable
145
+ return buildFallbackOnly(namespaces)
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Narrow runtime.caches and runtime.ctx to the minimal shape needed for using the Cache API and
151
+ * waitUntil in this module.
152
+ */
153
+ function getEdgeCache(runtime: Runtime): {
154
+ cache: {
155
+ match: (r: Request) => Promise<Response | undefined>
156
+ put: (r: Request, res: Response) => Promise<void>
157
+ } | null
158
+ waitUntil: ((p: Promise<unknown>) => void) | null
159
+ } {
160
+ return {
161
+ cache: getCacheFromStorage(),
162
+ waitUntil: runtime.cfContext?.waitUntil ?? null
163
+ }
164
+ }
165
+
166
+ function getCacheFromStorage(): {
167
+ match: (r: Request) => Promise<Response | undefined>
168
+ put: (r: Request, res: Response) => Promise<void>
169
+ } | null {
170
+ const c = Reflect.get(Object(caches), "default")
171
+ try {
172
+ assertDefaultCache(c)
173
+ return c
174
+ } catch {
175
+ return null
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Build a stable cache key and synthetic Request object used with the Cache API. The URL origin is
181
+ * irrelevant; only the string identity matters.
182
+ */
183
+ function buildCacheKey<N extends Namespace>(
184
+ lang: Locale,
185
+ namespaces: readonly N[]
186
+ ): { cacheId: string; cacheRequest: Request } {
187
+ const cacheId = `i18n:v${TRANSLATIONS_VERSION}:${lang}:${namespaces.join(",")}`
188
+
189
+ const cacheRequest = new Request(`https://i18n-cache/${encodeURIComponent(cacheId)}`)
190
+
191
+ return { cacheId, cacheRequest }
192
+ }
193
+
194
+ /**
195
+ * Read bulk translations from KV and merge with optional FALLBACK_* dictionaries. This is the main
196
+ * "happy-path" loader.
197
+ */
198
+ async function loadTranslationsFromKV<N extends Namespace>(
199
+ environment: Env,
200
+ keys: string[],
201
+ namespaces: readonly N[]
202
+ ): Promise<PickSchema<N>> {
203
+ const resultMap = await environment.TRANSLATIONS.get(keys, KV_JSON_OPTIONS)
204
+
205
+ const data: Partial<PickSchema<N>> = {}
206
+
207
+ for (let i = 0; i < namespaces.length; i++) {
208
+ const ns = namespaces[i]!
209
+ const key = keys[i]!
210
+ const value = resultMap.get(key) ?? {}
211
+ assertRecord(value)
212
+
213
+ const fallbackConstName = `FALLBACK_${String(ns).toUpperCase()}`
214
+ const fallback = FALLBACKS?.[fallbackConstName]
215
+
216
+ if (fallback) {
217
+ assertRecord(fallback)
218
+ Reflect.set(data, ns, deepMerge(fallback, value))
219
+ } else {
220
+ Reflect.set(data, ns, value)
221
+ }
222
+ }
223
+
224
+ assertA<N>(data)
225
+ return data
226
+ }
227
+
228
+ /**
229
+ * Build a payload using only FALLBACK_* dictionaries. Used when KV is unavailable or throws
230
+ * unexpectedly.
231
+ */
232
+ function buildFallbackOnly<N extends Namespace>(namespaces: readonly N[]): PickSchema<N> {
233
+ const empty: Partial<PickSchema<N>> = {}
234
+
235
+ for (let i = 0; i < namespaces.length; i++) {
236
+ const ns = namespaces[i]!
237
+ const fallbackConstName = `FALLBACK_${String(ns).toUpperCase()}`
238
+ const fallback = FALLBACKS?.[fallbackConstName]
239
+
240
+ ;(empty as Record<string, unknown>)[ns] = fallback ?? {}
241
+ }
242
+
243
+ assertSchema<N>(empty)
244
+ return empty
245
+ }
246
+
247
+ /**
248
+ * Determine whether edge caching is enabled.
249
+ *
250
+ * Wrangler var: I18N_CACHE = "on" | "off" | "true" | "false" | "1" | "0"
251
+ *
252
+ * After adding the variable:
253
+ *
254
+ * - Update wrangler.jsonc / wrangler.toml or .dev.vars
255
+ * - Run: npm run typegen
256
+ */
257
+ function isCacheEnabled(environment: Env): boolean {
258
+ // env.I18N_CACHE is generated by `wrangler types`
259
+ return parseBooleanFlag(environment.I18N_CACHE, /* defaultValue */ true)
260
+ }
261
+
262
+ /**
263
+ * Debug logger controlled by a Wrangler variable.
264
+ *
265
+ * Wrangler var: DEBUG_I18N = "on" | "off" | "true" | "false" | "1" | "0"
266
+ *
267
+ * After adding the variable:
268
+ *
269
+ * - Update wrangler.jsonc / wrangler.toml or .dev.vars
270
+ * - Run: npm run typegen
271
+ */
272
+ function debug(environment: Env, ...args: unknown[]) {
273
+ const enabled = parseBooleanFlag(environment.DEBUG_I18N, /* defaultValue */ false)
274
+ if (!enabled) return
275
+
276
+ // eslint-disable-next-line no-console
277
+ console.log(...args)
278
+ }
@@ -1,16 +1,16 @@
1
- import type { Locale } from "./schema"
2
-
3
- // Explicit locale → flag mapping
4
- export const FLAG_CODES = {
5
- en: "us-4x3",
6
- ja: "jp-4x3",
7
- de: "de-4x3",
8
- es: "es-4x3",
9
- pt: "br-4x3"
10
- } as const
11
-
12
- export type FlagCode = (typeof FLAG_CODES)[keyof typeof FLAG_CODES]
13
-
14
- export const localeToFlagCode = (locale: Locale): FlagCode | null => {
15
- return locale in FLAG_CODES ? FLAG_CODES[locale] : null
16
- }
1
+ import type { Locale } from "./schema"
2
+
3
+ // Explicit locale → flag mapping
4
+ export const FLAG_CODES = {
5
+ en: "us-4x3",
6
+ ja: "jp-4x3",
7
+ de: "de-4x3",
8
+ es: "es-4x3",
9
+ pt: "br-4x3"
10
+ } as const
11
+
12
+ export type FlagCode = (typeof FLAG_CODES)[keyof typeof FLAG_CODES]
13
+
14
+ export const localeToFlagCode = (locale: Locale): FlagCode | null => {
15
+ return locale in FLAG_CODES ? FLAG_CODES[locale] : null
16
+ }