better-translation 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.
package/README.md ADDED
@@ -0,0 +1,773 @@
1
+ # `better-translation`
2
+
3
+ Vite plugin and runtime helpers for extracting UI copy, generating locale JSON files, and rendering translations in React and server code.
4
+
5
+ It scans your source for translation markers, creates stable message ids, keeps locale JSON files in sync, and lets you plug in your own translation pipeline.
6
+
7
+ ## Features
8
+
9
+ - Extracts messages from function calls, React components, and tagged templates
10
+ - Generates stable message ids from the source text and optional context
11
+ - Writes locale JSON files for every configured locale
12
+ - Supports a custom async `translate()` function for auto-filling missing translations
13
+ - Caches translated results to avoid re-translating unchanged messages
14
+ - Includes React helpers for providers, hooks, and JSX interpolation
15
+ - Includes server helpers for loading messages and translating templates
16
+
17
+ ## Requirements
18
+
19
+ - `node >= 24`
20
+ - `vite >= 8`
21
+ - `react >= 19` if you use the React helpers from `better-translation/react`
22
+
23
+ ## Installation
24
+
25
+ Install the package:
26
+
27
+ ```bash
28
+ bun add better-translation
29
+ ```
30
+
31
+ ```bash
32
+ pnpm add better-translation
33
+ ```
34
+
35
+ ```bash
36
+ npm install better-translation
37
+ ```
38
+
39
+ ```bash
40
+ yarn add better-translation
41
+ ```
42
+
43
+ If you are using the React helpers, make sure `react` is installed in your app.
44
+
45
+ ## What It Does
46
+
47
+ At build time and during dev, the plugin:
48
+
49
+ 1. Scans all matching files under your configured roots for translation markers such as `t("...")`, `<T>...</T>`, and `msg("id")\`...\``.
50
+ 2. Extracts the default message, placeholders, source locations, and optional context.
51
+ 3. Generates a stable message id for each entry.
52
+ 4. Writes locale JSON files for every configured locale.
53
+ 5. In dev, it can call your custom `translate(messages, locale)` function for missing non-default translations.
54
+ 6. Stores translated results in a cache file so unchanged messages do not need to be translated again.
55
+
56
+ ## Quick Start
57
+
58
+ ```ts
59
+ import { defineConfig } from "vite"
60
+ import react from "@vitejs/plugin-react"
61
+ import { betterTranslate } from "better-translation/vite"
62
+
63
+ export default defineConfig({
64
+ plugins: [
65
+ betterTranslate({
66
+ locales: ["en", "nl", "fr", "es"],
67
+ defaultLocale: "en",
68
+ storage: { type: "bundle", output: "src/lib/bt" },
69
+ async translate(messages, locale) {
70
+ const result: Record<string, string> = {}
71
+
72
+ for (const message of messages) {
73
+ result[message.id] = await translateWithYourService({
74
+ text: message.text,
75
+ locale,
76
+ context: message.meta.context,
77
+ placeholders: message.placeholders,
78
+ })
79
+ }
80
+
81
+ return result
82
+ },
83
+ }),
84
+ react(),
85
+ ],
86
+ })
87
+ ```
88
+
89
+ With bundle storage enabled, the plugin writes files such as:
90
+
91
+ ```text
92
+ src/lib/bt/locales/en.json
93
+ src/lib/bt/locales/nl.json
94
+ src/lib/bt/locales/fr.json
95
+ src/lib/bt/locales/es.json
96
+ src/lib/bt/manifest.json
97
+ ```
98
+
99
+ ## Basic Configuration
100
+
101
+ ```ts
102
+ betterTranslate({
103
+ locales: ["en", "nl"],
104
+ defaultLocale: "en",
105
+ rootDir: "src",
106
+ cacheFile: ".cache/better-translation.json",
107
+ logging: true,
108
+ storage: {
109
+ type: "bundle",
110
+ output: "src/lib/bt",
111
+ },
112
+ })
113
+ ```
114
+
115
+ ### Options
116
+
117
+ #### `locales`
118
+
119
+ All locale codes the plugin should generate files for.
120
+
121
+ ```ts
122
+ locales: ["en", "nl", "fr"]
123
+ ```
124
+
125
+ #### `defaultLocale`
126
+
127
+ The source locale. Messages in this locale always use the original source text.
128
+
129
+ ```ts
130
+ defaultLocale: "en"
131
+ ```
132
+
133
+ #### `cacheFile`
134
+
135
+ Where translated results are cached between runs.
136
+
137
+ Default:
138
+
139
+ ```ts
140
+ ".cache/better-translation.json"
141
+ ```
142
+
143
+ #### `logging`
144
+
145
+ Enables plugin logging.
146
+
147
+ #### `rootDir`
148
+
149
+ Controls which source directory or directories the plugin looks in for messages.
150
+
151
+ ```ts
152
+ rootDir: "src"
153
+ ```
154
+
155
+ Default: `"src"`
156
+
157
+ You can also pass multiple directories:
158
+
159
+ ```ts
160
+ rootDir: ["src", "app"]
161
+ ```
162
+
163
+ #### `storage`
164
+
165
+ Controls where locale runtime data comes from.
166
+
167
+ ```ts
168
+ storage: {
169
+ type: "bundle",
170
+ output: "src/lib/bt",
171
+ }
172
+ ```
173
+
174
+ `bundle` uses editable locale JSON files from the configured directory and expects your server build to include that directory for runtime loading.
175
+
176
+ `remote` exists in the API, but remote sync and remote runtime fetching are currently stubs, so bundle storage is the recommended setup right now.
177
+
178
+ #### `translate`
179
+
180
+ Async callback for filling missing translations.
181
+
182
+ ```ts
183
+ type TranslateFn = (
184
+ messages: Array<{
185
+ id: string
186
+ text: string
187
+ meta: { context?: string }
188
+ placeholders: string[]
189
+ sources: Array<{
190
+ file: string
191
+ kind: "call" | "component" | "tagged-template"
192
+ marker: string
193
+ line: number
194
+ column: number
195
+ endLine: number
196
+ endColumn: number
197
+ start: number
198
+ end: number
199
+ }>
200
+ }>,
201
+ locale: string,
202
+ ) => Promise<Record<string, string>>
203
+ ```
204
+
205
+ Return a map keyed by `message.id`.
206
+
207
+ If a message id is missing from the returned object, the plugin falls back to the source text for that entry.
208
+
209
+ ## How To Translate Text
210
+
211
+ The plugin extracts three kinds of translation markers.
212
+
213
+ ### 1. Function Calls
214
+
215
+ Use this for labels, validation messages, errors, button text passed as props, and other non-JSX values.
216
+
217
+ ```tsx
218
+ import { useT } from "better-translation/react"
219
+
220
+ function SignInForm() {
221
+ const t = useT()
222
+
223
+ return (
224
+ <>
225
+ <input aria-label={t("Email")} />
226
+ <button>{t("Sign in")}</button>
227
+ <p>{t("Could not sign in", { context: "Authentication error toast" })}</p>
228
+ </>
229
+ )
230
+ }
231
+ ```
232
+
233
+ ### 2. `<T>` Component
234
+
235
+ Use this when the translated text lives directly in JSX.
236
+
237
+ ```tsx
238
+ import { T } from "better-translation/react"
239
+
240
+ export function Header() {
241
+ return (
242
+ <>
243
+ <h1>
244
+ <T>Sign in</T>
245
+ </h1>
246
+ <p>
247
+ <T context="Sign-in page helper copy">Enter your email and password to continue.</T>
248
+ </p>
249
+ </>
250
+ )
251
+ }
252
+ ```
253
+
254
+ For static `<T>` content, the plugin injects a stable hashed `id` at build time so runtime can skip re-hashing the source text.
255
+
256
+ You can also provide an explicit id yourself:
257
+
258
+ ```tsx
259
+ <T id="auth.sign-in.title">Sign in</T>
260
+ ```
261
+
262
+ ### 3. Tagged Templates on the Server
263
+
264
+ Use this for server-side template strings with placeholders.
265
+
266
+ ```ts
267
+ import { createTranslator, v } from "better-translation/server"
268
+
269
+ const { msg } = createTranslator(messages)
270
+
271
+ const subject = msg("invite-email-subject")`You were invited to ${v("organization", organization.name)}`
272
+ ```
273
+
274
+ The tagged template id is explicit, which is useful for emails and server-rendered content.
275
+
276
+ ## Passing Variables Into Translations
277
+
278
+ ### In React with `<Var>`
279
+
280
+ ```tsx
281
+ import { T, Var } from "better-translation/react"
282
+
283
+ function WelcomeMessage({ userName }: { userName: string }) {
284
+ return (
285
+ <T>
286
+ Welcome back, <Var userName={userName} />
287
+ </T>
288
+ )
289
+ }
290
+ ```
291
+
292
+ That extracts the default message:
293
+
294
+ ```text
295
+ Welcome back, {userName}
296
+ ```
297
+
298
+ For plain identifiers, the shorthand `<Var>{userName}</Var>` also works and is normalized at build time.
299
+
300
+ ### On the Server with `v()`
301
+
302
+ ```ts
303
+ import { createTranslator, v } from "better-translation/server"
304
+
305
+ const { msg } = createTranslator(messages)
306
+
307
+ const body = msg("welcome-email")`Welcome back, ${v("name", user.name)}`
308
+ ```
309
+
310
+ ## Loading Messages for a Locale
311
+
312
+ How you load messages depends on your deployment setup.
313
+
314
+ The first argument is a single locale code. You do not pass the whole `locales` array here.
315
+
316
+ The full list of supported locales belongs in the plugin config:
317
+
318
+ ```ts
319
+ betterTranslate({
320
+ locales: ["en", "nl", "fr"],
321
+ defaultLocale: "en",
322
+ storage: { type: "bundle", output: "src/lib/bt" },
323
+ })
324
+ ```
325
+
326
+ ### Server Runtime
327
+
328
+ The plugin generates a typed `load-messages.ts` next to your locale JSON files. Import it directly from your server code:
329
+
330
+ ```ts
331
+ import { loadMessages } from "@/lib/bt/load-messages"
332
+
333
+ const messages = await loadMessages("nl")
334
+ ```
335
+
336
+ `loadMessages` statically imports each locale JSON file, so bundlers tree-shake unused locales and runtime lookups stay cheap.
337
+
338
+ ### Client-Side Fetch From `public/` Or A CDN
339
+
340
+ If your app does not have a server runtime, or you want to load translations directly in the browser, fetch the locale JSON yourself and pass the result to `TranslateProvider`.
341
+
342
+ You do not need a special browser loader from this package. `TranslateProvider` only needs a flat `Record<string, string>`.
343
+
344
+ If you publish your locale files under `public/locales`, you can fetch them like this:
345
+
346
+ ```tsx
347
+ import { useEffect, useState } from "react"
348
+
349
+ import { TranslateProvider } from "better-translation/react"
350
+
351
+ export function App({ locale }: { locale: string }) {
352
+ const [messages, setMessages] = useState<Record<string, string> | null>(null)
353
+
354
+ useEffect(() => {
355
+ let cancelled = false
356
+
357
+ async function load() {
358
+ const response = await fetch(`/locales/${locale}.json`)
359
+ const nextMessages = (await response.json()) as Record<string, string>
360
+ if (!cancelled) setMessages(nextMessages)
361
+ }
362
+
363
+ void load()
364
+
365
+ return () => {
366
+ cancelled = true
367
+ }
368
+ }, [locale])
369
+
370
+ if (!messages) return null
371
+
372
+ return (
373
+ <TranslateProvider messages={messages}>
374
+ <Routes />
375
+ </TranslateProvider>
376
+ )
377
+ }
378
+ ```
379
+
380
+ If your locale files are hosted on a CDN, use the same pattern with an absolute URL:
381
+
382
+ ```ts
383
+ const response = await fetch(`https://cdn.example.com/locales/${locale}.json`)
384
+ const messages = (await response.json()) as Record<string, string>
385
+ ```
386
+
387
+ This browser-fetch approach also works in full-stack apps when you prefer serving locale files as static assets instead of loading them on the server.
388
+
389
+ ## Using The React Components And Hooks
390
+
391
+ ### `TranslateProvider`
392
+
393
+ Wrap the part of your app that needs translations.
394
+
395
+ ```tsx
396
+ import { TranslateProvider } from "better-translation/react"
397
+
398
+ export function App({ messages }: { messages: Record<string, string> }) {
399
+ return (
400
+ <TranslateProvider messages={messages}>
401
+ <Routes />
402
+ </TranslateProvider>
403
+ )
404
+ }
405
+ ```
406
+
407
+ ### `useT()`
408
+
409
+ Returns a translation function for non-JSX values.
410
+
411
+ ```tsx
412
+ import { useT } from "better-translation/react"
413
+
414
+ function SubmitButton() {
415
+ const t = useT()
416
+ return <button>{t("Save changes")}</button>
417
+ }
418
+ ```
419
+
420
+ ### `useMessages()`
421
+
422
+ Returns the raw flattened message map from the current provider.
423
+
424
+ ```tsx
425
+ import { useMessages } from "better-translation/react"
426
+
427
+ function DebugMessages() {
428
+ const messages = useMessages()
429
+ return <pre>{JSON.stringify(messages, null, 2)}</pre>
430
+ }
431
+ ```
432
+
433
+ ### `T`
434
+
435
+ Renders translated JSX content.
436
+
437
+ ```tsx
438
+ import { T } from "better-translation/react"
439
+
440
+ function EmptyState() {
441
+ return <T>No projects yet</T>
442
+ }
443
+ ```
444
+
445
+ ### `Var`
446
+
447
+ Marks placeholder content inside `T`.
448
+
449
+ ```tsx
450
+ import { T, Var } from "better-translation/react"
451
+
452
+ function InviteMessage({ count }: { count: number }) {
453
+ return (
454
+ <T>
455
+ You have <Var count={count} /> pending invites
456
+ </T>
457
+ )
458
+ }
459
+ ```
460
+
461
+ ## Custom Translation Function
462
+
463
+ The plugin calls your `translate(messages, locale)` callback only in dev, and only for missing translations in non-default locales.
464
+
465
+ Each message includes:
466
+
467
+ - `id`: stable key for the locale file
468
+ - `text`: source-language text
469
+ - `meta.context`: optional translator context
470
+ - `placeholders`: placeholder names such as `["name"]`
471
+ - `sources`: source file and location metadata
472
+
473
+ Example using your own API:
474
+
475
+ ```ts
476
+ import { betterTranslate } from "better-translation/vite"
477
+
478
+ export default {
479
+ plugins: [
480
+ betterTranslate({
481
+ locales: ["en", "nl"],
482
+ defaultLocale: "en",
483
+ storage: { type: "bundle", output: "src/lib/bt" },
484
+ async translate(messages, locale) {
485
+ const response = await fetch("https://your-translator.example.com/translate", {
486
+ method: "POST",
487
+ headers: { "Content-Type": "application/json" },
488
+ body: JSON.stringify({
489
+ locale,
490
+ messages: messages.map((message) => ({
491
+ id: message.id,
492
+ text: message.text,
493
+ context: message.meta.context,
494
+ placeholders: message.placeholders,
495
+ })),
496
+ }),
497
+ })
498
+
499
+ const data = (await response.json()) as { translations: Record<string, string> }
500
+ return data.translations
501
+ },
502
+ }),
503
+ ],
504
+ }
505
+ ```
506
+
507
+ Guidelines for a good custom translator:
508
+
509
+ - Preserve placeholders exactly, such as `{name}`.
510
+ - Use `message.meta.context` when tone or meaning is ambiguous.
511
+ - Return translations keyed by `message.id`.
512
+ - Return plain strings only.
513
+ - Keep translations deterministic when possible so the cache stays useful.
514
+
515
+ For `storage: { type: "bundle" }`, production builds are check-only. They never call `translate()` and never regenerate locale artifacts. Instead, they validate the committed locale JSON files and committed generated helper files, then fail the build if anything is missing or out of sync.
516
+
517
+ ## Server-Side Helpers
518
+
519
+ ### `loadMessages()`
520
+
521
+ The plugin generates `load-messages.ts` next to your locale JSON files. Import it directly:
522
+
523
+ ```ts
524
+ import { loadMessages } from "@/lib/bt/load-messages"
525
+
526
+ const messages = await loadMessages("en")
527
+ ```
528
+
529
+ It statically imports each locale JSON file and returns the flattened message map.
530
+
531
+ ### `createTranslator()`
532
+
533
+ Creates lightweight server helpers:
534
+
535
+ ```ts
536
+ import { createTranslator } from "better-translation/server"
537
+
538
+ const { t, msg } = createTranslator(messages)
539
+ ```
540
+
541
+ Use `t()` for plain strings:
542
+
543
+ ```ts
544
+ const errorMessage = t("Could not sign in")
545
+ ```
546
+
547
+ Use `msg()` for template messages with placeholders:
548
+
549
+ ```ts
550
+ const sentence = msg("account-invite")`You were invited to ${v("organization", organization.name)}`
551
+ ```
552
+
553
+ ### `v()`
554
+
555
+ Marks placeholder values for `msg()`:
556
+
557
+ ```ts
558
+ import { v } from "better-translation/server"
559
+
560
+ v("name", user.name)
561
+ ```
562
+
563
+ ## Locale File Shape
564
+
565
+ With bundle storage, each runtime locale file is a flat message map:
566
+
567
+ ```json
568
+ {
569
+ "m_hd339n": "Inloggen"
570
+ }
571
+ ```
572
+
573
+ It also keeps a private metadata manifest at `locales/manifest.json`:
574
+
575
+ ```json
576
+ {
577
+ "m_hd339n": {
578
+ "defaultMessage": "Sign in",
579
+ "meta": {
580
+ "context": "The main login page header"
581
+ },
582
+ "placeholders": [],
583
+ "sources": [
584
+ {
585
+ "file": "src/routes/sign-in.tsx",
586
+ "kind": "component",
587
+ "marker": "T",
588
+ "line": 12,
589
+ "column": 5,
590
+ "endLine": 12,
591
+ "endColumn": 30,
592
+ "start": 123,
593
+ "end": 148
594
+ }
595
+ ]
596
+ }
597
+ }
598
+ ```
599
+
600
+ For bundle storage, the plugin also writes runtime metadata at `src/lib/bt/runtime.json` and a generated `load-messages.ts` for consuming locales on the server.
601
+
602
+ ## Important Notes
603
+
604
+ - `t()` only extracts static string literals.
605
+ - `<T>` only extracts static text plus `<Var someName={value} />` placeholders or `<Var>{identifier}</Var>` shorthand.
606
+ - `msg("id")\`...\``only extracts templates that use`v("name", value)` placeholders.
607
+ - Missing translations can fall back to the source text in dev while locale JSON files are being filled.
608
+ - In local mode, locale JSON files are committed in the repo and loaded one locale at a time.
609
+ - Client-only apps can fetch locale JSON from `public/` or a CDN and pass the result directly to `TranslateProvider`.
610
+ - The generated `load-messages.ts` is typed with an `AppLocale` union and statically imports each locale JSON so bundlers tree-shake unused locales.
611
+ - In local mode, production builds are check-only and fail if committed locale artifacts are missing or out of sync.
612
+ - Remote storage is not fully implemented yet, so bundle storage is the recommended path for now.
613
+
614
+ ## Example Flow
615
+
616
+ 1. Add the plugin to `vite.config.ts`.
617
+ 2. Configure `locales`, `defaultLocale`, and bundle storage.
618
+ 3. Mark text with `t()`, `<T>`, or `msg()`.
619
+ 4. Load one locale with `loadMessages(locale)` from the generated `load-messages.ts` or fetch the locale JSON in the browser.
620
+ 5. Wrap your UI in `TranslateProvider`.
621
+ 6. Use `useT()`, `T`, `Var`, `createTranslator()`, and `msg()` where appropriate.
622
+ 7. Let the plugin write locale JSON files in dev and call your custom translator for missing entries.
623
+
624
+ ## Step-By-Step: How It Works
625
+
626
+ This is the full local-storage flow from source code to translated UI.
627
+
628
+ ### 1. You configure the plugin
629
+
630
+ You add `betterTranslate(...)` to your Vite config and tell it:
631
+
632
+ - which locales exist
633
+ - which locale is the default source language
634
+ - which roots and file extensions should be scanned
635
+ - where locale files should be written
636
+ - whether missing translations should be auto-filled with `translate()`
637
+
638
+ ### 2. The plugin scans all matching files under your configured roots
639
+
640
+ At startup, the plugin walks every matching file under the configured scan roots, not just files that Vite has already loaded into the module graph.
641
+
642
+ That gives it a complete view of:
643
+
644
+ - every extracted message id
645
+ - every default message
646
+ - every placeholder list
647
+ - every source location
648
+
649
+ This full scan is what lets the plugin build a stable manifest for the whole app instead of only the currently visited route.
650
+
651
+ ### 3. It extracts messages from translation markers
652
+
653
+ The extractor looks for:
654
+
655
+ - `t("...")` and similar configured call markers
656
+ - `<T>...</T>` JSX blocks
657
+ - `msg("id")\`...\`` tagged templates
658
+
659
+ For each match it records:
660
+
661
+ - the message id
662
+ - the source-language text
663
+ - optional context
664
+ - placeholder names
665
+ - source file and location metadata
666
+
667
+ For static `<T>` elements, the plugin also injects an `id="..."` attribute into the source so runtime does not need to re-derive the id every time.
668
+
669
+ ### 4. It builds an in-memory manifest
670
+
671
+ All extracted messages are grouped into a manifest keyed by message id.
672
+
673
+ Each manifest entry stores the canonical shape of that message:
674
+
675
+ - `defaultMessage`
676
+ - `meta`
677
+ - `placeholders`
678
+ - `sources`
679
+
680
+ If two different messages collide onto the same id but do not have the same shape, the plugin throws an error instead of silently picking one.
681
+
682
+ ### 5. It writes generated helper files
683
+
684
+ In local mode, the plugin writes a few generated files alongside your locales:
685
+
686
+ - `manifest.json`: private metadata manifest
687
+ - `runtime.json`: runtime config for locale loading
688
+ - `load-messages.ts`: typed loader that imports each locale JSON file
689
+ - `.gitignore`: ignores the private manifest
690
+
691
+ These files are only rewritten when their contents actually change.
692
+
693
+ ### 6. It writes locale JSON files
694
+
695
+ For each configured locale, the plugin writes a flat `Record<string, string>` JSON file.
696
+
697
+ - For the default locale, values always come from the current source text.
698
+ - For non-default locales, existing committed translations are preserved.
699
+ - If a translation is not present in the locale file, the plugin can fall back to the cache.
700
+
701
+ In dev, existing locale entries are preserved so partial rescans or incremental changes do not wipe translations from disk.
702
+
703
+ ### 7. It optionally auto-translates missing entries in dev
704
+
705
+ If you provide `translate(messages, locale)`, the plugin collects only the missing entries for non-default locales and sends them to your callback.
706
+
707
+ The callback receives:
708
+
709
+ - stable `id`
710
+ - source `text`
711
+ - optional `meta.context`
712
+ - `placeholders`
713
+ - `sources`
714
+
715
+ The returned translations are stored in the cache and then written back into the locale JSON files.
716
+
717
+ ### 8. It caches translations between runs
718
+
719
+ The cache file stores translations keyed by message id plus locale.
720
+
721
+ That means unchanged messages do not need to be translated again across restarts, as long as:
722
+
723
+ - the message id is unchanged
724
+ - the locale is unchanged
725
+ - the cache schema is still valid
726
+
727
+ ### 9. It keeps the manifest in sync during dev
728
+
729
+ When a file is added, changed, or removed under the scan roots, the plugin rescans that file and updates the manifest.
730
+
731
+ - If the actual message content changed, locale files are updated.
732
+ - If only source locations changed, the private manifest is updated.
733
+ - If a file is temporarily invalid and cannot be parsed, the plugin skips removing its previous messages instead of treating that as a deletion.
734
+
735
+ This makes dev behavior much less destructive during normal editing.
736
+
737
+ ### 10. Your app loads one locale at runtime
738
+
739
+ At runtime, your app loads a single locale's message map.
740
+
741
+ The common local-mode path is:
742
+
743
+ 1. Call `loadMessages(locale)`.
744
+ 2. Receive a flat `Record<string, string>`.
745
+ 3. Pass that object into `TranslateProvider`.
746
+ 4. Read translations with `useT()`, `T`, or the server helpers.
747
+
748
+ ### 11. Runtime lookups are just id lookups
749
+
750
+ Once messages are loaded, translation is a plain lookup:
751
+
752
+ - `useT()` hashes the source text plus optional context into the deterministic message id and looks it up in the loaded map.
753
+ - `<T>` uses its explicit injected id when present, or computes the same deterministic id from its static source content.
754
+ - `createTranslator()` on the server looks up ids in the same flat message map.
755
+
756
+ Because ids are deterministic, unchanged source text resolves to the same key across restarts.
757
+
758
+ ### 12. Production local builds are check-only
759
+
760
+ For `storage: { type: "bundle" }`, production builds do not call `translate()` and do not rewrite locale artifacts.
761
+
762
+ Instead, the plugin:
763
+
764
+ 1. rebuilds the manifest from source
765
+ 2. checks that committed generated files such as `runtime.json` and `load-messages.ts` are present and up to date
766
+ 3. checks that every committed locale file exists
767
+ 4. checks that every locale file has the expected ids
768
+ 5. checks that the default locale still matches the current source text
769
+ 6. fails the build if anything is missing, stale, or orphaned
770
+
771
+ The private `manifest.json` is still generated for dev/debugging, but it is not required to be committed for production builds.
772
+
773
+ That keeps production behavior predictable: either the committed locale artifacts are correct, or the build stops.