@oneflowab/pomes

A simple and powerful package to translate your react applications.

Usage no npm install needed!

<script type="module">
  import oneflowabPomes from 'https://cdn.skypack.dev/@oneflowab/pomes';
</script>

README

🍎 Pomes

pomes is a simple yet powerful package to translate your react applications using react-redux.

Build Status codecov npm version downloads

Installation

npm i @oneflowab/pomes --save

or

yarn add @oneflowab/pomes --save

Features

  • Translate literals.
  • Pluralize literals.
  • Designed for react-redux.
  • Compatible with Immutable.js.
  • Export translations to POT files (make your translations with Poedit).
  • Import translations from .PO files to translations.js object (for use in your project).
  • Add comments for translators.

Requirements

  • node >= 6.0.0

Overview

pomes offers your app the t() function to translate literals.

The t() function is available in the components of your app via React context. To achieve this you need to wrap your app into the <I18n /> component from pomes that provides for the context. Furthermore, for all components that want to use the t() function you need to define contextTypes, e.g.:

// import ...
import PropTypes from 'prop-types'

class MyComponent extends React.Component {
  render() {
    return <div>{this.context.t("Hello World!")}</div>
  }
}

MyComponent.contextTypes = {
  t: PropTypes.func
}

If contextTypes is not defined, then context will be an empty object.

The t() function takes up to three arguments t(textKey [, params, comments]), where textKey is either the string to be translated or --- for pluralization --- an object as defined below.

For setting the language in the redux store pomes offers an action creator setLanguage.

To manage the translations in your React app, pomes supports two choices:

  1. load all your translations into a one big JS object
  2. load your translations into a slice of your redux store

For the latter pomes provides an action function creator setTranslations. As setTranslations is an action function creator you need to add redux-thunk to your middleware for it to work.

pomes supports your store in plain JavaScript structures, but also if it is managed by help of immutable.js.

Finally, pomes offers scripts to generate a translations object from po files that can be managed in Poedit.

Usage

The package provides a parent component to encapsulate your application as well as helpers functions to translate your project.

// import ...
import I18n from "pomes"
// with Immutable.js:
import I18n from "pomes/immutable"

import {translations} from "./translations"

class Root extends React.Component {
  render() {
    return (
      <Provider store={store}>
        <I18n translations={translations}>
          <App />
        </I18n>
      </Provider>
    )
  }
}

Where translations is a dictionary similar to this:

export const translations = {
  "es": {
    "Translate this text": "Traduce este texto",
    "Hello {n}!": "Hola {n}!",
  }
}

You can also set the initial language with the initialLang prop:

<I18n translations={translations} initialLang="es">
  <div>
    <h1>My Project</h1>
    {this.props.children}
  </div>
</I18n>

If you have partial translations, this means that you don't have your translations at 100% and you want to show untranslated literals in an other language, you can use the fallbackLang prop.

<I18n translations={translations} initialLang="de" fallbackLang="en">
  <div>
    <h1>My Project</h1>
    {this.props.children}
  </div>
</I18n>

In this case, if you want to show this translations:

<div>{this.context.t('_hello_')}</div>

And this isn't in "de" language, it will show in "en".

Redux Reducer

The language state is managed in a slice of the store named i18nState. Therefore, you have to add the i18nState reducer in your combineReducers.

import {otherreducers} from "./Yourproject"

import {i18nState} from "pomes"
// with Immutable.js:
import {i18nState} from "pomes/immutable"

const appReducer = combineReducers({
  otherreducers,
  i18nState
})

The current language is contained in the lang key of i18nState.

The i18nState is initially defined as

const defaultI18nState = {
  lang: 'en',
  translations: {},
  forceRefresh: false
}

// immutablejs
const defaultI18nState = new Map({
  lang: 'en',
  translations: {},
  forceRefresh: false
})

When you map your state to props with connect you can also access the lang attribute in your components:

export default connect(state => ({
  lang: state.i18nState.lang,
}))(Home)

// with Immutable.js:
export default connect(state => ({
  lang: state.getIn(['i18nState', 'lang']),
}))(Home)

Translate literals

You can access the functions of <I18n /> using your component's context. For example:

Home.contextTypes = {
  t: PropTypes.func.isRequired
}

...you will then be able to use the t() method in your component.

render() {
  return (
    <div>
      <strong>Your current language, is: {this.props.lang}</strong><br/>
      {this.context.t("Translate this text")}<br/>
      {this.context.t("Hello {n}!", {n: "World"})}<br/><br/>
      <button onClick={this.changeLanguage.bind(this)}>Change Language</button>
    </div>
  )
}

You can also use the t() function to change date formats

export const translations = {
  "de": {
    "YYYY-MM-DD": "DD.MM.YYYY"
  }
}
render() {
  let today = moment()
  return (
    <div>
      {today.format(this.context.t("YYYY-MM-DD"))}
    </div>
  )
}

Add comments for translators.

render() {
  return (
    <div>
      {this.context.t("Translate this text", {},
                      "This is a comment for translators.")}
      {this.context.t("Hello {n}!", {n: "Cesc"},
                      "This is another comment.")}
    </div>
  )
}

Here's how Poedit will show the comments:

Poedit screenshot

HTML Object as parameter

const user = {name: 'World'}
const name = <span>{user.name}</span>
return <div dangerouslySetInnerHTML={{ __html: context.t('Hello {name}', {name: name}) }}/>

Result:

Hello <span>Cesc</span>

Notice that for security reasons we can't print html code directly, which is why we need to use the "dangerouslySetInnerHTML" method for that.

Stateless components

Example:

const Foo = ({}, context) => <h1>{context.t("Hello World")}</h1>

Pluralize

To use plurals in your translations.

<div>{this.context.t(['una noche', '{n} noches', 'n'], {n: 1})}</div>

Pass an array instead of a string as first parameter. The first element is a singular term, the second is the plural form and the last one is an object used to set the quantity.

After extracting the translations to a POT file and opening it with Poedit you will see the following:

Poedit screenshot

Also the translations object allows to set an options node. There you can set a plurals form rule and a plurals number. For example:

export const translations = {
  "es": {
    "Translate this text": "Traduce este texto",
    "Hello {n}!": "Hola {n}!",
  },
  "options": {
    "plural_rule": "n > 1",
    "plural_number": "2",
  }
}

When the translations are generated from po import file, this node is created automatically.

Note: Versions >=1.5.10 allow to use all existing pluralization rules: http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html

Change language

Use the setLanguage action.

import {setLanguage} from "pomes"

componentWillMount() {
  this.props.dispatch(setLanguage("es"))
}

If you work with combined languages like "es-ES", "en-GB", but your translations object doesn't include those properties...

export const translations = {
  "es": {
    ...
  },
  "en": {
    ...
  }
}

...pomes will fallback on the closest property. In this case, "es" or "en".

Extract/Import scripts

pomes includes a script to extract your translation strings to a .pot template which you can use in Poedit, and another to import strings from po files to a translation.js.

Add the scripts in your package.json for this purpose:

"scripts": {
  "extract": "i18n_extract",
  "import": "i18n_import"
}

You can then run the following commands in your terminal:

npm run extract
npm run import

Extract texts and build template.pot

npm run extract

By default, this script will search for all literals inside your src folder with a regular expression and build a locales/template.pot file. This file can then be used in Poedit to build en.po, es.po, etc. files.

If you want to set other source folder, you can use the --source switch.

"scripts": {
  "extract": "i18n_extract --source=mysourcefolder",
  "import": "i18n_import"
}

Or if you want to export your locales to a different folder...

"scripts": {
  "extract": "i18n_extract --source=mysourcefolder --locales=mylocalesfolder",
  "import": "i18n_import"
}

By default this command find in all .js and .jsx file extensions, but you can customize it with fexts parameter. Check out this example:

"scripts": {
  "extract": "i18n_extract --fexts=js,jsx,coffee",
  "import": "i18n_import"
}

The default regular expression will search all occurrences of this.context.t string, but you can also supply your own custom pattern, as in the following example:

export default function App({aProp, bProp}, {t: translate}) {
  return <div>{translate('Hello world!')}</div>;
}

You will then need to set the --pattern flag in package.json:

"scripts": {
  "extract": "i18n_extract --pattern=translate",
  "import": "i18n_import"
}

Import .po files

When your translators are done translating your terms, you can import your po files running the import script:

npm run import

This script read all po files inside your locales folder, extract all translations and build a src/translations.js that you can then use in your project.

Your .po files must define header language, check mininal format for more information.

You can also set another locales folder:

"scripts": {
  "extract": "i18n_extract --source=mysource --locales=mylocales",
  "import": "i18n_import --locales=mylocales"
}

Or, save translation.js to a different location:

"scripts": {
  "extract": "i18n_extract --source=mysource --locales=mylocales",
  "import": "i18n_import --locales=mylocales --translations=myfolder"
}

You can also change the encoding for your extraction from PO (default is iso-8859-1)

"scripts": {
  "extract": "i18n_extract --source=mysource --locales=mylocales",
  "import": "i18n_import --encoding=utf-8"
}

Async translations

When applications grow, translations tend to bigger as well, adding a lot to the overall size of the js bundle.

You can set an empty translations object to the <I18n/> component and set the useReducer prop to true to use the store as the source of strings. For example:

<Provider store={this.store}>
  <I18n translations={{}} useReducer={true}>
    <MainApp/>
  </I18n>
</Provider>

Then you can use the setTranslations action.

import {setTranslations} from 'pomes'
api.get('...').then(r => this.props.dispatch(setTranslations(r.translations)))

You can pass a second parameter to the action to set the language. Depending on your response's structure, it could look like this:

api.get('...').then(r => this.props.dispatch(setTranslations(r.translations, 'en')))

Since version 1.5.1 is possible pass a dictionary as a second param with some options. This allows us set more functionalities to method.

  • preserveExisting (bool): If is true, the translations received does merge with existing translations.
  • language (string): Language code

Some examples:

setTranslations(newTranslations, {preserveExisting: true})
setTranslations({'Hello': 'Hallo'}, {language: 'de'})

InitialState

Sometimes language is set initially by the redux store creation, or in an isomorphic way. In this case, you can set the initialized prop to stop the I18n provider from dispatching an action.

HOC

If you want to isolate the use of context from your components, you can import the Localize Hoc to provide the translate function as a prop to your component. For example:

import { localize } from 'pomes'

class SomeComponent extends Component {
  render() {
    return this.props.t('hello world')
  }
}

export default localize()(SomeComponent)

You can also change the name of the provided prop:

import { localize } from 'pomes'

class SomeComponent extends Component {
  render() {
    return this.props.translate('hello world')
  }
}

export default localize('translate')(SomeComponent)

Please, if you like my package, don't forget to rate it. Click on the "star"!