@menapo/vi18n 0.0.0-stage → 0.1.1
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/LICENSE +49 -0
- package/README.md +110 -2
- package/dist/apply-Bre-lY9J.js +228 -0
- package/dist/captured-Bj8bO6vd.js +1034 -0
- package/dist/check-BIKEuu6B.js +201 -0
- package/dist/check-HsmmpJZg.js +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +41 -0
- package/dist/codec-DuvU2VQy.js +207 -0
- package/dist/collect-Cx4_SKOe.js +13 -0
- package/dist/collisions-CfpZU8e4.js +217 -0
- package/dist/compile-BaDh0cHG.js +245 -0
- package/dist/compile-Cc97JaW2.js +2502 -0
- package/dist/concurrency-C9UUWuDC.js +96 -0
- package/dist/concurrency-DONL7t4V.js +1 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +39 -0
- package/dist/dir-zw6bGMKQ.js +290 -0
- package/dist/duplicates-yuRljQ6h.js +185 -0
- package/dist/engine-C2ENBRJe.js +531 -0
- package/dist/engine.d.ts +43 -0
- package/dist/engine.js +1241 -0
- package/dist/exclude-Du-Zd_qI.js +26 -0
- package/dist/export-C9Sy6UWv.js +103 -0
- package/dist/extract-C3S8btx0.js +298 -0
- package/dist/formats-9unqj_fZ.js +781 -0
- package/dist/i18next-9HAoxwUI.js +1 -0
- package/dist/i18next-BEJgv_S8.js +1596 -0
- package/dist/import-nANvQutw.js +225 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +239 -0
- package/dist/init-C2ScuW6a.js +792 -0
- package/dist/library-D5CWvTQs.js +1882 -0
- package/dist/lingui-zUiCl9Um.js +833 -0
- package/dist/lint.d.ts +105 -0
- package/dist/lint.js +971 -0
- package/dist/load-CQRz4PpM.js +2950 -0
- package/dist/load-gW-oJt1Q.js +747 -0
- package/dist/locales-DX4MOBFE.js +827 -0
- package/dist/lsp-RMMMc88P.js +31 -0
- package/dist/match-BGr8EXB3.js +89 -0
- package/dist/mcp-CHVwBoRP.js +28 -0
- package/dist/merge-driver-BTX87aVm.js +422 -0
- package/dist/messages-BfjIExyB.js +166 -0
- package/dist/migrate-IpLDMXmq.js +1484 -0
- package/dist/migrate-rCQuOGsf.js +1 -0
- package/dist/openai-CTaMDcl-.js +346 -0
- package/dist/output-DWE3SIUA.js +164 -0
- package/dist/parse.d.ts +62 -0
- package/dist/parse.js +156 -0
- package/dist/pipeline-C9Dbotkh.js +2031 -0
- package/dist/pipeline-DbX6rlo5.js +1 -0
- package/dist/project-Bw9UPIdx.js +1046 -0
- package/dist/project-C4GpaN-n.js +381 -0
- package/dist/pseudo-DRv30iN5.js +145 -0
- package/dist/raw-C4Y6vyGY.js +4708 -0
- package/dist/react-intl-BP0u-D5m.js +519 -0
- package/dist/restore-CiYv-jxS.js +175 -0
- package/dist/rewrite-EsIw_lsq.js +803 -0
- package/dist/rich-DCUKl5HM.js +97 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/run-BEcnBbu-.js +694 -0
- package/dist/runtime-DTMmliT1.js +64 -0
- package/dist/scan-worker.d.ts +62 -0
- package/dist/scan-worker.js +1 -0
- package/dist/scoped-BMYpSthj.js +44 -0
- package/dist/server-Da1Ot9IN.js +10485 -0
- package/dist/server-deBOq2vq.js +11823 -0
- package/dist/server.d.ts +169 -0
- package/dist/server.js +1234 -0
- package/dist/sitemap-DJXzKoza.js +159 -0
- package/dist/sources-D19czDSA.js +2242 -0
- package/dist/status-zr4jY59Y.js +149 -0
- package/dist/stdio-CVaJfxA7.js +29 -0
- package/dist/stdio-FPOKQAgu.js +95 -0
- package/dist/sync-B9V2KFwb.js +658 -0
- package/dist/t-DC-c9zE5.js +454 -0
- package/dist/test.d.ts +116 -0
- package/dist/test.js +319 -0
- package/dist/translate-CQnJfLjO.js +299 -0
- package/dist/ts-BaU5yOf0.js +4863 -0
- package/dist/types-CgUCkFU1.d.ts +707 -0
- package/dist/types-D3QGM8XO.js +146 -0
- package/dist/types-DsdWBG0b.d.ts +558 -0
- package/dist/types-DtvQ7UdD.js +1948 -0
- package/dist/types-wSh9fCBY.js +11 -0
- package/dist/units-CQ5Q99RC.js +150 -0
- package/dist/url-CRfCOhUI.js +172 -0
- package/dist/url-CsBkRaJy.js +225 -0
- package/dist/validate-BxpupdAf.js +2615 -0
- package/dist/validate-D6DJepZv.js +365 -0
- package/dist/version-Byqi2guX.js +28 -0
- package/dist/vite-scan-CT_vsp4g.js +242 -0
- package/dist/vite.d.ts +34 -0
- package/dist/vite.js +7558 -0
- package/dist/vue-i18n-g0iGy6CZ.js +651 -0
- package/dist/which-BOemZ5Q8.js +116 -0
- package/dist/worker-84U6uYB3.js +1656 -0
- package/package.json +97 -3
- package/schemas/catalog.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 menapo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
dist/ bundles the raw-transfer deserializer of oxc-parser 0.153.0
|
|
26
|
+
(https://github.com/oxc-project/oxc), which is under this license:
|
|
27
|
+
|
|
28
|
+
MIT License
|
|
29
|
+
|
|
30
|
+
Copyright (c) 2024-present VoidZero Inc. & Contributors
|
|
31
|
+
Copyright (c) 2023 Boshen
|
|
32
|
+
|
|
33
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
34
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
35
|
+
in the Software without restriction, including without limitation the rights
|
|
36
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
37
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
38
|
+
furnished to do so, subject to the following conditions:
|
|
39
|
+
|
|
40
|
+
The above copyright notice and this permission notice shall be included in all
|
|
41
|
+
copies or substantial portions of the Software.
|
|
42
|
+
|
|
43
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
44
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
45
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
46
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
47
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
48
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
49
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,111 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @menapo/vi18n
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
i18n for Vite. Write the source-language string where it is used; vi18n gives
|
|
4
|
+
it an id, keeps `.po` or `.json` catalogs in sync, ships each route's
|
|
5
|
+
translations as a small bundle cut along your build's own chunks, and
|
|
6
|
+
switches languages in place without a reload. One API in React, Preact, Vue,
|
|
7
|
+
Svelte, Solid, Astro and plain JavaScript, in SPAs and in SvelteKit, Astro,
|
|
8
|
+
TanStack Start, React Router, Nuxt, SolidStart and Cloudflare Workers.
|
|
9
|
+
|
|
10
|
+
**Status: 0.x preview.** Every 1.0 feature is implemented and tested against
|
|
11
|
+
an example app per framework. The docs site, with the guide and the
|
|
12
|
+
benchmarks, is built from this repository; publishing it and the benchmarks
|
|
13
|
+
waits on the repository going public. Until then the repository is private,
|
|
14
|
+
and so is the guide linked below.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { t } from "@menapo/vi18n";
|
|
18
|
+
|
|
19
|
+
t("Sign out");
|
|
20
|
+
t("Hello {name}", { name: user.name });
|
|
21
|
+
t("{count, plural, =0 {No items} one {# item} other {# items}}", { count });
|
|
22
|
+
t({ id: "hero.cta", message: "Join now", context: "Primary call to action" });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Why
|
|
26
|
+
|
|
27
|
+
- **No keys to invent.** A message's id is a hash of its text, computed at
|
|
28
|
+
build time; the build ships `t("OVaF9kgM8SVi", { name })` and the source
|
|
29
|
+
text leaves your client JavaScript. Manual ids (`hero.cta`) are there when
|
|
30
|
+
you want them.
|
|
31
|
+
- **Exact source or nothing.** A translation is used only for the exact text
|
|
32
|
+
it was made from. Edit the English, and the outdated German stops shipping
|
|
33
|
+
at once instead of silently lingering.
|
|
34
|
+
- **Per-route bundles from the chunk graph.** No namespaces, no route config:
|
|
35
|
+
a visitor downloads the strings of the routes they open, in their locale,
|
|
36
|
+
in parallel with the route's JavaScript. Server-rendered pages hydrate with
|
|
37
|
+
zero catalog requests, and a translation-only deploy changes no JavaScript
|
|
38
|
+
file.
|
|
39
|
+
- **In-place switching everywhere.** `setLocale("de")` loads what the page
|
|
40
|
+
needs, then flips every visible string, `<html lang dir>` and the URL in one
|
|
41
|
+
step; the plugin generates the framework glue, React Compiler included.
|
|
42
|
+
- **One `handle()` for SSR.** Web-standard per-request isolation, locale
|
|
43
|
+
negotiation, canonical redirects, SEO alternates and payload injection on
|
|
44
|
+
Node and Cloudflare Workers (any runtime with `AsyncLocalStorage`).
|
|
45
|
+
- **Catalogs that humans, tools and git can live with.** Standard gettext
|
|
46
|
+
`.po` (or JSON), deterministic, never wrapped, with the source beside each
|
|
47
|
+
translation so concurrent edits conflict instead of merging wrongly.
|
|
48
|
+
- **AI translation that validates ICU** (Gemini by default, any
|
|
49
|
+
OpenAI-compatible API or provider of your own), follows your glossary,
|
|
50
|
+
marks its output, and never re-requests a string it already has.
|
|
51
|
+
- **Small.** About 2 KB gzip for `t()` and 3 KB for the whole API without
|
|
52
|
+
`fmt`, plus a client engine of about 3.5 KB loaded once; the test suite
|
|
53
|
+
enforces a ceiling for each.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
vp add @menapo/vi18n
|
|
59
|
+
vp exec vi18n init
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`vi18n init` writes `vi18n.config.ts`, adds the plugin to `vite.config.*` and
|
|
63
|
+
creates the catalogs:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// vite.config.ts
|
|
67
|
+
import { defineConfig } from "vite";
|
|
68
|
+
import vi18n from "@menapo/vi18n/vite";
|
|
69
|
+
|
|
70
|
+
export default defineConfig({ plugins: [vi18n()] });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Requires Vite 8.3 or a later 8.x and Node 22.18 or newer.
|
|
74
|
+
|
|
75
|
+
## Documentation
|
|
76
|
+
|
|
77
|
+
- [Getting started](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/getting-started.md)
|
|
78
|
+
- [Writing messages](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/messages.md): `t`, ICU, `msg`, rich text, `fmt`, the locale API
|
|
79
|
+
- [Catalogs and the CLI](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/catalogs-and-cli.md): `extract`, `check`, `status`, `compile`, `translate`, `export` / `import`, `restore`, `which`, the git merge driver
|
|
80
|
+
- [Configuration reference](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/configuration.md)
|
|
81
|
+
- [Localized paths, domains and sitemaps](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/localized-urls.md)
|
|
82
|
+
- [Linting](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/lint.md): the `@menapo/vi18n/lint` rules
|
|
83
|
+
- [Testing](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/testing.md): `@menapo/vi18n/test` · [Web Workers](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/workers.md) · [Libraries](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/libraries.md)
|
|
84
|
+
- Recipes: [SPAs](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/recipes/spa.md) (React, Preact, Vue, Svelte, Solid, plain JS) · [SvelteKit](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/recipes/sveltekit.md) · [Astro](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/recipes/astro.md) · [TanStack Start](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/recipes/tanstack-start.md) · [React Router](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/recipes/react-router.md) · [Nuxt](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/recipes/nuxt.md) · [SolidStart](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/recipes/solid-start.md) · [Hono and Workers](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/recipes/hono-workers.md)
|
|
85
|
+
- [Deploying: cache headers per host](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/deploy.md) · [Content Security Policy](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/csp.md)
|
|
86
|
+
- [Migrating from i18next, react-intl, Lingui or vue-i18n](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/migrate-from-libraries.md) · [from `@saas/i18n`](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/migrate-from-saas-i18n.md)
|
|
87
|
+
- Design: [PLAN.md](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/PLAN.md)
|
|
88
|
+
|
|
89
|
+
The same pages, with the benchmarks, build as a site from
|
|
90
|
+
[`site/`](https://github.com/menapo/kit/tree/main/site) in the
|
|
91
|
+
repository; `vp run --filter @menapo/site dev` serves it locally.
|
|
92
|
+
|
|
93
|
+
## Entry points
|
|
94
|
+
|
|
95
|
+
| Import | For |
|
|
96
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
97
|
+
| `@menapo/vi18n` | App code, browser and server: `t`, `msg`, `rich`, `richHtml`, `richParts`, `fmt`, `getLocale`, `setLocale`, `subscribe`, `setNavigator`, `locales`, `dir`, `localizeHref`, `localizeUrl`, `delocalizeUrl`, `ready`, `preload`, `onMissing`, `T`, `localeName`, `vi18nZod` |
|
|
98
|
+
| `@menapo/vi18n/server` | Servers: `handle`, `withRequest`, `withLocale`, `createTranslator`, `negotiate`, `alternates`, `registerCatalogs` and more |
|
|
99
|
+
| `@menapo/vi18n/parse` | Text that is not in your code (a CMS, a database): `parse`, `format`, `onParseError`, for ICU and MessageFormat 2 |
|
|
100
|
+
| `@menapo/vi18n/vite` | The `vi18n()` plugin, and `vi18nTransform()` for Rollup/Rolldown pipelines |
|
|
101
|
+
| `@menapo/vi18n/config` | `defineConfig`, build-time `localizedPaths` and the `openaiCompatible` provider for `vi18n.config.ts` |
|
|
102
|
+
| `@menapo/vi18n/lint` | Lint rules for `vp check`, oxlint or ESLint: untranslated text, dynamic messages, ICU errors with fixes ([guide](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/lint.md)) |
|
|
103
|
+
| `@menapo/vi18n/test` | Tests in another locale: `createTestI18n`, `loadCatalog`, `setPseudoLocale`, `scanPseudo` ([guide](https://github.com/menapo/kit/blob/main/packages/vi18n/docs/guide/testing.md)) |
|
|
104
|
+
| `vi18n` | The CLI: `init`, `extract`, `check`, `status`, `compile`, `translate`, `migrate`, `sitemap`, `export`, `import`, `restore`, `which`, `merge-driver`, `lsp`, `mcp` |
|
|
105
|
+
|
|
106
|
+
There is no per-framework package: the plugin generates the little glue a
|
|
107
|
+
framework needs from the framework you already use.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { o as isDiagnosticError } from "./types-D3QGM8XO.js";
|
|
2
|
+
import { a as isManualId, r as validateTranslation } from "./validate-BxpupdAf.js";
|
|
3
|
+
import { S as entryKey } from "./library-D5CWvTQs.js";
|
|
4
|
+
import { r as resolveDuplicates } from "./duplicates-yuRljQ6h.js";
|
|
5
|
+
import { i as entrySource, r as canonicalOrNull } from "./units-CQ5Q99RC.js";
|
|
6
|
+
//#region src/core/exchange/apply.ts
|
|
7
|
+
/**
|
|
8
|
+
* Applying imported rows and restored entries to a catalog (PLAN §4.3, §4.4,
|
|
9
|
+
* §5.19). Both are pure functions of the catalog they receive, so writers
|
|
10
|
+
* re-run them inside `updateCatalogFile` when the file changed under them.
|
|
11
|
+
* A translation is attached only to the exact source it was made for (D10):
|
|
12
|
+
* a row whose source differs from the entry's is reported, never imported,
|
|
13
|
+
* and a key-only row of a manual id must carry its source's label.
|
|
14
|
+
*/
|
|
15
|
+
/** Every entry index per key: a duplicate gets the same translation as its twin. */
|
|
16
|
+
function keyIndex(catalog, codec) {
|
|
17
|
+
const index = /* @__PURE__ */ new Map();
|
|
18
|
+
catalog.entries.forEach((entry, i) => {
|
|
19
|
+
const key = entryKey(entry, catalog.layout, codec);
|
|
20
|
+
if (key === null) return;
|
|
21
|
+
const list = index.get(key);
|
|
22
|
+
if (list === void 0) index.set(key, [i]);
|
|
23
|
+
else list.push(i);
|
|
24
|
+
});
|
|
25
|
+
return index;
|
|
26
|
+
}
|
|
27
|
+
/** The ICU errors that keep a translation from shipping, as `usableTranslation` finds them. */
|
|
28
|
+
function translationErrors(translation, canonical, codec, locale) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = codec.parse(translation);
|
|
31
|
+
return validateTranslation(parsed, codec.parse(canonical), {
|
|
32
|
+
locale,
|
|
33
|
+
text: translation
|
|
34
|
+
}).filter((diagnostic) => diagnostic.severity === "error");
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (isDiagnosticError(error)) return [error.diagnostic];
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function applyImport(input) {
|
|
41
|
+
const { catalog, records, context, locale } = input;
|
|
42
|
+
const { codec } = context;
|
|
43
|
+
const index = keyIndex(catalog, codec);
|
|
44
|
+
const resolved = resolveDuplicates(catalog, codec).catalog;
|
|
45
|
+
const current = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const entry of resolved.entries) {
|
|
47
|
+
const key = entryKey(entry, catalog.layout, codec);
|
|
48
|
+
if (key !== null && !current.has(key)) current.set(key, entry);
|
|
49
|
+
}
|
|
50
|
+
const outcome = {
|
|
51
|
+
catalog,
|
|
52
|
+
changed: [],
|
|
53
|
+
unchanged: 0,
|
|
54
|
+
skipped: [],
|
|
55
|
+
diagnostics: []
|
|
56
|
+
};
|
|
57
|
+
let entries = null;
|
|
58
|
+
const taken = /* @__PURE__ */ new Map();
|
|
59
|
+
const skip = (record, reason, id) => {
|
|
60
|
+
outcome.skipped.push({
|
|
61
|
+
ref: record.ref,
|
|
62
|
+
line: record.line,
|
|
63
|
+
reason,
|
|
64
|
+
id
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
for (const record of records) {
|
|
68
|
+
if (record.unsupported !== void 0) {
|
|
69
|
+
skip(record, "unsupported", record.key);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
let key = record.key;
|
|
73
|
+
let recordCanonical = null;
|
|
74
|
+
if (record.source !== null) {
|
|
75
|
+
recordCanonical = canonicalOrNull(record.source, codec);
|
|
76
|
+
if (recordCanonical === null) {
|
|
77
|
+
skip(record, key === null ? "unknown" : "source-changed", key);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
key ??= codec.label(recordCanonical);
|
|
81
|
+
}
|
|
82
|
+
const entry = key === null ? void 0 : current.get(key);
|
|
83
|
+
if (key === null || entry === void 0) {
|
|
84
|
+
skip(record, "unknown", key);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const source = entrySource(entry, key, context);
|
|
88
|
+
if (source === null) {
|
|
89
|
+
skip(record, "no-source", key);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (recordCanonical !== null) {
|
|
93
|
+
if (!(context.layout === "id" ? codec.label(recordCanonical) === source.label : recordCanonical === source.canonical)) {
|
|
94
|
+
skip(record, "source-changed", key);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (record.label !== null && record.label !== source.label) {
|
|
99
|
+
skip(record, "source-changed", key);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (recordCanonical === null && record.label === null && isManualId(key)) {
|
|
103
|
+
skip(record, "no-source", key);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (record.translation === "") {
|
|
107
|
+
skip(record, "empty", key);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (record.draft) {
|
|
111
|
+
skip(record, "draft", key);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const earlier = taken.get(key);
|
|
115
|
+
if (earlier !== void 0) {
|
|
116
|
+
if (earlier !== record.translation) skip(record, "duplicate", key);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
taken.set(key, record.translation);
|
|
120
|
+
const errors = translationErrors(record.translation, source.canonical, codec, locale);
|
|
121
|
+
if (errors.length > 0) {
|
|
122
|
+
skip(record, "invalid", key);
|
|
123
|
+
for (const diagnostic of errors) {
|
|
124
|
+
const located = {
|
|
125
|
+
...diagnostic,
|
|
126
|
+
locale,
|
|
127
|
+
id: key
|
|
128
|
+
};
|
|
129
|
+
delete located.column;
|
|
130
|
+
if (record.line > 0) located.line = record.line;
|
|
131
|
+
else delete located.line;
|
|
132
|
+
outcome.diagnostics.push(located);
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const state = record.state ?? (entry.translation === record.translation ? entry.state : "translated");
|
|
137
|
+
if (entry.translation === record.translation && entry.state === state && !entry.fuzzy) {
|
|
138
|
+
outcome.unchanged++;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
entries ??= catalog.entries.slice();
|
|
142
|
+
for (const at of index.get(key) ?? []) entries[at] = {
|
|
143
|
+
...entries[at],
|
|
144
|
+
translation: record.translation,
|
|
145
|
+
state,
|
|
146
|
+
fuzzy: false
|
|
147
|
+
};
|
|
148
|
+
outcome.changed.push({
|
|
149
|
+
id: key,
|
|
150
|
+
line: record.line
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (entries !== null) outcome.catalog = {
|
|
154
|
+
...catalog,
|
|
155
|
+
entries
|
|
156
|
+
};
|
|
157
|
+
return outcome;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Fills the catalog's empty entries from an older revision of it (PLAN
|
|
161
|
+
* §5.19 `vi18n restore`): a generated entry from the old entry with the same
|
|
162
|
+
* canonical source, a manual entry from the old entry with the same manual
|
|
163
|
+
* id and the same canonical source. The old file may record another id
|
|
164
|
+
* scheme, so old entries are matched by text, never by key.
|
|
165
|
+
*/
|
|
166
|
+
function applyRestore(input) {
|
|
167
|
+
const { catalog, context, old, oldContext, locale } = input;
|
|
168
|
+
const { codec } = context;
|
|
169
|
+
const generated = /* @__PURE__ */ new Map();
|
|
170
|
+
const manual = /* @__PURE__ */ new Map();
|
|
171
|
+
for (const entry of resolveDuplicates(old, oldContext.codec).catalog.entries) {
|
|
172
|
+
const key = entryKey(entry, old.layout, oldContext.codec);
|
|
173
|
+
if (key === null || entry.translation === "" || entry.fuzzy) continue;
|
|
174
|
+
const source = entrySource(entry, key, oldContext);
|
|
175
|
+
if (source === null) continue;
|
|
176
|
+
const map = entry.manualId === null ? generated : manual;
|
|
177
|
+
const at = entry.manualId === null ? source.canonical : `${entry.manualId}\u0000${source.canonical}`;
|
|
178
|
+
if (!map.has(at)) map.set(at, entry);
|
|
179
|
+
}
|
|
180
|
+
const outcome = {
|
|
181
|
+
catalog,
|
|
182
|
+
restored: [],
|
|
183
|
+
diagnostics: []
|
|
184
|
+
};
|
|
185
|
+
let entries = null;
|
|
186
|
+
catalog.entries.forEach((entry, i) => {
|
|
187
|
+
if (entry.translation !== "") return;
|
|
188
|
+
const key = entryKey(entry, catalog.layout, codec);
|
|
189
|
+
if (key === null) return;
|
|
190
|
+
const source = entrySource(entry, key, context);
|
|
191
|
+
if (source === null) return;
|
|
192
|
+
const found = entry.manualId === null ? generated.get(source.canonical) : manual.get(`${entry.manualId}\u0000${source.canonical}`);
|
|
193
|
+
if (found === void 0) return;
|
|
194
|
+
const errors = translationErrors(found.translation, source.canonical, codec, locale);
|
|
195
|
+
if (errors.length > 0) {
|
|
196
|
+
for (const diagnostic of errors) {
|
|
197
|
+
const located = {
|
|
198
|
+
...diagnostic,
|
|
199
|
+
locale,
|
|
200
|
+
id: key
|
|
201
|
+
};
|
|
202
|
+
delete located.column;
|
|
203
|
+
if (entry.line > 0) located.line = entry.line;
|
|
204
|
+
else delete located.line;
|
|
205
|
+
outcome.diagnostics.push(located);
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
entries ??= catalog.entries.slice();
|
|
210
|
+
entries[i] = {
|
|
211
|
+
...entry,
|
|
212
|
+
translation: found.translation,
|
|
213
|
+
state: found.state,
|
|
214
|
+
fuzzy: false
|
|
215
|
+
};
|
|
216
|
+
outcome.restored.push({
|
|
217
|
+
id: key,
|
|
218
|
+
line: entry.line
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
if (entries !== null) outcome.catalog = {
|
|
222
|
+
...catalog,
|
|
223
|
+
entries
|
|
224
|
+
};
|
|
225
|
+
return outcome;
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
228
|
+
export { applyRestore as n, applyImport as t };
|