A translation file that parses successfully can still break an interface. It may contain a repeated name whose earlier value disappears, omit a dynamically selected label, or lose an interpolation variable during translation. i18n catalog validation should treat these as separate contracts rather than assuming that valid JSON means usable localized text.
This guide develops a small, read-only validation model with Python 3.11.15 and its standard library. The examples use synthetic strings held in memory. They do not open application resources or change existing catalogs. The underlying Markdown note supplies the problem categories; environment details, product identifiers, historical counts, and reported outcomes are deliberately excluded. The engineering conclusion is narrower and more useful than a blanket quality claim: structural checks are necessary, but they do not establish translation accuracy or correct rendering.
Define the resource contract before choosing a scanner
i18n catalog validation needs an explicit resource shape, a baseline language, and a separately maintained set of required keys. Without those boundaries, a successful comparison can hide shared omissions, and a parser can accept structures that the application does not actually intend to support.
The example contract accepts a flat JSON object whose values are nonempty strings. Arrays, nested objects, numbers, and whitespace-only values are rejected. This is a deliberate limitation of this test utility, not a claim about what localization frameworks support. A project with nested resources needs a different traversal contract and tests for how paths are represented. In particular, a literal key containing a dot must not accidentally become indistinguishable from a nested object path.
Choose a baseline language for comparison, but do not confuse that role with completeness. Two catalogs can agree perfectly while both omit a label that a component constructs at runtime. Maintain a separate required collection that represents the interface's expectations. Check it against the baseline as well as the target language. The baseline then becomes one input to validation, rather than an authority that cannot be questioned.
Separate outcomes in the review process. A parsing error means the resource does not satisfy the accepted structural shape. A contract difference means a key or variable needs attention. An unresolved editorial question means the language review is incomplete. Combining all three into a single green status makes it impossible to understand what was actually established. The test harness below intentionally proves only the first two categories.
Detect duplicate names before creating a dictionary
Duplicate names must be rejected while the decoder still exposes the original name-value pairs. Python's default JSON decoder keeps the last value for a repeated name, so comparing dictionaries afterward cannot recover the overwritten entry or demonstrate that the original catalog was unambiguous.
The Python JSON documentation explicitly describes this default behavior. For a translation catalog, a repeated label can silently change what the interface displays while leaving the file parseable. A check that calls ordinary json.loads and then counts dictionary entries has already discarded the evidence needed to detect that situation.
The example passes unique_pairs through object_pairs_hook. The hook receives an ordered list of pairs and builds a dictionary only after checking each name. Repetition raises ValueError. The regression test demonstrates both sides of the boundary: the default decoder retains the second value, while the strict catalog loader rejects the same input. Testing the failure mode and the protective rule together is more informative than showing that a clean example loads.
Although the hook also participates when nested objects are decoded, the final catalog function still rejects nested values. That distinction matters. A helper being capable of observing a nested object does not mean the whole validator supports nested localization resources. An extension should define accepted paths, error locations, and legal value types together. The minimal utility reports error categories without printing the source strings, keeping its diagnostic surface small.
Give dynamic keys an independent requirement list
Literal-string scanning is evidence of which fixed names appear in source code, not proof that other names are unused. Dynamically selected titles, menus, and component states need an explicit requirement list that is checked against both the baseline language and each target catalog.
Searching translation calls is a useful starting point. It becomes misleading when an expression selects the name from a component state or assembles it from a controlled prefix. The scanner sees a variable, not every value that the variable can hold. A search with no match therefore establishes only a limitation of that search. It is not sufficient evidence for deleting a resource entry.
A practical design gives each component that creates dynamic names a finite, reviewed list of requirements. Merge those lists for validation and define who updates them when the component changes. The sample deliberately leaves panel.title out of both catalogs. Passing it in required must produce a baseline-missing result and a target-missing result. Finding only one of those conditions would fail the test and reveal an incomplete coverage rule.
Extra keys deserve separate treatment. They may belong to a transition period, a component not included in the current inventory, or an intentionally broader namespace. The target must contain every baseline key and every required key; the baseline is also checked against required. This utility does not delete extra target keys or claim they are unused. A cleanup decision needs additional evidence and review. Keeping validation read-only avoids turning a conservative diagnostic into an accidental resource editor, especially when the inventory is known to be incomplete.
Compare placeholder multiplicity without fixing word order
Placeholder validation should preserve variable names and occurrence counts while allowing languages to reorder the surrounding sentence. A multiset comparison catches a dropped repetition that a plain set would miss, but the accepted interpolation grammar must remain explicit rather than pretending to cover every framework feature.
The sample recognizes ordinary identifier names inside double braces, such as {{name}}, and permits whitespace around the name. A Counter records how often each name appears. If the baseline uses a variable twice and the target uses it once, the audit reports a placeholder difference. If two variables exchange positions, the counts still match. This respects natural changes in word order without allowing the data contract to drift unnoticed.
Matching occurrence counts is a conservative editorial policy, not a universal rule imposed by localization engines. A language may reasonably avoid a repeated form of address. If a project permits that exception, record it explicitly and review the affected key rather than weakening all comparisons. Passing this policy also says nothing about parameter types, number formatting, plural selection, or the meaning of the surrounding sentence.
The official i18next interpolation source documents a broader feature set than the subset implemented here. Dot-separated model paths, formatting options, and other brace forms are outside the example. After removing recognized placeholders, the helper rejects any remaining brace. That makes unsupported input passed to slots() visible. Interpolation syntax and name/count checks in audit() apply only to keys shared by both catalogs; this example does not scan every entry for unsupported syntax. A baseline-only key produces a target-missing error without a syntax check, and a target-only key is not checked for placeholders. Projects using ICU syntax, custom delimiters, or rich nested expressions should use the corresponding parser and their own fixed-version integration tests, not grow this regular expression into an imitation language engine.
Verification: run the fixed-version local test
Run the test in a new local practice folder with Python 3.11.15 and no third-party packages. All fixtures remain in memory. Passing requires explicit assertions for duplicate rejection, shared missing-key detection, placeholder multiplicity, valid input, and unsupported shapes; a failed assertion must produce a nonzero process exit.
The prerequisite is an existing Python 3.11.15 installation available through python. Save the complete program below as i18n_contract_test.py in that practice folder, not in an application's resource directory. No framework installation or service connection is necessary. The embedded version guard deliberately stops execution on another Python release so that the reported result remains tied to the stated environment.
- Run
python --versionand confirmPython 3.11.15. Save the full program before executing the remaining commands. A different version is a failed prerequisite, not a compatible result inferred from memory. - Run
python i18n_contract_test.py ContractTests.test_duplicate. It passes only when the default decoder retains the second value and the strict loader raises the expected duplicate error. Expectokand exit code zero. - Run
python i18n_contract_test.py ContractTests.test_dynamic_required. The expected result contains both missing-key categories forpanel.title. Reporting only the target omission is insufficient. - Run
python i18n_contract_test.py ContractTests.test_placeholder_count. The missing repetition must be detected, while swapping the order of distinct variables must remain acceptable. - Run
python i18n_contract_test.py. Expect five tests markedok, a finalOK, and exit code zero. The additional tests cover a valid catalog pair and rejection of unsupported data shapes and placeholder syntax.
import json
import re
import sys
import unittest
from collections import Counter
def unique_pairs(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError('duplicate key')
result[key] = value
return result
def catalog(text):
value = json.loads(text, object_pairs_hook=unique_pairs)
if not isinstance(value, dict):
raise ValueError('expected object')
if any(not isinstance(v, str) or not v.strip() for v in value.values()):
raise ValueError('expected nonempty strings')
return value
def slots(text):
pattern = r'\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}'
remainder = re.sub(pattern, '', text)
if '{' in remainder or '}' in remainder:
raise ValueError('unsupported placeholder syntax')
return Counter(re.findall(pattern, text))
def audit(base, target, required):
errors = []
for key in sorted(required - base.keys()):
errors.append('base-missing:' + key)
for key in sorted((base.keys() | required) - target.keys()):
errors.append('target-missing:' + key)
for key in sorted(base.keys() & target.keys()):
if slots(base[key]) != slots(target[key]):
errors.append('placeholder:' + key)
return errors
class ContractTests(unittest.TestCase):
def test_duplicate(self):
text = '{"label":"first","label":"second"}'
self.assertEqual(json.loads(text)['label'], 'second')
with self.assertRaisesRegex(ValueError, 'duplicate'):
catalog(text)
def test_dynamic_required(self):
base = catalog('{"hello":"Hello {{name}}"}')
target = catalog('{"hello":"Welcome {{name}}"}')
self.assertEqual(audit(base, target, {'hello', 'panel.title'}),
['base-missing:panel.title', 'target-missing:panel.title'])
def test_placeholder_count(self):
base = catalog('{"hello":"{{name}} / {{name}}"}')
target = catalog('{"hello":"{{name}}"}')
self.assertEqual(audit(base, target, {'hello'}), ['placeholder:hello'])
self.assertEqual(slots('{{first}} {{second}}'), slots('{{second}} {{first}}'))
def test_valid(self):
base = catalog('{"hello":"Hello {{name}}"}')
target = catalog('{"hello":"Welcome {{ name }}"}')
self.assertEqual(audit(base, target, {'hello'}), [])
def test_rejected_shapes(self):
for text in ['[]', '{"x":" "}', '{"x":{}}']:
with self.assertRaises(ValueError):
catalog(text)
for text in ['{{user.name}}', '{{- name}}', '{0}']:
with self.assertRaises(ValueError):
slots(text)
if __name__ == '__main__':
if sys.version_info[:3] != (3, 11, 15):
raise SystemExit('Requires Python 3.11.15')
unittest.main(verbosity=2)
Expected test summary (elapsed time varies by environment):
Ran 5 tests in ...s
OK
Actual scheduler execution: the complete program ran under Python 3.11.15. All five tests passed and the process returned exit code zero. This is evidence for the synthetic contract cases only. It is not an application integration result, and no performance measurement was performed. Browser loading, framework fallback behavior, rendered output, and linguistic accuracy were not exercised by this run. They require separate checks in an appropriately versioned test application.
A failure should remain visible. Do not replace an exception with an empty catalog or treat an unsupported placeholder as ordinary prose. Both approaches convert unavailable evidence into an apparent pass. When adapting the code to real files, keep the same fail-closed behavior, add bounded input handling, and preserve the separation between inspection and editing. File rewriting is intentionally outside this article's scope.
Keep language review independent from structural checks
Structural validation can establish key coverage and interpolation consistency, but it cannot decide whether a term fits a screen or a regional audience. Language review needs interface context, an existing terminology baseline, and an explicit decision about ambiguous labels before a catalog is accepted.
A fluent translation can still refer to the wrong concept. Reviewers should see the purpose of the control, nearby labels, and the terminology already used in related screens. A spreadsheet containing isolated strings may conceal distinctions that are obvious in the interface. Two different actions translated to the same label also deserve review even if every placeholder remains intact.
Script-based language detection is only a warning layer. Abbreviations, product names, and intentionally retained English words may be legitimate in another language. A detector that flags unfamiliar letter ranges should produce review candidates, not automatic replacements. Store the rationale for accepted exceptions so that future reviewers do not repeatedly undo a deliberate terminology decision.
Fallback behavior is another separate concern. An interface displaying baseline-language text does not prove that the target catalog is complete. This article does not start i18next and does not claim to have tested its resolution chain. Framework documentation supports the discussion of interpolation syntax; it does not turn a Python test into a browser rendering test. Keeping those evidence boundaries explicit is essential when a technical article is reused in architecture or delivery reviews.
Evidence, adoption checklist, and useful measurements
Adopt the checks as a small review gate, then refine the policy from confirmed defects and editorial decisions. Official references establish library behavior, the local note supplies problem context, and the execution record supports only the cases that actually ran in the fixed test environment.
Both language editions use the same references and verification facts:
- Python 3.11 JSON documentation documents repeated-name handling and the ordered-pair interface exposed through
object_pairs_hook. - Python 3.11 collections documentation documents
Counteras a way to count hashable items. - Official i18next interpolation documentation source describes double-brace interpolation and broader data-model expressions, supporting the stated limits of this example.
Before accepting the approach for a test project, confirm these boundaries:
- The resource shape is explicit, with flat and nested names kept distinct.
- Repeated names are rejected before dictionary construction discards them.
- Required dynamic names are maintained independently of the baseline catalog.
- Placeholder names and counts are checked without enforcing sentence order.
- Unsupported placeholder syntax in shared keys fails visibly; unshared keys are outside that syntax comparison, and editorial questions remain with reviewers.
- Unexecuted browser and framework checks are not reported as completed tests.
This is MOFU technical-evaluation content serving an informational search intent. Relevant content measurements are query impressions, click-through rate, and reader navigation to related engineering work. Those are suggested future indicators, not achieved outcomes. Engineering review should track confirmed missing keys, duplicate names, and interpolation differences rather than treating catalog size or a mechanical pass rate as a proxy for language quality.
The final decision rule is simple: establish the resource contract first, then establish the language and rendering contracts separately. A small read-only test makes the first boundary observable. Before broadening the supported grammar or connecting it to an application, add both passing and failing cases for the new behavior. Keep unexamined, unshared entries distinct from shared keys whose syntax was checked. A validator is trustworthy when those limits are visible.