feat(entries): add a localize action - #48
Conversation
Translating a page had no route through the MCP. create with another site makes an unrelated entry with its own id rather than a localization, and update with a site the entry has no localization in correctly refuses, so the localization had to be made in the Control Panel first. localize uses Statamic's makeLocalization(), so the new entry keeps its origin and a structured collection places it in the target site's tree. data is optional — an empty localization inheriting everything is the normal starting point. The localization's own slug is injected into the validated payload, since Statamic's default blueprint marks slug required and it is an entry property rather than a data key (cf. cboxdk#39). Guards cover the collection not being available in the target site, the target being the origin site, and a localization that already exists. Added to the write-action lists so it maps to entries:write rather than defaulting to a read scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
addValues() populates every field in the blueprint, so taking all of values() wrote an explicit null for each field the translator left alone. Two things broke. The localization stopped falling back to its origin: a stored null is a value, not an absence, so every untranslated field read back as empty instead of inheriting. And the next update to that localization failed on fields the caller never sent. update validates the stored data merged with the incoming payload, so those nulls reached rules the fields would otherwise have skipped — "seo_canonical_entry: The Entry field must be an array", "seo_canonical_custom: The URL field must be a valid URL". The only way past it was to send a valid value for each unrelated field. Intersect the processed values with the payload, so a localize stores what it was given and nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reviewed and approved on the substance — this just needs a rebase on
This was the one in the batch I looked at hardest, because a new write action is exactly where an authorization gap would hide. It's gated correctly:
On the implementation, the part I'd have most expected to get wrong is handled: taking Also verified the guards do what they say: |
Description
There is no way to translate an entry.
createwith another site makes an unrelated entry with its own id, not a localization, andupdatewith a site the entry has no localization in correctly refuses. The localization had to be created by hand in the Control Panel first.Adds a
localizeaction takingcollection,id, targetsiteand optionaldata.Type of Change
Related Issue
None — found on a multi-site install where the only route to a second locale turned out to be the Control Panel.
Testing
composer test)composer quality)tests/Feature/Routers/EntriesLocalizeTest.php— 8 cases: translated values, an empty localization falling back to the origin, a blueprint with a required slug, all three guards, a missing entry, andupdateediting the localization afterwards.Manual testing on a real multi-site install is what caught the required-slug case: the first version failed with "The Slug field is required" on any blueprint using Statamic's default slug rules. The regression test added for it fails on the unfixed code and passes here.
Verified against
main: all 8 fail there, all pass here.Full gate green — pint, PHPStan level 9, 1114 tests / 5625 assertions.
Environment
Checklist
Notes
Uses Statamic's
makeLocalization()rather than assembling an entry, so the origin is set — untranslated fields keep falling back — and a structured collection places the localization in the target site's tree in the same position as the origin.The localization's own slug is injected into the validated payload: slug is an entry property rather than a data key, and Statamic's default blueprint marks it required, so without it every localize fails (cf. #39). The
{id}replacement excludes the entry fromUniqueEntryValue, which the origin would otherwise trip.datais optional: an empty localization inheriting everything is the normal starting point for a translator, andupdatewith that site edits it afterwards.localizeis added to the write-action lists inEnforcesResourcePolicyandRouterHelpers, so it maps toentries:writerather than falling through to a read scope, and it requires the same Statamic permission ascreate.