Files
mintlify__docs/guides/internationalization.mdx
mintlify[bot] 65e9a5bc2c docs: document zh-CN, zh-TW, and ja-JP locale codes (#6423)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2026-07-08 08:19:33 -07:00

756 lines
25 KiB
Plaintext

---
title: "How to set up multi-language documentation"
sidebarTitle: "Internationalization"
description: "Set up multi-language documentation with locale-based routing, language switcher navigation, and translated content to reach global audiences."
keywords: ["internationalization", "i18n", "multi-language documentation", "translations", "localization", "language switcher"]
---
Internationalization (i18n) is the process of designing software or content to work for different languages and locales. This guide explains how to structure files, configure navigation, and maintain translations effectively so that you can help users access your documentation in their preferred language and improve global reach.
## File structure
Organize translated content in language-specific directories to keep your documentation maintainable and structure your navigation by language.
Create a separate directory for each language using [ISO 639-1 language codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). Place translated files in these directories with the same structure as your default language.
<Expandable title="Supported language codes">
- `ar` - Arabic
- `ca` - Catalan
- `zh` or `zh-Hans` or `zh-CN` - Chinese (Simplified)
- `zh-Hant` or `zh-TW` - Chinese (Traditional)
- `cs` - Czech
- `da` - Danish
- `de` - German
- `en` - English
- `es` - Spanish
- `fi` - Finnish
- `fr` - French
- `fr-CA` - French (Canadian)
- `he` - Hebrew
- `hi` - Hindi
- `hu` - Hungarian
- `id` - Indonesian
- `it` - Italian
- `ja` or `ja-JP` - Japanese
- `ko` - Korean
- `lv` - Latvian
- `nl` - Dutch
- `no` - Norwegian
- `pl` - Polish
- `pt` or `pt-BR` - Portuguese
- `ro` - Romanian
- `ru` - Russian
- `sv` - Swedish
- `tr` - Turkish
- `uk` - Ukrainian
- `uz` - Uzbek
- `vi` - Vietnamese
</Expandable>
```text Example file structure
docs/
├── index.mdx # English (default)
├── quickstart.mdx
├── fr/
│ ├── index.mdx # French
│ ├── quickstart.mdx
├── es/
│ ├── index.mdx # Spanish
│ ├── quickstart.mdx
└── zh/
├── index.mdx # Chinese
└── quickstart.mdx
```
<Tip>
Keep the same filenames and directory structure across all languages. This makes it easier to maintain translations and identify missing content.
</Tip>
## Configure the language switcher
To add a language switcher to your documentation, configure the `languages` array in your `docs.json` navigation.
```json docs.json
{
"navigation": {
"languages": [
{
"language": "en",
"groups": [
{
"group": "Getting started",
"pages": ["index", "quickstart"]
}
]
},
{
"language": "es",
"groups": [
{
"group": "Comenzando",
"pages": ["es/index", "es/quickstart"]
}
]
}
]
}
}
```
Each language entry in the `languages` array requires:
- `language`: ISO 639-1 language code
- Full navigation structure
- Paths to translated files
The navigation structure can differ between languages to accommodate language-specific content needs.
<Warning>
Do not use the same page path in more than one language. Duplicating paths across languages results in undefined behavior. Each page path must appear in only one language's navigation.
</Warning>
### Set default language
The first language in the `languages` array is automatically used as the default. To use a different language as the default, either reorder the array or add the `default` property:
```json docs.json
{
"navigation": {
"languages": [
{
"language": "es",
"groups": [...]
},
{
"language": "en",
"groups": [...]
}
]
}
}
```
Alternatively, use the `default` property to override the order:
```json docs.json
{
"navigation": {
"languages": [
{
"language": "en",
"groups": [...]
},
{
"language": "es",
"default": true,
"groups": [...]
}
]
}
}
```
### Single language documentation
If you only want one language available without a language switcher, remove the `languages` field from your navigation configuration. Instead, define your navigation structure directly:
```json docs.json
{
"navigation": {
"tabs": [
{
"tab": "Documentation",
"groups": [
{
"group": "Getting started",
"pages": ["index", "quickstart"]
}
]
}
]
}
}
```
This displays your documentation in a single language without the language switcher UI.
<Tip>
Translate navigation labels like group or tab names to match the language of the content. This creates a fully localized experience for your users.
</Tip>
### Global navigation
To add global navigation elements that appear across all languages, configure the `global` object in your `docs.json` navigation.
```json docs.json
{
"navigation": {
"global": {
"anchors": [
{
"anchor": "Documentation",
"href": "https://example.com/docs"
},
{
"anchor": "Blog",
"href": "https://example.com/blog"
}
]
},
"languages": [
// Language-specific navigation
]
}
}
```
### Localized footer and navbar
Customize the footer and navbar for each language to display translated content and region-specific links.
Add `footer` and `navbar` properties to each language configuration:
```json docs.json
{
"navigation": {
"languages": [
{
"language": "en",
"footer": {
"socials": {
"x": "https://x.com/mintlify"
},
"links": [
{
"header": "Resources",
"items": [
{ "label": "Documentation", "href": "/en/docs" },
{ "label": "Blog", "href": "https://mintlify.com/blog" }
]
}
]
},
"navbar": {
"links": [
{ "label": "Docs", "href": "/en/docs" }
],
"primary": {
"type": "button",
"label": "Get Started",
"href": "/en/quickstart"
}
},
"groups": [
{
"group": "Getting started",
"pages": ["en/quickstart", "en/index"]
}
]
},
{
"language": "es",
"footer": {
"socials": {
"x": "https://x.com/mintlify"
},
"links": [
{
"header": "Recursos",
"items": [
{ "label": "Documentación", "href": "/es/docs" },
{ "label": "Blog", "href": "https://mintlify.com/blog" }
]
}
]
},
"navbar": {
"links": [
{ "label": "Documentación", "href": "/es/docs" }
],
"primary": {
"type": "button",
"label": "Comenzar",
"href": "/es/quickstart"
}
},
"groups": [
{
"group": "Comenzando",
"pages": ["es/quickstart", "es/index"]
}
]
}
]
}
}
```
Language-specific `footer` and `navbar` override the global settings for that language. If a language doesn't define these properties, it inherits the global configuration.
You can also configure a language-specific `banner` using the same pattern.
## Maintain translations
Keep translations accurate and synchronized with your source content.
### Translation workflow
1. Update source content in your primary language.
2. Identify changed content.
3. Translate changed content.
4. Review translations for accuracy.
5. Update translated files.
6. Verify navigation and links work.
### Automated translations
For automated translation solutions, [set up an automation](/automations) to run the agent on a schedule or in response to repository pushes.
### External translation providers
If you work with your own translation vendors or regional translators, you can integrate their workflow with your Mintlify documentation using GitHub Actions or similar CI/CD tools.
1. **Export source content**: Extract MDX files that need translation.
2. **Send to translators**: Provide files to your translation provider.
3. **Receive translations**: Get translated MDX files back.
4. **Import and deploy**: Add translated files to language directories and update navigation.
This GitHub Actions workflow automatically exports changed English content for translation when PRs merge to main.
```yaml .github/workflows/export-for-translation.yml
name: Export content for translation
on:
push:
branches: [main]
paths:
- '*.mdx'
- '!es/**'
- '!fr/**'
- '!zh/**'
# Prevent concurrent workflow runs to avoid race conditions
concurrency:
group: translation-export-${{ github.ref }}
cancel-in-progress: false
jobs:
export:
runs-on: ubuntu-latest
# Early exit if no changes detected (optional - acts as additional safety)
outputs:
files-changed: ${{ steps.changed.outputs.has-files }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Get changed MDX files
id: changed
run: |
# Check if parent commit exists (handles initial push)
if ! git rev-parse HEAD~1 >/dev/null 2>&1; then
echo "has-files=false" >> $GITHUB_OUTPUT
echo "files=" >> $GITHUB_OUTPUT
echo "No parent commit found - skipping export"
exit 0
fi
# Get list of changed MDX files (excluding translation dirs)
files=$(git diff --name-only HEAD~1 HEAD -- '*.mdx' ':!es/' ':!fr/' ':!zh/' | tr '\n' ' ')
if [ -z "$files" ]; then
echo "has-files=false" >> $GITHUB_OUTPUT
echo "files=" >> $GITHUB_OUTPUT
echo "No MDX files changed - skipping export"
else
echo "has-files=true" >> $GITHUB_OUTPUT
echo "files=$files" >> $GITHUB_OUTPUT
echo "Found changed files: $files"
fi
shell: bash
- name: Create translation package directory
if: steps.changed.outputs.has-files == 'true'
run: |
mkdir -p translation-export
echo "Created translation-export directory"
- name: Copy changed files to export directory
if: steps.changed.outputs.has-files == 'true'
run: |
failed_count=0
for file in ${{ steps.changed.outputs.files }}; do
if [ -f "$file" ]; then
target_dir="translation-export/$(dirname "$file")"
mkdir -p "$target_dir"
cp "$file" "$target_dir/"
echo "✓ Copied: $file"
else
echo "✗ File not found: $file"
((failed_count++))
fi
done
if [ $failed_count -gt 0 ]; then
echo "Warning: $failed_count file(s) could not be copied"
fi
shell: bash
- name: Validate translation package
if: steps.changed.outputs.has-files == 'true'
run: |
echo "Translation package contents:"
find translation-export -type f -name "*.mdx" | sort
echo ""
file_count=$(find translation-export -type f -name "*.mdx" | wc -l)
echo "Total MDX files: $file_count"
- name: Upload translation package
if: steps.changed.outputs.has-files == 'true'
uses: actions/upload-artifact@v4
with:
name: translation-export-${{ github.sha }}
path: translation-export/
retention-days: 30
if-no-files-found: error
compression-level: 9
- name: Print job summary
if: steps.changed.outputs.has-files == 'true'
run: |
echo "## Translation Export Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Artifact:** \`translation-export-${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Changed Files:**" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.changed.outputs.files }}" | tr ' ' '\n' | sed 's/^/- /' >> $GITHUB_STEP_SUMMARY
```
This GitHub Actions workflow validates and imports translated content when added via PR.
```yaml .github/workflows/import-translations.yml
name: Import translations
on:
pull_request:
paths:
- 'es/**'
- 'fr/**'
- 'zh/**'
# Define explicit permissions
permissions:
contents: read
pull-requests: write
jobs:
validate:
runs-on: ubuntu-latest
outputs:
validation-status: ${{ steps.final-check.outputs.status }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history to ensure origin/main is available
- name: Fetch origin/main reference
run: |
git fetch origin main:origin/main 2>/dev/null || echo "origin/main not available, using latest"
continue-on-error: true
- name: Get changed translation files
id: changed-files
run: |
# Get all changed MDX files in translation directories
files=$(git diff --name-only origin/main..HEAD -- 'es/**/*.mdx' 'fr/**/*.mdx' 'zh/**/*.mdx' | sort)
if [ -z "$files" ]; then
echo "No translation MDX files detected in this PR"
echo "files=" >> $GITHUB_OUTPUT
echo "count=0" >> $GITHUB_OUTPUT
else
echo "Found $(echo "$files" | wc -l) translation files"
echo "$files"
echo "files=$files" >> $GITHUB_OUTPUT
echo "count=$(echo "$files" | wc -l)" >> $GITHUB_OUTPUT
fi
shell: bash
- name: Validate frontmatter
id: frontmatter
if: steps.changed-files.outputs.count > 0
run: |
failed_files=()
success_count=0
total=${{ steps.changed-files.outputs.count }}
while IFS= read -r file; do
if [ ! -f "$file" ]; then
echo "✗ File not found: $file"
failed_files+=("$file")
continue
fi
# Check for valid frontmatter (lines 1-2 must be ---)
first_line=$(sed -n '1p' "$file")
second_line=$(sed -n '2p' "$file")
last_line=$(awk 'NF' "$file" | tail -1)
if [ "$first_line" = "---" ] && grep -q "^---$" "$file"; then
echo "✓ Valid frontmatter: $file"
((success_count++))
else
echo "✗ Invalid frontmatter in $file"
echo " Line 1: '$first_line'"
failed_files+=("$file")
fi
done <<< "${{ steps.changed-files.outputs.files }}"
echo ""
echo "Frontmatter check: $success_count/$total passed"
if [ ${#failed_files[@]} -gt 0 ]; then
echo "frontmatter_valid=false" >> $GITHUB_OUTPUT
printf 'failed_files=%s\n' "${failed_files[@]}" >> $GITHUB_OUTPUT
else
echo "frontmatter_valid=true" >> $GITHUB_OUTPUT
fi
shell: bash
- name: Check file structure
id: structure
if: steps.changed-files.outputs.count > 0
run: |
missing_sources=()
orphaned_count=0
while IFS= read -r translated_file; do
# Extract language and relative path
# e.g., "es/docs/guide.mdx" -> lang="es", relative_path="docs/guide.mdx"
lang=$(echo "$translated_file" | cut -d'/' -f1)
relative_path=$(echo "$translated_file" | cut -d'/' -f2-)
source_file="$relative_path"
if [ ! -f "$source_file" ]; then
echo "Missing source: $translated_file -> $source_file"
missing_sources+=("$translated_file")
((orphaned_count++))
else
echo "✓ Found source: $translated_file -> $source_file"
fi
done <<< "${{ steps.changed-files.outputs.files }}"
echo ""
echo "Structure check: $orphaned_count orphaned file(s)"
if [ $orphaned_count -gt 0 ]; then
echo "structure_valid=false" >> $GITHUB_OUTPUT
printf 'missing_sources=%s\n' "${missing_sources[@]}" >> $GITHUB_OUTPUT
else
echo "structure_valid=true" >> $GITHUB_OUTPUT
fi
shell: bash
- name: Validate file integrity
id: integrity
if: steps.changed-files.outputs.count > 0
run: |
integrity_passed=true
while IFS= read -r file; do
# Check file is readable and not empty
if [ ! -r "$file" ] || [ ! -s "$file" ]; then
echo "✗ File integrity issue: $file (not readable or empty)"
integrity_passed=false
fi
# Basic check: file should have content after frontmatter
line_count=$(wc -l < "$file")
if [ "$line_count" -lt 5 ]; then
echo "File is suspiciously short: $file ($line_count lines)"
fi
done <<< "${{ steps.changed-files.outputs.files }}"
if [ "$integrity_passed" = true ]; then
echo "integrity_valid=true" >> $GITHUB_OUTPUT
else
echo "integrity_valid=false" >> $GITHUB_OUTPUT
fi
shell: bash
- name: Generate validation report
if: always()
run: |
echo "## Translation Validation Report" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Files Changed:** ${{ steps.changed-files.outputs.count }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.changed-files.outputs.count }}" = "0" ]; then
echo "No translation MDX files found in this PR" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "> This could mean:" >> $GITHUB_STEP_SUMMARY
echo "- Only non-MDX files in es/, fr/, or zh/ directories were changed" >> $GITHUB_STEP_SUMMARY
echo "- Workflow was triggered but no translation content to validate" >> $GITHUB_STEP_SUMMARY
else
echo "### Validation Results" >> $GITHUB_STEP_SUMMARY
echo "- Frontmatter: ${{ steps.frontmatter.outputs.frontmatter_valid }}" >> $GITHUB_STEP_SUMMARY
echo "- File Structure: ${{ steps.structure.outputs.structure_valid }}" >> $GITHUB_STEP_SUMMARY
echo "- File Integrity: ${{ steps.integrity.outputs.integrity_valid }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
shell: bash
- name: Final validation check
id: final-check
# Only run this check if we actually had MDX files to validate
if: steps.changed-files.outputs.count > 0
run: |
validation_failed=false
if [ "${{ steps.frontmatter.outputs.frontmatter_valid }}" != "true" ]; then
echo "Frontmatter validation failed"
validation_failed=true
fi
if [ "${{ steps.structure.outputs.structure_valid }}" != "true" ]; then
echo "File structure validation failed"
validation_failed=true
fi
if [ "${{ steps.integrity.outputs.integrity_valid }}" != "true" ]; then
echo "File integrity validation failed"
validation_failed=true
fi
if [ "$validation_failed" = true ]; then
echo "status=failed" >> $GITHUB_OUTPUT
exit 1
else
echo "status=passed" >> $GITHUB_OUTPUT
echo "All validations passed"
fi
shell: bash
- name: Handle no-files-to-validate case
# Run only when there are no MDX files to validate
if: steps.changed-files.outputs.count == 0
run: |
echo "No translation MDX files to validate - PR is valid"
echo "status=no-changes" >> ${{ steps.final-check.outputs }}
shell: bash
```
**Best practices for external translation workflows**
- **Preserve frontmatter**: Ensure translators keep YAML frontmatter intact, translating only `title` and `description` values.
- **Protect code blocks**: Mark code blocks as "do not translate" for your vendors.
- **Use translation memory**: Provide glossaries with technical terms that should remain in English or have specific translations.
- **Automate validation**: Use CI checks to verify MDX syntax and frontmatter before merging translations.
- **Version control**: Track the source version for each translation to identify outdated content.
### Images and media
Store translated images in language-specific directories.
```
images/
├── dashboard.png # English version
├── fr/
│ └── dashboard.png # French version
└── es/
└── dashboard.png # Spanish version
```
Reference images using relative paths in your translated content.
```mdx es/index.mdx
![Captura de pantalla del panel de control](/images/es/dashboard.png)
```
## SEO for multi-language sites
Optimize each language version for search engines.
### Page metadata
Include translated metadata in each file's frontmatter:
```mdx fr/index.mdx
---
title: "Commencer"
description: "Apprenez à commencer avec notre produit."
keywords: ["démarrage", "tutoriel", "guide"]
---
```
## Best practices
### Date and number formats
Consider locale-specific formatting for dates and numbers.
- Date formats: MM/DD/YYYY (month/day/year) vs DD/MM/YYYY (day/month/year)
- Number formats: 1,000.00 vs 1.000,00
- Currency symbols: $100.00 vs 100,00€
Include examples in the appropriate format for each language or use universally understood formats.
### Maintain consistency
- Maintain content parity across all languages to ensure every user gets the same quality of information.
- Create a translation glossary for technical terms.
- Keep the same content structure across languages.
- Match the tone and style of your source content.
- Use Git branches to manage translation work separately from main content updates.
### Layout differences
Some languages require more or less space than English. Test your translated content on different screen sizes to ensure:
- Navigation fits properly.
- Code blocks don't overflow.
- Tables and other formatted text remain readable.
- Images scale appropriately.
### Character encoding
Ensure your development environment and deployment pipeline support UTF-8 encoding to properly display all characters in languages with different alphabets and special characters.
## Frequently asked questions
<AccordionGroup>
<Accordion title="Do I need to translate every page before launching a new language?">
No. You can launch a language with partial coverage and expand over time. A common approach is to translate your most-visited pages first—typically getting started content, authentication, and top how-to guides—and leave lower-traffic reference content in the default language until translations are ready. Users generally prefer some translated content over none.
</Accordion>
<Accordion title="What happens when a translated page is missing?">
If a user navigates to a translated URL that doesn't exist, they'll see a 404. To avoid this, either only include translated pages in your language-specific navigation or maintain parity between your default language and translated content. Using the same file structure across languages makes it easy to identify which translations are missing.
</Accordion>
<Accordion title="Should navigation labels be translated?">
Yes. Navigation labels—group names, tab titles, anchor text—should match the language of the content. An English "Getting started" label in a Spanish documentation section creates a jarring experience. Mintlify supports language-specific navigation structures, so each language can have fully translated labels.
</Accordion>
<Accordion title="How do I handle code examples in translated content?">
Do not translate code itself—variable names, function calls, and syntax are language-agnostic. Translate comments within code blocks if they explain concepts users need to understand. Fully translate instructions surrounding code blocks.
</Accordion>
<Accordion title="Does adding more languages affect site performance?">
Mintlify serves each language version as a separate set of static pages, so a reader visiting `/es/quickstart` only loads the Spanish content for that page—not every translation. Adding languages does not change page weight or runtime performance for end users.
Adding languages does grow your repository and your build's working set. To keep authoring and CI fast:
* Store translated images in language-specific directories so unused locales aren't pulled into other locales' content.
* In larger repos, scope `mint broken-links` and other CI checks to changed files rather than the full tree.
</Accordion>
</AccordionGroup>