@babelize/engine 0.1.2 → 0.1.3

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.
@@ -34,9 +34,18 @@ const TRANSLATE_CONCURRENCY = 3; // Max concurrent locales/batches
34
34
  * many sentences (the translator service's batch limit).
35
35
  */
36
36
  const BRANCH_BATCH_SIZE = 50;
37
- /** A plain source whose stored translation is ICU only fits a project whose runtime formats ICU. */
38
- function reusableTranslation(source, translated, targetPlurals) {
39
- return targetPlurals || (0, icu_message_1.isIcuMessage)(source) || !(0, icu_message_1.isIcuMessage)(translated);
37
+ /**
38
+ * A plain source whose stored translation is ICU only fits a project whose
39
+ * runtime formats ICU. A stored ICU translation stored before a check it now
40
+ * fails (e.g. Czech without its `few` branch) is translated again.
41
+ */
42
+ function reusableTranslation(source, translated, targetPlurals, targetLocale) {
43
+ if (!targetPlurals && !(0, icu_message_1.isIcuMessage)(source) && (0, icu_message_1.isIcuMessage)(translated))
44
+ return false;
45
+ if (!(0, icu_message_1.isIcuMessage)(translated))
46
+ return true;
47
+ const pluralizable = (0, icu_message_1.isIcuMessage)(source) ? [] : (0, icu_message_1.countArguments)(source);
48
+ return !(0, icu_message_1.compareIcuMessages)(source, translated, targetLocale, { pluralizable }).some((i) => i.severity === 'error');
40
49
  }
41
50
  /** Per-entry instruction: pluralize a count argument if the target language needs it. */
42
51
  function pluralHint(args, targetLocale) {
@@ -419,7 +428,7 @@ async function translateEntries(entries, sourceLang, targetLang, projectId, onPr
419
428
  const cacheProblem = cached.hit && cached.data !== null ? reuseProblem(text, cached.data) : null;
420
429
  if (cacheProblem)
421
430
  logger_1.engineLogger.info('Cached translation breaks the glossary; translating again', { key, targetLocale, problem: cacheProblem });
422
- if (cached.hit && cached.data !== null && !cacheProblem && reusableTranslation(text, cached.data, !!options.targetPlurals) && !(0, untranslated_1.isUntranslatedInto)(text, cached.data, targetLocale)) {
431
+ if (cached.hit && cached.data !== null && !cacheProblem && reusableTranslation(text, cached.data, !!options.targetPlurals, targetLocale) && !(0, untranslated_1.isUntranslatedInto)(text, cached.data, targetLocale)) {
423
432
  result[key] = cached.data;
424
433
  cacheHits++;
425
434
  // Reused, not translated this run (billing counts only fresh output).
@@ -497,7 +506,7 @@ async function translateEntries(entries, sourceLang, targetLang, projectId, onPr
497
506
  const tmProblem = lookup.hit && lookup.translatedText ? reuseProblem(entry.value, lookup.translatedText) : null;
498
507
  if (tmProblem)
499
508
  logger_1.engineLogger.info('TM translation breaks the glossary; translating again', { key: entry.key, targetLocale, problem: tmProblem });
500
- if (lookup.hit && lookup.translatedText && !tmProblem && reusableTranslation(entry.value, lookup.translatedText, !!options.targetPlurals) && !(0, untranslated_1.isUntranslatedInto)(entry.value, lookup.translatedText, targetLocale)) {
509
+ if (lookup.hit && lookup.translatedText && !tmProblem && reusableTranslation(entry.value, lookup.translatedText, !!options.targetPlurals, targetLocale) && !(0, untranslated_1.isUntranslatedInto)(entry.value, lookup.translatedText, targetLocale)) {
501
510
  result[entry.key] = lookup.translatedText;
502
511
  tmHits++;
503
512
  metadata[entry.key] = { fromMemory: true, inTm: true };
@@ -38,8 +38,8 @@ export interface IcuIssue {
38
38
  /**
39
39
  * Compare a translated ICU message to its source. Errors mean the message is
40
40
  * broken at runtime (unparseable, arguments renamed, select branches changed,
41
- * no `other`). Missing target plural categories are only a warning: ICU
42
- * falls back to `other` for them.
41
+ * no `other`, a target plural category whole numbers use). A missing category
42
+ * only fractions use is a warning: ICU falls back to `other` for it.
43
43
  */
44
44
  export declare function compareIcuMessages(source: string, translated: string, targetLocale: string, options?: {
45
45
  /**
@@ -190,11 +190,29 @@ function pluralCategories(locale, type) {
190
190
  return [...CLDR_ORDER];
191
191
  }
192
192
  }
193
+ /**
194
+ * Whole numbers (0–999) a plural category is used for, e.g. Czech few → 2, 3, 4,
195
+ * Czech many → none (only fractions like 1.5). CLDR integer rules repeat
196
+ * within every hundred, so this range covers them.
197
+ */
198
+ function integersOf(locale, type, category) {
199
+ try {
200
+ const rules = new Intl.PluralRules(locale, { type });
201
+ const out = [];
202
+ for (let n = 0; n < 1000; n++)
203
+ if (rules.select(n) === category)
204
+ out.push(n);
205
+ return out;
206
+ }
207
+ catch {
208
+ return [];
209
+ }
210
+ }
193
211
  /**
194
212
  * Compare a translated ICU message to its source. Errors mean the message is
195
213
  * broken at runtime (unparseable, arguments renamed, select branches changed,
196
- * no `other`). Missing target plural categories are only a warning: ICU
197
- * falls back to `other` for them.
214
+ * no `other`, a target plural category whole numbers use). A missing category
215
+ * only fractions use is a warning: ICU falls back to `other` for it.
198
216
  */
199
217
  function compareIcuMessages(source, translated, targetLocale, options = {}) {
200
218
  let sourceAst;
@@ -291,11 +309,29 @@ function compareIcuMessages(source, translated, targetLocale, options = {}) {
291
309
  }
292
310
  const missing = allowed.filter((c) => !target.keys.includes(c));
293
311
  if (missing.length) {
294
- issues.push({
295
- severity: 'warning',
296
- code: 'icu_plural_category_missing',
297
- message: `{${target.name}, ${target.kind}} has no [${missing.join(', ')}] branch for ${targetLocale}; "other" will be used`,
298
- });
312
+ // A missing category whole numbers reach (Czech few: 2–4) shows the
313
+ // wrong form for common counts ("2 souborů"): an error. One only
314
+ // fractions reach (Czech many: 1.5), or whose numbers all have `=n`
315
+ // branches, falls back to `other` rarely: a warning.
316
+ const type = target.kind === 'selectordinal' ? 'ordinal' : 'cardinal';
317
+ const reached = missing
318
+ .map((c) => ({ c, ns: integersOf(targetLocale, type, c).filter((n) => !target.keys.includes(`=${n}`)) }))
319
+ .filter((m) => m.ns.length > 0);
320
+ if (reached.length) {
321
+ const examples = (ns) => `${ns.slice(0, 3).join(', ')}${ns.length > 3 ? ', …' : ''}`;
322
+ issues.push({
323
+ severity: 'error',
324
+ code: 'icu_plural_category_missing',
325
+ message: `{${target.name}, ${target.kind}} has no ${reached.map((m) => `${m.c} branch (${examples(m.ns)})`).join(', ')} for ${targetLocale}; those counts would get the "other" form`,
326
+ });
327
+ }
328
+ else {
329
+ issues.push({
330
+ severity: 'warning',
331
+ code: 'icu_plural_category_missing',
332
+ message: `{${target.name}, ${target.kind}} has no [${missing.join(', ')}] branch for ${targetLocale}; "other" will be used`,
333
+ });
334
+ }
299
335
  }
300
336
  }
301
337
  return issues;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babelize/engine",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Babelize translation pipeline (extract, keygen, translate, inject). Infrastructure is injected through provider/store interfaces.",
5
5
  "keywords": [
6
6
  "babelize",
@@ -11,10 +11,6 @@
11
11
  ],
12
12
  "license": "Apache-2.0",
13
13
  "homepage": "https://babelize.co",
14
- "repository": {
15
- "type": "git",
16
- "url": "git+https://github.com/babelize/babelize-dashboard.git"
17
- },
18
14
  "main": "./dist/index.js",
19
15
  "types": "./dist/index.d.ts",
20
16
  "files": [