Skip to main content

Overview

The @proton/i18n package is a command-line tool for extracting and validating translations from Proton application codebases. It works with the ttag library to manage internationalization (i18n) in JavaScript and TypeScript projects.

Installation

npm install -D @proton/i18n

Key Features

  • Extract translatable strings from source code
  • Validate translation files
  • Lint translation function usage
  • Support for ttag library
  • POT/PO file generation and validation
  • TypeScript and JavaScript support

CLI Usage

The package provides a proton-i18n command-line tool.

Commands

Extract translatable strings from source code
proton-i18n extract [options]

Extracting Translations

Extract translatable strings from your codebase to create POT files.
# Extract from default locations
proton-i18n extract

# Extract with custom configuration
proton-i18n extract --config ./i18n.config.js

Configuration

Create an i18n.config.js file:
module.exports = {
  // Source files to extract from
  source: './src',
  
  // Output POT file
  output: './locales/template.pot',
  
  // File patterns to include
  include: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
  
  // File patterns to exclude
  exclude: ['**/*.test.ts', '**/*.spec.ts', 'node_modules/**'],
};

Validating Translations

Validate translation files and usage patterns.
# Validate all translations
proton-i18n validate

# Validate specific directory
proton-i18n validate lint-functions ./src

# Verbose output
proton-i18n validate --verbose

Validation Commands

Validates that translation functions (c, t, jt, etc.) are used correctly
proton-i18n validate lint-functions ./src
Checks for:
  • Correct function usage
  • String literals (no template literals)
  • Proper context usage
  • Missing translations
Validates PO/POT file structure and syntax
proton-i18n validate check-files ./locales
Checks for:
  • Valid PO file format
  • Consistent msgid/msgstr pairs
  • Proper headers
  • Encoding issues

Usage in Applications

With ttag Library

The i18n tool works with the ttag library for runtime translations.
import { c, t, jt, msgid, ngettext } from 'ttag';

// Simple translation
const translated = t`Hello, world!`;

// Translation with context
const withContext = c('Button label').t`Save`;

// Translation with variable
const userName = 'Alice';
const greeting = t`Hello, ${userName}!`;

// Plural translations
const count = 5;
const message = ngettext(
  msgid`You have ${count} message`,
  `You have ${count} messages`,
  count
);

// JSX translation with components
const jsxMessage = jt`Click ${
  <a href="/settings">here</a>
} to update settings`;

Setting Up in a Project

// app/i18n.ts
import { addLocale, useLocale } from 'ttag';

export async function loadLocale(locale: string) {
  if (locale === 'en') {
    // English is the default
    return;
  }
  
  try {
    const translations = await import(
      `../locales/${locale}.json`
    );
    addLocale(locale, translations);
    useLocale(locale);
  } catch (error) {
    console.error(`Failed to load locale: ${locale}`, error);
  }
}
// app/App.tsx
import { useEffect } from 'react';
import { loadLocale } from './i18n';

function App() {
  useEffect(() => {
    const userLocale = navigator.language.split('-')[0];
    loadLocale(userLocale);
  }, []);
  
  return <div>{/* Your app */}</div>;
}

Translation Workflow

1

Extract strings

Run extraction to create POT template file
proton-i18n extract
2

Create PO files

Create language-specific PO files from template
msginit -i locales/template.pot -o locales/fr.po -l fr
3

Translate

Use a PO editor (Poedit, Lokalize) or translation platform to translate strings
4

Validate

Validate translations before committing
proton-i18n validate
5

Compile

Convert PO files to JSON for runtime use
ttag po2json locales/fr.po > locales/fr.json

Best Practices

Writing Translatable Strings

Always use string literals, never template literals or concatenated strings
// ✅ Good - String literal
const msg = t`Welcome to Proton`;

// ❌ Bad - Template literal
const msg = t`Welcome to ${appName}`; // Won't be extracted

// ✅ Good - Variable interpolation
const msg = t`Welcome to ${appName}`; // If appName is in scope

// ✅ Good - With context
const msg = c('Navigation').t`Home`;

Providing Context

Use context to disambiguate identical strings with different meanings:
// "Close" as verb
const closeAction = c('Action').t`Close`;

// "Close" as adjective (near)
const closeDistance = c('Distance').t`Close`;

Pluralization

Handle plural forms correctly:
import { ngettext, msgid } from 'ttag';

function ItemCount({ count }: { count: number }) {
  const text = ngettext(
    msgid`${count} item`,
    `${count} items`,
    count
  );
  
  return <span>{text}</span>;
}

Variables in Translations

// ✅ Simple variable
const msg = t`Hello, ${userName}`;

// ✅ Multiple variables
const msg = t`${userName} sent you ${count} messages`;

// ✅ With formatting
const msg = t`Last login: ${formatDate(date)}`;

Package Scripts

Add these scripts to your package.json:
{
  "scripts": {
    "i18n:extract": "proton-i18n extract",
    "i18n:validate": "proton-i18n validate lint-functions",
    "i18n:validate:files": "proton-i18n validate check-files ./locales",
    "i18n:compile": "ttag po2json locales/*.po"
  }
}

Directory Structure

Recommended project structure:
project/
├── src/
│   ├── app/
│   │   └── i18n.ts          # i18n setup
│   └── components/
│       └── *.tsx            # Components with translations
├── locales/
│   ├── template.pot         # Extraction template
│   ├── en.po                # English translations
│   ├── fr.po                # French translations
│   ├── de.po                # German translations
│   └── *.json               # Compiled translations
└── i18n.config.js           # i18n configuration

Linting Rules

The validator checks for common mistakes:
// ❌ Bad
const msg = t`Hello ${name}`; // If name is dynamic

// ✅ Good
const msg = t`Hello ${userName}`; // If userName is in scope at extraction
// ❌ Bad
const msg = t`Hello ` + name;

// ✅ Good
const msg = t`Hello ${name}`;
// ❌ Bad - Ambiguous
const label = t`Close`;

// ✅ Good - Clear context
const label = c('Button').t`Close`;
// ❌ Bad
const msg = count === 1 ? t`1 item` : t`${count} items`;

// ✅ Good
const msg = ngettext(msgid`${count} item`, `${count} items`, count);

CI Integration

Add validation to your CI pipeline:
# .github/workflows/i18n.yml
name: i18n

on: [push, pull_request]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 22
      - run: yarn install
      - run: yarn i18n:validate
      - run: yarn i18n:validate:files

Verbose Output

Get detailed information during validation:
proton-i18n validate --verbose
This will show:
  • Files being processed
  • Specific validation errors
  • Line numbers and context
  • Suggestions for fixes

Dependencies

  • ttag-cli - ttag CLI tools
  • gettext-parser - PO/POT file parsing
  • glob - File pattern matching
  • execa - Process execution

Node.js Requirements

Requires Node.js >= 22.14.0
# Check your Node version
node --version

# Should output v22.14.0 or higher

Testing

# Run test suite
yarn test

# Run tests in CI mode
yarn test:ci

ttag Documentation

Official ttag library documentation

gettext Manual

GNU gettext documentation

Common Issues

  • Check that files match include patterns
  • Verify you’re using template literals: t`text`
  • Make sure strings are not in excluded directories
  • Use --verbose flag to see detailed errors
  • Check for template literals in translation calls
  • Verify PO file format is correct
  • Ensure PO files are compiled to JSON
  • Check locale is loaded before use
  • Verify JSON files are imported correctly

ttag

Runtime translation library

@proton/shared

Shared utilities including i18n helpers

Build docs developers (and LLMs) love