Skip to content

Populate attribute name in flow error messages - #68

Merged
brionmario merged 1 commit into
thunder-id:mainfrom
NipuniBhagya:attribute-placeholder-fix
Aug 11, 2026
Merged

Populate attribute name in flow error messages#68
brionmario merged 1 commit into
thunder-id:mainfrom
NipuniBhagya:attribute-placeholder-fix

Conversation

@NipuniBhagya

@NipuniBhagya NipuniBhagya commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Purpose

This pull request introduces an unified approach to parameter substitution in i18n translation strings across the JavaScript, React, and Vue SDKs. It adds a utility for substituting parameters in both backend ({{param(name)}}) and bundle ({name}) placeholder syntaxes, updates all translation hooks and providers to use it, and refines error message extraction to ensure fully resolved, user-presentable messages. Comprehensive tests are included to validate the new logic.


Approach

Parameter substitution improvements:

  • Added a new utility, substituteTranslationParams, which handles both backend ({{param(name)}}) and bundle ({name}) parameter syntaxes, and a helper hasUnresolvedTranslationParams to detect unresolved placeholders. Exported these from the main SDK entry point. [1] [2]
  • Updated the I18nMessage interface to clarify that defaultValue is pre-substituted by the backend, while bundle lookups require substitution with params.

SDK integration (React & Vue):

  • Refactored all translation hooks and providers in React (I18nProvider.tsx, useTranslation.ts) and Vue (I18nProvider.ts) to use the new substituteTranslationParams utility, replacing ad-hoc parameter substitution code. [1] [2] [3] [4] [5] [6]

Error message extraction logic:

  • Updated error extraction in flowTransformer.ts (React and Vue) to:
    • Pass params to translation lookups.
    • Use hasUnresolvedTranslationParams to fall back to defaultValue if any placeholders remain after substitution, ensuring only fully-resolved messages are shown to users.
    • Accept the updated I18nMessage type for error messages and descriptions. [1] [2] [3] [4] [5]

Testing:

  • Added comprehensive unit tests for the new substitution utility and for error extraction logic in both React and Vue, covering edge cases and fallback scenarios. [1] [2]

These changes ensure consistent, correct, and maintainable parameter substitution for all i18n messages and error handling across the SDKs.

Related Issues

Related PRs

  • N/A

Checklist

  • Followed the contribution guidelines.
  • Manual test round performed and verified.
  • Documentation provided. (Add links if there are any)
  • Tests provided. (Add links if there are any)
    • Unit Tests
    • Integration Tests
  • Breaking changes. (Fill if applicable)
    • Breaking changes section filled.
    • breaking change label added.

Security checks

  • Followed secure coding standards.
  • Confirmed that this PR doesn't commit any keys, passwords, tokens, usernames, or other secrets.

Summary by CodeRabbit

  • New Features

    • Added translation parameter substitution for backend and SDK placeholder formats.
    • Added support for passing parameters with localized flow error messages.
    • Exposed translation utilities for detecting unresolved placeholders.
  • Bug Fixes

    • Improved fallback handling when translations are missing or contain unresolved parameters.
    • Standardized translation behavior across React and Vue integrations.
  • Tests

    • Added coverage for parameter replacement, fallback messages, missing parameters, and flow error translation scenarios.

Flow error messages resolved from a translation bundle kept the backend's
`{{param(name)}}` placeholders literally, because `t()` only substituted the
`{name}` syntax used by the SDK's own bundles and `extractErrorMessage` never
passed `error.message.params` through. The server ships the whole `system`
namespace via flow meta, so the bundle lookup always won over the
already-substituted `defaultValue`, and an attribute uniqueness failure
rendered as "User already exists with the provided {{param(attribute)}}".

Add a shared `substituteTranslationParams` util that handles both placeholder
syntaxes, use it in all three `t()` implementations, and pass the error params
into the lookups. A translation that still holds an unresolved placeholder is
now treated as a miss so resolution falls back to `defaultValue`.

Fixes thunder-id/thunderid#4796

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Translation parameter handling

Layer / File(s) Summary
Shared substitution utility
packages/javascript/src/utils/substituteTranslationParams.ts, packages/javascript/src/models/embedded-flow.ts, packages/javascript/src/index.ts, packages/javascript/src/utils/__tests__/*
Adds support for backend and SDK placeholder formats. Exports substitution and unresolved-placeholder detection. Adds params to I18nMessage and tests the behavior.
React and Vue translation integration
packages/react/src/contexts/I18n/I18nProvider.tsx, packages/react/src/hooks/useTranslation.ts, packages/vue/src/providers/I18nProvider.ts
Replaces local parameter substitution with the shared utility.
Error translation and fallback
packages/react/src/utils/flowTransformer.ts, packages/react/src/utils/__tests__/flowTransformer.test.ts, packages/vue/src/utils/flowTransformer.ts, packages/vue/src/utils/__tests__/flowTransformer.test.ts
Passes message parameters to translation lookups, retries system keys, rejects unresolved placeholders, and covers fallback behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: brionmario

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary fix for populating attribute names in flow error messages.
Description check ✅ Passed The description covers the purpose, approach, linked issue, implementation details, testing, and repository checklist.
Linked Issues check ✅ Passed The changes address [#4796] by substituting error parameters and passing them into React and Vue translation lookups.
Out of Scope Changes check ✅ Passed The shared utility, SDK integrations, error handling updates, and tests directly support the linked issue objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/javascript/src/index.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

packages/javascript/src/models/embedded-flow.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

packages/javascript/src/utils/__tests__/substituteTranslationParams.test.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 8 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/react/src/utils/__tests__/flowTransformer.test.ts`:
- Around line 4-29: Replace the local createTranslator parameter-substitution
implementations in packages/react/src/utils/__tests__/flowTransformer.test.ts
(lines 4-29) and packages/vue/src/utils/__tests__/flowTransformer.test.ts (lines
4-29) with substituteTranslationParams from `@thunderid/browser`. Preserve each
test translator’s existing bundle lookup and missing-key behavior while
delegating parameter substitution to the shared helper in both files.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 290210a4-961d-42bf-bc2f-f0e831e7278e

📥 Commits

Reviewing files that changed from the base of the PR and between 60577d3 and c930265.

📒 Files selected for processing (11)
  • packages/javascript/src/index.ts
  • packages/javascript/src/models/embedded-flow.ts
  • packages/javascript/src/utils/__tests__/substituteTranslationParams.test.ts
  • packages/javascript/src/utils/substituteTranslationParams.ts
  • packages/react/src/contexts/I18n/I18nProvider.tsx
  • packages/react/src/hooks/useTranslation.ts
  • packages/react/src/utils/__tests__/flowTransformer.test.ts
  • packages/react/src/utils/flowTransformer.ts
  • packages/vue/src/providers/I18nProvider.ts
  • packages/vue/src/utils/__tests__/flowTransformer.test.ts
  • packages/vue/src/utils/flowTransformer.ts

Comment on lines +4 to +29
import {describe, it, expect} from 'vitest';
import {extractErrorMessage} from '../flowTransformer';

const UNIQUENESS_KEY = 'flows.executor.errors.attribute_not_unique';

/**
* Builds a `t` stub backed by a flat bundle, mirroring how `I18nProvider` resolves keys:
* a miss returns the key itself, and params are substituted into the resolved value.
*/
const createTranslator =
(bundle: Record<string, string> = {}) =>
(key: string, params?: Record<string, string | number>): string => {
const translation: string = bundle[key] ?? key;

if (!params) {
return translation;
}

return Object.entries(params).reduce(
(acc: string, [paramKey, paramValue]: [string, string | number]): string =>
acc
.replace(new RegExp(`\\{\\{\\s*param\\(\\s*${paramKey}\\s*\\)\\s*\\}\\}`, 'g'), String(paramValue))
.replace(new RegExp(`\\{${paramKey}\\}`, 'g'), String(paramValue)),
translation,
);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify that the shared helper is exported and find the duplicate test fixtures.
rg -n -C 3 'substituteTranslationParams|const createTranslator' packages

Repository: thunder-id/javascript-sdks

Length of output: 13090


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- shared helper ---'
cat -n packages/javascript/src/utils/substituteTranslationParams.ts

printf '%s\n' '--- browser exports ---'
rg -n -C 3 'substituteTranslationParams' packages/browser/src packages/javascript/src/index.ts

printf '%s\n' '--- React fixture ---'
cat -n packages/react/src/utils/__tests__/flowTransformer.test.ts | sed -n '1,35p'

printf '%s\n' '--- Vue fixture ---'
cat -n packages/vue/src/utils/__tests__/flowTransformer.test.ts | sed -n '1,35p'

printf '%s\n' '--- package dependency declarations ---'
rg -n -C 2 '"`@thunderid/`(javascript|browser)"' packages/react/package.json packages/vue/package.json

Repository: thunder-id/javascript-sdks

Length of output: 7403


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import re

def local_substitute(translation, params):
    for param_key, param_value in params.items():
        translation = re.sub(
            r'\{\{\s*param\(\s*' + param_key + r'\s*\)\s*\}\}',
            lambda m: str(param_value),
            translation,
        )
        translation = re.sub(
            r'\{' + param_key + r'\}',
            lambda m: str(param_value),
            translation,
        )
    return translation

def shared_substitute(translation, params):
    if not translation or not params:
        return translation
    for param_key, param_value in params.items():
        escaped = re.escape(param_key)
        translation = re.sub(
            r'\{\{\s*param\(\s*' + escaped + r'\s*\)\s*\}\}',
            lambda m: str(param_value),
            translation,
        )
        translation = re.sub(
            r'\{' + escaped + r'\}',
            lambda m: str(param_value),
            translation,
        )
    return translation

cases = [
    ("Value: {{param(attribute)}}", {"attribute": "$&"}),
    ("Value: {a.b}", {"a.b": "ok"}),
    ("Value: {a+b}", {"a+b": "ok"}),
]
for translation, params in cases:
    print(repr((translation, params)))
    print("local :", repr(local_substitute(translation, params)))
    print("shared:", repr(shared_substitute(translation, params)))
PY

Repository: thunder-id/javascript-sdks

Length of output: 404


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- browser package exports ---'
fd -i 'index.ts' packages/browser packages/javascript | sort | xargs -r rg -n -C 4 'substituteTranslationParams|`@thunderid/javascript`'

printf '%s\n' '--- JavaScript replacement semantics ---'
node - <<'JS'
function localSubstitute(translation, params) {
  return Object.entries(params).reduce(
    (acc, [paramKey, paramValue]) =>
      acc
        .replace(new RegExp(`\\{\\{\\s*param\\(\\s*${paramKey}\\s*\\)\\s*\\}\\}`, 'g'), String(paramValue))
        .replace(new RegExp(`\\{${paramKey}\\}`, 'g'), String(paramValue)),
    translation,
  );
}

function escapeRegExp(value) {
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function sharedSubstitute(translation, params) {
  return Object.entries(params).reduce((acc, [paramKey, paramValue]) => {
    const escapedKey = escapeRegExp(paramKey);
    const value = String(paramValue);
    return acc
      .replace(new RegExp(`\\{\\{\\s*param\\(\\s*${escapedKey}\\s*\\)\\s*\\}\\}`, 'g'), () => value)
      .replace(new RegExp(`\\{${escapedKey}\\}`, 'g'), () => value);
  }, translation);
}

for (const [translation, params] of [
  ['Value: {{param(attribute)}}', {attribute: '$&'}],
  ['Value: {{param(a+b)}}', {'a+b': 'ok'}],
  ['Value: {a.b}', {'a.b': 'ok'}],
]) {
  console.log(JSON.stringify({translation, params}));
  console.log('local :', JSON.stringify(localSubstitute(translation, params)));
  console.log('shared:', JSON.stringify(sharedSubstitute(translation, params)));
}
JS

Repository: thunder-id/javascript-sdks

Length of output: 1988


Use substituteTranslationParams from @thunderid/browser in both test translators.

The local implementations do not escape parameter names and pass replacement values as strings. Values such as a+b or $& therefore differ from provider behavior in both files.

🧰 Tools
🪛 ast-grep (0.45.1)

[warning] 24-24: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{\\{\\s*param\\(\\s*${paramKey}\\s*\\)\\s*\\}\\}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity

(regexp-from-variable)


[warning] 25-25: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{${paramKey}\\}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity

(regexp-from-variable)

📍 Affects 2 files
  • packages/react/src/utils/__tests__/flowTransformer.test.ts#L4-L29 (this comment)
  • packages/vue/src/utils/__tests__/flowTransformer.test.ts#L4-L29
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react/src/utils/__tests__/flowTransformer.test.ts` around lines 4 -
29, Replace the local createTranslator parameter-substitution implementations in
packages/react/src/utils/__tests__/flowTransformer.test.ts (lines 4-29) and
packages/vue/src/utils/__tests__/flowTransformer.test.ts (lines 4-29) with
substituteTranslationParams from `@thunderid/browser`. Preserve each test
translator’s existing bundle lookup and missing-key behavior while delegating
parameter substitution to the shared helper in both files.

Source: Coding guidelines

@brionmario
brionmario merged commit ca1799d into thunder-id:main Aug 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Attribute placeholder not populated with the attribute name which failed uniqueness with the uniqueness validator executor

2 participants