From f3d743e3cdf85e30a6d1c7385a8e091de2cf50d5 Mon Sep 17 00:00:00 2001 From: i-just Date: Wed, 10 Jun 2026 12:24:12 +0200 Subject: [PATCH 1/4] respect alt text translation method from the start --- src/config/app.php | 2 +- src/elements/Asset.php | 53 ++++--------- src/elements/db/AssetQuery.php | 26 ++----- ...0_065719_translatable_alt_text_content.php | 77 +++++++++++++++++++ 4 files changed, 99 insertions(+), 59 deletions(-) create mode 100644 src/migrations/m260610_065719_translatable_alt_text_content.php diff --git a/src/config/app.php b/src/config/app.php index 0751efe2de9..64068528103 100644 --- a/src/config/app.php +++ b/src/config/app.php @@ -4,7 +4,7 @@ 'id' => 'CraftCMS', 'name' => 'Craft CMS', 'version' => '5.10.5', - 'schemaVersion' => '5.10.0.0', + 'schemaVersion' => '5.11.0.0', 'minVersionRequired' => '4.5.0', 'basePath' => dirname(__DIR__), // Defines the @app alias 'runtimePath' => '@storage/runtime', // Defines the @runtime alias diff --git a/src/elements/Asset.php b/src/elements/Asset.php index 4f6113d0c26..41d1645e344 100644 --- a/src/elements/Asset.php +++ b/src/elements/Asset.php @@ -1205,20 +1205,6 @@ private static function isFolderIndex(): bool */ private ?int $_oldVolumeId = null; - /** - * @inheritdoc - */ - public function __construct($config = []) - { - // alt='' actually means something, so we should preserve it. - $alt = ArrayHelper::remove($config, 'alt'); - if ($alt !== null) { - $this->alt = $alt; - } - - parent::__construct($config); - } - /** * @inheritdoc */ @@ -1298,20 +1284,6 @@ public function init(): void $this->_oldVolumeId = $this->_volumeId; } - /** - * @inheritdoc - */ - public function setAttributesFromRequest(array $values): void - { - // alt='' actually means something, so we should preserve it. - $alt = ArrayHelper::remove($values, 'alt'); - if ($alt !== null) { - $this->alt = $alt; - } - - parent::setAttributesFromRequest($values); - } - /** * Returns the volume’s ID. * @@ -3385,10 +3357,6 @@ public function afterSave(bool $isNew): void $record->mimeType = $this->_mimeType; } - if ($record->alt === null) { - $record->alt = $this->alt; - } - if ($this->getHasFocalPoint()) { $focal = $this->getFocalPoint(); $record->focalPoint = number_format($focal['x'], 4) . ';' . number_format($focal['y'], 4); @@ -3397,8 +3365,16 @@ public function afterSave(bool $isNew): void } $record->save(false); + + // we're not propagating at this point, so save the alt ONLY against the site we're saving to + Db::upsert(Table::ASSETS_SITES, [ + 'assetId' => $this->id, + 'siteId' => $this->siteId, + 'alt' => $this->alt, + ]); } + $upsert = false; if ( $this->propagating && $this->propagatingFrom && @@ -3411,15 +3387,18 @@ public function afterSave(bool $isNew): void $this->alt !== $from->alt && $this->getAltTranslationKey() === $from->getAltTranslationKey() ) { + $upsert = true; $this->alt = $from->alt; } } - Db::upsert(Table::ASSETS_SITES, [ - 'assetId' => $this->id, - 'siteId' => $this->siteId, - 'alt' => $this->alt, - ]); + if ($upsert or $this->propagateAll) { + Db::upsert(Table::ASSETS_SITES, [ + 'assetId' => $this->id, + 'siteId' => $this->siteId, + 'alt' => $this->alt, + ]); + } parent::afterSave($isNew); } diff --git a/src/elements/db/AssetQuery.php b/src/elements/db/AssetQuery.php index afafe3a9b68..51b2741015d 100644 --- a/src/elements/db/AssetQuery.php +++ b/src/elements/db/AssetQuery.php @@ -1015,29 +1015,15 @@ protected function afterPrepare(): bool { if ($this->hasAlt !== null) { $hasAltCondition = [ - 'or', + 'and', ['not', ['assets_sites.alt' => '']], - [ - 'and', - ['assets_sites.alt' => null], - ['not', ['assets.alt' => '']], - ['not', ['assets.alt' => null]], - ], + ['not', ['assets_sites.alt' => null]], ]; $withoutAltCondition = [ 'or', ['assets_sites.alt' => ''], - [ - 'and', - ['assets_sites.alt' => null], - [ - 'or', - ['assets.alt' => ''], - ['assets.alt' => null], - ], - - ], + ['assets_sites.alt' => null], ]; $this->subQuery @@ -1153,11 +1139,9 @@ private function _normalizeVolumeId(): void */ public function createElement(array $row): ElementInterface { - // Use the site-specific alt text, if set + // Use the site-specific alt text $siteAlt = ArrayHelper::remove($row, 'siteAlt'); - if ($siteAlt !== null) { - $row['alt'] = $siteAlt; - } + $row['alt'] = $siteAlt; return parent::createElement($row); } diff --git a/src/migrations/m260610_065719_translatable_alt_text_content.php b/src/migrations/m260610_065719_translatable_alt_text_content.php new file mode 100644 index 00000000000..913f78b2723 --- /dev/null +++ b/src/migrations/m260610_065719_translatable_alt_text_content.php @@ -0,0 +1,77 @@ +leftJoin('{{%elements_sites}}', '[[elements_sites.elementId]] = [[assets.id]]') + ->select(['assets.id', 'elements_sites.siteId', 'assets.alt']); + + $assetsRecords = $query->createCommand()->queryAll(); + + // for each of those records, + foreach ($assetsRecords as $assetRecord) { + $exists = (new Query()) + ->from(Table::ASSETS_SITES) + ->where([ + 'assetId' => $assetRecord['id'], + 'siteId' => $assetRecord['siteId'], ] + ) + ->exists(); + + if ($exists) { + // if the assets_sites row exists and the alt column is null, copy over the value from the assets.alt + Db::update( + Table::ASSETS_SITES, + [ + 'alt' => $assetRecord['alt'], + ], + [ + 'assetId' => $assetRecord['id'], + 'siteId' => $assetRecord['siteId'], + 'alt' => null, + ] + ); + } else { + // if the assets_sites row doesn't exist, insert one with the assets.alt value + Db::insert( + Table::ASSETS_SITES, + [ + 'assetId' => $assetRecord['id'], + 'siteId' => $assetRecord['siteId'], + 'alt' => $assetRecord['alt'], + ] + ); + } + // the above ensures we're not changing the alt text developers might expect during the update, + // but at the same time we're all set to no longer use the assets.alt column + } + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + echo "m260610_065719_translatable_alt_text_content cannot be reverted.\n"; + return false; + } +} From 991b950670fa2bbae428aff856803f318bcb21fe Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 5 Aug 2026 08:14:16 -0700 Subject: [PATCH 2/4] Simplify the migration Only create missing assets_sites rows for assets that have globally-defined alt text, and null/missing translated alt text --- ...0_065719_translatable_alt_text_content.php | 69 ++++++++----------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/src/migrations/m260610_065719_translatable_alt_text_content.php b/src/migrations/m260610_065719_translatable_alt_text_content.php index 913f78b2723..43ec20294ab 100644 --- a/src/migrations/m260610_065719_translatable_alt_text_content.php +++ b/src/migrations/m260610_065719_translatable_alt_text_content.php @@ -6,7 +6,6 @@ use craft\db\Query; use craft\db\Table; use craft\helpers\Db; -use craft\records\Asset; /** * m260610_065719_translatable_alt_text_content migration. @@ -18,49 +17,39 @@ class m260610_065719_translatable_alt_text_content extends Migration */ public function safeUp(): bool { - // get all asset records - we only need the id, alt and siteId - $query = Asset::find() - ->leftJoin('{{%elements_sites}}', '[[elements_sites.elementId]] = [[assets.id]]') - ->select(['assets.id', 'elements_sites.siteId', 'assets.alt']); + // Find assets that have globally-defined alt text and null/missing translated alt text, + // and fill in their translated values. - $assetsRecords = $query->createCommand()->queryAll(); + $query = (new Query()) + // fetch assets_sites.assetId so we know whether the row already exists + ->select(['assets.id', 'elements_sites.siteId', 'assets.alt', 'assets_sites.assetId']) + ->from(['assets' => Table::ASSETS]) + ->innerJoin(['elements_sites' => Table::ELEMENTS_SITES], '[[elements_sites.elementId]] = [[assets.id]]') + ->leftJoin(['assets_sites' => Table::ASSETS_SITES], [ + 'and', + '[[assets_sites.assetId]] = [[assets.id]]', + '[[assets_sites.siteId]] = [[elements_sites.siteId]]', + ]) + ->where(['not', ['assets.alt' => null]]) + ->andWhere(['not', ['assets.alt' => '']]) + ->andWhere(['assets_sites.alt' => null]); - // for each of those records, - foreach ($assetsRecords as $assetRecord) { - $exists = (new Query()) - ->from(Table::ASSETS_SITES) - ->where([ - 'assetId' => $assetRecord['id'], - 'siteId' => $assetRecord['siteId'], ] - ) - ->exists(); - - if ($exists) { - // if the assets_sites row exists and the alt column is null, copy over the value from the assets.alt - Db::update( - Table::ASSETS_SITES, - [ - 'alt' => $assetRecord['alt'], - ], - [ - 'assetId' => $assetRecord['id'], - 'siteId' => $assetRecord['siteId'], - 'alt' => null, - ] - ); + foreach (Db::each($query) as $row) { + // If the assets_sites.assetId value came back, the row already exists + if (isset($row['assetId'])) { + Db::update(Table::ASSETS_SITES, [ + 'alt' => $row['alt'], + ], [ + 'assetId' => $row['id'], + 'siteId' => $row['siteId'], + ]); } else { - // if the assets_sites row doesn't exist, insert one with the assets.alt value - Db::insert( - Table::ASSETS_SITES, - [ - 'assetId' => $assetRecord['id'], - 'siteId' => $assetRecord['siteId'], - 'alt' => $assetRecord['alt'], - ] - ); + Db::insert(Table::ASSETS_SITES, [ + 'assetId' => $row['id'], + 'siteId' => $row['siteId'], + 'alt' => $row['alt'], + ]); } - // the above ensures we're not changing the alt text developers might expect during the update, - // but at the same time we're all set to no longer use the assets.alt column } return true; From c516b0ee7682fceac63d6dd6bb9e86efff4d0790 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 5 Aug 2026 08:15:21 -0700 Subject: [PATCH 3/4] Cleanup --- src/elements/db/AssetQuery.php | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/elements/db/AssetQuery.php b/src/elements/db/AssetQuery.php index ac64058968b..1334cf6d0e6 100644 --- a/src/elements/db/AssetQuery.php +++ b/src/elements/db/AssetQuery.php @@ -1015,25 +1015,25 @@ protected function beforePrepare(): bool protected function afterPrepare(): bool { if ($this->hasAlt !== null) { - $hasAltCondition = [ + $this->subQuery->leftJoin(['assets_sites' => Table::ASSETS_SITES], [ 'and', - ['not', ['assets_sites.alt' => '']], - ['not', ['assets_sites.alt' => null]], - ]; + '[[assets_sites.assetId]] = [[assets.id]]', + '[[assets_sites.siteId]] = [[elements_sites.siteId]]', + ]); - $withoutAltCondition = [ - 'or', - ['assets_sites.alt' => ''], - ['assets_sites.alt' => null], - ]; - - $this->subQuery - ->leftJoin(['assets_sites' => Table::ASSETS_SITES], [ + if ($this->hasAlt) { + $this->subQuery->andWhere([ 'and', - '[[assets_sites.assetId]] = [[assets.id]]', - '[[assets_sites.siteId]] = [[elements_sites.siteId]]', - ]) - ->andWhere($this->hasAlt ? $hasAltCondition : $withoutAltCondition); + ['not', ['assets_sites.alt' => '']], + ['not', ['assets_sites.alt' => null]], + ]); + } else { + $this->subQuery->andWhere([ + 'or', + ['assets_sites.alt' => ''], + ['assets_sites.alt' => null], + ]); + } } return parent::afterPrepare(); From 312a3101dbc79b315a49890bc7f8749037c667c2 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Thu, 6 Aug 2026 10:53:43 -0700 Subject: [PATCH 4/4] Release note [ci skip] --- CHANGELOG-5.11.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG-5.11.md b/CHANGELOG-5.11.md index 78b75435980..6a4c4815899 100644 --- a/CHANGELOG-5.11.md +++ b/CHANGELOG-5.11.md @@ -1,5 +1,8 @@ # Release Notes for Craft CMS 5.11 (WIP) +### Content Management +- Asset alt text no longer gets propagated to all sites the first time it’s filled in. ([#19067](https://github.com/craftcms/cms/pull/19067)) + ### Development - The `params` argument of the `url()` Twig function now accepts `false` to remove all params from the passed-in URL. ([#19102](https://github.com/craftcms/cms/pull/19102)) - Added `craft\web\DbSession`, which should be used instead of `yii\web\DbSession` to prevent “headers already sent” warnings from getting logged. ([#19139](https://github.com/craftcms/cms/issues/19139))