Skip to main content

Convert Xcode String Catalog (.xcstrings) to legacy .strings — degrade gracefully to flat key-value

Free CLI to convert Apple's modern .xcstrings catalog into legacy iOS Localizable.strings. Picks the source-language values, preserves comments, deterministic order.

Free CLI — convert xcstrings files to ios-strings:

npm install -g @i18n-agent/i18n-convert
Need to translate, not just convert? Try i18nagent.ai MCP →

Xcode 15 introduced the String Catalog (.xcstrings) as Apple's new opinionated format for iOS and macOS localization. It is a single JSON file that holds every locale, every translation state, every plural variant, and every device-specific override under one tree. The legacy .strings format — flat, one-locale-per-file, no state tracking — is not going anywhere quickly: it is still what SwiftUI's LocalizedStringKey ultimately reads, what hundreds of CI scripts grep, and what most third-party translation tools speak fluently. Going from catalog to legacy strings is the conversion every team needs when they want to adopt .xcstrings for editing but still ship .strings for compatibility.

i18n-convert reads the catalog's sourceLanguage, walks each entry's localizations map, and emits a flat .strings file using the source-language stringUnit.value as the displayed text. Per-entry comment fields become C-style /* */ comments above the corresponding key, so translator notes survive the conversion. Two pieces of data are intentionally dropped: per-entry state (translated / new / needs-review) and per-entry extractionState. The CLI surfaces these as INFO warnings and requires --force because they have no representation in the flat .strings format. Entries are emitted in alphabetical order for deterministic diffs.

Command

i18n-convert simple.xcstrings --to ios-strings --force -o Localizable.strings

Input

{
  "sourceLanguage" : "en",
  "strings" : {
    "greeting" : {
      "localizations" : {
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Hello"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "こんにちは"
          }
        }
      }
    },
    "farewell" : {
      "comment" : "Said when leaving",
      "localizations" : {
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Goodbye"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "さようなら"
          }
        }
      }
    },
    "app_name" : {
      "extractionState" : "manual",
      "localizations" : {
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "MyApp"
          }
        }
      }
    }
  },
  "version" : "1.0"
}

Output

"app_name" = "MyApp";

/* Said when leaving */
"farewell" = "Goodbye";

"greeting" = "Hello";

Related conversions