🚨 This documentation is out of date.
View our New Documentation!
Open Collective Docs
Go back to Open Collective
  • Welcome
  • About
    • Introduction
    • Documentation
    • Terminology
    • Company
    • Pricing
    • Team
    • The Open Collective Way
      • Mission and Values
      • Community Guidelines
    • Hiring
    • Investors
    • Contributing
    • Refund Policy
    • Contact Us
  • Product
    • Features
    • Roadmap
    • Comparison
    • User Profile
    • Dashboard
      • Preview Features
    • Currencies
    • Log-in System
    • Privacy Policy
    • Moderation
    • Security
    • Ledger
      • Individual Transactions
      • Transaction Pairs, Groups & Perspectives
      • Viewing Transactions
      • Exporting Transactions
      • Fiscal Host Ledger Perspective
      • Contributions in the Ledger
      • Added Funds in the Ledger
      • Expenses in the Ledger
      • Ledger Changelog
    • Notifications
    • Two-factor Authentication
    • Activity Log
  • Collectives
    • Collectives FAQ
    • Creating a Collective
    • Quick Start Guide
    • Collective Settings
      • Customize Collective
      • Team
      • Collective Goals & Tiers
      • Expense Policy
      • Data Export
      • Security
      • Integrations
      • Zero Collective Balance
      • Closing a Collective
    • Add Fiscal Host
    • Change Fiscal Host
    • Open Source Collectives
      • Setting up GitHub Sponsors
    • Transparent Budget
    • Expenses
    • Updates & Comms
    • Custom Email
    • Moderation
    • Conversations
    • Events
    • Projects
    • Funding Options
    • Buttons & Banners
    • Connected Collectives
    • Contribution flow
  • Financial Contributors
    • Financial Contributors FAQ
    • Guest contributions
    • Payments
    • Platform Tips
    • Website Badge
    • Receipts
    • Collective to Collective
    • Organizations
      • Organization FAQ
      • Funds
      • Bulk Transfers
      • Gift Cards
      • Sustainer Resources
  • Expenses & Getting Paid
    • Expenses FAQ
    • Submitting Expenses
      • Inviting a third-party to submit an Expense
    • Expense Comments
    • Edit or Download an Expense
    • Receiving payment through Payoneer or Wise
    • Tax Information
  • Fiscal Hosts
    • Fiscal Hosts FAQ
    • Becoming a Fiscal Host
    • Creating a Fiscal Host
    • Organisation Settings
      • Info
      • Customize Profile Page
      • Connect external accounts
      • Accounting Categories
      • Security
      • Manage updates
      • Policies
    • Fiscal Host Dashboard
      • Expenses
      • Financial contributions
      • Pending applications
      • Hosted Collectives
      • Vendors
      • Transaction Report
        • Reports
    • Receiving Money
      • Bank Transfers
      • Credit Card
      • Add Funds Manually
      • Expected Funds
    • Payouts
      • Virtual Card Settings
      • Payouts with PayPal
      • Payouts with Wise
      • Two-factor authentication for payouts
      • Refunds
    • Host Fees
    • Local Tax Support
    • Agreement Templates
  • Independent Collectives
    • About Independent Collectives
    • Create an Independent Collective
      • Migrate from Self-Hosted to Independent Collective
      • Migrate from a Fiscal Host to Independent Collective
    • Independent Collective Setup
    • Independent Collective Management
      • Money coming in: Contributions
      • Money going out: Expenses
    • Close an Independent Collective
  • Contributing
    • Design
      • Design Workflow
      • Design Contribution Guidelines
    • Development
      • Contribution Guide
      • Best Practice Guidelines
      • Bounties
      • Architecture
      • API
        • Members
        • Collectives
        • Events
      • README Integration
      • PayPal
      • TransferWise
      • Post-Donation Redirect
      • Manual Reporting
      • GitHub Permissions
      • Internationalization (i18n) system
      • Testing with Cypress
      • Testing features
        • Conversations
      • Collective's locations
      • Virtual Cards
    • Documentation
      • Resources for documentarians
      • Style guide
      • Suggesting changes
    • Translation
  • Developers
    • OAuth
    • Personal Tokens
Powered by GitBook

Helpful Links

  • Website
  • Blog
  • Contact
  • Slack

â’¸ Open Collective 2024

On this page
  • Good practices
  • Use "select" when a value has a limited number of options
  • Don't assume word's order stays the same in other languages
  • Don't split strings
  • Use I18nFormatters to format rich text (bold, italic...etc)
  • Provide an ID when the translation depends on the context
  • Translate links inline
  • FormattedMessage
  • Add a new language

Was this helpful?

Edit on GitHub
Export as PDF
  1. Contributing
  2. Development

Internationalization (i18n) system

Documenting how we handle translations in the code

PreviousGitHub PermissionsNextTesting with Cypress

Last updated 2 years ago

Was this helpful?

We use to manage our translations. They are extracted from the code to src/lang/${locale}.json files using the npm run build:langs command (CI will notify you if the translation files are outdated). Don't translate the strings directly in the files, we use to manage our translatations.

Good practices

Use "select" when a value has a limited number of options

Example

<FormattedMessage
    defaultMessage="{action, select, delete {Delete this} archive {Archive this} other {Do something with this}}"
    values={{ action: 'delete' }}
/>

// => "Delete this"

<FormattedMessage
    defaultMessage="{action, select, delete {Delete this} archive {Archive this} other {Do something with this}}"
    values={{ action: 'eat' }}
/>

// => "Do something with this"
  • defaultMessage string breakdown:

    • action variable name

    • select keyword

    • delete and archive possible values

    • other all other values will use this key

An exception to this rule: very common enums or the ones with many possible values should be implemented as a separate file listing all values because:

  • Re-usability

  • A map of translations is easier to read than a long select string with tons of options

Don't assume word's order stays the same in other languages

The order of the words may change from a language to another. For this reason we must always pass the values to be replaced in values so their order can later be changed.

Example

// Bad
<div>
    <FormattedMessage id="str" defaultMessage="Pending approval from " />
    <Link route={`/${host.slug}`}>{host.name} </Link>
</div>

// Good
<div>
    <FormattedMessage  
        defaultMessage="Pending approval from {host}" 
        values ={{ 
          host: <Link route={`/${host.slug}`}>{host.name} </Link>
        }}
    />
</div>

Don't split strings

Splitting a string is alway problematic, because translators loose the context: the strings may not be next to each others when they'll be translated.

// Bad
<FormattedMessage
  defaultMessage="Do you want to {createSomething} in this list?"
  values={{
    createSomething: (
      <blink>
        <FormattedMessage defaultMessage="create something"/>
      </blink>
    )
  }}
/>

// Good
<FormattedMessage
  defaultMessage="Do you want to <blink>create something</blink> in this list?"
  values={{
    createSomething: function BlinkComponent(msg) {
      return (<blink>{msg}</blink>)
    }
  }}
/>

Use I18nFormatters to format rich text (bold, italic...etc)

import I18nFormatters from '../../I18nFormatters';

<FormattedMessage 
  id="_" 
  defaultMessage="This <strong>is</strong> <i>easier</i>." 
  values={I18nFormatters}
/>

Provide an ID when the translation depends on the context

In many Latin languages, the translation for a string like "Created at" will depend on the context because of the feminine/masculine forms. The best way to provide this context is to set an ID on the string:

<FormattedMessage
  id="expense.createdAt"
  defaultMessage"Created at"
/>

Translate links inline

In some parts of the code we translate links like this:

// Please don't do that!
<FormattedMessage
  defaultMessage="Please check our {documentationLink} to learn more!"
  values={{
    documentationLink: (
      <a href="https://docs.opencollective.com">
        <FormattedMessage id="documentation" defaultMessage="documentation" />
      </a>
    ),
  }}
/>

This is bad because we're creating two strings and translators loose the context when they translate one. You should do this instead:

import { getI18nLink } from './I18nFormatters';

// External link
<FormattedMessage
  defaultMessage="Please check our <link>documentation</link> to learn more!"
  values={{
    link: getI18nLink({ href: 'https://docs.opencollective.com' }),
  }}
/>

// Internal link
import Link from './Link';

<FormattedMessage
  defaultMessage="Please check <link>hosts page</link> to learn more!"
  values={{
    link: getI18nLink({ as: Link, route: 'hosts' }),
  }}
/>

FormattedMessage

The FormattedMessage component is the main way to translate strings. To use it, you just need to add the following import:

import { FormattedMessage } from 'react-intl'

Then you just add the component with an unique id and a defaultMessage.

For VSCode users, you can use the following snippet to make your life easier:

{
  "Formatted Message (react-intl)": {
    "scope": "javascript",
    "prefix": "formatted-message",
    "body": "<FormattedMessage id=\"$TM_FILENAME_BASE.$0\" defaultMessage=\"$1\"/>",
    "description": "Put the given string in a FormattedMessage"
  }
}

Add a new language

Add the language on Crowdin

Activate the language in the code

See as an example.

Just go to , click on Target languages pick the language and click Update.

To activate a language on the website, we usually wait to have a correct translated ratio (20-30%). Then activate it by adding a new line in .

react-intl
Crowdin
i18n-member-role
https://crowdin.com/project/opencollective/settings#translations
https://github.com/opencollective/opencollective-frontend/blob/main/lib/constants/locales.js