import { useCallback, useEffect, useState } from 'preact/hooks'; import { ConsentManagerSupportedTranslationValue, LocaleValue, LOCALE_KEY, LOCALE_TRANSLATION_MAP, TranslatedMessages, Translations, } from '@transcend-io/internationalization'; import { settings } from '../settings'; import { getUserLocales } from '../utils/get-user-locales'; import { substituteHtml } from '../utils/substitute-html'; import { invertSafe } from '@transcend-io/type-utils'; export const loadedTranslations: Translations = Object.create(null); /** Mapping of AWS base translation keys to list of Transcend locales that should use them */ export const INVERTED_TRANSLATE_LOCALE = invertSafe(LOCALE_TRANSLATION_MAP); const getDuplicativeLocales = (lang: LocaleValue): LocaleValue[] => INVERTED_TRANSLATE_LOCALE[LOCALE_TRANSLATION_MAP[lang]]; /** * Get nearest matching locale from a list of supported locales * * @param preferred - Sorted locale list in order of most preferable to least preferable * @param supported - List of supported locales to match from * @returns Nearest supported locale, sorted by preferred locale list */ export const getNearestSupportedLocale = ( preferred: LocaleValue[], supported: LocaleValue[], ): LocaleValue | undefined => { // eslint-disable-next-line no-restricted-syntax for (const preferredLocale of preferred) { if (supported.includes(preferredLocale)) { return preferredLocale; } } return undefined; }; /** * Picks a default language for the user * * @param supportedLocales - Set of supported locales * @returns the language key of the best default language for this user */ export function pickDefaultLanguage( supportedLocales: ConsentManagerSupportedTranslationValue[], ): ConsentManagerSupportedTranslationValue { if (settings.locale && supportedLocales.includes(settings.locale)) { return settings.locale; } const preferredLocales = getUserLocales(); /* We should refactor this ASAP TODO: https://transcend.height.app/T-39777 * Extend supportedLanguages to include locales that we consider equivalent * e.g. instead of just having en, include en-US, en-GB, en-AU, etc */ const extendedSupportedLanguages = supportedLocales.flatMap((lang) => getDuplicativeLocales(lang), ); const nearestExtendedLanguage = getNearestSupportedLocale(preferredLocales, extendedSupportedLanguages) || LOCALE_KEY.En; let nearestTranslation = nearestExtendedLanguage; if (!(supportedLocales as string[]).includes(nearestTranslation)) { nearestTranslation = getDuplicativeLocales(nearestTranslation).find((lang) => supportedLocales.includes( lang as ConsentManagerSupportedTranslationValue, ), ) ?? LOCALE_KEY.En; } return nearestTranslation as ConsentManagerSupportedTranslationValue; } /** * Fetch message translations * * @param translationsLocation - Base path to fetching messages * @param language - Language to fetch * @returns The translations */ export const getTranslations = async ( translationsLocation: string, language: ConsentManagerSupportedTranslationValue, ): Promise => { loadedTranslations[language] ??= await (async () => { const pathToFetch = `${translationsLocation}/${language}.json`; const response = await fetch(pathToFetch); if (!response.ok) { throw new Error(`Failed to load translations for language ${language}`); } return response.json(); })(); return loadedTranslations[language]; }; /** * Sets the language to use in translator * * @param options - Options * @returns the language and a change language callback */ export function useLanguage({ supportedLanguages, translationsLocation, }: { /** Set of supported languages */ supportedLanguages: ConsentManagerSupportedTranslationValue[]; /** Base path to fetching messages */ translationsLocation: string; }): { /** The language in use */ language: ConsentManagerSupportedTranslationValue; /** A change language callback */ handleChangeLanguage: ( language: ConsentManagerSupportedTranslationValue, ) => void; /** Message translations */ messages: TranslatedMessages | undefined; /** HTML opening/closing tab variables */ htmlTagVariables: Record; } { // The current language const [language, setLanguage] = useState(() => // choose a default language based on the browser selected pickDefaultLanguage(supportedLanguages), ); // Hold the translations for that language (fetched async) const [messages, setMessages] = useState(); // Store the HTML opening/closing tags we need to replace our tag variables with const [htmlTagVariables, setHtmlTagVariables] = useState< Record >({}); // Load the default translations useEffect(() => { getTranslations(translationsLocation, language).then((messages) => { // Replace raw HTML tags with variables bc raw HTML causes parsing errors const { substitutedMessages, tagVariables } = substituteHtml(messages); setHtmlTagVariables(tagVariables); setMessages(substitutedMessages); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleChangeLanguage = useCallback( async (language: ConsentManagerSupportedTranslationValue) => { const newMessages = await getTranslations(translationsLocation, language); // Replace raw HTML tags with variables bc raw HTML causes parsing errors const { substitutedMessages, tagVariables } = substituteHtml(newMessages); setMessages(substitutedMessages); setHtmlTagVariables(tagVariables); setLanguage(language); }, [setLanguage, translationsLocation], ); return { language, handleChangeLanguage, messages, htmlTagVariables }; }