Documentation Guide
This guide explains how Merlon's documentation is organized, translated, and checked, and how to run the checks locally.
Canonical language and layout
English is the canonical language for all hand-written documentation, under
docs/ in the repository root. Every other locale is a translation of that
source.
docs/decisions/**(Architecture Decision Records) is excluded from the built site entirely and is written in Japanese; it is not translated and the documentation checks skip it.docs/api/**is generated reference output (OpenAPI, gRPC proto, and JSON Schema reference pages) and is gitignored. Never edit it directly — it is overwritten bymake generate-openapi,make generate-proto-docs, and the website's schema-doc generator. See Protocol Buffers Workflow for the proto side of this pipeline.
ja translations
Japanese translations live at
website/i18n/ja/docusaurus-plugin-content-docs/current/, mirroring the
relative path of each English doc under docs/. The reference generators
(OpenAPI, proto, schema) emit both the en and ja reference pages directly
from their source comments/schemas, so docs/api/** and its ja counterpart
are kept in sync automatically and are out of scope for translation checks.
Any hand-written English doc that does not yet have a real translation is
filled in at build time with an English fallback copy by
website/scripts/sync-i18n-fallback.mjs, so relative links between pages
never 404 in the ja locale. Fallback copies are recorded in
website/.i18n-fallback-manifest.json and are not real translations —
the checks below treat them the same as a missing translation.
Translation workflow rule
Any pull request that changes a hand-written English doc under docs/ (i.e.
not docs/api/** or docs/decisions/**) must do one of:
- Update the corresponding ja file under
website/i18n/ja/docusaurus-plugin-content-docs/current/in the same PR, or - Add an entry to
website/docs-lint/i18n-freshness-acks.jsonacknowledging that the ja translation is intentionally not yet updated for this en revision (see below).
This keeps translations from silently rotting behind the English source.
The five checks
node website/scripts/checks/run-all.mjs runs all five checks below and
prints a pass/fail table; it exits non-zero if any check fails. Run it with
make docs-check.
1. check-en-language
Fails if Japanese characters appear anywhere in a hand-written English doc,
or in proto/merlon/v1/*.proto. Allowed exceptions (e.g. Japanese statute
names quoted intentionally in prose) are listed in
website/docs-lint/jp-allowlist.json, an array of exact-substring entries:
[{ "file": "docs/compliance/data-retention.md", "text": "<exact substring to allow>" }]
Stale-entry rule: if an allowlist entry's text no longer appears in its
file, the check fails — the allowlist must always describe text that
actually exists, or it silently stops guarding anything.
2. check-title-style
For hand-written docs and _category_.json labels: frontmatter title must
exist and, if the doc has an H1, must equal it exactly. Titles, frontmatter
sidebar_labels, and category labels must be Title Case (every word
capitalized except minor words — a, an, the, and, or, nor, but, for, of, in,
on, to, at, by, with, vs, via — unless first or last). Proper nouns listed in
website/docs-lint/proper-nouns.json (gRPC, OpenAPI, API, CDD, JWT, ...) must
appear exactly as spelled there.
3. check-i18n-parity
Every hand-written English doc must have a ja counterpart at the same
relative path that is a real translation: it must exist, must not be
byte-identical to the English source, must not be a fallback copy per the
manifest, and its first H1 must contain Japanese characters. Exceptions
(docs intentionally left untranslated) go in
website/docs-lint/i18n-exceptions.json, an array of docs/-relative paths.
Stale-entry rule: an exception for a path that is no longer a hand-written English doc fails the check.
4. check-i18n-freshness
For each real en/ja translation pair, the ja file must not be older than the
English source, compared by last-commit time (git log -1 --format=%ct). If
the English file has uncommitted local changes, it is always treated as
newer than any committed ja translation. A brand-new pair where both sides
are uncommitted (added together in one PR) passes.
An out-of-date pair can be acknowledged in
website/docs-lint/i18n-freshness-acks.json, mapping the docs/-relative
path to the English file's current HEAD commit hash
(git log -1 --format=%H -- <file>):
{ "getting-started.md": "a1b2c3d4..." }
If the English file has uncommitted local modifications there is no commit
hash to pin to; use a content-hash ack instead, prefixed with sha256::
{ "getting-started.md": "sha256:<sha256 hex digest of the en file content>" }
Stale-entry rule: an ack whose hash does not match the English file's
current HEAD hash (or, for sha256: acks, the current file content) fails
the check — it must be re-confirmed (or the translation updated) after any
further English change.
5. check-ui-translations
Checks that the Docusaurus UI-string translation files exist and are valid
JSON: website/i18n/ja/code.json,
website/i18n/ja/docusaurus-plugin-content-docs/current.json,
website/i18n/ja/docusaurus-theme-classic/navbar.json, and
.../footer.json. Every message value must contain Japanese characters, or
be entirely composed of tokens allowlisted in
website/docs-lint/ui-string-allowlist.json (brand names and acronyms like
GitHub, Merlon, REST API; numerals and punctuation never count against a
message).
Running the checks
make docs-check
This is equivalent to node website/scripts/checks/run-all.mjs and requires
no npm install — the checks are dependency-free Node ESM scripts. CI runs
the same command in the "Docs checks" step of docs-deploy.yml, before the
site build, using full git history (fetch-depth: 0) so the freshness check
can compare commit timestamps.