Setting up a new localization

Step-by-step maintainer guide for onboarding a new language localization to the OpenTelemetry website.

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:

  1. Under import: — import the cspell dictionary for your locale:

    - '@cspell/dict-CSPELL_DICT_ID/cspell-ext.json'
    
  2. Under dictionaryDefinitions: — register the custom word list:

    - name: LANG_ID-words
      path: .cspell/LANG_ID-words.txt
    
  3. Under 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.

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.

  1. 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-approvers as a fallback owner until the locale graduates.

  2. 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:
  • 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-approvers team 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); pull or triage means 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 .permission
    

    Expect write, maintain, or admin for each member listed (the permission reported is the member’s effective one, from all sources).

Step 7 — Slack channel and community listing

  1. Create a channel for the locale in the CNCF Slack workspace, named #otel-localization-LANG_ID (for example, #otel-localization-pl for Polish).
  2. Add the OpenTelemetry Admin as a channel manager.
  3. Open a PR against open-telemetry/community that adds a localization-LANG_ID entry to workstreams.yml, modeled on the existing locale entries, so that the team appears under Localization Teams in the generated sigs.md. For the procedure, see Updating SIG information.

Step 8 — Project tracking

Update projects/localization.md with the new locale’s information:

  1. 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/
    
  2. 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`
    
  3. Add the lang:LANG_ID label to the Labels section:

    - [`lang:LANG_ID`][issues-lang-LANG_ID] - EnglishName localization
    

    With the corresponding link definition:

    [issues-lang-LANG_ID]:
      https://github.com/open-telemetry/opentelemetry.io/issues?q=is%3Aissue%20state%3Aopen%20label%3Alang%3ALANG_ID
    
  4. Add 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.yml updated
  • Step 4 — .prettierignore updated (if applicable for the script)
  • Step 5 — .github/component-label-map.yml (label entry) and data/locale-teams.yaml (registry entry) updated; .github/CODEOWNERS regenerated
  • Step 6 — Team open-telemetry/admin PR merged; approvers team holds write access; eligible members added and verified
  • Step 7 — Slack channel #otel-localization-LANG_ID created; OpenTelemetry Admin added as channel manager; team listed in the community sigs.md
  • Step 8 — projects/localization.md updated 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 the lang:LANG_ID label is applied automatically.