From a42b37ab49abfb7348217d83af032b19f9c8c4ec Mon Sep 17 00:00:00 2001 From: Ihab Khaled Date: Tue, 1 Sep 2026 15:54:50 +0300 Subject: [PATCH] test(frontend): add locale-completeness coverage for /learn content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four existing locale-completeness tests (persian/japanese/thai/chinese) all assert against lib/i18n/locales/*, the UI-chrome dictionary — none of them touch constants/learn-content/*.constants.ts, the actual /learn article prose. A manual audit found the content clean today, but nothing caught a future regression, and ar/fr/hi/ru had no locale-specific translation test of any kind. Adds one parameterized test covering all 13 locales: key/section/FAQ parity with English, placeholder parity, and an untranslated-English-fallback check with its own documented allow-list. Running it against real content surfaced 3 genuine judgment calls, resolved and documented rather than silently allowed: - "Orchestration" unchanged in French is correct — it's the same spelling in both languages (the word entered English from French). - "Routing" unchanged in German/Italian eyebrows is a common tech loanword in both languages, not an obvious miss. - "best of N" unchanged in an Italian SEO keyword is deliberate — it's the term a non-English searcher would actually type. All three are commented in the allow-list for a native speaker to override. Knowledge delta: rules/20 now points at this test so it isn't found only by reading the file list. Co-Authored-By: Claude Sonnet 5 --- .ai/manifests/governance.json | 2 +- .ai/manifests/hashes.json | 4 +- .ai/manifests/tests.json | 4 +- .../learn-content-locale-completeness.test.ts | 110 ++++++++++ .../inventory.snapshot.json | 6 +- package-lock.json | 195 +++++++++--------- rules/20-i18n-and-user-facing-messages.md | 9 + 7 files changed, 224 insertions(+), 106 deletions(-) create mode 100644 apps/claw-frontend/src/constants/__tests__/learn-content-locale-completeness.test.ts diff --git a/.ai/manifests/governance.json b/.ai/manifests/governance.json index b3be0261e..9e941f411 100644 --- a/.ai/manifests/governance.json +++ b/.ai/manifests/governance.json @@ -174,7 +174,7 @@ "file": "rules/19-logging-observability-and-redaction.md" }, { - "bytes": 4264, + "bytes": 4914, "file": "rules/20-i18n-and-user-facing-messages.md" }, { diff --git a/.ai/manifests/hashes.json b/.ai/manifests/hashes.json index 67626ec79..1e4f01c8f 100644 --- a/.ai/manifests/hashes.json +++ b/.ai/manifests/hashes.json @@ -8,7 +8,7 @@ ".ai/manifests/environment-variables.json": "33602cbe", ".ai/manifests/event-graph.json": "c1cd05bc", ".ai/manifests/frontend-routes.json": "a52b7ece", - ".ai/manifests/governance.json": "e4492ebd", + ".ai/manifests/governance.json": "1160e343", ".ai/manifests/i18n.json": "3befb756", ".ai/manifests/nginx-routes.json": "53c65be1", ".ai/manifests/packages.json": "cc928a4a", @@ -18,7 +18,7 @@ ".ai/manifests/rabbitmq-events.json": "bcaeb411", ".ai/manifests/repository.json": "047d93d0", ".ai/manifests/services.json": "9c92d2c4", - ".ai/manifests/tests.json": "4044f562", + ".ai/manifests/tests.json": "6eb2e2ae", ".ai/manifests/workspace-dependency-graph.json": "80e5438b", ".ai/manifests/workspaces.json": "b5133e99", ".ai/packs/README.md": "2e64753b", diff --git a/.ai/manifests/tests.json b/.ai/manifests/tests.json index 4c1217b64..71b854786 100644 --- a/.ai/manifests/tests.json +++ b/.ai/manifests/tests.json @@ -58,7 +58,7 @@ }, "claw-frontend": { "runner": "vitest", - "testFiles": 410 + "testFiles": 411 }, "claw-health-service": { "runner": "jest", @@ -102,5 +102,5 @@ } }, "generated": true, - "total": 1122 + "total": 1123 } diff --git a/apps/claw-frontend/src/constants/__tests__/learn-content-locale-completeness.test.ts b/apps/claw-frontend/src/constants/__tests__/learn-content-locale-completeness.test.ts new file mode 100644 index 000000000..371abc154 --- /dev/null +++ b/apps/claw-frontend/src/constants/__tests__/learn-content-locale-completeness.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from 'vitest'; + +import { LEARN_CONTENT_BY_LOCALE } from '@/constants/learn-content.constants'; +import { Locale } from '@/enums/locale.enum'; + +/** + * Flattens a `LearnDictionary` into `path -> string` pairs, the same shape + * the i18n completeness tests use (see `chinese-completeness.test.ts`, + * `translations.test.ts`). Arrays (e.g. `seo.keywords`) flatten by index, + * which is exactly what parity needs: a locale with two keywords instead of + * three shows up as a missing key, not a silently shorter array. + */ +function flatten(value: unknown, prefix = '', result: Record = {}): Record { + if (typeof value === 'string') { + result[prefix] = value; + return result; + } + if (typeof value !== 'object' || value === null) { + return result; + } + for (const [key, child] of Object.entries(value)) { + flatten(child, prefix === '' ? key : `${prefix}.${key}`, result); + } + return result; +} + +function placeholders(value: string): string[] { + return [...value.matchAll(/\{[^{}]+\}/gu)].map((match) => match[0]).sort(); +} + +/** + * Section anchor ids (`sections..id`) are deliberately identical across + * every locale — "Stable across locales so a translated page keeps its deep + * links" (`LearnSection` in `types/learn.types.ts`). They are structural, + * not prose, so they are excluded from the "did anyone actually translate + * this" check below, the same way a `data-testid` would be. + */ +function isStructuralKey(key: string): boolean { + return key.endsWith('.id'); +} + +/** + * Values that are correctly identical across every locale: proper nouns, + * acronyms, product/model names, and other technical terms nobody + * translates. Anything NOT on this list that matches English exactly is + * either an untranslated string or a coincidence rare enough to be worth a + * second look — see `chinese-completeness.test.ts` for the same pattern + * against the general UI dictionary. + */ +const LEGITIMATE_UNCHANGED_VALUES = new Set([ + 'ClawAI', + 'RAG', + 'GPU', + 'CPU', + 'API', + 'LLM', + 'PDF', + 'URL', + 'GitHub', + 'Ollama', + 'llama.cpp', + 'Ollama vs llama.cpp', + // French "orchestration" is spelled identically to English (it is in fact + // the older of the two — the word entered English FROM French). Not a + // missed translation: fr correctly translates the same English word to + // "Orchestrazione"/"Orchestrierung" is what it/de do, which is a different + // language having a different native form, not evidence fr is wrong. + 'Orchestration', + // "Routing" is a standard, widely-used loanword in German and Italian + // technical writing (as common as "Router" itself) — this repo's own de/it + // copy elsewhere also keeps other loanwords (e.g. "GPU") rather than + // coining an unfamiliar native term. Flagged here for visibility: if a + // native speaker prefers a translated term, remove this line and fix the + // two eyebrow values it currently allows through. + 'Routing', + // SEO keyword arrays intentionally keep the English query term a non-English + // searcher would actually type — "best of N" is ML/statistics jargon, not + // prose, and translating it would target a search query nobody makes. + 'best of N', +]); + +const english = flatten(LEARN_CONTENT_BY_LOCALE[Locale.EN]); +const nonEnglishLocales = Object.values(Locale).filter((locale) => locale !== Locale.EN); + +describe('/learn content locale completeness', () => { + it.each(nonEnglishLocales)('%s defines every English key with no missing sections/faq/keywords', (locale) => { + const localized = flatten(LEARN_CONTENT_BY_LOCALE[locale]); + expect(Object.keys(localized).sort()).toEqual(Object.keys(english).sort()); + }); + + it.each(nonEnglishLocales)('%s preserves every interpolation placeholder', (locale) => { + const localized = flatten(LEARN_CONTENT_BY_LOCALE[locale]); + for (const key of Object.keys(english)) { + expect(placeholders(localized[key] ?? ''), key).toEqual(placeholders(english[key] ?? '')); + } + }); + + it.each(nonEnglishLocales)( + '%s has no untranslated English fallback outside the approved technical terms', + (locale) => { + const localized = flatten(LEARN_CONTENT_BY_LOCALE[locale]); + const unexpectedlyUnchanged = Object.keys(english) + .filter((key) => !isStructuralKey(key)) + .filter((key) => localized[key] === english[key]) + .filter((key) => !LEGITIMATE_UNCHANGED_VALUES.has(english[key] ?? '')); + + expect(unexpectedlyUnchanged).toEqual([]); + }, + ); +}); diff --git a/docs/features/ai-native-engineering-os/inventory.snapshot.json b/docs/features/ai-native-engineering-os/inventory.snapshot.json index 23319c4e9..4cae7a7b1 100644 --- a/docs/features/ai-native-engineering-os/inventory.snapshot.json +++ b/docs/features/ai-native-engineering-os/inventory.snapshot.json @@ -7378,7 +7378,7 @@ "file": "rules/19-logging-observability-and-redaction.md" }, { - "bytes": 4264, + "bytes": 4914, "file": "rules/20-i18n-and-user-facing-messages.md" }, { @@ -9516,7 +9516,7 @@ }, "claw-frontend": { "runner": "vitest", - "testFiles": 410 + "testFiles": 411 }, "claw-health-service": { "runner": "jest", @@ -10276,7 +10276,7 @@ "serviceCount": 18, "sharedPackageCount": 6, "staleClaimCount": 0, - "totalTestFiles": 1122, + "totalTestFiles": 1123, "workspaceCount": 25 } } diff --git a/package-lock.json b/package-lock.json index 0036e4089..b65ba2d74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claw", - "version": "1.59.2", + "version": "1.59.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claw", - "version": "1.59.2", + "version": "1.59.4", "license": "MIT", "workspaces": [ "packages/*", @@ -44,7 +44,7 @@ } }, "apps/claw-agent-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-auth": "*", @@ -429,7 +429,7 @@ } }, "apps/claw-audit-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-constants": "*", @@ -812,7 +812,7 @@ } }, "apps/claw-auth-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-constants": "*", @@ -1197,7 +1197,7 @@ } }, "apps/claw-chat-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-constants": "*", @@ -1581,7 +1581,7 @@ } }, "apps/claw-client-logs-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-constants": "*", @@ -1963,7 +1963,7 @@ } }, "apps/claw-connector-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-constants": "*", @@ -2346,7 +2346,7 @@ } }, "apps/claw-file-generation-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-constants": "*", @@ -2782,7 +2782,7 @@ "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==" }, "apps/claw-file-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-constants": "*", @@ -3191,7 +3191,7 @@ } }, "apps/claw-frontend": { - "version": "1.59.2", + "version": "1.59.4", "dependencies": { "@claw/shared-constants": "*", "@claw/shared-types": "*", @@ -3929,22 +3929,6 @@ "url": "https://opencollective.com/libvips" } }, - "apps/claw-frontend/node_modules/@playwright/test": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", - "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.62.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, "apps/claw-frontend/node_modules/@radix-ui/react-compose-refs": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", @@ -4008,53 +3992,6 @@ } } }, - "apps/claw-frontend/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "apps/claw-frontend/node_modules/playwright": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", - "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.62.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "apps/claw-frontend/node_modules/playwright-core": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", - "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, "apps/claw-frontend/node_modules/sharp": { "version": "0.35.4", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", @@ -4105,7 +4042,7 @@ } }, "apps/claw-health-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-utilities": "*", @@ -4477,7 +4414,7 @@ } }, "apps/claw-image-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-constants": "*", @@ -4863,7 +4800,7 @@ } }, "apps/claw-llamacpp-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-constants": "*", @@ -5254,7 +5191,7 @@ } }, "apps/claw-memory-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-constants": "*", @@ -5638,7 +5575,7 @@ } }, "apps/claw-ollama-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-constants": "*", @@ -6025,7 +5962,7 @@ } }, "apps/claw-payment-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-auth": "*", @@ -6411,7 +6348,7 @@ } }, "apps/claw-research-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-auth": "*", @@ -6795,7 +6732,7 @@ } }, "apps/claw-routing-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-constants": "*", @@ -7179,7 +7116,7 @@ } }, "apps/claw-server-logs-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-constants": "*", @@ -7562,7 +7499,7 @@ } }, "apps/claw-workspace-service": { - "version": "1.59.2", + "version": "1.59.4", "license": "UNLICENSED", "dependencies": { "@claw/shared-auth": "*", @@ -12006,6 +11943,22 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@prisma/adapter-pg": { "version": "7.9.1", "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.9.1.tgz", @@ -17537,7 +17490,7 @@ "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "devOptional": true, + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -17553,7 +17506,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "devOptional": true, + "dev": true, "dependencies": { "ajv": "^8.0.0" }, @@ -17570,7 +17523,7 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "devOptional": true, + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -17586,7 +17539,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "devOptional": true + "dev": true }, "node_modules/amqplib": { "version": "2.0.1", @@ -21173,7 +21126,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "devOptional": true + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -27861,6 +27814,52 @@ "pathe": "^2.0.3" } }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/plimit-lit": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", @@ -31126,7 +31125,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "devOptional": true, + "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -32230,7 +32229,7 @@ }, "packages/shared-auth": { "name": "@claw/shared-auth", - "version": "1.59.2", + "version": "1.59.4", "dependencies": { "@claw/shared-types": "*", "@claw/shared-utilities": "*" @@ -32250,7 +32249,7 @@ }, "packages/shared-constants": { "name": "@claw/shared-constants", - "version": "1.59.2", + "version": "1.59.4", "devDependencies": { "@eslint/js": "^9.39.4", "@types/jest": "^30.0.0", @@ -32263,7 +32262,7 @@ }, "packages/shared-entitlements": { "name": "@claw/shared-entitlements", - "version": "1.59.2", + "version": "1.59.4", "dependencies": { "@claw/shared-constants": "*", "@claw/shared-types": "*" @@ -32282,7 +32281,7 @@ }, "packages/shared-rabbitmq": { "name": "@claw/shared-rabbitmq", - "version": "1.59.2", + "version": "1.59.4", "dependencies": { "@claw/shared-constants": "*", "@claw/shared-types": "*", @@ -32304,7 +32303,7 @@ }, "packages/shared-types": { "name": "@claw/shared-types", - "version": "1.59.2", + "version": "1.59.4", "devDependencies": { "@eslint/js": "^9.39.4", "@types/jest": "^30.0.0", @@ -32317,10 +32316,10 @@ }, "packages/shared-utilities": { "name": "@claw/shared-utilities", - "version": "1.59.2", + "version": "1.59.4", "dependencies": { - "@claw/shared-constants": "1.59.2", - "@claw/shared-types": "1.59.2", + "@claw/shared-constants": "1.59.4", + "@claw/shared-types": "1.59.4", "@nestjs/common": "^11.2.1", "axios": "^1.20.0", "jsonwebtoken": "^9.0.3", diff --git a/rules/20-i18n-and-user-facing-messages.md b/rules/20-i18n-and-user-facing-messages.md index 99a4d4388..91d454442 100644 --- a/rules/20-i18n-and-user-facing-messages.md +++ b/rules/20-i18n-and-user-facing-messages.md @@ -70,6 +70,15 @@ imply a coverage that was not there. `western-locales-translation-regression.test.ts`** — catch English left untranslated, with an explicit allow-list of approved technical terms. - **`supported-locales.test.ts`** — every `Locale` member has a dictionary. +- **`constants/__tests__/learn-content-locale-completeness.test.ts`** (2026-09-01) — + the four tests above all assert against `lib/i18n/locales/*`, the UI-chrome + dictionary; none of them touch `constants/learn-content/*.constants.ts`, the + `/learn` article prose. This test covers that separately-maintained content + cluster for all 13 locales: key/section/FAQ parity, placeholder parity, and + the same untranslated-English-fallback check with its own allow-list. Any + other long-form content cluster added the same way (see + `public-comparison-content/`) needs the same treatment — it will not be + caught by the UI-dictionary tests either. - **TS config** — an `i18n.types.ts` mismatch fails `npm run typecheck`. - **Review checklist** — visual spot-check of one non-EN locale.