Commit 2f06a7e2 by Vladislav Lagunov

Merge branch 'master' into bitmaster-core

parents 42697ecc 9d299260
/**
* Прямая композиция
*/
export default function compose<A, B>(f: (a: A) => B): (a: A) => B;
export default function compose<A, B, C>(f: (a: A) => B, g: (b: B) => C): (a: A) => C;
export default function compose<A, B, C, D>(f: (a: A) => B, g: (b: B) => C, h: (c: C) => D): (a: A) => D;
export default function compose<A, B, C, D, E>(f: (a: A) => B, g: (b: B) => C, h: (c: C) => D, j: (d: D) => E): (a: A) => E;
export default function compose(...funcs: Function[]): Function {
return a => funcs.reduce((acc, f) => f(acc), a);
}
## Пример использования
```ts
import { makeGettext, withGettext, I18nString } from '~/gettext';
import { Transaction, withGettext, I18nString } from '~/gettext';
const gettext = makeGettext(require('./i18n.po.yml'));
const translations = Transaction.fromJed(require('./i18n'));
// Отложенный перевод строк
function printStatus(status: 'buzy'|'idle'): I18nString {
const { __ } = translations;
switch (status) {
case 'buzy': return gettext('Buzy');
case 'idle': return gettext('Idle');
case 'buzy': return __('Buzy');
case 'idle': return __('Idle');
}
}
......@@ -34,72 +35,9 @@ class Widget extends React.Component<Props> {
</div>;
}
}
export default withGettext(gettext)(Widget);
export default withGettext(translations)(Widget);
// Или так
// export default withGettext(require('./i18n.po.yml'))(Widget);
// export default withGettext(require('./i18n'))(Widget);
```
## Формат *.po.yml
> 2018-10-31 DEPRECATED: Решил использовать стантартный формат *.po
Альтернативный формат, для *.po файлов предназначен предназначен для
более простой манипуляции с gettext-данными, и для обеспечения
возможности хранения переводов вместе с UI-компонентами модуль
```yml
- "":
locale: "ru-RU"
"Accommodations": "Проживания"
"Select guests": "Выберите гостя"
"%podata":
- msgid: "%d sec"
msgid_plural: "%d sec"
msgstr: ["%d секунда", "%d секунды", "%d секунд"]
- msgid: "<b>%d</b>&ndash;<b>%d</b> of <b>%d</b>"
msgid_plural: "<b>%d</b>&ndash;<b>%d</b> of <b>%d</b>"
msgstr: ["<b>%d</b>&ndash;<b>%d</b> из <b>%d</b>", "<b>%d</b>&ndash;<b>%d</b> из <b>%d</b>", "<b>%d</b>&ndash;<b>%d</b> из <b>%d</b>" ]
```
В корне *.po.yml должен быть массив, каждый элемент которого
соответствует целому *.po файлу.
```
- "":
locale: "ru-RU"
```
в поле с пустым именем содержатся метаданные файла (как в секции msgid
"" в оригинальном *.po)
```
"%podata":
- msgid: "%d sec"
msgid_plural: "%d sec"
msgstr: ["%d секунда", "%d секунды", "%d секунд"]
```
Еще одно специальное поле в объекте внутри массива — `%podata` внутри
содержатся поля соответствующие записям из оригинального формата po
`msgid`, `msgid_plural`, `msgctx`, `msgstr`
```
- ""
# ...
"Accommodations": "Проживания"
"Select guests": "Выберите гостя"
# Тоже самое в *.po
msgid "Accommodations"
msgstr "Проживания"
msgid "Select guests"
msgstr "Выберите гостя"
```
Остальные ключи внутри эелемента в корне массива соответствуют краткой
форме для записи в po в которой присутствуют только две строки `msgid` и `msgstr`
......@@ -3,58 +3,33 @@ import { Omit } from '@material-ui/core';
import memoize from '~/functions/memoize';
// Делимитер контекста
// Делимитер контекста в jed-формате
const CONTEXT_DELIMITER = String.fromCharCode(4);
// Translations
export type Translations = Record<string, Record<string, string[]|string>>;
export type GettextData = {
'': Record<string, unknown>;
'%podata': GettextExplicitKey[],
} & {
[K in string]: string|string[];
};
// Содержимое поля `%podata`
export type GettextExplicitKey = {
msgid: string;
msgid_plural?: string;
msgctxt?: string;
msgstr: string|string[];
}
export type Data = Record<string, Record<string, string[]|string>>;
export type JedFormat = { locale_data: { messages: Record<string, string[]> } };
export type ES6Module<T> = { default: T };
// Хелпер для перевода
export type Gettext = (singular_key: string, context?: string, plural_key?: string, val?: number) => string;
export type DeferredGettext = (singular_key: string, context?: string, plural_key?: string, val?: number) => I18nString;
export type I18nString = (locale?: string|LocaleCtx|null) => string;
// Использование локали из `ctx`
export type LocaleCtx = { locale: string };
export class Translations {
constructor(
readonly data: Data
) {}
/**
* Создание хелпера для переводов
*/
export function makeGettext(...webpackContexts): DeferredGettext {
const translations: Translations = assignData({}, ...flattenModules(webpackContexts));
return (singular_key: string, context?: string, plural_key: string = singular_key, n: number = 1) => locale_ => {
let locale = !locale_ ? 'en-US' : typeof(locale_) === 'string' ? locale_ : locale_.locale;
locale = locale.replace(/_/, '-');
if (!(locale in translations)) {
// Не найдена локаль в `translations`
return n === 1 ? singular_key : plural_key;
}
const key = context ? context + CONTEXT_DELIMITER + singular_key : singular_key;
const dict = translations[locale];
const pluralIdx = getPluralForm(locale)(n);
const pluralList = dict[key] || [singular_key, plural_key];
return nth(pluralList, pluralIdx);
};
static fromJed(...data: JedFormat[]): Translations;
static fromJed(...data: ES6Module<JedFormat>[]): Translations;
static fromJed(...data: JedFormat[][]): Translations;
static fromJed(...data: ES6Module<JedFormat>[][]): Translations;
static fromJed(...webpackContexts) {
const jedTranslations: JedFormat[] = flattenModules(webpackContexts);
return new Translations(assignJedData({}, ...jedTranslations));
function flattenModules(xs: any[]) {
return Array.prototype.concat.apply([], xs.map(x => {
......@@ -62,14 +37,42 @@ export function makeGettext(...webpackContexts): DeferredGettext {
return Array.isArray(x) ? flattenModules(x) : [x]
}));
}
}
__: (singular_key: string, plural_key?: string, context?: string, n?: number) => I18nString = (singular_key, plural_key?, context?, n?) => {
return localeOrCtx => {
let locale = !localeOrCtx ? 'en-US' : typeof(localeOrCtx) === 'string' ? localeOrCtx : localeOrCtx.locale;
locale = locale.replace(/_/, '-');
if (!(locale in this.data)) {
// Не найдена локаль в `this.data`
return n === 1 ? singular_key : plural_key!;
}
const key = context ? context + CONTEXT_DELIMITER + singular_key : singular_key;
const dict = this.data[locale];
const pluralIdx = getPluralForm(locale)(n || 1);
const pluralList = dict[key] || [singular_key, plural_key];
return nth(pluralList, pluralIdx)!;
};
function nth<A>(xs: A|A[], idx: number): A {
if (!Array.isArray(xs)) return xs;
return idx >= xs.length ? xs[xs.length - 1] : xs[idx];
}
}
bind(localeOrCtx?: string|LocaleCtx|null): Gettext {
return (...args) => {
return this.__(...args)(localeOrCtx);
};
}
}
// Использование локали из `ctx`
export type LocaleCtx = { locale: string };
/**
* HOC для добавления переводов (для разделения переводов по фичам)
* ```ts
......@@ -77,10 +80,11 @@ export function makeGettext(...webpackContexts): DeferredGettext {
* ```
*/
export function withGettext(...webpackContexts) {
const gettext = typeof(webpackContexts[0]) === 'function' ? webpackContexts[0] : makeGettext(...webpackContexts);
const translations: Translations = webpackContexts[0] instanceof Translations ? webpackContexts[0] : Translations.fromJed(...webpackContexts);
return <P extends { __: Gettext, ctx?: { locale: string } }>(Component: React.ComponentType<P>) => {
class WithTranslations extends React.Component<Omit<P, '__'>> {
makeGettext: (locale?: string|LocaleCtx) => Gettext = memoize((locale) => (...args) => gettext.apply(void 0, args)(locale))
makeGettext: (locale?: string|LocaleCtx) => Gettext = memoize((locale) => translations.bind(locale))
render() {
const { ctx } = this.props;
......@@ -98,32 +102,10 @@ export function withGettext(...webpackContexts) {
/**
* Хелпер для импорта нескольких переводов
* ```ts
* export default requireTranslations(require.context('./i18', false, /\.po$/));
* ```
*/
export function requireTranslations(webpackContext /*: WebpackContext*/): Translations {
return webpackContext.keys().reduce((acc, k) => mergeTranslations(acc, webpackContext(k)), {});
function mergeTranslations(acc, data) {
data = data.__esModule ? data.default : data;
for (const k of Object.keys(data)) {
acc[k] = acc[k] || {};
Object.assign(acc[k], data[k]);
}
return acc;
}
}
/**
* Добавление переводов из `srcs` в `dst`
*/
function assignData(dst: Translations, ...srcs: GettextData[]): Translations {
function assignJedData(dst: Data, ...srcs: JedFormat[]): Data {
srcs.forEach(data => {
// @ts-ignore
if ('locale_data' in data) data = data['locale_data']['messages'];
const locale_ = data[''].language || data[''].lang || data[''].locale; if (!locale_ || typeof(locale_) !== 'string') return;
const locale = locale_.replace(/_/g, '-');
const podata = data['%podata'];
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment