StringLane

Browse docsWhat Is a .stringsdict File? iOS Plurals and Width Variants
Concept

What Is a .stringsdict File? iOS Plurals and Width Variants

A .stringsdict is the XML plist that holds iOS plural forms and width-adaptive strings next to your .strings file. What is inside it, and how to edit it safely.

Last updated

.stringsdict is the file extension for an iOS strings dictionary: an XML property list that holds the string variants Localizable.strings cannot express. It sits beside the .strings file it belongs to, in the same <lang>.lproj folder, and carries two things — plural forms, and width-adaptive strings that change with the space available on screen.

What a .stringsdict looks like#

The file is a plist whose root dictionary is keyed by translation key. Each key maps to a dictionary holding a format key and one sub-dictionary per format variable:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>files_selected</key>
  <dict>
    <key>NSStringLocalizedFormatKey</key>
    <string>%#@count@ selected</string>
    <key>count</key>
    <dict>
      <key>NSStringFormatSpecTypeKey</key>
      <string>NSStringPluralRuleType</string>
      <key>NSStringFormatValueTypeKey</key>
      <string>d</string>
      <key>one</key>
      <string>%d file</string>
      <key>other</key>
      <string>%d files</string>
    </dict>
  </dict>
</dict>
</plist>

Four parts do the work:

  • NSStringLocalizedFormatKey is the sentence, with %#@count@ standing in for the variable. Anything around it is literal text that ships as-is.
  • count is the variable's own name — it is not a keyword. The key in the dictionary has to match the name inside %#@…@, and you can call it whatever you like.
  • NSStringFormatSpecTypeKey is the rule type: what the forms below are keyed by.
  • NSStringFormatValueTypeKey is the printf conversion for the value (d, lld, @, f, …).

Note that the forms use printf specifiers — %d, %@ — not ICU's #. A .stringsdict is not ICU MessageFormat; it is Apple's own structure for the same problem. If you also ship Flutter or i18next, see What Is ICU MessageFormat? for the contrast.

The two rule types#

NSStringPluralRuleType is the ordinary one. Its forms are CLDR plural categories — zero, one, two, few, many, other — and which of them a language actually requires varies: English needs two, Polish and Ukrainian need few and many as well, Arabic uses all six.

NSStringVariableWidthRuleType is the one most people have never used. Introduced in iOS 9, its forms are keyed by device M-width — approximately how many capital M glyphs fit in the space available — rather than by grammatical number:

  <key>bluetooth_label</key>
  <dict>
    <key>NSStringLocalizedFormatKey</key>
    <string>%#@width@</string>
    <key>width</key>
    <dict>
      <key>NSStringFormatSpecTypeKey</key>
      <string>NSStringVariableWidthRuleType</string>
      <key>20</key>
      <string>BT</string>
      <key>40</key>
      <string>Bluetooth</string>
    </dict>
  </dict>

That label renders BT on a narrow screen and Bluetooth where there is room. Width entries carry no NSStringFormatValueTypeKey — there is no number to format.

The distinction matters more than it looks. Treat a width entry as a plural and the tooling starts demanding CLDR categories it will never have: a project that can never reach 100% translated, and bulk translation that writes one and other into an entry iOS selects by width. Xcode cannot read the result.

Entries with more than one variable#

A single entry may declare several format variables, usually written positionally so translators can reorder them:

    <key>NSStringLocalizedFormatKey</key>
    <string>%1$#@files@ and %2$#@folders@</string>

Each variable then gets its own sub-dictionary with its own rule type and its own forms. This is valid and it works, but it is considerably harder for tooling to model than the single-variable case — see below.

How StringLane works with .stringsdict#

StringLane loads a .stringsdict as the companion of its .strings file: same .lproj directory, same filename, different extension. en.lproj/Localizable.strings pairs with en.lproj/Localizable.stringsdict. An absent companion is normal — not every locale ships plurals. A .lproj folder holding a .stringsdict with no matching .strings is not picked up, because the pairing is derived from the .strings entry.

Plural keys appear in the detail pane as one panel per locale with a row per form, each with its own validation rail and character-width bar. To turn an existing .strings key into a plural, hover a non-empty cell and click its {} Format helper button, switch to the Plural reference tab, and use Convert this key to plural (it appears only for keys that are not plurals already).

The itemsSelected plural key in the detail pane: English, German, French and Japanese panels each with one and other rows, French offering a CLDR-required many chip, and Japanese showing both forms as MISSING

Editing width entries#

A width-adaptive key announces itself. The panel shows a badge reading Selected by available width (NSStringVariableWidthRuleType), not by grammatical number, the form count reads "2 widths" rather than "2 forms", and the row labels are the numbers themselves.

The bluetoothLabel key with a WIDTH-ADAPTIVE badge on every locale panel, rows labelled 20 M and 40 M holding BT and Bluetooth in English and French, MISSING in German and Japanese, and an Add width input under each panel

The CLDR chip row is replaced by an Add width: input. A width has to be a plain positive integer — 20, not 020, +20, or 2,0 — because 020 and 20 are distinct plist keys but the same number, and accepting both would let you create two rows iOS reads as one threshold. Reserved names (NSStringFormatSpecTypeKey, NSStringFormatValueTypeKey) are rejected too: they live in the same dictionary as the forms and would collide with the entry's own declarations. A width that is already in your file is displayed and preserved whatever it looks like — the rule gates what StringLane will write, not what it will read.

New widths are inserted in numeric order rather than appended, so adding 30 to an entry that has 20 and 40 puts it between them. If the existing widths are not in ascending order, that is your arrangement, and the new one appends instead of imposing a sort on a file you did not ask to be reordered.

AI translation is width-aware: the model is told the number is an M-width and not a quantity, told not to invent plural categories, and told that a form's translation must be no longer than the source for that same width — otherwise the string stops fitting the screen the entry exists to serve. Validation likewise raises no "missing plural category" issue against a width entry.

Multi-variable entries are read-only#

An entry declaring more than one format variable is shown but not editable. It is marked UNSUPPORTED in the key sidebar, and its detail pane reads StringLane can't edit entries with more than one format variable. Edit this one in Xcode. — with the format string and every variable's forms rendered below it, so you can still read what is there.

The read-only panel for filesAndFolders: the message that StringLane can't edit entries with more than one format variable, the format string, and the files and folders variables each listed with their one and other forms

It is preserved exactly on save. The entry is transcribed whole when the file loads and written back from that record, so editing other keys in the same file never disturbs it.

What round-trips unchanged#

Saving a .stringsdict rebuilds the file, so anything StringLane does not record would be lost. It records, per entry:

  • the format variable's name — your %#@itemCount@ does not come back as %#@count@
  • the literal text around the variable, so %#@count@ selected keeps its trailing word
  • the value type, including its absence — an entry that declared none does not acquire one
  • the rule type, so a width entry is never rewritten as a plural
  • the order of keys inside each sub-dictionary, and the order of entries in the file

One rule overrides the recorded value: a width rule is never emitted above CLDR quantity forms. That pairing is not a valid entry in any circumstance — Xcode cannot read it — so preserving the declaration would mean writing you a broken file rather than preserving your file.

When the file gets written#

The companion is written only when the locale actually has plural or multi-variable content. Removing the last plural key is the exception that has to reach disk, or the key would return on the next load.

Before emptying a .stringsdict, StringLane re-reads it and re-parses it, and replaces it only if it parses to content StringLane recognises. An empty in-memory map is also what an unreadable plist produces, and a file whose only fault is that we do not understand it should not be destroyed on that signal.

.stringsdict vs .xcstrings#

String Catalogs (.xcstrings, Xcode 15+) fold all of this into one JSON file per string table, with plural and device variations expressed inline and a translation state per string. There is no companion file to keep in sync.

If you are starting a new project, use String Catalogs. If you are on .strings and .stringsdict, there is no urgency — Xcode still builds them, and plenty of shipping apps stay there. StringLane edits both, and handles workspaces that mix the two mid-migration.

Next steps#

Frequently asked questions

What is a .stringsdict file for?
A .strings file holds one string per key, so it cannot express "1 file" versus "3 files". The companion .stringsdict is an XML property list that stores the variants: one sub-dictionary per key, holding a form for each plural category the language needs. iOS picks the right one at runtime.
What do the numbers 20 and 40 mean in a .stringsdict?
They are device M-widths, used by entries whose rule type is NSStringVariableWidthRuleType. The number is roughly how many capital M glyphs fit in the space available, so one label can render BT on a narrow screen and Bluetooth where there is room. They are not quantities, and CLDR plural rules do not apply to them.
Do I still need a .stringsdict if I use String Catalogs?
No. A .xcstrings catalog expresses plural and device variations inline, so there is no separate file. .stringsdict is only needed for projects still on .strings, which Xcode continues to build.
Can I put plurals directly in Localizable.strings instead?
No. The .strings format is flat key-value pairs with no way to express variants, which is exactly why .stringsdict exists. The two files work as a pair: the key lives in .strings and its plural forms live in the .stringsdict beside it.