packages/lib/i18n/i18n.ts (153 lines of code) (raw):
type PluralsFunction = (n: number) => number;
type TranslationValue = string | string[];
type ContextualTranslation = Record<string, TranslationValue>;
type Dictionary = Record<string, TranslationValue | ContextualTranslation>;
/**
* Taken from https://github.com/JetBrains/hub-dashboard-addons/blob/master/components/localization/src/localization.js
*/
class I18n {
static GET_PLURALS_MAP: Record<string, PluralsFunction> = {
ru: (n: number): number => {
if (n % 10 === 1 && n % 100 !== 11) {
return 0;
}
return (n % 10 >= 2) && (n % 10 <= 4) && (n % 100 < 10 || n % 100 >= 20)
? 1
: 2;
},
fr: (n: number): number => (n > 1 ? 1 : 0),
ja: (): number => 0,
OTHERS: (n: number): number => (n !== 1 ? 1 : 0)
};
static DEFAULT_CONTEXT = '$$noContext';
private dictionary: Dictionary;
private getPlurals: PluralsFunction;
constructor() {
this.dictionary = {};
this.getPlurals = I18n.GET_PLURALS_MAP.OTHERS!;
}
setTranslations(lang: string, dictionary?: Dictionary): void {
this.dictionary = dictionary || {};
this.getPlurals = I18n.GET_PLURALS_MAP[lang] ?? I18n.GET_PLURALS_MAP.OTHERS!;
}
interpolate(text: string, interpolationObject?: Record<string, string | number>): string {
if (!interpolationObject || !Object.keys(interpolationObject).length) {
return text;
}
const substringsForReplacing =
getSubstringsForReplacing(text, interpolationObject);
let resultText = text;
Object.keys(substringsForReplacing).forEach(key => {
if (substringsForReplacing[key] !== undefined) {
resultText = resultText.replace(key, String(substringsForReplacing[key]));
}
});
return resultText;
function getSubstringsForReplacing(str: string, interpolationValues: Record<string, string | number>): Record<string, string | number> {
let currentInterpolatedFragmentStart = -1;
const substringToValueMap: Record<string, string | number> = {};
for (let i = 0; i < (str.length - 1); ++i) {
if (str[i] === '{' && str[i + 1] === '{') {
currentInterpolatedFragmentStart = i + 2;
i = currentInterpolatedFragmentStart;
} else if (str[i] === '}' && str[i + 1] === '}' &&
currentInterpolatedFragmentStart > 0) {
const variableName = str.substring(
currentInterpolatedFragmentStart, i
);
const value = interpolationValues[variableName.trim()];
if (value !== undefined) {
substringToValueMap[`{{${variableName}}}`] = value;
}
}
}
return substringToValueMap;
}
}
getTranslationString(text: string, numberForPlurals?: number, context?: string): string | undefined {
const contexts = this.dictionary[text];
if (!contexts) {
return undefined;
}
if (typeof contexts === 'string' || Array.isArray(contexts)) {
// Direct translation value or array of plurals
if (typeof contexts === 'string') {
return contexts;
}
if (Array.isArray(contexts)) {
const pluralFormId = this.getPlurals(
Number.isInteger(numberForPlurals) ? numberForPlurals! : 1
);
return contexts[pluralFormId];
}
} else {
// Contextual translation object
const currentTranslation = contexts[context || I18n.DEFAULT_CONTEXT] || contexts;
if (typeof currentTranslation === 'string') {
return currentTranslation;
}
if (Array.isArray(currentTranslation)) {
const pluralFormId = this.getPlurals(
Number.isInteger(numberForPlurals) ? numberForPlurals! : 1
);
return currentTranslation[pluralFormId];
}
}
return undefined;
}
translate(text: string, interpolationObject?: Record<string, string | number>, numberForPlurals?: number, context?: string): string {
const currentTranslation = this.getTranslationString(
text, numberForPlurals, context
);
return this.interpolate(
currentTranslation || text, interpolationObject
);
}
translatePlural(
count: number, textForUnique: string, textForPlural: string, interpolationObject?: Record<string, string | number>, context?: string
): string {
debugger
const currentTranslation = this.getTranslationString(
textForUnique,
count,
context
);
const stringToInterpolate = currentTranslation ||
(count === 1 ? textForUnique : textForPlural);
return this.interpolate(
stringToInterpolate,
Object.assign({$count: count}, interpolationObject || {})
);
}
}
const I18N_INSTANCE = new I18n();
type I18nFunction = {
(text: string, interpolationObject?: Record<string, string | number>, numberForPlurals?: number, context?: string): string;
plural: (count: number, textForUnique: string, textForPlural: string, interpolationObject?: Record<string, string | number>, context?: string) => string;
};
export const i18n = I18N_INSTANCE.translate.bind(I18N_INSTANCE) as I18nFunction;
i18n.plural = I18N_INSTANCE.translatePlural.bind(I18N_INSTANCE);
// Declare fecha interface to avoid type errors
interface FechaI18n {
dayNamesShort: string[];
dayNames: string[];
monthNamesShort: string[];
monthNames: string[];
amPm: string[];
}
interface FechaMasks {
datePresentation: string;
dateAndTimePresentation: string;
}
interface Fecha {
i18n: FechaI18n;
masks: FechaMasks;
}
// @deprecated Use another date library
export function configureFecha(fecha: Fecha): void {
if (typeof fecha !== 'undefined') {
fecha.i18n.dayNamesShort = i18n('Sun|Mon|Tue|Wed|Thu|Fri|Sat').split('|');
fecha.i18n.dayNames = i18n('Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday').split('|');
fecha.i18n.monthNamesShort = i18n('Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec').split('|');
fecha.i18n.monthNames = i18n('January|February|March|April|May|June|July|August|September|October|November|December').split('|');
fecha.i18n.amPm = i18n('am|pm').split('|');
fecha.masks.datePresentation = 'DD MMM YYYY';
fecha.masks.dateAndTimePresentation = 'DD MMM YYYY HH:MM';
}
}
export function setLocale(lang: string, translations: Record<string, unknown>): void {
I18N_INSTANCE.setTranslations(lang, translations[lang] as Dictionary);
}