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 ```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 { function printStatus(status: 'buzy'|'idle'): I18nString {
const { __ } = translations;
switch (status) { switch (status) {
case 'buzy': return gettext('Buzy'); case 'buzy': return __('Buzy');
case 'idle': return gettext('Idle'); case 'idle': return __('Idle');
} }
} }
...@@ -34,72 +35,9 @@ class Widget extends React.Component<Props> { ...@@ -34,72 +35,9 @@ class Widget extends React.Component<Props> {
</div>; </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,73 +3,76 @@ import { Omit } from '@material-ui/core'; ...@@ -3,73 +3,76 @@ import { Omit } from '@material-ui/core';
import memoize from '~/functions/memoize'; import memoize from '~/functions/memoize';
// Делимитер контекста // Делимитер контекста в jed-формате
const CONTEXT_DELIMITER = String.fromCharCode(4); const CONTEXT_DELIMITER = String.fromCharCode(4);
// Translations // Translations
export type Translations = Record<string, Record<string, string[]|string>>; export type Data = Record<string, Record<string, string[]|string>>;
export type GettextData = { export type JedFormat = { locale_data: { messages: Record<string, string[]> } };
'': Record<string, unknown>; export type ES6Module<T> = { default: T };
'%podata': GettextExplicitKey[],
} & {
[K in string]: string|string[];
};
// Содержимое поля `%podata`
export type GettextExplicitKey = {
msgid: string;
msgid_plural?: string;
msgctxt?: string;
msgstr: string|string[];
}
// Хелпер для перевода // Хелпер для перевода
export type Gettext = (singular_key: string, context?: string, plural_key?: string, val?: number) => string; 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; export type I18nString = (locale?: string|LocaleCtx|null) => string;
// Использование локали из `ctx` export class Translations {
export type LocaleCtx = { locale: string }; constructor(
readonly data: Data
) {}
/**
* Создание хелпера для переводов static fromJed(...data: JedFormat[]): Translations;
*/ static fromJed(...data: ES6Module<JedFormat>[]): Translations;
export function makeGettext(...webpackContexts): DeferredGettext { static fromJed(...data: JedFormat[][]): Translations;
const translations: Translations = assignData({}, ...flattenModules(webpackContexts)); static fromJed(...data: ES6Module<JedFormat>[][]): Translations;
static fromJed(...webpackContexts) {
return (singular_key: string, context?: string, plural_key: string = singular_key, n: number = 1) => locale_ => { const jedTranslations: JedFormat[] = flattenModules(webpackContexts);
let locale = !locale_ ? 'en-US' : typeof(locale_) === 'string' ? locale_ : locale_.locale; return new Translations(assignJedData({}, ...jedTranslations));
locale = locale.replace(/_/, '-');
if (!(locale in translations)) { function flattenModules(xs: any[]) {
// Не найдена локаль в `translations` return Array.prototype.concat.apply([], xs.map(x => {
return n === 1 ? singular_key : plural_key; if ('__esModule' in x) x = x.default;
return Array.isArray(x) ? flattenModules(x) : [x]
}));
} }
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);
};
function flattenModules(xs: any[]) { __: (singular_key: string, plural_key?: string, context?: string, n?: number) => I18nString = (singular_key, plural_key?, context?, n?) => {
return Array.prototype.concat.apply([], xs.map(x => { return localeOrCtx => {
if ('__esModule' in x) x = x.default; let locale = !localeOrCtx ? 'en-US' : typeof(localeOrCtx) === 'string' ? localeOrCtx : localeOrCtx.locale;
return Array.isArray(x) ? flattenModules(x) : [x] 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];
}
} }
function nth<A>(xs: A|A[], idx: number): A { bind(localeOrCtx?: string|LocaleCtx|null): Gettext {
if (!Array.isArray(xs)) return xs; return (...args) => {
return idx >= xs.length ? xs[xs.length - 1] : xs[idx]; return this.__(...args)(localeOrCtx);
};
} }
} }
// Использование локали из `ctx`
export type LocaleCtx = { locale: string };
/** /**
* HOC для добавления переводов (для разделения переводов по фичам) * HOC для добавления переводов (для разделения переводов по фичам)
* ```ts * ```ts
...@@ -77,10 +80,11 @@ export function makeGettext(...webpackContexts): DeferredGettext { ...@@ -77,10 +80,11 @@ export function makeGettext(...webpackContexts): DeferredGettext {
* ``` * ```
*/ */
export function withGettext(...webpackContexts) { 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>) => { return <P extends { __: Gettext, ctx?: { locale: string } }>(Component: React.ComponentType<P>) => {
class WithTranslations extends React.Component<Omit<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() { render() {
const { ctx } = this.props; const { ctx } = this.props;
...@@ -98,32 +102,10 @@ export function withGettext(...webpackContexts) { ...@@ -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` * Добавление переводов из `srcs` в `dst`
*/ */
function assignData(dst: Translations, ...srcs: GettextData[]): Translations { function assignJedData(dst: Data, ...srcs: JedFormat[]): Data {
srcs.forEach(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_ = data[''].language || data[''].lang || data[''].locale; if (!locale_ || typeof(locale_) !== 'string') return;
const locale = locale_.replace(/_/g, '-'); const locale = locale_.replace(/_/g, '-');
const podata = data['%podata']; 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