Skip to content

Instantly share code, notes, and snippets.

@daftspunk
Created December 12, 2025 23:44
Show Gist options
  • Select an option

  • Save daftspunk/3814997a701d0de4d81b6d793ac318f1 to your computer and use it in GitHub Desktop.

Select an option

Save daftspunk/3814997a701d0de4d81b6d793ac318f1 to your computer and use it in GitHub Desktop.
<?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