From e321729702b117c8cfea5ae861cf3767030cc9c4 Mon Sep 17 00:00:00 2001 From: Timofei Iatsenko Date: Thu, 15 Feb 2024 08:52:28 +0100 Subject: [PATCH] fix: don't replace octothorpe coming from variable (#1850) --- packages/core/src/interpolate.test.ts | 19 +++++++++ packages/core/src/interpolate.ts | 57 +++++++++++++++++++-------- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/packages/core/src/interpolate.test.ts b/packages/core/src/interpolate.test.ts index 05e2adbb7..7b385f8fb 100644 --- a/packages/core/src/interpolate.test.ts +++ b/packages/core/src/interpolate.test.ts @@ -58,6 +58,25 @@ describe("interpolate", () => { expect(plural({ value: 2 })).toEqual("2 Books") }) + it("should not replace `#` symbol passed in the variable in the jsx expression", () => { + const plural = prepare( + "{value, plural, one {There is a notification in <1>{documentTitle}} other {There are # notifications in <1>{documentTitle}}}" + ) + + expect(plural({ value: 1, documentTitle: "Title #1" })).toEqual( + "There is a notification in <1>Title #1" + ) + expect(plural({ value: 2, documentTitle: "Title #1" })).toEqual( + "There are 2 notifications in <1>Title #1" + ) + }) + + it("should replace more than one octothorpe symbols in message", () => { + const plural = prepare("{value, plural, one {} other {# and #}}") + + expect(plural({ value: 2 })).toEqual("2 and 2") + }) + it("when a value is defined (even when empty) plural will return it. Conversely, if a value is not defined, plural defaults to 'other'", () => { const plural = prepare("{value, plural, =0 {} other {#% discount}}") expect(plural({ value: 0 })).toEqual("") diff --git a/packages/core/src/interpolate.ts b/packages/core/src/interpolate.ts index cc6318537..ef58ed0a5 100644 --- a/packages/core/src/interpolate.ts +++ b/packages/core/src/interpolate.ts @@ -2,9 +2,12 @@ import { CompiledMessage, Formats, Locales, Values } from "./i18n" import { date, number, plural, type PluralOptions } from "./formats" import { isString } from "./essentials" import { unraw } from "unraw" +import { CompiledIcuChoices } from "@lingui/message-utils/compileMessage" export const UNICODE_REGEX = /\\u[a-fA-F0-9]{4}|\\x[a-fA-F0-9]{2}/g +const OCTOTHORPE_PH = "%__lingui_octothorpe__%" + const getDefaultFormats = ( locale: string, passedLocales?: Locales, @@ -22,7 +25,7 @@ const getDefaultFormats = ( ? style("number") : undefined const valueStr = number(locales, value, numberFormat) - return message.replace("#", valueStr) + return message.replace(new RegExp(OCTOTHORPE_PH, "g"), valueStr) } return { @@ -51,16 +54,12 @@ const getDefaultFormats = ( value: string, format: string | Intl.DateTimeFormatOptions ): string => date(locales, value, style(format)), - - undefined: undefinedFormatter, } as const } const selectFormatter = (value: string, rules: Record) => rules[value] ?? rules.other -const undefinedFormatter = (value: unknown) => value - /** * @param translation compiled message * @param locale Locale of message @@ -78,26 +77,50 @@ export function interpolate( return (values: Values = {}, formats?: Formats): string => { const formatters = getDefaultFormats(locale, locales, formats) - const formatMessage = (message: CompiledMessage | number | undefined) => { - if (!Array.isArray(message)) return message + const formatMessage = (tokens: CompiledMessage | number | undefined) => { + if (!Array.isArray(tokens)) return tokens - return message.reduce((message, token) => { - if (isString(token)) return message + token + return tokens.reduce((message, token) => { + if (token === "#") { + return message + OCTOTHORPE_PH + } + + if (isString(token)) { + return message + token + } const [name, type, format] = token let interpolatedFormat: any = {} - if (format != null && typeof format === "object") { - Object.entries(format).forEach(([key, value]) => { - interpolatedFormat[key] = formatMessage(value) - }) + + if ( + type === "plural" || + type === "selectordinal" || + type === "select" + ) { + Object.entries(format as CompiledIcuChoices).forEach( + ([key, value]) => { + interpolatedFormat[key] = formatMessage(value) + } + ) } else { interpolatedFormat = format } - // @ts-ignore TS2538: Type undefined cannot be used as an index type. - const formatter = formatters[type] - const value = formatter(values[name], interpolatedFormat) - if (value == null) return message + + let value: unknown + + if (type) { + // run formatter, such as plural, number, etc. + const formatter = (formatters as any)[type] + value = formatter(values[name], interpolatedFormat) + } else { + // simple placeholder variable interpolation eq {variableName} + value = values[name] + } + + if (value == null) { + return message + } return message + value }, "")