@courthive/i18n 0.4.9 → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Build-time CLI: scans `dist/locales/*.json`, computes a SHA256 per
3
- * file, counts keys and `__TODO__` sentinels, and emits
3
+ * file, counts keys and translator sentinels, and emits
4
4
  * `dist/manifest.json`.
5
5
  *
6
6
  * The manifest is the authoritative contract served by CFS at
@@ -27,6 +27,16 @@
27
27
  * `keyCount` is therefore the *translatable* key count, which is ≥ the number of
28
28
  * keys present in the served file. That is the useful reading of it, but it does
29
29
  * mean keyCount cannot be used to validate a downloaded locale's key count.
30
+ *
31
+ * ## Completeness counts both sentinels
32
+ *
33
+ * `__TODO__` means no translation exists. `__STALE__<translation>` means one
34
+ * exists but the English it was made from has since changed, so it may now
35
+ * answer the wrong question — see `scripts/merge-source-en.cjs`. Both are
36
+ * outstanding translator work and both count against `completeness`; a stale
37
+ * string is not a correct one. The stale prefix is stripped by `copy-locales`
38
+ * on the way to dist, so it is only ever visible in `src` — which is exactly
39
+ * why the metric is read from `src`.
30
40
  */
31
41
  export interface ManifestLocaleEntry {
32
42
  /** BCP47 language tag (e.g. 'en', 'pt-BR', 'zh-CN'). */
@@ -41,7 +51,7 @@ export interface ManifestLocaleEntry {
41
51
  size: number;
42
52
  /** Total leaf-key count. */
43
53
  keyCount: number;
44
- /** 1 - (__TODO__ count / total keys). */
54
+ /** 1 - ((__TODO__ + __STALE__ count) / total keys). */
45
55
  completeness: number;
46
56
  /** Right-to-left rendering required. */
47
57
  rtl: boolean;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Build-time CLI: scans `dist/locales/*.json`, computes a SHA256 per
3
- * file, counts keys and `__TODO__` sentinels, and emits
3
+ * file, counts keys and translator sentinels, and emits
4
4
  * `dist/manifest.json`.
5
5
  *
6
6
  * The manifest is the authoritative contract served by CFS at
@@ -27,6 +27,16 @@
27
27
  * `keyCount` is therefore the *translatable* key count, which is ≥ the number of
28
28
  * keys present in the served file. That is the useful reading of it, but it does
29
29
  * mean keyCount cannot be used to validate a downloaded locale's key count.
30
+ *
31
+ * ## Completeness counts both sentinels
32
+ *
33
+ * `__TODO__` means no translation exists. `__STALE__<translation>` means one
34
+ * exists but the English it was made from has since changed, so it may now
35
+ * answer the wrong question — see `scripts/merge-source-en.cjs`. Both are
36
+ * outstanding translator work and both count against `completeness`; a stale
37
+ * string is not a correct one. The stale prefix is stripped by `copy-locales`
38
+ * on the way to dist, so it is only ever visible in `src` — which is exactly
39
+ * why the metric is read from `src`.
30
40
  */
31
41
  import { createHash } from 'node:crypto';
32
42
  import { readFileSync, readdirSync, writeFileSync, statSync, existsSync } from 'node:fs';
@@ -35,6 +45,7 @@ import { dirname, join } from 'node:path';
35
45
  import { localeLabels } from './locale-labels.js';
36
46
  const __dirname = dirname(fileURLToPath(import.meta.url));
37
47
  const TODO_SENTINEL = '__TODO__';
48
+ const STALE_SENTINEL = '__STALE__';
38
49
  function countLeafKeys(obj) {
39
50
  if (obj === null || typeof obj !== 'object' || Array.isArray(obj))
40
51
  return 1;
@@ -44,14 +55,15 @@ function countLeafKeys(obj) {
44
55
  }
45
56
  return n;
46
57
  }
47
- function countTodos(obj) {
58
+ /** Leaves carrying either sentinel — untranslated or translated-then-drifted. */
59
+ function countOutstanding(obj) {
48
60
  if (typeof obj === 'string')
49
- return obj === TODO_SENTINEL ? 1 : 0;
61
+ return obj === TODO_SENTINEL || obj.startsWith(STALE_SENTINEL) ? 1 : 0;
50
62
  if (obj === null || typeof obj !== 'object' || Array.isArray(obj))
51
63
  return 0;
52
64
  let n = 0;
53
65
  for (const key of Object.keys(obj)) {
54
- n += countTodos(obj[key]);
66
+ n += countOutstanding(obj[key]);
55
67
  }
56
68
  return n;
57
69
  }
@@ -77,16 +89,16 @@ function build() {
77
89
  const raw = readFileSync(fullPath, 'utf8');
78
90
  const sha = createHash('sha256').update(raw).digest('hex');
79
91
  const size = statSync(fullPath).size;
80
- // … while the translation metric is read from src, where `__TODO__` still
81
- // lives. Fall back to the dist copy if src is unavailable (e.g. a consumer
92
+ // … while the translation metric is read from src, where both sentinels
93
+ // still live. Fall back to the dist copy if src is unavailable (e.g. a consumer
82
94
  // running the generator against an extracted package) rather than silently
83
95
  // reporting 100% complete.
84
96
  const srcPath = join(__dirname, '..', 'src', 'locales', file);
85
97
  const metricSource = existsSync(srcPath) ? srcPath : fullPath;
86
98
  const parsed = JSON.parse(readFileSync(metricSource, 'utf8'));
87
99
  const keyCount = countLeafKeys(parsed);
88
- const todos = countTodos(parsed);
89
- const completeness = keyCount > 0 ? 1 - todos / keyCount : 1;
100
+ const outstanding = countOutstanding(parsed);
101
+ const completeness = keyCount > 0 ? 1 - outstanding / keyCount : 1;
90
102
  entries.push({
91
103
  code,
92
104
  label: labelEntry.label,
@@ -98,7 +110,8 @@ function build() {
98
110
  rtl: labelEntry.rtl,
99
111
  });
100
112
  if (completeness < 1) {
101
- console.warn(`manifest.gen: locale "${code}" is ${(completeness * 100).toFixed(1)}% complete (${todos}/${keyCount} __TODO__)`);
113
+ console.warn(`manifest.gen: locale "${code}" is ${(completeness * 100).toFixed(1)}% complete ` +
114
+ `(${outstanding}/${keyCount} outstanding)`);
102
115
  }
103
116
  }
104
117
  // Deterministic order: alphabetical by code so manifest diffs are stable.
@@ -1,44 +1,44 @@
1
1
  {
2
- "version": "@courthive/i18n@0.4.9",
3
- "generatedAt": "2026-08-23T00:55:10.377Z",
2
+ "version": "@courthive/i18n@0.5.0",
3
+ "generatedAt": "2026-08-31T16:24:10.199Z",
4
4
  "locales": [
5
5
  {
6
6
  "code": "ar",
7
7
  "label": "Arabic",
8
8
  "nativeLabel": "العربية",
9
- "version": "sha256-0765e316012e6743413bcdb0cae1ee0a3f2360292faff711f3ecbeb368d2e649",
10
- "size": 132643,
11
- "keyCount": 2642,
12
- "completeness": 0.9015897047691143,
9
+ "version": "sha256-f726c6bd2fa8f49155682e0e929dcf50762ef2808cd6c0d75bda90984819af3b",
10
+ "size": 169719,
11
+ "keyCount": 2888,
12
+ "completeness": 1,
13
13
  "rtl": true
14
14
  },
15
15
  {
16
16
  "code": "cs",
17
17
  "label": "Czech",
18
18
  "nativeLabel": "Čeština",
19
- "version": "sha256-ef7eba0c4b064ae8ee8eb6455ca39bf46fadc3527a09f7151fb0fb7d53ed34b0",
20
- "size": 110834,
21
- "keyCount": 2642,
22
- "completeness": 0.9015897047691143,
19
+ "version": "sha256-472d4b81562a8b485f319de257f17107853d9a346e39bf05e006367f11971c3a",
20
+ "size": 141170,
21
+ "keyCount": 2888,
22
+ "completeness": 1,
23
23
  "rtl": false
24
24
  },
25
25
  {
26
26
  "code": "de",
27
27
  "label": "German",
28
28
  "nativeLabel": "Deutsch",
29
- "version": "sha256-0e6b1d7a0b3628ce0c21afbb933681d77317a01c0f75e6446e3c9d2eba8f22ce",
30
- "size": 112807,
31
- "keyCount": 2642,
32
- "completeness": 0.9015897047691143,
29
+ "version": "sha256-52d8fd4c8278899b910ccf749a118dd47de255d4f86b2945d7e8c766274b4404",
30
+ "size": 144043,
31
+ "keyCount": 2888,
32
+ "completeness": 1,
33
33
  "rtl": false
34
34
  },
35
35
  {
36
36
  "code": "en",
37
37
  "label": "English",
38
38
  "nativeLabel": "English",
39
- "version": "sha256-c68ec79cd1ae8ad0f5ab6f1d2d25a90da7acc2ce2a468c055892c013054aeecb",
40
- "size": 116333,
41
- "keyCount": 2642,
39
+ "version": "sha256-337885289fcb09e9519c4be1054c73f4aeb47547f3cd84cd7fa0b73cea1f7265",
40
+ "size": 130650,
41
+ "keyCount": 2888,
42
42
  "completeness": 1,
43
43
  "rtl": false
44
44
  },
@@ -46,50 +46,50 @@
46
46
  "code": "es",
47
47
  "label": "Spanish",
48
48
  "nativeLabel": "Español",
49
- "version": "sha256-8b9f2879ac438323ab49a362be6ae22e530f28ec0b1551027d963bad4b3d4366",
50
- "size": 113081,
51
- "keyCount": 2642,
52
- "completeness": 0.9015897047691143,
49
+ "version": "sha256-b0d9ce14f4ddc2281805f7245cd1b5d0fab75e6908568e4883530bd5e4bacf80",
50
+ "size": 143885,
51
+ "keyCount": 2888,
52
+ "completeness": 1,
53
53
  "rtl": false
54
54
  },
55
55
  {
56
56
  "code": "fr",
57
57
  "label": "French",
58
58
  "nativeLabel": "Français",
59
- "version": "sha256-220bcadb8c2305271646759eee6930bc2c3eeefc156ac1704fbecc552bb92664",
60
- "size": 116190,
61
- "keyCount": 2642,
62
- "completeness": 0.9015897047691143,
59
+ "version": "sha256-755c098b022da4cf4202b7af33a8132f98b57b928dcc332b49b6d61c522fc4c0",
60
+ "size": 147980,
61
+ "keyCount": 2888,
62
+ "completeness": 1,
63
63
  "rtl": false
64
64
  },
65
65
  {
66
66
  "code": "hr",
67
67
  "label": "Croatian",
68
68
  "nativeLabel": "Hrvatski",
69
- "version": "sha256-4f04284c811700e5cfe42742312c56076628e6350df2fc6edb24599dccd4d7b7",
70
- "size": 109069,
71
- "keyCount": 2642,
72
- "completeness": 0.9015897047691143,
69
+ "version": "sha256-7f6dd49a642e65b67239ba356921b5d99149e2960a5cafa1182223f7c4ade20a",
70
+ "size": 138630,
71
+ "keyCount": 2888,
72
+ "completeness": 1,
73
73
  "rtl": false
74
74
  },
75
75
  {
76
76
  "code": "pt-BR",
77
77
  "label": "Portuguese (Brazil)",
78
78
  "nativeLabel": "Português (Brasil)",
79
- "version": "sha256-b008dbf9941075a7a96d3f9e772135b2e7887cf922dae1758643d5e72e931b69",
80
- "size": 112047,
81
- "keyCount": 2642,
82
- "completeness": 0.9015897047691143,
79
+ "version": "sha256-8be7d6aaaf8d40ae225eb452f193823bbb36f15eb9f89a29fd9cd0718d5df96d",
80
+ "size": 142783,
81
+ "keyCount": 2888,
82
+ "completeness": 1,
83
83
  "rtl": false
84
84
  },
85
85
  {
86
86
  "code": "zh-CN",
87
87
  "label": "Chinese (Simplified)",
88
88
  "nativeLabel": "简体中文",
89
- "version": "sha256-b1596afc6520a778c850df76ae994af7ee8204a8fc039051ee72bfb8dad8b0df",
90
- "size": 100716,
91
- "keyCount": 2642,
92
- "completeness": 0.9015897047691143,
89
+ "version": "sha256-abc965d89a65ed427e1ab2fe70f577ff9d29b8f1a7d3296f3c9c4b2e7e5418dc",
90
+ "size": 128550,
91
+ "keyCount": 2888,
92
+ "completeness": 1,
93
93
  "rtl": false
94
94
  }
95
95
  ]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@courthive/i18n",
3
- "packageManager": "pnpm@11.21.0",
4
- "version": "0.4.9",
3
+ "packageManager": "pnpm@11.22.0",
4
+ "version": "0.5.0",
5
5
  "description": "Translation locales for the CourtHive ecosystem. Source-of-truth bundle served at runtime by competition-factory-server to TMX clients.",
6
6
  "author": "Charles Allen <charles@CourtHive.com> (CourtHive.com)",
7
7
  "license": "MIT",