# 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.

Source: https://stringlane.app/docs/what-is-a-stringsdict-file
Last updated: 2026-08-19

`.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
<?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?](/docs/icu-messageformat-explained) 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:

```xml
  <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:

```xml
    <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](/docs-screenshots/stringsdict-plural-forms.webp)

### 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](/docs-screenshots/stringsdict-width-adaptive.webp)

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](/docs-screenshots/stringsdict-unsupported-entry.webp)

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`](/docs/what-is-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

- **[iOS strings editor](/strings-editor)**: what StringLane validates in a `.strings` project.
- **[How to Use the Format Editor](/docs/use-format-editor)**: format-specific editing for iOS,
  Android, and i18next.
- **[Working with ICU Plurals and Select Forms](/docs/working-with-icu-plurals)**: the ICU side,
  for ARB and i18next projects.
- **[How to Read Validation Badges](/docs/read-validation-badges)**: what each badge means.

## 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.
