tx-i18n

Auto-translate for your application (ICU, pluralization, TSX/React supported)

Usage no npm install needed!

<script type="module">
  import txI18n from 'https://cdn.skypack.dev/tx-i18n';
</script>

README

🈂️ tx-i18n

Auto-translate for your application (TSX/React supported)

npm i --save-dev tx-i18n

Feature


Usage with "pure" TypeScript

ttypescript — it's not a typo 👐🏻

npm i --save-dev ttypescript
Add transformer-plugin to tsconfig.json in compilerOptions section
{
  "compilerOptions": {
    "plugins": [{
      "transform": "tx-i18n/plugin",
      "humanTextCheckRegExp": "[а-яё]",
      "output": "./src/locale/default.ts"
    }]
  },
}
Edit package.json and use ttsc (yep, double t) instead of tsc
{
  "name": "your-package",
  "scripts": {
    "build": "ttsc"
  }
}

Usage with webpack

webpack.config.js
const { i18nTx, i18nExtractor } = require('tx-i18n/webpack');

module.exports = {
    // ...
    module: {
        rules: [{
            test: /\.tsx?$/,
            loader: 'awesome-typescript-loader',
            exclude: /node_modules/,
            options: {
                getCustomTransformers: () => ({
                    before: [
                        i18nTx({}), // <!--- (1) TypeScript i18n Transformer
                    ],
                    after: [],
                }),
            },
        }],
    },

    plugins: [
        new i18nExtractor({
            output: './src/locale/__default__.ts', // <!--- (2) Extract original phrases as ES Module
        }),
    ],
};
app-entry-point.ts
import { setLang, setLocale } from 'tx-i18n';
import { plural } from 'tx-i18n/icu/plural/en';
import locale from './locale/en';

setLocale('en', locale, plural);
setLang('en');
./src/locale/__default__.ts
export default {
    'default': {
        'Hi, <#1>!': 'Hi, <#1>!',
        'Click <1>here</1> for help': 'Click <1>here</1> for help',
    },
};

Context

const dict = {
    firstName: 'Имя',
    lastName: 'Фамилия',
};

/** @tx-i18n context: personal */
const personalDict = {
    firstName: 'Имя',
    lastName: 'Фамилия',
};

const Dialog = () => (
    <div>
        {/** @tx-i18n context: personal */}
        <form>
            <h1>Ваши данные</h1>
            <fieldset>...</fieldset>
        </form>
    </div>
);
./src/locale/__default__.ts
export default {
    'default': {
        'Имя': 'Имя',
        'Фамилия': 'Фамилия',
    },
    'personal': {
        'Имя': 'Имя',
        'Фамилия': 'Фамилия',
        'Ваши данные': 'Ваши данные',
    },
};

Storybook

1. Create a file called addons.js in your Storybook config, if there is no any and append following line:

import 'tx-i18n/storybook-addon/register';

2. Then in your story's config or in a global config for the project (config.js)

import { addParameters, addDecorator } from '@storybook/react';
import { withTXI18n } from 'tx-i18n/storybook-addon';
import en from '../locale/en'; // any locale of your application

addParameters({
  'tx-i18n': {
    locales: {en},
    defaultLang: 'ru',
  },
});

addDecorator(withTXI18n);

Pluralization

import { enPlural as plural } from 'tx-i18n/plural/en';

const Hello = ({name, unreadCount}) => (
    <div>
        Hello <b>{name}</b>!<br/>
        You have <a href="#unread">{plural(unreadCount, {one: '# message', other: '# messages'})}</a>.
    </div>
);

API

getLang(): string

Get a current lang.


setLang(lang)

Change a current lang.

  • lang: string

setLocale(lang, locale)

Set a locale for a lang.

  • lang: string
  • locale: Locale

addLangObserver(listener): unobserve

Add an observer on a lang changes.

  • listener: (lang, prevLang) => void

getTranslate(phrase[, lang]): string

Get a translate for a phrase.

  • phrase: string
  • lang: string

Internal API

i18nTx

Is a magic typescript transformer ;]

  • fnName: string — the name of the function that will be used to wrap strings. (optional, default: __)
  • packageName: string — the name of the package from which will be exported as default the function with the name fnName. (optional, default: tx-i18n)
  • include: Array<string|regexp> — an array of files or paths that need for a transform. (optional)
  • exclude: Array<string|regexp> — an array of files or paths that need to exclude for a transform. (optional)
  • pharsesStore: ContextedPharses — a reference to a variable which will be used to collect phrases (optional)
  • normalizeText: (text: string) => string — (optional)
  • isHummanText: (text: string, node: ts.Node) => boolean — (optional)
  • isTranslatableJsxAttribute: (attr: ts.JsxAttribute, elem: ts.JsxElement) => boolean — (optional)
  • overrideHumanTextChecker: (isHummanText: HumanTextChecker) => HumanTextChecker — (optional)

i18nExtractor

Is a webpack plugin for save all phrases to translate

  • output: string | (phases: ContextedPhrases) => Array<{file: string; phases: ContextedPhrases}> — the filename or function that returns the array for separation phrases by files.
    • .json — save as json
    • If you use .ts or .js, file will be saved as ES Module.
  • stringify: (locale: ContextedLocale) => string — convertor to json before save (optional)
  • indent: string (optional)
type ContextedPhrases = {
    [context: string]: Pharse[];
}

type Pharse = {
    value: string;
    file: string;
    loc: {
        start: {
            line: number;
            character: number;
        };
        end: {
            line: number;
            character: number;
        };
    };
}

type ContextedLocale = {
    [context: string]: Locale;
}

type Locale = {
    [pharse: string]: string;
}

How it works

Using the Compiler API, i18Tx traversing the AST-tree and wrap the text nodes a special function + add the import of this function, it looks like this:

Simple text

// Original
const text = 'Hello world';

// Transformed (after bundle build)
import __ from 'tx-i18n';
const text = __('Hello world');

Literal template

// Original
const text = `Hi, ${username}!`;

// Transformed (after bundle build)
import __ from 'tx-i18n';
const text = __('Hi, {v1}!', [username]);

TSX / React

// Original
const Fragment = () => (
    <div title="This is fragment" data-name="frag">
        <h1>Fragment of HTML</h1>
        <div>
            Click <a href="#help" title="How to use tx-i18n">here</a> for detail.
        </div>
    </div>
);

// Transformed (after bundle build)
import __ from 'tx-i18n';
const Fragment = () => (
    <div title={__('This is fragment')} data-name="frag">
        <h1>{__('Fragment of HTML')}</h1>
        {__.jsx('Click <1>here</1> for detail.', [
            {type: 'div', props: {}},
            {
                type: 'a',
                props: {
                    href: '#help'
                    title: __('How to use tx-i18n'),
                },
            },
        ])}
    </div>
);

Development


Support

v0.5

  • webpack: 4.29
  • typescript: 3.0
  • awesome-typescript-loader: 5.2