Only support single-digit placeholders in formatString (#33934)

Often placeholders can appear next to an actual digit in the
translated string (e.g. <ph name="X">$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).
This commit is contained in:
Kevin Smith
2026-02-13 15:03:59 -05:00
committed by GitHub
parent f6724c5b22
commit fecf39eff4
2 changed files with 22 additions and 9 deletions
+19 -6
View File
@@ -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) => <span>{content}</span>
}), <>Hello from <span>me</span>{" to "}{"me"}. <span>bob</span> 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')
})
})
+3 -3
View File
@@ -91,7 +91,7 @@ export function formatString<T extends Replacement>(
}
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<T extends Replacement>(
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<T extends Replacement>(
// 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)