StringLane

Release Notes

What's new in StringLane. The desktop app and the free stringlane CLI ship separately, so their version numbers are unrelated.

Showing 21 of 21 release notes.

v1.6.0

Desktop

Added#

  • A warning for an i18next count with no plural forms. i18next uses a key's _one and _other forms only when they exist, so "{{count}} genres" reads "1 genres", and in Russian "1 жанров". The Issues panel now shows Count without plural forms on such a key in your source language. A label that shows the number apart from the words, such as Found: {{count}}, is not marked.
  • Add plural forms from the Issues panel. A Count without plural forms row has an Add plural forms… button. Write the forms your source language needs, such as one and other in English, and StringLane adds them to that key's source file. Unsaved edits are saved first, and nothing is written if saving fails. No AI is used. The dialog reads {{name}} and {{- name}} placeholders, and refuses a form that drops one or writes it the other way. A note then lists the forms each other language still needs.

Improved#

  • Fewer "Same as source" warnings for names and terms. When at least 3 of your languages keep a value identical to your source, and they are at least half of the languages that have the key, the Issues panel lists it as Same as source at info level, not as a warning. The cell keeps its Same badge, whose tooltip says how many languages kept the value, and its rail no longer turns amber, so names and terms such as SSH or DLC no longer crowd out the language that has a real gap. A value that reads as a sentence, such as "Link copied to clipboard", stays a warning. A regional variant of your source language, such as British English next to English, does not count.
  • Issue messages read as plain sentences. Validation findings, "not for translation" tooltips and the hints shown when no locale files are found use commas, colons and full stops where they used long dashes. The wording is otherwise the same.

Fixed#

  • i18next: plural forms a language holds under a single source string are kept. If your source holds "nGenres": "{{count}} genres" and a translation holds only nGenres_one, nGenres_few and so on, i18next shows those forms when your app passes count. StringLane reported the key as missing and offered it to Translate all missing as one string. Those forms are now that language's translation: each is checked against the source text, a missing form is reported by name, the cell is locked, and the forms count toward completion. Edit them in the JSON file.

v0.9.0

CLI

Added#

  • check warns on an i18next count with no plural forms. i18next uses a key's _one and _other forms only when they exist, so "{{count}} genres" reads "1 genres", and in Russian "1 жанров" and "2 жанров". check, validate and the MCP server report such a key, {{- count}} included, as count_without_plural, a warning, once on your source language. A label that shows the number apart from the words, such as Found: {{count}}, is not reported. Like any warning, it makes check exit 1 until you add the forms or record it in your baseline.
  • stringlane pluralize gives a key its plural forms. stringlane pluralize --key item_count --one "You have {{count}} item" --other "You have {{count}} items" replaces the key's single string with the plural forms your base language needs, in the base file only, and lists what each other locale still owes. It shows the file and the forms and asks first; with no terminal it needs --yes, and --json without --yes prints the change and writes nothing. It reads {{name}} and {{- name}}, and refuses a form that drops a placeholder or writes it the other way. It works in namespaced i18next projects. Then run stringlane update or stringlane plan to translate the new forms.
  • The MCP server has a pluralize_key tool. The same fix for your agent: it sends the key and the base language's forms, StringLane writes them to the base file and lists what each locale still owes, and prepare_translation_plan then offers it. There is no preview, so the call writes at once and the tool is marked destructive; review the forms in the diff. The server now has eleven tools, six of which write. i18next projects only. The write guard's prompt for a hand edit of a locale file names pluralize_key too.
  • The agent plugin installs in Cursor. It brings the MCP server, the commands, the skills and the write guard. Cursor's hooks cannot ask, so the guard refuses a hand edit to a locale file and names the tool to use instead. Its Cursor Marketplace listing is not live yet: until it is, copy plugins/stringlane from a clone of thebedcoder/stringlane-plugin into ~/.cursor/plugins/local/stringlane and reload the window.
  • Gemini CLI installs the plugin from its URL. gemini extensions install https://github.com/thebedcoder/stringlane-plugin now works, with no clone: each plugin version is a release on that repository, and Gemini's update check follows those releases.

Improved#

  • A name or term most of your languages keep is info, not a warning. A translation identical to your source is untranslated, and on real projects most of these are names and terms kept on purpose, such as SSH or DLC. When at least 3 of your languages keep the same value, and they are at least half of the languages that have the key, check and the MCP server report it at info level and say how many kept it. A value that reads as a sentence, such as "Try again.", stays a warning. A language of your source's own family, such as en-GB under en, does not count. The code is the same, so your baseline still matches and --fail-on untranslated still fails on it. On nine public projects, untranslated warnings went from 8,742 to 3,129.
  • Output reads as plain sentences. Help text, reports, refusals, what the MCP tools return, the agent rules block, the comments init writes into stringlane.yaml, the header add-locale writes into a new Localizable.strings, and the skills setup skills writes use commas, colons and full stops where they used long dashes. The meaning is the same.

Fixed#

  • i18next: plural forms a language holds under a single source string are kept. If your source holds "nGenres": "{{count}} genres" and a translation holds only nGenres_one, nGenres_few and so on, check reported missing_key, plan offered the key as one string, and apply wrote it by deleting every form. Now each form is checked against the source text and only a form the language lacks is reported, by name. inspect counts the forms, read_translations returns them, and plan and update no longer offer the key, so apply keeps the forms.
  • check and inspect say what their completion percentage counts. The line above the table read 86% complete while its rows, which count keys, added up to 89%. The percentage counts strings, each plural form being one, and now reads 86% of strings filled. --json is unchanged.
  • Gemini CLI: the plugin's MCP server connects without a global stringlane. It showed "Disconnected" unless the CLI was installed globally. It now runs the plugin's own launcher, which falls back to npx.
  • On Cursor and Codex, the plugin's write guard names the right off-switch. Its refusal said to run stringlane setup hooks --host <host> --remove, which cannot remove a hook the plugin ships. It now points to Cursor's Customize settings or Codex's /hooks.

v1.5.1

Desktop

Improved#

  • A missing many form in French, Spanish, Italian, Portuguese or Catalan is a warning. These languages use many only for counts like 1,000,000, so the Issues panel now marks a translation without it as a warning that says so, not an error. It still counts against completion and is still offered for translation. In Ukrainian, Russian, Polish and Arabic, where many covers counts like 5 or 11, it stays an error.
  • Two new checks for Flutter projects. Plural form overridden marks a plural branch Flutter never shows, because the plural holds both =1 and one (or =0 and zero, =2 and two). Not in the source marks a key that a translation file holds and your source file does not. Your source language is no longer counted as missing such a key.
  • Flutter: an error for a plural form Flutter cannot build. Flutter builds only =0, =1 and =2 as exact plural forms, and flutter gen-l10n fails on a file that holds any other, such as =3. The Issues panel now shows Plural form Flutter rejects on that form, in every language including your source.
  • Fewer false warnings in Flutter projects. A British or American English file next to an English source is no longer marked untranslated, and two keys whose source text differs only in capital letters, such as "Open File" and "Open file", are no longer reported as sharing one translation.

Fixed#

  • i18next ordinal plurals use ordinal forms. A key written as place_ordinal_one, place_ordinal_two and so on is an ordinal, which i18next picks by each language's ordinal rules: 1st, 2nd, 3rd in English. The Issues panel, the completion percentage, the Add form chips and AI translate now use those forms, so a correct Italian many and other pair no longer shows a missing one, and English is asked for two and few. Your file keeps its _ordinal_ spelling.
  • AI translate is told when an i18next key is an ordinal. Your AI provider is now given the language's ordinal forms, each one a position (1st, 2nd, 3rd), instead of its ordinary plural forms. Italian many is asked for as the 8th, 11th and 80th, not as the form for exact millions.
  • Flutter: a plural translated in StringLane keeps your source's variable. A plural added to a language through the plural editor or AI translate was saved as {count, plural, …} even when your source used another name, such as {n, plural, …}, and flutter gen-l10n then reported count as unknown. It is now saved with your source's variable.
  • Saving a Flutter ARB file keeps its blank lines. A save no longer removes the blank lines between key groups or adds "@@locale" to a file that did not have it.
  • The width bar measures an inline plural by its longest form. A correct translation of a plural with text around it no longer shows a red bar because several forms were measured together.

v0.8.0

CLI

Added#

  • check warns when Flutter never shows a plural branch. A Flutter plural that holds both =1 and one, =0 and zero, or =2 and two has one dead branch: Flutter treats each pair as one form and keeps the later one. check, validate and the MCP server report it as icu_plural_selector_overridden, a warning, on the branch that is never shown.
  • check fails on a Flutter plural form that stops flutter gen-l10n. Flutter builds only =0, =1 and =2 as exact plural forms, and a plural with any other, such as =3{three items}, makes flutter gen-l10n fail on the whole file. It is reported as icu_plural_selector_unsupported, an error, in every language including your source, and apply refuses a translation that adds one. It is an error, so check exits 1 on such a project after you upgrade.
  • Keys that only a translation holds are reported. A key in a translation file but not in your source file is reported as orphan_key, at info level, in each language that holds it. Add it to the source, or remove it from the translation.

Improved#

  • A missing many that only exact millions use is a warning. In French, Spanish, Italian, Portuguese and Catalan, many is used only for counts like 1,000,000, so a first check could show hundreds of errors for counts the app almost never shows. It is now a warning that says so, under the same code, so your baseline still matches it. It still counts against completion and plan still offers it: with i18next, a count of 1,000,000 shows your fallback language's text, not your other form. In Ukrainian, Russian, Polish and Arabic, where many covers counts like 5 or 11, it is still an error.
  • Each language's progress is counted in keys. check and inspect counted plural forms, so a language that needs more forms had a larger total. Every row now counts keys out of the same total, and --json adds totalKeys, filledKeys and missingKeys to each row.
  • A plan keeps a Flutter plural's exact zero text. plan also offers =0, =1 or =2 where Flutter can show them, such as =0 in Ukrainian, so "No notifications" is no longer lost. It does not offer =1 to a language that has a one form.

Fixed#

  • i18next ordinal plurals are checked against ordinal forms. place_ordinal_one, place_ordinal_two and the rest are now checked against each language's ordinal forms (1st, 2nd, 3rd in English), not its ordinary plural forms. A correct Italian _ordinal_many and _ordinal_other pair is no longer reported as missing one. check may report new errors for English ordinals without _ordinal_two or _ordinal_few, and exits 1 until you add them.
  • stringlane update asks for an i18next ordinal's own forms. For a key like place_ordinal_*, the AI prompt listed the language's ordinary plural forms and called Italian many the form for exact millions. It now names the language's ordinal forms, each one a position (1st, 2nd, 3rd), so Italian many is asked for as the 8th, 11th and 80th. Prompts for other plural keys are unchanged.
  • apply keeps the layout of a Flutter ARB file. It keeps the blank lines between key groups, adds no @@locale, and puts each new key where your source file has it.
  • Your source language is no longer counted as missing keys that only a translation holds.
  • A British or American English file is not called untranslated. A regional variant of your source language in the same script no longer gets untranslated. Traditional Chinese next to a Simplified source still does.
  • reused_translation ignores case. "Open File" and "Open file" are one meaning.
  • An inline plural is measured by its longest form. expansion_budget_exceeded compares the longest form each side shows, not the whole ICU text.

v1.5.0

Desktop

Added#

  • See which translations are out of date, whoever wrote them — Turn on Track stale translations in Settings → General → Source stamps. StringLane records which base text each translation was confirmed against, in .stringlane/source-stamps.json. When a base text changes, its translations get an amber Stale badge with a note showing the base text Was and Now; a translation nobody confirmed yet is Unconfirmed. Editing a translation confirms it when auto-save writes it, and Accept translation confirms one that still fits as it stands — in an Xcode string catalog it also sets a unit marked for review to translated. Filter the sidebar to Stale or unconfirmed to find them. The same file works with stringlane check and stringlane accept, so your team and your CI share one record.

Improved#

  • iOS: a warning for the width format Apple ignores — A .stringsdict width entry under a format variable (%#@w@) is not resolved by Apple's Foundation: the app shows %#@w@, and can crash. The Issues panel now shows Width format Apple ignores on each language that has one. Move the widths directly under the key with NSStringVariableWidthRuleType.

Fixed#

  • iOS .stringsdict: device and width variants are kept, and device variants are editable — Apple's own formats for device variants and width variants were not read, and the next save deleted them. StringLane now shows one row per device (with Add device chips) and per width, checks them, and saves them back in the same format. An entry with both, or with a value that is not text, is kept read-only and marked UNSUPPORTED. A comment above a key in both .strings and .stringsdict is kept too.
  • iOS: a plural or width variant in a language's .stringsdict is that language's translation — not a missing key, and typing in the cell no longer deletes it. The forms show read-only and the cell is locked; edit them in Xcode.
  • A damaged iOS plurals file is reported — a .stringsdict broken by a git conflict or a cut-off write is a load error naming the file, and StringLane never writes over it.
  • String Catalogs — keys with no English entry show their own text as English and count in every language; stale strings no longer count as missing, get no source warnings, and are not added to a new language; a language that varies a string by plural on its own is no longer missing, and its forms show read-only.
  • Android regional folders — values-en-rGB/, values-fr-rCA/ or values-b+es+419/ owe only the strings they define. Inherited strings show the parent language's text instead of Missing, and are no longer offered to the AI.
  • Android — new regional languages get a folder Android reads (values-fr-rCA/, values-b+zh+Hant/), b+ folders are checked for every plural form their language needs, and Canadian French gets the Canadian spacing rule.
  • Renaming a plural key keeps its plural forms.
  • Tab and Enter on plural and select rows no longer get stuck when the next language does not show the row.
  • The key info button is highlighted only when a key has metadata you wrote.
  • Swift: keys used in SwiftUI are no longer reported unused — every string in your Swift code counts as a possible use of a key, string interpolation included.
  • Format specifiers alone are copied, not translated — %lld or %.2f is no longer sent to the AI.
  • Strings not for translation no longer lower the completion percentage, and the Translate… popover's "missing" count matches what it translates.
  • Flutter: a plural spelled differently in a translation is no longer reported missing, and Translate all locales no longer replaces it.
  • i18next — a path: in stringlane.yaml opens that folder alone, and opening src/i18n or a locales folder directly finds its languages.
  • Keys named constructor, toString or __proto__ are checked, translated and kept like any other key.

v0.7.0

CLI

Added#

  • Know which translations are out of date, whoever wrote them — Add sourceStamps: true to stringlane.yaml, run stringlane accept . --all once, and commit .stringlane/source-stamps.json. In a new project that loads cleanly, stringlane init offers to do this for you. check then reports translation_stale when a translation's base text changed since it was confirmed, and translation_unconfirmed when nobody confirmed it. Both are warnings, so check exits 1. plan, update and the MCP server offer a stale translation again with the text it was confirmed against, and apply and update record what they write. Without the flag, every command works as before.
  • stringlane accept confirms translations you checked — --all confirms every translation; --key, --locale and --quantity select some. It shows the counts per locale and asks first, and needs --yes without a terminal. There is no MCP tool for it, on purpose: confirming a translation is a person's decision.
  • Translations Xcode marks for review stay visible until you confirm them — With sourceStamps: true, check reports a .xcstrings translation marked for review, and plan offers it again. Confirming it with accept, or writing it with apply, update or the MCP server, marks it translated in the catalog in the same write. accept --all and init leave these alone.
  • A baseline cannot hide a stale translation — baseline write leaves out translation_stale and translation_unconfirmed. check refuses a baseline that holds either code and exits 2.
  • The write guard asks before an agent edits .stringlane/source-stamps.json, as it does for stringlane.yaml. A shell stringlane accept --yes is not visible to the guard: review the file in pull requests.

Improved#

  • Swift: every string in your code counts as a possible use of a key — A key passed to your own views, returned from a case or given as a labelled argument was unused_key. On three public apps the unused keys went from 802 to 9, 872 to 12 and 278 to 108, and a possible use never reports a key as undefined.
  • iOS: check warns on the width format Apple ignores — A .stringsdict width entry under a format variable (%#@w@) is reported as framed_width_variant. It is a warning, so check exits 1 on such a project after you upgrade.
  • The MCP server sends a bounded list of stale and unconfirmed translations — at most 50 of each, with a STAMP_ISSUES_TRUNCATED warning. issueCounts keeps the true totals, and stringlane check --json gives the full list.
  • The code usage scan stays fast on files built to slow it down — Each file's work is limited in proportion to its size, so a crafted file in a cloned repository can no longer stall scan or check.

Fixed#

  • Android regional folders are checked for what they override — values-en-rGB/, values-fr-rCA/ or values-b+es+419/ owe only the strings they define, as Android reads them. One public app had 8,059 false missing_key.
  • check asks Android b+ folders for every plural form their language needs. These are errors, so check may fail on a project that passed before. Canadian French folders also get the Canadian spacing rule.
  • add-locale writes the folder Android reads — fr-CA now creates values-fr-rCA/ and zh-Hant creates values-b+zh+Hant/.
  • iOS .stringsdict: device variants, Apple's width format, and entries with both are kept — apply no longer deletes them; device and width variants are read, checked and translated, and an entry StringLane cannot edit is kept as it is and no longer reported missing. A comment above a key in both .strings and .stringsdict is kept too.
  • iOS: a .stringsdict plural or width variant under a .strings string is the translation — not a missing key that apply would overwrite.
  • A damaged .stringsdict is a load error that names the file, instead of loading with no plurals or both sides of a git conflict.
  • String Catalogs — keys with no English entry are checked and translated, stale strings owe no translation and get no source warnings, a language that varies a string by plural on its own is no longer missing, and add-locale adds no stale entries and counts only the keys the language needs.
  • ARB: a plural spelled differently in a translation is no longer reported missing, and plan and update no longer offer to replace it.
  • Swift: SwiftUI keys are no longer unused_key, and interpolated keys are no longer undefined_key.
  • Format specifiers alone are copied, not translated — %lld or %.2f is no longer sent to the model.
  • inspect coverage no longer counts strings not for translation as missing.
  • A path: in stringlane.yaml loads that folder alone, and i18next projects with one folder per language load from src/i18n or the locales folder itself.
  • MCP — read_translations returns a translation held in the other plural shape, and the usage warning names a tool the server has.
  • Telemetry — Turning usage data off with DO_NOT_TRACK=1, STRINGLANE_TELEMETRY=0 or in CI no longer creates an install ID, and stringlane telemetry status no longer creates one either. telemetry on no longer suggests a variable that does nothing.

v1.4.1

Desktop

Fixed#

  • No false Param badge on a quoted Flutter placeholder — A Flutter string like Remove '{name}' from your profile? was read as having no placeholder, so every translation that kept {name} showed a Param badge, and one that really dropped it showed none. Flutter treats ' as ordinary text unless your l10n.yaml sets use-escaping: true, so {name} there is a placeholder with quote marks around it. StringLane now reads it the same way, and follows use-escaping: true when your project sets it. An apostrophe right before a brace, as in other{the divers'} or French l'{name}, no longer shows an ICU error; a plural written that way opens with one row per form and saves with your apostrophes intact; the select and ICU editors open it; and AI translation no longer refuses a translation that keeps a quoted {name}.
  • ICU messages in regional locales are no longer reported as syntax errors — In a locale whose code names a region, such as en_GB, pt_BR or zh_CN, every plural or select written inside a string was reported as ICU syntax error, even when it was correct. The same happened in Android locales such as values-zh-rCN, and in Flutter's en_XA pseudo-locale. A correct message is no longer reported. A message with a real syntax mistake still is.

v0.6.1

CLI

Fixed#

  • check reads apostrophes in ARB files the way Flutter does — A value like Remove '{name}' from your profile? was read as having no placeholder, so every translation that kept {name} was reported as placeholder_mismatch — extra: name, and a translation that really dropped a quoted placeholder passed. Flutter's gen_l10n treats ' as ordinary text unless l10n.yaml sets use-escaping: true, so {name} there is a placeholder with quote marks around it. check now reads it that way, and follows use-escaping: true when your l10n.yaml sets it. An apostrophe right before a brace, as in other{the divers'} or one{l'{name}}, is no longer an icu_parse_error; such a plural is checked for its forms and select branches; and update and the MCP server no longer refuse an AI translation that keeps a quoted {name}.
  • icu_parse_error no longer fires for every ICU message in a regional locale — In a locale whose code names a region, such as en_GB, pt_BR or zh_CN, every plural or select written inside a string was reported as icu_parse_error with "RangeError: Incorrect locale information provided", so check failed on correct projects. The same happened for Android locales such as values-zh-rCN and for Flutter's en_XA pseudo-locale. A correct message is no longer reported; a real syntax mistake still is.
  • update no longer writes a literal # into Flutter plurals — When the model answered an ARB plural with # for the count, as in # Artikel, update wrote it as is, and Flutter shows # rather than the number. It now writes the plural's own variable, such as {count} Artikel, the way the desktop app already did.
  • One bad plural answer no longer stops update — When the model answered a plural with a placeholder the key does not have (such as {anzahl}) or a broken brace, update and the MCP server refused to write any translation in the run. That key is now left untranslated and named, and every other translation is written.

v1.4.0

Desktop

This release is about plurals and about not losing what is already in your files. ICU plurals inside a longer string are now checked and translated with the forms the target language actually uses, and a long list of saves that could drop a key, a line ending, a catalog or a setting now keep them.

Added#

  • Choose how many AI requests run at once — Settings → AI → Concurrent requests sets how many requests bulk translate, Translate all locales and bulk metadata send at a time, from 1 to 8. The default stays 2. Raise it if your provider plan allows more requests per minute.

Improved#

  • Inline plurals are checked for the forms each language needs — A plural with text around it, such as You have {n, plural, one {# item} other {# items}} left, was only checked for an other form. A Polish, Russian or Ukrainian translation without few and many is now reported as Missing plural category, as is a French, Spanish, Italian, Portuguese or Catalan one without many. Fix with AI adds exactly the missing forms. These are errors, so existing projects may see new ones after updating. easy_localization projects are not asked for few and many.
  • Inline plurals translate with the target language's own forms — Translating such a plural into Polish, Russian, Ukrainian, Czech, Arabic and similar languages used to keep English's one and other, and a translation that added the missing forms was refused. The AI is now told which forms the language needs, and a translation that adds exactly those is accepted.
  • No AI cost for values with nothing to translate — A value made only of placeholders, numbers or punctuation, such as {name} or —, is copied without an AI call, and is no longer reported as untranslated. Text in braces that reads as words, such as {Feature Name}, is still translated.
  • The ICU editor opens more messages in Build mode — A plural or select with placeholders beside it now opens in Build mode, with the surrounding text shown and kept exactly as it was. A plural offset: is no longer dropped on save.
  • easy_localization and slang projects are checked by their own rules — Placeholders are checked in each package's own syntax ({}, {name}, $name), and a nested map of plural forms is one plural key, checked for the forms each language needs and saved back where it was.

Fixed#

  • Edits save to your files again — In 1.3.0 no edit reached disk: every change stayed in the app and the status bar showed Error saving. Dragging a single translation file onto the window also opens its project folder again.
  • Quitting right after an edit no longer loses it — Quit now saves first, and asks Quit Without Saving or Keep Open if an edit cannot be saved.
  • Changes made outside StringLane show up in every project layout — A git pull or another editor changing a file in lib/l10n/, an .lproj folder, res/values-* or locales/ now shows up by itself. Projects combining several modules or namespaces still ask before saving rather than reloading.
  • A change made outside StringLane is no longer overwritten when you had a cell open — Typing into a cell that was opened before the file changed on disk now stops and asks, instead of saving the old text over the change.
  • String Catalogs keep every language — Removing a language from an .xcstrings project deleted the whole catalog. It now saves the catalog without that language. Deleted keys and removed languages also stay gone instead of coming back on reopen.
  • Removing a language from an iOS .strings project removes its .stringsdict too, and lists it in the confirmation.
  • Saves keep Windows line endings and a byte order mark — A file is saved with the line endings and BOM it had, and files that start with a BOM now open in every format.
  • =1 counts as the "one" form where it covers it — A Flutter-style =0 / =1 / other plural is no longer reported as missing one in German, Polish, Spanish and similar languages.
  • ICU messages with quote escapes are left exactly as written — Editing a plural quantity, a select branch or applying the # fix no longer turns '{'Name'}' into a placeholder or a broken message.
  • Flutter plurals with markup in a form stay plurals — An ARB plural with a tag such as <b> in one form opens with one row per form again.
  • i18next files no longer lose keys when a new key shares a name with a dotted nesting level, and a capitalised plural form such as items_ONE is kept on save.
  • Android text that looks like an XML entity is kept — &amp;lt; no longer opens as < and saves back changed.
  • Changing a project setting keeps the rest of stringlane.yaml — Fields the app does not know, such as defaultNamespace:, are left as they were, and an unreadable file is not overwritten.
  • Namespaced i18next projects no longer show live keys as unused — t('home.title') now counts as a use of the key in the default namespace.
  • A symlinked file in a namespaced i18next project is no longer followed — it is listed in the load errors instead.
  • Your licence is checked again each time the app starts — in the background; if the server cannot be reached you keep working.
  • The usage-data switch no longer calls the data anonymous — it now reads Share usage data and says how the device ID is derived. What is sent has not changed.

v0.6.0

CLI

Added#

  • stringlane update --parallelism <1-8> — Sets how many AI requests run at once. The default stays 2. STRINGLANE_AI_PARALLELISM sets it for every run, and the flag wins. A value outside 1–8 exits 2 before anything is sent.
  • update says how many items replace an existing translation — The spend question says how many items overwrite a translation whose source or context changed, and with --yes the report says it after the write. --json gains retranslatedCount.

Improved#

  • plan offers a translation again when its source text changed — Edit the English, a description or other context after an apply, and plan now offers that cell again as source-changed or context-changed, instead of saying every locale is complete. A cell somebody re-translated by hand is left alone. This works for translations applied by 0.6.0 or later: one applied by 0.5.0 carries no record of the text it wrote, so it is left alone as before. Empty it and run plan and apply again to bring it under the new rule.
  • Inline plurals are checked for the forms each language needs — A plural with text around it, such as You have {n, plural, one {# item} other {# items}} left, was only checked for other. A Polish, Russian or Ukrainian translation without few and many is now an icu_missing_plural_category error, as is a French, Spanish, Italian, Portuguese or Catalan one without many. easy_localization projects are not asked for few and many. A Flutter-style =1 now counts as the one form wherever it covers it. These are errors, so check may fail after you upgrade on a project that passed before; the printf check below can do the same. Fix the findings, or re-record your baseline with stringlane baseline write.
  • Inline plurals translate with the target language's own forms — update and the MCP server used to leave such a plural untranslated when the model correctly added few or many. The model is now told which forms the language needs, and an answer that adds exactly those is accepted.
  • One broken AI answer no longer stops the run — An answer that drops a placeholder or a plural branch is left untranslated and named, and every other translation is written. Before, one such value made the whole run refuse to write.
  • No AI request for values with nothing to translate — A value made only of placeholders, numbers or punctuation, such as {name} or —, is copied without a request, and check no longer calls the copy untranslated.
  • check compares the types in printf placeholders — In .strings, .xcstrings and Android XML, %d items against %s Elemente used to pass. It crashes on Android and is undefined behaviour on iOS, and is now a format_placeholder_mismatch.
  • plan is several times faster on projects with product context.

Fixed#

  • Ctrl+C during a write no longer leaves a half-finished save — apply, add-key, add-locale, describe and the MCP server's writing tools now finish the write, say so, and then stop.
  • Writes keep your files as they were — CRLF line endings and a UTF-8 byte order mark are kept, an Xcode String Catalog keeps Xcode's layout, i18next files no longer lose keys next to a dotted nesting level or a capitalised plural form, and Android text such as &amp;lt; is no longer changed.
  • Files that start with a byte order mark load in every format — Before, ARB, i18next JSON and Android XML files with one were reported as malformed, an .xcstrings catalog stopped the project from loading, and a .stringsdict loaded with no plurals.
  • Multi-module projects no longer get one module's file filled with every module's strings — describe and the MCP tool describe_keys now refuse a Gradle multi-module, multi-.lproj or catalog-plus-.lproj project and write nothing. Restore the affected file from version control if this happened to you.
  • apply writes an iOS .strings file as .strings in a project that also has a String Catalog — It wrote JSON into it before. Restore the affected Localizable.strings and run plan again if this happened to you.
  • apply accepts a Flutter plural translation that contains markup, such as <b>.
  • plan offers a cell again after you undo an apply with git checkout.
  • same_plural_forms no longer fails a correct German project — Languages whose only forms are one and other are not compared.
  • A stringlane.yaml without path no longer hides a namespaced i18next project, and a symlinked file in such a project is reported as a load error instead of being read.
  • The write guard asks before an agent creates a new language's .stringsdict.
  • setup hooks no longer offers --apply on hosts where it writes nothing — On Codex, Cursor, Gemini CLI, Windsurf and VS Code it tells you to paste the document instead.

v0.5.0

CLI

Added#

  • stringlane setup skills gives agents without a StringLane plugin the plugin's skills — Codex, Cursor, Gemini CLI, GitHub Copilot in VS Code, Windsurf, Zed and OpenCode can now use the skills the Claude Code plugin comes with, for adding a new string, translating a locale and reviewing the result. --apply writes them into your repository under .agents/skills/, one stringlane-* folder each, so a team working in several editors shares one committed copy. StringLane's host table records all seven hosts as reading that folder, but none has yet been confirmed doing so in a recorded session, so check that the skills show up in yours.
  • Installed skills stay current, and edited ones are left alone — Every file is stamped with the CLI version that wrote it. Running --apply again after an upgrade replaces a stale copy. A copy somebody edited, or one a newer CLI wrote, is left alone, so a teammate a release behind cannot downgrade it. --remove deletes only unedited copies. stringlane check prints one line on stderr when your copies are older than the CLI, and its exit code and --json output are unaffected.

Fixed#

  • easy_localization and slang projects are checked by their own rules — 0.4.0 opened these projects but checked them by i18next's rules, so check reported no errors for a translation that dropped an easy_localization {} or {name}, a slang $name, or a Ukrainian plural's few and many. StringLane now works out which package a project uses from pubspec.yaml and checks placeholders in that package's syntax. A nested map of plural forms, such as "apples": {"one": …, "other": …}, is now one plural key, checked for the forms each language needs. i18next projects are unchanged.

  • easy_localization's few and many forms are no longer required — easy_localization does not show them unless you set ignorePluralRules: false. A missing one no longer counts against completion, is not asked for by plan or update, and is reported at info level. Pass --fail-on missing_key if your app turns the rules on.

  • Expect check to go red on upgrade for some easy_localization and slang projects — The findings are real, and StringLane was not checking for them before. A baseline entry that named a plural form as apples.other no longer matches, because that form is now the other form of apples. Fix the findings, or re-record with stringlane baseline write.

  • The licence no longer says the CLI sends nothing — Version 1.1 of the licence covers three things:

    • the usage data the CLI sends and how to turn it off;
    • the daily npm update check, which stringlane telemetry off does not stop but STRINGLANE_NO_UPDATE_CHECK=1 does;
    • the text stringlane update sends to the AI provider you configure.

    The CLI's behaviour has not changed.

v0.4.1

CLI

Added#

  • The MCP server is in the Official MCP Registry — StringLane is listed as io.github.thebedcoder/stringlane, so an MCP client that installs from the registry runs npx @stringlane/cli mcp . — the same server stringlane setup mcp and the agent plugin configure. Nothing changes for an install you already have.

v1.3.0

Desktop

This release is mostly about saving. StringLane writes to your source files as you type, and until now it assumed nothing else was writing to them. That assumption stopped holding once the free stringlane CLI — and AI agents using it — could write to the same project you have open, and it was never true of a git pull or another editor. The app now takes its turn, notices when a file changed underneath it, and asks rather than guessing which version you meant to keep.

The rest widens what opens with no setup: Flutter projects that keep their translations as JSON, Android apps whose default strings are not English, and single-module Gradle projects in an unusual place.

Added#

  • A file that changed on disk is never saved over behind your back — If a locale file changed outside StringLane — a git pull, another editor, the stringlane CLI — while you had unsaved edits to it, the app could write your edits over the new version, or replace your edits with it, without a word. It now stops and asks, naming the file: Reload from disk reads the new version and drops your unsaved edits to that file (edits to other files are saved first), and Keep my edits writes yours over it. Close the dialog and nothing is written or lost — the status bar says "File changed on disk" until you decide, and clicking it brings the question back.

  • Saving waits its turn instead of failing when the CLI is writing — If the CLI, or an AI agent using it, was writing to the project you had open, the app's save could land at the same moment or show "Error saving" for something that was working fine. It now waits for the other write to finish and saves your edits by itself; past a few seconds the status bar says "Waiting for another StringLane write…". If you quit, switch or close the project while edits still cannot be saved, the app asks first instead of dropping them.

  • A save lock left by a crashed session is recovered, and a stuck one can be cleared — If a StringLane process stopped while holding a project's save lock, the app takes it over, saves, and says so in the status bar, so you know an earlier write may not have finished. When a save has been waiting on another process for a few seconds, the note offers Clear lock — after a confirmation that warns what clearing a live lock can do. Temporary staging files left by a crashed save are cleaned up too; .restore.tmp files, which may hold an original, are kept.

  • slang projects that use namespaces now open — widgets_en.i18n.json, errorDialogs_fr.i18n.json and slang's older strings_en.i18n.json naming open with no setup, including a namespace kept in its own folder, and edits save back to each namespace's own file. StringLane follows slang's own namespaces: setting, so keys read the way you call them. Adding a language creates one file per namespace.

  • Your source language can be written down, and it sticks — StringLane guesses a base locale: English if you have it, otherwise the first alphabetically. Changing it in Settings → General used to last only until you closed the project. It is now saved to stringlane.yaml as baseLocale:, and the CLI and MCP server read the same value — so the app and stringlane check in your pipeline can no longer measure completion against two different source columns. Name a locale your project does not have and StringLane falls back to guessing, and tells you which one it used.

  • An Android project whose values/ folder is not English can say so — res/values/strings.xml names no language, so StringLane assumed English and loaded a German app's strings into the en column. Set defaultFileLocale: de in stringlane.yaml and the unnamed file loads as German. A values-fr/ folder or a tools:locale attribute still wins.

Improved#

  • easy_localization and slang projects just open — StringLane looked only for .arb in Flutter projects, so one that keeps its translations as JSON got "No localization files found". It now opens easy_localization (assets/translations/, assets/langs/) and slang (lib/i18n/, assets/i18n/) with no configuration, nested keys read as example.hello, and the unused-key scan understands both packages' call styles.

  • Unused keys stop flagging live Flutter strings — The scan now follows an accessor you bind once and use bare (final l10n = AppLocalizations.of(context)!; … l10n.save), any getter name off a BuildContext extension, and S.current.save — the form you use where there is no BuildContext, in validators, repositories and services. Keys called only from those places were listed as not referenced, and that is the list people act on by deleting. What it still cannot see is written on the Unused Keys page. The first scan after updating re-reads your source rather than reusing what it remembered.

Fixed#

  • The bulk-translate summary now adds up — Translated, skipped and failed were counted independently, so a four-key run could report "Translated 3 of 4 keys. 4 errors." — or, when a batch gave up quietly, no error count at all. Every key is now accounted for exactly once.

  • Cancelling a bulk translate is no longer a dead end — The summary now offers Re-run whenever keys are left over. It retries only what is outstanding, so it never spends your API budget twice on keys that already landed.

  • The trial day-count stops going stale — Left open across midnight, the banner kept the previous day's number. It now stays current, and reaching zero mid-session never closes your editor.

  • An Android project with one module in an unusual place opened empty — A single Gradle module under mobile/, shared/ or core/ui/ now opens on the first try, with keys exactly as written and the path saved to stringlane.yaml.

v0.4.0

CLI

The write guard is the headline. On Codex it let every patch through, and on Windsurf there was none; both now refuse a hand-edit to a locale file and tell the agent how to lift the refusal. The rest widens what opens with no setup — Flutter projects that keep their translations as JSON, Android apps whose default strings are not English, a single Gradle module in an unusual place — and stops stringlane plan calling an unfinished project finished.

If your CI branches on exit codes, read the apply entry under Fixed. One situation that used to exit 1 now exits 2.

The write guard#

  • On Codex, the guard now holds — The plugin gave Codex the hook it gives Claude Code, which looks for a file path Codex's patches do not carry, so every edit was allowed; and where it did recognise a locale file it answered with an "ask" Codex does not support. Codex now runs its own guard, which refuses a hand-edit to a locale file and tells the agent how to lift it. Codex runs a plugin's hooks only after you trust them: open /hooks in Codex and trust the StringLane hook.

  • Windsurf gets one — stringlane setup hooks --host windsurf prints a hook for Windsurf (now Devin Desktop) that refuses a hand-edit to a locale file, says why, and names the command that removes it. Paste it exactly as printed.

  • setup mcp now says the guard is a second step — On every host but Claude Code and Codex, installing the server does not install the guard, and nothing said so. Every setup mcp report now ends with the command that does: stringlane setup hooks --host <host>, or stringlane setup guard --host zed|opencode.

Telemetry#

  • The notice no longer calls the data anonymous — Every event carries a random ID generated on your machine, so runs from one install can be linked together. The first-run notice now says so, and stringlane telemetry status shows the ID. Your strings, keys and file paths are still not part of it.

  • More CI systems switch it off — Only CI was checked, and Azure Pipelines, TeamCity, AWS CodeBuild, Jenkins and Bamboo do not set it. TF_BUILD, TEAMCITY_VERSION, CODEBUILD_BUILD_ID, JENKINS_URL, bamboo_buildKey and BUILDKITE now switch off telemetry and the update check too.

Added#

  • easy_localization and slang projects just open — check, inspect and the MCP server looked only for .arb in a Flutter project, so one that keeps its translations as JSON got "No localization files found". They now open easy_localization (assets/translations/, assets/langs/) and slang (lib/i18n/, assets/i18n/) with no configuration, nested keys read as example.hello, and stringlane scan understands both packages' call styles. A project with both .arb files and a JSON folder still opens as ARB.

  • slang namespaces too — widgets_en.i18n.json, errorDialogs-fr.i18n.json and one folder per namespace open with no setup. add-locale creates one file per namespace, and the agent hook stops an agent creating one by hand.

  • stringlane.yaml can name your source language — StringLane guesses a base locale: English if you have it, otherwise the first alphabetically. Write baseLocale: uk and check, plan, update and the MCP server measure against it instead. The desktop app writes the same field, so the app and your pipeline can no longer disagree about which column is the source. Codes are spelled the way your files spell them (zh-rCN, en_GB, en-us). Name a locale the project does not have and StringLane falls back to guessing, with a base_locale_not_loaded warning naming the one it used.

  • An Android project whose values/ folder is not English can say so — res/values/strings.xml names no language, so StringLane loaded a German app's strings as en. Set defaultFileLocale: de and the unnamed file loads as German. A values-fr/ folder or a tools:locale attribute still wins.

  • Reports say whether the base locale was declared or guessed — stringlane inspect and stringlane check --json carry baseLocaleSource, "declared" or "detected", beside baseLocale.

Improved#

  • Flutter keys your code calls stop being reported as unused — The scan now follows an accessor you bind once and use bare (final l10n = AppLocalizations.of(context)!; … l10n.save), any getter name off a BuildContext extension, and S.current.save — the form you use where there is no BuildContext, in validators, repositories and services. Keys called only from those places were listed as not referenced, and that is the list people act on by deleting. The first scan after upgrading re-reads your source instead of reusing its cache — one slower run, once.

  • --help stops telling you to pass a folder to commands that refuse one — upgrade and the three telemetry commands no longer print [project] in their usage line.

Fixed#

  • apply and validate refuse a plan that names a file your project does not manage — A plan.json lists every file the apply must check for changes, and apply opened each one and said in its refusal whether it matched — so a plan edited to name .env could ask about .env. It never wrote to those files. Both commands now refuse such a plan before opening anything, which matters most when apply runs in CI on a branch you did not write. Plans StringLane produced are unaffected. One ordinary case now reports differently: build a plan, then delete, rename or break a locale file, and you get STALE_PLAN with exit 2, where some of those cases used to exit 1 with a per-item problem. Re-run plan.

  • stringlane plan stops telling you a project is finished when it is not — An empty plan has six meanings and two of them printed the wrong one: a locale file that failed to parse produced "this project has no locale but its base", and a language missing one module's file in a multi-origin project produced "Every target locale is complete." Each of the six now says what is wrong and what to do next. plan --json and the MCP server's prepare_translation_plan carry the verdict as emptyReason, so neither a script nor an agent has to work it out from counts.

  • stringlane add-locale put new files in the wrong folder for some i18next projects — With namespaces under a single locales/ folder, the new language's files landed at the project root, where nothing reads them. They now land in locales/<code>/.

  • Namespaced i18next projects reported live keys as missing and dead — t('home.title') is now matched in the default namespace: translation, or whatever you set as defaultNamespace: in stringlane.yaml (next-i18next projects use common).

  • An Android project with one module in an unusual place loaded as empty — A single Gradle module under mobile/, shared/ or core/ui/ is now found, with keys exactly as written.

v1.2.0

Desktop

This release is mostly about the app knowing more about your project than what is in the locale files. It reads your source code now, so it can tell you which keys nothing calls. It checks ten things it did not check before. And the parts of a string it could not previously show you — the branches of an ICU select, the wording around an ordinal — are now editable one branch at a time.

Underneath, the app and the free stringlane CLI run the same engine commands. That is invisible if you only use the app, and it is what stops the terminal and the project window from growing two different ideas of what a valid translation is.

Added#

  • Unused keys — Open any key and the detail panel now says used in N places, with the file and line of each call site; Show all opens the rest. The sidebar has a Show only unused filter, and an Unused chip stays visible while it is on so you always know the list is filtered. It reads Dart, JavaScript and TypeScript, Swift and Objective-C, Kotlin and Java.

    Two things to know, because they are the difference between a useful list and a dangerous one. "Not referenced" is what the scan saw, not a verdict — a key built at runtime (t('errors.' + code)), reached through a wrapper StringLane does not recognise, named only in a file type it does not read, or read straight out of the locale file by the platform will appear in that list and still be live. And a project StringLane has not read yet says not scanned, never "used in 0 places": those two look identical and mean opposite things. The scan starts after your project opens, so it never delays the editor.

  • Selectors view — ICU select and selectordinal used to render as one raw string you edited as a whole, braces and all. Each branch is now its own row, the way plural categories already were, so you translate {gender, select, male{…} female{…} other{…}} one branch at a time without retyping the syntax around it. Ordinal categories come from the target language's own CLDR rules, so a language needing few for 3rd and other for 4th is shown both.

    This also fixed a case where your translation was invisible: a locale whose plural held nested ICU rendered an empty block, so the text you had written did not appear at all.

  • Ten new checks. The Issues panel gained: trailing and leading whitespace that differs from the source; two consecutive spaces; a zero-width character that arrived by copy-paste; a word repeated consecutively; trailing punctuation that differs from the source, including the per-language rules (French needs a non-breaking space before ! and :); the same source string translated two different ways in different keys; one translated string reused for two different source strings; two plural categories in a language that requires them to differ carrying identical text; a base string using unnamed positional placeholders such as %@, which a language with different word order cannot reorder; and a base string that already exceeds its own length limit, so every translation will too.

    Three of those fire on things that are frequently correct, so they are reported at a new quieter info level rather than as warnings.

  • HTML and XML markup is checked — a dropped closing tag inside a value, or one the source did not have, is now a validation error rather than something you find in the running app.

  • A length budget — a translation much longer than a translation of that source usually needs is flagged at info level, so a string that will break your layout is visible before you ship it. This is separate from the length limit you set yourself, which is still an error when exceeded.

Improved#

  • Guarded words are a term-level rule now, not a whole-string freeze. A key containing a guarded word had no correct translation: the AI prompt told the model to keep the term verbatim inside a translated sentence, and the validator then flagged the sentence for not being identical to the base. Worse, a guarded word silently switched off the other checks on that key — a base of Download for {os} translated with a broken placeholder reported the guarded word and never mentioned the placeholder. A guarded term must now appear in the translation in the base's own spelling, and every other check on the key keeps running. To freeze a whole key, mark it not-for-translation as before.

  • Plural categories come from CLDR itself. The table of which categories each language requires is now generated from the CLDR data rather than transcribed by hand, so a language whose requirements were recorded wrong is no longer asked for the wrong set of forms.

  • Adding a language to a multi-origin project writes one file per origin, in a single change: either the locale exists everywhere it should or it exists nowhere. It used to create one file and leave the rest of the project behind.

  • The app and the CLI share one engine. Every project command the app runs is now the same code path stringlane check runs. Three defects turned up in the app while doing it and were fixed.

Fixed#

  • Editing one plural form no longer resurrects the old single-value text. A key that had been a plain string and later became a plural could keep both on disk, and the stale single value could win on the next save.

  • Two validation codes that could never fire have been removed — json_syntax_error and placeholder_count_mismatch were declared but nothing produced them. If either is in a --fail-on list in your CI, take it out: that gate has been permanently green.

Also new, and free#

The stringlane CLI is published, and so is the agent plugin. Same engine, same validation, no licence: the CLI runs the whole loop from a terminal or CI, and eight coding agents can run it through the Model Context Protocol. Seven of those eight also get a write guard that gets in the way before one of them hand-edits a locale file — what that guard can actually do differs per host, because it is built from whatever each host lets StringLane intercept.

The app licence and the CLI are separate in both directions — neither grants the other.

v0.3.2

CLI

The npm page for @stringlane/cli now opens on a header image, a row of links to the docs it refers to, and a badge showing the published version, rather than on a bare # stringlane.

That is the whole release. Comparing the cli-v0.3.1 and cli-v0.3.2 tags, the only things that moved are the README, a continuous-integration shell script with its test, the documentation in the repository, and the version number. No file that ships in the npm package changed except package.json, so the command surface, the checks, the MCP tools and the write guard are all exactly what the v0.3.0 notes below describe.

There is no reason to upgrade from 0.3.1. If you are pinning a version in CI, 0.3.2 and 0.3.1 will behave identically.

v0.3.1

CLI

This is the page you are most likely to have read first, and it was wrong.

The README that npm renders still carried the note written before there was a release — "Not published to npm yet" — followed by the only install instruction on the page: npm pack in a source tree you do not have, installing a 0.1.0 tarball that was never published. Anyone who found the package before finding this site was told to do something impossible.

Install is now what it should have been:

npm install -g @stringlane/cli

with npx @stringlane/cli check . for the no-install path, and a CI snippet that installs from npm.

Nothing about the tool changed. The two bundles differ by exactly one byte — the version string the binary prints — so there is no reason to upgrade beyond the page now telling the truth. Everything in the v0.3.0 notes below applies unchanged.

v0.3.0

CLI

The stringlane command line is public, and it is free. No licence, no account, no sign-in — the app licence and the CLI do not grant each other anything in either direction.

It is built on the same engine as the desktop app — one implementation of the parsers, the validation rules and the completion figures, called from both surfaces rather than two copies that drift apart.

This is 0.3.0 rather than 1.0 on purpose: the command surface is not promised yet.

The loop, in a terminal#

stringlane init detects the project and writes stringlane.yaml. After that:

  • inspect — what the project holds and what is missing, per locale.
  • check — validate everything and set an exit code.
  • plan → validate → apply — list what needs translating, check an agent's answers without touching your files, then write the ones that passed. The middle step is the point: an agent proposes, the engine decides, and nothing reaches your locale files unchecked.
  • update — do all three at once with your own AI provider key.
  • add-key, add-locale, describe, context for the edits that are not translations.

A gate for CI#

stringlane check exits 0 when there are no issues and 1 when the project has them. The other two exist because a broken gate and a failing gate are not the same event: 2 means the command never got far enough to have an opinion, and 3 means the command was fine but something it depends on did not answer.

A CI step that treats every non-zero exit the same way will eventually report a misconfigured runner as a localization failure, which is how a gate stops being read.

--fail-on narrows which issue codes set exit 1. baseline write records today's issues so a project with existing problems can start clean and only fail on new ones.

Keys your code no longer calls#

stringlane scan reads your source and reports two things: keys your project defines that no call site names, and keys your code calls that the project never defined. The second kind is a bug that is already shipping: the screen renders a raw key where a sentence should be.

It knows how each framework actually calls a string, so it matches the spelling your runtime uses rather than the key as StringLane spells it. Those differ in a multi-module project, and a grep for the wrong one reports every key unused, which looks exactly like a filthy codebase and gets acted on.

It refuses rather than guessing. If it read no source files, or read them and recognised no keys at all, it says so and reports nothing as unused — "I could not read your code" and "everything is dead" produce an identical empty list, and only one of them is safe to act on. When it does answer, it states its own blind spot beside the list: a key built at runtime, reached through a wrapper it does not recognise, or read straight out of the locale file by the platform will look unused and be live.

check runs the scan too, at info level, so upgrading does not turn your CI red. --no-scan skips it; --ignore-key-pattern and --ignore-file silence a family or a directory, and silenced findings are still counted, separately by cause — a report saying "0 not referenced" because you silenced everything must not look like a clean project.

For coding agents#

stringlane mcp serves the project over the Model Context Protocol: ten tools, five of which write. The five writers are declared as writers, so the approval prompt your agent host shows before a write is the gate — StringLane does not add a second one of its own, and there is no argument an agent can pass to skip yours.

stringlane setup mcp prints the config block for your host — and on Claude Code, --apply writes it for you.

The write guard, and what it can actually do per host#

An agent that edits a locale file by hand bypasses every check on this page. The guard gets in the way first — but what it can do is built from whatever each host lets StringLane intercept, so it is not the same promise everywhere:

HostHow you install itWhat the guard does
Claude CodePluginAsks you first
Codex CLIPluginAsks you first
Gemini CLIExtensionRefuses, and says how to lift it
CursorConfig you pasteRefuses, and says how to lift it
ZedConfig you pasteConfirms, from Zed's own permissions
OpenCodeConfig you pasteConfirms, from OpenCode's own permissions
VS Code CopilotOne commandInstalls, but guards nothing yet
WindsurfConfig you pasteNone

"Asks you first" and "refuses, and says how to lift it" are different promises: Cursor and Gemini CLI have no ask in their hook protocol, so the refusal there names the way to lift it instead of prompting a human.

--apply turns a printed block into a written one. For setup mcp and setup hooks it does that on Claude Code alone: on the other seven it prints the same bytes and exits non-zero rather than merging into a config file nobody has recorded that host writing. What you lose by pasting is the backup and the merge, not the configuration.

setup guard never writes, on any host. It prints a block for Zed's or OpenCode's own permission table — the two hosts that run no hook StringLane speaks — and --apply there exits 2 with the block on stderr, because both files are JSONC and a JSON writer would drop your comments on the way back out.

setup rules --apply — three locale-file rules written into your own AGENTS.md, between markers it can take back out — works on all eight, and --remove works everywhere too.

The full breakdown is in the agent hosts guide.

What it reads, and what it writes#

stringlane writes into your repository only when you ask it to — init, apply, update, add-key, add-locale, describe, context and baseline write. Each of them asks first, and refuses when there is no terminal to ask at unless you pass --yes. inspect and plan write nothing at all. check and scan write nothing into your repository, but they do cache the source scan in StringLane's own state directory, outside your project.

It sends usage data by default, under a random per-install ID, and prints a notice saying so the first time you run it on a machine, before anything is sent. stringlane telemetry off stops it, and so do DO_NOT_TRACK=1, STRINGLANE_TELEMETRY=0 and CI=true. Your strings, your keys and your file paths are not part of it.

It also asks npm, at most once a day, whether a newer version exists, and prints three lines on stderr when there is one — never on stdout, so --json | jq is unaffected. It can never fail your build: offline, a proxy, a registry outage and a 500 all print nothing and change no exit code. STRINGLANE_NO_UPDATE_CHECK=1 turns it off, as do the telemetry opt-outs. The write guard never checks on any path — nothing that runs on every file an agent touches is allowed to make a network request.

Install#

npm install -g @stringlane/cli

Claude Code and Codex CLI take the plugin instead, which brings the MCP server and the guard with it. Start at the CLI docs, or hand the agent setup page to your coding agent and let it do the whole thing.

v1.1.1

Desktop

Fixed#

  • Your window opens at the size you left it — StringLane came back full-screen-sized on every launch, whatever size the window actually was when you quit. Two things were wrong. The size was never written down at all: quitting with ⌘Q, from the menu, or by closing the window all skipped the save, so the file StringLane reads on startup had been frozen for months. And on macOS a single trip into full screen was recorded as "maximised" and then re-applied on every launch after, which is why resizing the window never seemed to stick. Both are fixed — your window's size and position are saved on every way out of the app, and on macOS StringLane restores the frame you left rather than forcing the window wide. One thing to expect: if your window still opens oversized the first time you launch this version, resize it once and quit. That size is what comes back from then on.

v1.1.0

Desktop

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 &amp; — 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.

AI#

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

v1.0.0

Desktop

StringLane 1.0 is here. Open your project folder and edit translations across every locale side by side: changes stream straight to your source files, with no import, no export, and no account.

Edit every locale, side by side#

  • Open a folder, see everything: point StringLane at your project and it loads every locale file; edits auto-save back to source.
  • One key at a time, every locale visible: the list-view editor shows all translations for the active key together, with a sidebar for search, filters, and quick actions.
  • Status at a glance: completion %, key and issue counts, and save state live in the status bar.
  • Drag & drop to open: drop a folder or file on the window. Recent projects remember format, locale count, and completion.

Every major format#

  • ARB (Flutter), Apple .strings, iOS .xcstrings, Android XML, and i18next JSON: edit each in place, no conversion.
  • Project auto-detection for Flutter, iOS/Apple, Android, and Web layouts, including multi-module Android, multi-target iOS, and namespaced i18next, all merged into one editable project.
  • Run flutter gen-l10n from inside the app, and edit @key ARB metadata (descriptions, placeholders, types).

AI translation, bring your own key#

  • Per-cell, per-key, and bulk "translate all missing" across every locale.
  • Seven cloud providers (OpenAI, Anthropic, Google Gemini, Groq, Mistral, DeepSeek, xAI) plus any local OpenAI-compatible runner (Ollama, LM Studio, Jan, llama.cpp). Keys are bring-your-own and stored in the OS keychain.
  • Context-aware: cultural/locale context and guarded-word rules shape results; plural keys are translated per quantity form.

Plurals and ICU, without the syntax#

  • Plural support: create, edit, and convert plural keys across ARB, iOS, Android, and i18next, with CLDR-required forms filled automatically.
  • Visual ICU editor for {count, plural, …} and {gender, select, …}, plus an inline {} placeholder helper.
  • One-click parameter autofix for broken {placeholder} usage.

Catch problems before they ship#

  • Inline validation for missing translations, same-as-source, placeholder mismatches, length budgets, ICU errors, guarded words, and malformed syntax.
  • Issues Panel (⌘J) lists every problem across the project; fix many of them, including with AI, in place.

Fast, keyboard-first, and yours#

  • Command Palette (⌘P) for every action, key jump, and setting; full keyboard shortcuts with remappable keybindings.
  • Appearance: light, dark, or follow-system, with density, locale flags, and zebra-striping options.
  • Local-first and private: no account, no cloud, no telemetry. Your strings never leave your machine.

Platforms#

  • macOS: signed with a Developer ID certificate and notarized by Apple, so it opens without Gatekeeper warnings.