* [I18n] Add pseudo-localization to i18n engine * Add pseudo-localization to React * Remove lookbehind from regex * Resolve comments * Add comment
This commit is contained in:
parent
94637b538f
commit
46b83db605
|
@ -47,3 +47,5 @@ Error: The intl string context variable 'numPhotos' was not provided to the stri
|
|||
other {# photos.}
|
||||
}'"
|
||||
`;
|
||||
|
||||
exports[`I18n engine translateUsingPseudoLocale should translate message using pseudo-locale 1`] = `"Ṁéšššàĝĝé ŵîîţĥ àà [ɱàŕŕķðôôŵñ ļļîñķķ](http://localhost:5601/url) àñðð àñ <strong>ĥĥţɱļļ éļééɱéññţ</strong>"`;
|
||||
|
|
|
@ -787,4 +787,19 @@ describe('I18n engine', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('translateUsingPseudoLocale', () => {
|
||||
test('should translate message using pseudo-locale', () => {
|
||||
i18n.setLocale('en-xa');
|
||||
const message = i18n.translate('namespace.id', {
|
||||
defaultMessage:
|
||||
'Message with a [markdown link](http://localhost:5601/url) and an {htmlElement}',
|
||||
values: {
|
||||
htmlElement: '<strong>html element</strong>',
|
||||
},
|
||||
});
|
||||
|
||||
expect(message).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,6 +24,7 @@ import IntlRelativeFormat from 'intl-relativeformat';
|
|||
import { Messages, PlainMessages } from '../messages';
|
||||
import { Formats, formats as EN_FORMATS } from './formats';
|
||||
import { hasValues, isObject, isString, mergeAll } from './helper';
|
||||
import { isPseudoLocale, translateUsingPseudoLocale } from './pseudo_locale';
|
||||
|
||||
// Add all locale data to `IntlMessageFormat`.
|
||||
import './locales.js';
|
||||
|
@ -177,25 +178,28 @@ export function translate(
|
|||
defaultMessage: '',
|
||||
}
|
||||
) {
|
||||
const shouldUsePseudoLocale = isPseudoLocale(currentLocale);
|
||||
|
||||
if (!id || !isString(id)) {
|
||||
throw new Error('[I18n] An `id` must be a non-empty string to translate a message.');
|
||||
}
|
||||
|
||||
const message = getMessageById(id);
|
||||
const message = shouldUsePseudoLocale ? defaultMessage : getMessageById(id);
|
||||
|
||||
if (!message && !defaultMessage) {
|
||||
throw new Error(`[I18n] Cannot format message: "${id}". Default message must be provided.`);
|
||||
}
|
||||
|
||||
if (!hasValues(values)) {
|
||||
return message || defaultMessage;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
try {
|
||||
const msg = getMessageFormat(message, getLocale(), getFormats());
|
||||
// We should call `format` even for messages without any value references
|
||||
// to let it handle escaped curly braces `\\{` that are the part of the text itself
|
||||
// and not value reference boundaries.
|
||||
const formattedMessage = getMessageFormat(message, getLocale(), getFormats()).format(values);
|
||||
|
||||
return msg.format(values);
|
||||
return shouldUsePseudoLocale
|
||||
? translateUsingPseudoLocale(formattedMessage)
|
||||
: formattedMessage;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[I18n] Error formatting message: "${id}" for locale: "${getLocale()}".\n${e}`
|
||||
|
|
104
packages/kbn-i18n/src/core/pseudo_locale.ts
Normal file
104
packages/kbn-i18n/src/core/pseudo_locale.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Matches every single [A-Za-z] character, `<tag attr="any > text">` and `](markdown-link-address)`
|
||||
*/
|
||||
const CHARS_FOR_PSEUDO_LOCALIZATION_REGEX = /[A-Za-z]|(\]\([\s\S]*?\))|(<([^"<>]|("[^"]*?"))*?>)/g;
|
||||
const PSEUDO_ACCENTS_LOCALE = 'en-xa';
|
||||
|
||||
export function isPseudoLocale(locale: string) {
|
||||
return locale.toLowerCase() === PSEUDO_ACCENTS_LOCALE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces every latin char by pseudo char and repeats every third char twice.
|
||||
*/
|
||||
function replacer() {
|
||||
let count = 0;
|
||||
|
||||
return (match: string) => {
|
||||
// if `match.length !== 1`, then `match` is html tag or markdown link address, so it should be ignored
|
||||
if (match.length !== 1) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const pseudoChar = pseudoAccentCharMap[match] || match;
|
||||
return ++count % 3 === 0 ? pseudoChar.repeat(2) : pseudoChar;
|
||||
};
|
||||
}
|
||||
|
||||
export function translateUsingPseudoLocale(message: string) {
|
||||
return message.replace(CHARS_FOR_PSEUDO_LOCALIZATION_REGEX, replacer());
|
||||
}
|
||||
|
||||
const pseudoAccentCharMap: Record<string, string> = {
|
||||
a: 'à',
|
||||
b: 'ƀ',
|
||||
c: 'ç',
|
||||
d: 'ð',
|
||||
e: 'é',
|
||||
f: 'ƒ',
|
||||
g: 'ĝ',
|
||||
h: 'ĥ',
|
||||
i: 'î',
|
||||
l: 'ļ',
|
||||
k: 'ķ',
|
||||
j: 'ĵ',
|
||||
m: 'ɱ',
|
||||
n: 'ñ',
|
||||
o: 'ô',
|
||||
p: 'þ',
|
||||
q: 'ǫ',
|
||||
r: 'ŕ',
|
||||
s: 'š',
|
||||
t: 'ţ',
|
||||
u: 'û',
|
||||
v: 'ṽ',
|
||||
w: 'ŵ',
|
||||
x: 'ẋ',
|
||||
y: 'ý',
|
||||
z: 'ž',
|
||||
A: 'À',
|
||||
B: 'Ɓ',
|
||||
C: 'Ç',
|
||||
D: 'Ð',
|
||||
E: 'É',
|
||||
F: 'Ƒ',
|
||||
G: 'Ĝ',
|
||||
H: 'Ĥ',
|
||||
I: 'Î',
|
||||
L: 'Ļ',
|
||||
K: 'Ķ',
|
||||
J: 'Ĵ',
|
||||
M: 'Ṁ',
|
||||
N: 'Ñ',
|
||||
O: 'Ô',
|
||||
P: 'Þ',
|
||||
Q: 'Ǫ',
|
||||
R: 'Ŕ',
|
||||
S: 'Š',
|
||||
T: 'Ţ',
|
||||
U: 'Û',
|
||||
V: 'Ṽ',
|
||||
W: 'Ŵ',
|
||||
X: 'Ẋ',
|
||||
Y: 'Ý',
|
||||
Z: 'Ž',
|
||||
};
|
|
@ -19,9 +19,10 @@
|
|||
|
||||
import * as PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { IntlProvider, intlShape } from 'react-intl';
|
||||
|
||||
import * as i18n from '../core';
|
||||
import { isPseudoLocale, translateUsingPseudoLocale } from '../core/pseudo_locale';
|
||||
|
||||
/**
|
||||
* The library uses the provider pattern to scope an i18n context to a tree
|
||||
|
@ -29,12 +30,30 @@ import * as i18n from '../core';
|
|||
* IntlProvider should wrap react app's root component (inside each react render method).
|
||||
*/
|
||||
export class I18nProvider extends React.PureComponent {
|
||||
public static propTypes = {
|
||||
children: PropTypes.object,
|
||||
};
|
||||
public static propTypes = { children: PropTypes.element.isRequired };
|
||||
public static contextTypes = { intl: intlShape };
|
||||
public static childContextTypes = { intl: intlShape };
|
||||
|
||||
public getChildContext() {
|
||||
// if pseudo locale is set, default intl.formatMessage should be decorated
|
||||
// with the pseudo localization function
|
||||
if (this.context.intl && isPseudoLocale(i18n.getLocale())) {
|
||||
const formatMessage = this.context.intl.formatMessage;
|
||||
this.context.intl.formatMessage = (...args: any[]) => {
|
||||
return translateUsingPseudoLocale(formatMessage.apply(this.context.intl, args));
|
||||
};
|
||||
}
|
||||
|
||||
return { intl: this.context.intl };
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { children } = this.props;
|
||||
const child = React.Children.only(this.props.children);
|
||||
if (this.context.intl) {
|
||||
// We can have IntlProvider somewhere within ancestors so we just reuse it
|
||||
// and don't recreate with another IntlProvider
|
||||
return child;
|
||||
}
|
||||
|
||||
return (
|
||||
<IntlProvider
|
||||
|
@ -45,7 +64,12 @@ export class I18nProvider extends React.PureComponent {
|
|||
defaultFormats={i18n.getFormats()}
|
||||
textComponent={React.Fragment}
|
||||
>
|
||||
{children}
|
||||
{
|
||||
// We use `<I18nProvider>{child}</I18nProvider>` trick to decorate intl.formatMessage
|
||||
// in `getChildContext()` method. I18nProdiver will have `this.context.intl` so the
|
||||
// recursion won't be infinite
|
||||
}
|
||||
{isPseudoLocale(i18n.getLocale()) ? <I18nProvider>{child}</I18nProvider> : child}
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue