Translation Keys Used as Data
This learning is based on a real production incident where user-facing translation values were mistakenly used as part of a data contract, leading to a silent failure.
Let’s investigate how coupling user-facing translation values with a backend data contract led to an incident, and how we can spot and prevent this category of bugs during code review.
The Scenario
- The feature displays a list of Circle products so users can select which ones they want to give feedback on.
- The implementation uses the resolved translation value (e.g., “Wallets”) for both the checkbox’s visible label and the
name/valuesubmitted in the form. - Later, an engineer updated some of the translation values (e.g., “Programmable Wallets” → “Wallets”) without realising those same strings were also submitted as form values.
- The form posts to a Google Docs form that expects exact string matches; the updated values no longer matched the column options and the submissions were silently rejected.
- Unit tests for the GraphQL submission were missing so business logic changes were not caught when the translation key values changed.
- No integration tests were in place to validate the data contract with the Google Form, so the breaking change shipped unnoticed.
- A hot-fix introduced a stable enum (e.g.,
wallets,scp,gas, …) for thename/valuewhile keeping translation keys—and their visible values—solely for UI labels.
Before
FeatureRequestProducts.tsx
const products = [
t`products.wallets`,
t`products.scp`,
t`products.gas`,
t`products.cctp`,
t`products.usdc`,
t`products.eurc`,
];
return (
<Checkbox.Group name="products">
{products.map((item) => (
<Checkbox
key={item}
label={item}
name={item}
/>
))}
</Checkbox.Group>
);PR Comment
Choose the comment that you think is the most constructive and helpful.
Click here to learn more
Key Lessons
1. Separate Presentation from Data
- Translation keys belong in the UI layer only
- Submitted data should use stable, locale-agnostic values (e.g., enums, IDs)
- Avoid hidden coupling between localisation files and business logic
2. Testing Strategy
- Unit tests alone won’t catch changes in translation keys
- Integration or contract tests should validate data sent to the backend
- End-to-end tests can guard against regressions in critical flows
3. Code Review Best Practices
- Question architectural choices that blend presentation and data layers
- Ask how changes affect downstream systems and data contracts
- Verify that tests cover contract boundaries, not just happy paths
Tips for Reviewers
1. Validate Data Contracts
- Ask how data contracts are enforced and validated, especially for external integrations.
- What happens if the value sent from the UI changes?
2. Verify Data Stability
- Verify that user-facing changes, like editing translation values, cannot inadvertently alter submitted data.
- Look for places where display text is used as a data identifier.
3. Promote Stable Identifiers
- Encourage the use of enums or other canonical constants for values sent over the wire.
- Advocate for decoupling the submitted ID from the displayed label (e.g.,
{ id: 'wallets', label: t('wallets_label') }).
Common Pitfalls to Avoid
1. Coupling Display Text with API Values
- ❌
<Checkbox name={t('product.name')} /> - ✅
<Checkbox name="PRODUCT_ID" label={t('product.name')} />
2. Assuming External Contracts are Lenient
- ❌ “It’s just a Google Form, we can send whatever we want.”
- ✅ “This external service expects an exact string match. The contract is rigid and must be respected.”
3. Relying Only on Component-Level Tests
- ❌ “All the component tests pass, so the feature works.”
- ✅ “The component is correct, but we need an integration test to verify the submission to the external service.”
Remember: Clean separation between presentation and data layers prevents surprises when translations change.
Last updated on