Skip to content

Translate, Interplate, format text into different localisations for React applications.

Notifications You must be signed in to change notification settings

blueberryapps/ts-translate

Repository files navigation

Translate CircleCI Dependency Status

Helps you to translate text from messages object. Allows you to format numbers, dates, interpolate text to string templates, default text as key, scoping translations, putting it to React Redux application with Provider and decorator (same pattern as Redux is using).

Also it supports connection to translation server with possibility to download translation on both server and browser side. Freeze translations with releases created in translation server. Also there is possibility of live updates of translations via Server Side Events.

yarn add ts-translate

Vanilla JS usage

import { fromJS } from 'immutable';
import { Translator } from 'ts-translate';


const translator = new Translator({
  messages: fromJS({
    en: {
      foo: 'bar',
      bar: {
        foo: 'FooBar'
        'Some Headline': 'Super Headline'
      }
    }
  }),
  locale: 'en'
});

translator.text('foo') // => 'bar'
translator.msg('foo') // => 'bar'
translator.msg('foo', { scope: 'bar' }) // => 'FooBar'
translator.msg('Some headline') // Not found will return text in first argument => 'Some headline'
translator.msg('Some headline', { scope: 'bar' }) // => 'Super Headline'

translator.hasMsg('foo') // => true'
translator.hasMsg('Some headline') //=> false

translator.msg('Not found text', { disableDefault: true })
// => Will not return default text (Not found text) instead it will return 'null'

translator.formatNumber(123456.78) // => 123,456.78
translator.formatNumber(123456.78, {precision: 1}) // => 123,456.8
translator.formatNumber(123456.78, {precision: 0}) // => 123,457
translator.formatNumber(123456.78, {delimiter: '.', separator: ','}) // => 123.456,78
translator.formatNumber(123456.78, {template: '%n %u', unit: '%'}) // => 123.456,78 %
translator.formatNumber(123456.78, {template: '%n $'}) // => 123.456,78 $

translator.formatCurrency(123456.78, {unit: '€'}) // => 123.456,78 €
translator.formatPercentage(123456.78) // => 123.456,78 %

translator.formatDate(new Date, 'M.D. YYYY') // => 28.2. 2017 -> using moment.js syntax
translator.formatDate(new Date, 'short') // => 28.2. 2017 -> using aliases (look down to Formats specification)

Msg & Cnt

msg() is returning by default React component which has set toString() set to return translated result so msg() will work correctly in <input placeholder={msg('placeholder')} />. cnt() is alias for msg() (deprecated).

if you need to return only translated result use text().

Interpolation

You can easily interpolate your translations with variables that will be evaluated during the execution of code and inserted into resulting strings.

{
  messages: fromJS({
    'This is value of %{key}: %{value}': 'Este es el valor de %{key}: %{value}',
    noninterpolatedKey: 'You have %{count} unread messages',
    deeper: {
      scopedKey: 'This is scoped and shows %{foo}',
    }
  })
}

Usage:

  msg('This is value of %{key}: %{value}', { key: 'foo', value: 'bar' })
  // => "Este es el valor de foo: bar"
  msg('noninterpolatedKey', { count: 42 })
  // => "You have 42 unread messages"
  msg('No need for translation to %{action} %{what}', { action: 'show', what: 'that' })
  // => "No need for translation to show that"
  msg('scopedKey', { scope: 'deeper', foo: 'BAR!' })
  // => "This is scoped and shows BAR!"

Formats Spefication

you can modify default formatting options by specifying:

{
  messages fromJS({
    en: {
      formats: {
        date: {
          default: { // formatDate(new Date)
            format: 'D.M.YYYY',
          },
          shortTime: { // formatDate(new Date, 'short')
            format: 'H:m'
          },
          long: { // formatDate(new Date, 'long')
            format: 'D.M.YY H:m:s'
          }
        },
        number: {
          default: {
            precision: 2
          },
          currency: { // formatNumber(123, 'percentage') === formatPercentage(123)
            unit: '€'
          },
          percentage: { // formatNumber(123, 'percentage') === formatPercentage(123)
            template: '%n %'
          },
          custom: { // formatNumber(123, 'custom')
            precision: 3
          }
        }
      }
    }
  })
}

this will enable you to use:

translator.formatNumber(123456.78) // => 123,456.78
translator.formatNumber(123456.78, 'currency') // => 123,456.78 € -> using alias
translator.formatCurrency(123456.78) // => 123.456,78 €
translator.formatPercentage(123456.78) // => 123.456,78 %

translator.formatDate(new Date, 'shortTime') // => 19:23 -> using alias
translator.formatDate(new Date, 'long') // => 28.2.2017 19:23:16 -> using alias

translator.formatNumber(123456.78, { precision: 1, separator: ',', delimiter: '' }) // => 123456,7 € -> using custom options

All number formatting options:

{
  precision: 10, // number of decimal places
  delimiter: ',', // used in 10,000,000.10
  format: defaultDateFormat,
  separator: '.', // used in 10.10
  template: '%n %u', // %n is placeholder for number, %u is for unit
  unit: '',
  trimTrailingZeros: true, // will remove exessive zeros 10.1200 -> 10.12
}

React + Redux

import { Provider as TranslateProvider, translate, reducer, changeLocale } from 'ts-translate';
import { fromJS } from 'immutable'
import { createStore, combineReducers } from 'redux';
import { Provider as ReduxProvider } from 'react-redux';

const initial = {
  messages: fromJS({
    cs: { home: { description: 'Foo', fallback: 'Fallback' } },
    en: { home: { description: 'enFoo', about: 'Bar' } }
  }),
  locale: 'cs',
  fallbackLocale: 'en'
};

// translation reducer must be added under `translate` key
const store = createStore(combineReducers({ translate: reducer }), { translate: initial});

class MyComponent extends React.Component {
  render() {
    const { msg } = this.props;
    return (<div>{msg('Something nice')}</div>);
  }
}

const TranslatedMyComponent = translate()(MyComponent);
const TranslatedScopedMyComponent = translate('home')(MyComponent);

store.dispatch(changeLocale('en')); // change locale to en

<ReduxProvider store={store} >
  <TranslateProvider>
    <TranslatedMyComponent /> // will look for key 'Something nice' in root of locale messages
    <TranslatedScopedMyComponent /> // will look for key 'Something nice' in `home` scope of locale messages
  </TranslateProvider>
</ReduxProvider>

Translation server

Enable application to send and receive translations from Translation server.

  • apiUrl - url to translation server
  • apiToken - project token is needed for enabling synchronization
  • sync - enable synchronization of translations (remembers translations, sends them to server and receives translations back)
  • liveSync - enable Server Side Events for instant refresh of translations directly after update in translation server
  • releasesDir - directory to store releases (needed for releases workflow)

Task for communicating with Translation server

Enables you to list and download releases to your project.

yarn translate-list // List all created releases
yarn translate-fetch // Download one specified release (for locale) to specified folder
yarn translate-fetch-latest-releases // Download all latest releases for all locales

Both commands supports taking apiToken and apiUrl from ENV variables (TRANSLATE_API_TOKEN, TRANSLATE_API_URL) or command line (--apiToken, --apiUrl) arguments.

Fetch commands also needs --releasesDir option for specifiyng where to save downloaded releases.

Passing config to Provider

import { Provider as TranslateProvider } from 'ts-translate';

const config = {
  apiUrl: 'https://translations.blueberry.io',
  apiToken: 'XYZ',
  sync: true,
  liveSync: true
}

<TranslateProvider config={config}>
  // ...
</TranslateProvider>

How to locate translations on different pages

You need to pass pathname or history object to TranslateProvider

<TranslateProvider config={config} pathname="/some/path">
  // ...
</TranslateProvider>
import createHistory from 'history/createBrowserHistory';
const history = createHistory();

<TranslateProvider config={config} history={history}>
  // ...
</TranslateProvider>

In App editor

Translation Editor

Just add new wrapper TranslateEditor around your code. This Editor must be under TranslateProvider context

import TranslateEditor from 'ts-translate/lib/editor';

<TranslateProvider config={config} pathname="/some/path">
  <TranslateEditor pathname="/some/path">
    // ...
  </TranslateEditor>
</TranslateProvider>

same rule for pathname and history as for TranslateProvider applies for TranslateEditor.

for disabling whole editor functionality (for example in production) set ENV variable TRANSLATE_DISABLE_EDITOR=t. When you are using webpack don't forget to add this to DefinePlugin as new webpack.DefinePlugin({ 'process.env.TRANSLATE_DISABLE_EDITOR': true }). Also if you are using minifier the whole code gets removed by death code removal. So it is pretty easy to use this editor in development and stage, but it will not increase size in production build.

On server side preload latest translations from TS

import { fetchTranslations } from 'ts-translates';

fetchTranslations(config)(dispatch);

Made with love by