Skip to content

directives: $t expands replacement patterns and re-substitutes params; $truncate splits surrogate pairs #474

Open

Description

@trackoor

Version

@json-render/directives 0.22.0, @json-render/core 0.22.0 (latest), and main (f5c1240)

Steps to reproduce

npm install @json-render/directives@0.22.0 @json-render/core@0.22.0 zod
node repro.mjs

repro.mjs:

import { createI18nDirective, truncateDirective } from "@json-render/directives";

const ctx = { stateModel: {} };
const t = createI18nDirective({
  locale: "en",
  messages: { en: { greet: "Hello, {{name}}!", price: "Total: {{amount}} ({{note}})" } },
});

console.log(t.resolve({ $t: "greet", params: { name: "$& and $'" } }, ctx));
console.log(t.resolve({ $t: "price", params: { amount: "$$5.00", note: "gift" } }, ctx));
console.log(t.resolve({ $t: "price", params: { amount: "{{note}}", note: "gift" } }, ctx));
try {
  console.log(t.resolve({ $t: "greet", params: { "na(me": "x" } }, ctx));
} catch (e) {
  console.log(String(e));
}
console.log(JSON.stringify(truncateDirective.resolve({ $truncate: "Great day \u{1F600} yes", length: 11 }, ctx)));

Expected behavior

Hello, $& and $'!
Total: $$5.00 (gift)
Total: {{note}} (gift)
Hello, {{name}}!
"Great day ..."

Parameter values in $t should be inserted verbatim without String.prototype.replace $-pattern expansion or re-substitution by later parameters, parameter keys should be matched literally rather than compiled as raw regular expressions, and $truncate should slice by Unicode code points (or avoid splitting a surrogate pair, yielding "Great day ..." or "Great day \u{1F600}...").

Actual behavior

Hello, {{name}} and !!
Total: $5.00 (gift)
Total: gift (gift)
SyntaxError: Invalid regular expression: /\{\{na(me\}\}/g: Unterminated group
"Great day \ud83d..."

In packages/directives/src/i18n.ts, createI18nDirective loops over Object.entries(raw.params) and calls template.replace(new RegExp(`\\{\\{${paramKey}\\}\\}`, "g"), String(resolved)):

  • Passing String(resolved) as the replacement string expands $$, $&, $`, and $' in parameter values (for example, "$$5.00" loses a $ and "$&" expands to "{{name}}").
  • Mutating template on each iteration re-substitutes any {{otherParam}} text present in an earlier parameter's value.
  • Putting paramKey into new RegExp(...) without escaping throws SyntaxError on regex characters like ( or [, and matches . as any character.

In packages/directives/src/truncate.ts, text.slice(0, maxLength) slices UTF-16 code units, leaving a lone high surrogate (\ud83d, rendered as U+FFFD) when length falls inside a surrogate pair.

Environment

Node.js v26.13.0, Linux x86_64

Credit: https://ucsbplse.github.io/

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions