easyi18n_cli 0.12.0
easyi18n_cli: ^0.12.0 copied to clipboard
Command-line tool for easyi18n. Pulls translated localization files (Mode A, native .arb) and pushes your tr() source strings for translation.
Changelog #
0.12.0 #
-
easyi18n mcpruns an MCP server on stdio for an AI agent to drive. It takes the key fromEASYI18N_TOKENin the server's env and the project from theproject_dirargument, thenEASYI18N_PROJECT_DIR, then the working directory. It refuses to send the key to abaseUrlother than the easyi18n API over HTTPS or a loopback host, unlessEASYI18N_BASE_URLnames that exact origin. Tools:doctor,status,init,extract,pull,apply,push_preview,push,fill_status,authorize,publish,rollbackandadd_language; prompts:setupandtranslate. Apushthat costs credits needs theoperation_idof an approvedauthorize {action: translate}, whosemax_creditsis clamped toEASYI18N_MCP_MAX_CREDITS(default 50,0disables paid ingests); a push that costs nothing needs no approval.fill_statusreports what is still pending and settles any request that left without an answer. Every tool result is capped at 32 KiB.extract,apply,push_preview,pushandfill_statusreadtr()calls, so they serve Flutter projects; the tools for an Xcode catalog are not in this release. -
easyi18n --versionprintseasyi18n <version>, or{"version": ...}under--json. -
A global
--jsonflag makesinit,doctor,status,extract,pull,apply,push,sync,rollback,languages addandformats addprint one JSON object on stdout; warnings stay on stderr and the text output is unchanged. A failure prints{"error": {"message", "code", "statusCode", "retryable"}}, whereretryableis true for transport failures and for the HTTP statuses the CLI already retried. Under--jsonnothing prompts:pushandsyncrefuse to spend credits without--yesand fail withconfirmation_required. -
pushandsyncno longer read a confirmation from piped stdin. Without a terminal they decline and say to pass--yes, as before when the answer was noty. -
init --stack flutter|xcode|auto(defaultflutter).xcodefinds the project's String Catalog (Localizable.xcstringsfirst,InfoPlist.xcstringsnever), writescatalog:,format: xcstringsandmode: a, and scaffolds nothing else;--catalog <path>names a different one.autodetects the stack and refuses a tree it does not recognize.doctorreports the detectedstackandfamilyand checks the catalog, format, mode and Xcode setup of a file-first project. -
The MCP tools
publishandrollbackfreeze and restore a version behind a human approval:authorize {action: publish|rollback}returns theapproval_url, and the tool refuses without the approvedoperation_id. A rollback names its target aspreviousby default; the server resolves that alias when it mints the operation, so what the human approves is the version id that goes live. -
push --json --publishworks: the record gains apublishedblock withversion_id,key_count,unchangedandrefused(the ICU and markup rows plus the server's totals, which exceed the rows once the 50-row cap bites). Until now the combination was refused withjson_unsupported, so a CI job that published had no way to read which version it minted. -
easyi18n languages add <code> [--fill-mode ai|manual]and the MCP tooladd_languageadd a target language throughPOST /v1/projects/{id}/languages, with a key that has thewritescope. Re-adding a removed language revives it. The command prints the code as the server stored it (PT_BRbecomespt-br);--jsoncarriescode,fill_mode,project_dir,base_urlandnext. Against a backend without the route the server answers 404 and the command fails with it. -
The MCP
doctortool asks the server whether the key inEASYI18N_TOKENis an Agent key (GET /v1/projects/{id}/credential) and warns when it is not: that key spends and publishes without a human approval. Against a backend without the route the check readsunverified.easyi18n doctordoes not ask, because a CI key is the other valid kind. -
A failed command on a 5xx now prints the server's correlation id (from the error body, else the
x-correlation-idheader) to quote in a support ticket, and a 500 gets its own hint instead of the bareinternal error. -
syncprints one line per diagnostic code the server cut from its listing, with the number of rows left out and the languages they came from;--jsoncarries them asdiagnostics_omitted. The backend now lists 10 rows per (code, language). -
Breaking file-workflow requirements:
syncwith publication and Mode Apush --publishnow stop before preview if metadata is unreadable, malformed or lacks output-formats API version 1, even for an already configured project. Mode Apush --publishnow requiresreadin addition totranslate,publish; adopting a format also requireswrite. -
Remove the exported
SyncRunner.fetchMetaandcheckOutputFormathelpers; file workflow preflight now belongs toOutputFormatAdoption. -
Adopt formats conditionally in file-delivery flows after budget/consent, require backend capability before preview, and publish with
requiredOutputFormat. Preserve confirmed configuration changes in later errors and sync JSON. Mode B push and sync without publication retain their existing permissions. Empty Mode A pushes are covered. -
Recover formatless pulls after a real format error, retrying the same version/lang once. Healthy pulls add no lookup; watch adopts at most once, read-only keys receive write-scope advice, and dry-run never adopts.
-
Add
easyi18n formats add <id>with the write scope, project ID/alias resolution and text/JSON confirmation of the persisted format list. Repeating the command reports whether configuration changed, including materialization of legacy formats. It does not publish translations. -
Share typed POST transport with publish/rollback while preserving their success statuses, parsers and replay policy. Invalid add responses provide add-specific recovery advice.
-
One noun for the refusal counts (#1175).
push,syncandrollbackreport the same two counters (icuRejected/markupRejected) and printed three different words for them —translation(s),value(s)andcell(s). Since #1000 a refused row can name the base language, where the refused value is the stored source and not a translation, so two of the three were wrong for part of what they counted. All three now printcell(s), the one noun true of both natures. The number itself has not changed. -
pull --require-complete --dry-rungates without writing (#1178). The gate runs before the writes and does not read--dry-run, so the pair is the combination a CI job wants: it answers "is the published version complete?" and leaves the working tree untouched. Behaviour is unchanged — this pins it, because a dry run used as a cheap gate that exits 0 over the state the real run refuses is the defectsyncsettled in #955. The--require-completeand--dry-runrows of the README say so. -
A parse finding now names the language column it was lost from (#726).
/v1/syncraises the parsers' findings per language pass and forwards the pass on each row; the CLI grouped them by code and message shape, and the messages ofPLURAL_SHAPE_MISMATCHandDEVICE_VARIATION_FALLBACKname nothing (the key is infield, which the shape blanks out), so two languages losing the same string printed as one line naming neither. The line now carries— in: en, fr, capped and counted likealso on:, and the language joins the identity of an already-printed row so a row only the real run carries is no longer discarded against the preview's. A row with no language — the base parse's and theLANG_*family — prints exactly the line it always printed. -
pull --require-complete— the completeness gate a Mode A CI job can fail on (#1052). A Mode A project's translations live on the server and its publish skips the cells it has no translation for, so an incomplete locale rendered as a shorter file that no command could turn red:sync --require-completeneeds acatalog:a gen_l10n project does not have,pushhas notr()units to gate,pullandstatusnever looked at per-locale coverage, anddoctorskipped its grading on Mode A and printedAll checks passed.over half-translated locales. The export now reports per-language coverage off the published snapshot, andpull --require-completefails, naming each incomplete language and its missing count, when any target language is short.doctorgrades the same thing on a Mode A project instead of skipping it (#1176): a short language is a FAIL, and a version it cannot judge — or a missing credential — reports "could not verify", which keeps the run from printingAll checks passed.Back on the gate:pull --require-completerefuses a backend that does not report coverage ("cannot verify"), never passes it — and so refuses a version the count cannot judge: one published withincompleteMode: source(the base text is baked into every untranslated cell, so it always reads complete), one published before the server started stamping how it treated those cells, and one with no target language at all. Only a cell that renders as non-empty text counts as translated. The flag refuses outright, before any request, on amode:other thana(a config with nomode:line at all reads asb— the refusal names addingmode: aas the fix), on--langnaming the base language, and with--watch(the gate is one-shot) — nine refusals in all, so a CI step gets a verdict or an error, never a green it did not earn. The ninth is a basis the version states and this client does not recognize, which is separate from a version that states none.doctorreads the same set the gate does — one symbol, not a copied predicate (#1248): onlyskipis judgeable, sosource, a basis this client cannot read, and an unstamped version are all reported as unverified rather than graded, and each is reported with its OWN message and its own remedy, the same three the gate refuses on. A version whose basis this client does not recognize is told to upgrade the CLI, not to republish — republishing stamps the same basis again. A version with no target language at all is reported as nothing to grade rather than as a check that could not run (#1176). That last state is the one where the two commands answer the same version differently, on purpose: a base-only project is healthy for a health check and has nothing to gate on for a gate, sodoctorpasses it whilepull --require-completerefuses it.doctornames the sibling's refusal in that line, so the disagreement is not something a red CI job has to be reverse-engineered from.
doctor can now exit 1 where it exited 0 — read this before upgrading. A
Mode A project with a short target language used to print All checks passed.; it is now a FAIL naming each language and its missing count. Every
"could not verify" arm — no credential, a 403, a version the basis cannot
judge, a backend that reports no coverage, an unreachable API — still exits
0, so a job only turns red on a state that is actually incomplete. On a Mode
A project doctor also makes its first authenticated request: it sends
EASYI18N_TOKEN to baseUrl to read the published coverage, and warns
first when that baseUrl is not the production origin, like the other
commands that send the token.
A green CI job can start writing something different — read this before
upgrading. sync used to tell the server that its catalog was current as of
the moment it ran pull. It now sends the instant the content it pulled was
published, which is earlier — often by a day, and by weeks under pull --version. That is the point: the pull renders the last published
snapshot, never the live keys, so a correction made in the dashboard since
that publish was never in the file. Claiming the pull moment told the server
the file already carried it, and the server silently reverted the correction
with no conflict row and no trace (#898).
With the correct claim, those keys now come back as base-language conflict
rows. Under the default onConflict: server the project's source is kept and
your file's base text is no longer imported for them; --on-conflict file
keeps today's outcome and still reports the row. A plain pull refuses
nothing new and adds no exit path: a conflict row is a warning, never a
failure (the two new refusals in this version are both opt-in flags:
sync --dry-run --require-complete and pull --require-complete, both in
this section). What
it can change is what the run has left to do — --require-complete still
decides by what the server reports as unfilled — and it can write less than it
did and print warning lines it did not. This affects a plain
pull too, not only --version, and it closes the rollback → pull → sync
sequence, which reproduced the same silent revert with no flag at all. A caret
constraint over 0.x pins the minor, so this does not install itself into an
existing job.
pullrecords what its files are not behind, andsyncclaims that. The server now reportssnapshotAton the pull response, and the CLI stores it in.easyi18n/state.jsonnext topulledAt. The two are separate on purpose:snapshotAtis the claim,pulledAtis whateasyi18n statusdisplays, and apull --versionfrom two weeks ago must not makestatussay "pulled 13 days ago" a second after it ran. (#898)- Nothing new can fail because of it. A server that does not report the
field, a value that names no timezone, and a value of the wrong type all
fall back to dating the pull by the local clock — the previous behaviour —
instead of failing the pull. State written by an older CLI keeps arming the
guard through its
pulledAt, so upgrading never drops the claim. (#898) - Known residual — a version restored before the server side of this shipped. Its stored instant is the restore's, not its content's, so a pull of it still over-claims until the project publishes or restores again. That is today's behaviour, unchanged, not a new failure.
- Known residual.
sync's own final pull deliberately records no state, so after asyncthat publishes, the stored claim still names the previous version. The nextsynccan therefore report one conflict row too many for a key the dashboard moved and that publish already carried. The direction is safe — a row, never a silent revert — and an explicitpullresets it. sync --dry-runnow reports exhausted and held pairs, and--require-completegates it. The preview always carried theexhaustedrows and thependingSkippedcount the real run refuses on, but the dry run returned before reading them: it said nothing wherepush --dry-runwarns, andsync --dry-run --require-completeexited 0 in silence. It now warns naming each exhausted pair and counting the held ones and, under--require-complete, fails (exit ≠ 0): a CI job using the dry run as a cheap gate can go red on upgrade, on exactly the state the real run would have refused. What only a fill decides (a job stopping short) is still the real run's call. (#955)
0.11.0 #
Two new refusals — read before upgrading a CI job. push --publish --require-complete now fails (exit ≠ 0, nothing published) when the AI retry
budget is spent, exactly as sync --require-complete already did, or when the
scan finds no extractable units. A previously-green CI job may go red on
upgrade: #752 means untranslated pairs would have shipped as source text;
#952 means push scanned no units and could not verify the remote catalog at
all. Everything else below is new output printed permanently, a new option
whose default preserves today's behaviour, or a crash that becomes a success.
A caret constraint over 0.x pins the minor, so none of this installs itself
into an existing job.
- A malformed baked email bundle now blocks deploy with an attributed error
instead of a stack trace. Invalid UTF-8, invalid JSON, a non-object root,
or a non-object
tokenIndexnames the exact bundle to replace or regenerate and exits 2. Filesystem and extractor failures keep their original types, whiletokenIndexvalues remain intentionally unvalidated because this gate consumes only its keys. (#1018) - A 200 whose body is JSON of the wrong shape now fails with a sentence
instead of a stack trace. A response that parsed as JSON but carried an
array, a string, or a field of the wrong type left the CLI with an uncaught
TypeErrorand exit 255 — from a proxy that answers with its own envelope, abaseUrlaimed at the wrong service, or a schema regression. Every success body is now read field by field: the failure names the endpoint and the field, and exits 1. This covers every HTTP response reader in the CLI, not just the API client —syncno longer dies on a diagnostic row whosealsoSpelledis not a list, anddoctorno longer dies on a delivery manifest whosetokenAlgoVersionsis not one (it reports the manifest as unreadable instead). No response that succeeds today starts failing — the readers accept exactly what the casts they replace accepted (a field that is absent or null still falls back to its default), and the lists the CLI never iterates stay tolerant of a stray non-string element, as they are today. Two observable changes that are not refusals: the exit code of an already-crashing response moves from 255 to 1, and apendinglist holding no usable language no longer counts its unit as needing a fill. A realpushorsyncthat fails this way adds one line: the server may already have reserved credits and queued a fill, so the line names that re-running is safe and does not pay twice. A--dry-run(and the cost preview everysyncopens with) does not print it — that request is answered before the hold and before the enqueue, so there is nothing to re-run into. (#972) - A non-string
projectin the localeasyi18n.lockis now treated as absent. The field is inert when the lockfile is read;extractandpushconsume its units, while a push writes the resolved project id. Any value that is not a string — a number, a boolean, an array, an object — now falls back to the config seed without losing units. This is a crash-to-success tolerance fix, not another refusal. (#989) pushandsynccan no longer disagree about an exhausted pair. The server reports a pair whose AI retry budget is spent in both commands' responses, deliberately outsidepending(the fill will never retry it on its own), andpushread onlypending— so it printed "Everything is already translated.", exited 0, and published the pair as source text, under the very flag whose job is to refuse that.syncrefused correctly. Both commands now share one decision: under--require-completethe run fails listing everykey [lang]before anything is published; without the flag every push surface — the real push, the zero-estimate path, and--dry-run— names the pairs on stderr instead of counting them as translated, and the completion claim is never printed over them. Retry the pairs from the dashboard; a source edit or a model change also re-arms the fill. (#752)- An empty
push --publish --require-completenow fails before any request. With no extracted units,pushhas no pending/exhausted snapshot and cannot prove that the remote catalog is complete; it previously published that state and returned 0. Code-first projects are told to check--source-dir. Mode A recommendssync --require-completeonly for a non-empty, supported single-filecatalog:; canonical gen_l10n/ARB configs instead name the dashboard becausesynccannot evaluate them. Removing the flag preserves deliberate publication of the current remote state. (#952) pullnow reports every replacement of your catalog's local source text, with the text it discarded. A pull run between an Xcode edit of the base language and the nextsyncused to revert that edit silently — in an identifier-keyed catalog (InfoPlist.xcstrings, #612) the English is prose the developer wrote, so the loss was authorship, not a derived render. The pull still applies the served value (exempting the base language is refuted in the issue: it breaks converted printf values, base plural forms, and the empty shells Xcode creates), but each pair is named on stderr asConflict: <key> [<lang>]with one line per discarded form, so the text survives in the log and in the pull action's PR diff. Remedy: re-apply the edit in the dashboard, or in the file followed byeasyi18n sync.pull --dry-runprints the same rows in the conditional, before anything is written.sync's own write-back stays quiet — the server already reports that pair on the conflicts channel. (#646)syncnow tells the server when this catalog was pulled, and--on-conflictdecides who wins. With a dated pull the server can see that the project's text changed after your file was written, and reports the row instead of letting the upload bury it. The new option chooses the outcome:server(the default, and what every earlier version did) keeps the project's text and names the row;fileimports the catalog's over it — including over a source correction made in the dashboard, which is the one case worth reading twice before switching. The claim is only sent when the recorded pull describes this file (catalogandformatboth match the config), so a config switch cannot arm the guard with a timestamp that dates another file, and a repo that only ever runssync— the CI flow — sends nothing and is served exactly as before. A row on the base language is about the key's source, and every line that describes one now says so rather than calling it a translation; under--no-publishthose rows are counted apart instead of folded into "N kept value(s)". ApulledAtin.easyi18n/state.jsonwritten without a zone is dropped rather than guessed — reading it as process-local time moves the instant west of UTC and would silently revert an in-app source edit made inside that window. Known residual:pull --version <old>dates the state with now (#898). (#671)- A malformed served entry no longer crashes the xcstrings merge. If the
server delivered a key whose value was a bare string instead of an object,
pullandsync's write-back died on aTypeErrormid-merge (present since 0.8.1) and the catalog was left unwritten. Such an entry is now skipped and the local entry is kept verbatim, under both key policies — there are no translations inside a scalar to apply, and a payload the CLI cannot read is never a reason to overwrite the developer's file. (#966) syncstates which languages it is about to translate, before the credit estimate. The estimate has always covered the project's configured, non-manual languages and nothing else; it just never said which those were, so there was no moment at which the number could be checked against the intent.Target languages (N): …now precedes it (#592).- The two ways your catalog and your project can disagree are named, with
the remedy. A language the project configures that your catalog has no
entry for is translated from scratch and paid for — and when your catalog
spells the same language differently (project
cs-cz, filecs), both spellings are named and neither is changed for you: which one is meant is yours to decide. A locale your catalog carries that the project does not configure is reported too; it was always ignored, it just said so nowhere. One grouped line per kind, never one per locale — and never the same locale twice across the two moments. - After the pull,
syncreads the catalog it just wrote and warns by name for any configured language missing from it, and for any locale in it that the project configures nowhere. This is the check that could not be fooled: a language with nothing to deliver produces no entry at all, and the pull merges into your file rather than replacing it, so a locale you already had survives untouched and used to read as one we produced.manual-fill languages are excluded — nothing was going to translate them, so naming them would contradict the line above that just excused them. A catalog whose every entry isshouldTranslate: falseis excluded too: no fill will ever put a locale in one, so "re-run the sync" would be advice that can never work. Warnings only;--require-completeis still the flag that makes CI red. - A backend that does not say what the project is configured for gets none of this rather than a guess. An empty target list is not that case — it is a project whose only language is its base, and it still hears about the locales its catalog carries that nobody configured.
- A grouped parse-diagnostic line no longer speaks for a row it does not
describe. One code can print several lines — one per message shape — and
past a cap of five shapes the extra rows folded into the LAST line printed,
taking that line's wording with them. Since the server now emits two
different consequences under
DUPLICATE_PLURAL_FORM(#878), a sixth shape could tell an operator that a value was truncated when it was in fact stored intact. Rows past the cap now get their own line, which quotes no row's message and names the keys it covers up to the same cap every other line uses, counting the rest. That line says how many rows it stands for and nothing else about them, and the count a line prints is now the rows on that line — it used to be labelled "for this code in total" while counting one message shape. Output-only; no run changes result.
0.10.0 #
Behaviour changes — read before upgrading a CI job. Five of the fixes below
change what a run that passes today does. sync now keeps a newer server-side
translation instead of overwriting it with the file's (--on-conflict file
restores the old default, #550), and it fails where it used to exit 0: a key
lost to a concurrent sync (#564), a fill that stopped short (#602), and pairs
already held by another fill under --require-complete (#591). An output
format the project has not enabled is now caught before the run spends anything
rather than after (#530). Hence the minor bump — a ^0.9.0 constraint will not
pick this up on its own.
-
A busy backend no longer kills the run on the first try. Firestore's write-bandwidth throttle is quota-wide — it is a property of the database, shared by every client of it, not of your catalog — and a large first-time sync is exactly the run that meets it. It used to arrive as an opaque
500and end the job, with the project half-applied; the server now reports it per entry, so the keys that landed keep their work andsyncretries just those entries with exponential backoff and jitter instead of re-posting instantly. A run that meets the throttle leaves the translation for that retry rather than queuing one it might not be able to take back. GET requests (the fill poll, the pull) replay a transient failure as well — the poll runs after the credits were spent, so dying there used to lose a paid job, and the cost preview retries too — losing a whole run to a busy moment before a single write had been attempted was the cheapest failure to avoid. What is never repeated automatically is a request that may have changed something: with no answer from the server, "it failed" and "it is still being applied" are indistinguishable, and a run that failed after reserving credits can still be holding them — an automatic retry would then report a balance problem you do not have. In that case the run says the service was busy and re-running picks up exactly what is left. (#591) -
A run whose pairs are held by another fill no longer passes for complete. When every pair the catalog needs is already marked as being filled, the server has nothing to queue and accepts instantly with no tracking token — so the wait loop never started,
--require-completepassed, and the publish minted a version without those cells. The response now reports how many pairs it skipped for that reason;syncwarns on every run and refuses to publish under--require-complete. (#591) -
A translation the markup gate refused is now named in the output. The publish response carries two rejection lists and the CLI read one: a markdown or html translation that lost the source's structure (a dropped
**span, a mangled<a>) was refused at publish and reported inmarkupRejected, which nothing printed.pushshowed a successful publish andsynca clean run, while the string you wrote never reached users — underpushthe base language shipped in its place, undersyncthe locale simply had no value for that key. Both commands now print the markup rejections under their own heading, next to the ICU ones: same consequence, different fix. Neither list changes the exit code — a version that is missing some cells is still a version, and--require-completegoverns untranslated pairs, not a gate refusal. Both counts are now the server's own: the arrays are capped at 50, so the CLI reported "50" on exactly the publishes with the most damage. (#609) -
syncchecks the output format before it spends anything. A project that has not enabled the format informat:still translated, charged and published, and only then failed on the pull withformat "xcstrings" not configured for this project— credits gone, a version cut, and no catalog written. The format is now checked against the project's configuration up front, so the run stops before the first upload and says what to enable.--max-creditsanswered "is this run too expensive?" but never "can this run deliver anything at all?". Skipped by--no-publish, which never pulls. (#530) -
syncno longer reverts a translation the project produced after your catalog was pulled. The per-string decision was "the file differs, so the file wins", and a dashboard edit, an AI fill or a reviewer's sign-off made between your last pull and your next sync went back to the file's older text — counted as "imported", with nothing in the log. It is now kept on the server and reported, one warning perkey [lang]naming who wrote it and when (also under--dry-run); the pull at the end of the run brings the kept value into your catalog, so the PR diff is where you review it (under--no-publishthat pull is skipped and the CLI says so — the kept value stays server-side and the conflict reports again on the next run). Pass--on-conflict fileto make the catalog win anyway — the pair is still reported, including the human sign-off it wipes. A value that came from a file in the first place is still yours to change from the file, silently, as before. Conflicts are reported off every request, not just the cost preview, so one that only appears once the run starts is named too, and the summary counts the server's total rather than the rows it listed. (#550) -
pushnow tells you when a string was skipped because its key is switched off. A key switched off in the app is excluded from published output, so the backend never translates it — but the push looked clean while those strings silently stayed untranslated. It now warns, and says what to do (re-enable the key). Unlike the source-verification gate this is not a deferral: nothing will translate them until someone switches the key back on. The same push also stopped being billed for those keys. (#563) -
syncno longer exits 0 after losing a key to a concurrent sync. When two CI jobs sync at the same time, the one that loses a key's create gets that entry back ascontendedinstead of a 500.syncnow re-POSTs the catalog (up to twice) so the entry lands on the key the other run created, and fails the run if it is still unwritten — the two jobs may carry different catalogs, in which case the peer's write is not yours and a green job would drop this branch's strings until someone pushed again. (#564) -
syncnow reports the strings your code no longer references. A String Catalog carries Xcode's own verdict per phrase (extractionState: "stale"— its compiler pass over your sources could not find the string), and nothing read it, so there was no way to see which of your keys were leftovers from a reworked screen. The summary now ends withN no longer in your code, and the dashboard flags those keys with a "Not in code" badge and a filter to review them in bulk. It is a signal, not an action: nothing is disabled or deleted, and nothing stops publishing, becausestaleis a conjecture — a string reached through a computed key is marked stale while still on screen, so the call is yours. (#558)
0.9.0 #
-
pullnow writes to thecatalog:you configured. It resolved its destination fromoutput:alone, so a project whose catalog lives atEscapeCall/Localizable.xcstringsgot a straylib/l10n/tree instead — in an Xcode project with nolib/at all — while the run reported success and recorded the wrong path as fact.catalog:names one file and now decides wherepullwrites it;output:stays what it always was, the directory for formats that render one file per locale. -
pullmerges into a String Catalog instead of replacing it. A.xcstringsis written by two tools, and only one of them is us: Xcode owns the file's structure, key order, per-key metadata (extractionState,isCommentAutoGenerated) and every key added since the last publish. Overwriting it deleted them, and only an Xcode rebuild brought them back. The pull now injects the served translations into the local file and re-prints it in Xcode's exact style — the same mergesynchas done since 0.8.1, with one difference: a key that exists only on the server is added here, since a pull is the only way a key created in the dashboard reaches your file. A pull that changes no translations is now a byte-exact no-op, including the first one on a fresh checkout. The trade is that a pull no longer removes anything: a key deleted from the project stays in your catalog until Xcode's own extraction pass drops it, which is the right owner for that decision but worth knowing. -
Unknown settings in
easyi18n.yamlare reported.ouput:used to be read past in silence, leaving the run on a default. It is now a warning naming the key and listing the ones the CLI reads — a warning and not an error, so a config written for a newer CLI keeps working on a pinned one. -
catalog:is refused with a per-locale format. It names one file holding every language, so pairing it with a format that renders one file per locale is a contradiction — and a silent one when the project has a single language, because then exactly one file is served and that locale's render would land inside the catalog reporting success. The run now stops before the request, naming both settings. -
--langis refused withformat: xcstrings. A String Catalog holds every language in one file, and a single-locale render drops the base values its keys are addressed by — it would have merged nothing while reporting success. -
easyi18n statusnow sees a catalog pull (it recorded nothing before). -
pushnow checks--langagainst the project before spending a request. The server does not reject an unknown code — it intersects the list with the project's targets, so a typo is dropped silently and the run reports success having translated nothing for it. That silence is what this catches: an unknown code now fails locally with the configured targets listed. Naming only the base language fails too (the base is never a fill target, so the server answersno_target_langs), and a language set to manual translation is refused when named explicitly — the server's one real rejection here — or reported when it is merely part of the default set. Codes are matched the way the backend stores them, sopt_BRandpt-brare the same language. -
push --publishsays what will happen to strings that are still untranslated, instead of describing both outcomes and committing to neither:/metanow carries the project's integration mode. -
push --publishwarns when the localmode:and the dashboard's project mode disagree. They are set independently and never synced, and until now nothing could see both halves at once. -
All three degrade quietly:
pushneeds thetranslatescope and/metaneedsread, so a narrower key keeps working exactly as before. -
push --publishon a Mode A config (mode: a) now publishes withincompleteMode: skipexplicitly. Baking source text into the pulled files marks untranslated keys as done forever — a TMS workflow's own untranslated report is the signal that matters. Other configs keep omitting the field; the server now resolves an omitted value from the project's declared integration mode (skip for TMS, source otherwise).
0.8.1 #
syncnow merges translations into your catalog instead of replacing it. Opening a catalog in Xcode rewrites the file — its own" : "spacing, a localized key ordering, entries re-synced from code, andextractionStatemetadata. Replacing the file with the server's rendering reverted all of that, so Xcode and the sync PR would reformat each other's work forever. The local file now owns structure, order and metadata; the sync only injects the translatedlocalizationsper key, serialized in Xcode's exact style (pinned byte-for-byte by a golden captured from a real Xcode save). A converged catalog round-trips byte-identical.
0.8.0 #
- New
easyi18n sync— continuous i18n for file-first projects (iOS String Catalogs first). Pointcatalog:ineasyi18n.yamlat yourLocalizable.xcstringsand one command does the whole loop: upload the catalog, translate only what changed (translations the file already carries are imported verbatim, never re-charged), publish, and write the translated catalog back onto the same path. Built for CI:--yes,--dry-run,--max-credits,--no-publish,--require-complete. Re-running on an unchanged catalog is a byte-exact no-op, so a workflow can run it on every push and only open a PR when git sees a diff. Keys removed from the catalog are disabled server-side (and re-enabled if the phrase returns) — nothing is ever deleted. Pairs whose AI retry budget is spent are reported separately with a dashboard pointer instead of spinning the retry loop. publish(used by push and sync) understands the backend's newskipIfIdenticalanswer: identical content returns the current version asunchangedinstead of minting a new one.- The
Run 'flutter gen-l10n'hint after a pull now only prints forarbpulls — it made no sense after pulling an.xcstringsor.po.
0.7.1 #
- Installable in a Flutter project again. The
analyzerfloor was^13.3.0, but analyzer>=13.1.0requiresmeta ^1.18.3while the Flutter SDK pinsmeta 1.18.0— sodart pub add dev:easyi18n_cli, the documented install path, failed version solving in every Flutter app. The floor is now>=13.0.0 <14.0.0; nothing in the CLI used an API newer than 13.0.0. This hit Mode A users hardest: they only runpull, and the analyzer is used solely bypush/apply. Global activation (dart pub global activate easyi18n_cli) was, and remains, unaffected.
0.7.0 #
push --publishis no longer all-or-nothing. A string whose AI fill failed (e.g. a transient backend error dropped its chunk) used to abort the whole publish, holding every completed translation hostage. The CLI now re-queues failed strings up to 2 times (a failed fill releases its hold, so the retry only charges work that was never paid for) and, if stragglers remain after the retries or the 10-minute wait, publishes anyway with a warning — the missing cells serve the source text until a laterpush --publishfills them. Pass the new--require-completeflag to restore the previous fail-instead-of-publishing behaviour.
0.6.0 #
- Fix
--max-creditsunits. The API reports estimates and balances in millicredits (1 credit = 1000 mc), andeasyi18n pushcompared the raw mc value against--max-credits— so a documented-as-credits ceiling was enforced 1000× too strictly (--max-credits 500really meant 0.5 credits). The flag is now read in credits (decimals allowed, e.g.--max-credits 0.5) and converted at the edge. Cost previews, confirmation prompts, and abort messages now print credits (~107.3 credit(s)) instead of raw millicredits. If you were compensating by passing mc-scale ceilings, divide them by 1000.
0.5.0 #
easyi18n pullnow records what it writes (.easyi18n/state.json) and, on the next full pull of the same format and output dir, removes recorded files the server no longer serves — e.g. a locale renamed to its platform-canonical filename (app_zh-hant.arb→app_zh_Hant.arb), sweeping stranded.tmpfiles and emptied dirs along. A--langpull never prunes; aformat:oroutput:switch lists the old target's stranded files instead of deleting across targets;--dry-runpreviews removals. The first pull that records its list warns about files it cannot attribute instead of deleting them.
0.4.0 #
-
easyi18n push --publish: after the push, waits for the AI fill to complete (polling the dry-run estimate) and publishes a new version via the public publish endpoint — no dashboard needed.--approved-onlytreats unapproved translations as missing when publishing. Needs an API key with thepublishscope. -
New
easyi18n rollback [version]: restores a prior published version as a NEW version (history is never rewritten). With no argument it restores the version just before the current one. -
New
easyi18n doctor: read-only health check of the whole integration. Verifies the local wiring (config,easyi18ndependency,assets/easyi18n/floor,com.apple.security.network.clientin both macOS entitlements,Easyi18nScopemounted), lints yourtr()source strings as ICU (malformed ICU renders raw to users), and actively probes delivery — manifest reachable, CORS served, then your tr() scan graded against the published bundles: missing (never pushed/published), untranslated per locale, and unused keys. Bundles are integrity-checked against their content address (a stale CDN object fails doctor the same way the runtime rejects it), a 200 that isn't a manifest is a failure (SPA catch-all / captive portal), and a CORS header locked to one origin is surfaced instead of passing as "present". Exit 1 on blocking problems; warnings don't block. -
push/extract: atr()call whosectx:is not a string literal is now reported as dynamic instead of being registered as the (wrong) no-ctx unit.
0.3.0 #
easyi18n initis now one-command project setup: besideseasyi18n.yamlit adds theeasyi18n:dependency and theassets/easyi18n/offline floor topubspec.yaml(creating the dir), ignores the derived.easyi18n/state in.gitignore, grantscom.apple.security.network.clientin BOTH macOS.entitlements(delivery fetches fail silently in the sandbox without it), and wiresEasyi18nScopearound a trivialrunApp(...)(printed as a snippet whenmain.dartis not trivially patchable). Idempotent; new--dry-runflag reports without writing. An existingeasyi18n.yamlis now kept (re-run friendly) instead of an error —--forcestill rewrites it.
0.2.0 #
easyi18n push: statically extractstr()source strings from your source tree (canonicalcontext.tr('…')and the'…'.tr()sugar), diffs them against a committedeasyi18n.lock, previews the credit cost, and registers + translates them via the backend. Flags:--dry-run,--yes,--max-credits,--prune,--lang,--source-dir.easyi18n extract: the same scan, offline and read-only: reports extractable strings, new/orphaned vs the lockfile, and dynamic (non-literal) calls.--fail-on-orphansfor CI drift checks.
0.1.0 #
- Initial CLI (
easyi18n) for Mode A (native.arb). easyi18n initwrites aneasyi18n.yamlconfig.easyi18n pulldownloads translated files from the authenticated backend (GET /v1/projects/{id}/translations) and writes them to disk so you can runflutter gen-l10n. Supports--version,--lang, and--dry-run.