Set up multi-language documentation to reach global audiences.
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.
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. Place translated files in these directories with the same structure as your 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:
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:
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.
Export source content: Extract MDX files that need translation.
Send to translators: Provide files to your translation provider.
Receive translations: Get translated MDX files back.
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.
.github/workflows/export-for-translation.yml
Report incorrect code
Copy
Ask AI
name: Export content for translationon: push: branches: [main] paths: - '*.mdx' - '!es/**' - '!fr/**' - '!zh/**'# Prevent concurrent workflow runs to avoid race conditionsconcurrency: group: translation-export-${{ github.ref }} cancel-in-progress: falsejobs: 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.
.github/workflows/import-translations.yml
Report incorrect code
Copy
Ask AI
name: Import translationson: pull_request: paths: - 'es/**' - 'fr/**' - 'zh/**'# Define explicit permissionspermissions: contents: read pull-requests: writejobs: 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.
Ensure your development environment and deployment pipeline support UTF-8 encoding to properly display all characters in languages with different alphabets and special characters.