react-native-localize-ext
Version:
make i18n in react-native much smoother
284 lines (219 loc) • 6.94 kB
text/typescript
interface Language<T> {
key: string;
values: T;
}
export interface UnListen {
(): void;
}
export interface I18nConfig<T> {
defaultLocale: Language<T>;
loader?: (localName: string) => Promise<any>;
}
export class I18n<U extends object = object, T = object> {
protected readonly defaultLocale: U;
protected readonly loader: I18nConfig<U>['loader'];
protected locales: Partial<Record<string, U>> = {};
protected hashes: Partial<Record<string, string>> = {};
// @ts-ignore
protected current: U = {};
protected loadings: string[] = [];
protected currentName: string = '';
protected listeners: ((localeName: string) => void)[] = [];
protected caches: Partial<Record<string, any>> = {};
private static CACHE_ROOT_KEY = '_._i18n_root_._';
constructor(config: I18nConfig<U>) {
this.defaultLocale = config.defaultLocale.values;
this.loader = config.loader;
this.define(config.defaultLocale.key, config.defaultLocale.values).locale(config.defaultLocale.key);
}
public define(localeName: string, values: U): this {
this.locales[localeName] = values;
return this;
}
public getLocaleName(): string {
return this.currentName;
}
public async locale(name: string): Promise<void> {
const resolved = new Promise<void>((resolve) => resolve());
if (name === this.currentName) {
this.clearLoading();
return resolved;
}
if (this.isLoading(name)) {
// Make sure this locale can publish
this.loadings.push(name);
return resolved;
}
const language = this.locales[name];
if (language) {
this.publish(name, language);
this.clearLoading();
return resolved;
}
if (this.loader) {
this.loadings.push(name);
try {
const response = await this.loader(name);
let locale: U;
if (response && response.__esModule) {
locale = response.default;
if (!this.isValidLocale(locale)) {
throw new TypeError(`The locale named "${name}" has no default export`);
}
} else {
locale = response;
if (!this.isValidLocale(locale)) {
throw new TypeError(`The locale data named "${name}" is invalid`);
}
}
this.define(name, locale);
if (this.canPublishFromLoader(name)) {
this.publish(name, locale);
}
this.clearLoading();
} catch (error) {
// Fallback to previous locale name (if have)
this.removeLoading(name);
throw error;
}
return;
}
return new Promise((_, reject) => reject(new ReferenceError(`I18n can't find locale "${name}"`)));
}
public listen(fn: (localeName: string) => void): UnListen {
this.listeners.push(fn);
return () => {
this.listeners = this.listeners.filter((item) => item !== fn);
};
}
public t(key: string): any {
if (this.caches[key]) {
return this.caches[key];
}
const properties: string[] = key.split('.');
const firstProperty = properties.shift()!;
let result = this.chain()[firstProperty];
if (properties.length) {
let index = 0;
for (; index < properties.length; ++index) {
result = result[properties[index]];
if (result === undefined) {
break;
}
}
if (result === undefined) {
result = this.notFound(key);
}
}
return result;
}
public chain(): T {
const key = I18n.CACHE_ROOT_KEY;
if (!this.caches[key]) {
this.caches[key] = this.proxy(this.current, [], false);
}
return this.caches[key];
}
protected proxy(data: object, allProperties: string[], useDefaultLocal: boolean) {
if (Array.isArray(data)) {
return (params: object): string => {
let message: string = data[0];
const defaultParams: object = data[1];
const newParams = Object.assign({}, defaultParams, params);
for (const key of Object.keys(newParams)) {
let replaceValue;
if (typeof newParams[key] === 'function') {
// Indeed, it's assigned from defaultParams.
// It means that user doesn't input the value of this key, it only happens when Function has a default value.
replaceValue = newParams[key]();
} else if (typeof defaultParams[key] === 'function') {
replaceValue = defaultParams[key](newParams[key]);
} else {
replaceValue = newParams[key];
}
message = message.replace(new RegExp(`\{\{${key}\}\}`, 'gm'), replaceValue);
}
return message;
};
}
if (typeof data === 'object') {
return this.createProxy(data, allProperties, useDefaultLocal);
}
return data;
}
protected createProxy(data: object, allProperties: string[], useDefaultLocal: boolean) {
return new Proxy(data, {
get: (target, property) => {
if (!this.isValidProperty(property)) {
return undefined;
}
return this.getProxyData(target, allProperties, property, useDefaultLocal);
},
});
}
protected getProxyData(target: object, allProperties: string[], property: string, useDefaultLocal: boolean): any {
const newAllProperties = allProperties.concat(property);
const cacheKey = newAllProperties.join('.');
let result: any;
if (this.caches[cacheKey]) {
return this.caches[cacheKey];
}
let proxyData = target[property];
if (proxyData === undefined) {
if (useDefaultLocal) {
proxyData = this.notFound(newAllProperties);
} else {
// Fallback to default locale
proxyData = this.recursiveDefaultData(newAllProperties);
if (proxyData === undefined) {
proxyData = this.notFound(newAllProperties);
} else {
// Found key in default locale
useDefaultLocal = true;
}
}
}
result = this.proxy(proxyData, newAllProperties, useDefaultLocal);
this.caches[cacheKey] = result;
return result;
}
protected recursiveDefaultData(allProperties: string[]) {
let proxyData = this.defaultLocale;
for (const name of allProperties) {
proxyData = proxyData[name];
if (proxyData === undefined) {
break;
}
}
return proxyData;
}
protected notFound(properties: string | string[]): string {
const data = typeof properties === 'string' ? properties : properties.join('.');
console.error(`I18n can't find property "${data}"`);
return data;
}
protected isValidProperty(property: string | number | symbol): property is string {
return property !== '$$typeof' && typeof property === 'string';
}
protected isValidLocale(locale: U): boolean {
return locale !== null && typeof locale === 'object';
}
protected isLoading(name: string): boolean {
return this.loadings.indexOf(name) >= 0;
}
protected canPublishFromLoader(name: string): boolean {
return this.loadings.length > 0 && this.loadings[this.loadings.length - 1] === name;
}
protected removeLoading(name: string): void {
this.loadings = this.loadings.filter((item) => item !== name);
}
protected clearLoading(): void {
this.loadings = [];
}
protected publish(name: string, values: U): void {
this.currentName = name;
this.current = values;
this.caches = {};
this.listeners.forEach((listener) => listener(this.currentName));
}
}