From fecf39eff44ae6601f12ebdcbb7be4f577b1b3c6 Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Fri, 13 Feb 2026 15:03:59 -0500 Subject: [PATCH] Only support single-digit placeholders in formatString (#33934) Often placeholders can appear next to an actual digit in the translated string (e.g. $1>0), which will appear on the front end as "$10". In order to interpret such strings correctly, we should only support single-digit placeholders by default. Support for multi-digit placeholders may be added in the future. In addition, formatString has been modified to write errors to the console instead of throw (which can take down the entire page). --- components/common/formatString.test.tsx | 25 +++++++++++++++++++------ components/common/formatString.ts | 6 +++--- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/components/common/formatString.test.tsx b/components/common/formatString.test.tsx index 7d7cae2fc91..4b2280b155e 100644 --- a/components/common/formatString.test.tsx +++ b/components/common/formatString.test.tsx @@ -57,18 +57,24 @@ describe('formatString', () => { }), 'People in NZ say Kiora') }) - it('should fail if a replacement does not exist', () => { - assert.throws(() => formatString('$1 in $2nz/$2 say $3', { + it('should log an error if a replacement does not exist', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation() + formatString('$1 in $2nz/$2 say $3', { $1: 'People', $2: (content) => content.toUpperCase(), $3: 'Kiora', $4: 'MISSING!' - })) + }) + expect(consoleSpy).toHaveBeenCalled() + consoleSpy.mockRestore() }) - it('should fail if a replacement does not exist using an array of replacements', () => { - assert.throws(() => formatString('$1 in $2nz/$2 say $3', - ['People', 'NZ', 'Kiora', 'MISSING!'])) + it('should log an error if a replacement does not exist using an array of replacements', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation() + formatString('$1 in $2nz/$2 say $3', + ['People', 'NZ', 'Kiora', 'MISSING!']) + expect(consoleSpy).toHaveBeenCalled() + consoleSpy.mockRestore() }) it('should not fail if a replacement does not exist if noErrorOnMissingReplacement is true', () => { @@ -192,5 +198,12 @@ describe('formatString', () => { $2: (content) => {content} }), <>Hello from me{" to "}{"me"}. bob says hi too) }) + + it('should only support single-digit placeholders ($1-$9)', () => { + // $10 should be treated as $1 followed by literal "0" + assert.equal(formatString('Hello $10', { + $1: 'world' + }), 'Hello world0') + }) }) diff --git a/components/common/formatString.ts b/components/common/formatString.ts index 7233038ef83..f8e73d6a53f 100644 --- a/components/common/formatString.ts +++ b/components/common/formatString.ts @@ -91,7 +91,7 @@ export function formatString( } const stack = [result] - const regex = /\/?\$(\d+)/gm + const regex = /\/?\$([1-9])/gm // Keep track of the keys we've seen, so we can throw an error if a key is // missing. @@ -116,7 +116,7 @@ export function formatString( if (stack.at(-1)!.key === key) { stack.pop() } else { - throw new Error(`Mismatched closing tag: ${tag} in message "${text}"`) + console.error(`Mismatched closing tag: ${tag} in message "${text}"`) } continue } @@ -155,7 +155,7 @@ export function formatString( // If we should throw an error for missing replacements check to see if they // were all present in our text. if (!options?.noErrorOnMissingReplacement && seen.size < Object.keys(replacements).length) { - throw new Error(`Missing replacements (${Object.keys(replacements).filter(key => !seen.has(key)).join(', ')} we not found in ${text})`) + console.error(`Missing replacements (${Object.keys(replacements).filter(key => !seen.has(key)).join(', ')} we not found in ${text})`) } const formatted = getReplacedContent(result)