Skip to main content
Technology & EngineeringI18n Services243 lines

Pontoon

"Mozilla Pontoon: open-source translation management, Fluent/FTL support, in-place editing, community localization, VCS sync"

Quick Summary11 lines
Pontoon is Mozilla's open-source translation management system designed for community-driven localization. It supports the Fluent (FTL) localization format natively alongside traditional formats like PO, XLIFF, and JSON. Pontoon's standout feature is in-place translation — translators work directly on a live preview of the application rather than in a spreadsheet-style editor. It syncs with version control systems (Git, Mercurial) to keep translations aligned with the source repository. Pontoon is self-hostable and powers the localization of Firefox, MDN, and other Mozilla projects.

## Key Points

- Use Fluent (FTL) format for new projects — it handles plurals, gender, and complex grammar far better than key-value JSON or gettext
- Structure FTL files by feature or component so translators work on focused, contextually related strings
- Enable in-place translation mode so translators see strings in context, reducing mistranslations caused by ambiguous keys
- Placing variables inside Fluent terms (prefixed with `-`) — terms cannot accept external variables, only messages can
- Forgetting that Pontoon's VCS sync overwrites local translation edits — always commit translation changes through Pontoon, not directly in the repository
skilldb get i18n-services-skills/PontoonFull skill: 243 lines
Paste into your CLAUDE.md or agent config

Pontoon

Core Philosophy

Pontoon is Mozilla's open-source translation management system designed for community-driven localization. It supports the Fluent (FTL) localization format natively alongside traditional formats like PO, XLIFF, and JSON. Pontoon's standout feature is in-place translation — translators work directly on a live preview of the application rather than in a spreadsheet-style editor. It syncs with version control systems (Git, Mercurial) to keep translations aligned with the source repository. Pontoon is self-hostable and powers the localization of Firefox, MDN, and other Mozilla projects.

Setup

Self-hosting with Docker

# Clone the Pontoon repository
git clone https://github.com/mozilla/pontoon.git
cd pontoon

# Copy environment template
cp .env.template .env

# Configure .env with required values:
# DATABASE_URL=postgres://pontoon:pontoon@db/pontoon
# SECRET_KEY=your-secret-key
# SITE_URL=https://pontoon.example.com
# ALLOWED_HOSTS=pontoon.example.com

# Start with Docker Compose
docker-compose up -d

Project configuration in Pontoon

# Pontoon syncs projects via VCS. Configure a project in the admin panel:
# 1. Admin -> Projects -> Add Project
# 2. Set repository URL (Git or Mercurial)
# 3. Define resource paths using the project's l10n structure

# Typical repository structure Pontoon expects:
# locales/
#   en/
#     messages.ftl    (source locale)
#   fr/
#     messages.ftl
#   de/
#     messages.ftl

# Or for Django/gettext projects:
# locale/
#   en/LC_MESSAGES/django.po
#   fr/LC_MESSAGES/django.po

Fluent (FTL) file format

# messages.ftl — Mozilla's Fluent localization format

welcome-title = Welcome to { -brand-name }
welcome-description =
    Discover features designed to put your
    privacy first.

# Variables
login-greeting = Hello, { $userName }!

# Plurals with select expressions
emails-count =
    { $count ->
        [one] You have { $count } new email.
       *[other] You have { $count } new emails.
    }

# Attributes for element properties
login-button =
    .label = Log in
    .accesskey = L
    .title = Click to log in to your account

# Terms (reusable, prefixed with -)
-brand-name = Firefox
-company-name = Mozilla

Core Patterns

Using Fluent in a web application

import { FluentBundle, FluentResource } from "@fluent/bundle";
import { negotiateLanguages } from "@fluent/langneg";

// Load FTL content (fetched from your locales directory)
const ftlContent = `
welcome-title = Welcome to { -brand-name }
-brand-name = MyApp
login-greeting = Hello, { $userName }!
emails-count =
    { $count ->
        [one] You have { $count } new email.
       *[other] You have { $count } new emails.
    }
`;

const bundle = new FluentBundle("en");
const resource = new FluentResource(ftlContent);
const errors = bundle.addResource(resource);

if (errors.length) {
  console.error("Fluent parse errors:", errors);
}

// Format messages
const welcome = bundle.getMessage("welcome-title");
const formatted = bundle.formatPattern(welcome.value);
// "Welcome to MyApp"

const greeting = bundle.getMessage("login-greeting");
const greetingText = bundle.formatPattern(greeting.value, {
  userName: "Alice",
});
// "Hello, Alice!"

const emails = bundle.getMessage("emails-count");
const emailText = bundle.formatPattern(emails.value, { count: 5 });
// "You have 5 new emails."

React integration with @fluent/react

import { LocalizationProvider, Localized } from "@fluent/react";
import { ReactLocalization } from "@fluent/react";
import { FluentBundle, FluentResource } from "@fluent/bundle";

function createLocalization(locale: string, ftlContent: string) {
  const bundle = new FluentBundle(locale);
  bundle.addResource(new FluentResource(ftlContent));
  return new ReactLocalization([bundle]);
}

function App() {
  const l10n = createLocalization("en", enFtl);

  return (
    <LocalizationProvider l10n={l10n}>
      <Localized id="welcome-title">
        <h1>Welcome</h1>
      </Localized>

      <Localized id="login-greeting" vars={{ userName: "Alice" }}>
        <p>Hello!</p>
      </Localized>

      <Localized
        id="emails-count"
        vars={{ count: 3 }}
      >
        <p>You have emails.</p>
      </Localized>
    </LocalizationProvider>
  );
}

Pontoon API for automation

// Pontoon exposes a GraphQL API for querying project and translation data

const PONTOON_URL = "https://pontoon.example.com";

// Fetch project translation status
const query = `
  query {
    project(slug: "my-project") {
      name
      localizations {
        locale { code, name }
        totalStrings
        approvedStrings
        missingStrings
      }
    }
  }
`;

const response = await fetch(`${PONTOON_URL}/graphql`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ query }),
});

const { data } = await response.json();

// Report completion per locale
for (const loc of data.project.localizations) {
  const pct = ((loc.approvedStrings / loc.totalStrings) * 100).toFixed(1);
  console.log(`${loc.locale.name}: ${pct}% complete`);
}

VCS sync configuration

# Pontoon syncs translations to/from your repository on a schedule.
# Configure sync in the admin panel or via environment variables:

# SYNC_INTERVAL=3600  (seconds between syncs)

# Manual sync trigger via management command:
python manage.py sync_projects --projects=my-project

# Pontoon commits translations back to the repository with:
# Author: Mozilla Pontoon <pontoon@mozilla.com>
# Commit message pattern: "Update {locale} localization of {project}"

Best Practices

  • Use Fluent (FTL) format for new projects — it handles plurals, gender, and complex grammar far better than key-value JSON or gettext
  • Structure FTL files by feature or component so translators work on focused, contextually related strings
  • Enable in-place translation mode so translators see strings in context, reducing mistranslations caused by ambiguous keys

Common Pitfalls

  • Placing variables inside Fluent terms (prefixed with -) — terms cannot accept external variables, only messages can
  • Forgetting that Pontoon's VCS sync overwrites local translation edits — always commit translation changes through Pontoon, not directly in the repository

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add i18n-services-skills

Get CLI access →