How to Structure JSON Translation Files for Scalable UI

Structuring JSON translation files properly is essential for building internationalized (i18n) applications that scale without degrading performance or developer experience. Poorly organized localization files lead to duplicated strings, bloated bundles, merge conflicts, and translation errors. This guide outlines the best practices for structuring, naming, and managing JSON translation files to support clean codebases, seamless localization workflows, and fast UI rendering as your application grows.

1. Organize by Feature or Page, Not by UI Element

Organizing keys around components like buttons.json or headers.json quickly causes confusion and duplication. Instead, group your translations by domain, feature, or page, with a dedicated namespace for shared UI elements:

{
  "common": {
    "save": "Save",
    "cancel": "Cancel",
    "loading": "Loading..."
  },
  "auth": {
    "login": {
      "title": "Log in to your account",
      "submit_button": "Sign In"
    }
  },
  "dashboard": {
    "welcome_message": "Welcome back, {userName}!"
  }
}

This structure makes it immediately obvious where a string belongs and limits the blast radius of changes to specific application views.

2. Limit Nesting to Two or Three Levels

While nesting provides organizational hierarchy, deeply nested JSON objects create brittle access paths (e.g., t('pages.settings.security.two_factor.modal.buttons.confirm')) and increase the risk of missing-key bugs.

Keep depth shallow:

  • Level 1: Feature or Namespace (settings)
  • Level 2: Sub-feature or Component (two_factor)
  • Level 3: Specific Element or State (confirm_button)

Flat keys with dot notation (e.g., "settings.two_factor.confirm_button": "Confirm") are also widely supported by modern i18n libraries and reduce payload parsing overhead.

3. Establish Strict Key Naming Conventions

Define a clear naming syntax across all development teams. Keys should describe their semantic function and context, not their current English text value.

  • Pattern: [context]_[element_type]_[action/state]
  • Examples:
    • checkout_button_pay_now (Clear)
    • pay_now (Too vague)
    • button1 (Non-descriptive)

Choose one casing style—such as snake_case or camelCase—and enforce it consistently across all translation files.

4. Split Monolithic Files into Namespaces

Shipping a single, massive en.json file forces the client to download unused translations for the entire application on the initial page load. Split your translations into modular namespace files:

locales/
├── en/
│   ├── common.json
│   ├── auth.json
│   └── dashboard.json
└── es/
    ├── common.json
    ├── auth.json
    └── dashboard.json

Modern frameworks (such as Next.js, React, or Vue) can lazy-load these chunks on demand based on the active route, reducing initial bundle sizes significantly.

5. Use ICU MessageFormat for Plurals and Variables

Avoid string concatenation or custom logic to handle dynamic data and pluralization. Different languages have complex pluralization rules that do not match English. Use industry standards like ICU MessageFormat:

{
  "cart": {
    "item_count": "{count, plural, =0 {Your cart is empty} one {# item in your cart} other {# items in your cart}}"
  }
}

This delegates pluralization, gender agreements, and number formatting to your i18n library, preventing translation bugs in non-English locales.

6. Automate Linting and Key Synchronization

As your codebase scales, manual maintenance of translation files becomes unsustainable. Integrate automated tools into your CI/CD pipeline:

  • Key Parity Checkers: Verify that all locale files (fr.json, de.json) contain the exact same keys present in your source locale (en.json).
  • Unused Key Detectors: Scan the codebase to identify and purge obsolete translation keys.
  • JSON Schema Validation: Enforce proper syntax and structure before changes merge to production.