tx-i18n + ICU = 💜. Release v1 🎊🎉🍻

Lebedev Konstantin
6 min readMar 14, 2020

Да, это наконец случилось, я наконец доделал (на самом деле нет, но у меня работает) свой лучший инструмент, который мечтал сделать уже как минимум 5, а может и 6 лет.

Но, прежде чем начать, надо пояснить, почему же меня так волновала эта тем.

Предыстория

В свой жизни мне приходилось заниматься интернационализацией не один раз и всегда это была боль, жгучая боль, которая никогда не заканчивалась.

Боль заключалась в том, что обычно проект уже давно сделан, никто о язык никогда не думал и текст написан там где он необходим и поэтому, что в проект добавить i18n, у вас есть только один путь, найти весь этот текст и загнать…

Dictionary

Создаём словарик, а дальше в нужно месте используем его, типа

// Было
<h1>Привет, {user.name}!</h1>
⬇️ ⬇️ ⬇️// Выносим текст в словарик
const dict = i18n.createDictionary({
greeting: "Привет, {user.name}!",
});
⬇️ ⬇️ ⬇️// Используем
<h1>{dict.get("greeting", {user})</h1>

GetText

По факту мало чем отличается от словаря, но у каждого из способов есть свои плюсы и минусы, но пока рассмотрим, как же выглядит gettext:

// Было
<h1>Привет, {user.name}!</h1>
⬇️ ⬇️ ⬇️// Используем
<h1>{__("Привет, {user.name}!", {user})</h1>

__ — это функция, которую дальше находит постпроцессор и сам создаёт словарик (на самом деле там хитра штука с po файлами, но про это сами читайте на Wiki)

Короче, как видите, по факту оба способа подразумевают, что вам придётся отредактировать весь проект и заменить весь текст на некую функцию, которая будет принимать либо id словарика, либо исходный текст, который по факту и есть id.

Так какие же плюсы/минусы у эти способов?

У Dictionary огромный плюс, что вы используете id, а не саму фразу в качестве id , ибо в случае GetText каждый раз редактируя фразу, например в случае опечатки, вы получаете новый id и теряете перевод, что не происходит c Dictionary.

Казалось бы, выбор очевиден, но у Dictionary есть м-а-а-аленькая проблема, во-первых, чтобы избежать дубликатов, вы должны напрягать либо глаза, либо какой-то инструмент, который будет бить по рукам, во-вторых, что более важно, это когда у вас в коде появляется html-разметка и она вместе с текстом уезжает в словарик, а это полный пиздец.

Именно по этому, даже учитывая недостатки GetText, я выбираю его, поэтому что-то вам не нужно контролировать дубликаты, а главное, текст и разметка, остаётся там, где она и находится.

Предыстория. Часть II.

И так, как я уже написал выше, чтобы добавить i18n в проект, его нужно в любом случае разметить, притом не только разметку, но и в коде.

Каждый раз, когда это происходило, я постоянно задавал окружающим вопрос «ЗАЧЕМ?».

Ну серьезно, зачем вы добавляете дополнительную разметку в xml/html-разметку?

Ответа на этот вопрос я так и не получил, поэтому из года в года, наблюдаю одну и тужу картину.

Вы только посмотрите, что предлагают! Они ваще, нормальные? (react-intl и другие)

Поэтому 27 августа 2018 года был сделан первый комит tx-i18n тулзы, а точне TypeScript transformer, который за вас находит «текст» в TS или TSX и размечает его на стадии трансформации в JS.

tx-i18n

Честно говоря, я очень долго думал, как и о чем в этой статье написать, ведь тут нечего рассказывать как в том же react-intl, мол учите документацию, api и так далее, у меня всего этого нет, просто подключаете трансформе и происходит чудо, никакой магии.

И потом меня осенило, в react-intl, или formatjs, на котором он основан, вам нужно не просто выучить какое API имет либо, но ещё и понять, как описывать переменные или туже плюрализацию, ибо всё это базируется на ICU Message Format с каторым вам придётся разобраться (благо он сложный).

Но ICU Message есть проблема, это просто текст с мета разметкой, которую не поддерживают IDE, а валидируется она в рантайме, т.е. смотрите, типичный пример:

<div className="info">
<FormattedMessage
defaultMessage={`
Hello {name}, you have {unread} {unread, plural,
one {message}
other {messages}}

`}
values={{
name: <b>{user.name}</b>,
unread: user.unread.messages,
}}
/>
</div>

Внимательно взгляните на defaultMessages и values , эти два параметра связанны, но на самом деле, они ничего друг о друге не знаю, малейшая опечатка и привет undefined.

Окей, допустим мы сделаем warning на такой случай, но остаётся вторая проблема, это «мясо» тяжело описывать и поддерживать, по сравнению с tx-i18n, где тот же пример выглядит так:

<div>
Hello {user.name}, you have {plural(user.unread.messages, {
one: '# message',
other: '# messages',
})}

</div>

Т.е. он выглядит так, как если бы у вам была не нужна i18n!

Дальше, после работы tx-i18n, в итоговом js-файле будет вот такой код

<div>{__(`Hello {v1}, you have {v2, plural,
one {# message}
other {# messages}}
`,
[
user.name,
{$: plural, one: '# message', other: '# messages']},
],
)}</div>

И в систему переводов улетит «текст» согласно стандарту, который вам даже знать необязательно ;]

Pluralization, Select и etc

А теперь самое офигенное, ещё раз посмотрите, что именно вас заставляет писать formatjs (react-int и подобные):

{unread, plural, 
one {message}
other {messages}
}

one, other, что?
Откуда их взять?
А если я по-русски?

Plural categories

Всего их 6 (zero, one, two, few, many, other) и для каждого языка есть правила, который вы можете найти на CLDR: Language Plural Rules.

Узнать то вы их узнаете, но никто не помешает случайно пропустить одну из категорий, что приведёт к ошибке.

Но с tx-i18n такое не возможно, т.к. вы используете чистый TS, и плюрализация у вас это не часть «мета разметки», полноценная функция.

Притом эта функция именно для русского, для english, будет другая

Т.е. TypeScript будет бить вас по рукам, если вы забудете одну из категорий, кроме этого, ещё можно делать так:

const messagesPlural = (v: number) => plural(v, {
one: "message",
other: "messages",
});

Но самый крутяк, это создание плюрализатора и для этого опять вернемся к CLDR, а именно последнему столбцу Rules:

Ничего не напоминает?
Правильно, это DSL!

Честно, никогда такого не видел, поэтому если вам просто нужна плюрализация, то эта штука лежит у меня отдельно:

Эпилог

Думал написать, как всё это настраивать, но всё де для этого есть readme, поэтому просто перечислю фичи, который доступны на данный момент:

npm i --save-dev tx-i18n

--

--