Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to Tolk are documented in this file.

## Unreleased

### Changed

- **Translate from origin** is a button in the field's header (the book icon) instead of an item in the field's actions menu, so one field takes one click. It asks first only when the field holds text of its own: an empty field, or one that still shows the origin's text, is translated at once. While a translation runs, the buttons are disabled instead of hidden.

## 1.0.0 - 2026-10-15

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Tolk translates multi-site Statamic content from an origin site into its localizations with large language models from Anthropic, OpenAI or Google Gemini. It works in the entry editor, where translations are filled into the open form for the editor to review and save, on single fields, in bulk from listings and from the command line. It tracks which translations are outdated or edited by hand, so it updates only what has changed.

- **In the editor**: create a localization and Tolk offers to translate it; Translate and Update fill the form, unsaved, field by field. Bard, Replicator, Grid, Markdown and tables keep their structure, formatting and links.
- **One field at a time**: "Translate from origin" in a field's actions.
- **One field at a time**: the "Translate from origin" button in a field's header.
- **In bulk**: a listing action, an Overview of every collection, taxonomy, global set and navigation by language, and runs with an estimate before they start, progress, cancel and resume. No queue or database needed.
- **From the command line**: translate collections, or the whole website into a new language in one command, plus the app's `lang/` files and the texts of forms.
- **Updates, not retranslations**: when the origin changes, only the changed paragraphs are sent again. Text a human edited is never overwritten unless you ask. Optionally, saving an entry updates its translations by itself.
Expand Down
12 changes: 11 additions & 1 deletion TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ A **run** is a bulk job: `{ id (ULID), status, driver, created_by, created_at, s
### §A13 CP surfaces

1. **Entry and term publish forms**: a banner component pushed above the tabs with `pushComponent` (R3 §2.4). On a localization it shows field status counts and Translate / Update / Review; on an origin it shows the status of each localization and Translate into… / Update. It also opens the offer after `localization.created`.
2. **Field actions**: "Translate from origin" on top-level fields of supported fieldtypes, inside localizations, in the base form only.
2. **Field actions**: "Translate from origin", a button in the header of top-level fields of supported fieldtypes, inside localizations, in the base form only; it asks first only before it replaces text of the localization's own (T11.7).
3. **Bulk action** "Translate…" in entry and term listings and in a structured collection's tree view, which creates a pending run and redirects to its page.
4. **Run page**: item counts, a progressively computed estimate, a Start button, then progress, usage, per-item results (with the reviewer's corrections), cancel, resume and retry.
5. **Overview and Audit pages** (Inertia): the status matrix by content type, container and site, with buttons that create runs (global sets and navs are translated from here); blueprint and setup checks.
Expand Down Expand Up @@ -1836,6 +1836,7 @@ Goal: many targets at once, with progress, from the CP and the CLI, on a site wi
- Built in T5.9: on a localization created in the form, `translations.translate()` first puts the origin's slug back (the store's `created` and `slugKept`), so translating the title through the action does not change the slug either.
- Built in T7.5: `translations.translate()` also takes `review` and `reviewer`; left null, the field action reviews when `review_enabled` is on, as the banner's buttons do. Its summary then has the `reviewed` line and the corrections for View changes.
- Built (planner's details): `createFieldActions()` in `editor/fieldActions.js` (`register()` once, `action`, `visible`, `run`; the shared `fieldActions` is registered in `cp.js`). `visible` also needs the store entry of `base` to belong to the open item (`container.reference === ctx.reference.value`: the CP keeps the store across Inertia visits, and a global set's form is also named `base`), a model (`!needsSetup(status)`), the handle in the status's `localization.fields` with a status other than `not_applicable`, and a phase outside `BUSY`: the dropdown ignores `disabled`, so the action hides while a translation runs. The value check compares deeply without regard to key order. `translations.translate()` gained `replace`: the requested fields are kept out of `skip`, because a field the editor typed into would otherwise be skipped as `edited_in_form`, against the confirmation's text; Retry keeps it. After the translation a toast reports the error the phase ended with, or a field that was not filled (`describeSummary()`'s failed, edited or "No fields were translated." line), but nothing after Cancel or a switch. While core saves the form (`translations.saving('base')`) it only says so, before calling `translate()`, whose refusal would leave an earlier error phase to be reported. The banner's working line names the field of a one-field translation ("Translating :field into :site…", `data-tolk-working-field`).
- Changed in T11.7 (owner, 2026-10-20): the action is a quick button in the field's header, disabled rather than hidden while a translation runs, and it asks first only when the field holds text that is not the origin's.
- **Self-check:** `tests/Browser/field-action.spec.js`: the action shows on a localizable text, a Bard and a Replicator field of a localization; it does not show on the origin, on nested fields or on non-localizable fields; running it replaces only that field and leaves the form unsaved; with a related entry open in a stack, the action is absent on the stacked form's fields.

- [x] **T8.13 Origin view: translate into other languages and update all translations**
Expand Down Expand Up @@ -2164,6 +2165,14 @@ In T0.1 (2026-10-02) the owner asked for none of these for v1 (D29). Each task i
- Not built, discussed with the owner: a catalog feed published by Reach (fetched and cached, with `resources/models.php` as the fallback), so that Automatic follows new models without a release, and the providers' model lists for availability and retirement notices.
- **Self-check:** `ModelCatalogTest` (the merge by id, the defaults, a changed tier), `ConfigTest` (the empty `models` lists and the pin of `resources/models.php`) and `DocsCoverageTest` (entry keys read from `resources/models.php`) pass, and so does the whole suite.

- [x] **T11.7 Translate from origin as a button in the field's header** (owner, 2026-10-20)
- T8.12's action sits in the field's dots menu and always asks first, so translating one field takes three clicks, even on a field with nothing to lose. It becomes a quick action (`quick: true`): an icon button in the field's header, `dictionary-language-book` like the addon's other surfaces, with the title as its tooltip and `aria-label` (core's `FieldActions.vue` at v6.35.0). Core lists an addon's actions before the fieldtype's own, so it comes first among the header's buttons.
- It asks first only when the translation could lose text: the form's value of the field has text (`textsOf()`) and that text is not the origin's (`sameText()` with the base context's `originValues`). A field that is empty (an empty Bard paragraph included) or that shows the origin's text (an untranslated field inherits it, and so does a copy saved over a translation) is translated at once. Otherwise the confirmation of T8.12 stays as it is.
- Quick actions honour `disabled`, unlike the dropdown's items, so while a translation is under way (`BUSY`) the button stays in place, disabled, instead of the action hiding. T8.12's other visibility rules stay.
- Focus: Cancel and Esc give the focus back to the button (the field header's button whose `aria-label` is the title). The browser drops the focus of a control that becomes disabled, so once a translation starts, with or without the confirmation, a lost focus moves into the banner as after Translate in T8.12.
- Docs: `docs/editor.md` ("Translating one field"), README's feature line, `docs/marketplace-listing.md`, `CHANGELOG.md` (Unreleased); the docs site's editor page and introduction (`/Volumes/1TB/Sites/tolk-docs`).
- **Self-check:** `resources/js/editor/fieldActions.test.js`: quick with the icon; disabled, not hidden, while busy, and re-evaluated by a computed; translates at once on an empty field, an empty Bard value and the origin's text; asks first on text of the localization's own and on text typed into an untranslated field; Cancel gives the focus back to the button, and a direct translation moves a lost focus into the banner. `tests/Browser/field-action.spec.js`: the action is a button in the header of the same fields as before and of none of the others; on a typed title it asks first, the buttons are disabled while the translation runs, and on the untranslated Bard field it translates without a dialog. `npm run cp:build` committed.

## Phase 12 — Hardening

- [x] **T12.1 Processes and locks under real concurrency**
Expand Down Expand Up @@ -2556,3 +2565,4 @@ Owner decision 2026-10-14 (D28 amended, reproduced by failing tests first): a fi
Owner decision 2026-10-14 (reproduced by failing tests first): a run went on through every item while the provider rejected each request (`request_rejected`, a 400 that is not about the key or the account, such as a model option it refuses), so a setting it did not accept cost a whole run of failed items and the reviewer's rejections left every item unreviewed. `UnitOutcome::$rejected` keeps the provider's sanitised message (`Pipeline::call()` and `review()`), and `RunProcessor` counts such units in a row per kind, translation or review (`tolk:run-rejected:{id}:{kind}`, cleared by an answered unit of that kind and by `start()`); at `MAX_REJECTED_UNITS` (3) it forgets the item's plan, so a resume plans it again, and aborts the run with the message and "The run stopped after 3 requests in a row were rejected." §A10's 4xx row, §A12 "Waiting" and T8.4 amended; `docs/bulk-and-runs.md`, `docs/troubleshooting.md` ("Rejected requests") and the changelog. Tests: `RunTranslateTest` +3 (three rejected translations abort with the message and the last item replanned on resume; three rejected reviews between answered translations abort; rejections not in a row do not, a guard that passed before; the two aborts failed before).
Owner decision 2026-10-14 (D5 kept, reproduced by failing tests first): a copy of the origin's text saved over a field Tolk translated (a form that still held it, saved after a run or an automatic update translated the field, or an inherited Bard field set back to inherit that rewrote itself) read manual with an unchanged source, so its item read up to date while the field was in the source language, and nothing pointed to it. `StatusResolver` now flags such a field `FieldStatusInfo::$sameAsOrigin` (manual, a machine record, `T == C` with text; `C` is walked only for those fields), which `needsAttention()` counts, so the item reads **attention** on the Overview, in `tolk:status` and in the origin's list; Translate and Update still leave it alone (D5). `editor/status` sends `same_as_origin` per field, and the banner names the fields ("Title holds the text of the origin again. Translate it, or mark it as up to date to keep it.") with **Translate it** / **Translate them** (`translations.translate()` with `mode: all`, those fields and `overwriteManual`, so a field typed into is still skipped and Undo restores the copy); "Mark manual fields as up to date" writes a baseline, which is no machine record, and clears the flag. §A6 rule 3 and the item status amended; `docs/statuses.md` ("Needs attention", item statuses, "A copy saved over a translation"), `docs/troubleshooting.md`, `docs/editor.md`, `docs/cli.md` (`tolk:baseline`) and the changelog. Tests: `StatusResolverTest` +1 (failed before), `EditorStatusTest` +1 (the payload key; failed before), the pinning test in `TargetTranslatorTest` now expects attention, `statuses.test.js` +1, `EditorBanner.test.js` +2.
Owner decision 2026-10-15: the repository is public, so `docs/research/` was removed from every commit with `git filter-repo` before tagging 1.0.0 and is git-ignored; the reports stay in the owner's working copy. The commits of pull requests #1 and #2 still hold it on GitHub until GitHub Support removes them.
T11.7 done 2026-10-20: at the owner's request, "Translate from origin" is a quick action, a `dictionary-language-book` icon button in the field's header with the title as tooltip and `aria-label`, disabled instead of hidden while a translation runs (`disabled()`, `BUSY`); `run` asks first only when the form's value has text that is not the origin's (`replacesText()`: `textsOf()` and `sameText()` with `originValues`) and otherwise translates at once (`start()`); Cancel and Esc give the focus back to the header's button with that `aria-label`, and a focus the disabled button lost moves into the banner. Tests: `fieldActions.test.js` 34 (5 new, 4 changed: quick with the icon; disabled, not hidden, in each phase and through a computed; enabled without a base form; at once on '', null, an empty Bard paragraph and the origin's text; asks on the localization's own and on typed text; asks without a base form and reports it; the focus into the banner after a direct translation, back to the button on Cancel), 9 mutations each fail a test; `tests/Browser/field-action.spec.js` (3: the button in the same headers as before and in none of the others, typed title with the confirmation, both buttons disabled during the translation, the untranslated Bard field without a dialog, the focus in the banner after it). Docs: `docs/editor.md`, README, `docs/marketplace-listing.md`, `CHANGELOG.md`; the docs site's editor page, introduction and `field-action` screenshots. Vitest 772 passed, Pest 4,008 passed, Pint clean, `npm run cp:build` committed; browser 69 passed, 4 skipped, 2 failed in `editor-push.spec.js`: "Update translations changes only the edited paragraph" (a Bard image stored wrapped in a paragraph) fails on 1.0.0 too whenever the spec runs after `editor-options.spec.js` and passes alone, and "Translate into…" (the button not disabled after typing) failed only in the full run and passed in four reruns; neither is caused by this task, both left open. Sandbox restored.
7 changes: 3 additions & 4 deletions docs/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,11 @@ The **Offer to translate new localizations** switch turns the offer on or off fo

## Translating one field

Every top-level field Tolk can translate has **Translate from origin** in its field actions (the menu next to its label), in a localization:
In a localization, every top-level field Tolk can translate has a **Translate from origin** button in its header, at the right: the book icon, whose tooltip names it. Press it, and the field is translated again from the origin, whatever its status, edited by hand included, and filled into the form unsaved. The rest of the form is left alone. It sends the same context as the banner's buttons (the site profile, the instructions, the language's instructions, the glossary, and the entry's title and summary) and follows the review setting.

1. Open the field's actions and choose **Translate from origin**.
2. Confirm: "The current content of this field will be replaced."
When the field holds text of its own, Tolk asks first: "The current content of this field will be replaced." A field that is empty, or that still shows the origin's text, such as a field not translated yet, is translated at once, since nothing is lost. Either way, **Undo** in the banner puts the field back until you save.

The field is translated again from the origin, whatever its status, edited by hand included, and filled into the form unsaved. The rest of the form is left alone.
While a translation runs, the buttons are disabled.

The action is not offered on nested fields (inside a Replicator, Grid or Bard set), on fields that are not localizable, in the origin, or while another entry is open in a stack.

Expand Down
2 changes: 1 addition & 1 deletion docs/marketplace-listing.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Translate multi-site content into every language with Claude, GPT or Gemini: in

Tolk translates multi-site Statamic content from an origin site into its localizations with large language models from Anthropic, OpenAI or Google Gemini, using your own API keys.

In the entry editor, Tolk offers to translate a new localization and fills the translations into the open form, unsaved, so an editor reads them and presses Save. One field at a time, "Translate from origin" sits in the field's actions. In bulk, a listing action and an Overview of every collection, taxonomy, global set and navigation start runs with a cost estimate, progress, cancel and resume, and the run keeps going in the browser when there is no queue. From the command line, one command translates a whole website into a new language, and two more translate the app's `lang/` files and the texts of forms.
In the entry editor, Tolk offers to translate a new localization and fills the translations into the open form, unsaved, so an editor reads them and presses Save. One field at a time, a "Translate from origin" button sits in each field's header. In bulk, a listing action and an Overview of every collection, taxonomy, global set and navigation start runs with a cost estimate, progress, cancel and resume, and the run keeps going in the browser when there is no queue. From the command line, one command translates a whole website into a new language, and two more translate the app's `lang/` files and the texts of forms.

Tolk tracks, per field, whether a translation is outdated or was edited by hand. An Update sends only the paragraphs that changed and never overwrites text a human wrote unless you ask. A site profile, instructions, per-language formality and a glossary go with every request, and an optional second model reviews each translation and applies its corrections with reasons. New localizations are saved as drafts by default.

Expand Down

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion resources/dist/build/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"isEntry": true
},
"resources/js/cp.js": {
"file": "assets/cp-7pjkSVYi.js",
"file": "assets/cp-CtqImns9.js",
"name": "cp",
"src": "resources/js/cp.js",
"isEntry": true
Expand Down
Loading
Loading