import { isEmpty } from 'lodash'; import { CrowdinLocale } from './constants'; import { getMessage } from '../util/i18n/functions/getMessage'; import { pluralsDictionary, simpleDictionary } from './locales'; export type SimpleDictionary = typeof simpleDictionary; export type PluralDictionary = typeof pluralsDictionary; export type SimpleLocalizerTokens = keyof SimpleDictionary; export type PluralLocalizerTokens = keyof PluralDictionary; export type MergedLocalizerTokens = SimpleLocalizerTokens | PluralLocalizerTokens; type Logger = (message: string) => void; let logger: Logger | undefined; export function setLogger(cb: Logger) { if (logger) { // eslint-disable-next-line no-console console.log('logger already initialized'); } logger = cb; } function log(message: Parameters[0]) { logger?.(message); } export function isSimpleToken(token: string): token is SimpleLocalizerTokens { return token in simpleDictionary; } export function isPluralToken(token: string): token is PluralLocalizerTokens { return token in pluralsDictionary; } type TokenWithArgs = { [K in keyof D]: D[K] extends { args: undefined } | { args: never } ? never : K; }[keyof D]; type MergedTokenWithArgs = TokenWithArgs | TokenWithArgs; export function isTokenWithArgs(token: string): token is MergedTokenWithArgs { return ( (isSimpleToken(token) && !isEmpty(simpleDictionary[token]?.args)) || (isPluralToken(token) && !isEmpty(pluralsDictionary[token]?.args)) ); } type DynamicArgStr = 'string' | 'number'; export type LocalizerDictionary = SimpleDictionary; type ArgsTypeStrToTypes = T extends 'string' ? string : T extends 'number' ? number : never; // those are still a string of the type "string" | "number" and not the typescript types themselves type ArgsFromTokenStr = T extends SimpleLocalizerTokens ? SimpleDictionary[T] extends { args: infer A } ? A extends Record ? A : never : never : T extends PluralLocalizerTokens ? PluralDictionary[T] extends { args: infer A } ? A extends Record ? A : never : never : never; export type ArgsFromToken = MappedToTsTypes>; /** The arguments for retrieving a localized message */ export type GetMessageArgs = T extends MergedLocalizerTokens ? T extends MergedTokenWithArgs ? [T, ArgsFromToken] : [T] : never; type MappedToTsTypes> = { [K in keyof T]: ArgsTypeStrToTypes; }; /** NOTE: Because of docstring limitations changes MUST be manually synced between {@link setupI18n.inEnglish } and {@link window.i18n.inEnglish } */ /** * Retrieves a message string in the {@link en} locale, substituting variables where necessary. * * NOTE: This does not work for plural strings. This function should only be used for debug and * non-user-facing strings. Plural string support can be added splitting out the logic for * {@link setupI18n.formatMessageWithArgs} and creating a new getMessageFromDictionary, which * specifies takes a dictionary as an argument. This is left as an exercise for the reader. * @deprecated this will eventually be replaced by LocalizedStringBuilder * * @param token - The token identifying the message to retrieve. * @param args - An optional record of substitution variables and their replacement values. This is equired if the string has dynamic variables. */ export function inEnglish([token, args]: GetMessageArgs) { if (!isSimpleToken(token)) { throw new Error('inEnglish only supports simple strings for now'); } const rawMessage = simpleDictionary[token].en; if (!rawMessage) { log(`Attempted to get forced en string for nonexistent key: '${token}' in fallback dictionary`); return token; } return formatMessageWithArgs(rawMessage, args); } /** * Retrieves a localized message string, substituting variables where necessary. Then strips the message of any HTML and custom tags. * * @deprecated This will eventually be replaced altogether by LocalizedStringBuilder * * @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. */ export function stripped( ...[token, args]: GetMessageArgs ): string { const sanitizedArgs = args ? sanitizeArgs(args, '\u200B') : undefined; const i18nString = getMessage(...([token, sanitizedArgs] as GetMessageArgs)); const strippedString = i18nString.replaceAll(/<[^>]*>/g, ''); return deSanitizeHtmlTags(strippedString, '\u200B'); } /** * Sanitizes the args to be used in the i18n function * @param args The args to sanitize * @param identifier The identifier to use for the args. Use this if you want to de-sanitize the args later. * @returns The sanitized args */ export function sanitizeArgs( args: Record, identifier?: string ): Record { return Object.fromEntries( Object.entries(args).map(([key, value]) => [ key, typeof value === 'string' ? sanitizeHtmlTags(value, identifier) : value, ]) ); } /** * Formats a localized message string with arguments and returns the formatted string. * @param rawMessage - The raw message string to format. After using @see {@link getRawMessage} to get the raw string. * @param args - An optional record of substitution variables and their replacement values. This * is required if the string has dynamic variables. This can be optional as a strings args may be defined in @see {@link LOCALE_DEFAULTS} * * @returns The formatted message string. * * @deprecated * */ export function formatMessageWithArgs( rawMessage: string, args?: ArgsFromToken ): string { /** Find and replace the dynamic variables in a localized string and substitute the variables with the provided values */ return rawMessage.replace(/\{(\w+)\}/g, (match: any, arg: string) => { const matchedArg = args ? args[arg as keyof typeof args] : undefined; return matchedArg?.toString() ?? match; }); } /** * Retrieves a localized message string, without substituting any variables. This resolves any plural forms using the given args * @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. * * NOTE: This is intended to be used to get the raw string then format it with {@link formatMessageWithArgs} */ export function getRawMessage( crowdinLocale: CrowdinLocale, ...[token, args]: GetMessageArgs ): string { try { if ( typeof window !== 'undefined' && window?.sessionFeatureFlags?.replaceLocalizedStringsWithKeys ) { return token as T; } if (isSimpleToken(token)) { return simpleDictionary[token][crowdinLocale]; } if (!isPluralToken(token)) { throw new Error('invalid token, neither simple nor plural'); } const pluralsObjects = pluralsDictionary[token]; const localePluralsObject = pluralsObjects[crowdinLocale]; if (!localePluralsObject || isEmpty(localePluralsObject)) { log(`Attempted to get translation for nonexistent key: '${token}'`); return token; } const num = args && 'count' in args ? args.count : 0; const cardinalRule = new Intl.PluralRules(crowdinLocale).select(num); const pluralString = getStringForRule({ dictionary: pluralsDictionary, crowdinLocale, cardinalRule, token, }); if (!pluralString) { log(`Plural string not found for cardinal '${cardinalRule}': '${pluralString}'`); return token as T; } return pluralString.replaceAll('#', `${num}`); } catch (error) { log(error.message); return token as T; } } export function getStringForRule({ dictionary, token, crowdinLocale, cardinalRule, }: { dictionary: PluralDictionary; token: PluralLocalizerTokens; crowdinLocale: CrowdinLocale; cardinalRule: Intl.LDMLPluralRule; }) { const dictForLocale = dictionary[token][crowdinLocale]; return cardinalRule in dictForLocale ? ((dictForLocale as any)[cardinalRule] as string) : token; } /** * Replaces all html tag identifiers with their escaped equivalents * @param str The string to sanitize * @param identifier The identifier to use for the args. Use this if you want to de-sanitize the args later. * @returns The sanitized string */ export function sanitizeHtmlTags(str: string, identifier: string = ''): string { if (identifier && /[a-zA-Z0-9>/g, `${identifier}>${identifier}`); } /** * Replaces all sanitized html tags with their real equivalents * @param str The string to de-sanitize * @param identifier The identifier used when the args were sanitized * @returns The de-sanitized string */ export function deSanitizeHtmlTags(str: string, identifier: string): string { if (!identifier || /[a-zA-Z0-9>'); }