@gusnips/vite 0.8.15 → 0.8.17
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 +76 -0
- package/dist/i18n.d.ts +111 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +680 -0
- package/dist/i18n.js.map +1 -0
- package/package.json +10 -4
- package/src/i18n.test.ts +490 -0
- package/src/i18n.ts +909 -0
package/README.md
CHANGED
|
@@ -178,6 +178,82 @@ The script runs on its own, before your bundle, so it must not use anything outs
|
|
|
178
178
|
tag goes at the end of `<head>` by default. `position: "head-prepend"` puts it first instead, so
|
|
179
179
|
it does not wait for your stylesheets to download. Use that for a script that may leave the page.
|
|
180
180
|
|
|
181
|
+
## Check your translations
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
// scripts/check-i18n.ts
|
|
185
|
+
import { readFile } from "node:fs/promises";
|
|
186
|
+
import { checkI18n, formatI18nReport, type I18nBundle } from "@gusnips/vite/i18n";
|
|
187
|
+
|
|
188
|
+
const web: I18nBundle = {
|
|
189
|
+
name: "web",
|
|
190
|
+
locales: ["en", "pt-BR", "es"],
|
|
191
|
+
canonical: "en",
|
|
192
|
+
load: async (locale) => ({
|
|
193
|
+
translation: JSON.parse(await readFile(`src/i18n/locales/${locale}.json`, "utf8")),
|
|
194
|
+
}),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const report = await checkI18n([web]);
|
|
198
|
+
|
|
199
|
+
console.log(formatI18nReport(report));
|
|
200
|
+
process.exit(report.problems.some((p) => p.level === "error") ? 1 : 0);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Your `canonical` language is typed, so a `t("key")` it lacks already fails to compile. The other
|
|
204
|
+
languages are not, and neither is anything inside a string. This checks what the compiler can't:
|
|
205
|
+
|
|
206
|
+
| It fails when | Because the reader gets |
|
|
207
|
+
| -------------------------------------------------------------- | ----------------------------------------------------- |
|
|
208
|
+
| a key is in one language and not another, or is blank | the canonical language, in the middle of their own |
|
|
209
|
+
| a counted key lacks a form its language needs (pt `_many`) | English for exactly 1,000,000 of something |
|
|
210
|
+
| `{{name}}` became `{{nome}}`, or `{{n, number}}` lost `number` | a gap, or 5000 where they write 5.000 |
|
|
211
|
+
| `<0>…</0>` is out of order or not closed | the wrong words bold, or the tag as text |
|
|
212
|
+
| a placeholder is named `lng`, `ns` or another `t()` option | another language, not the value |
|
|
213
|
+
| a language fails to load | nothing — every other rule would have passed it empty |
|
|
214
|
+
|
|
215
|
+
`load` returns one language's catalogs by namespace, so JSON files, TS modules and a runtime
|
|
216
|
+
merge all fit. If it throws, the check fails with the error. It never skips.
|
|
217
|
+
|
|
218
|
+
Give it `code` and it reads your source too: every static `t("key")` exists, a template key's
|
|
219
|
+
fixed start exists (``t(`plan.${id}.title`)`` needs `plan`), `<Trans>` has no children (they
|
|
220
|
+
shift every `<0>`), and copy with `<strong>` in it is not read through `t()`, which prints the
|
|
221
|
+
tag as text.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
await checkI18n([{ ...web, code: ["src/**/*.{ts,tsx}"] }]);
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Keys the server sends are shown in whatever language the reader has, so each one must be in every
|
|
228
|
+
language. Pass the list if you can import it, or the server's source to scan for `messageKey:`:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
await checkI18n([{ ...web, serverKeys: MESSAGE_KEYS }]);
|
|
232
|
+
// or, with no list to import:
|
|
233
|
+
await checkI18n([
|
|
234
|
+
{ ...web, serverCode: ["apps/api/src/**/*.ts"], serverKeyPrefixes: ["serverErrors."] },
|
|
235
|
+
]);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`report.unused` lists canonical keys no code seems to reach. It is a hint, not a failure: a key
|
|
239
|
+
built at runtime from a variable can't be seen by a scan.
|
|
240
|
+
|
|
241
|
+
If your prices come from a formatter, pass `{ price: /\$\s?\d/ }` as the second argument and a
|
|
242
|
+
price typed into copy fails too.
|
|
243
|
+
|
|
244
|
+
**Fragments.** If several people write screens at once, keep one file per area with every
|
|
245
|
+
language side by side — `{ "en": {…}, "pt-BR": {…} }` — and merge them. `writeFragments` writes
|
|
246
|
+
`out/<locale>.json` with sorted keys, so two runs write the same bytes, and writes nothing if two
|
|
247
|
+
fragments disagree. The check then fails if a committed catalog no longer matches its fragments:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
await checkI18n([{ ...web, fragments: { dir: "src/i18n/fragments", out: "src/i18n/locales" } }]);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
If the app merges fragments at runtime with a spread (`{ ...auth.en, ...nav.en }`), leave `out`
|
|
254
|
+
off and set `ownTopLevel: true`. A spread keeps only the second of two fragments that share a
|
|
255
|
+
top-level key, so that is refused.
|
|
256
|
+
|
|
181
257
|
## The vite preset
|
|
182
258
|
|
|
183
259
|
```ts
|
package/dist/i18n.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/** One i18next namespace's catalog: nested objects with strings at the leaves. */
|
|
2
|
+
export type Catalog = object;
|
|
3
|
+
/** One language's catalogs, by namespace: `{ translation: {…} }`. */
|
|
4
|
+
export type Catalogs = Readonly<Record<string, Catalog>>;
|
|
5
|
+
export interface I18nBundle {
|
|
6
|
+
/** Shown beside every problem: `"apps/web"`. */
|
|
7
|
+
name: string;
|
|
8
|
+
locales: readonly string[];
|
|
9
|
+
/** The app's `fallbackLng`: the language the others are compared to, and code keys must be in. */
|
|
10
|
+
canonical: string;
|
|
11
|
+
/**
|
|
12
|
+
* One language's catalogs. A throw fails the check with its message — it never skips the bundle,
|
|
13
|
+
* because every other rule compares languages and a missing one would pass them all.
|
|
14
|
+
*/
|
|
15
|
+
load: (locale: string) => Catalogs | Promise<Catalogs>;
|
|
16
|
+
/** Copy kept as fragments, one area per file: see `mergeFragments`. Paths are relative to `root`. */
|
|
17
|
+
fragments?: FragmentsCheck;
|
|
18
|
+
/** Source that calls `t()`, as globs relative to `root`. Tests and `i18n/` folders are skipped. */
|
|
19
|
+
code?: readonly string[];
|
|
20
|
+
/** Every key the server can send, imported from where it is declared. Preferred over a scan. */
|
|
21
|
+
serverKeys?: readonly string[];
|
|
22
|
+
/** Server source to scan for `messageKey: "…"` when there is no list to import. */
|
|
23
|
+
serverCode?: readonly string[];
|
|
24
|
+
/** String literals in `serverCode` starting with one of these are keys too: `["serverErrors."]`. */
|
|
25
|
+
serverKeyPrefixes?: readonly string[];
|
|
26
|
+
/** Placeholders the app fills before i18next sees the string, so they may use a reserved name. */
|
|
27
|
+
brandVars?: Readonly<Record<string, string>>;
|
|
28
|
+
/** The namespace a `t()` bound to nothing reads. Default `"translation"`. */
|
|
29
|
+
defaultNamespace?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface FragmentsCheck {
|
|
32
|
+
dir: string;
|
|
33
|
+
/**
|
|
34
|
+
* Where the merged catalogs are written, for an app that commits them. `out/<locale>.json` must
|
|
35
|
+
* then equal a fresh merge, so a hand edit or a fragment saved without re-merging fails. Leave it
|
|
36
|
+
* out when the app merges at runtime.
|
|
37
|
+
*/
|
|
38
|
+
out?: string;
|
|
39
|
+
/** Refuse two fragments that share a top-level key: see `MergeOptions`. */
|
|
40
|
+
ownTopLevel?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface CheckI18nOptions {
|
|
43
|
+
/** What `code`, `serverCode` and `fragments` are relative to. Default: the working directory. */
|
|
44
|
+
root?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Refuse a price typed into copy, for a product whose prices come from a formatter. Off by
|
|
47
|
+
* default: most copy that matches is an example, not a price.
|
|
48
|
+
*/
|
|
49
|
+
price?: RegExp;
|
|
50
|
+
}
|
|
51
|
+
export type I18nRule = "load" | "fragments" | "parity" | "plural" | "placeholders" | "reserved" | "tags" | "price" | "missing-key" | "dynamic-key" | "trans-children" | "markup" | "server-key";
|
|
52
|
+
export interface I18nProblem {
|
|
53
|
+
rule: I18nRule;
|
|
54
|
+
/** A warning is shown and does not fail the gate. */
|
|
55
|
+
level: "error" | "warning";
|
|
56
|
+
bundle: string;
|
|
57
|
+
locale?: string;
|
|
58
|
+
key?: string;
|
|
59
|
+
/** `file:line`, for a problem found in code. */
|
|
60
|
+
at?: string;
|
|
61
|
+
message: string;
|
|
62
|
+
}
|
|
63
|
+
export interface I18nReport {
|
|
64
|
+
problems: I18nProblem[];
|
|
65
|
+
/** Canonical keys no static reference, template prefix or key-path literal reaches. Advisory. */
|
|
66
|
+
unused: {
|
|
67
|
+
bundle: string;
|
|
68
|
+
key: string;
|
|
69
|
+
}[];
|
|
70
|
+
}
|
|
71
|
+
export interface MergeOptions {
|
|
72
|
+
/**
|
|
73
|
+
* Refuse two fragments that share a top-level key. Set it when the app merges fragments at
|
|
74
|
+
* runtime with a spread — `{ ...auth.en, ...nav.en }` — where the second `nav` replaces the
|
|
75
|
+
* first instead of joining it, and the keys of one fragment vanish with no error anywhere.
|
|
76
|
+
*/
|
|
77
|
+
ownTopLevel?: boolean;
|
|
78
|
+
}
|
|
79
|
+
export interface MergedFragments {
|
|
80
|
+
/** The text each `<locale>.json` must hold: keys sorted, two-space indent, a final newline. */
|
|
81
|
+
files: Map<string, string>;
|
|
82
|
+
/** A fragment that does not parse, or two that disagree. When not empty, nothing may be written. */
|
|
83
|
+
errors: string[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fold `dir/*.json` — one file per area, every language side by side, `{ "en": {…}, "pt-BR": {…} }`
|
|
87
|
+
* — into one catalog per language. Fragments exist so that people and agents building screens in
|
|
88
|
+
* parallel do not all edit the same three files. Deterministic: files in name order, keys sorted,
|
|
89
|
+
* so two runs write the same bytes and a derived catalog can be compared with a fresh merge.
|
|
90
|
+
*/
|
|
91
|
+
export declare function mergeFragments(dir: string, locales: readonly string[], options?: MergeOptions): Promise<MergedFragments>;
|
|
92
|
+
/** Merge `dir` and write `out/<locale>.json`. Writes nothing when a fragment is broken, so a
|
|
93
|
+
* catalog is never missing the half that failed to parse. Returns the errors. */
|
|
94
|
+
export declare function writeFragments(dir: string, out: string, locales: readonly string[], options?: MergeOptions): Promise<string[]>;
|
|
95
|
+
/**
|
|
96
|
+
* Run every rule over every bundle. Returns what it found rather than printing or exiting, so a repo
|
|
97
|
+
* can add checks of its own to the same report and decide how CI fails:
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* const report = await checkI18n([web, site]);
|
|
101
|
+
* console.log(formatI18nReport(report));
|
|
102
|
+
* process.exit(report.problems.some((p) => p.level === "error") ? 1 : 0);
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export declare function checkI18n(bundles: readonly I18nBundle[], options?: CheckI18nOptions): Promise<I18nReport>;
|
|
106
|
+
/** The report as lines for a terminal: one per problem, grouped by bundle, and a last line that
|
|
107
|
+
* says whether the gate passed. Pass `showUnused` to list the advisory keys, not just count them. */
|
|
108
|
+
export declare function formatI18nReport(report: I18nReport, options?: {
|
|
109
|
+
showUnused?: boolean;
|
|
110
|
+
}): string;
|
|
111
|
+
//# sourceMappingURL=i18n.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"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"}
|