This release is about fidelity to your source files. StringLane used to rebuild a file
from the keys and text it understood, so anything it didn't show you — a comment, an
attribute, the sentence around a plural count, the exact spelling of a key — was gone
the moment you edited an unrelated string. That is fixed across every format: a file
StringLane saves is now identical to the one it opened, apart from the translations you
actually changed.
Added#
- Mark a key "Not for translation" from the metadata panel — Keys that should never be translated (a build identifier, an API base URL, an internal flag) could only be marked that way by editing the file in Xcode or Android Studio. The switch is now in the metadata panel, on every format. Turning it on locks the key's cells in every language, takes it out of the Issues panel and out of every AI action, and writes the flag where your format keeps it:
shouldTranslate: false in a String Catalog, translatable="false" in strings.xml, x-no-translate in an ARB file's @key block, and alongside your other StringLane metadata for .strings and i18next. Turning it off works even when the flag came from the file itself.
Improved#
Your files keep what you wrote in them#
- Your
Localizable.strings keeps its comments — Every comment was deleted the moment StringLane saved: the header Xcode wrote with your project name and copyright, the note above a key, the genstrings placeholder on every un-annotated line. They all survive now, and a comment you haven't edited comes back exactly as you wrote it — line breaks, indentation, /* … */ or // …. A comment directly above an entry is that key's description: it shows in the metadata panel, you can edit it there, and your edit replaces that comment in the file. Two things to know: a description you edit is rewritten as one line, and a comment that trailed an entry on the same line moves to the end of the file.
- Your Android
strings.xml keeps its comments — Same story for every <!-- -->: the licence header, the note above a key, the "TODO: translate these" at the bottom. A comment sitting directly above a <string> or <plurals> is that key's description, editable in the metadata panel and written back to the same place. A multi-line comment above a key becomes one line, so the file and the panel agree; a comment written inside a <plurals>, between its rows, comes back after the last row, because the rows are re-sorted into CLDR order on every save and no row can honestly claim it.
- Your Android
strings.xml keeps every attribute you wrote — formatted="false", tools:ignore="…" and any custom attribute on a <string> were deleted the moment you edited any other string in that file. They now come back out of a save exactly as they went in, in the order you wrote them, and the xmlns: declarations on <resources> come with them. One consequence you will see: a translatable="false" string that also carried another attribute used to be hidden from the editor entirely — that was the only way to avoid destroying the attribute — and it now appears as a locked key like any other, so key counts and completion percentages move on those projects.
- Your
<plurals> blocks keep their attributes and their notes too — A <plurals> was rebuilt from its name and its quantities on every save, so a tools:ignore on the block, a formatted="false", a note on one <item> row and the comment you wrote between two rows were all deleted. All of it comes back now, with between-row comments landing after the last row for the CLDR-ordering reason above.
- Bold, italics and
<xliff:g> inside an Android string survive now — A <string> written as Hello <b>%1$s</b> loaded as Hello — markup gone, text either side joined — and that is what got written back. On an AOSP-style project it was worse than cosmetic: <xliff:g id="name">%1$s</xliff:g> is how those files name a placeholder, so the placeholder went with it and the string came out as Hi ! in every language. Markup now stays in the value with its attributes and nesting, and the cell shows it inline so you can move a bold run when the word order changes. Plural rows are covered too. Two things to know: the tags are text in the cell, so a & beside them has to be written & — StringLane fixes a bare one rather than saving a file Android can't read — and an unclosed tag saves the whole value as plain text, visibly, rather than producing a file that won't reopen.
- A plural form with an unusual name no longer breaks your Android file —
strings.xml writes each plural row as <item quantity="…">, and the quantity was copied straight through without escaping. A file whose quantity contained a quote character came back out of a save as XML that could not be read, so the project failed to load the next time you opened it. Rare, but unrecoverable without editing the file by hand.
iOS plurals stop drifting#
- Your
.stringsdict files keep their sentence wording and number type — An iOS plural entry can wrap its count in a sentence — "There are %#@items@ left" — and can count something other than a plain Int, using %lld, %f or %@. StringLane rebuilt these from a fixed template on save, so the words around the count were thrown away and the number type reset to %d — every time, on the first save after you edited any key in that language. Both are now written back exactly as they were. Together with the variable-name fix below, an iOS plural file StringLane saves is identical to the one it opened apart from the translations you changed. (Plural keys you create inside StringLane still start from the plain %d default.)
- Width-adaptive iOS strings are no longer turned into plurals — A
.stringsdict entry can pick between wordings based on how much room is on screen rather than on a number — "BT" on a narrow label, "Bluetooth" where there's space. StringLane saved these back marked as plural rules, so iOS started choosing between them by grammatical number instead of by available width. These entries now keep their original rule. Note this is preservation, not editing support: StringLane still shows the width options as if they were plural forms and will flag them as incomplete, so leave them alone in StringLane and edit them in Xcode.
- iOS plural files keep the order you wrote them in — StringLane rewrote the entries inside a plural key in its own order. For ordinary plurals you'd never notice, but width-based entries are keyed by numbers and were silently re-sorted, so a file listing the widest option first came back with the narrowest first. Harmless to the app, but it turned every save into a diff you didn't write.
- Plural keys stay where you put them in the file — If your plural keys are plain numbers — a project keyed by error code, say
404 and 500 — saving moved them to the top of the file and sorted them lowest-first. Saving now keeps every key where it was, and a language file StringLane creates for you follows the same order as your base language.
- A language you translate inside StringLane now matches the rest of your project — When you translated a plural into a language whose
.stringsdict didn't exist yet, StringLane created it using its own defaults — %#@count@, a plain %d — even though every other language used your wording and your number type. New files now copy those declarations from your base language, so only the translations differ.
Flutter and ARB#
- A plural whose counter is not called
count keeps its name — An .arb plural written as {n, plural, one{# apple} other{# apples}} came back out of the first save as {count, plural, …} whenever the key also had a @key block below it, which is how Flutter writes them. That renames the argument in the code flutter gen-l10n generates, so every call site stops compiling.
- A nonsensical character limit in your
.arb file no longer flags every string you have — A @key block's "x-max-length" was trusted exactly as written. A 0, a negative number, 24.5 or "twenty" — from a hand edit, a merge conflict, or another tool — made the Length check fail on every non-empty translation in every language. StringLane now ignores a limit that is not a whole number of one or more, along with a description that is not text and a translation-rule flag that is not a true/false. Your file is not corrected or rewritten.
i18next keys survive round trips#
- When one key is written two ways, StringLane keeps the one your app reads — A JSON file can spell the same key in more than one way:
{"a": {"": {"b": "…"}}} and {"a.": {"b": "…"}} are both the key a..b. StringLane kept whichever came last in the file — which is not the one i18next returns — so the editor showed a value your app never used, you could translate it, and the value your app did use was thrown away on the next save. It now shows and edits exactly what i18next resolves. The other spelling stays in your file, unread but never removed.
- Your i18next file keeps the key shapes you wrote — A key written flat,
"nav.home": "Home", came back out of the first save rewritten as nested {"nav": {"home": "Home"}}. On most projects that's an unasked-for change to your file and nothing worse. If your app sets keySeparator: false, it is fatal and total: nesting leaves nothing for i18next to find, so every string in that file stops resolving the first time you edit any key in it. Keys now come back spelled exactly as they went in, and a file that mixes both ways keeps both. Keys you add inside StringLane are still written nested.
- Two spellings of one plural form no longer cost you one of them — StringLane matches the CLDR suffix on an i18next plural key without minding its case, so an
items_ONE written by an older tool sits in the same plural row as items_other. If your file held both spellings of the same form — items_one beside items_ONE — the row could only show one, and the other was deleted the first time you saved. Both stay now, and the row shows the lowercase spelling i18next actually resolves.
- A key and a path through it can both survive now — i18next lets you have
nav.home and nav.home.label at the same time; both are legal. StringLane loaded both, showed both, let you translate both — then deleted whichever came second in the file on save. Both survive now: the one that can't be written as a nested branch is written flat beside it, exactly how i18next reads it back. This also closes two places where the loss happened with no edit anywhere near it: adding a language wrote the new file already missing a key, and adding a key beside an existing one could remove it.
- An i18next key with a leading or doubled dot no longer loses its name — A key written
".b", ".", ".." or "a..b" was quietly renamed. The first save wrote it as a nested level with an empty name, and the next time you opened the project that empty level was read back as if it weren't there — so .b became b, and the original name was gone with nothing to restore it from. It took two saves, so the save that looked fine was the one that set it up. Keys with empty segments now survive any number of saves, plural keys included.
One home for descriptions and limits#
- A description or comment now lives in exactly one place — StringLane kept its own copy of every description, character limit and translation-rule flag in
.stringlane/metadata.yaml, even when your format already had somewhere to put them. Two copies meant one had to win, and StringLane's did: a comment you edited in Xcode, or a translatable="false" you cleared in Android Studio, could be quietly replaced the next time you opened the project. Each format now keeps whatever it can in your own file — a String Catalog its comment and shouldTranslate, strings.xml its translatable="false", an ARB file its whole @key block — and .stringlane/metadata.yaml holds only what your format has no room for.
- iOS and Android descriptions are your file's comment now, not a StringLane file — Now that comments can be written back, descriptions for
.strings and strings.xml projects live in the file itself, the same rule String Catalog and ARB projects already followed. A comment your teammate edits in Xcode or Android Studio is simply what you see next time you open the project. Two things to check: a description typed before this version stays in .stringlane/metadata.yaml and is no longer read, so it will look blank in the panel — type it again and it goes where the rest of your team can see it. And a description on an iOS plural key still goes to .stringlane/metadata.yaml, because a plural lives in the companion .stringsdict and that file has no comment to put it in.
- Flutter projects no longer get a
.stringlane/ folder at all — An .arb file's @key block already holds every field the metadata panel can set, so there was never anything for a second file to keep. Nothing new appears in your repository.
- A key's character limit lives in one place now — StringLane kept two copies of every max-length limit: one in your project settings file, one on the key. The settings copy won, so on an Android, iOS or i18next project a limit you typed into a key's metadata panel was saved, shown back to you, and then ignored while validation enforced the old number. Opening a project now moves any settings copy onto the key and removes it from the settings file; Settings → Key Constraints and the metadata panel are two views of the same setting, and the panel tells you which file yours is written to. One caveat: a project migrated by this version and then opened in an older one will have lost its limits.
- The metadata panel's "Guarded" checkbox said it did something it never did — Its description read "prevents AI translation". It never prevented anything: it only warned you when a translation differed from the base text, and AI translated the key like any other. It is now called Must not change, and says so, with the editor badge renamed to match. If you ticked it expecting AI to leave the key alone, that is what Not for translation does.
- Text from your locale files can no longer rewrite an AI prompt — Key names, descriptions, placeholder names and examples were pasted into the prompt exactly as they appear in the file. A newline in any of them started a new line the model read as one of StringLane's own instructions, so a locale file from a vendor, a contractor or a public repo could quietly steer what the AI wrote back into your project. They all travel as quoted data now. Your product context and branding guidelines are deliberately unchanged — those are your words from Settings, and they are meant to be instructions. One visible side effect: a description longer than 500 characters is shortened before it is sent, so a very long one can no longer crowd the source string out of the model's view. Your file keeps the full text.
- "Fix with AI" now knows what the string is for — Fixing a broken placeholder or malformed plural sent the model the source text, your current translation and the error, and nothing else. It never saw the key's description, its character limit or its placeholder notes — not on any format, including ARB — so a fix was written blind and could wander off the string's meaning or blow past its length limit. Those now go with every fix request, single or "Fix all".
- AI metadata generation stopped talking about Flutter on non-Flutter projects — When you generated descriptions for an Android, iOS, i18next or String Catalog project, StringLane introduced the task to the model as an ARB job and listed Dart types (
String, int, DateTime) as the valid placeholder types. Those only mean anything in a Flutter .arb file, and the placeholder notes the model invented for them were thrown away on arrival — generated, billed and discarded. The prompt now asks only for what your format can actually store.
- Bulk metadata generation skips keys you marked "Not for translation" — A description is there to tell a translator what a string means, and nobody translates these keys. "Translate all missing" has skipped them for a while; ✦ Metadata was still sending them to the model and charging you for descriptions no one would read. Generating for a single key from the metadata panel still works — that one you asked for.
Multi-package projects#
- Adding a language now reaches every package, including ones translated differently from the rest — StringLane worked out where to put new files by copying the layout of a single existing language — whichever one appeared in the most packages. A package that didn't ship that language had nothing to copy from and silently got no file: you'd add Italian and one package would never receive it, with nothing in the dialog to say so. Each package now works out its own layout from a language it actually has, so every package you tick gets its file, and the preview in the dialog shows the same list that gets created.
- A package written in a different base language no longer looks untranslated — If your project mixes packages that don't share a base language — an app written in English alongside a module written in German — keys belonging to the German module had nothing to show in the base column and read as missing even though the text was there. That column now shows the module's own base-language text, greyed out and tagged with the language it came from. It is shown for information only: the key still counts as missing in your project's base language, and you can still type the base translation yourself.
Fixed#
- AI translations now actually reach your files — "Translate all missing" applied its results, showed them in the editor and reported success, but for any key missing from a language file entirely, nothing was written to disk. You'd reopen the project and every translation was gone, along with the AI credits you spent generating them. Nothing warned you: the run reported success and the values were visibly there until you reloaded. This affected every AI action that fills a key a language file didn't already contain, including "Fix all with AI".
- Your Android
strings.xml keeps everything StringLane doesn't edit — Anything that isn't a translatable <string> or <plurals> was silently deleted the moment you edited any other string in that file: <string-array> lists, strings marked translatable="false" (API URLs, feature flags, build config), <bool> and <integer> values. They now come back out of a save exactly as they went in. StringLane still doesn't let you edit them, it just no longer throws them away.
- Android projects whose default language isn't English now load correctly — If your
res/values/strings.xml is in Russian, German or any language other than English, StringLane assumed English. When the project also had a res/values-en/ folder, both files claimed English and one quietly replaced the other — so one of your two files vanished from the editor, along with any string that only existed in it. StringLane now reads the tools:locale attribute from the <resources> tag (the same one Android Studio writes) and keeps it on save. If a base file doesn't declare a language and its assumed English clashes with a real values-en/ folder, you get an error banner naming the file instead of silently losing it.
- iOS String Catalogs keep their "needs review" marks — Xcode marks a translation
needs_review when the English text changed after it was translated — the signal that tells you what has drifted, and the main reason to use a String Catalog. Saving from StringLane relabelled every one of those as "translated", across every language, after you edited a single unrelated key. Xcode's own "what still needs translating" view was wrong from then on, with no way to get the marks back. StringLane now only changes a translation's state when you actually change that translation.
- The Needs Review badge shows up — String Catalog entries awaiting review looked identical to finished ones in the editor; the amber Needs Review badge could never appear for a real catalog. It now does.
- Strings Xcode marked as not-for-translation are no longer counted as errors — A String Catalog key set to "don't translate" was reported as a missing translation in every language while the editor correctly locked those cells. The error count was inflated by entries working exactly as intended, your project could never reach 100%, and the bulk AI actions offered to translate them. They're now skipped by the checks and left out of the bulk actions.
- Multi-target iOS projects no longer lose comments and settings in
.xcstrings files — In a project with more than one catalog (an app plus a framework or extension), saving rebuilt each catalog from scratch, wiping your string comments, Xcode's extraction state, and keys marked "don't translate" — the last of which disappeared from the file entirely. Every catalog now keeps its own original contents, and saving only updates the translations you changed.
- Multi-target iOS projects no longer reformat your
.strings files — If a .strings file used unquoted keys (greeting = "Hello";), saving a project with more than one iOS target rewrote every key into the quoted form, producing a large and pointless diff. Each file's original style is now preserved, matching what single-target projects already did.
- Your
.stringsdict files keep their own plural variable name — If an entry used a variable other than count — %#@items@, %#@photoCount@ — saving rewrote it to %#@count@. Nothing broke in Xcode, but the name you chose was gone and every save showed up as a diff in source control. Each entry is now written back with the variable name it came with.
- iOS plural files that use two variables in one string now open, and are never damaged — An entry can combine two plurals in one sentence — "3 files and 2 folders", where files and folders each have their own wording. StringLane's plural editor models one variable per key, so it read just the first, ignored the second, and then — the next time you edited anything in that language — saved the file back with the second variable deleted, silently and permanently. StringLane now reads the whole entry and keeps it: the file opens normally, every other key stays editable, and the two-variable entry appears with an UNSUPPORTED badge and a read-only view pointing you to Xcode. It is written back exactly as you wrote it whenever the file saves, left out of your completeness percentage, raises no missing-translation issues, and is never sent to AI. You can delete or rename it from StringLane; to change its wording, edit it in Xcode. As a bonus, plural entries written with numbered variables (like
%1$#@count@) now load correctly — they used to be invisible in the editor and were deleted on the next save.
- A broken translation file is no longer wiped out when you save — If one of your translation files couldn't be read when the project opened — a stray comma in
de/billing.json — StringLane told you about it, then quietly emptied that file the next time you edited anything else in that language, replacing it with {}. A one-character mistake turned into the whole namespace gone. StringLane now leaves unreadable files strictly alone: fix the file, reopen the project, and everything is still there. If you type a translation into a key that lives in an unreadable file, the save indicator turns red and stays red rather than pretending it saved. (Only i18next namespaced projects were affected.)
- Editing a translation file StringLane created for you now keeps saving — If a language was missing one of your namespace files — you have
en/billing.json but no de/billing.json — StringLane created it the first time you translated a key into it, exactly as it should. But every save after that turned the save indicator red and never reached disk: StringLane mistook the file it had just written for one it had failed to read. Your second and later edits to that namespace were lost until you reopened the project. Files StringLane creates now save normally from then on.
- Saving no longer creates empty translation files for namespaces a language doesn't use — StringLane rewrote every one of a language's files each time you edited any of them, and where a file didn't exist yet it got created containing
{}. So a project that ships billing.json only in English would sprout an empty billing.json in German, French and every other language the first time you saved. A file is now only created once you actually put a translation in it. Files that already exist still save normally, including when you delete the last key in one, and adding a new language still creates all of its files as before.
- Adding a language to packages StringLane can't place it in now tells you — In a multi-package project, if StringLane couldn't work out where a new language's file belonged in any of the packages you picked, it added the language anyway. You'd get a new empty column that never saved and no explanation. It now says it couldn't place the language instead of adding one that can't work.
- Picking packages for a new language now selects the one you clicked — In a project with two packages whose folder names differ only by punctuation —
web-app and web.app side by side — clicking a package's name in the Add or Remove Language list ticked the first of the two instead of the one you clicked. Clicking the checkbox itself always worked, so the wrong package could be selected without you noticing. Each row is now its own control.
- Renaming a key and pressing ⌘T straight away now translates the new name — If you typed a new name into the key title and then pressed ⌘T (Ctrl+T on Windows/Linux) instead of Enter, StringLane renamed the key correctly but then translated the old name, which no longer existed. You'd usually get a translation error, and in some cases a stray entry under the old name was written into your language files.