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 +773 -0
- package/dist/message-id-7Mx7G9xT.mjs +32 -0
- package/dist/message-id-7Mx7G9xT.mjs.map +1 -0
- package/dist/react.d.mts +45 -0
- package/dist/react.d.mts.map +1 -0
- package/dist/react.mjs +91 -0
- package/dist/react.mjs.map +1 -0
- package/dist/server.d.mts +26 -0
- package/dist/server.d.mts.map +1 -0
- package/dist/server.mjs +35 -0
- package/dist/server.mjs.map +1 -0
- package/dist/types-Di93oTw4.d.mts +106 -0
- package/dist/types-Di93oTw4.d.mts.map +1 -0
- package/dist/vite.d.mts +9 -0
- package/dist/vite.d.mts.map +1 -0
- package/dist/vite.mjs +855 -0
- package/dist/vite.mjs.map +1 -0
- package/package.json +50 -0
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.
|