Commit 1aec7d53 by Vladislav Lagunov

Рефакторинг ~/gettext

parent c80ce1ab
msgid ""
msgstr ""
"Language: ru_RU\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Confirm deletion"
msgstr "Подтвердите удаление"
msgid "Are you sure you want to delete this item?"
msgstr "Вы действительно хотите удалить этот объект?"
msgid "Cancel"
msgstr "Отмена"
msgid "Confirm"
msgstr "Подтвердить"
......@@ -2,7 +2,7 @@
```ts
import { makeGettext, withGettext, I18nString } from '~/gettext';
const gettext = makeGettext(require('./i18n.po.yml'));
const gettext = makeGettext(require('./i18n));
// Отложенный перевод строк
function printStatus(status: 'buzy'|'idle'): I18nString {
......@@ -40,66 +40,3 @@ export default withGettext(gettext)(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,71 @@ 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 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 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;
export class Translations {
constructor(
readonly data: Data
) {}
static concat(...webpackContexts) {
const jedTranslations: JedFormat[] = flattenModules(webpackContexts);
return new Translations(assignJedData({}, ...jedTranslations));
function flattenModules(xs: any[]) {
return Array.prototype.concat.apply([], xs.map(x => {
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[]) {
return Array.prototype.concat.apply([], xs.map(x => {
if ('__esModule' in x) x = x.default;
return Array.isArray(x) ? flattenModules(x) : [x]
}));
__(singular_key: string, plural_key?: string, context?: string, n?: number): I18nString {
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];
}
}
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 +75,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.concat(...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 +97,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