From a90af0be6a40561ba33ebb2760cd9cb9012b7d29 Mon Sep 17 00:00:00 2001 From: Jay George Date: Mon, 24 Aug 2026 11:00:07 +0100 Subject: [PATCH 01/20] Prototype --- resources/js/components/globals/Sites.vue | 297 ++++++++++++++++------ 1 file changed, 222 insertions(+), 75 deletions(-) diff --git a/resources/js/components/globals/Sites.vue b/resources/js/components/globals/Sites.vue index b55fe8650c3..331ce6dd9b2 100644 --- a/resources/js/components/globals/Sites.vue +++ b/resources/js/components/globals/Sites.vue @@ -1,82 +1,148 @@ @@ -126,7 +126,7 @@ v-if="option._groupLabel" size="sm" class="px-2.5 pb-1 pt-1.5 font-semibold uppercase tracking-wide text-gray-950 text-2xs dark:text-gray-300" - :text="__(option._groupLabel)" + :text="option._groupLabel" /> diff --git a/resources/js/components/inputs/relationship/Item.vue b/resources/js/components/inputs/relationship/Item.vue index 283ae05d923..6e9ab1009cc 100644 --- a/resources/js/components/inputs/relationship/Item.vue +++ b/resources/js/components/inputs/relationship/Item.vue @@ -119,7 +119,7 @@ export default { return null; } - return __(this.itemWithSiteGroup.group); + return this.itemWithSiteGroup.group; }, itemWithSiteGroup() { diff --git a/resources/js/components/inputs/relationship/SelectField.vue b/resources/js/components/inputs/relationship/SelectField.vue index c1ee5f9211b..85e490c304e 100644 --- a/resources/js/components/inputs/relationship/SelectField.vue +++ b/resources/js/components/inputs/relationship/SelectField.vue @@ -29,7 +29,7 @@ v-if="option._groupLabel" size="sm" class="px-2.5 pb-1 pt-1.5 font-semibold uppercase tracking-wide text-gray-950 text-2xs dark:text-gray-300" - :text="__(option._groupLabel)" + :text="option._groupLabel" /> diff --git a/resources/js/util/site-groups.js b/resources/js/util/site-groups.js index ff68ad9c27e..9c3abeb2759 100644 --- a/resources/js/util/site-groups.js +++ b/resources/js/util/site-groups.js @@ -71,7 +71,7 @@ export function flatOptionsFromSiteGroups(groups, { filterItems = (items) => ite export function selectedSiteGroupLabel(item, namedGroupsExist) { if (item?.group) { - return __(item.group); + return item.group; } return namedGroupsExist ? __('Other') : null; From 2a4aa03b08383251247be1387f1819b4c0bad494 Mon Sep 17 00:00:00 2001 From: Jay George Date: Mon, 24 Aug 2026 15:49:48 +0100 Subject: [PATCH 12/20] Remove unused Site::filterByGroup helper It was only covered by unit tests and never called from the CP. --- src/Facades/Site.php | 1 - src/Sites/Sites.php | 23 ----------------------- tests/Sites/SitesTest.php | 27 --------------------------- 3 files changed, 51 deletions(-) diff --git a/src/Facades/Site.php b/src/Facades/Site.php index 80ed7d766f6..80982061c23 100644 --- a/src/Facades/Site.php +++ b/src/Facades/Site.php @@ -11,7 +11,6 @@ * @method static mixed authorized() * @method static mixed default() * @method static bool hasMultiple() - * @method static \Illuminate\Support\Collection filterByGroup($handles, ?string $siteHandle) * @method static mixed get($handle) * @method static mixed findByUrl($url) * @method static mixed current() diff --git a/src/Sites/Sites.php b/src/Sites/Sites.php index 73feb7fb4a7..e2cd70a8df6 100644 --- a/src/Sites/Sites.php +++ b/src/Sites/Sites.php @@ -58,29 +58,6 @@ public function hasMultiple() return $this->sites->count() > 1; } - public function filterByGroup($handles, ?string $siteHandle) - { - if (! $siteHandle || ! ($site = $this->get($siteHandle))) { - return collect($handles); - } - - $groupKey = $site->groupHandle() ?? $site->group(); - - if (! $groupKey) { - return collect($handles); - } - - return collect($handles)->filter(function ($handle) use ($groupKey) { - $other = $this->get($handle); - - if (! $other) { - return false; - } - - return ($other->groupHandle() ?? $other->group()) === $groupKey; - }); - } - public function get($handle) { return $this->sites->get($handle); diff --git a/tests/Sites/SitesTest.php b/tests/Sites/SitesTest.php index 91712141ec4..45988d6d193 100644 --- a/tests/Sites/SitesTest.php +++ b/tests/Sites/SitesTest.php @@ -56,33 +56,6 @@ public function gets_all_sites() }); } - #[Test] - public function filters_handles_by_group() - { - $this->sites->setSites([ - 'en' => ['url' => '/', 'group' => 'London'], - 'fr' => ['url' => '/fr', 'group' => 'London'], - 'de' => ['url' => '/de', 'group' => 'Paris'], - 'nl' => ['url' => '/nl'], - ]); - - $handles = collect(['en', 'fr', 'de', 'nl']); - - $this->assertEquals(['en', 'fr'], $this->sites->filterByGroup($handles, 'en')->values()->all()); - $this->assertEquals(['de'], $this->sites->filterByGroup($handles, 'de')->values()->all()); - $this->assertEquals(['en', 'fr', 'de', 'nl'], $this->sites->filterByGroup($handles, 'nl')->values()->all()); - $this->assertEquals(['en', 'fr', 'de', 'nl'], $this->sites->filterByGroup($handles, null)->values()->all()); - - $this->sites->setSites([ - 'en' => ['url' => '/', 'group' => 'London', 'group_handle' => 'london'], - 'fr' => ['url' => '/fr', 'group' => 'London', 'group_handle' => 'paris'], - 'de' => ['url' => '/de', 'group' => 'London', 'group_handle' => 'london'], - ]); - - $this->assertEquals(['en', 'de'], $this->sites->filterByGroup(['en', 'fr', 'de'], 'en')->values()->all()); - $this->assertEquals(['fr'], $this->sites->filterByGroup(['en', 'fr', 'de'], 'fr')->values()->all()); - } - #[Test] public function gets_authorized_sites() { From ec3633e2c6a7689e58e56260a122b0a2ce1988ae Mon Sep 17 00:00:00 2001 From: Jay George Date: Mon, 24 Aug 2026 15:52:22 +0100 Subject: [PATCH 13/20] Reject circular entry origins on save Walk origin chains with a visit set so ancestors() cannot hang, and block saving entries that form a loop. --- src/Data/HasOrigin.php | 33 +++++++++++++++++++++++++++++++-- src/Entries/Entry.php | 18 ++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/Data/HasOrigin.php b/src/Data/HasOrigin.php index 6f3dd4328be..281b5a38d2a 100644 --- a/src/Data/HasOrigin.php +++ b/src/Data/HasOrigin.php @@ -141,6 +141,32 @@ public function isRoot() return ! $this->hasOrigin(); } + public function hasOriginCycle(): bool + { + $seen = []; + $entry = $this; + + while ($entry->hasOrigin()) { + $id = $entry->id(); + + if ($id !== null && isset($seen[$id])) { + return true; + } + + if ($id !== null) { + $seen[$id] = true; + } + + $entry = $entry->origin(); + + if (! $entry) { + break; + } + } + + return false; + } + public function root() { $entry = $this; @@ -149,11 +175,14 @@ public function root() while ($entry->hasOrigin()) { $id = $entry->id(); - if (isset($seen[$id])) { + if ($id !== null && isset($seen[$id])) { break; } - $seen[$id] = true; + if ($id !== null) { + $seen[$id] = true; + } + $entry = $entry->origin(); } diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index a96829a49c1..41bdf93d6ed 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Support\Responsable; use Illuminate\Support\Carbon; use Illuminate\Support\Traits\Localizable; +use Illuminate\Validation\ValidationException; use LogicException; use Statamic\Contracts\Auth\Protect\Protectable; use Statamic\Contracts\Data\Augmentable; @@ -399,6 +400,12 @@ public function saveQuietly() public function save() { + if ($this->hasOriginCycle()) { + throw ValidationException::withMessages([ + 'origin' => __('Origin sites cannot reference each other in a loop.'), + ]); + } + $isNew = is_null(Facades\Entry::find($this->id())); $withEvents = $this->withEvents; @@ -848,10 +855,21 @@ public function in($locale) public function ancestors() { $ancestors = collect(); + $seen = []; $origin = $this->origin(); while ($origin) { + $id = $origin->id(); + + if ($id !== null && isset($seen[$id])) { + break; + } + + if ($id !== null) { + $seen[$id] = true; + } + $ancestors->push($origin); $origin = $origin->origin(); } From 30c78458c43457e6750a4310c12981e19b466c3b Mon Sep 17 00:00:00 2001 From: Jay George Date: Mon, 24 Aug 2026 15:52:22 +0100 Subject: [PATCH 14/20] Add tests for circular origin guards Cover HasOrigin value/root resolution and Entry save/ancestors behavior when origins loop. --- tests/Data/Entries/EntryTest.php | 39 +++++++++++++++++ tests/Data/HasOriginTest.php | 73 ++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 tests/Data/HasOriginTest.php diff --git a/tests/Data/Entries/EntryTest.php b/tests/Data/Entries/EntryTest.php index 2f800df2f60..fc6c916e6a9 100644 --- a/tests/Data/Entries/EntryTest.php +++ b/tests/Data/Entries/EntryTest.php @@ -322,6 +322,45 @@ public function values_fall_back_to_the_origin_then_the_collection() $this->assertEquals('from origin', $entry->value('test')); } + #[Test] + public function it_rejects_saving_when_origins_form_a_cycle() + { + Collection::make('test')->sites(['en', 'fr'])->save(); + + $this->setSites([ + 'en' => ['locale' => 'en_US', 'url' => '/'], + 'fr' => ['locale' => 'fr_FR', 'url' => '/fr'], + ]); + + $english = EntryFactory::id('en-entry')->locale('en')->collection('test')->create(); + $french = EntryFactory::id('fr-entry')->locale('fr')->collection('test')->origin($english)->create(); + + $english->origin($french); + + $this->expectException(\Illuminate\Validation\ValidationException::class); + + $english->save(); + } + + #[Test] + public function ancestors_stop_when_origins_form_a_cycle() + { + Collection::make('test')->sites(['en', 'fr'])->save(); + + $this->setSites([ + 'en' => ['locale' => 'en_US', 'url' => '/'], + 'fr' => ['locale' => 'fr_FR', 'url' => '/fr'], + ]); + + $english = EntryFactory::id('en-entry')->locale('en')->collection('test')->create(); + $french = EntryFactory::id('fr-entry')->locale('fr')->collection('test')->origin($english)->create(); + + $english->origin($french); + + $this->assertEquals(['fr-entry', 'en-entry'], $english->ancestors()->map->id()->all()); + $this->assertEquals(['en-entry', 'fr-entry'], $french->ancestors()->map->id()->all()); + } + #[Test] public function it_gets_values_from_origin_and_collection() { diff --git a/tests/Data/HasOriginTest.php b/tests/Data/HasOriginTest.php new file mode 100644 index 00000000000..d1b6e472880 --- /dev/null +++ b/tests/Data/HasOriginTest.php @@ -0,0 +1,73 @@ + 'A']); + $b = new OriginStub('b', ['title' => 'B'], $a); + $a->setOrigin($b); + + $this->assertTrue($a->hasOriginCycle()); + $this->assertTrue($b->hasOriginCycle()); + $this->assertEquals('A', $a->value('title')); + $this->assertEquals('B', $b->value('title')); + $this->assertTrue($a->keys()->contains('title')); + $this->assertEquals('a', $a->root()->id()); + } + + #[Test] + public function it_resolves_values_through_a_normal_origin_chain() + { + $root = new OriginStub('root', ['title' => 'Root', 'shared' => 'from-root']); + $child = new OriginStub('child', ['title' => 'Child'], $root); + + $this->assertFalse($child->hasOriginCycle()); + $this->assertEquals('Child', $child->value('title')); + $this->assertEquals('from-root', $child->value('shared')); + $this->assertEquals('root', $child->root()->id()); + } +} + +class OriginStub +{ + use ContainsData, FluentlyGetsAndSets, HasOrigin; + + private ?self $originObject; + + public function __construct(private string $stubId, array $data, ?self $origin = null) + { + $this->data($data); + $this->originObject = $origin; + $this->origin = $origin?->id(); + } + + public function id() + { + return $this->stubId; + } + + public function setOrigin(self $origin): self + { + $this->originObject = $origin; + $this->origin = $origin->id(); + + return $this; + } + + public function getOriginByString($origin) + { + return $this->originObject && $this->originObject->id() === $origin + ? $this->originObject + : null; + } +} From 99e749e3abb17fe7b45e09b09b1a654004a0c49a Mon Sep 17 00:00:00 2001 From: Jay George Date: Mon, 24 Aug 2026 16:04:12 +0100 Subject: [PATCH 15/20] Drop non-English Localization translations English uses the string key via __(); other locales can come from contributors. --- lang/ar.json | 1 - lang/az.json | 1 - lang/cs.json | 1 - lang/da.json | 1 - lang/de.json | 1 - lang/de_CH.json | 1 - lang/es.json | 1 - lang/et.json | 1 - lang/fa.json | 1 - lang/fr.json | 1 - lang/hu.json | 1 - lang/id.json | 1 - lang/it.json | 1 - lang/ja.json | 1 - lang/ms.json | 1 - lang/nb.json | 1 - lang/nl.json | 1 - lang/pl.json | 1 - lang/pt.json | 1 - lang/pt_BR.json | 1 - lang/ru.json | 1 - lang/sl.json | 1 - lang/sv.json | 1 - lang/tr.json | 1 - lang/uk.json | 1 - lang/vi.json | 1 - lang/zh_CN.json | 1 - lang/zh_TW.json | 1 - 28 files changed, 28 deletions(-) diff --git a/lang/ar.json b/lang/ar.json index 9c74eef2ce1..215f2aadc71 100644 --- a/lang/ar.json +++ b/lang/ar.json @@ -601,7 +601,6 @@ "Locale": "اللغة المحلية", "Localizable": "قابل للتعريب", "Localizable field": "حقل قابل للتعريب", - "Localization": "ترجمة", "Localizations": "ترجمات", "Location": "موقع", "Locked": "مغلق", diff --git a/lang/az.json b/lang/az.json index dbd0d4e83c5..0150b3cf1ca 100644 --- a/lang/az.json +++ b/lang/az.json @@ -601,7 +601,6 @@ "Locale": "Region", "Localizable": "Tərcümə Edilə Bilən", "Localizable field": "Tərcümə Edilə Bilən Sahə", - "Localization": "Tərcümə", "Localizations": "Tərcümələr", "Location": "Məkan", "Locked": "Kilidli", diff --git a/lang/cs.json b/lang/cs.json index 91f1be29229..7a3f43ea6e1 100644 --- a/lang/cs.json +++ b/lang/cs.json @@ -602,7 +602,6 @@ "Locale": "Místní", "Localizable": "Lokalizovatelný", "Localizable field": "Lokalizovatelné pole", - "Localization": "Lokalizace", "Localizations": "Lokalizace", "Location": "Umístění", "Locked": "Zamčeno", diff --git a/lang/da.json b/lang/da.json index f42a51d0ffe..0168158ed81 100644 --- a/lang/da.json +++ b/lang/da.json @@ -601,7 +601,6 @@ "Locale": "Lokal", "Localizable": "Kan oversættes", "Localizable field": "Felt kan oversættes", - "Localization": "Lokalisering", "Localizations": "Lokaliseringer", "Location": "Beliggenhed", "Locked": "Låst", diff --git a/lang/de.json b/lang/de.json index a25496b8d44..7a7f6a6387e 100644 --- a/lang/de.json +++ b/lang/de.json @@ -704,7 +704,6 @@ "Loading": "Laden", "Locale": "Region", "Localizable": "Lokalisierbar", - "Localization": "Lokalisierung", "Localizations": "Lokalisierungen", "Location": "Standort", "Log in": "Anmelden", diff --git a/lang/de_CH.json b/lang/de_CH.json index dcbd01c51b0..8d35bcebe1f 100644 --- a/lang/de_CH.json +++ b/lang/de_CH.json @@ -704,7 +704,6 @@ "Loading": "Laden", "Locale": "Region", "Localizable": "Lokalisierbar", - "Localization": "Lokalisierung", "Localizations": "Lokalisierungen", "Location": "Standort", "Log in": "Anmelden", diff --git a/lang/es.json b/lang/es.json index 458792a91ce..c5ac7ff6e2c 100644 --- a/lang/es.json +++ b/lang/es.json @@ -601,7 +601,6 @@ "Locale": "Local", "Localizable": "Localizable", "Localizable field": "Campo traducible", - "Localization": "Localización", "Localizations": "Localizaciones", "Location": "Ubicación", "Locked": "Bloqueado", diff --git a/lang/et.json b/lang/et.json index 41f1473c5eb..216602fa011 100644 --- a/lang/et.json +++ b/lang/et.json @@ -602,7 +602,6 @@ "Locale": "Lokaal", "Localizable": "Lokaliseeritav", "Localizable field": "Lokaliseeritav väli", - "Localization": "Lokaliseerimine", "Localizations": "Lokaliseerimised", "Location": "Asukoht", "Locked": "Lukustatud", diff --git a/lang/fa.json b/lang/fa.json index d469aadb555..2b4bd86f7d3 100644 --- a/lang/fa.json +++ b/lang/fa.json @@ -601,7 +601,6 @@ "Locale": "منطقه", "Localizable": "چندزبانی", "Localizable field": "فیلد چندزبانی", - "Localization": "ترجمه", "Localizations": "ترجمه‌ها", "Location": "موقعیت", "Locked": "قفل شده", diff --git a/lang/fr.json b/lang/fr.json index 0ce4f927543..bc6fc3342c2 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -703,7 +703,6 @@ "Loading": "Chargement", "Locale": "Paramètres régionaux", "Localizable": "Localisable", - "Localization": "Langue", "Localizations": "Langues", "Location": "Emplacement", "Log in": "Se connecter", diff --git a/lang/hu.json b/lang/hu.json index 4b4e80f19e6..e38ee5b27b8 100644 --- a/lang/hu.json +++ b/lang/hu.json @@ -601,7 +601,6 @@ "Locale": "Helyi", "Localizable": "Lokalizálható", "Localizable field": "Lokalizálható mező", - "Localization": "Lokalizáció", "Localizations": "Lokalizációk", "Location": "Elhelyezkedés", "Locked": "Zárt", diff --git a/lang/id.json b/lang/id.json index 1aa660ab7d3..223011a5c88 100644 --- a/lang/id.json +++ b/lang/id.json @@ -601,7 +601,6 @@ "Locale": "Lokal", "Localizable": "Dapat dilokalkan", "Localizable field": "Bidang yang dapat dilokalkan", - "Localization": "Lokalisasi", "Localizations": "Lokalisasi", "Location": "Lokasi", "Locked": "Terkunci", diff --git a/lang/it.json b/lang/it.json index 321da9f3375..c16aa72ee44 100644 --- a/lang/it.json +++ b/lang/it.json @@ -640,7 +640,6 @@ "Locale": "Localizzazione", "Localizable": "Localizzabile", "Localizable field": "Campo localizzabile", - "Localization": "Localizzazione", "Localizations": "Localizzazioni", "Location": "Posizione", "Locked": "Bloccato", diff --git a/lang/ja.json b/lang/ja.json index bbc28435b33..515db01d7e8 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -601,7 +601,6 @@ "Locale": "地元", "Localizable": "位置特定可能", "Localizable field": "ローカライズ可能なフィールド", - "Localization": "ローカリゼーション", "Localizations": "ローカリゼーション", "Location": "位置", "Locked": "ロックされています", diff --git a/lang/ms.json b/lang/ms.json index cf69c586941..ba90e6c449d 100644 --- a/lang/ms.json +++ b/lang/ms.json @@ -601,7 +601,6 @@ "Locale": "Tempatan", "Localizable": "Boleh disetempatkan", "Localizable field": "Bidang yang boleh disetempatkan", - "Localization": "Penyetempatan", "Localizations": "Penyetempatan", "Location": "Lokasi", "Locked": "Terkunci", diff --git a/lang/nb.json b/lang/nb.json index 1af949a16db..0d5602792cb 100644 --- a/lang/nb.json +++ b/lang/nb.json @@ -636,7 +636,6 @@ "Locale": "Oversettelse", "Localizable": "Kan oversettes", "Localizable field": "Oversettbart felt", - "Localization": "Oversettelse", "Localizations": "Oversettelser", "Location": "Lokasjon", "Locked": "Låst", diff --git a/lang/nl.json b/lang/nl.json index 9b58d233ee9..01a7c751276 100644 --- a/lang/nl.json +++ b/lang/nl.json @@ -679,7 +679,6 @@ "Loading": "Laden", "Locale": "Lokaal", "Localizable": "Vertaalbaar", - "Localization": "Vertaling", "Localizations": "Vertalingen", "Location": "Locatie", "Log in": "Inloggen", diff --git a/lang/pl.json b/lang/pl.json index 759f16f35a0..b4cbc563fa7 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -601,7 +601,6 @@ "Locale": "Lokalny", "Localizable": "W wielu językach", "Localizable field": "Pole w wielu językach", - "Localization": "Język", "Localizations": "Języki", "Location": "Lokalizacja", "Locked": "Zablokowany", diff --git a/lang/pt.json b/lang/pt.json index 54fa922442b..2e6989bc133 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -601,7 +601,6 @@ "Locale": "Local", "Localizable": "Traduzível", "Localizable field": "Campo traduzível", - "Localization": "Localização", "Localizations": "Localizations", "Location": "Localização", "Locked": "Bloqueado", diff --git a/lang/pt_BR.json b/lang/pt_BR.json index 74977ae6547..d74100204fd 100644 --- a/lang/pt_BR.json +++ b/lang/pt_BR.json @@ -601,7 +601,6 @@ "Locale": "Local", "Localizable": "Localizável", "Localizable field": "Campo localizável", - "Localization": "Localização", "Localizations": "Localizações", "Location": "Localização", "Locked": "Bloqueado", diff --git a/lang/ru.json b/lang/ru.json index 31eb1250ccb..2833d06d0d9 100644 --- a/lang/ru.json +++ b/lang/ru.json @@ -719,7 +719,6 @@ "Locale": "Локаль", "Localizable": "Локализуемый", "Localizable field": "Локализуемое поле", - "Localization": "Локализация", "Localizations": "Локализации", "Location": "Расположение", "Locked": "Заблокирован", diff --git a/lang/sl.json b/lang/sl.json index 16fed5c9ea9..d41e713573d 100644 --- a/lang/sl.json +++ b/lang/sl.json @@ -601,7 +601,6 @@ "Locale": "Lokalno", "Localizable": "Lokalizirati", "Localizable field": "Lokalizirano polje", - "Localization": "Lokalizacija", "Localizations": "Lokalizacije", "Location": "Lokacija", "Locked": "Zaklenjeno", diff --git a/lang/sv.json b/lang/sv.json index 8a5c20651c1..ad136411452 100644 --- a/lang/sv.json +++ b/lang/sv.json @@ -601,7 +601,6 @@ "Locale": "Lokal", "Localizable": "Översättningsbar", "Localizable field": "Översättningsbart fält", - "Localization": "Översättning", "Localizations": "Översättningar", "Location": "Plats", "Locked": "Låst", diff --git a/lang/tr.json b/lang/tr.json index d7803718856..7dde91e297d 100644 --- a/lang/tr.json +++ b/lang/tr.json @@ -601,7 +601,6 @@ "Locale": "Yerel Ayar", "Localizable": "Yerelleştirilebilir", "Localizable field": "Yerelleştirilebilir alan", - "Localization": "Yerelleştirme", "Localizations": "Yerelleştirme", "Location": "Konum", "Locked": "Kilitli", diff --git a/lang/uk.json b/lang/uk.json index 8017dfeb40c..47d8f95df69 100644 --- a/lang/uk.json +++ b/lang/uk.json @@ -601,7 +601,6 @@ "Locale": "Локаль", "Localizable": "Локалізується", "Localizable field": "Локалізоване поле", - "Localization": "Локалізація", "Localizations": "Локалізації", "Location": "Розташування", "Locked": "Заблоковано", diff --git a/lang/vi.json b/lang/vi.json index adc1ca996a4..c4a28ac1ce1 100644 --- a/lang/vi.json +++ b/lang/vi.json @@ -601,7 +601,6 @@ "Locale": "Miền địa phương", "Localizable": "Name", "Localizable field": "Trường địa phương", - "Localization": "Miền địa phương", "Localizations": "Miền địa phương", "Location": "Địa điểm", "Locked": "Bị khóa", diff --git a/lang/zh_CN.json b/lang/zh_CN.json index 04e8669bf1f..f421d5de2af 100644 --- a/lang/zh_CN.json +++ b/lang/zh_CN.json @@ -601,7 +601,6 @@ "Locale": "当地的", "Localizable": "可本地化", "Localizable field": "可本地化的字段", - "Localization": "本地化", "Localizations": "本地化", "Location": "地点", "Locked": "锁定的", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index e9d63a9d85e..27b6b03531e 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -601,7 +601,6 @@ "Locale": "当地的", "Localizable": "可當地語系化", "Localizable field": "可當地語系化的欄位", - "Localization": "當地語系化", "Localizations": "當地語系化", "Location": "地点", "Locked": "已鎖定", From 9157cfdbee9201841c9943a8ff395acba162b62a Mon Sep 17 00:00:00 2001 From: Jay George Date: Mon, 24 Aug 2026 16:06:39 +0100 Subject: [PATCH 16/20] Use existing Localizable label for globals sites section Avoid a new Localization string; Localizable is already translated across locales. --- src/Http/Controllers/CP/Globals/GlobalsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Controllers/CP/Globals/GlobalsController.php b/src/Http/Controllers/CP/Globals/GlobalsController.php index 9b5c205b1bb..fa964582a91 100644 --- a/src/Http/Controllers/CP/Globals/GlobalsController.php +++ b/src/Http/Controllers/CP/Globals/GlobalsController.php @@ -225,7 +225,7 @@ protected function editFormBlueprint($set) if (Site::multiEnabled()) { $fields['sites'] = [ - 'display' => __('Localization'), + 'display' => __('Localizable'), 'fields' => [ 'sites' => [ 'type' => 'global_set_sites', From 1a01ef6371b48423295b6550abe44e2bd98c5831 Mon Sep 17 00:00:00 2001 From: Jay George Date: Mon, 24 Aug 2026 16:33:35 +0100 Subject: [PATCH 17/20] Drop Socialite TestCase workaround again Reintroduced by merging multisite-entry-grouping; keep it out of this branch. --- tests/TestCase.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index 83ac192c449..16930353a7e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -74,17 +74,15 @@ public function tearDown(): void protected function getPackageProviders($app) { - return array_values(array_filter([ + return [ \Statamic\Providers\StatamicServiceProvider::class, \Inertia\ServiceProvider::class, \Rebing\GraphQL\GraphQLServiceProvider::class, \Wilderborn\Partyline\ServiceProvider::class, \Archetype\ServiceProvider::class, \Spatie\LaravelRay\RayServiceProvider::class, - class_exists(\Laravel\Socialite\SocialiteServiceProvider::class) - ? \Laravel\Socialite\SocialiteServiceProvider::class - : null, - ])); + \Laravel\Socialite\SocialiteServiceProvider::class, + ]; } protected function getPackageAliases($app) From 4267a4327498ffba64bc85b7bd5c48b3a2094cb4 Mon Sep 17 00:00:00 2001 From: Jay George Date: Mon, 24 Aug 2026 17:19:40 +0100 Subject: [PATCH 18/20] Add checkbox selection to groups too --- resources/js/components/globals/Sites.vue | 55 ++++++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/resources/js/components/globals/Sites.vue b/resources/js/components/globals/Sites.vue index 1d5f3f2e8a1..f08e9e5a3d4 100644 --- a/resources/js/components/globals/Sites.vue +++ b/resources/js/components/globals/Sites.vue @@ -71,13 +71,22 @@