From 3b65f19d155c1d7887f9f110d544d6ddade858dc Mon Sep 17 00:00:00 2001 From: Cami <50341430+csmccarthy@users.noreply.github.com> Date: Wed, 9 Oct 2024 19:08:32 -0500 Subject: [PATCH] Csmccarthy/intl html substitution (#182) * substitute html tags with format.js variables --- package.json | 2 +- src/components/App.tsx | 4 ++-- src/hooks/useLanguage.ts | 23 ++++++++++++++++++----- src/utils/substitute-html.ts | 30 ++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 src/utils/substitute-html.ts diff --git a/package.json b/package.json index e2e049a3..16de241a 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://github.com/transcend-io/consent-manager-ui.git" }, "homepage": "https://github.com/transcend-io/consent-manager-ui", - "version": "4.18.1", + "version": "4.18.2", "license": "MIT", "main": "build/ui", "files": [ diff --git a/src/components/App.tsx b/src/components/App.tsx index 18308ecd..d362e1ed 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -69,7 +69,7 @@ export function App({ }); // Language setup - const { language, handleChangeLanguage, messages } = useLanguage({ + const { language, handleChangeLanguage, messages, htmlTagVariables } = useLanguage({ supportedLanguages, translationsLocation: // Order of priority: @@ -121,7 +121,7 @@ export function App({ {/** Ensure messages are loaded before any UI is displayed */} {messages ? (
void; /** Message translations */ messages: TranslatedMessages | undefined; + /** HTML opening/closing tab variables */ + htmlTagVariables: Record; } { // The current language const [language, setLanguage] = useState(() => @@ -173,22 +176,32 @@ export function useLanguage({ // 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>({}); + // Load the default translations useEffect(() => { - getTranslations(translationsLocation, language).then((messages) => - setMessages(messages), - ); + 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: ConsentManagerLanguageKey) => { const newMessages = await getTranslations(translationsLocation, language); - setMessages(newMessages); + + // 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 }; + return { language, handleChangeLanguage, messages, htmlTagVariables }; } diff --git a/src/utils/substitute-html.ts b/src/utils/substitute-html.ts new file mode 100644 index 00000000..6b14a470 --- /dev/null +++ b/src/utils/substitute-html.ts @@ -0,0 +1,30 @@ +import { TranslatedMessages } from '@transcend-io/internationalization'; + +/** + * Takes in a set of messages which may or may not contain HTML and replaces any + * HTML tags so that format.js can successfully internationalize them without + * running into parsing errors and using the fallback option + * + * @param messages - Raw precompiled messages (sometimes containing html tags) + * @returns Messages with all HTML opening/closing tags substituted and the variables to replace those substitutions + */ +export function substituteHtml(messages: TranslatedMessages): { + /** The set of messages with their HTML opening/closing tags substituted */ + substitutedMessages: TranslatedMessages, + /** The set of variables used to replace the substitutions with their corresponding HTML opening/closing tags */ + tagVariables: Record +} { + const substitutedMessages = { ...messages}; + const tagVariables: Record = {}; + Object.entries(substitutedMessages).forEach(([key, rawMessage]) => { + let placeholderMessage = rawMessage; + const htmlTags = [...rawMessage.matchAll(/<[^>]+>/g)].flat() + htmlTags.forEach((tag, idx) => { + const uniqKey = key.replaceAll('.', '_'); + placeholderMessage = placeholderMessage.replace(tag, `{tcm_${uniqKey}_tag_match_${idx}}`); + tagVariables[`tcm_${uniqKey}_tag_match_${idx}`] = tag + }); + substitutedMessages[key] = placeholderMessage; + }); + return { substitutedMessages, tagVariables }; +}