<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gentian Asani</title>
    <description>The latest articles on DEV Community by Gentian Asani (@yinspeace).</description>
    <link>https://dev.to/yinspeace</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3946101%2Fd5932c15-75f1-4aa4-85a0-11e81ba50d0e.png</url>
      <title>DEV Community: Gentian Asani</title>
      <link>https://dev.to/yinspeace</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yinspeace"/>
    <language>en</language>
    <item>
      <title>Migrating from .strings to Xcode String Catalogs without breaking your translators</title>
      <dc:creator>Gentian Asani</dc:creator>
      <pubDate>Fri, 22 May 2026 19:04:52 +0000</pubDate>
      <link>https://dev.to/yinspeace/migrating-from-strings-to-xcode-string-catalogs-without-breaking-your-translators-eel</link>
      <guid>https://dev.to/yinspeace/migrating-from-strings-to-xcode-string-catalogs-without-breaking-your-translators-eel</guid>
      <description>&lt;p&gt;If you maintain a multilingual iOS app, you've probably looked at String Catalogs (&lt;code&gt;.xcstrings&lt;/code&gt;) at least twice and put off the migration both times. They're a real improvement on &lt;code&gt;Localizable.strings&lt;/code&gt; plus &lt;code&gt;Localizable.stringsdict&lt;/code&gt;: one file per resource, declarative plurals, type-safe lookups via &lt;code&gt;String(localized:)&lt;/code&gt;, device-class variations. But the migration is messier than the Apple sample projects let on, and the part that bites hardest is the bit Apple's documentation barely covers: keeping your translator pipeline working through the transition.&lt;/p&gt;

&lt;p&gt;This guide covers the part Apple skipped. It's written for iOS teams shipping to five or more App Store regions who use external translators and exchange XLIFF files with them. If you have one translator (or you're the translator), some of this still applies but the round-trip risk is lower.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the migration actually changes
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;Localizable.strings&lt;/code&gt; file is just key-value lines, one locale per file, with comments above each entry. Plurals live in a sibling &lt;code&gt;Localizable.stringsdict&lt;/code&gt; file with verbose XML.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;Localizable.xcstrings&lt;/code&gt; file is one JSON document covering all locales. It has dedicated fields for plural variants, device variants, comments, extraction state, and per-locale translation state.&lt;/p&gt;

&lt;p&gt;That last part is the one that matters during migration. In &lt;code&gt;.strings&lt;/code&gt; files, a string is either present or absent. In &lt;code&gt;.xcstrings&lt;/code&gt;, every key for every locale has an explicit &lt;code&gt;state&lt;/code&gt;: &lt;code&gt;new&lt;/code&gt;, &lt;code&gt;translated&lt;/code&gt;, &lt;code&gt;needs_review&lt;/code&gt;, &lt;code&gt;stale&lt;/code&gt;, or absent. The state model is closer to how teams actually ship (some keys translated, some not), but it also means migration tools have to make a choice for every existing translation: is it &lt;code&gt;translated&lt;/code&gt;, or is it &lt;code&gt;stale&lt;/code&gt; because it predates a source change?&lt;/p&gt;

&lt;p&gt;Xcode's auto-migration assumes everything that was in your existing &lt;code&gt;.strings&lt;/code&gt; files is &lt;code&gt;translated&lt;/code&gt;. That's optimistic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why translators care
&lt;/h2&gt;

&lt;p&gt;If you have a translator pipeline, the format you ship them is XLIFF. Xcode 16.4 emits XLIFF version 1.2. The root element of an export looks like: &lt;code&gt;&amp;lt;xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2"&amp;gt;&lt;/code&gt;. Xcode generates XLIFF from String Catalogs the same way it does from &lt;code&gt;.strings&lt;/code&gt;, via &lt;code&gt;xcodebuild -exportLocalizations&lt;/code&gt;. The output is an &lt;code&gt;.xcloc&lt;/code&gt; bundle per target locale, each containing a &lt;code&gt;Localized Contents/&amp;lt;lang&amp;gt;.xliff&lt;/code&gt; file plus a &lt;code&gt;Source Contents/&lt;/code&gt; folder for screenshots and a &lt;code&gt;Notes/&lt;/code&gt; folder for translator notes. Most CAT tools accept the &lt;code&gt;.xcloc&lt;/code&gt; directly; some only want the inner &lt;code&gt;.xliff&lt;/code&gt;. Translator receives XLIFF, works in their CAT tool (Trados, memoQ, OmegaT) or TMS (Lokalise, Phrase, Crowdin), and returns XLIFF. You then &lt;code&gt;xcodebuild -importLocalizations&lt;/code&gt; and the changes land in your catalog.&lt;/p&gt;

&lt;p&gt;The round-trip looks clean in theory. In practice, five things go wrong during the migration window:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Plural categories that exist in source but not target, or vice versa.&lt;/li&gt;
&lt;li&gt;HTML entity escaping that differs between &lt;code&gt;.strings&lt;/code&gt;, &lt;code&gt;.xcstrings&lt;/code&gt;, and XLIFF.&lt;/li&gt;
&lt;li&gt;Translator notes that disappear because the comment field maps differently in each format.&lt;/li&gt;
&lt;li&gt;Empty target tagging that confuses the translator about what's new versus what's existing.&lt;/li&gt;
&lt;li&gt;Variable interpolation tokens that survive the catalog but break in the XLIFF round-trip.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'll cover each. If you miss them, you find out in a 1-star Polish review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plural rules: the CLDR problem nobody documents
&lt;/h2&gt;

&lt;p&gt;English has two plural categories: one and other. "1 item" versus "2 items". &lt;code&gt;.stringsdict&lt;/code&gt; files with English source usually only define these two, and &lt;code&gt;.xcstrings&lt;/code&gt; inherits the same coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why translators care
&lt;/h2&gt;

&lt;p&gt;If you have a translator pipeline, the format you ship them is XLIFF. Xcode 16.4 emits XLIFF version 1.2. The root element of an export looks like: &lt;code&gt;&amp;lt;xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2"&amp;gt;&lt;/code&gt;. Xcode generates XLIFF from String Catalogs the same way it does from &lt;code&gt;.strings&lt;/code&gt;, via &lt;code&gt;xcodebuild -exportLocalizations&lt;/code&gt;. The output is an &lt;code&gt;.xcloc&lt;/code&gt; bundle per target locale, each containing a &lt;code&gt;Localized Contents/&amp;lt;lang&amp;gt;.xliff&lt;/code&gt; file plus a &lt;code&gt;Source Contents/&lt;/code&gt; folder for screenshots and a &lt;code&gt;Notes/&lt;/code&gt; folder for translator notes.&lt;/p&gt;

&lt;p&gt;Most CAT tools accept the &lt;code&gt;.xcloc&lt;/code&gt; directly; some only want the inner &lt;code&gt;.xliff&lt;/code&gt;. Translator receives XLIFF, works in their CAT tool (Trados, memoQ, OmegaT) or TMS (Lokalise, Phrase, Crowdin), and returns XLIFF. You then &lt;code&gt;xcodebuild -importLocalizations&lt;/code&gt; and the changes land in your catalog.&lt;/p&gt;

&lt;p&gt;The round-trip looks clean in theory. In practice, five things go wrong during the migration window:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Plural categories that exist in source but not target, or vice versa.&lt;/li&gt;
&lt;li&gt;HTML entity escaping that differs between &lt;code&gt;.strings&lt;/code&gt;, &lt;code&gt;.xcstrings&lt;/code&gt;, and XLIFF.&lt;/li&gt;
&lt;li&gt;Translator notes that disappear because the comment field maps differently in each format.&lt;/li&gt;
&lt;li&gt;Empty target tagging that confuses the translator about what's new versus what's existing.&lt;/li&gt;
&lt;li&gt;Variable interpolation tokens that survive the catalog but break in the XLIFF round-trip.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'll cover each. If you miss them, you find out in a 1-star Polish review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plural rules: the CLDR problem nobody documents
&lt;/h2&gt;

&lt;p&gt;English has two plural categories: one and other. "1 item" versus "2 items". &lt;code&gt;.stringsdict&lt;/code&gt; files with English source usually only define these two, and &lt;code&gt;.xcstrings&lt;/code&gt; inherits the same coverage.&lt;/p&gt;

&lt;p&gt;Polish has four: one, few, many, other. The rough mapping is "1 item" (one), "2-4 items" (few), "5-20 items" (many), "1.5 items" (other), but the actual rule is modulo-based, so 21 reverts to &lt;code&gt;one&lt;/code&gt;, 22 to &lt;code&gt;few&lt;/code&gt;, and so on. Russian has the same four categories as Polish. Arabic has all six (zero, one, two, few, many, other). Welsh and Irish each have their own variants. If your source only specified one and other, the migration carries the same gap into String Catalogs, and your Polish translator either fills in the missing categories (&lt;code&gt;few&lt;/code&gt;, &lt;code&gt;many&lt;/code&gt;) or returns XLIFF that's missing them.&lt;/p&gt;

&lt;p&gt;If the XLIFF is missing them, your app will hit &lt;code&gt;one&lt;/code&gt; for "1 item" and &lt;code&gt;other&lt;/code&gt; for everything else, which is grammatically wrong in Polish for most cases. Apple's runtime quietly falls back. The user sees "5 elementów" stylized as "5 elementem". Your testers won't catch it unless they're Polish speakers checking edge counts.&lt;/p&gt;

&lt;p&gt;What to do: before sending the first post-migration XLIFF to a Polish, Russian, or Arabic translator, audit the source's plural coverage against the CLDR plural rules for that locale. The Unicode CLDR project publishes them at &lt;a href="https://cldr.unicode.org/" rel="noopener noreferrer"&gt;https://cldr.unicode.org/&lt;/a&gt;. The minimum coverage for each language is a documented contract, not a suggestion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plural rules: the CLDR problem nobody documents
&lt;/h2&gt;

&lt;p&gt;English has two plural categories: one and other. "1 item" versus "2 items". &lt;code&gt;.stringsdict&lt;/code&gt; files with English source usually only define these two, and &lt;code&gt;.xcstrings&lt;/code&gt; inherits the same coverage.&lt;/p&gt;

&lt;p&gt;Polish has four: one, few, many, other. The rough mapping is "1 item" (one), "2-4 items" (few), "5-20 items" (many), "1.5 items" (other), but the actual rule is modulo-based, so 21 reverts to &lt;code&gt;one&lt;/code&gt;, 22 to &lt;code&gt;few&lt;/code&gt;, and so on. Russian has the same four categories as Polish. Arabic has all six (zero, one, two, few, many, other). Welsh and Irish each have their own variants. If your source only specified one and other, the migration carries the same gap into String Catalogs, and your Polish translator either fills in the missing categories (&lt;code&gt;few&lt;/code&gt;, &lt;code&gt;many&lt;/code&gt;) or returns XLIFF that's missing them.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTML entity drift across three formats
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.strings&lt;/code&gt; files store characters mostly as-is. &lt;code&gt;Hello, "world"!&lt;/code&gt; works. Apostrophes and quotes pass through.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.xcstrings&lt;/code&gt; files are JSON. They escape with backslash-u, and they accept Unicode literally for most ranges. Smart quotes survive.&lt;/p&gt;

&lt;p&gt;XLIFF files are XML. They require entity encoding for ampersand, less-than, and greater-than, and most translation tools auto-escape on export. Your translator's CAT tool might emit &lt;code&gt;&amp;amp;amp;quot;&lt;/code&gt; instead of a literal quote, or it might leave a literal &lt;code&gt;&amp;lt;&lt;/code&gt; that breaks the XLIFF parser on re-import.&lt;/p&gt;

&lt;p&gt;A naive round-trip during the migration window can corrupt strings in ways you won't catch without a diff. The string "AT&amp;amp;T" in source might come back as "AT&amp;amp;T" in some locales and "AT&amp;amp;amp;T" in others depending on whose tool ran double-escaping. Both will display incorrectly to users.&lt;/p&gt;

&lt;p&gt;What to do: after re-importing translator XLIFF into the catalog, scan every target string for the four common corruption patterns: &lt;code&gt;&amp;amp;amp;amp;&lt;/code&gt;, &lt;code&gt;&amp;amp;amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;amp;amp;gt;&lt;/code&gt;, and literal &lt;code&gt;&amp;lt;&lt;/code&gt; or &lt;code&gt;&amp;gt;&lt;/code&gt; in non-HTML contexts. A 20-line Python or Node script catches the entire class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Translator notes get lost
&lt;/h2&gt;

&lt;p&gt;Comments in &lt;code&gt;.strings&lt;/code&gt; look like this above each line: &lt;code&gt;/* Title for the welcome screen */&lt;/code&gt;. They become the comment field in &lt;code&gt;.xcstrings&lt;/code&gt;. They become &lt;code&gt;&amp;lt;note&amp;gt;&lt;/code&gt; elements in XLIFF.&lt;/p&gt;

&lt;p&gt;The mapping is supposed to be lossless. It often isn't. Xcode's XLIFF exporter writes plain &lt;code&gt;&amp;lt;note&amp;gt;Comment text&amp;lt;/note&amp;gt;&lt;/code&gt; elements with no &lt;code&gt;from&lt;/code&gt; attribute, and emits &lt;code&gt;&amp;lt;note/&amp;gt;&lt;/code&gt; for keys without a comment. Some translator CAT tools strip &lt;code&gt;&amp;lt;note&amp;gt;&lt;/code&gt; elements on save, treating them as their own internal metadata. Others rewrite them with a &lt;code&gt;from="translator"&lt;/code&gt; attribute that Xcode then ignores on re-import. Re-importing without manual reconciliation loses the developer context.&lt;/p&gt;

&lt;p&gt;Here's how that plays out: the next time the translator opens that key, they see "Welcome" with no context, and they translate it as a noun. The original meaning was the verb (an action your app is performing). Now your German users see "Willkommen" where you wanted "Heißen wir willkommen" because the comment that would have disambiguated this was lost two round trips ago.&lt;/p&gt;

&lt;p&gt;What to do: keep a separate source-of-truth for translator notes (a sidecar JSON or YAML file mapping key to comment), and validate after every round-trip that the comment field in &lt;code&gt;.xcstrings&lt;/code&gt; matches the canonical source. This is the kind of thing that should be a CI check, and isn't, in any tool I know of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Empty target tagging confuses translators
&lt;/h2&gt;

&lt;p&gt;When Xcode emits XLIFF from a String Catalog, a key that exists in the catalog with no target translation for German shows up as a &lt;code&gt;&amp;lt;trans-unit&amp;gt;&lt;/code&gt; with an empty &lt;code&gt;&amp;lt;target&amp;gt;&lt;/code&gt; and (depending on Xcode version) a &lt;code&gt;state&lt;/code&gt; of &lt;code&gt;new&lt;/code&gt; or no state at all.&lt;/p&gt;

&lt;p&gt;A translator opening that XLIFF in their tool sees a list of segments. Tools generally show "new" segments first. But "no state" segments may sort with existing translations, depending on the tool. The translator picks up only the explicitly-new segments and skips the no-state ones, assuming someone else already handled them.&lt;/p&gt;

&lt;p&gt;The result: keys that look translated to the translator come back untranslated in the next import. You think you sent 50 new strings; the translator thinks they handled 30 new strings; 20 mysterious empties show up in your re-imported catalog and nobody knows why.&lt;/p&gt;

&lt;p&gt;What to do: before sending XLIFF to translators, scan for every &lt;code&gt;&amp;lt;trans-unit&amp;gt;&lt;/code&gt; with an empty &lt;code&gt;&amp;lt;target&amp;gt;&lt;/code&gt; and stamp it with &lt;code&gt;state="needs-translation"&lt;/code&gt; if it isn't already stamped. This is a one-line &lt;code&gt;xmllint&lt;/code&gt; operation but, again, not built into Xcode or any major translator tool by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Variable interpolation tokens
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.strings&lt;/code&gt; files use &lt;code&gt;%@&lt;/code&gt;, &lt;code&gt;%d&lt;/code&gt;, &lt;code&gt;%lld&lt;/code&gt;, and friends. &lt;code&gt;.xcstrings&lt;/code&gt; files inherit them, and the Swift compiler enforces them at the call site via &lt;code&gt;String(localized:defaultValue:table:bundle:locale:comment:)&lt;/code&gt; and friends.&lt;/p&gt;

&lt;p&gt;What breaks: the order of tokens in the translated string. English "You have %d items in %@" might be German "Sie haben %d Artikel in %@" (same order) or French "Vous avez %d articles dans %@" (same order) or Japanese "%@に%dアイテムあります" (reversed order, requires &lt;code&gt;%2$@&lt;/code&gt; and &lt;code&gt;%1$d&lt;/code&gt; positional tokens).&lt;/p&gt;

&lt;p&gt;If your translator returns Japanese without positional tokens, Swift's runtime fills the placeholders in source order, and your Japanese users see something like "5にカートアイテムあります" instead of "カートに5アイテムあります". The grammar is wrong and the variables are in the wrong place.&lt;/p&gt;

&lt;p&gt;What to do: validate that the count and types of format specifiers in every target string match the source. If source has one &lt;code&gt;%@&lt;/code&gt; and one &lt;code&gt;%d&lt;/code&gt;, target must have exactly one of each, and if positional tokens are used, every position must be present. The Swift compiler does some of this at compile time for keys it can statically resolve, but not for runtime-loaded strings or for non-default tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you migrate: three prerequisites
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The one build setting that breaks the migration if you skip it.&lt;/strong&gt; In your project's Build Settings, search for "Use Compiler to Extract Swift Strings" and set it to &lt;code&gt;YES&lt;/code&gt;. This enables Xcode's automatic extraction of localizable strings into the catalog from your Swift source. If you skip this, the auto-migration still works on existing &lt;code&gt;.strings&lt;/code&gt; keys, but any new &lt;code&gt;String(localized:)&lt;/code&gt; call you add afterward will never make it into the catalog. You only find out months later when the Polish version is missing half your strings. This is the single most common silent failure I've seen during migrations and it's invisible until it bites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot what you have.&lt;/strong&gt; Commit the current &lt;code&gt;.strings&lt;/code&gt;, &lt;code&gt;.stringsdict&lt;/code&gt;, and your last &lt;code&gt;.xcloc&lt;/code&gt; bundle from &lt;code&gt;xcodebuild -exportLocalizations&lt;/code&gt; to version control. The &lt;code&gt;.xcloc&lt;/code&gt; is the bundle Xcode hands to translators: it contains the XLIFF plus screenshots and notes. If you've never exported before, do it now so you have a pre-migration baseline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decide on extraction mode.&lt;/strong&gt; Xcode supports &lt;code&gt;automatic&lt;/code&gt; extraction (compiler pulls strings from Swift source) and &lt;code&gt;manual&lt;/code&gt; (you maintain the catalog by hand). Most teams use &lt;code&gt;automatic&lt;/code&gt;. If you have a framework target or Swift package with localizable strings, you also need the &lt;code&gt;#bundle&lt;/code&gt; macro (Xcode 26 and later) or &lt;code&gt;Bundle.module&lt;/code&gt; for those callers. The migration plan below assumes the app target plus &lt;code&gt;automatic&lt;/code&gt; extraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  A migration plan that doesn't burn the translators
&lt;/h2&gt;

&lt;p&gt;Step 0: With the prerequisites in place, snapshot the current &lt;code&gt;.strings&lt;/code&gt; files plus the last XLIFF export sent to translators. Both go in version control.&lt;/p&gt;

&lt;p&gt;Step 1: Let Xcode auto-migrate. Right-click &lt;code&gt;Localizable.strings&lt;/code&gt; in Xcode, "Migrate to String Catalog." Xcode generates &lt;code&gt;Localizable.xcstrings&lt;/code&gt; and pulls in existing translations. Don't delete the old &lt;code&gt;.strings&lt;/code&gt; files yet. If you have a separate &lt;code&gt;InfoPlist.strings&lt;/code&gt;, it gets its own &lt;code&gt;InfoPlist.xcstrings&lt;/code&gt; and the same migration steps apply.&lt;/p&gt;

&lt;p&gt;Step 2: Validate the auto-migration with a per-locale per-key diff. For every key in the old &lt;code&gt;Localizable.strings&lt;/code&gt; for every locale, confirm the same source-target pair exists in &lt;code&gt;Localizable.xcstrings&lt;/code&gt;. A 30-line script does this.&lt;/p&gt;

&lt;p&gt;Step 3: Re-export XLIFF from the new String Catalog. Diff against the last XLIFF you sent translators. Expected differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New keys with empty targets (these are new strings to translate)&lt;/li&gt;
&lt;li&gt;Keys with &lt;code&gt;state="needs-translation"&lt;/code&gt; for previously-untranslated entries&lt;/li&gt;
&lt;li&gt;No existing translations should have changed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If existing translations changed, you have a migration bug. Stop and investigate before sending anything to translators.&lt;/p&gt;

&lt;p&gt;Step 4: Audit plural coverage per target locale against CLDR requirements. Fill any gaps in source before the first translator round-trip on the new format.&lt;/p&gt;

&lt;p&gt;Step 5: Audit translator notes survived the migration. Compare comment fields in &lt;code&gt;.xcstrings&lt;/code&gt; against your canonical source.&lt;/p&gt;

&lt;p&gt;Step 6: Send the new XLIFF to translators with a note that the format is now String Catalogs and that any state confusion should be reported back. Translators are human, tell them what changed.&lt;/p&gt;

&lt;p&gt;Step 7: After the first clean round-trip, you can delete the old &lt;code&gt;.strings&lt;/code&gt; files.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CI tooling should catch
&lt;/h2&gt;

&lt;p&gt;If you're shipping multilingual iOS apps and migrating to String Catalogs, here's the CI checklist worth automating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every key in source has a target string in every supported locale, or an explicit &lt;code&gt;needs-translation&lt;/code&gt; state.&lt;/li&gt;
&lt;li&gt;Every plural rule in source covers the CLDR categories required by each target locale.&lt;/li&gt;
&lt;li&gt;Variable interpolation tokens are preserved across source and target, with positional tokens used where source order doesn't match target word order.&lt;/li&gt;
&lt;li&gt;No string contains corrupted entity encoding (&lt;code&gt;&amp;amp;amp;amp;&lt;/code&gt;, &lt;code&gt;&amp;amp;amp;lt;&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;li&gt;Translator notes round-trip from source through XLIFF and back without loss.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Localizable.xcstrings&lt;/code&gt; parses as valid JSON without manual edits.&lt;/li&gt;
&lt;li&gt;A round-trip export-import-export produces zero drift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most iOS teams handle two or three of these in ad-hoc scripts. None ship them as a unified CI step. App Review won't flag any of these. App Store users in non-English locales will, in 1-star reviews with screenshots.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on tooling
&lt;/h2&gt;

&lt;p&gt;The TMS platforms in this space (Lokalise, Phrase, Crowdin) all support &lt;code&gt;.xcstrings&lt;/code&gt; import/export and have CI integrations of varying depth. What none of them ships out of the box is a single validation step that catches missing translations, broken CLDR plural coverage, placeholder count mismatches, and migration-drift between legacy and catalog formats, all in one tool you can drop into an existing GitHub Action. Most teams I've seen handle two or three of these in ad-hoc scripts.&lt;/p&gt;

&lt;p&gt;I'm building one. It's called LocaleLint, the free CLI plus GitHub Action are at &lt;a href="https://localelint.gen-a.dev" rel="noopener noreferrer"&gt;https://localelint.gen-a.dev&lt;/a&gt;. If you're mid-migration and want updates as it matures, sign up there. If you've already migrated and got bitten by one of the issues above, I'd love to hear what bit you most: open an issue at &lt;a href="https://github.com/YinsPeace/localelint" rel="noopener noreferrer"&gt;https://github.com/YinsPeace/localelint&lt;/a&gt; or comment on this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Migrate via Xcode's built-in tool, not by hand.&lt;/li&gt;
&lt;li&gt;Don't delete &lt;code&gt;.strings&lt;/code&gt; files until you've validated parity for every key in every locale.&lt;/li&gt;
&lt;li&gt;Re-export XLIFF and diff against your last translator-facing version before sending anything to translators.&lt;/li&gt;
&lt;li&gt;Validate plural rule coverage per target locale against CLDR.&lt;/li&gt;
&lt;li&gt;Validate variable interpolation tokens are preserved.&lt;/li&gt;
&lt;li&gt;Round-trip XLIFF export-import-export and confirm zero drift.&lt;/li&gt;
&lt;li&gt;Treat migration as a translator-coordination event, not a code-only refactor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your future German, Polish, Arabic, and Japanese users won't write you a thank-you note. They also won't write you a 1-star review, which is what you're actually buying here.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Senior full-stack engineer based in Sweden, building indie dev tools at &lt;a href="https://gen-a.dev" rel="noopener noreferrer"&gt;gen-a.dev&lt;/a&gt;. LocaleLint is at &lt;a href="https://localelint.gen-a.dev" rel="noopener noreferrer"&gt;localelint.gen-a.dev&lt;/a&gt; and on &lt;a href="https://github.com/YinsPeace/localelint" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>xcode</category>
      <category>localization</category>
    </item>
  </channel>
</rss>
