| name | setup-new-localization |
| description | Set up a new website localization (language) for opentelemetry.io. Use when adding a new language/locale, wiring a new `content/<lang>/` tree, or onboarding a localization team. |
| argument-hint | <kickoff-issue | lang-code> |
| allowed-tools | Bash Read Edit Write Grep Glob |
| cSpell:ignore | endonym unstaffed |
Set up a new localization
Wire a new language into the site end-to-end. Start from one of:
- A kickoff issue (preferred) — its number or URL, e.g.
#9577. Read it with
gh issue view <n> -R open-telemetry/opentelemetry.io and pull out:
- ISO 639-1 code →
<lang> (required)
- Locale, if the issue specifies one →
<locale> (optional), e.g. pl-PL,
zh-CN — otherwise decide in step (a)
- Language name →
<native-label>
- Locale mentor + Contributors → the approvers (
data/locale-teams.yaml)
- A bare
<lang> ISO 639-1 code when there's no issue yet. You then gather
the team handles and native label yourself.
Either way, derive these; don't ask for them as separate arguments:
- Hugo locale — from the issue if it specifies one, otherwise decided in
step (a).
<native-label> — the endonym for that language (from the issue, or see
gotchas for picking the right one).
<country> — the language's primary country, for the Slack channel / team
naming and the label color (step (g)).
[!NOTE]
Most lists below are ordered alphabetically by <lang> (English en stays
first). Some aren't — projects/localization.md's "Current language teams"
section is ordered by English name. At each insertion point, match the
ordering of the existing entries rather than assuming one global rule.
Steps
a. config/_default/hugo.yaml — language block
First decide the locale:
- If the kickoff issue already specifies a
<locale>, use it.
- Otherwise, if
<lang> maps to a single common locale, omit locale:.
Hugo defaults it to <lang> (the ISO 639-1 code), as bn, es, fr, and
ja do.
- Otherwise, if
<lang> has several regional variants in real use (e.g. pt →
pt-BR / pt-PT, zh → zh-CN / zh-TW, en → en-US / en-GB), ask
the user which locale with the AskUserQuestion tool (if available;
otherwise ask in plain text), then set locale: to their answer.
Then add the block under languages:, in alphabetical order:
<lang>:
label: <native-label>
locale: <LOCALE>
params:
description: <translated site description>
b. config/_default/module-template.yaml — content mounts
Copy the ## <lang> block from an existing language (e.g. ## ja) into
alphabetical position. The per-locale edits are just the language code and the
YAML anchor name: the primary mount defines &<lang>-matrix, and the fallback
mounts (_includes, announcements, docs) reference it as *<lang>-matrix.
- source: content/<lang>
target: content
sites: &<lang>-matrix
matrix: { languages: [<lang>] }
c. content/<lang>/.gitkeep — placeholder content dir
The mount in step (b) points at content/<lang>, so the directory must exist.
Create it empty — git won't track an empty folder, so the .gitkeep is the
tracked file:
mkdir -p content/<lang> && touch content/<lang>/.gitkeep
Do NOT copy the English homepage into the setup PR. The translated homepage
lands in its own follow-up PR (e.g. #10431 for Korean). Copying
content/en/_index.md here only creates merge conflicts with that PR and drags
in a default_lang_commit you'd then have to manage. Keep the setup PR to the
wiring; let .gitkeep hold the empty tree.
d. .cspell.yml — custom word list
Add the custom list in both sections (alphabetical):
dictionaryDefinitions:
- name: <lang>-words
path: .cspell/<lang>-words.txt
dictionaries:
- <lang>-words
Upstream dict: check the cspell-dicts repo, not a guessed npm name. The
published package names are inconsistent (dict-es-es with a hyphen vs
dict-pl_pl with an underscore), but the dictionary folders
follow one rule: <lang> (e.g. bn) or <lang>_<REGION> (e.g. es_ES,
pl_PL, pt_BR, uk_UA). A matching folder means a dict exists; no folder (no
ko, ja, zh) means there isn't one — the folder listing is the source of
truth.
The dictionary id is the folder name lowercased with _ → - (pl_PL →
pl-pl). For the exact package name and version, read them from npm
(npm view @cspell/dict-<id> name version, falling back to the underscore form
if the hyphen one 404s). Then wire it in three places:
.cspell.yml import: '@cspell/dict-pl_pl/cspell-ext.json' (exact package
name)
.cspell.yml dictionaries: id: pl-pl (hyphen, even though the package
uses _)
package.json devDependency: "@cspell/dict-pl_pl": "<version>"
Korean has no folder, so for ko add only the custom ko-words list and
leave the package.json devDependencies unchanged.
e. .cspell/<lang>-words.txt — empty custom list
touch .cspell/<lang>-words.txt
f. Prettier prose-wrapping — default: leave it alone
Default: no Prettier changes for a new locale. Per localization.md,
prose-wrap exceptions exist only for languages Prettier mishandles; a locale
opts in later, when its team hits the problem — not at setup time.
If the team does opt in, it's two coupled edits, and one without the other
is a silent no-op:
- a
/content/<lang> line in .prettierignore (exempts the dir from the
default proseWrap: always pass), and
content/<lang> in the _check:format:nowrap script in package.json
(re-checks it with --prose-wrap preserve).
Read .prettierignore and that script for the current members rather than
trusting a snapshot here.
g. lang:<lang> label — labeler config and the GitHub label
Wire the auto-labeler in .github/component-label-map.yml:
lang:<lang>:
- changed-files:
- any-glob-to-any-file:
- content/<lang>/**
The lang:<lang> GitHub label must also exist, or the labeler has nothing to
apply. Create it with the country's main flag color (a maintainer action —
needs repo triage/write):
gh label create "lang:<lang>" -R open-telemetry/opentelemetry.io --color <hex>
h. data/locale-teams.yaml — the CODEOWNERS source of truth
Since #10295 this registry generates the locale section of CODEOWNERS. It
records the expected membership of the docs-<lang>-* teams (created in the
admin repo — see Out-of-repo). Under the
locales: map, in alphabetical order, add:
<lang>:
maintainers: []
approvers: [<mentor-and-contribs>]
Fill approvers with whatever roles are already known — even partial — rather
than leaving it empty waiting to assess contributions. Empty maintainers marks
the locale unstaffed: its CODEOWNERS lines fall back to
@open-telemetry/docs-approvers.
i. Regenerate CODEOWNERS
npm run fix:codeowners
npm run check:codeowners
Never hand-edit the generated block in .github/CODEOWNERS.
j. projects/localization.md — 5 insertions (match neighbor ordering)
- Supported-languages list:
- [<native-label> - <Lang> (<lang>)][<lang>]
and its link ref [<lang>]: https://opentelemetry.io/<lang>/
- "Current language teams" section block (Website / Slack / Maintainers /
Approvers, mirroring a sibling) — this section is ordered by English
name, not
<lang>
- Labels list:
- [`lang:<lang>`][issues-lang-<lang>] - <Lang> localization
- Slack channel ref:
[otel-localization-<lang>]: <channel-url>
- Issues label ref:
[issues-lang-<lang>]: <issues-search-url>
Do NOT (gotchas)
- Do NOT touch
.github/component-owners.yml. Pre-#10295 the old process
added a content/<lang>: block there; #10295 removed all locale teams
from that file. It now holds only non-locale component reviewers. Re-adding a
locale duplicates ownership.
- Do NOT bundle the homepage. The setup PR creates only
content/<lang>/.gitkeep; the translated _index.md is a separate PR — see
step (c).
- Do NOT hand-edit the generated locale section of
.github/CODEOWNERS —
see step (i).
- Pick the right endonym. Korean is
한국어 (South Korea), not 조선말
(North Korea). Choose the endonym matching the language's primary country.
- Never assume an upstream cSpell dict exists — see step (d).
Out-of-repo: create the org teams {#out-of-repo-create-the-org-teams}
The generated CODEOWNERS references @open-telemetry/docs-<lang>-approvers;
GitHub flags it as invalid until that team exists. The two teams live in the
open-telemetry/admin repo's teams.tf, with
docs-<lang>-maintainers as a child of docs-<lang>-approvers:
resource "github_team" "docs-<lang>-approvers" {
name = "docs-<lang>-approvers"
description = ""
privacy = "closed"
}
resource "github_team" "docs-<lang>-maintainers" {
name = "docs-<lang>-maintainers"
parent_team_id = github_team.docs-<lang>-approvers.id
description = ""
privacy = "closed"
}
This is a separate PR against open-telemetry/admin (example:
admin#716) that an org admin must merge; the admin then applies
team membership to match data/locale-teams.yaml. You can prepare and describe
this PR, but you can't merge it.
Validation checklist
npm run check:codeowners
npm run check:format
git diff --name-only
git diff --name-only | grep component-owners.yml && echo "BUG: do not touch" || echo OK
gh label list -R open-telemetry/opentelemetry.io | grep "lang:<lang>"
Expected changed/added paths:
config/_default/hugo.yaml
config/_default/module-template.yaml
content/<lang>/.gitkeep (new)
.cspell.yml
.cspell/<lang>-words.txt (new)
.github/component-label-map.yml
.github/CODEOWNERS (generated)
data/locale-teams.yaml
projects/localization.md
package.json only if an upstream @cspell/dict-* was added (step d)