Skip to main content

Overview

Proton WebClients uses ttag for internationalization and the @proton/i18n package for translation management. Translations are managed through Crowdin and extracted from the codebase.

Translation System

ttag

JavaScript i18n library for marking translatable strings

@proton/i18n

CLI tool for extracting, validating, and managing translations

Crowdin

Translation management platform for collaborating with translators

gettext

PO file format for storing translations

proton-i18n CLI

The @proton/i18n package provides the proton-i18n command-line tool:
{
  "bin": {
    "proton-i18n": "index.js"
  }
}

Building i18n Tools

Before using i18n commands, build the i18n package:
yarn workspace @proton/i18n build
The build script runs automatically via Turbo when needed by dependent tasks.

Translation Workflow

1. Extract Translations

Extract translatable strings from source code:
yarn workspace proton-mail i18n:extract:web
This runs:
proton-i18n extract
Extraction creates or updates .po files in the po/ directory.

2. Validate Translations

Validate translation strings for common issues:
Validate that translatable strings follow best practices:
yarn workspace proton-mail i18n:validate
This runs:
proton-i18n validate lint-functions
Can also target specific directories:
proton-i18n validate lint-functions lib

3. Update Crowdin

Upload translations to Crowdin for translator collaboration:
yarn workspace proton-mail i18n:upgrade
This runs:
proton-i18n extract --verbose && \
proton-i18n crowdin -u --verbose
1

Extract

Extract latest strings from source code
2

Upload

Upload extracted strings to Crowdin with -u flag

4. Get Latest Translations

Download latest translations from Crowdin:
yarn workspace proton-mail i18n:getlatest
This runs:
proton-i18n upgrade
This downloads updated .po files from Crowdin into your po/ directory.

Common i18n Scripts

All applications follow these script conventions:
ScriptDescriptionCommand
i18n:extract:webExtract stringsproton-i18n extract
i18n:validateValidate lint functionsproton-i18n validate lint-functions
i18n:validate:context:webValidate contextproton-i18n validate
i18n:upgradeExtract & upload to Crowdinproton-i18n extract && crowdin -u
i18n:getlatestDownload from Crowdinproton-i18n upgrade

Application Examples

package.json
{
  "scripts": {
    "i18n:extract:web": "proton-i18n extract",
    "i18n:getlatest": "proton-i18n upgrade",
    "i18n:upgrade": "proton-i18n extract --verbose && proton-i18n crowdin -u --verbose",
    "i18n:validate": "proton-i18n validate lint-functions",
    "i18n:validate:context:web": "proton-i18n validate"
  }
}
package.json
{
  "scripts": {
    "i18n:extract:local": "yarn workspace @proton/i18n build && yarn build:web && proton-i18n extract",
    "i18n:extract:web": "proton-i18n extract",
    "i18n:getlatest": "proton-i18n upgrade",
    "i18n:upgrade": "proton-i18n extract --verbose && proton-i18n crowdin -u --verbose",
    "i18n:validate": "proton-i18n validate lint-functions",
    "i18n:validate:context:web": "proton-i18n validate"
  }
}
package.json
{
  "scripts": {
    "i18n:extract:web": "proton-i18n extract",
    "i18n:upgrade": "proton-i18n extract --verbose && proton-i18n crowdin --verbose",
    "i18n:validate": "proton-i18n validate lint-functions",
    "i18n:validate:context:web": "proton-i18n validate"
  }
}
package.json
{
  "scripts": {
    "i18n:extract:web": "proton-i18n extract",
    "i18n:getlatest": "proton-i18n upgrade",
    "i18n:upgrade": "proton-i18n extract --verbose && proton-i18n crowdin -u --verbose",
    "i18n:validate": "proton-i18n validate lint-functions",
    "i18n:validate:context:web": "proton-i18n validate"
  }
}
package.json
{
  "scripts": {
    "i18n:validate": "proton-i18n validate lint-functions lib"
  }
}
Packages can specify a directory (lib) for validation.

Using ttag in Code

Basic Translation

import { c } from 'ttag';

const message = c('Action').t`Delete`;

Translation with Variables

import { c } from 'ttag';

const count = 5;
const message = c('Info').t`You have ${count} unread messages`;

Plural Forms

import { c } from 'ttag';

const n = items.length;
const message = c('Info').ngettext(
  msgid`${n} item selected`,
  `${n} items selected`,
  n
);

Context

Provide context to help translators:
import { c } from 'ttag';

// Context helps distinguish "Mail" (email) from "Mail" (send)
const mailNoun = c('Navigation').t`Mail`;
const mailVerb = c('Action').t`Mail`;

Translation File Structure

PO Files

Translations are stored in .po files in the po/ directory:
applications/mail/po/
├── en.po          # English (source)
├── fr.po          # French
├── de.po          # German
├── es.po          # Spanish
└── ...

PO File Format

#: src/app/components/Header.tsx
msgctxt "Action"
msgid "Delete"
msgstr "Supprimer"

#: src/app/components/List.tsx
msgctxt "Info"
msgid "You have ${count} unread messages"
msgstr "Vous avez ${count} messages non lus"

Turbo Configuration

Translation tasks are orchestrated through Turbo:
{
  "tasks": {
    "@proton/i18n#build": {},
    "i18n:validate": {},
    "i18n:extract:web": {
      "dependsOn": ["@proton/i18n#build", "build:web"],
      "outputs": ["po/**"]
    },
    "i18n:validate:context:web": {
      "inputs": ["po/**"]
    }
  }
}
Turbo ensures i18n tools are built before extraction and caches translation outputs.

Validation Rules

Lint Functions

The validate lint-functions command checks for:
  • Missing context: All strings should have context (e.g., c('Action'))
  • Invalid placeholders: Variables must use valid syntax
  • Concatenation issues: Strings should not be concatenated
  • HTML in strings: Avoid raw HTML in translatable strings

Context Validation

The validate command checks:
  • Consistent translations: Same source string with different contexts
  • Variable consistency: Placeholders match across translations
  • Plural forms: Correct plural form syntax

Best Practices

1

Always Provide Context

Use context to help translators understand meaning:
c('Action').t`Save`  // Button label
c('Title').t`Save`   // Dialog title
2

Avoid String Concatenation

Use template literals instead:
// Bad
c('Info').t`Hello ` + userName

// Good
c('Info').t`Hello ${userName}`
3

Extract After Changes

Run extraction after adding new translatable strings:
yarn workspace <app> i18n:extract:web
4

Validate Before Committing

Always validate translations:
yarn workspace <app> i18n:validate
yarn workspace <app> i18n:validate:context:web

Crowdin Integration

Upload to Crowdin

yarn workspace proton-mail i18n:upgrade
This command:
  1. Extracts latest strings from source
  2. Uploads .po files to Crowdin
  3. Makes strings available for translation

Download from Crowdin

yarn workspace proton-mail i18n:getlatest
This command:
  1. Downloads translated .po files
  2. Updates local po/ directory
  3. Translations become available in the app
Crowdin credentials are required for upload/download operations. These are typically configured in CI/CD environments.

Troubleshooting

Ensure the application is built first:
yarn workspace proton-mail build:web
yarn workspace proton-mail i18n:extract:web
Or use the local extraction script:
yarn workspace proton-drive i18n:extract:local
Fix reported issues in source code:
  • Add missing context: c('Context').t
  • Fix placeholder syntax: Use ${variable}
  • Avoid concatenation: Use template literals
Re-run validation after fixes:
yarn workspace proton-mail i18n:validate
Download latest from Crowdin:
yarn workspace proton-mail i18n:getlatest
If still missing, strings may not be translated yet in Crowdin.
PO files can have merge conflicts. Use gettext tools to resolve:
msgcat --use-first file1.po file2.po -o merged.po
Or download fresh from Crowdin:
yarn workspace proton-mail i18n:getlatest

Additional Resources

ttag Documentation

Official ttag library documentation

Crowdin

Translation management platform

GNU gettext

PO file format specification

Translation Community

Proton’s translation community blog post
For contributing translations, visit the Proton translation community page to learn how you can help translate Proton products into your language.

Build docs developers (and LLMs) love