Skip to content
Documentation Localization: i18n Tools, Translation Workflows, and Locale Adaptation

Documentation Localization: i18n Tools, Translation Workflows, and Locale Adaptation

DodaTech Updated Jun 20, 2026 9 min read

Documentation localization is the process of adapting documentation for different languages and regions — going beyond translation to account for cultural context, formatting conventions, legal requirements, and technical infrastructure.

What You’ll Learn

  • Internationalization (i18n) vs localization (l10n): what’s the difference
  • Setting up Hugo/Docusaurus for multilingual documentation
  • Translation management tools: Crowdin, Transifex, Lokalise
  • Translation workflows: human translation, machine translation, hybrid
  • Locale adaptation: dates, currencies, images, and cultural context
  • Quality assurance for translated documentation

Why Localization Matters

Documentation in a user’s native language dramatically improves comprehension, adoption, and satisfaction. 72% of users spend most of their time on sites in their own language, and 55% only buy from websites with information in their language. For global products like Doda Browser and Durga Antivirus Pro, localized documentation is essential for reaching international markets — a single untranslated error message can break trust with users in any market.

Learning Path

    flowchart LR
  A[Content Strategy] --> B[Localization Guide<br/>You are here]
  B --> C[Translation Workflows]
  C --> D[Locale Adaptation]
  D --> E[i18n Automation]
  style B fill:#f90,color:#fff
  

i18n vs l10n

Internationalization (i18n)

The technical infrastructure that enables localization — making your site capable of supporting multiple languages:

  • Content separation from templates
  • Language detection and routing
  • Right-to-left (RTL) support
  • Unicode/UTF-8 encoding
  • Locale-aware formatting

Localization (l10n)

The actual adaptation of content for specific locales:

Locale Code Structure: language_REGION

en-US — English (United States)
en-GB — English (United Kingdom)
fr-FR — French (France)
fr-CA — French (Canada)
ja-JP — Japanese (Japan)
zh-CN — Chinese (Simplified, China)

Setting Up Multilingual Hugo

# hugo.yaml
defaultContentLanguage: en
languages:
  en:
    languageName: English
    weight: 1
    contentDir: content/en
    params:
      description: "Official documentation"
  ja:
    languageName: 日本語
    weight: 2
    contentDir: content/ja
    params:
      description: "公式ドキュメント"
  fr:
    languageName: Français
    weight: 3
    contentDir: content/fr
    params:
      description: "Documentation officielle"

Content Directory Structure

content/
├── en/
│   ├── getting-started/
│   │   ├── _index.md
│   │   ├── installation.md
│   │   └── quickstart.md
│   └── api-reference/
│       └── _index.md
├── ja/
│   ├── getting-started/
│   │   ├── _index.md
│   │   └── installation.md
│   └── api-reference/
│       └── _index.md
└── fr/
    └── getting-started/
        └── _index.md

Language Switcher

<!-- partials/lang-switcher.html -->
{{ if .Site.IsMultiLingual }}
<nav class="lang-switcher">
  {{ range .Site.Languages }}
    <a href="{{ . | relLangURL }}" lang="{{ .Lang }}">
      {{ .LanguageName }}
    </a>
  {{ end }}
</nav>
{{ end }}

Translation Management Tools

ToolBest ForPricingFeatures
CrowdinOpen source, large projectsFree for open sourceGit integration, TM, glossary
TransifexEnterprisePaidAI translation, CMS integration
LokaliseSmall-medium teamsPaidScreenshots, comments, QA
POEditorSimple translationFree tierPO file editing, API
WeblateSelf-hostedFree / paidGit integration, consistent UI

Crowdin Workflow

# crowdin.yml
project_id: "123456"
api_token: "your_token"
base_path: "content/en"

files:
  - source: "/**/*.md"
    translation: "/%two_letters_code%/**/%original_file_name%"
    update_option: "update_as_unapproved"
    languages_mapping:
      two_letters_code:
        zh-CN: zh-CN
        pt-BR: pt-BR

Translation Workflows

Human Translation

Writer (English) → Review → Merge → Push to Crowdin
                                       ↓
                              Translators translate
                                       ↓
                              In-context review
                                       ↓
                              Proofreading
                                       ↓
                              Pull to repository
                                       ↓
                              CI build & deploy

Machine Translation + Human Review

# Auto-translate with DeepL API, then human review
curl -X POST 'https://api.deepl.com/v2/translate' \
  --data 'auth_key=your_key' \
  --data 'text=Hello world' \
  --data 'target_lang=JA'

# Output: {"translations":[{"text":"こんにちは世界"}]}

Translation Memory

Translation memory (TM) stores previously translated segments. When the same or similar text appears, it suggests the existing translation:

Source (English): "Click Save to continue"
TM Match: 100% → "クリック保存して続行"

Source (English): "Click Save to confirm"
TM Match: 80% → "クリック保存して確認" (needs review)

Locale Adaptation Beyond Translation

Date and Time Formats

LocaleDate FormatExample
en-USM/D/YYYY3/15/2026
en-GBD/M/YYYY15/3/2026
ja-JPYYYY年M月D日2026年3月15日
de-DED.M.YYYY15.3.2026

Number and Currency

# en-US
"1,234.56" and "$1,234.56"

# de-DE
"1.234,56" and "1.234,56 €"

# ja-JP
"1,234.56" and "¥1,235"

Cultural Adaptation

  • Images and screenshots: Replace images with culturally appropriate ones (different skin tones, clothing, settings)
  • Examples: Use locally relevant names and contexts (local cities, common names, familiar scenarios)
  • Color meanings: Red means “danger” in Western cultures but “good luck” in Chinese culture
  • Legal/compliance: Privacy policies, cookie notices, and terms must be adapted for local laws (GDPR for EU, CCPA for California)

Technical Adaptation

# Locale-aware date formatting
from datetime import datetime
import locale

def format_date_for_locale(date, locale_code):
    # Set locale
    locale.setlocale(locale.LC_TIME, locale_code)

    # Format with locale-aware pattern
    formats = {
        'en_US': '%B %d, %Y',
        'ja_JP': '%Y年%m月%d日',
        'de_DE': '%d. %B %Y',
    }

    pattern = formats.get(locale_code, '%Y-%m-%d')
    return date.strftime(pattern)

today = datetime(2026, 3, 15)
print(format_date_for_locale(today, 'en_US'))  # March 15, 2026
print(format_date_for_locale(today, 'ja_JP'))  # 2026年03月15日
print(format_date_for_locale(today, 'de_DE'))  # 15. März 2026

Quality Assurance

Translation QA Checklist

## Translation QA Checklist

### Accuracy
- [ ] Technical terms translated correctly
- [ ] Code examples not translated (code stays in English)
- [ ] Numbers, units, and measurements converted correctly
- [ ] Links and cross-references point to correct locale

### Completeness
- [ ] All source content is translated
- [ ] No untranslated strings remaining
- [ ] Images are localized or replaced
- [ ] Downloadable files are in the correct language

### Formatting
- [ ] Markdown formatting preserved
- [ ] Code blocks correctly formatted
- [ ] Tables maintain structure
- [ ] No broken layouts

### Cultural
- [ ] Examples are culturally appropriate
- [ ] No offensive or inappropriate content
- [ ] Locale-appropriate date/number formats
- [ ] Legal content adapted for local laws

Automated QA

# Check for untranslated strings
grep -r '{{< translate >}}' content/ja/ | wc -l
# Counts how many strings still need translation

# Check for broken links in translated content
find content/ja -name "*.md" -exec markdown-link-check {} \;

# Verify all source files have translation files
for f in content/en/**/*.md; do
  ja_file="${f/content\/en/content\/ja}"
  if [ ! -f "$ja_file" ]; then
    echo "Missing translation: $ja_file"
  fi
done

CI/CD for Localization

# .github/workflows/localization.yml
name: Localization
on:
  push:
    branches: [main]
    paths: ['content/en/**']

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Push to Crowdin
        uses: crowdin/github-action@v1
        with:
          upload_sources: true
          upload_translations: false
          download_translations: true
          crowdin_branch_name: main
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
          CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

  build-all:
    needs: sync
    strategy:
      matrix:
        lang: [en, ja, fr, de, zh-CN]
    steps:
      - uses: actions/checkout@v4
      - name: Build ${{ matrix.lang }} site
        run: |
          hugo --config hugo.${{ matrix.lang }}.yaml \
               --destination public/${{ matrix.lang }}

Content Management for Localization

Source Content Guidelines

Write source (English) content with localization in mind:

  • Use simple sentences: “Click Save” not “Upon clicking the Save button…”
  • Avoid idioms: “Piece of cake” becomes “it’s easy to do”
  • Be consistent: Use the same term for the same concept everywhere
  • Leave placeholders: {username} not “the user’s name”
  • Avoid culture-specific references: Sports analogies, local celebrities

Managing Translated Content

# translation_coverage.py
import os

class TranslationTracker:
    def __init__(self, content_dir):
        self.source_dir = f"{content_dir}/en"
        self.locales = ["ja", "fr", "de", "zh-CN"]

    def calculate_coverage(self):
        source_files = self._get_files(self.source_dir)
        total = len(source_files)
        coverage = {}

        for locale in self.locales:
            locale_dir = f"{content_dir}/{locale}"
            translated = 0
            for sf in source_files:
                lf = sf.replace("/en/", f"/{locale}/")
                if os.path.exists(lf):
                    translated += 1
            coverage[locale] = (translated / total) * 100

        print(f"Source files: {total}")
        for locale, pct in coverage.items():
            print(f"{locale}: {pct:.0f}% translated")

tracker = TranslationTracker('content/')
tracker.calculate_coverage()

Common Localization Mistakes

1. Translating Code

Code examples, variable names, and CLI commands should NOT be translated.

Fix: Keep code examples in English. Only translate surrounding explanations.

2. Ignoring Text Expansion

German text is ~30% longer than English. Japanese is ~10% shorter.

Fix: Design layouts with flexible containers. Test with each language.

3. No Context for Translators

Translating “Run” without knowing if it’s a button label or a verb creates errors.

Fix: Provide screenshots and context notes in the translation tool.

4. Inconsistent Terminology

A button labeled “Save” in one place and “Save Changes” in another confuses users.

Fix: Create a glossary of key terms. Require translators to use it.

5. Only Translating the Main Content

Navigation, error messages, alt text, and metadata are also user-facing.

Fix: Include all user-visible strings in the translation scope.

6. Not Updating Translations

When the English source changes, translations become outdated.

Fix: Automatically flag changed source content for retranslation.

7. Assuming All Locales Work Like English

Right-to-left (Arabic, Hebrew), character-based (Chinese, Japanese), and non-Latin scripts need special handling.

Fix: Test with each locale’s content. Verify layout, fonts, and input methods.

Practice Questions

1. What is the difference between i18n and l10n?

Internationalization (i18n) is the technical infrastructure for supporting multiple languages. Localization (l10n) is the actual translation and adaptation of content for specific locales.

2. What is translation memory and how does it help?

A database of previously translated segments. It suggests existing translations when the same or similar text appears, improving consistency and reducing translation cost.

3. Why should code examples not be translated?

Programming languages, commands, and variable names are language-agnostic. Translating them produces non-working code that misleads users.

4. What is text expansion and why does it matter for localization?

Translated text is often longer or shorter than the source. German expands ~30%; Japanese contracts ~10%. Layouts must accommodate these differences.

5. How do you handle RTL (right-to-left) languages in documentation?

Use CSS direction and text-align properties that adapt to the language attribute. Test layouts with Arabic and Hebrew content.

Challenge: Set up a multilingual Hugo site with English and Japanese. Translate 3 pages into Japanese using Crowdin or a manual workflow. Verify the translated site builds correctly with no missing content.

FAQ

How much does localization cost?
Professional translation costs $0.10-$0.30 per word. Machine translation + human review is cheaper: $0.02-$0.10 per word. Open source projects often use volunteer translators through Crowdin.
Which languages should I localize first?
Start with the languages spoken by your largest user base (check your analytics). Common first targets: Japanese, German, French, Spanish, and Portuguese.
How often should I update translations?
When the source content changes significantly, flag it for retranslation. Set up automated workflows that trigger translation requests on content changes.
Can I use machine translation for documentation?
Machine translation (DeepL, Google Translate) is useful for first-pass translation, but always require human review for technical documentation. Errors in translated technical content cause confusion and support tickets.
What is the best approach for screenshots with text?
Use screenshots with English text and provide instructions for translators to re-record or overlay translated text in the image.

What’s Next

TutorialWhat You’ll Learn
Internationalization Tools GuideDeep dive into i18n tools and frameworks
Content Strategy for DocsPlanning content for global audiences
SEO for Technical WritingInternational SEO and hreflang tags

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro