champollion 0.3.4 → 0.4.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 (132) hide show
  1. package/README.md +41 -26
  2. package/bin/cli.js +53 -5
  3. package/index.js +63 -2
  4. package/lib/api-key.js +17 -4
  5. package/lib/autofix.js +83 -36
  6. package/lib/bridge/method_bridge.py +15 -3
  7. package/lib/cards/reader.js +34 -0
  8. package/lib/cards/remote.js +15 -0
  9. package/lib/cards/search-names.js +178 -0
  10. package/lib/command-help.js +286 -85
  11. package/lib/commands/audit.js +10 -3
  12. package/lib/commands/card.js +583 -226
  13. package/lib/commands/doctor.js +54 -18
  14. package/lib/commands/help.js +37 -32
  15. package/lib/commands/init.js +1689 -87
  16. package/lib/commands/integrity.js +127 -40
  17. package/lib/commands/leaderboard.js +187 -67
  18. package/lib/commands/models.js +9 -2
  19. package/lib/commands/provenance.js +7 -2
  20. package/lib/commands/recommend.js +43 -14
  21. package/lib/commands/register-corpus.js +632 -125
  22. package/lib/commands/seal-corpus.js +1 -1
  23. package/lib/commands/status.js +564 -27
  24. package/lib/commands/submit.js +17 -12
  25. package/lib/commands/sync.js +31 -7
  26. package/lib/commands/tm.js +15 -9
  27. package/lib/commands/verify.js +27 -3
  28. package/lib/commands/wrap.js +63 -5
  29. package/lib/commands/xliff.js +135 -64
  30. package/lib/commercial-eligibility.js +1 -1
  31. package/lib/config.js +196 -14
  32. package/lib/content-estimate.js +96 -0
  33. package/lib/content-refusals.js +270 -0
  34. package/lib/content-review.js +372 -0
  35. package/lib/content-sync.js +1127 -344
  36. package/lib/content.js +94 -7
  37. package/lib/corpus-registration.mjs +194 -35
  38. package/lib/cost-label.js +29 -0
  39. package/lib/cost-report.js +726 -78
  40. package/lib/diff.js +38 -4
  41. package/lib/docusaurus-sync.js +965 -253
  42. package/lib/edit-distance.js +31 -0
  43. package/lib/fallback.js +964 -0
  44. package/lib/file-scope.js +106 -0
  45. package/lib/flatten.js +80 -3
  46. package/lib/flutter-locales.js +124 -0
  47. package/lib/format.js +266 -12
  48. package/lib/hash.js +146 -21
  49. package/lib/icu-structure.js +929 -0
  50. package/lib/integrity.js +223 -75
  51. package/lib/language-pair.js +157 -0
  52. package/lib/lint.js +78 -16
  53. package/lib/local-only-marks.js +106 -0
  54. package/lib/locale-layout.js +1103 -0
  55. package/lib/locale-state.js +571 -0
  56. package/lib/methods/anthropic.js +5 -0
  57. package/lib/methods/apertium.js +6 -3
  58. package/lib/methods/api.js +138 -25
  59. package/lib/methods/base.js +17 -0
  60. package/lib/methods/coaching-data.js +153 -0
  61. package/lib/methods/content-separator.js +43 -0
  62. package/lib/methods/deepl.js +1 -1
  63. package/lib/methods/direct-llm.js +252 -103
  64. package/lib/methods/external.js +146 -63
  65. package/lib/methods/gemini.js +1 -0
  66. package/lib/methods/google-translate.js +1 -0
  67. package/lib/methods/http-utils.js +41 -0
  68. package/lib/methods/libretranslate.js +7 -2
  69. package/lib/methods/llm-coached.js +68 -128
  70. package/lib/methods/llm.js +80 -31
  71. package/lib/methods/local.js +93 -10
  72. package/lib/methods/microsoft-translator.js +1 -2
  73. package/lib/methods/openai.js +4 -2
  74. package/lib/methods/openrouter-client.js +20 -19
  75. package/lib/methods/openrouter-pricing.js +150 -13
  76. package/lib/methods/prompt-methods.js +20 -0
  77. package/lib/methods/provider-pricing.js +42 -1
  78. package/lib/methods/request-capture.js +104 -0
  79. package/lib/methods/tilde.js +1 -1
  80. package/lib/methods/translated.js +1 -2
  81. package/lib/missing-key.js +93 -0
  82. package/lib/models.js +11 -0
  83. package/lib/name-rules.js +32 -0
  84. package/lib/named-keys.js +172 -0
  85. package/lib/no-translate.js +4 -3
  86. package/lib/output.js +160 -19
  87. package/lib/pairs.js +586 -30
  88. package/lib/placeholders.js +394 -0
  89. package/lib/plugins.js +8 -0
  90. package/lib/plural-gap-redo.js +109 -0
  91. package/lib/plurals.js +323 -0
  92. package/lib/po.js +1187 -0
  93. package/lib/public-catalogue.js +74 -0
  94. package/lib/recommend.js +527 -32
  95. package/lib/redo.js +95 -0
  96. package/lib/refusal-category.js +44 -0
  97. package/lib/registers.js +255 -11
  98. package/lib/repair-script.js +20 -13
  99. package/lib/scripts.js +6 -1
  100. package/lib/seal.mjs +4 -3
  101. package/lib/sealed-qualifier.mjs +1 -1
  102. package/lib/segment.js +2 -1
  103. package/lib/seo.js +19 -9
  104. package/lib/serve.js +43 -6
  105. package/lib/shared-output-seed.js +164 -0
  106. package/lib/source-contexts.js +39 -0
  107. package/lib/submit.mjs +57 -5
  108. package/lib/sync.js +2923 -474
  109. package/lib/terminology.js +13 -4
  110. package/lib/tm-evict.js +179 -0
  111. package/lib/tm-seed.js +5 -2
  112. package/lib/tm.js +818 -36
  113. package/lib/translate-pair.js +639 -34
  114. package/lib/translate.js +78 -5
  115. package/lib/types.js +22 -3
  116. package/lib/validate.js +880 -17
  117. package/lib/verify.js +1296 -104
  118. package/lib/watch.js +32 -13
  119. package/lib/xliff.js +44 -3
  120. package/package.json +1 -1
  121. package/shared/CORPORA-CARDS.md +2 -0
  122. package/shared/cards-fallback.json +1 -1
  123. package/shared/curated-orthography-conventions.json +26 -8
  124. package/shared/gettext-plural-forms.json +45 -0
  125. package/shared/method-registry.json +2 -0
  126. package/shared/metric-registry.json +96 -18
  127. package/shared/schemas/champollion-plugin.schema.json +4 -0
  128. package/shared/schemas/corpora-card.schema.json +8 -2
  129. package/shared/schemas/method-index-record.schema.json +67 -0
  130. package/shared/schemas/method-registry.schema.json +4 -0
  131. package/shared/schemas/metric-registry.schema.json +55 -1
  132. package/shared/docent/corpus.json +0 -11739
package/lib/watch.js CHANGED
@@ -20,7 +20,7 @@
20
20
  import fs from 'node:fs';
21
21
  import path from 'node:path';
22
22
  import { resolveConfig } from './config.js';
23
- import { detectFormatFromDir, getExtension } from './format.js';
23
+ import { discoverLocaleLayout } from './locale-layout.js';
24
24
  import { runSync } from './sync.js';
25
25
  import { output } from './output.js';
26
26
 
@@ -53,13 +53,27 @@ import { output } from './output.js';
53
53
  async function startWatch(options = {}) {
54
54
  const { cwd = process.cwd(), cliArgs = {} } = options;
55
55
  const config = resolveConfig(cliArgs, cwd);
56
- const format = config.format !== 'auto'
57
- ? config.format
58
- : detectFormatFromDir(config.localesDir);
59
- const ext = getExtension(format);
60
- const inputLocale = config.inputLocale;
61
- const sourceFile = `${inputLocale}${ext}`;
62
- const sourcePath = path.join(config.localesDir, sourceFile);
56
+ // Every source file of the layout is watched: one for a flat project,
57
+ // one per namespace for a folder-per-locale project (en/common.json,
58
+ // en/admin.json, …). Docusaurus watches its source locale's JSON the
59
+ // same way the flat lane always did (i18n/<source>.json does not exist
60
+ // there, so it keeps the historical single-path behaviour).
61
+ let sourcePaths;
62
+ let sourceLabel;
63
+ if (config.format === 'docusaurus') {
64
+ sourcePaths = [path.join(config.localesDir, `${config.inputLocale}.json`)];
65
+ sourceLabel = `${config.inputLocale}.json`;
66
+ } else {
67
+ const layout = discoverLocaleLayout(config, { cwd });
68
+ sourcePaths = layout.sourceFiles.map(f => f.path);
69
+ sourceLabel = layout.namespaced
70
+ ? `${layout.sourceFiles.length} source file(s) (${layout.display})`
71
+ : layout.sourceFiles[0].rel;
72
+ if (sourcePaths.length === 0) {
73
+ throw new Error(`No ${layout.format} source files found for ${config.inputLocale} (${layout.display}) — nothing to watch.`);
74
+ }
75
+ }
76
+ const sourceFile = sourceLabel;
63
77
 
64
78
  output.info(`Watching ${sourceFile} for changes...`);
65
79
 
@@ -101,9 +115,11 @@ async function startWatch(options = {}) {
101
115
  }
102
116
  }
103
117
 
104
- // Watch the source file using stat polling (fs.watchFile).
105
- // On change, debounce for 500ms then sync.
106
- fs.watchFile(sourcePath, { interval: 500 }, (curr, prev) => {
118
+ // Watch the source file(s) using stat polling (fs.watchFile).
119
+ // On change, debounce for 500ms then sync. A namespace file ADDED after
120
+ // the watcher started is picked up by the next sync, but is not itself
121
+ // watched until watch mode restarts.
122
+ const onChange = (curr, prev) => {
107
123
  // Only react to actual content changes (mtime changed)
108
124
  if (curr.mtimeMs === prev.mtimeMs) return;
109
125
 
@@ -120,7 +136,10 @@ async function startWatch(options = {}) {
120
136
  // inside doSync() and logged, never thrown.
121
137
  doSync();
122
138
  }, 500);
123
- });
139
+ };
140
+ for (const sourcePath of sourcePaths) {
141
+ fs.watchFile(sourcePath, { interval: 500 }, onChange);
142
+ }
124
143
 
125
144
  // Signal that the watcher is active and ready for file changes.
126
145
  // Tests use this line to know when it's safe to modify the source file.
@@ -135,7 +154,7 @@ async function startWatch(options = {}) {
135
154
  // if startWatch were ever called more than once in the same process.
136
155
  process.once('SIGINT', () => {
137
156
  if (debounceTimer) clearTimeout(debounceTimer);
138
- fs.unwatchFile(sourcePath);
157
+ for (const sourcePath of sourcePaths) fs.unwatchFile(sourcePath);
139
158
  resolve(); // Allow the promise to resolve so cleanup can happen
140
159
  process.exit(0);
141
160
  });
package/lib/xliff.js CHANGED
@@ -20,6 +20,15 @@
20
20
  * third-party tools with extensions, inline markup, etc. If a user
21
21
  * imports XLIFF from memoQ, we extract <target> text and ignore the rest.
22
22
  *
23
+ * UNIT IDS are locale keys. A gettext key with a context is
24
+ * `msgctxt + U+0004 + msgid` (lib/po.js), and XML 1.0 cannot carry U+0004
25
+ * at all — not even as a character reference — so the id writes it as "␄"
26
+ * (U+2404, the visible symbol for that character, the same form reports
27
+ * and `--force-keys` use) and import maps it back. Newlines, tabs and CRs
28
+ * in an id (multi-line msgids) are written as character references: a
29
+ * literal newline in an attribute is turned into a space by every XML
30
+ * parser, which would silently rename the key in a CAT tool round trip.
31
+ *
23
32
  * ZERO DEPENDENCIES. Uses regex-based XML parsing because:
24
33
  * 1. Our XLIFF output is predictable and well-formed
25
34
  * 2. We only need to extract source/target text from <trans-unit> elements
@@ -66,7 +75,7 @@ function exportXLIFF({ sourceLocale, targetLocale, sourceFlat, targetFlat, origi
66
75
  const body = exempt ? sourceValue : (hasTarget ? targetValue : '');
67
76
 
68
77
  units.push(
69
- ` <trans-unit id="${escapeXML(key)}"${exempt ? ' translate="no"' : ''} xml:space="preserve">` +
78
+ ` <trans-unit id="${encodeUnitId(key)}"${exempt ? ' translate="no"' : ''} xml:space="preserve">` +
70
79
  `\n <source>${escapeXML(sourceValue)}</source>` +
71
80
  `\n <target state="${state}">${escapeXML(body)}</target>` +
72
81
  `\n </trans-unit>`
@@ -125,7 +134,7 @@ function importXLIFF(xliffString) {
125
134
  let match;
126
135
 
127
136
  while ((match = unitPattern.exec(xliffString)) !== null) {
128
- const unitId = unescapeXML(match[1]);
137
+ const unitId = decodeUnitId(match[1]);
129
138
  const unitContent = match[0];
130
139
 
131
140
  // Extract target content (if present and non-empty)
@@ -157,14 +166,44 @@ function escapeXML(str) {
157
166
  .replace(/'/g, '&apos;');
158
167
  }
159
168
 
169
+ /** gettext's msgctxt separator, and how an XLIFF id writes it. */
170
+ const CONTEXT_SEPARATOR = '\u0004';
171
+ const CONTEXT_SEPARATOR_VISIBLE = '\u2404';
172
+
173
+ /**
174
+ * A locale key as an XLIFF unit id (see "UNIT IDS" above).
175
+ *
176
+ * @param {string} key
177
+ * @returns {string} Attribute-safe id
178
+ */
179
+ function encodeUnitId(key) {
180
+ return escapeXML(key.split(CONTEXT_SEPARATOR).join(CONTEXT_SEPARATOR_VISIBLE))
181
+ .replace(/\n/g, '&#10;')
182
+ .replace(/\r/g, '&#13;')
183
+ .replace(/\t/g, '&#9;');
184
+ }
185
+
186
+ /**
187
+ * The locale key an XLIFF unit id names.
188
+ *
189
+ * @param {string} raw - Attribute value as found in the file
190
+ * @returns {string}
191
+ */
192
+ function decodeUnitId(raw) {
193
+ return unescapeXML(raw).split(CONTEXT_SEPARATOR_VISIBLE).join(CONTEXT_SEPARATOR);
194
+ }
195
+
160
196
  /**
161
- * Unescape XML entities back to raw characters.
197
+ * Unescape XML entities back to raw characters (named entities and
198
+ * numeric character references).
162
199
  *
163
200
  * @param {string} str - XML-escaped string
164
201
  * @returns {string} Raw string
165
202
  */
166
203
  function unescapeXML(str) {
167
204
  return str
205
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
206
+ .replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number(dec)))
168
207
  .replace(/&apos;/g, "'")
169
208
  .replace(/&quot;/g, '"')
170
209
  .replace(/&gt;/g, '>')
@@ -181,4 +220,6 @@ export {
181
220
  importXLIFF,
182
221
  escapeXML,
183
222
  unescapeXML,
223
+ encodeUnitId,
224
+ decodeUnitId,
184
225
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "champollion",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Research-grade translation engine for i18n projects. Pluggable methods, per-pair quality tiers, and deterministic script converters. Supports JSON (next-intl, i18next), TOML, and YAML (Hugo).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,6 +54,8 @@ A pair-specific, community-curated dataset used for official Champollion evaluat
54
54
 
55
55
  The `eval-` prefix makes it instantly clear this is a benchmark, not just a dataset catalogue entry.
56
56
 
57
+ The `{segment}` part is optional: the schema's id pattern only requires `eval-`/`ref-` plus lower-case kebab-case, and some cards carry none (`eval-in22-gen-v1`). Cards written by `champollion register-corpus` are `eval-{src}-{tgt}-{name}[-{role}]-v1`: `{name}` is the slugged `--name` (the publisher only when the name has no a–z/0–9 letters), and `{role}` (`test` / `dev` / `train`) appears only when the author passed `--role`. No role is ever guessed, and an id already registered is never re-derived.
58
+
57
59
  ---
58
60
 
59
61
  ## Official Champollion Evaluation