import React, { useContext, useState } from 'react'; import PropTypes from 'prop-types'; const DEFAULT_LANGUAGE = 'en'; const LANGUAGES_PATH = 'static/locales'; const LANGUAGES_FILE = 'translations.json'; const VARIABLE_REGEX = /\{([^}]+)\}/g; export const store = { /* dictionaries */ }; /** * Set a dictionary for a specific language * @param {String} lang language * @param {Object} data dictionary * @return {Object} dictionary added */ export const setDict = (lang, data) => (store[lang] = data); /** * Get a dictionary for specific language * @param {String} lang language * @return {Object} dictionary */ export const getDict = lang => store[lang]; /** * Fetch a dictionary for specific language defined in JSON format * @param {Strong} lang language * @return {Object} dictionary loaded */ export const fetchStore = async lang => { if (!store[lang]) { const res = await fetch(`/${LANGUAGES_PATH}/${lang}/${LANGUAGES_FILE}`); store[lang] = await res.json(); } return store[lang]; }; /** * Interpolates values given in 'params' * @param {String} str text * @param {Object} params object to interpolate * @return {String} */ const compile = (str = '', params) => { const matches = str.match(VARIABLE_REGEX); if (matches) { matches .map(v => v.replace(/\{|\}/g, '')) .forEach(v => (str = str.replace('{' + v + '}', params[v]))); } return str; }; /** * Reads an object value using dot notation for nested keys * @param {Object} obj object to parse * @param {String} skey key to search * @return {String|null} */ export const accessKey = (obj, skey = '') => skey.split('.').reduce((a, b) => a && a[b], obj); /** * Returns the translated text for given key and interpolates values in 'params' * @param {String} key the key to search for * @param {String} lang language * @param {Object} params object with params to interpolate * @return {[type]} [description] */ export const translateKey = (key, lang, params = {}) => compile(accessKey(getDict(lang), key)); // ===================== // REACT INTERFACE // ===================== /** * Context used to handle translation in the component tree */ const TranslateContext = React.createContext({ language: DEFAULT_LANGUAGE, changeLanguage: () => {} }); /** * Component provider to pass down context to child components * Must be used to wrap application */ export class Provider extends React.Component { changeLanguage = language => { fetchStore(language).then(() => { this.setState(state => ({ language })); }); }; state = { language: DEFAULT_LANGUAGE, changeLanguage: this.changeLanguage }; constructor(props) { super(props); if (props.store) { Object.keys(props.store).forEach(l => setDict(l, props.store[l])); } } render() { return ( {this.props.children} ); } } /** * HOC to provide 'changeLanguage' and 't' method to WrappedComponent */ export function withTranslation(WrappedComponent) { return function TranslatedComponent(props) { const { language, changeLanguage } = useContext(TranslateContext); const t = (k, l, p) => translateKey(k, l || language, p); return ; }; } /** * Component to translate a given key * If key is missing, render children without changes */ export const Trans = props => { const { language } = useContext(TranslateContext); const { i18nKey, lang, params, children } = props; return [translateKey(i18nKey, lang || language, params) || children]; }; Trans.proptypes = { i18nKey: PropTypes.string.isRequired, lang: PropTypes.string, params: PropTypes.object };