Locales & the translate pre-commit hook
src/locales/{en,zh_CN}.json, the auto-translate pre-commit hook, and the Chinese terminology conventions AdminJS labels follow.
Deep-dive doc split out of
.claude/rules/architecture.md(which is now an index). Append new findings about this area here, not to the index.
The translate pre-commit hook writes locale entries for you — review them
Committing a Prisma schema change that adds columns triggers a translate step in the pre-commit hook. It generates an entry for every new field name in both src/locales/en.json and src/locales/zh_CN.json, machine-translating the Chinese via gpt-4o-mini, and stages the result into your commit. A commit you expected to touch 13 files lands as 15.
This is convenient but it means machine-translated, user-visible Chinese labels enter the product without anyone writing them. Always read the locale diff after committing a schema change (git show --stat HEAD to spot it, then git show HEAD -- src/locales/). Observed failure modes, all from INFRA-638's four new keys:
- Wrong domain term.
baseSize→基础尺寸. In apparel,尺码is the garment size;尺寸is a physical dimension. See the convention below. - Invented vocabulary that ignores the feature's existing terms.
lengthVariant→长度变体, while the sibling ticket in the same epic (INFRA-637) already shipped可变长度for the identical concept. Two Chinese names for one feature across two screens. Check what the rest of the epic already uses before accepting the hook's guess. - Keys for fields that never render (see reverse relations below).
The hook skips cleanly when no matching files are staged (translate (skip) no matching staged files), so a locale-only follow-up commit does not re-trigger it.
Keys live under properties, not labels
Prisma field labels land in the properties block. There is a separate labels block with its own overlapping-looking keys — labels.Size is 尺寸 while properties.size is 尺码. When checking a convention or fixing a term, confirm which block you are in; grepping the raw JSON by line number does not tell you.
Top-level blocks: labels, properties, components, resources, buttons, actions, messages, time, pages.
Chinese terminology: apparel size is 尺码
Under properties, the size family is consistently 尺码, and ID fields take a bare ID suffix:
| Key | Chinese |
|---|---|
size | 尺码 |
sizeCode | 尺码号 |
sizeRange | 尺码范围 |
sizeRangeId | 尺码范围ID |
sizeRangeToSize | 尺码范围至尺码 |
baseSize / baseSizeId | 基础尺码 / 基础尺码ID |
Counts under properties: 10 uses of 尺码 vs 1 of 尺寸. The lone outlier is sizeId: 尺寸ID, which contradicts its own sibling sizeRangeId: 尺码范围ID and looks like the same mistake made earlier and never caught — a candidate cleanup, untouched so far because it is user-visible and out of scope for the tickets that noticed it.
Reverse array relations get keys that never render
Adding a self-relation or any bidirectional relation produces keys for both sides, e.g. baseSize (to-one, FK-owning) and lengthVariants (Size[], reverse). Only the FK-owning side is ever looked up: @adminjs/prisma's Resource.prepareProperties() skips relation fields where the model doesn't own the FK (field.relationName && !field.relationFromFields?.length), so no label is requested for the array side. Those entries are inert.
The file already carries such keys (sizeRangeToSize is the same kind of reverse relation and has one), so leaving them is consistent with precedent rather than a bug — but don't spend effort translating them well, and don't assume a key's existence means a UI surface exists.
A reverse key therefore often duplicates its FK-side sibling's translation, and that is expected. lengthVariant and lengthVariants are both 可变长度 in zh_CN.json, because Chinese does not inflect for plural and only the singular ever renders. A locale-coverage audit will see this as a silent duplicate; it is inert, not a missed translation. Invent a distinct string only if the reverse key ever becomes user-visible — which, per the prepareProperties() behaviour above, would itself be the surprising part worth investigating first.
Pre-existing en/zh key-parity gaps
A parity check across both files is not clean at baseline, so don't read a mismatch as something your change caused. As of 2026-08 the standing gaps are:
- EN-only:
pages.dashboard.quarterSuffix - CN-only:
actions.exportMeasurements,actions.uploadMeasurements
Verify against git show HEAD:src/locales/... before attributing a gap to your own diff.