@menapo/vi18n 0.0.0-stage → 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.
Files changed (100) hide show
  1. package/LICENSE +49 -0
  2. package/README.md +110 -2
  3. package/dist/apply-Bre-lY9J.js +228 -0
  4. package/dist/captured-Bj8bO6vd.js +1034 -0
  5. package/dist/check-D34P-S--.js +1 -0
  6. package/dist/check-DyrAbv73.js +201 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +41 -0
  9. package/dist/codec-DuvU2VQy.js +207 -0
  10. package/dist/collect-Cx4_SKOe.js +13 -0
  11. package/dist/collisions-CfpZU8e4.js +217 -0
  12. package/dist/compile-B5Mz6WJm.js +245 -0
  13. package/dist/compile-Cc97JaW2.js +2502 -0
  14. package/dist/concurrency-C9UUWuDC.js +96 -0
  15. package/dist/concurrency-DONL7t4V.js +1 -0
  16. package/dist/config.d.ts +21 -0
  17. package/dist/config.js +39 -0
  18. package/dist/dir-zw6bGMKQ.js +290 -0
  19. package/dist/duplicates-yuRljQ6h.js +185 -0
  20. package/dist/engine-B7p40M__.js +531 -0
  21. package/dist/engine.d.ts +43 -0
  22. package/dist/engine.js +1241 -0
  23. package/dist/exclude-Du-Zd_qI.js +26 -0
  24. package/dist/export-DGsMbcTZ.js +103 -0
  25. package/dist/extract-Dujlsgnk.js +298 -0
  26. package/dist/formats-KPIg-08V.js +781 -0
  27. package/dist/i18next-CrUnKFfh.js +1596 -0
  28. package/dist/i18next-Rgo-v9l0.js +1 -0
  29. package/dist/import-Cwdhcywt.js +225 -0
  30. package/dist/index.d.ts +156 -0
  31. package/dist/index.js +239 -0
  32. package/dist/init-BecCTvKi.js +792 -0
  33. package/dist/library-D5CWvTQs.js +1882 -0
  34. package/dist/lingui--PcOOYry.js +833 -0
  35. package/dist/lint.d.ts +105 -0
  36. package/dist/lint.js +971 -0
  37. package/dist/load-CQRz4PpM.js +2950 -0
  38. package/dist/load-gW-oJt1Q.js +747 -0
  39. package/dist/locales-DX4MOBFE.js +827 -0
  40. package/dist/lsp-Cq1Nuu-Q.js +31 -0
  41. package/dist/match-BGr8EXB3.js +89 -0
  42. package/dist/mcp-Cf0T7qPW.js +28 -0
  43. package/dist/merge-driver-CpiiSVIc.js +422 -0
  44. package/dist/messages-Cxa-5tR0.js +166 -0
  45. package/dist/migrate-CntqvrW4.js +1 -0
  46. package/dist/migrate-dPgq9uzy.js +1484 -0
  47. package/dist/openai-CTaMDcl-.js +346 -0
  48. package/dist/output-DsaHKHEt.js +164 -0
  49. package/dist/parse.d.ts +62 -0
  50. package/dist/parse.js +156 -0
  51. package/dist/pipeline-C7g11THL.js +2031 -0
  52. package/dist/pipeline-wxS0SApG.js +1 -0
  53. package/dist/project-Dwx7iPm6.js +381 -0
  54. package/dist/project-pXipT8iG.js +1046 -0
  55. package/dist/pseudo-DRv30iN5.js +145 -0
  56. package/dist/raw-C4Y6vyGY.js +4708 -0
  57. package/dist/react-intl-CaBVwJ9u.js +519 -0
  58. package/dist/restore-Btq66Tid.js +175 -0
  59. package/dist/rewrite-CiTRAICl.js +803 -0
  60. package/dist/rich-DCUKl5HM.js +97 -0
  61. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  62. package/dist/run-DYD_VXKy.js +694 -0
  63. package/dist/runtime-DTMmliT1.js +64 -0
  64. package/dist/scan-worker.d.ts +62 -0
  65. package/dist/scan-worker.js +1 -0
  66. package/dist/scoped-BMYpSthj.js +44 -0
  67. package/dist/server-BDkykMjb.js +11823 -0
  68. package/dist/server-qv3mz70d.js +10485 -0
  69. package/dist/server.d.ts +169 -0
  70. package/dist/server.js +1234 -0
  71. package/dist/sitemap-DV3o8QD7.js +159 -0
  72. package/dist/sources-D19czDSA.js +2242 -0
  73. package/dist/status-DZArW5aE.js +149 -0
  74. package/dist/stdio-CVaJfxA7.js +29 -0
  75. package/dist/stdio-FPOKQAgu.js +95 -0
  76. package/dist/sync-CcVzger_.js +658 -0
  77. package/dist/t-DC-c9zE5.js +454 -0
  78. package/dist/test.d.ts +116 -0
  79. package/dist/test.js +319 -0
  80. package/dist/translate-DZR3MkSA.js +299 -0
  81. package/dist/ts-BaU5yOf0.js +4863 -0
  82. package/dist/types-CgUCkFU1.d.ts +707 -0
  83. package/dist/types-D3QGM8XO.js +146 -0
  84. package/dist/types-DsdWBG0b.d.ts +558 -0
  85. package/dist/types-DtvQ7UdD.js +1948 -0
  86. package/dist/types-wSh9fCBY.js +11 -0
  87. package/dist/units-CQ5Q99RC.js +150 -0
  88. package/dist/url-CRfCOhUI.js +172 -0
  89. package/dist/url-CsBkRaJy.js +225 -0
  90. package/dist/validate-BxpupdAf.js +2615 -0
  91. package/dist/validate-D6DJepZv.js +365 -0
  92. package/dist/version-BlyLGlOa.js +28 -0
  93. package/dist/vite-scan-B_YW4X_a.js +242 -0
  94. package/dist/vite.d.ts +34 -0
  95. package/dist/vite.js +7558 -0
  96. package/dist/vue-i18n-CEMnpv7k.js +651 -0
  97. package/dist/which-C6PFI1Dm.js +116 -0
  98. package/dist/worker-84U6uYB3.js +1656 -0
  99. package/package.json +97 -3
  100. 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
- # Temporary Holding Version
1
+ # @menapo/vi18n
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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/vite-plugins/blob/main/packages/vi18n/docs/guide/getting-started.md)
78
+ - [Writing messages](https://github.com/menapo/vite-plugins/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/vite-plugins/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/vite-plugins/blob/main/packages/vi18n/docs/guide/configuration.md)
81
+ - [Localized paths, domains and sitemaps](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/localized-urls.md)
82
+ - [Linting](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/lint.md): the `@menapo/vi18n/lint` rules
83
+ - [Testing](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/testing.md): `@menapo/vi18n/test` · [Web Workers](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/workers.md) · [Libraries](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/libraries.md)
84
+ - Recipes: [SPAs](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/recipes/spa.md) (React, Preact, Vue, Svelte, Solid, plain JS) · [SvelteKit](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/recipes/sveltekit.md) · [Astro](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/recipes/astro.md) · [TanStack Start](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/recipes/tanstack-start.md) · [React Router](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/recipes/react-router.md) · [Nuxt](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/recipes/nuxt.md) · [SolidStart](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/recipes/solid-start.md) · [Hono and Workers](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/recipes/hono-workers.md)
85
+ - [Deploying: cache headers per host](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/deploy.md) · [Content Security Policy](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/csp.md)
86
+ - [Migrating from i18next, react-intl, Lingui or vue-i18n](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/migrate-from-libraries.md) · [from `@saas/i18n`](https://github.com/menapo/vite-plugins/blob/main/packages/vi18n/docs/guide/migrate-from-saas-i18n.md)
87
+ - Design: [PLAN.md](https://github.com/menapo/vite-plugins/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/vite-plugins/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/vite-plugins/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/vite-plugins/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 };