nextjs-intl-translation-structure-consistency
Next.js i18n Translation Structure Consistency
Problem
When working with next-intl in TypeScript projects, adding new translation sections can cause cryptic type errors if the structure doesn't match exactly across all locale files. TypeScript requires all locale files to have identical key structures (including key naming conventions and nested sections).
Context / Trigger Conditions
This issue occurs when:
-
Error message pattern:
error TS2322: Type '{ ... }' is not assignable to type '{ ... }'. The types of '"sectionName"["subsection"]' are incompatible between these types. Property '"missingKey"' is missing in type '{ ... }' but required in type '{ ... }'. -
Specific scenarios:
- Added new translation section to
en.jsonbut forgot to add to other locale files - Used inconsistent key naming (e.g.,
camelCasevssnake_case) - Added new nested keys but missed them in some locale files
- Reordered keys differently across locale files (though this alone won't cause errors)
- Added new translation section to
-
File location: Errors typically appear in
lib/i18n-helpers.tsxor similar files that import and validate translation types
Solution
Step 1: Identify the Exact Mismatch
The error message will tell you:
- Which section has the mismatch (e.g.,
"adminVerification"["documentReview"]) - Which specific key is missing or incompatible (e.g.,
"summary","pending_revalidation")
Example error:
Property '"summary"' is missing in type '{ statusBadge: {...}, actions: {...} }'
but required in type '{ statusBadge: {...}, actions: {...}, summary: {...} }'
This means the summary key exists in en.json but is missing in other locale files.
Step 2: Compare Translation Files
Use grep to find where the section exists:
grep -n '"sectionName":' apps/web/strings/*.json
Check that ALL locale files have the exact same structure.
Step 3: Fix Key Naming Inconsistencies
Common pitfall: Mixing snake_case and camelCase in the same project.
❌ Wrong (inconsistent):
// en.json
"statusBadge": {
"pendingRevalidation": "Pending Revalidation"
}
// ur.json
"statusBadge": {
"pending_revalidation": "Pending Revalidation"
}
✅ Correct (consistent):
// Both files
"statusBadge": {
"pending_revalidation": "Pending Revalidation"
}
Step 4: Add Missing Sections
For each locale file (en.json, ur.json, ar.json, etc.), ensure the new section exists
with the EXACT same structure:
"newSection": {
"subsection": {
"key1": "[LOCALE] Translation text",
"key2": "[LOCALE] Translation text"
}
}
Use locale prefixes like [UR], [AR], [SO] for non-English translations until proper
translations are available.
Step 5: Verify with Type Check
turbo types
The error should disappear if all structures match exactly.
Verification
- Type check passes:
turbo typesruns without errors - All locale files have identical keys: Use diff to compare structure:
# Extract keys only (remove translation text) jq 'walk(if type == "object" then with_entries(.value = "...") else . end)' \ apps/web/strings/en.json > /tmp/en-structure.json jq 'walk(if type == "object" then with_entries(.value = "...") else . end)' \ apps/web/strings/ur.json > /tmp/ur-structure.json diff /tmp/en-structure.json /tmp/ur-structure.json - Application loads without i18n errors: No runtime errors about missing keys
Example
Scenario: Added documentReview section to English translations, got type error.
Error:
lib/i18n-helpers.tsx(15,3): error TS2322: Type '{ ... }' is not assignable to type '{ ... }'.
The types of '"adminVerification"["documentReview"]' are incompatible between these types.
Property '"summary"' is missing in type '{ ... }' but required in type '{ ... }'.
Root cause:
- Added
documentReviewtoen.jsonwithsummarysubsection - Added
documentReviewto other locale files but forgotsummarysubsection - Used
pendingRevalidation(camelCase) instead ofpending_revalidation(snake_case)
Fix:
# Check which files have the section
grep -n '"documentReview":' apps/web/strings/*.json
# Compare structure
# Found en.json has "summary" and "pending_revalidation"
# ur.json, ar.json, so.json were missing "summary" and had "pendingRevalidation"
# Edit each file to match en.json structure exactly
After adding the missing summary section and fixing snake_case consistency, type check passed.
Notes
Key Naming Conventions
- Stick to one convention: Choose either
camelCaseorsnake_caseand use it consistently - Next-intl default: The library doesn't enforce a convention, but TypeScript type inference requires exact matches
- Status enums: Database enum values often use
SCREAMING_SNAKE_CASE(e.g.,PENDING_REVALIDATION), but translation keys typically use lowercasesnake_case(e.g.,pending_revalidation)
Common Mistakes
- Adding section to English only: Always add to ALL locale files simultaneously
- Inconsistent nesting depth: All locales must have the same depth of nesting
- Missing optional sections: Even optional sections must exist in all files (can be empty objects)
- Case sensitivity:
camelCase≠camelcase- JavaScript object keys are case-sensitive
Prevention
- Use a script: Create a script to validate all locale files have matching structures
- Git pre-commit hook: Check structure consistency before allowing commits
- Add to CI/CD: Run structure validation in GitHub Actions
- Use i18n linting tools: Tools like
i18n-unusedcan catch missing keys
Alternative Solutions
If you need truly optional sections, consider:
// Type definition that allows undefined
type Translations = {
section?: {
key: string;
};
};
However, next-intl's type inference doesn't support this pattern well. It's better to include all sections in all files.