champollion 0.3.3 → 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.
- package/README.md +52 -37
- package/bin/cli.js +53 -5
- package/index.js +63 -2
- package/lib/api-key.js +17 -4
- package/lib/autofix.js +83 -36
- package/lib/bridge/method_bridge.py +15 -3
- package/lib/cards/reader.js +51 -3
- package/lib/cards/remote.js +15 -0
- package/lib/cards/search-names.js +178 -0
- package/lib/command-help.js +289 -88
- package/lib/commands/audit.js +10 -3
- package/lib/commands/card.js +583 -226
- package/lib/commands/doctor.js +54 -18
- package/lib/commands/help.js +37 -32
- package/lib/commands/init.js +1689 -87
- package/lib/commands/integrity.js +127 -40
- package/lib/commands/leaderboard.js +187 -67
- package/lib/commands/models.js +9 -2
- package/lib/commands/provenance.js +7 -2
- package/lib/commands/recommend.js +43 -14
- package/lib/commands/register-corpus.js +649 -130
- package/lib/commands/seal-corpus.js +1 -1
- package/lib/commands/status.js +564 -27
- package/lib/commands/submit.js +17 -12
- package/lib/commands/sync.js +31 -7
- package/lib/commands/tm.js +16 -10
- package/lib/commands/verify.js +27 -3
- package/lib/commands/wrap.js +63 -5
- package/lib/commands/xliff.js +135 -64
- package/lib/commercial-eligibility.js +1 -1
- package/lib/config.js +196 -14
- package/lib/content-estimate.js +96 -0
- package/lib/content-refusals.js +270 -0
- package/lib/content-review.js +372 -0
- package/lib/content-sync.js +1127 -344
- package/lib/content.js +94 -7
- package/lib/corpus-registration.mjs +197 -38
- package/lib/cost-label.js +29 -0
- package/lib/cost-report.js +726 -78
- package/lib/diff.js +38 -4
- package/lib/docusaurus-sync.js +965 -253
- package/lib/edit-distance.js +31 -0
- package/lib/fallback.js +964 -0
- package/lib/file-scope.js +106 -0
- package/lib/flatten.js +80 -3
- package/lib/flutter-locales.js +124 -0
- package/lib/format.js +266 -12
- package/lib/hash.js +146 -21
- package/lib/icu-structure.js +929 -0
- package/lib/integrity.js +223 -75
- package/lib/language-pair.js +157 -0
- package/lib/lint.js +78 -16
- package/lib/local-only-marks.js +106 -0
- package/lib/locale-layout.js +1103 -0
- package/lib/locale-state.js +571 -0
- package/lib/methods/anthropic.js +5 -0
- package/lib/methods/apertium.js +6 -3
- package/lib/methods/api.js +138 -25
- package/lib/methods/base.js +17 -0
- package/lib/methods/coaching-data.js +153 -0
- package/lib/methods/content-separator.js +43 -0
- package/lib/methods/deepl.js +1 -1
- package/lib/methods/direct-llm.js +252 -103
- package/lib/methods/external.js +146 -63
- package/lib/methods/gemini.js +1 -0
- package/lib/methods/google-translate.js +1 -0
- package/lib/methods/http-utils.js +41 -0
- package/lib/methods/libretranslate.js +7 -2
- package/lib/methods/llm-coached.js +68 -128
- package/lib/methods/llm.js +80 -31
- package/lib/methods/local.js +93 -10
- package/lib/methods/microsoft-translator.js +1 -2
- package/lib/methods/openai.js +4 -2
- package/lib/methods/openrouter-client.js +20 -19
- package/lib/methods/openrouter-pricing.js +150 -13
- package/lib/methods/prompt-methods.js +20 -0
- package/lib/methods/provider-pricing.js +42 -1
- package/lib/methods/request-capture.js +104 -0
- package/lib/methods/tilde.js +1 -1
- package/lib/methods/translated.js +1 -2
- package/lib/missing-key.js +93 -0
- package/lib/models.js +11 -0
- package/lib/name-rules.js +32 -0
- package/lib/named-keys.js +172 -0
- package/lib/no-translate.js +4 -3
- package/lib/output.js +160 -19
- package/lib/pairs.js +586 -30
- package/lib/placeholders.js +394 -0
- package/lib/plugins.js +8 -0
- package/lib/plural-gap-redo.js +109 -0
- package/lib/plurals.js +323 -0
- package/lib/po.js +1187 -0
- package/lib/public-catalogue.js +74 -0
- package/lib/recommend.js +527 -32
- package/lib/redo.js +95 -0
- package/lib/refusal-category.js +44 -0
- package/lib/registers.js +255 -11
- package/lib/repair-script.js +20 -13
- package/lib/scripts.js +193 -106
- package/lib/seal.mjs +6 -5
- package/lib/sealed-qualifier.mjs +2 -2
- package/lib/segment.js +2 -1
- package/lib/seo.js +19 -9
- package/lib/serve.js +43 -6
- package/lib/shared-output-seed.js +164 -0
- package/lib/source-contexts.js +39 -0
- package/lib/submit.mjs +57 -5
- package/lib/sync.js +2923 -474
- package/lib/terminology.js +13 -4
- package/lib/tm-evict.js +179 -0
- package/lib/tm-seed.js +5 -2
- package/lib/tm.js +818 -36
- package/lib/translate-pair.js +639 -34
- package/lib/translate.js +78 -5
- package/lib/types.js +22 -3
- package/lib/validate.js +880 -17
- package/lib/verify.js +1296 -104
- package/lib/watch.js +32 -13
- package/lib/xliff.js +44 -3
- package/package.json +3 -2
- package/shared/CORPORA-CARDS.md +2 -0
- package/shared/DATA-SOVEREIGNTY.md +19 -20
- package/shared/LANGUAGE-CARD-FIELDS.md +1 -1
- package/shared/cards-fallback.json +1 -1
- package/shared/catalogue/card-config.json +1 -1
- package/shared/curated-orthography-conventions.json +26 -8
- package/shared/docent/faq.en.json +14 -16
- package/shared/docent/system-prompt.md +17 -19
- package/shared/explainers/tc-features.json +15 -15
- package/shared/gettext-plural-forms.json +45 -0
- package/shared/human-services.json +1 -1
- package/shared/method-registry.json +2 -0
- package/shared/metric-registry.json +96 -18
- package/shared/schemas/champollion-plugin.schema.json +4 -0
- package/shared/schemas/corpora-card.schema.json +20 -10
- package/shared/schemas/human-services.schema.json +2 -2
- package/shared/schemas/language-card.schema.json +1 -1
- package/shared/schemas/method-card.schema.json +1 -1
- package/shared/schemas/method-index-record.schema.json +67 -0
- package/shared/schemas/method-registry.schema.json +4 -0
- package/shared/schemas/metric-registry.schema.json +55 -1
- package/shared/docent/corpus.json +0 -11333
package/lib/plurals.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18next plural keys — give every target locale exactly ITS plural forms.
|
|
3
|
+
*
|
|
4
|
+
* THE PROBLEM: i18next (v21+, "JSON v4") stores plurals as sibling keys with
|
|
5
|
+
* a CLDR category suffix:
|
|
6
|
+
*
|
|
7
|
+
* en.json { "item_one": "{{count}} item", "item_other": "{{count}} items" }
|
|
8
|
+
*
|
|
9
|
+
* English has two categories (one, other). French and Spanish have three
|
|
10
|
+
* (one, many, other); Japanese has one (other); Arabic has six. A sync that
|
|
11
|
+
* mirrors the SOURCE's keys writes `item_one`/`item_other` into every
|
|
12
|
+
* locale: French silently loses its `many` form (i18next falls back to the
|
|
13
|
+
* key for 1 000 000), and Japanese carries a `_one` it never selects.
|
|
14
|
+
*
|
|
15
|
+
* THE RULE (applied per target locale, json key-value files only):
|
|
16
|
+
* - The target's categories come from CLDR via
|
|
17
|
+
* Intl.PluralRules(target).resolvedOptions().pluralCategories — no
|
|
18
|
+
* hardcoded language table.
|
|
19
|
+
* - Each target category is translated from the source's `_other` form,
|
|
20
|
+
* except `_one` (from the source's `_one`) and `_zero` (from the
|
|
21
|
+
* source's `_zero` when it has one — i18next looks `key_zero` up for
|
|
22
|
+
* count 0 in EVERY language, so a source that defines it keeps it).
|
|
23
|
+
* - Source categories the target does not use are not created. If an
|
|
24
|
+
* earlier sync wrote one, sync removes it only when the Translation
|
|
25
|
+
* Memory proves the pipeline produced that value; a hand-written value
|
|
26
|
+
* is kept (and reported as an extra key).
|
|
27
|
+
* - Ordinal groups (`key_ordinal_one`, …) use Intl.PluralRules with
|
|
28
|
+
* type 'ordinal'.
|
|
29
|
+
* - A locale CLDR has no plural rules for (Intl falls back to another
|
|
30
|
+
* locale) keeps the source's categories, and the caller says so —
|
|
31
|
+
* guessing a category set for a language CLDR does not describe would
|
|
32
|
+
* be inventing grammar.
|
|
33
|
+
*
|
|
34
|
+
* DETECTION is deliberately strict, because a false positive invents keys:
|
|
35
|
+
* a group needs an `_other` key plus at least one other category, OR — for
|
|
36
|
+
* a source language whose only category is `other` (ja, zh, ko) — an
|
|
37
|
+
* `_other` value that interpolates {{count}}. A lone `gender_other: "Other"`
|
|
38
|
+
* is an ordinary key.
|
|
39
|
+
*
|
|
40
|
+
* Everything here is pure: callers (sync, the cost estimate, verify,
|
|
41
|
+
* integrity) pass the source map in and get the per-target expected map out,
|
|
42
|
+
* so all of them judge a target by the same keys.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const CATEGORIES = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
|
46
|
+
const SUFFIX_RE = /^(.+?)(_ordinal)?_(zero|one|two|few|many|other)$/;
|
|
47
|
+
|
|
48
|
+
/** Normalize a locale code for Intl (pt_BR → pt-BR). */
|
|
49
|
+
function intlTag(code) {
|
|
50
|
+
return String(code).replace(/_/g, '-');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* CLDR plural categories for a locale, or null when CLDR (as shipped in
|
|
55
|
+
* this runtime's Intl) has no rules for it.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} code - Locale code
|
|
58
|
+
* @param {'cardinal'|'ordinal'} [type='cardinal']
|
|
59
|
+
* @returns {string[]|null}
|
|
60
|
+
*/
|
|
61
|
+
function pluralCategoriesFor(code, type = 'cardinal') {
|
|
62
|
+
const tag = intlTag(code);
|
|
63
|
+
try {
|
|
64
|
+
if (Intl.PluralRules.supportedLocalesOf([tag]).length === 0) return null;
|
|
65
|
+
return new Intl.PluralRules(tag, { type }).resolvedOptions().pluralCategories;
|
|
66
|
+
} catch {
|
|
67
|
+
// RangeError: not a well-formed BCP 47 tag — no CLDR rules either.
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Find i18next plural groups in a flat source map.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} sourceFlat - Flat key → value map of ONE source file
|
|
76
|
+
* @param {string} sourceLocale - Source locale code
|
|
77
|
+
* @returns {Map<string, { base: string, ordinal: boolean, keys: Object<string, string> }>}
|
|
78
|
+
* groupId → group; `keys` maps category → source key
|
|
79
|
+
*/
|
|
80
|
+
function findPluralGroups(sourceFlat, sourceLocale) {
|
|
81
|
+
const candidates = new Map();
|
|
82
|
+
for (const [key, value] of Object.entries(sourceFlat)) {
|
|
83
|
+
if (typeof value !== 'string') continue;
|
|
84
|
+
const m = SUFFIX_RE.exec(key);
|
|
85
|
+
if (!m) continue;
|
|
86
|
+
const ordinal = !!m[2];
|
|
87
|
+
const id = `${m[1]}\u0000${ordinal ? 'ordinal' : 'cardinal'}`;
|
|
88
|
+
if (!candidates.has(id)) candidates.set(id, { base: m[1], ordinal, keys: {} });
|
|
89
|
+
candidates.get(id).keys[m[3]] = key;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const groups = new Map();
|
|
93
|
+
for (const [id, group] of candidates) {
|
|
94
|
+
const cats = Object.keys(group.keys);
|
|
95
|
+
if (!cats.includes('other')) continue;
|
|
96
|
+
if (cats.length >= 2) { groups.set(id, group); continue; }
|
|
97
|
+
// Single-category source languages (ja/zh/ko…) write only `_other`.
|
|
98
|
+
const sourceCats = pluralCategoriesFor(sourceLocale, group.ordinal ? 'ordinal' : 'cardinal');
|
|
99
|
+
const otherValue = sourceFlat[group.keys.other];
|
|
100
|
+
if (sourceCats && sourceCats.length === 1 && sourceCats[0] === 'other'
|
|
101
|
+
&& /\{\{\s*count\b/.test(otherValue)) {
|
|
102
|
+
groups.set(id, group);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return groups;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A few integers that select `category` in `code` — prompt context so the
|
|
110
|
+
* model writes, e.g., French `many` as the form for 1 000 000.
|
|
111
|
+
*/
|
|
112
|
+
function sampleCounts(code, category, type) {
|
|
113
|
+
let rules;
|
|
114
|
+
try { rules = new Intl.PluralRules(intlTag(code), { type }); } catch { return []; }
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const n of [0, 1, 2, 3, 4, 5, 6, 7, 11, 12, 21, 22, 100, 101, 1000000]) {
|
|
117
|
+
if (rules.select(n) === category) out.push(n);
|
|
118
|
+
if (out.length === 3) break;
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build the map a target locale is expected to contain for one source file.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} sourceFlat - Flat source map (one file)
|
|
127
|
+
* @param {string} sourceLocale
|
|
128
|
+
* @param {string} targetLocale
|
|
129
|
+
* @param {Map} [groups] - Precomputed findPluralGroups() result
|
|
130
|
+
* @returns {null | {
|
|
131
|
+
* flat: object, // expected target keys → SOURCE text to translate
|
|
132
|
+
* origin: Object<string, string>, // target key → source key it is translated from
|
|
133
|
+
* descriptions: Object<string, string>, // prompt context for generated categories
|
|
134
|
+
* unused: string[], // source plural keys this target does not use
|
|
135
|
+
* borrowed: Object<string, string>, // target key → its plural form, for a key
|
|
136
|
+
* // translated from ANOTHER category's source
|
|
137
|
+
* // (French `_many` from `_other`): "many",
|
|
138
|
+
* // "ordinal-few" — its own cache identity
|
|
139
|
+
* // (lib/tm-evict.js tmTextFor)
|
|
140
|
+
* groups: number,
|
|
141
|
+
* unknownLocale: boolean, // CLDR has no rules → source categories kept
|
|
142
|
+
* }} null when the file has no plural groups (callers keep the source map as is)
|
|
143
|
+
*/
|
|
144
|
+
function expandPluralsForLocale(sourceFlat, sourceLocale, targetLocale, groups = null) {
|
|
145
|
+
const found = groups || findPluralGroups(sourceFlat, sourceLocale);
|
|
146
|
+
if (found.size === 0) return null;
|
|
147
|
+
|
|
148
|
+
const groupOfKey = new Map();
|
|
149
|
+
for (const [id, group] of found) {
|
|
150
|
+
for (const key of Object.values(group.keys)) groupOfKey.set(key, id);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const flat = {};
|
|
154
|
+
const origin = {};
|
|
155
|
+
const descriptions = {};
|
|
156
|
+
const unused = [];
|
|
157
|
+
const borrowed = {};
|
|
158
|
+
const emitted = new Set();
|
|
159
|
+
let unknownLocale = false;
|
|
160
|
+
|
|
161
|
+
for (const [key, value] of Object.entries(sourceFlat)) {
|
|
162
|
+
const id = groupOfKey.get(key);
|
|
163
|
+
if (id === undefined) { flat[key] = value; continue; }
|
|
164
|
+
if (emitted.has(id)) continue;
|
|
165
|
+
emitted.add(id);
|
|
166
|
+
|
|
167
|
+
const group = found.get(id);
|
|
168
|
+
const type = group.ordinal ? 'ordinal' : 'cardinal';
|
|
169
|
+
const sourceCats = Object.keys(group.keys);
|
|
170
|
+
let targetCats = pluralCategoriesFor(targetLocale, type);
|
|
171
|
+
// No CLDR rules for this locale: mirror the source's forms one-to-one
|
|
172
|
+
// (the pre-plural-aware behaviour), and let the caller report it.
|
|
173
|
+
const mirror = !targetCats;
|
|
174
|
+
if (mirror) {
|
|
175
|
+
unknownLocale = true;
|
|
176
|
+
targetCats = CATEGORIES.filter(c => sourceCats.includes(c));
|
|
177
|
+
} else if (group.keys.zero && !targetCats.includes('zero')) {
|
|
178
|
+
// i18next resolves key_zero for count 0 in every language.
|
|
179
|
+
targetCats = ['zero', ...targetCats];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const prefix = `${group.base}${group.ordinal ? '_ordinal' : ''}_`;
|
|
183
|
+
for (const cat of CATEGORIES.filter(c => targetCats.includes(c))) {
|
|
184
|
+
const fromCat = mirror || ((cat === 'one' || cat === 'zero') && group.keys[cat]) ? cat : 'other';
|
|
185
|
+
const fromKey = group.keys[fromCat];
|
|
186
|
+
const targetKey = `${prefix}${cat}`;
|
|
187
|
+
flat[targetKey] = sourceFlat[fromKey];
|
|
188
|
+
origin[targetKey] = fromKey;
|
|
189
|
+
if (fromCat !== cat) {
|
|
190
|
+
// Same source text as the key it borrows from, a different form:
|
|
191
|
+
// cached on its own (Round 6, i18next persona — `_many` and `_other`
|
|
192
|
+
// shared one entry, and a redo wrote the `_many` text into both).
|
|
193
|
+
borrowed[targetKey] = group.ordinal ? `ordinal-${cat}` : cat;
|
|
194
|
+
const samples = sampleCounts(targetLocale, cat, type);
|
|
195
|
+
descriptions[targetKey] = `${group.ordinal ? 'Ordinal' : 'Plural'} form "${cat}" (CLDR) of this message`
|
|
196
|
+
+ (samples.length > 0 ? `, used for counts such as ${samples.join(', ')}` : '')
|
|
197
|
+
+ '. Translate the plural text into that grammatical form.';
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const cat of sourceCats) {
|
|
201
|
+
if (!targetCats.includes(cat)) unused.push(group.keys[cat]);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { flat, origin, descriptions, unused, borrowed, groups: found.size, unknownLocale };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Map source-key lists (changed keys, forced keys) into the target key
|
|
210
|
+
* space: a generated key is changed/forced when the source key it is
|
|
211
|
+
* translated from is.
|
|
212
|
+
*
|
|
213
|
+
* @param {string[]} sourceKeys
|
|
214
|
+
* @param {ReturnType<typeof expandPluralsForLocale>} expansion
|
|
215
|
+
* @returns {string[]}
|
|
216
|
+
*/
|
|
217
|
+
function mapSourceKeysToTarget(sourceKeys, expansion) {
|
|
218
|
+
if (!expansion || !sourceKeys || sourceKeys.length === 0) return sourceKeys || [];
|
|
219
|
+
const set = new Set(sourceKeys);
|
|
220
|
+
const out = new Set(sourceKeys.filter(k => Object.prototype.hasOwnProperty.call(expansion.flat, k)));
|
|
221
|
+
for (const [targetKey, fromKey] of Object.entries(expansion.origin)) {
|
|
222
|
+
if (set.has(fromKey) || set.has(targetKey)) out.add(targetKey);
|
|
223
|
+
}
|
|
224
|
+
return [...out];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The source key a target key's value was translated from (identity for
|
|
229
|
+
* ordinary keys). Used to restore manifest hashes for failed keys.
|
|
230
|
+
*
|
|
231
|
+
* @param {string} targetKey
|
|
232
|
+
* @param {ReturnType<typeof expandPluralsForLocale>} expansion
|
|
233
|
+
* @returns {string}
|
|
234
|
+
*/
|
|
235
|
+
function originKey(targetKey, expansion) {
|
|
236
|
+
return (expansion && expansion.origin[targetKey]) || targetKey;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* What each target's CLDR plural forms change from the source's, in words,
|
|
241
|
+
* for the run's own targets: "es, fr add _many; ja keeps only _other; de: as
|
|
242
|
+
* en". Read from Intl.PluralRules (CLDR) at run time — the line used to say
|
|
243
|
+
* "e.g. French adds _many" whatever the project's languages, and the docs
|
|
244
|
+
* named French where Spanish gains the same form (Round 13, i18next persona).
|
|
245
|
+
*
|
|
246
|
+
* @param {string} sourceLocale
|
|
247
|
+
* @param {string[]} targets
|
|
248
|
+
* @param {'cardinal'|'ordinal'} [type='cardinal']
|
|
249
|
+
* @returns {string} '' when nothing can be said (no CLDR rules for the source)
|
|
250
|
+
*/
|
|
251
|
+
function describePluralFormChanges(sourceLocale, targets, type = 'cardinal') {
|
|
252
|
+
const src = pluralCategoriesFor(sourceLocale, type);
|
|
253
|
+
if (!src) return '';
|
|
254
|
+
const order = (cats) => CATEGORIES.filter(c => cats.includes(c)).map(c => `_${c}`).join(', ');
|
|
255
|
+
// One phrase per kind of change, the targets that share it named together.
|
|
256
|
+
const groups = new Map();
|
|
257
|
+
for (const code of [...new Set(targets)].sort()) {
|
|
258
|
+
const cats = pluralCategoriesFor(code, type);
|
|
259
|
+
let kind;
|
|
260
|
+
if (!cats) kind = { id: 'none' };
|
|
261
|
+
else {
|
|
262
|
+
const added = cats.filter(c => !src.includes(c));
|
|
263
|
+
const dropped = src.filter(c => !cats.includes(c));
|
|
264
|
+
if (added.length === 0 && dropped.length === 0) kind = { id: 'same' };
|
|
265
|
+
else if (cats.length === 1) kind = { id: `only ${cats[0]}`, only: cats[0] };
|
|
266
|
+
else kind = { id: `+${order(added)} -${order(dropped)}`, added: order(added), dropped: order(dropped) };
|
|
267
|
+
}
|
|
268
|
+
if (!groups.has(kind.id)) groups.set(kind.id, { kind, codes: [] });
|
|
269
|
+
groups.get(kind.id).codes.push(code);
|
|
270
|
+
}
|
|
271
|
+
return [...groups.values()].map(({ kind, codes }) => {
|
|
272
|
+
const one = codes.length === 1;
|
|
273
|
+
const who = codes.join(', ');
|
|
274
|
+
if (kind.id === 'none') return `${who}: no CLDR rules (the source's forms are kept)`;
|
|
275
|
+
if (kind.id === 'same') return `${who}: the same forms as ${sourceLocale}`;
|
|
276
|
+
if (kind.only) return `${who} ${one ? 'keeps' : 'keep'} only _${kind.only}`;
|
|
277
|
+
return `${who} ${[
|
|
278
|
+
kind.added && `${one ? 'adds' : 'add'} ${kind.added}`,
|
|
279
|
+
kind.dropped && `${one ? 'drops' : 'drop'} ${kind.dropped}`,
|
|
280
|
+
].filter(Boolean).join(' and ')}`;
|
|
281
|
+
}).join('; ');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Keys a target file holds for an i18next plural form its language does not
|
|
286
|
+
* have (Spanish `count_two`, from a source with `count_one`/`count_other`):
|
|
287
|
+
* i18next never selects them. `_zero` is never one (i18next uses it for 0 in
|
|
288
|
+
* every language), and a key the source file itself defines is left to the
|
|
289
|
+
* rule above (removed when the cache proves sync wrote it). ONE rule for
|
|
290
|
+
* what `verify` warns about and what `sync --prune plural-extras` removes —
|
|
291
|
+
* nothing else is ever removed by it (Round 13, i18next persona: verify said
|
|
292
|
+
* "delete them" and offered no way to but by hand).
|
|
293
|
+
*
|
|
294
|
+
* @param {{ flat: object, pluralGroups?: Map }} unit - A source unit (lib/locale-layout.js)
|
|
295
|
+
* @param {object} targetFlat - The target file's flat map
|
|
296
|
+
* @param {string} locale
|
|
297
|
+
* @returns {Array<{ key: string, category: string, cats: string[] }>}
|
|
298
|
+
*/
|
|
299
|
+
function pluralExtraKeys(unit, targetFlat, locale) {
|
|
300
|
+
const out = [];
|
|
301
|
+
if (!unit?.pluralGroups || unit.pluralGroups.size === 0 || !targetFlat) return out;
|
|
302
|
+
const has = (obj, k) => Object.prototype.hasOwnProperty.call(obj, k);
|
|
303
|
+
for (const group of unit.pluralGroups.values()) {
|
|
304
|
+
const cats = pluralCategoriesFor(locale, group.ordinal ? 'ordinal' : 'cardinal');
|
|
305
|
+
if (!cats) continue;
|
|
306
|
+
for (const c of CATEGORIES) {
|
|
307
|
+
if (c === 'zero' || cats.includes(c)) continue;
|
|
308
|
+
const key = `${group.base}${group.ordinal ? '_ordinal' : ''}_${c}`;
|
|
309
|
+
if (has(targetFlat, key) && !has(unit.flat, key)) out.push({ key, category: c, cats });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export {
|
|
316
|
+
pluralCategoriesFor,
|
|
317
|
+
findPluralGroups,
|
|
318
|
+
expandPluralsForLocale,
|
|
319
|
+
mapSourceKeysToTarget,
|
|
320
|
+
originKey,
|
|
321
|
+
describePluralFormChanges,
|
|
322
|
+
pluralExtraKeys,
|
|
323
|
+
};
|