lr-i18n
Version:
Allows to connect your `Laravel` Framework localization files with `React`.
214 lines (183 loc) • 6.08 kB
text/typescript
import { createElement, useEffect, useState } from 'react';
import { Context } from './context';
import type DefaultOptionsInterface from './interfaces/default-options';
import type I18nProviderProps from './interfaces/i18n-provider-props';
import type ReplacementsInterface from './interfaces/replacements';
import pluralization from './utils/pluralization';
import recognizer from './utils/recognizer';
import replacer from './utils/replacer';
import resolver from './utils/resolver';
/**
*
*/
const isServer = typeof window === 'undefined';
/**
* Map object for translations.
*/
const translation = new Map();
/**
* Get document lang meta from HTML.
*/
const documentLang =
typeof document !== 'undefined' ? document?.documentElement?.lang?.replace('-', '_') || 'en' : 'en';
/**
* The default options.
*/
const defaultOptions: DefaultOptionsInterface = {
locale: documentLang,
fallbackLocale: documentLang,
prevLocale: documentLang,
files: {}
};
/**
* Laravel React I18n Provider:
*/
export default function LaravelReactI18nProvider({ children, ssr, ...currentOptions }: I18nProviderProps) {
const [isFirstRender, setIsFirstRender] = useState<boolean>(true);
const [loading, setLoading] = useState<boolean>(!isServer);
const [options, setOptions] = useState<DefaultOptionsInterface>({
...defaultOptions,
...currentOptions
});
const { getLocales, isLocale } = recognizer(options.files);
// Determine if files are eagerly loaded.
const filesAreEagerlyLoaded = Object.values(options.files).every(
(value) => typeof value === 'object' && value !== null
);
const { locale, fallbackLocale } = options;
if (!translation.get(locale)) {
if (filesAreEagerlyLoaded) {
fetchLocaleSync(locale);
} else if (isServer) {
fetchLocaleServer(locale);
}
}
if (locale !== fallbackLocale && !translation.get(fallbackLocale)) {
if (filesAreEagerlyLoaded) {
fetchLocaleSync(fallbackLocale);
} else if (isServer) {
fetchLocaleServer(fallbackLocale);
}
}
useEffect(() => {
if (!filesAreEagerlyLoaded) {
if (!translation.get(locale)) fetchLocaleClient(locale);
if (locale !== fallbackLocale && !translation.get(fallbackLocale)) fetchLocaleClient(fallbackLocale);
}
}, [options.locale]);
function fetchLocaleSync(locale: string): void {
const responses = resolver(options.files, locale);
for (const response of responses) {
translation.set(locale, {
...(translation.get(locale) || {}),
...response.default
});
}
}
/**
* Initialise translations for server.
*/
if (isServer) {
const { locale, fallbackLocale } = options;
if (!translation.get(locale)) fetchLocaleServer(locale);
if (locale !== fallbackLocale && !translation.get(fallbackLocale)) fetchLocaleServer(fallbackLocale);
}
/**
* Fetching locale for client side.
*/
function fetchLocaleClient(locale: string): void {
const promises = resolver(options.files, locale);
setLoading(true);
Promise.all(promises)
.then((responses) => {
for (const response of responses) {
translation.set(locale, {
...(translation.get(locale) || {}),
...response.default
});
}
})
.then(() => {
if (isFirstRender) setIsFirstRender(false);
setLoading(false);
});
}
/**
* Fetching locale for server side.
*/
function fetchLocaleServer(locale: string): void {
const responses = resolver(options.files, locale);
for (const response of responses) {
translation.set(locale, {
...(translation.get(locale) || {}),
...response.default
});
}
}
/**
* Get the translation for the given key.
*/
function t(key: string, replacements: ReplacementsInterface = {}): string {
const { locale, fallbackLocale, prevLocale } = options;
function getNestedValue(obj: any, keyPath: string): any {
return keyPath.split('.').reduce((acc, part) => (acc && acc[part] !== undefined ? acc[part] : undefined), obj);
}
let message = getNestedValue(translation.get(fallbackLocale) || {}, key) || key;
if (isLocale(locale)) {
if (getNestedValue(translation.get(locale) || {}, key)) {
message = getNestedValue(translation.get(locale), key);
} else if (getNestedValue(translation.get(prevLocale) || {}, key)) {
message = getNestedValue(translation.get(prevLocale), key);
} else if (getNestedValue(translation.get(fallbackLocale) || {}, key)) {
message = getNestedValue(translation.get(fallbackLocale), key);
}
}
return replacer(message, replacements);
}
/**
* Translates the given message based on a count.
*/
function tChoice(key: string, number: number, replacements: ReplacementsInterface = {}): string {
const message = t(key, replacements);
const locale = isLocale(options.locale) ? options.locale : options.fallbackLocale;
return replacer(pluralization(message, number, locale), {
...replacements,
count: number.toString()
});
}
/**
* Set locale.
*/
function setLocale(locale: string) {
if (!isServer) {
// When setting the HTML lang attribute, hyphen must be use instead of underscore.
document.documentElement.setAttribute('lang', locale.replace('_', '-'));
}
setOptions((prevState) => ({
...options,
locale,
prevLocale: prevState.locale
}));
}
/**
* Current locale.
*/
function currentLocale(): string {
return options.locale || options.fallbackLocale;
}
return createElement(
Context.Provider,
{
value: {
t,
tChoice,
loading,
isLocale,
getLocales,
currentLocale,
setLocale
}
},
children
);
}