@courthive/i18n 0.1.1

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.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Build-time CLI: scans `dist/locales/*.json`, computes a SHA256 per
3
+ * file, counts keys and `__TODO__` sentinels, and emits
4
+ * `dist/manifest.json`.
5
+ *
6
+ * The manifest is the authoritative contract served by CFS at
7
+ * `GET /i18n/manifest` — its shape is documented in
8
+ * `Mentat/planning/I18N_DELIVERY.md`.
9
+ *
10
+ * Invocation: `node dist/manifest.gen.js` (chained after `tsc` in
11
+ * `pnpm build`).
12
+ */
13
+ export interface ManifestLocaleEntry {
14
+ /** BCP47 language tag (e.g. 'en', 'pt-BR', 'zh-CN'). */
15
+ code: string;
16
+ /** English label. */
17
+ label: string;
18
+ /** Native label. */
19
+ nativeLabel: string;
20
+ /** `sha256-<hex>` content hash of the locale JSON file. */
21
+ version: string;
22
+ /** File size in bytes. */
23
+ size: number;
24
+ /** Total leaf-key count. */
25
+ keyCount: number;
26
+ /** 1 - (__TODO__ count / total keys). */
27
+ completeness: number;
28
+ /** Right-to-left rendering required. */
29
+ rtl: boolean;
30
+ }
31
+ export interface Manifest {
32
+ /** `@courthive/i18n@<package-version>`. */
33
+ version: string;
34
+ /** ISO timestamp the manifest was generated at. */
35
+ generatedAt: string;
36
+ locales: ManifestLocaleEntry[];
37
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Build-time CLI: scans `dist/locales/*.json`, computes a SHA256 per
3
+ * file, counts keys and `__TODO__` sentinels, and emits
4
+ * `dist/manifest.json`.
5
+ *
6
+ * The manifest is the authoritative contract served by CFS at
7
+ * `GET /i18n/manifest` — its shape is documented in
8
+ * `Mentat/planning/I18N_DELIVERY.md`.
9
+ *
10
+ * Invocation: `node dist/manifest.gen.js` (chained after `tsc` in
11
+ * `pnpm build`).
12
+ */
13
+ import { createHash } from 'node:crypto';
14
+ import { readFileSync, readdirSync, writeFileSync, statSync } from 'node:fs';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { dirname, join } from 'node:path';
17
+ import { localeLabels } from './locale-labels.js';
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const TODO_SENTINEL = '__TODO__';
20
+ function countLeafKeys(obj) {
21
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj))
22
+ return 1;
23
+ let n = 0;
24
+ for (const key of Object.keys(obj)) {
25
+ n += countLeafKeys(obj[key]);
26
+ }
27
+ return n;
28
+ }
29
+ function countTodos(obj) {
30
+ if (typeof obj === 'string')
31
+ return obj === TODO_SENTINEL ? 1 : 0;
32
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj))
33
+ return 0;
34
+ let n = 0;
35
+ for (const key of Object.keys(obj)) {
36
+ n += countTodos(obj[key]);
37
+ }
38
+ return n;
39
+ }
40
+ function readPackageVersion() {
41
+ // dist/manifest.gen.js → dist/ → repo root
42
+ const pkgPath = join(__dirname, '..', 'package.json');
43
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
44
+ return `${pkg.name}@${pkg.version}`;
45
+ }
46
+ function build() {
47
+ const localesDir = join(__dirname, 'locales');
48
+ const files = readdirSync(localesDir).filter((f) => f.endsWith('.json'));
49
+ const entries = [];
50
+ for (const file of files) {
51
+ const code = file.replace(/\.json$/, '');
52
+ const labelEntry = localeLabels[code];
53
+ if (!labelEntry) {
54
+ console.warn(`manifest.gen: locale "${code}" has no entry in locale-labels.ts — skipping`);
55
+ continue;
56
+ }
57
+ const fullPath = join(localesDir, file);
58
+ const raw = readFileSync(fullPath, 'utf8');
59
+ const parsed = JSON.parse(raw);
60
+ const sha = createHash('sha256').update(raw).digest('hex');
61
+ const size = statSync(fullPath).size;
62
+ const keyCount = countLeafKeys(parsed);
63
+ const todos = countTodos(parsed);
64
+ const completeness = keyCount > 0 ? 1 - todos / keyCount : 1;
65
+ entries.push({
66
+ code,
67
+ label: labelEntry.label,
68
+ nativeLabel: labelEntry.nativeLabel,
69
+ version: `sha256-${sha}`,
70
+ size,
71
+ keyCount,
72
+ completeness,
73
+ rtl: labelEntry.rtl,
74
+ });
75
+ if (completeness < 1) {
76
+ console.warn(`manifest.gen: locale "${code}" is ${(completeness * 100).toFixed(1)}% complete (${todos}/${keyCount} __TODO__)`);
77
+ }
78
+ }
79
+ // Deterministic order: alphabetical by code so manifest diffs are stable.
80
+ entries.sort((a, b) => a.code.localeCompare(b.code));
81
+ return {
82
+ version: readPackageVersion(),
83
+ generatedAt: new Date().toISOString(),
84
+ locales: entries,
85
+ };
86
+ }
87
+ function main() {
88
+ const manifest = build();
89
+ const outPath = join(__dirname, 'manifest.json');
90
+ writeFileSync(outPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
91
+ console.log(`manifest.gen: wrote ${outPath} (${manifest.locales.length} locales)`);
92
+ }
93
+ main();
@@ -0,0 +1,96 @@
1
+ {
2
+ "version": "@courthive/i18n@0.1.1",
3
+ "generatedAt": "2026-05-19T20:14:44.491Z",
4
+ "locales": [
5
+ {
6
+ "code": "ar",
7
+ "label": "Arabic",
8
+ "nativeLabel": "العربية",
9
+ "version": "sha256-898ace24f03deb33e968b1b87b123cfb1176f3d68451b445b7a850129be754c7",
10
+ "size": 81545,
11
+ "keyCount": 1748,
12
+ "completeness": 0.8541189931350115,
13
+ "rtl": true
14
+ },
15
+ {
16
+ "code": "cs",
17
+ "label": "Czech",
18
+ "nativeLabel": "Čeština",
19
+ "version": "sha256-adab74b4421886d3b1ce87dcf15f58015ca2503ac17eacae7357c886a82b2664",
20
+ "size": 73071,
21
+ "keyCount": 1748,
22
+ "completeness": 0.9754004576659039,
23
+ "rtl": false
24
+ },
25
+ {
26
+ "code": "de",
27
+ "label": "German",
28
+ "nativeLabel": "Deutsch",
29
+ "version": "sha256-0595faff7802cbcc04f37b7c9f3faeb6c6dc9de48b39a1bff6088e55860cefb6",
30
+ "size": 69799,
31
+ "keyCount": 1748,
32
+ "completeness": 0.8541189931350115,
33
+ "rtl": false
34
+ },
35
+ {
36
+ "code": "en",
37
+ "label": "English",
38
+ "nativeLabel": "English",
39
+ "version": "sha256-e6df6c8687929901e800eb7a990f74e6b34c32f313744fbd29ac49ee184748d0",
40
+ "size": 68249,
41
+ "keyCount": 1748,
42
+ "completeness": 0.9994279176201373,
43
+ "rtl": false
44
+ },
45
+ {
46
+ "code": "es",
47
+ "label": "Spanish",
48
+ "nativeLabel": "Español",
49
+ "version": "sha256-bf19cc379b3a84dadd38702ee75ea6a02782a2724abc9e94c4de2cd7c9782ee7",
50
+ "size": 70785,
51
+ "keyCount": 1748,
52
+ "completeness": 0.8541189931350115,
53
+ "rtl": false
54
+ },
55
+ {
56
+ "code": "fr",
57
+ "label": "French",
58
+ "nativeLabel": "Français",
59
+ "version": "sha256-76eee82d25eb2d8ec730449abeef2e39ea4bb4ee97a657426eba86f8d8ae118c",
60
+ "size": 72243,
61
+ "keyCount": 1748,
62
+ "completeness": 0.8541189931350115,
63
+ "rtl": false
64
+ },
65
+ {
66
+ "code": "hr",
67
+ "label": "Croatian",
68
+ "nativeLabel": "Hrvatski",
69
+ "version": "sha256-9ac80c816f22b37feeb908a1079a03ec99b74dc4f4573dcb0eabaef5cc4525a4",
70
+ "size": 71578,
71
+ "keyCount": 1748,
72
+ "completeness": 0.9754004576659039,
73
+ "rtl": false
74
+ },
75
+ {
76
+ "code": "pt-BR",
77
+ "label": "Portuguese (Brazil)",
78
+ "nativeLabel": "Português (Brasil)",
79
+ "version": "sha256-2190a533602573f8af19128004f8f87106dfd19f0bf8c9cd5d42aa581554e93d",
80
+ "size": 70400,
81
+ "keyCount": 1748,
82
+ "completeness": 0.8541189931350115,
83
+ "rtl": false
84
+ },
85
+ {
86
+ "code": "zh-CN",
87
+ "label": "Chinese (Simplified)",
88
+ "nativeLabel": "简体中文",
89
+ "version": "sha256-3725b0fd07ed5c938b759475e496d2eb23c5e2149e8bbd0e4f3c5bfb5f06b652",
90
+ "size": 64697,
91
+ "keyCount": 1748,
92
+ "completeness": 0.8781464530892449,
93
+ "rtl": false
94
+ }
95
+ ]
96
+ }
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@courthive/i18n",
3
+ "packageManager": "pnpm@11.1.2",
4
+ "version": "0.1.1",
5
+ "description": "Translation locales for the CourtHive ecosystem. Source-of-truth bundle served at runtime by competition-factory-server to TMX clients.",
6
+ "author": "Charles Allen <charles@CourtHive.com> (CourtHive.com)",
7
+ "license": "MIT",
8
+ "type": "module",
9
+ "private": false,
10
+ "bugs": {
11
+ "url": "https://github.com/CourtHive/courthive-i18n/issues",
12
+ "email": "support@CourtHive.com"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/CourtHive/courthive-i18n.git"
17
+ },
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "./manifest": {
26
+ "types": "./dist/manifest.d.ts",
27
+ "default": "./dist/manifest.js"
28
+ },
29
+ "./locale-labels": {
30
+ "types": "./dist/locale-labels.d.ts",
31
+ "default": "./dist/locale-labels.js"
32
+ },
33
+ "./locales/*.json": "./dist/locales/*.json",
34
+ "./manifest.json": "./dist/manifest.json"
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "scripts": {
40
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/copy-locales.cjs && node dist/manifest.gen.js",
41
+ "check-types": "tsc --noEmit",
42
+ "lint": "eslint . --fix --max-warnings 0",
43
+ "format": "prettier --write \"src/**/*.{ts,json}\"",
44
+ "test": "node scripts/compare-keys.cjs && echo 'OK'",
45
+ "commitlint": "commitlint --edit",
46
+ "prepare": "husky && node scripts/install-husky-hooks.cjs",
47
+ "prepublishOnly": "pnpm build"
48
+ },
49
+ "lint-staged": {
50
+ "*.{js,ts,cjs,mjs}": "eslint --cache --fix",
51
+ "*": "prettier --ignore-unknown --write"
52
+ },
53
+ "engines": {
54
+ "node": ">=22"
55
+ },
56
+ "devDependencies": {
57
+ "@commitlint/cli": "^21.0.0",
58
+ "@commitlint/config-conventional": "^21.0.0",
59
+ "@types/node": "^24.0.0",
60
+ "@typescript-eslint/eslint-plugin": "^8.59.0",
61
+ "@typescript-eslint/parser": "^8.59.0",
62
+ "eslint": "^10.0.0",
63
+ "husky": "^9.1.7",
64
+ "lint-staged": "^17.0.2",
65
+ "prettier": "^3.8.3",
66
+ "typescript": "^6.0.0"
67
+ },
68
+ "pnpm": {
69
+ "ignoredBuiltDependencies": [
70
+ "@scarf/scarf",
71
+ "esbuild",
72
+ "unrs-resolver"
73
+ ]
74
+ }
75
+ }