@transcend-io/consent-manager-ui
Version:
Transcend Consent Manager reference consent UI
170 lines (153 loc) • 5.91 kB
text/typescript
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<TranslatedMessages> => {
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<string, string>;
} {
// The current language
const [language, setLanguage] =
useState<ConsentManagerSupportedTranslationValue>(() =>
// choose a default language based on the browser selected
pickDefaultLanguage(supportedLanguages),
);
// Hold the translations for that language (fetched async)
const [messages, setMessages] = useState<TranslatedMessages | undefined>();
// Store the HTML opening/closing tags we need to replace our tag variables with
const [htmlTagVariables, setHtmlTagVariables] = useState<
Record<string, string>
>({});
// 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 };
}