# How to Add Key Descriptions and Limits on Any Format

> Add a description, character limit and translation rules to a key in an .xcstrings, .strings, Android XML or i18next project — not just ARB.

Source: https://stringlane.app/docs/annotate-keys-any-format
Last updated: 2026-08-21

Key metadata used to be a Flutter/ARB feature: only an `@key` block could hold a description, so
the metadata button appeared on ARB projects and nowhere else. It is now available on all five
formats — `.arb`, `.xcstrings`, `.strings`, Android `strings.xml` and i18next JSON — and the panel
is the same one everywhere.

## Open the Key Metadata panel

1. Select the key you want to annotate.
2. Click the **ℹ** button — in the detail-pane header, or on the key's row in the sidebar.
3. Edit any field. Changes save automatically after a short pause; there is no Save button.
4. Close with **✕**, the Escape key, or by clicking outside the panel.

The **ℹ** is tinted when the key already has metadata and appears on row hover when it does not, so
you can see at a glance which keys are annotated.

## What you can set

| Field | What it does |
|---|---|
| **Description** | Context for translators and for AI: where the string appears, what constrains it. Passed verbatim into every AI translation prompt for that key. |
| **Max length** | Character limit. Any translation over it gets a **Length** badge, and the AI is asked to respect it. |
| **Disable character budget** | Hides the character-width preview bar for this key. A display preference, not a rule. |
| **Must not change** | Warns when a translation differs from the base value — brand names, legal text. It does **not** lock the cell or stop AI from writing one. |
| **Not for translation** | Locks the key in every locale immediately, and takes it out of the Issues panel and every AI action. It still counts toward your key count and completion percentage. |
| **Placeholders** *(ARB only)* | Types, examples and format strings for each `{name}` token. The types are Dart types, so this table appears on ARB projects and nowhere else. |

**Must not change** and **Not for translation** are two different rules and both are settable at
once. The first is a warning on a cell you can still edit; the second takes the key out of the
workflow entirely.

## Where each field is saved

Whatever your format can hold, it holds. A description written here is the `comment` in your String
Catalog, the `<!-- -->` above your Android `<string>`, the `/* … */` above your `.strings` entry —
visible to anyone on your team who opens the file in Xcode or Android Studio. **Not for
translation** writes `shouldTranslate: false` in a String Catalog and `translatable="false"` in
`strings.xml`, where Android lint and the rest of your toolchain can see it.

Everything your format has no field for goes to `.stringlane/metadata.yaml` beside your project.
That file is meant to be committed — see
[Where Key Metadata Is Stored](/docs/key-metadata-storage) for the full per-format table and
the `.gitignore` rule that silently eats it.

## Comments already in your files are imported

You do not have to retype anything. Open a project and every comment StringLane can attribute to a
key loads as that key's description — the `comment` an Xcode author wrote in a `.xcstrings` catalog,
the `<!-- -->` above a `<string>`, the `/* */` above a `.strings` entry. Edit one in StringLane and
the comment it came from is rewritten in place.

Two format-specific notes:

- **Android `translatable="false"` keys are visible now.** They used to be hidden from the editor
  entirely. They appear as locked keys, with a tooltip saying where the flag came from. Because they
  are counted like any other key, **key counts and completion percentages move** on projects that use
  the attribute — nothing was added to or removed from your files.
- **A `.strings` file header is not a description.** A `/* Localizable.strings */` block at the top
  of the file, and `genstrings`' own `No comment provided by engineer.`, are both ignored rather than
  imported onto the first key.

## Generate descriptions with AI

Writing a description for every key by hand is the reason most projects have none.

- **One key:** click **Generate with AI** in the panel. The result fills the form for you to read —
  nothing is saved until you click **Apply**, and **Discard** puts your original values back.
- **Every key:** open the [Command Palette](/docs/use-command-palette) (⌘P) and run **Generate AI
  metadata for all keys**. It processes keys in batches, shows progress and an ETA, and can be
  cancelled mid-run — partial results are kept.

The bulk run skips keys that are already complete and skips keys marked **Not for translation**
entirely: nobody translates them, so a translator-facing description would be paid for and never
read. On ARB, "complete" also means every detected placeholder has a type; on the other formats it
is the description alone.

Both paths need an AI provider configured — see [Set Up AI Translation](/docs/set-up-ai-translation),
or [Translate with a Local LLM](/docs/use-local-llm) if you would rather nothing left the machine.

## Setting max length across many keys

To set character limits without opening each key, use **Settings → Project → Key Constraints** (⌘,).
Type a key name, give it a limit, and it applies immediately.

It is the same limit as **Max length** in the metadata panel — one setting shown in two places. On
ARB it is written to `x-max-length` in the `@key` block; on every other format it goes to
`.stringlane/metadata.yaml`.

> **If you used Key Constraints before this version**, your limits were kept in the project settings
> file and that copy won over the one on the key — so on a non-ARB project a limit typed into the
> metadata panel was saved, shown back to you, and then ignored. Opening a project now moves the
> settings copy onto the key and removes it from the settings file. It happens once, on open, with
> nothing to do on your side. The one caveat: a project migrated by this version and then opened in
> an older build will have lost its limits, because the older build only knows how to read the copy
> that is no longer there.

## Related

- [Where Key Metadata Is Stored](/docs/key-metadata-storage) — the per-format table and the `.stringlane/` folder
- [How to Work with ARB Metadata Annotations](/docs/arb-metadata-fields) — the `@key` block in detail
- [How to Configure Guarded Words and Product Context](/docs/configure-guarded-words) — project-wide terms, as opposed to per-key rules

## Frequently asked questions

### Can I add a description to a key in an .xcstrings or Android project?

Yes. Select the key, click the info button in the detail-pane header or on the sidebar row, and type it into Description. It is saved as the comment on the string in a String Catalog and as the XML comment above the key in strings.xml, so the rest of your team sees it in the source file.

### How do I mark a string as not for translation?

Open the key metadata panel and tick Not for translation. The key locks in every locale straight away and drops out of the Issues panel and every AI action. It still counts toward your key count and completion percentage, so a locked key with empty locales keeps the project short of 100%. On a String Catalog it writes shouldTranslate: false, on Android it writes translatable="false", and on the other formats it is recorded in .stringlane/metadata.yaml.

### What is the difference between Must not change and Not for translation?

Must not change warns you when a translation differs from the base value — useful for brand names and legal text — but the cell stays editable and AI will still write into it. Not for translation removes the key from the workflow entirely: locked cells, no issues, no AI. Both can be set on the same key.

### Do comments already in my locale files show up in StringLane?

Yes, on open, with nothing to configure. The comment on a String Catalog string, the XML comment above an Android key, and the block comment above a .strings entry all load as that key's description. Editing the description rewrites the comment it came from rather than storing a second copy.
