Created
December 12, 2025 23:44
-
-
Save daftspunk/3814997a701d0de4d81b6d793ac318f1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| namespace OFFLINE\Boxes\Classes\Fixes; | |
| use OFFLINE\Boxes\Classes\Exceptions\PartialNotFoundException; | |
| use OFFLINE\Boxes\Classes\Partial\PartialReader; | |
| use OFFLINE\Boxes\Models\Box; | |
| use RainLab\Translate\Classes\Translator; | |
| /** | |
| * BoxTranslatableFix | |
| * | |
| * Fixes RainLab.Translate integration issues with Box model translatable fields. | |
| * | |
| * Problem: Translatable fields in Box partials are stored inside a JSON `data` column, | |
| * but RainLab.Translate expects them as regular model attributes. The Box model's | |
| * getAttribute() method returns values from the `data` JSON directly, bypassing | |
| * RainLab.Translate's event hooks. | |
| * | |
| * This fix: | |
| * 1. Removes translatable fields from `data` JSON when saving in non-default locale | |
| * (prevents overwriting primary locale values in the data column) | |
| * 2. Merges translated values into the `data` attribute after fetch in non-default locales | |
| * (ensures getAttribute() returns the correct translated values) | |
| * 3. Preloads translation data for proper form display | |
| * | |
| * Usage: Call BoxTranslatableFix::apply() in your plugin's boot() method. | |
| * | |
| * @example | |
| * // In your Plugin.php boot() method: | |
| * public function boot() | |
| * { | |
| * \OFFLINE\Boxes\Classes\Fixes\BoxTranslatableFix::apply(); | |
| * } | |
| */ | |
| class BoxTranslatableFix | |
| { | |
| /** | |
| * Apply the fix to the Box model via extension. | |
| */ | |
| public static function apply(): void | |
| { | |
| if (!class_exists(\RainLab\Translate\Models\Locale::class)) { | |
| return; | |
| } | |
| Box::extend(function (Box $model) { | |
| static::extendBeforeSetAttribute($model); | |
| static::extendAfterFetch($model); | |
| static::extendBeforeValidate($model); | |
| }); | |
| } | |
| /** | |
| * Intercept data attribute to remove translatable fields when saving in non-default locale. | |
| * | |
| * This prevents translated values from overwriting the primary locale values | |
| * in the offline_boxes_boxes.data column. | |
| */ | |
| protected static function extendBeforeSetAttribute(Box $model): void | |
| { | |
| $model->bindEvent('model.beforeSetAttribute', function ($key, $value) use ($model) { | |
| if ($key !== 'data' || !is_array($value)) { | |
| return $value; | |
| } | |
| // Get translatable fields from partial config | |
| $translatableFields = static::getTranslatableFields($model); | |
| if (empty($translatableFields)) { | |
| return $value; | |
| } | |
| // Check if we're saving in a non-default locale | |
| if (!$model->methodExists('shouldTranslate') || !$model->shouldTranslate()) { | |
| return $value; | |
| } | |
| // Remove translatable fields from data - they'll be stored in rainlab_translate_attributes | |
| foreach ($translatableFields as $field) { | |
| unset($value[$field]); | |
| } | |
| return $value; | |
| }, 999); // High priority to run before the original handler | |
| } | |
| /** | |
| * After fetching, merge translated values into the data attribute for non-default locales. | |
| * | |
| * Since Box::getAttribute() returns values directly from getDecodedData() without | |
| * firing the model.beforeGetAttribute event, we need to merge translated values | |
| * into the data attribute so they are returned correctly. | |
| * | |
| * This runs for both frontend AND backend to ensure consistent behavior. | |
| */ | |
| protected static function extendAfterFetch(Box $model): void | |
| { | |
| $model->bindEvent('model.afterFetch', function () use ($model) { | |
| // Ensure translatable fields are set first | |
| static::ensureTranslatableFieldsSet($model); | |
| // Only process if we're in a non-default locale | |
| if (!$model->methodExists('shouldTranslate') || !$model->shouldTranslate()) { | |
| return; | |
| } | |
| // Load translation data for current locale | |
| $currentLocale = Translator::instance()->getLocale(true); | |
| if ($model->methodExists('loadTranslatableData')) { | |
| $model->loadTranslatableData($currentLocale); | |
| } | |
| // Get translated values from RainLab.Translate | |
| if (!$model->methodExists('getTranslateAttributes')) { | |
| return; | |
| } | |
| $translatedValues = $model->getTranslateAttributes($currentLocale); | |
| if (empty($translatedValues)) { | |
| return; | |
| } | |
| // Merge translated values into the data attribute | |
| // This ensures Box::getAttribute() returns the translated values | |
| $data = json_decode($model->attributes['data'] ?? '{}', true) ?: []; | |
| $translatableFields = $model->translatable ?? []; | |
| foreach ($translatableFields as $field) { | |
| if (isset($translatedValues[$field]) && $translatedValues[$field] !== '') { | |
| $data[$field] = $translatedValues[$field]; | |
| } | |
| } | |
| // Update the data attribute with merged translations | |
| // Note: We set it directly on attributes to avoid triggering beforeSetAttribute | |
| $model->attributes['data'] = json_encode($data, JSON_UNESCAPED_UNICODE); | |
| // Clear the decoded data cache so getDecodedData() returns fresh values | |
| if (property_exists($model, 'decodedData')) { | |
| $model->decodedData = null; | |
| } | |
| }, 1000); // Run after the original afterFetch handler (which runs at default priority) | |
| } | |
| /** | |
| * Before validation/save, ensure translatable fields are set. | |
| */ | |
| protected static function extendBeforeValidate(Box $model): void | |
| { | |
| $model->bindEvent('model.beforeValidate', function () use ($model) { | |
| static::ensureTranslatableFieldsSet($model); | |
| }, 999); | |
| } | |
| /** | |
| * Get translatable fields from the Box's partial config. | |
| */ | |
| protected static function getTranslatableFields(Box $model): array | |
| { | |
| // If already set on model, use that | |
| if (!empty($model->translatable)) { | |
| return $model->translatable; | |
| } | |
| // Try to get from partial config | |
| try { | |
| $partial = $model->partial; | |
| if (empty($partial)) { | |
| return []; | |
| } | |
| $partialObj = PartialReader::instance()->findByHandle($partial); | |
| return $partialObj?->config->translatable ?? []; | |
| } catch (PartialNotFoundException $e) { | |
| return []; | |
| } catch (\Exception $e) { | |
| return []; | |
| } | |
| } | |
| /** | |
| * Ensure the translatable property is set on the model from its partial config. | |
| */ | |
| protected static function ensureTranslatableFieldsSet(Box $model): void | |
| { | |
| if (!empty($model->translatable)) { | |
| return; | |
| } | |
| $translatableFields = static::getTranslatableFields($model); | |
| if (!empty($translatableFields)) { | |
| $model->translatable = $translatableFields; | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment