From c0d9410094d6a91d338e466b0914c4b5479ca944 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 19 Aug 2024 14:54:47 +1000 Subject: [PATCH] feat: create html stripper for i18n strings and add plurals support --- ts/util/i18n.ts | 77 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/ts/util/i18n.ts b/ts/util/i18n.ts index 835a6f4f7..82e6292c5 100644 --- a/ts/util/i18n.ts +++ b/ts/util/i18n.ts @@ -14,6 +14,7 @@ import { import timeLocales from 'date-fns/locale'; import { isUndefined } from 'lodash'; import { + DictionaryWithoutPluralStrings, GetMessageArgs, LocalizerDictionary, LocalizerToken, @@ -23,6 +24,7 @@ import { import { DURATION_SECONDS, LOCALE_DEFAULTS } from '../session/constants'; import { updateLocale } from '../state/ducks/dictionary'; import { GetNetworkTime } from '../session/apis/snode_api/getNetworkTime'; +import { Dictionary } from '../localization/locales'; export function loadDictionary(locale: Locale) { return import(`../../_locales/${locale}/messages.json`) as Promise; @@ -98,32 +100,30 @@ const timeLocaleMap = { export type Locale = keyof typeof timeLocaleMap; -const enPluralFormRegex = /\{(\w+), plural, one \{(\w+)\} other \{(\w+)\}\}/; - const cardinalPluralRegex: Record = { - zero: /(zero) \{([^}]*)\}/, - one: /(one) \{([^}]*)\}/, - two: /(two) \{([^}]*)\}/, - few: /(few) \{([^}]*)\}/, - many: /(many) \{([^}]*)\}/, - other: /(other) \{([^}]*)\}/, + zero: /zero \[(.*?)\]/g, + one: /one \[(.*?)\]/g, + two: /two \[(.*?)\]/g, + few: /few \[(.*?)\]/g, + many: /many \[(.*?)\]/g, + other: /other \[(.*?)\]/g, }; -function getPluralKey(string: PluralString): PluralKey | undefined { - const match = string.match(enPluralFormRegex); - return match && match[1] ? match[1] : undefined; +function getPluralKey(string: PluralString): R { + const match = /{(\w+), plural, one \[.+\] other \[.+\]}/g.exec(string); + return (match?.[1] ?? undefined) as R; } function getStringForCardinalRule( localizedString: string, cardinalRule: Intl.LDMLPluralRule ): string | undefined { - const match = localizedString.match(cardinalPluralRegex[cardinalRule]); - return match ? match[2] : undefined; + const match = cardinalPluralRegex[cardinalRule].exec(localizedString); + return match?.[1] ?? undefined; } const isPluralForm = (localizedString: string): localizedString is PluralString => - enPluralFormRegex.test(localizedString); + /{\w+, plural, one \[.+\] other \[.+\]}/g.test(localizedString); /** * Logs an i18n message to the console. @@ -237,19 +237,21 @@ export const setupi18n = (locale: Locale, dictionary: LocalizerDictionary) => { } /** Find and replace the dynamic variables in a localized string and substitute the variables with the provided values */ - // @ts-expect-error TODO: Fix this type, now that we have plurals it doesnt quite work - return localizedString.replace(/\{(\w+)\}/g, (match, arg: keyof typeof args) => { - const substitution: string | undefined = args?.[arg]; + return (localizedString as DictionaryWithoutPluralStrings[T]).replace( + /\{(\w+)\}/g, + (match, arg) => { + const substitution: string | undefined = args?.[arg as keyof typeof args]; - if (isUndefined(substitution)) { - const defaultSubstitution = LOCALE_DEFAULTS[arg as keyof typeof LOCALE_DEFAULTS]; + if (isUndefined(substitution)) { + const defaultSubstitution = LOCALE_DEFAULTS[arg as keyof typeof LOCALE_DEFAULTS]; - return isUndefined(defaultSubstitution) ? match : defaultSubstitution; - } + return isUndefined(defaultSubstitution) ? match : defaultSubstitution; + } - // TODO: figure out why is was type never and fix the type - return (substitution as string).toString(); - }) as R; + // TODO: figure out why is was type never and fix the type + return (substitution as string).toString(); + } + ) as R; } catch (error) { i18nLog(`i18n: ${error.message}`); return token as R; @@ -258,9 +260,36 @@ export const setupi18n = (locale: Locale, dictionary: LocalizerDictionary) => { window.getLocale = () => locale; + /** + * Retrieves a localized message string, substituting variables where necessary. Then strips the message of any HTML and custom tags. + * + * @param token - The token identifying the message to retrieve. + * @param args - An optional record of substitution variables and their replacement values. This is required if the string has dynamic variables. + * + * @returns The localized message string with substitutions applied. Any HTML and custom tags are removed. + * + * @example + * // The string greeting is 'Hello, {name}! Welcome!' in the current locale + * window.i18n.stripped('greeting', { name: 'Alice' }); + * // => 'Hello, Alice! Welcome!' + */ + getMessage.stripped = ( + ...[token, args]: GetMessageArgs + ): R => { + const i18nString = getMessage( + ...([token, args] as GetMessageArgs) + ); + + return i18nString.replaceAll(/<[^>]*>/g, '') as R; + }; + return getMessage; }; +export const getI18nFunction = (stripTags: boolean) => { + return stripTags ? window.i18n.stripped : window.i18n; +}; + // eslint-disable-next-line import/no-mutable-exports export let langNotSupportedMessageShown = false;