Setting up a new localization
This guide walks OTel website maintainers through every change required to onboard a new language localization. It covers both repository-level changes and GitHub organization-level setup.
For the contributor-facing side — translation guidance, drift tracking, and
ongoing maintenance — see Site localization. For an agent-run version of the
repository-side steps, see the setup-new-localization skill.
The canonical registry of active localization teams and their resources is in
projects/localization.md.
Prerequisites
Before starting, confirm the following with the locale team:
- A kickoff issue has been filed following the steps in New localizations.
- The ISO 639-1 language code (
LANG_ID) has been agreed upon. - GitHub handles for the mentor and initial contributors are known.
In the rest of this guide, replace every occurrence of LANG_ID with the actual
ISO 639-1 code (for example, pl for Polish).
Step 1 — Hugo language configs
Step 1a. Language config entry
Add an entry for the new language in config/_default/hugo.yaml under the
languages: key:
LANG_ID:
label: NativeName
locale: LANG_ID-REGION # optional; see note below
params:
description: <site description translated into the new language>
The locale field is optional: use it when the site should emit a regional
language tag, such as en-US, pl-PL, or zh-CN, in RSS feeds, for example;
otherwise the language ID is used. Google Translate needs the full zh-CN tag
for Chinese, while most other languages use the primary subtag.
As an example, the Polish entry looks like:
pl:
label: Polski
locale: pl-PL
params:
description: Strona projektu OpenTelemetry
Step 1b. Translation File
Within the i18n directory, create a new file named LANG_ID.yaml (for
example, pl.yaml). This file will contain some translated strings for the new
language. These strings are used for UI elements and other site components that
may not necessarily be part of the main content, or are used in multiple pages.
Step 2 — Hugo content mounts
Hugo uses content mounts to route locale-specific content and to fall back to
English pages for sections that have not yet been translated. Add a block for
LANG_ID in config/_default/module-template.yaml under the top-level
mounts: section (this template is rendered into module.yaml).
Base setup
Every locale requires at least the following mounts — the locale’s own content plus fallbacks for the core English sections:
## LANG_ID
- source: content/LANG_ID # locale-specific pages
target: content
sites: &LANG_ID-matrix
matrix: { languages: [LANG_ID] }
# fallback pages (serve English content where no translation exists yet)
- source: content/en/_includes
target: content/_includes
sites: *LANG_ID-matrix
- source: content/en/announcements
target: content/announcements
sites: *LANG_ID-matrix
- source: content/en/docs
target: content/docs
files: ['! specs/**'] # exclude spec fragments (too large to fall back)
sites: *LANG_ID-matrix
Additional sections (such as ecosystem) can be added as the localization
matures. For example, the pt block includes an ecosystem fallback:
## pt
- source: content/pt
target: content
sites: &pt-matrix
matrix: { languages: [pt] }
# fallback pages
- source: content/en/_includes
target: content/_includes
sites: *pt-matrix
- source: content/en/announcements
target: content/announcements
sites: *pt-matrix
- source: content/en/docs
target: content/docs
files: ['! specs/**']
sites: *pt-matrix
- source: content/en/ecosystem
target: content/ecosystem
sites: *pt-matrix
Insert the new block alongside the existing locale blocks in
config/_default/module-template.yaml, following the current ordering
convention used in that file.
Step 3 — Spell checking
3a. Check for a cspell dictionary
Search npm for an existing cspell dictionary for the language:
npm search @cspell/dict
Look for a package matching @cspell/dict-LANG_ID or the closest regional
variant (for example, @cspell/dict-pl_pl for Polish). You can also browse the
full list of available dictionaries at the
cspell-dicts repository.
3b. Install the dictionary (if available)
npm install --save-dev @cspell/dict-LANG_ID
This adds the package to package.json. Commit the updated package.json and
package-lock.json.
3c. Create the custom word list
Create an empty file for site-local technical terms:
touch .cspell/LANG_ID-words.txt
Commit the empty file. Contributors will add locale-specific technical terms here over time.
3d. Update .cspell.yml
Add three entries to .cspell.yml to enable spell checking for the new
language:
Under
import:— import the cspell dictionary for your locale:- '@cspell/dict-CSPELL_DICT_ID/cspell-ext.json'Under
dictionaryDefinitions:— register the custom word list:- name: LANG_ID-words path: .cspell/LANG_ID-words.txtUnder
dictionaries:— activate both the imported dictionary and the custom word list:- CSPELL_DICT_ID # the @cspell/dict-CSPELL_DICT_ID package - LANG_ID-words # the .cspell/LANG_ID-words.txt list
Keep entries in each section in alphabetical order by language code.
If no cspell dictionary package exists for the language, skip steps 3b and the
import and dictionaries entries. Only create the custom word list (step
3c) and register it under dictionaryDefinitions.
Also add the locale path to the ignorePaths list in .cspell.yml so
that cspell does not attempt to spell-check content it cannot validate:
ignorePaths:
- content/LANG_ID
Step 4 — Prettier (conditional)
If Prettier does not handle the language well — for example, scripts that are
right-to-left or use non-Latin characters — add an ignore entry to
.prettierignore:
content/LANG_ID/**
Check existing ignore entries in .prettierignore to see whether other
locales with similar scripts have already been excluded, and follow the same
pattern. This step is optional and should only be done when Prettier is known to
produce incorrect formatting for the language.
Step 5 — GitHub repository automation
Component label map
In .github/component-label-map.yml, add an entry that triggers the
lang:LANG_ID label on any PR touching content/LANG_ID/:
lang:LANG_ID:
- changed-files:
- any-glob-to-any-file:
- content/LANG_ID/**
Keep the entries in alphabetical order.
Code owners
The locale section of .github/CODEOWNERS is generated from the
data/locale-teams.yaml registry; don’t edit it by hand.
Add the locale to the registry, in alphabetical order, listing the mentor and initial contributors as approvers:
LANG_ID: maintainers: [] approvers: [GITHUB_HANDLE1, GITHUB_HANDLE2]A locale with no maintainers is unstaffed: its CODEOWNERS lines also list
@open-telemetry/docs-approversas a fallback owner until the locale graduates.Regenerate the CODEOWNERS locale section:
npm run fix:codeowners
Include both changes in the PR that adds content/LANG_ID/: CI requires the
registry and the locale directories to agree.
Do not add entries to .github/component-owners.yml; changes to this
file are no longer required as of June 2026.
Step 6 — GitHub org-level setup
These steps happen outside the repository and require maintainer-level access to
the open-telemetry GitHub organization. Team creation and the repository grant
go into one pull request against the private open-telemetry/admin
repository, which an org admin merges and applies.
6a. Create the teams
In teams.tf, declare docs-LANG_ID-approvers and, as its child team,
docs-LANG_ID-maintainers, both with privacy = "closed" (a secret team can’t
be nested or act as a code owner). For an example of the team declarations, see
admin#588.
6b. Grant the approvers team write access
In the same PR, add github_team.docs-LANG_ID-approvers.id to the approvers
list of repo-opentelemetry-io.tf: a code owner team must itself hold write
access, whatever its members hold through other teams.
6c. Add team members
An org admin adds team members manually; the admin repository doesn’t manage memberships.
- Who qualifies:
- Org members elected approver per the community membership guidelines (Becoming an Approver).
- Kickoff contributors usually qualify later: until then their reviews count as described in New localizations, and the kickoff issue tracks their promotion.
- Maintainers team: can stay empty at first; the locale is then unstaffed.
- Delegation: once the locale has maintainers, add them to both teams and give them the GitHub team maintainer role on both, so they can manage membership themselves. The role requires membership in the team, so on the maintainers team grant it only to locale maintainers.
6d. Verify access
Confirm the team’s grant once the admin PR is applied, and each member’s access as members are added:
The
docs-LANG_ID-approversteam is listed under the repository’s collaborators and teams with the Write role. For example, using the GitHub CLI:gh api --paginate repos/open-telemetry/opentelemetry.io/teams \ --jq '.[] | select(.slug == "docs-LANG_ID-approvers") | .permission'Expect
push, the API’s name for the Write role. Nothing printed means the team isn’t in the repository’s access list (the grant in Step 6b is missing or not yet applied, or the team doesn’t exist yet);pullortriagemeans a grant below write.Every team member appears in the live team, and each has at least write access to the repository. For example:
gh api --paginate orgs/open-telemetry/teams/docs-LANG_ID-approvers/members --jq '.[].login' gh api repos/open-telemetry/opentelemetry.io/collaborators/GITHUB_HANDLE/permission --jq .permissionExpect
write,maintain, oradminfor each member listed (the permission reported is the member’s effective one, from all sources).
Step 7 — Slack channel and community listing
- Create a channel for the locale in the CNCF Slack workspace, named
#otel-localization-LANG_ID(for example,#otel-localization-plfor Polish). - Add the OpenTelemetry Admin as a channel manager.
- Open a PR against
open-telemetry/communitythat adds alocalization-LANG_IDentry toworkstreams.yml, modeled on the existing locale entries, so that the team appears under Localization Teams in the generatedsigs.md. For the procedure, see Updating SIG information.
Step 8 — Project tracking
Update projects/localization.md with the new locale’s information:
Add the language to the supported languages list at the top, in alphabetical order by language code:
- [NativeName - EnglishName (LANG_ID)][LANG_ID] [LANG_ID]: https://opentelemetry.io/LANG_ID/Add a team entry under Current language teams following the same structure as existing entries:
**EnglishName**: - Website: <https://opentelemetry.io/LANG_ID/> - Slack channel: [`#otel-localization-LANG_ID`](https://cloud-native.slack.com/archives/XXXXXXXXXXX) - Maintainers: `@open-telemetry/docs-LANG_ID-maintainers` - Approvers: `@open-telemetry/docs-LANG_ID-approvers`Add the
lang:LANG_IDlabel to the Labels section:- [`lang:LANG_ID`][issues-lang-LANG_ID] - EnglishName localizationWith the corresponding link definition:
[issues-lang-LANG_ID]: https://github.com/open-telemetry/opentelemetry.io/issues?q=is%3Aissue%20state%3Aopen%20label%3Alang%3ALANG_IDAdd the Slack channel link definition:
[otel-localization-LANG_ID]: https://cloud-native.slack.com/archives/CHANNEL_ID
Verification
Setup checklist
Use this checklist to track setup to completion. Steps 1-5 and 8 land in the setup PR; Steps 6 and 7 complete outside the repository.
- Step 1 — Language entry added to
config/_default/hugo.yaml - Step 2 — Content mounts added to
config/_default/module-template.yaml - Step 3 — cSpell configured: dictionary installed (or locale added to
ignorePaths), custom word list created at.cspell/LANG_ID-words.txt, and.cspell.ymlupdated - Step 4 —
.prettierignoreupdated (if applicable for the script) - Step 5 —
.github/component-label-map.yml(label entry) anddata/locale-teams.yaml(registry entry) updated;.github/CODEOWNERSregenerated - Step 6 — Team
open-telemetry/adminPR merged; approvers team holds write access; eligible members added and verified - Step 7 — Slack channel
#otel-localization-LANG_IDcreated; OpenTelemetry Admin added as channel manager; team listed in the communitysigs.md - Step 8 —
projects/localization.mdupdated with the language entry, team entry, label, and Slack channel link
Automated checks
After all PRs are merged, run the following to confirm the configuration is correct:
npm run build— confirms Hugo recognizes the new language without errors.npm run check:spelling— confirms the cspell configuration is valid and that no errors are introduced by the new dictionary entries.npm run check:codeowners— confirms the CODEOWNERS locale section matches the registry.- GitHub label automation — open a test PR that touches a file under
content/LANG_ID/and confirm thelang:LANG_IDlabel is applied automatically.