From 8b0ee0a920784c0e4898fcbe25847889b610adc0 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Mon, 19 Feb 2024 16:02:08 -0500 Subject: [PATCH 01/51] first draft of MLRepeaterFields --- Plugin.php | 1 + classes/EventRegistry.php | 22 +++- formwidgets/MLRepeaterFields.php | 109 +++++++++++++++++++ traits/MLControl.php | 11 +- traits/mlcontrol/partials/_locale_values.htm | 2 +- 5 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 formwidgets/MLRepeaterFields.php diff --git a/Plugin.php b/Plugin.php index d761fe4d..e6d2df3a 100644 --- a/Plugin.php +++ b/Plugin.php @@ -144,6 +144,7 @@ public function registerFormWidgets(): array \Winter\Translate\FormWidgets\MLRichEditor::class => 'mlricheditor', \Winter\Translate\FormWidgets\MLMarkdownEditor::class => 'mlmarkdowneditor', \Winter\Translate\FormWidgets\MLRepeater::class => 'mlrepeater', + \Winter\Translate\FormWidgets\MLRepeaterFields::class => 'mlrepeaterfields', \Winter\Translate\FormWidgets\MLMediaFinder::class => 'mlmediafinder', \Winter\Translate\FormWidgets\MLNestedForm::class => 'mlnestedform', ]; diff --git a/classes/EventRegistry.php b/classes/EventRegistry.php index 31480670..4a56ac33 100644 --- a/classes/EventRegistry.php +++ b/classes/EventRegistry.php @@ -10,6 +10,7 @@ use Str; use System\Classes\MailManager; use System\Classes\PluginManager; +use Winter\Storm\Html\Helper as HtmlHelper; use Winter\Translate\Classes\ThemeScanner; use Winter\Translate\Classes\Translator; use Winter\Translate\Models\Locale as LocaleModel; @@ -128,7 +129,19 @@ public function registerModelTranslation($widget) } - if (!$model->hasTranslatableAttributes() || $widget->isNested) { + if (!$model->hasTranslatableAttributes()) { + return; + } + + if ($widget->isNested) { + $nameArray = HtmlHelper::nameToArray($widget->arrayName); + array_shift($nameArray); // remove parent + array_pop($nameArray); // remove repeater index + + $parentName = array_shift($nameArray); + $parentName .= '[' . implode('][', $nameArray) . ']'; + + $widget->fields = $this->processFormMLFields($widget->fields, $model, $parentName); return; } @@ -151,7 +164,7 @@ public function registerModelTranslation($widget) * @param Model $model * @return array */ - protected function processFormMLFields($fields, $model) + protected function processFormMLFields($fields, $model, $parent = null) { $typesMap = [ 'markdown' => 'mlmarkdowneditor', @@ -173,7 +186,8 @@ protected function processFormMLFields($fields, $model) } foreach ($fields as $name => $config) { - $fieldName = $name; + $fieldName = $parent ? sprintf("%s[%s]", $parent, $name) : $name; + if (str_contains($name, '@')) { // apply to fields with any context list($fieldName, $context) = explode('@', $name); @@ -183,10 +197,10 @@ protected function processFormMLFields($fields, $model) } $type = array_get($config, 'type', 'text'); - if (array_key_exists($type, $typesMap)) { $fields[$name]['type'] = $typesMap[$type]; } + } return $fields; diff --git a/formwidgets/MLRepeaterFields.php b/formwidgets/MLRepeaterFields.php new file mode 100644 index 00000000..698cd855 --- /dev/null +++ b/formwidgets/MLRepeaterFields.php @@ -0,0 +1,109 @@ +initLocale(); + + if ($this->model) { + $this->model->extend(function () { + $this->addDynamicMethod('getJsonAttributeTranslated', function ($key, $locale) { + $names = HtmlHelper::nameToArray($key); + array_shift($names); // remove model + + $array = array_shift($names); + $field = array_pop($names); + + if ($array && $field && $names) { + return array_get($this->{$array}, implode('.', $names) . '.locale' . ucfirst($field) . '.' . $locale); + } + }); + }); + } + } + + public function render() + { + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + return $parentContent; + } + + protected function loadAssets() + { + $this->actAsParent(); + parent::loadAssets(); + $this->actAsParent(false); + } + + protected function getParentViewPath() + { + return base_path().'/modules/backend/formwidgets/repeater/partials'; + } + + /** + * {@inheritDoc} + */ + protected function getParentAssetPath() + { + return '/modules/backend/formwidgets/repeater/assets'; + } + + public function onAddItem() + { + $this->actAsParent(); + return parent::onAddItem(); + } + + public function getSaveValue($value) + { + return $this->getLocaleSaveValue(is_array($value) ? array_values($value) : $value); + } + + public function getLocaleSaveValue($value) + { + $fieldName = implode('.', HtmlHelper::nameToArray($this->formField->getName())); + + foreach (post('RLTranslate') as $locale => $_data) { + $items = array_get($_data, $fieldName, []); + foreach ($items as $index => $item) { + foreach ($item as $field => $fieldValue) { + if ($locale === $this->defaultLocale->code) { + $value[$index][$field] = $fieldValue; + } else { + $key = sprintf("locale%s", ucfirst($field)); + $value[$index][$key][$locale] = $fieldValue; + } + } + } + } + + return $value; + } +} diff --git a/traits/MLControl.php b/traits/MLControl.php index 2e8b2f29..cbaab59d 100644 --- a/traits/MLControl.php +++ b/traits/MLControl.php @@ -157,6 +157,9 @@ public function getLocaleValue($locale) } elseif ($this->objectMethodExists($this->model, 'getAttributeTranslated') && $this->defaultLocale->code != $locale) { $value = $this->model->noFallbackLocale()->getAttributeTranslated($key, $locale); + if (!$value && $this->objectMethodExists($this->model, 'getJsonAttributeTranslated')) { + $value = $this->model->getJsonAttributeTranslated($this->formField->getName(), $locale); + } } else { $value = $this->formField->value; @@ -180,6 +183,12 @@ protected function makeRenderFormField() return $field; } + public function getLocaleFieldName($code) + { + $names = HtmlHelper::nameToArray($this->formField->arrayName); + return $this->formField->getName('RLTranslate[' . $code . '][' . implode('][', $names) . ']'); + } + /** * {@inheritDoc} */ @@ -221,7 +230,7 @@ public function getLocaleSaveData() return $values; } - $fieldName = implode('.', HtmlHelper::nameToArray($this->fieldName)); + $fieldName = implode('.', HtmlHelper::nameToArray($this->formField->getName())); $isJson = $this->isLocaleFieldJsonable(); foreach ($data as $locale => $_data) { diff --git a/traits/mlcontrol/partials/_locale_values.htm b/traits/mlcontrol/partials/_locale_values.htm index 7913ec46..d35accbd 100644 --- a/traits/mlcontrol/partials/_locale_values.htm +++ b/traits/mlcontrol/partials/_locale_values.htm @@ -6,7 +6,7 @@ ?> getAttributes() ?> From faef2c3083c3ef79796165ae785eabf1bfb4fb1f Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Mon, 19 Feb 2024 16:59:32 -0500 Subject: [PATCH 02/51] merge new repeater field with internal fields translation --- Plugin.php | 1 - classes/EventRegistry.php | 22 ++++--- formwidgets/MLRepeater.php | 52 ++++++++++++++- formwidgets/MLRepeaterFields.php | 109 ------------------------------- 4 files changed, 63 insertions(+), 121 deletions(-) delete mode 100644 formwidgets/MLRepeaterFields.php diff --git a/Plugin.php b/Plugin.php index e6d2df3a..d761fe4d 100644 --- a/Plugin.php +++ b/Plugin.php @@ -144,7 +144,6 @@ public function registerFormWidgets(): array \Winter\Translate\FormWidgets\MLRichEditor::class => 'mlricheditor', \Winter\Translate\FormWidgets\MLMarkdownEditor::class => 'mlmarkdowneditor', \Winter\Translate\FormWidgets\MLRepeater::class => 'mlrepeater', - \Winter\Translate\FormWidgets\MLRepeaterFields::class => 'mlrepeaterfields', \Winter\Translate\FormWidgets\MLMediaFinder::class => 'mlmediafinder', \Winter\Translate\FormWidgets\MLNestedForm::class => 'mlnestedform', ]; diff --git a/classes/EventRegistry.php b/classes/EventRegistry.php index 4a56ac33..eea7de8d 100644 --- a/classes/EventRegistry.php +++ b/classes/EventRegistry.php @@ -134,14 +134,7 @@ public function registerModelTranslation($widget) } if ($widget->isNested) { - $nameArray = HtmlHelper::nameToArray($widget->arrayName); - array_shift($nameArray); // remove parent - array_pop($nameArray); // remove repeater index - - $parentName = array_shift($nameArray); - $parentName .= '[' . implode('][', $nameArray) . ']'; - - $widget->fields = $this->processFormMLFields($widget->fields, $model, $parentName); + $widget->fields = $this->processFormMLFields($widget->fields, $model, $this->getWidgetName($widget)); return; } @@ -158,6 +151,19 @@ public function registerModelTranslation($widget) } } + protected function getWidgetName($widget) + { + $nameArray = HtmlHelper::nameToArray($widget->arrayName); + + array_shift($nameArray); // remove parenta model + array_pop($nameArray); // remove repeater index + + $parentName = array_shift($nameArray); + $parentName .= '[' . implode('][', $nameArray) . ']'; + + return $parentName; + } + /** * Helper function to replace standard fields with multi lingual equivalents * @param array $fields diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index 460463fd..92843093 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -24,13 +24,32 @@ class MLRepeater extends Repeater */ protected $defaultAlias = 'mlrepeater'; + protected $translateFields = false; /** * {@inheritDoc} */ public function init() { parent::init(); + + $this->fillFromConfig(['translateFields']); $this->initLocale(); + + if ($this->translateFields && $this->model) { + $this->model->extend(function () { + $this->addDynamicMethod('getJsonAttributeTranslated', function ($key, $locale) { + $names = HtmlHelper::nameToArray($key); + array_shift($names); // remove model + + $array = array_shift($names); + $field = array_pop($names); + + if ($array && $field && $names) { + return array_get($this->{$array}, implode('.', $names) . '.locale' . ucfirst($field) . '.' . $locale); + } + }); + }); + } } /** @@ -42,7 +61,7 @@ public function render() $parentContent = parent::render(); $this->actAsParent(false); - if (!$this->isAvailable) { + if (!$this->isAvailable || $this->translateFields) { return $parentContent; } @@ -62,11 +81,38 @@ public function prepareVars() */ public function getSaveValue($value) { - $this->rewritePostValues(); + if (!$this->translateFields) { + $this->rewritePostValues(); + } return $this->getLocaleSaveValue(is_array($value) ? array_values($value) : $value); } + public function getLocaleSaveValue($value) + { + if (!$this->translateFields) { + return parent::getLocaleSaveValue(is_array($value) ? array_values($value) : $value); + } + + $fieldName = implode('.', HtmlHelper::nameToArray($this->formField->getName())); + + foreach (post('RLTranslate') as $locale => $_data) { + $items = array_get($_data, $fieldName, []); + foreach ($items as $index => $item) { + foreach ($item as $field => $fieldValue) { + if ($locale === $this->defaultLocale->code) { + $value[$index][$field] = $fieldValue; + } else { + $key = sprintf("locale%s", ucfirst($field)); + $value[$index][$key][$locale] = $fieldValue; + } + } + } + } + + return $value; + } + /** * {@inheritDoc} */ @@ -76,7 +122,7 @@ protected function loadAssets() parent::loadAssets(); $this->actAsParent(false); - if (Locale::isAvailable()) { + if (Locale::isAvailable() && !$this->translateFields) { $this->loadLocaleAssets(); $this->addJs('js/mlrepeater.js'); } diff --git a/formwidgets/MLRepeaterFields.php b/formwidgets/MLRepeaterFields.php deleted file mode 100644 index 698cd855..00000000 --- a/formwidgets/MLRepeaterFields.php +++ /dev/null @@ -1,109 +0,0 @@ -initLocale(); - - if ($this->model) { - $this->model->extend(function () { - $this->addDynamicMethod('getJsonAttributeTranslated', function ($key, $locale) { - $names = HtmlHelper::nameToArray($key); - array_shift($names); // remove model - - $array = array_shift($names); - $field = array_pop($names); - - if ($array && $field && $names) { - return array_get($this->{$array}, implode('.', $names) . '.locale' . ucfirst($field) . '.' . $locale); - } - }); - }); - } - } - - public function render() - { - $this->actAsParent(); - $parentContent = parent::render(); - $this->actAsParent(false); - - return $parentContent; - } - - protected function loadAssets() - { - $this->actAsParent(); - parent::loadAssets(); - $this->actAsParent(false); - } - - protected function getParentViewPath() - { - return base_path().'/modules/backend/formwidgets/repeater/partials'; - } - - /** - * {@inheritDoc} - */ - protected function getParentAssetPath() - { - return '/modules/backend/formwidgets/repeater/assets'; - } - - public function onAddItem() - { - $this->actAsParent(); - return parent::onAddItem(); - } - - public function getSaveValue($value) - { - return $this->getLocaleSaveValue(is_array($value) ? array_values($value) : $value); - } - - public function getLocaleSaveValue($value) - { - $fieldName = implode('.', HtmlHelper::nameToArray($this->formField->getName())); - - foreach (post('RLTranslate') as $locale => $_data) { - $items = array_get($_data, $fieldName, []); - foreach ($items as $index => $item) { - foreach ($item as $field => $fieldValue) { - if ($locale === $this->defaultLocale->code) { - $value[$index][$field] = $fieldValue; - } else { - $key = sprintf("locale%s", ucfirst($field)); - $value[$index][$key][$locale] = $fieldValue; - } - } - } - } - - return $value; - } -} From 2e30b7e555edaf4856e76b8ffba6631b81b45b14 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Mon, 19 Feb 2024 20:51:38 -0500 Subject: [PATCH 03/51] put all elements into the array then implode --- formwidgets/MLRepeater.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index 92843093..a762e24a 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -25,6 +25,7 @@ class MLRepeater extends Repeater protected $defaultAlias = 'mlrepeater'; protected $translateFields = false; + /** * {@inheritDoc} */ @@ -41,11 +42,13 @@ public function init() $names = HtmlHelper::nameToArray($key); array_shift($names); // remove model - $array = array_shift($names); + $arrayName = array_shift($names); $field = array_pop($names); - if ($array && $field && $names) { - return array_get($this->{$array}, implode('.', $names) . '.locale' . ucfirst($field) . '.' . $locale); + if ($arrayName && $field) { + $names[] = 'locale' . ucfirst($field); + $names[] = $locale; + return array_get($this->{$arrayName}, implode('.', $names)); } }); }); From 0ccb6c4df7900436a17b4332eb76603dc17dc7f2 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 20 Feb 2024 12:50:48 -0500 Subject: [PATCH 04/51] cleanup --- formwidgets/MLRepeater.php | 60 ++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index a762e24a..53ae60f6 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -24,7 +24,10 @@ class MLRepeater extends Repeater */ protected $defaultAlias = 'mlrepeater'; - protected $translateFields = false; + /** + * The repeater translation mode (repeater|fields) + */ + protected $translationMode = 'repeater'; /** * {@inheritDoc} @@ -32,11 +35,11 @@ class MLRepeater extends Repeater public function init() { parent::init(); - - $this->fillFromConfig(['translateFields']); $this->initLocale(); - if ($this->translateFields && $this->model) { + $this->fillFromConfig(['translationMode']); + + if ($this->translationMode === 'fields' && $this->model) { $this->model->extend(function () { $this->addDynamicMethod('getJsonAttributeTranslated', function ($key, $locale) { $names = HtmlHelper::nameToArray($key); @@ -64,7 +67,7 @@ public function render() $parentContent = parent::render(); $this->actAsParent(false); - if (!$this->isAvailable || $this->translateFields) { + if (!$this->isAvailable || $this->translationMode === 'fields') { return $parentContent; } @@ -84,36 +87,31 @@ public function prepareVars() */ public function getSaveValue($value) { - if (!$this->translateFields) { - $this->rewritePostValues(); - } - - return $this->getLocaleSaveValue(is_array($value) ? array_values($value) : $value); - } - - public function getLocaleSaveValue($value) - { - if (!$this->translateFields) { - return parent::getLocaleSaveValue(is_array($value) ? array_values($value) : $value); - } - - $fieldName = implode('.', HtmlHelper::nameToArray($this->formField->getName())); - - foreach (post('RLTranslate') as $locale => $_data) { - $items = array_get($_data, $fieldName, []); - foreach ($items as $index => $item) { - foreach ($item as $field => $fieldValue) { - if ($locale === $this->defaultLocale->code) { - $value[$index][$field] = $fieldValue; - } else { - $key = sprintf("locale%s", ucfirst($field)); - $value[$index][$key][$locale] = $fieldValue; + $value = is_array($value) ? array_values($value) : $value; + + // process internal fields translations + if ($this->translationMode === 'fields') { + $fieldName = implode('.', HtmlHelper::nameToArray($this->formField->getName())); + + foreach (post('RLTranslate') as $locale => $_data) { + $items = array_get($_data, $fieldName, []); + foreach ($items as $index => $item) { + foreach ($item as $field => $fieldValue) { + if ($locale === $this->defaultLocale->code) { + $value[$index][$field] = $fieldValue; + } else { + $key = sprintf("locale%s", ucfirst($field)); + $value[$index][$key][$locale] = $fieldValue; + } } } } + return $value; } - return $value; + // we translate the repeater formwidget itself ($this->translationMode === 'repeater') + $this->rewritePostValues(); + return $this->getLocaleSaveValue($value); } /** @@ -125,7 +123,7 @@ protected function loadAssets() parent::loadAssets(); $this->actAsParent(false); - if (Locale::isAvailable() && !$this->translateFields) { + if (Locale::isAvailable() && $this->translationMode === 'repeater') { $this->loadLocaleAssets(); $this->addJs('js/mlrepeater.js'); } From 203c99166e289f41e4e5f739371668754b0e38d6 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 20 Feb 2024 14:35:02 -0500 Subject: [PATCH 05/51] allow field translatable config for repeater forms in "fields" translationMode --- classes/EventRegistry.php | 9 +++++++-- formwidgets/MLRepeater.php | 6 ++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/classes/EventRegistry.php b/classes/EventRegistry.php index eea7de8d..4c84593a 100644 --- a/classes/EventRegistry.php +++ b/classes/EventRegistry.php @@ -134,7 +134,9 @@ public function registerModelTranslation($widget) } if ($widget->isNested) { - $widget->fields = $this->processFormMLFields($widget->fields, $model, $this->getWidgetName($widget)); + if (isset($widget->config->translationMode) && $widget->config->translationMode === 'fields') { + $widget->fields = $this->processFormMLFields($widget->fields, $model, $this->getWidgetName($widget)); + } return; } @@ -198,10 +200,13 @@ protected function processFormMLFields($fields, $model, $parent = null) // apply to fields with any context list($fieldName, $context) = explode('@', $name); } - if (!array_key_exists($fieldName, $translatable)) { + if (! (array_get($config, 'translatable') || array_key_exists($fieldName, $translatable)) ) { + // field is not in model's translatable array and its translatable config is false continue; } + // field is in model's translatable array or its translatable config is true + $type = array_get($config, 'type', 'text'); if (array_key_exists($type, $typesMap)) { $fields[$name]['type'] = $typesMap[$type]; diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index 53ae60f6..6564ccf0 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -34,11 +34,13 @@ class MLRepeater extends Repeater */ public function init() { + $this->fillFromConfig(['translationMode']); + // make the translationMode available to the repeater items formwidgets + $this->config->form['translationMode'] = $this->translationMode; + parent::init(); $this->initLocale(); - $this->fillFromConfig(['translationMode']); - if ($this->translationMode === 'fields' && $this->model) { $this->model->extend(function () { $this->addDynamicMethod('getJsonAttributeTranslated', function ($key, $locale) { From 685fa43d3ed4c65122bf5a9344f258f302458986 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 11:13:52 -0500 Subject: [PATCH 06/51] properly store repeater fields translations in attributes table --- formwidgets/MLRepeater.php | 35 +++++------------------------------ traits/MLControl.php | 2 +- 2 files changed, 6 insertions(+), 31 deletions(-) diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index 6564ccf0..98c07379 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -46,14 +46,8 @@ public function init() $this->addDynamicMethod('getJsonAttributeTranslated', function ($key, $locale) { $names = HtmlHelper::nameToArray($key); array_shift($names); // remove model - - $arrayName = array_shift($names); - $field = array_pop($names); - - if ($arrayName && $field) { - $names[] = 'locale' . ucfirst($field); - $names[] = $locale; - return array_get($this->{$arrayName}, implode('.', $names)); + if ($arrayName = array_shift($names)) { + return array_get($this->lang($locale)->{$arrayName}, implode('.', $names)); } }); }); @@ -91,29 +85,10 @@ public function getSaveValue($value) { $value = is_array($value) ? array_values($value) : $value; - // process internal fields translations - if ($this->translationMode === 'fields') { - $fieldName = implode('.', HtmlHelper::nameToArray($this->formField->getName())); - - foreach (post('RLTranslate') as $locale => $_data) { - $items = array_get($_data, $fieldName, []); - foreach ($items as $index => $item) { - foreach ($item as $field => $fieldValue) { - if ($locale === $this->defaultLocale->code) { - $value[$index][$field] = $fieldValue; - } else { - $key = sprintf("locale%s", ucfirst($field)); - $value[$index][$key][$locale] = $fieldValue; - } - } - } - } - return $value; + if ($this->translationMode === 'repeater') { + $this->rewritePostValues(); } - - // we translate the repeater formwidget itself ($this->translationMode === 'repeater') - $this->rewritePostValues(); - return $this->getLocaleSaveValue($value); + return $this->getLocaleSaveValue(is_array($value) ? array_values($value) : $value); } /** diff --git a/traits/MLControl.php b/traits/MLControl.php index cbaab59d..ff4491a7 100644 --- a/traits/MLControl.php +++ b/traits/MLControl.php @@ -235,7 +235,7 @@ public function getLocaleSaveData() foreach ($data as $locale => $_data) { $value = array_get($_data, $fieldName); - $values[$locale] = $isJson ? json_decode($value, true) : $value; + $values[$locale] = ($isJson && is_string($value)) ? json_decode($value, true) : $value; } return $values; From 1f73b765f517efd178cbc4870da69f49fe02e4e3 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 11:29:43 -0500 Subject: [PATCH 07/51] save a call by determining if the field parent is jsonable --- traits/MLControl.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/traits/MLControl.php b/traits/MLControl.php index ff4491a7..7bb63ac2 100644 --- a/traits/MLControl.php +++ b/traits/MLControl.php @@ -155,6 +155,9 @@ public function getLocaleValue($locale) if ($this->objectMethodExists($this->model, $mutateMethod)) { $value = $this->model->$mutateMethod($locale); } + elseif ($this->isFieldParentJsonable() && $this->defaultLocale->code != $locale) { + $value = $this->model->getJsonAttributeTranslated($this->formField->getName(), $locale); + } elseif ($this->objectMethodExists($this->model, 'getAttributeTranslated') && $this->defaultLocale->code != $locale) { $value = $this->model->noFallbackLocale()->getAttributeTranslated($key, $locale); if (!$value && $this->objectMethodExists($this->model, 'getJsonAttributeTranslated')) { @@ -250,6 +253,19 @@ public function getFallbackType() return defined('static::FALLBACK_TYPE') ? static::FALLBACK_TYPE : 'text'; } + public function isFieldParentJsonable() + { + $names = HtmlHelper::nameToArray($this->formField->getName()); + array_shift($names); //remove model name + $arrayName = array_shift($names); + + if (method_exists($this->model, 'isJsonable') && $this->model->isJsonable($arrayName)) { + return true; + } else { + return false; + } + } + /** * Returns true if widget is a repeater, or the field is specified * as jsonable in the model. From a8724e55cdca2b7e12618e787af41ce506b1b713 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 11:48:03 -0500 Subject: [PATCH 08/51] simplify code --- traits/MLControl.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/traits/MLControl.php b/traits/MLControl.php index 7bb63ac2..75c10075 100644 --- a/traits/MLControl.php +++ b/traits/MLControl.php @@ -255,9 +255,8 @@ public function getFallbackType() public function isFieldParentJsonable() { - $names = HtmlHelper::nameToArray($this->formField->getName()); - array_shift($names); //remove model name - $arrayName = array_shift($names); + $names = HtmlHelper::nameToArray($this->formField->arrayName); + $arrayName = $names[1]; if (method_exists($this->model, 'isJsonable') && $this->model->isJsonable($arrayName)) { return true; From f0da1c5deb364528e4431ac18c3005a7f147aa18 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 12:15:05 -0500 Subject: [PATCH 09/51] improve code robustness and add some comments --- traits/MLControl.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/traits/MLControl.php b/traits/MLControl.php index 75c10075..13bac256 100644 --- a/traits/MLControl.php +++ b/traits/MLControl.php @@ -155,7 +155,10 @@ public function getLocaleValue($locale) if ($this->objectMethodExists($this->model, $mutateMethod)) { $value = $this->model->$mutateMethod($locale); } - elseif ($this->isFieldParentJsonable() && $this->defaultLocale->code != $locale) { + elseif ($this->defaultLocale->code != $locale && $this->isFieldParentJsonable() && + $this->objectMethodExists($this->model, 'getJsonAttributeTranslated') + ) + { $value = $this->model->getJsonAttributeTranslated($this->formField->getName(), $locale); } elseif ($this->objectMethodExists($this->model, 'getAttributeTranslated') && $this->defaultLocale->code != $locale) { @@ -256,13 +259,15 @@ public function getFallbackType() public function isFieldParentJsonable() { $names = HtmlHelper::nameToArray($this->formField->arrayName); - $arrayName = $names[1]; + if (count($names) >= 2) { + // $names[0] is the Model, $names[1] is the top array name + $arrayName = $names[1]; - if (method_exists($this->model, 'isJsonable') && $this->model->isJsonable($arrayName)) { - return true; - } else { - return false; + if (method_exists($this->model, 'isJsonable') && $this->model->isJsonable($arrayName)) { + return true; + } } + return false; } /** From ef184a3e09e43af1667c74b31651052e8f2061ed Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 12:38:36 -0500 Subject: [PATCH 10/51] remove translatable config support --- classes/EventRegistry.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/classes/EventRegistry.php b/classes/EventRegistry.php index 4c84593a..ae9cdee9 100644 --- a/classes/EventRegistry.php +++ b/classes/EventRegistry.php @@ -200,13 +200,10 @@ protected function processFormMLFields($fields, $model, $parent = null) // apply to fields with any context list($fieldName, $context) = explode('@', $name); } - if (! (array_get($config, 'translatable') || array_key_exists($fieldName, $translatable)) ) { - // field is not in model's translatable array and its translatable config is false + if (!array_key_exists($fieldName, $translatable)) { continue; } - // field is in model's translatable array or its translatable config is true - $type = array_get($config, 'type', 'text'); if (array_key_exists($type, $typesMap)) { $fields[$name]['type'] = $typesMap[$type]; From 0b7d1bf4a58095c4435525d6614a8f23c23e9e66 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 12:45:01 -0500 Subject: [PATCH 11/51] cleanup --- classes/EventRegistry.php | 2 -- formwidgets/MLRepeater.php | 2 -- 2 files changed, 4 deletions(-) diff --git a/classes/EventRegistry.php b/classes/EventRegistry.php index ae9cdee9..39fe1951 100644 --- a/classes/EventRegistry.php +++ b/classes/EventRegistry.php @@ -203,12 +203,10 @@ protected function processFormMLFields($fields, $model, $parent = null) if (!array_key_exists($fieldName, $translatable)) { continue; } - $type = array_get($config, 'type', 'text'); if (array_key_exists($type, $typesMap)) { $fields[$name]['type'] = $typesMap[$type]; } - } return $fields; diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index 98c07379..f1d68bce 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -83,8 +83,6 @@ public function prepareVars() */ public function getSaveValue($value) { - $value = is_array($value) ? array_values($value) : $value; - if ($this->translationMode === 'repeater') { $this->rewritePostValues(); } From d9423489921fe312d6fd37d4b74f27260284c87c Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 12:50:02 -0500 Subject: [PATCH 12/51] add docstring param & remove old code --- classes/EventRegistry.php | 1 + traits/MLControl.php | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/classes/EventRegistry.php b/classes/EventRegistry.php index 39fe1951..883f47d4 100644 --- a/classes/EventRegistry.php +++ b/classes/EventRegistry.php @@ -170,6 +170,7 @@ protected function getWidgetName($widget) * Helper function to replace standard fields with multi lingual equivalents * @param array $fields * @param Model $model + * @param string $parent * @return array */ protected function processFormMLFields($fields, $model, $parent = null) diff --git a/traits/MLControl.php b/traits/MLControl.php index 13bac256..f2acd020 100644 --- a/traits/MLControl.php +++ b/traits/MLControl.php @@ -163,9 +163,6 @@ public function getLocaleValue($locale) } elseif ($this->objectMethodExists($this->model, 'getAttributeTranslated') && $this->defaultLocale->code != $locale) { $value = $this->model->noFallbackLocale()->getAttributeTranslated($key, $locale); - if (!$value && $this->objectMethodExists($this->model, 'getJsonAttributeTranslated')) { - $value = $this->model->getJsonAttributeTranslated($this->formField->getName(), $locale); - } } else { $value = $this->formField->value; From dddeef960b7cd21a518fdbe1b67eb26f45a6ff7b Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 12:54:56 -0500 Subject: [PATCH 13/51] simplify code --- classes/EventRegistry.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/EventRegistry.php b/classes/EventRegistry.php index 883f47d4..c7066925 100644 --- a/classes/EventRegistry.php +++ b/classes/EventRegistry.php @@ -134,7 +134,7 @@ public function registerModelTranslation($widget) } if ($widget->isNested) { - if (isset($widget->config->translationMode) && $widget->config->translationMode === 'fields') { + if (@$widget->config->translationMode === 'fields') { $widget->fields = $this->processFormMLFields($widget->fields, $model, $this->getWidgetName($widget)); } return; From 462b432c8ae64582f7bc29781eff9284025f84da Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 13:28:47 -0500 Subject: [PATCH 14/51] add example of translationMode: fields in README --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index bc40639b..e2ad4a0b 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,38 @@ There are ways to get and set attributes without changing the context. // Sets a single translated attribute for a language $user->setAttributeTranslated('name', 'Jean-Claude', 'fr'); +## Translation of repeater internal fields (translationMode = "fields") + +The MLRepeater formwidget now supports translating its internal fields instead of translating the repeater field itself. You need to use the `translationMode: fields` field config to do so. + + class User + { + public $implement = ['Winter.Translate.Behaviors.TranslatableModel']; + + public $jsonable = ['data']; + + public $translatable = [ + 'data[contacts]', + 'data[contacts][title]', + ]; + } + + models/user/fields.json: +```yaml +fields: + data[contacts]: + type: repeater + translationMode: fields + form: + fields: + name: + label: Name + title: + label: Job Title + phone: + label: Phone number +``` + ## Theme data translation It is also possible to translate theme customisation options. Just mark your form fields with `translatable` property and the plugin will take care about everything else: From 495726f9aeb9d75d2fe958f0d62e8b0dfbde6131 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 13:31:58 -0500 Subject: [PATCH 15/51] improve yaml syntax --- README.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e2ad4a0b..e1d6dd96 100644 --- a/README.md +++ b/README.md @@ -241,19 +241,21 @@ There are ways to get and set attributes without changing the context. The MLRepeater formwidget now supports translating its internal fields instead of translating the repeater field itself. You need to use the `translationMode: fields` field config to do so. - class User - { - public $implement = ['Winter.Translate.Behaviors.TranslatableModel']; +```php +class User +{ + public $implement = ['Winter.Translate.Behaviors.TranslatableModel']; - public $jsonable = ['data']; + public $jsonable = ['data']; - public $translatable = [ - 'data[contacts]', - 'data[contacts][title]', - ]; - } + public $translatable = [ + 'data[contacts]', + 'data[contacts][title]', + ]; +} +``` - models/user/fields.json: +models/user/fields.json: ```yaml fields: data[contacts]: From 0c835331bc70cbe8443a27cd0f5c286a2f99d298 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 13:39:32 -0500 Subject: [PATCH 16/51] improve readme syntax --- README.md | 140 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 82 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index e1d6dd96..80d7ceaa 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ This plugin activates a feature in the CMS that allows Mail template files to us ## Extending a plugin with translatable fields If you are extending a plugin and want the added fields in the backend to be translatable, you have to use the '[backend.form.extendFieldsBefore](https://wintercms.com/docs/events/event/backend.form.extendFieldsBefore)' and tell which fields you want to be translatable by pushing them to the array. -``` +```php public function boot() { Event::listen('backend.form.extendFieldsBefore', function($widget) { @@ -189,54 +189,61 @@ public function boot() { Models can have their attributes translated by using the `Winter.Translate.Behaviors.TranslatableModel` behavior and specifying which attributes to translate in the class. - class User - { - public $implement = ['Winter.Translate.Behaviors.TranslatableModel']; +```php +class User +{ + public $implement = ['Winter.Translate.Behaviors.TranslatableModel']; - public $translatable = ['name']; - } + public $translatable = ['name']; +} +``` The attribute will then contain the default language value and other language code values can be created by using the `translateContext()` method. +```php +$user = User::first(); - $user = User::first(); - - // Outputs the name in the default language - echo $user->name; +// Outputs the name in the default language +echo $user->name; - $user->translateContext('fr'); +$user->translateContext('fr'); - // Outputs the name in French - echo $user->name; +// Outputs the name in French +echo $user->name; +``` You may use the same process for setting values. +```php +$user = User::first(); - $user = User::first(); - - // Sets the name in the default language - $user->name = 'English'; +// Sets the name in the default language +$user->name = 'English'; - $user->translateContext('fr'); +$user->translateContext('fr'); - // Sets the name in French - $user->name = 'Anglais'; +// Sets the name in French +$user->name = 'Anglais'; +``` The `lang()` method is a shorthand version of `translateContext()` and is also chainable. - - // Outputs the name in French - echo $user->lang('fr')->name; +```php +// Outputs the name in French +echo $user->lang('fr')->name; +``` This can be useful inside a Twig template. - - {{ user.lang('fr').name }} +```twig +{{ user.lang('fr').name }} +``` There are ways to get and set attributes without changing the context. +```php +// Gets a single translated attribute for a language +$user->getAttributeTranslated('name', 'fr'); - // Gets a single translated attribute for a language - $user->getAttributeTranslated('name', 'fr'); +// Sets a single translated attribute for a language +$user->setAttributeTranslated('name', 'Jean-Claude', 'fr'); +``` - // Sets a single translated attribute for a language - $user->setAttributeTranslated('name', 'Jean-Claude', 'fr'); - ## Translation of repeater internal fields (translationMode = "fields") The MLRepeater formwidget now supports translating its internal fields instead of translating the repeater field itself. You need to use the `translationMode: fields` field config to do so. @@ -275,42 +282,50 @@ fields: It is also possible to translate theme customisation options. Just mark your form fields with `translatable` property and the plugin will take care about everything else: - tabs: - fields: - website_name: - tab: Info - label: Website Name - type: text - default: Your website name - translatable: true +```yaml +tabs: + fields: + website_name: + tab: Info + label: Website Name + type: text + default: Your website name + translatable: true +``` ## Fallback attribute values By default, untranslated attributes will fall back to the default locale. This behavior can be disabled by calling the `noFallbackLocale` method. - $user = User::first(); +```php +$user = User::first(); - $user->noFallbackLocale()->lang('fr'); +$user->noFallbackLocale()->lang('fr'); - // Returns NULL if there is no French translation - $user->name; +// Returns NULL if there is no French translation +$user->name; +``` ## Indexed attributes Translatable model attributes can also be declared as an index by passing the `$transatable` attribute value as an array. The first value is the attribute name, the other values represent options, in this case setting the option `index` to `true`. - public $translatable = [ - 'name', - ['slug', 'index' => true] - ]; +```php +public $translatable = [ + 'name', + ['slug', 'index' => true] +]; +``` Once an attribute is indexed, you may use the `transWhere` method to apply a basic query to the model. - +```php Post::transWhere('slug', 'hello-world')->first(); +``` The `transWhere` method accepts a third argument to explicitly pass a locale value, otherwise it will be detected from the environment. - - Post::transWhere('slug', 'hello-world', 'en')->first(); +```php +Post::transWhere('slug', 'hello-world', 'en')->first(); +``` ## URL translation @@ -339,15 +354,16 @@ The word "Contact" in French is the same so a translated URL is not given, or ne ## URL parameter translation It's possible to translate URL parameters by listening to the `translate.localePicker.translateParams` event, which is fired when switching languages. - +```php Event::listen('translate.localePicker.translateParams', function($page, $params, $oldLocale, $newLocale) { if ($page->baseFileName == 'your-page-filename') { return YourModel::translateParams($params, $oldLocale, $newLocale); } }); +``` In YourModel, one possible implementation might look like this: - +```php public static function translateParams($params, $oldLocale, $newLocale) { $newParams = $params; foreach ($params as $paramName => $paramValue) { @@ -359,37 +375,41 @@ In YourModel, one possible implementation might look like this: } return $newParams; } +``` ## Query string translation It's possible to translate query string parameters by listening to the `translate.localePicker.translateQuery` event, which is fired when switching languages. - +```php Event::listen('translate.localePicker.translateQuery', function($page, $params, $oldLocale, $newLocale) { if ($page->baseFileName == 'your-page-filename') { return YourModel::translateParams($params, $oldLocale, $newLocale); } }); +``` For a possible implementation of the `YourModel::translateParams` method look at the example under `URL parameter translation` from above. ## Extend theme scan - +```php Event::listen('winter.translate.themeScanner.afterScan', function (ThemeScanner $scanner) { ... }); +``` ## Settings model translation It's possible to translate your settings model like any other model. To retrieve translated values use: - +```php Settings::instance()->getAttributeTranslated('your_attribute_name'); +``` ## Conditionally extending plugins #### Models It is possible to conditionally extend a plugin's models to support translation by placing an `@` symbol before the behavior definition. This is a soft implement will only use `TranslatableModel` if the Translate plugin is installed, otherwise it will not cause any errors. - +```php /** * Blog Post Model */ @@ -411,13 +431,14 @@ It is possible to conditionally extend a plugin's models to support translation [...] } +``` The back-end forms will automatically detect the presence of translatable fields and replace their controls for multilingual equivalents. #### Messages Since the Twig filter will not be available all the time, we can pipe them to the native Laravel translation methods instead. This ensures translated messages will always work on the front end. - +```php /** * Register new Twig variables * @return array @@ -435,6 +456,7 @@ Since the Twig filter will not be available all the time, we can pipe them to th ] ]; } +``` # User Interface @@ -445,7 +467,7 @@ Users can switch between locales by clicking on the locale indicator on the righ ## Integration without jQuery and Winter CMS Framework files It is possible to use the front-end language switcher without using jQuery or the Winter CMS AJAX Framework by making the AJAX API request yourself manually. The following is an example of how to do that. - +```javascript document.querySelector('#languageSelect').addEventListener('change', function () { const details = { _session_key: document.querySelector('input[name="_session_key"]').value, @@ -477,9 +499,10 @@ It is possible to use the front-end language switcher without using jQuery or th .then(res => window.location.replace(res.X_WINTER_REDIRECT)) .catch(err => console.log(err)) }) +``` The HTML: - +```twig {{ form_open() }} {{ form_close() }} +``` From 634e6eedaa57a452da8603d367384ce6c1466d9c Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 13:45:16 -0500 Subject: [PATCH 17/51] more readme syntax improvements --- README.md | 327 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 168 insertions(+), 159 deletions(-) diff --git a/README.md b/README.md index 80d7ceaa..f0128a18 100644 --- a/README.md +++ b/README.md @@ -30,104 +30,113 @@ A visitor can select a language by prefixing the language code to the URL, this ## Language Picker Component A visitor can select their chosen language using the `LocalePicker` component. This component will display a simple dropdown that changes the page language depending on the selection. +``` +title = "Home" +url = "/" - title = "Home" - url = "/" - - [localePicker] - == +[localePicker] +== -

{{ 'Please select your language:'|_ }}

- {% component 'localePicker' %} +

{{ 'Please select your language:'|_ }}

+{% component 'localePicker' %} +``` If translated, the text above will appear as whatever language is selected by the user. The dropdown is very basic and is intended to be restyled. A simpler example might be: - - [...] - == - -

- Switch language to: - English, - Russian -

+``` +[...] +== + +

+ Switch language to: + English, + Russian +

+``` ## Message translation Message or string translation is the conversion of adhoc strings used throughout the site. A message can be translated with parameters. +```twig +{{ 'site.name'|_ }} - {{ 'site.name'|_ }} - - {{ 'Welcome to our website!'|_ }} +{{ 'Welcome to our website!'|_ }} - {{ 'Hello :name!'|_({ name: 'Friend' }) }} +{{ 'Hello :name!'|_({ name: 'Friend' }) }} +``` A message can also be translated for a choice usage. - - {{ 'There are no apples|There are :number applies!'|__(2, { number: 'two' }) }} - +```twig +{{ 'There are no apples|There are :number applies!'|__(2, { number: 'two' }) }} +``` Or you set a locale manually by passing a second argument. - - {{ 'this is always english'|_({}, 'en') }} - +```twig +{{ 'this is always english'|_({}, 'en') }} +``` Themes can provide default values for these messages by defining a `translate` key in the `theme.yaml` file, located in the theme directory. +```yaml +name: My Theme +# [...] - name: My Theme - # [...] - - translate: - en: - site.name: 'My Website' - nav.home: 'Home' - nav.video: 'Video' - title.home: 'Welcome Home' - title.video: 'Screencast Video' - -You may also define the translations in a separate file, where the path is relative to the theme. The following definition will source the default messages from the file **config/lang.yaml** inside the theme. - - name: My Theme - # [...] - - translate: config/lang.yaml - -This is an example of **config/lang.yaml** file with two languages: - +translate: en: site.name: 'My Website' nav.home: 'Home' nav.video: 'Video' title.home: 'Welcome Home' - hr: - site.name: 'Moje web stranice' - nav.home: 'Početna' - nav.video: 'Video' - title.home: 'Dobrodošli' - -You may also define the translations in a separate file per locale, where the path is relative to the theme. The following definition will source the default messages from the file **config/lang-en.yaml** inside the theme for the english locale and from the file **config/lang-fr.yaml** for the french locale. - - name: My Theme - # [...] - - translate: - en: config/lang-en.yaml - fr: config/lang-fr.yaml + title.video: 'Screencast Video' +``` -This is an example for the **config/lang-en.yaml** file: +You may also define the translations in a separate file, where the path is relative to the theme. The following definition will source the default messages from the file **config/lang.yaml** inside the theme. +```yaml +name: My Theme +# [...] +translate: config/lang.yaml +``` +This is an example of **config/lang.yaml** file with two languages: +```yaml +en: site.name: 'My Website' nav.home: 'Home' nav.video: 'Video' title.home: 'Welcome Home' +hr: + site.name: 'Moje web stranice' + nav.home: 'Početna' + nav.video: 'Video' + title.home: 'Dobrodošli' +``` + +You may also define the translations in a separate file per locale, where the path is relative to the theme. The following definition will source the default messages from the file **config/lang-en.yaml** inside the theme for the english locale and from the file **config/lang-fr.yaml** for the french locale. +```yaml +name: My Theme +# [...] + +translate: + en: config/lang-en.yaml + fr: config/lang-fr.yaml +``` + +This is an example for the **config/lang-en.yaml** file: +```yaml +site.name: 'My Website' +nav.home: 'Home' +nav.video: 'Video' +title.home: 'Welcome Home' +``` In order to make these default values reflected to your frontend site, go to **Settings -> Translate messages** in the backend and hit **Scan for messages**. They will also be loaded automatically when the theme is activated. The same operation can be performed with the `translate:scan` artisan command. It may be worth including it in a deployment script to automatically fetch updated messages: +```bash +php artisan translate:scan +``` - php artisan translate:scan - Add the `--purge` option to clear old messages first: - - php artisan translate:scan --purge - +```bash +php artisan translate:scan --purge +``` + ## Content translation This plugin activates a feature in the CMS that allows content files to use language suffixes, for example: @@ -355,53 +364,53 @@ The word "Contact" in French is the same so a translated URL is not given, or ne It's possible to translate URL parameters by listening to the `translate.localePicker.translateParams` event, which is fired when switching languages. ```php - Event::listen('translate.localePicker.translateParams', function($page, $params, $oldLocale, $newLocale) { - if ($page->baseFileName == 'your-page-filename') { - return YourModel::translateParams($params, $oldLocale, $newLocale); - } - }); +Event::listen('translate.localePicker.translateParams', function($page, $params, $oldLocale, $newLocale) { + if ($page->baseFileName == 'your-page-filename') { + return YourModel::translateParams($params, $oldLocale, $newLocale); + } +}); ``` In YourModel, one possible implementation might look like this: ```php - public static function translateParams($params, $oldLocale, $newLocale) { - $newParams = $params; - foreach ($params as $paramName => $paramValue) { - $records = self::transWhere($paramName, $paramValue, $oldLocale)->first(); - if ($records) { - $records->translateContext($newLocale); - $newParams[$paramName] = $records->$paramName; - } +public static function translateParams($params, $oldLocale, $newLocale) { + $newParams = $params; + foreach ($params as $paramName => $paramValue) { + $records = self::transWhere($paramName, $paramValue, $oldLocale)->first(); + if ($records) { + $records->translateContext($newLocale); + $newParams[$paramName] = $records->$paramName; } - return $newParams; } + return $newParams; +} ``` ## Query string translation It's possible to translate query string parameters by listening to the `translate.localePicker.translateQuery` event, which is fired when switching languages. ```php - Event::listen('translate.localePicker.translateQuery', function($page, $params, $oldLocale, $newLocale) { - if ($page->baseFileName == 'your-page-filename') { - return YourModel::translateParams($params, $oldLocale, $newLocale); - } - }); +Event::listen('translate.localePicker.translateQuery', function($page, $params, $oldLocale, $newLocale) { + if ($page->baseFileName == 'your-page-filename') { + return YourModel::translateParams($params, $oldLocale, $newLocale); + } +}); ``` For a possible implementation of the `YourModel::translateParams` method look at the example under `URL parameter translation` from above. ## Extend theme scan ```php - Event::listen('winter.translate.themeScanner.afterScan', function (ThemeScanner $scanner) { - ... - }); +Event::listen('winter.translate.themeScanner.afterScan', function (ThemeScanner $scanner) { + ... +}); ``` ## Settings model translation It's possible to translate your settings model like any other model. To retrieve translated values use: ```php - Settings::instance()->getAttributeTranslated('your_attribute_name'); +Settings::instance()->getAttributeTranslated('your_attribute_name'); ``` ## Conditionally extending plugins @@ -410,27 +419,27 @@ It's possible to translate your settings model like any other model. To retrieve It is possible to conditionally extend a plugin's models to support translation by placing an `@` symbol before the behavior definition. This is a soft implement will only use `TranslatableModel` if the Translate plugin is installed, otherwise it will not cause any errors. ```php - /** - * Blog Post Model - */ - class Post extends Model - { +/** + * Blog Post Model + */ +class Post extends Model +{ - [...] + [...] - /** - * Softly implement the TranslatableModel behavior. - */ - public $implement = ['@Winter.Translate.Behaviors.TranslatableModel']; + /** + * Softly implement the TranslatableModel behavior. + */ + public $implement = ['@Winter.Translate.Behaviors.TranslatableModel']; - /** - * @var array Attributes that support translation, if available. - */ - public $translatable = ['title']; + /** + * @var array Attributes that support translation, if available. + */ + public $translatable = ['title']; - [...] + [...] - } +} ``` The back-end forms will automatically detect the presence of translatable fields and replace their controls for multilingual equivalents. @@ -439,23 +448,23 @@ The back-end forms will automatically detect the presence of translatable fields Since the Twig filter will not be available all the time, we can pipe them to the native Laravel translation methods instead. This ensures translated messages will always work on the front end. ```php - /** - * Register new Twig variables - * @return array - */ - public function registerMarkupTags() - { - // Check the translate plugin is installed - if (!class_exists('Winter\Translate\Behaviors\TranslatableModel')) - return; - - return [ - 'filters' => [ - '_' => ['Lang', 'get'], - '__' => ['Lang', 'choice'], - ] - ]; - } +/** + * Register new Twig variables + * @return array + */ +public function registerMarkupTags() +{ + // Check the translate plugin is installed + if (!class_exists('Winter\Translate\Behaviors\TranslatableModel')) + return; + + return [ + 'filters' => [ + '_' => ['Lang', 'get'], + '__' => ['Lang', 'choice'], + ] + ]; +} ``` # User Interface @@ -468,49 +477,49 @@ Users can switch between locales by clicking on the locale indicator on the righ It is possible to use the front-end language switcher without using jQuery or the Winter CMS AJAX Framework by making the AJAX API request yourself manually. The following is an example of how to do that. ```javascript - document.querySelector('#languageSelect').addEventListener('change', function () { - const details = { - _session_key: document.querySelector('input[name="_session_key"]').value, - _token: document.querySelector('input[name="_token"]').value, - locale: this.value - } +document.querySelector('#languageSelect').addEventListener('change', function () { + const details = { + _session_key: document.querySelector('input[name="_session_key"]').value, + _token: document.querySelector('input[name="_token"]').value, + locale: this.value + } - let formBody = [] + let formBody = [] - for (var property in details) { - let encodedKey = encodeURIComponent(property) - let encodedValue = encodeURIComponent(details[property]) - formBody.push(encodedKey + '=' + encodedValue) - } + for (var property in details) { + let encodedKey = encodeURIComponent(property) + let encodedValue = encodeURIComponent(details[property]) + formBody.push(encodedKey + '=' + encodedValue) + } - formBody = formBody.join('&') - - fetch(location.href + '/', { - method: 'POST', - body: formBody, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - 'X-WINTER-REQUEST-HANDLER': 'onSwitchLocale', - 'X-WINTER-REQUEST-PARTIALS': '', - 'X-Requested-With': 'XMLHttpRequest' - } - }) - .then(res => res.json()) - .then(res => window.location.replace(res.X_WINTER_REDIRECT)) - .catch(err => console.log(err)) + formBody = formBody.join('&') + + fetch(location.href + '/', { + method: 'POST', + body: formBody, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-WINTER-REQUEST-HANDLER': 'onSwitchLocale', + 'X-WINTER-REQUEST-PARTIALS': '', + 'X-Requested-With': 'XMLHttpRequest' + } }) + .then(res => res.json()) + .then(res => window.location.replace(res.X_WINTER_REDIRECT)) + .catch(err => console.log(err)) +}) ``` The HTML: ```twig - {{ form_open() }} - - {{ form_close() }} +{{ form_open() }} + +{{ form_close() }} ``` From 579a9eb269f8fc957c2f70d0d88ff43da0d7dccc Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 13:52:32 -0500 Subject: [PATCH 18/51] improve translationMode section in readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f0128a18..392393ac 100644 --- a/README.md +++ b/README.md @@ -253,9 +253,9 @@ $user->getAttributeTranslated('name', 'fr'); $user->setAttributeTranslated('name', 'Jean-Claude', 'fr'); ``` -## Translation of repeater internal fields (translationMode = "fields") +## Repeater formwidget internal fields translation -The MLRepeater formwidget now supports translating its internal fields instead of translating the repeater field itself. You need to use the `translationMode: fields` field config to do so. +It is now possible to independently translate the fields defined within a repeater formwidget by setting its `translationMode` config to `fields` (see example below) ```php class User From e6a8df87c4796327001a9792972377379713b14e Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 14:55:52 -0500 Subject: [PATCH 19/51] add repeater translatable fields from themedata; fix issue --- Plugin.php | 7 +++++++ classes/EventRegistry.php | 9 +++++++-- formwidgets/MLRepeater.php | 9 ++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Plugin.php b/Plugin.php index d761fe4d..9ab1fd17 100644 --- a/Plugin.php +++ b/Plugin.php @@ -218,6 +218,13 @@ protected function extendCmsModule(): void if (!empty($field['translatable'])) { $translatable[] = $id; } + if (@$field['type'] === 'repeater') { + foreach (array_get($field, 'form.fields') as $name => $config) { + if (array_get($config, 'translatable')) { + $translatable[] = sprintf("%s[%s]", $id, $name); + } + } + } } $this->extendModel($model, 'model', $translatable); }); diff --git a/classes/EventRegistry.php b/classes/EventRegistry.php index c7066925..7ff190a1 100644 --- a/classes/EventRegistry.php +++ b/classes/EventRegistry.php @@ -157,11 +157,16 @@ protected function getWidgetName($widget) { $nameArray = HtmlHelper::nameToArray($widget->arrayName); - array_shift($nameArray); // remove parenta model + array_shift($nameArray); // remove parent model array_pop($nameArray); // remove repeater index $parentName = array_shift($nameArray); - $parentName .= '[' . implode('][', $nameArray) . ']'; + + if ($widget->model instanceof \Cms\Models\ThemeData) { + return $parentName; + } else { + $parentName .= '[' . implode('][', $nameArray) . ']'; + } return $parentName; } diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index f1d68bce..4d2eaf0f 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -83,10 +83,17 @@ public function prepareVars() */ public function getSaveValue($value) { + $results = $value; + if ($this->translationMode === 'repeater') { $this->rewritePostValues(); + $results = $this->getLocaleSaveValue(is_array($value) ? array_values($value) : $value); + } elseif ($this->translationMode === 'fields') { + $localeValues = $this->getLocaleSaveValue(is_array($value) ? array_values($value) : $value); + $results = array_merge($value, $localeValues); } - return $this->getLocaleSaveValue(is_array($value) ? array_values($value) : $value); + + return $results; } /** From edbe84993f3dda1c3a3e0a91a2c240b4b75c1c51 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 15:09:59 -0500 Subject: [PATCH 20/51] properly merge values --- formwidgets/MLRepeater.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index 4d2eaf0f..aa7dbcdc 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -83,17 +83,19 @@ public function prepareVars() */ public function getSaveValue($value) { - $results = $value; + $value = is_array($value) ? array_values($value) : $value; if ($this->translationMode === 'repeater') { $this->rewritePostValues(); - $results = $this->getLocaleSaveValue(is_array($value) ? array_values($value) : $value); + $value = $this->getLocaleSaveValue($value); } elseif ($this->translationMode === 'fields') { - $localeValues = $this->getLocaleSaveValue(is_array($value) ? array_values($value) : $value); - $results = array_merge($value, $localeValues); + $localeValues = $this->getLocaleSaveValue($value); + foreach ($value as $index => &$_data) { + $_data = array_merge($_data, $localeValues[$index]); + } } - return $results; + return $value; } /** From 36353b0912973c676178c5060f99f6ae6a981e3c Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 15:12:38 -0500 Subject: [PATCH 21/51] value might be null --- formwidgets/MLRepeater.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index aa7dbcdc..cedee0f9 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -90,8 +90,12 @@ public function getSaveValue($value) $value = $this->getLocaleSaveValue($value); } elseif ($this->translationMode === 'fields') { $localeValues = $this->getLocaleSaveValue($value); - foreach ($value as $index => &$_data) { - $_data = array_merge($_data, $localeValues[$index]); + if ($value) { + foreach ($value as $index => &$_data) { + $_data = array_merge($_data, $localeValues[$index]); + } + } else { + $value = $localeValues; } } From 0bed2fc59ef4e528ce0987e1d81f7cec4971ff1d Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 15:25:04 -0500 Subject: [PATCH 22/51] improve code readability --- Plugin.php | 14 +++++++------- formwidgets/MLRepeater.php | 3 ++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Plugin.php b/Plugin.php index 9ab1fd17..1a86f94a 100644 --- a/Plugin.php +++ b/Plugin.php @@ -214,14 +214,14 @@ protected function extendCmsModule(): void ThemeData::extend(function ($model) { $model->bindEvent('model.afterFetch', function() use ($model) { $translatable = []; - foreach ($model->getFormFields() as $id => $field) { - if (!empty($field['translatable'])) { - $translatable[] = $id; + foreach ($model->getFormFields() as $fieldName => $fieldConfig) { + if (array_get($fieldConfig, 'translatable', false)) { + $translatable[] = $fieldName; } - if (@$field['type'] === 'repeater') { - foreach (array_get($field, 'form.fields') as $name => $config) { - if (array_get($config, 'translatable')) { - $translatable[] = sprintf("%s[%s]", $id, $name); + if (array_get($fieldConfig, 'type', 'text') === 'repeater') { + foreach (array_get($fieldConfig, 'form.fields', []) as $repeaterFieldName => $repeaterFieldConfig) { + if (array_get($repeaterFieldConfig, 'translatable', false)) { + $translatable[] = sprintf("%s[%s]", $fieldName, $repeaterFieldName); } } } diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index cedee0f9..ff610962 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -88,7 +88,8 @@ public function getSaveValue($value) if ($this->translationMode === 'repeater') { $this->rewritePostValues(); $value = $this->getLocaleSaveValue($value); - } elseif ($this->translationMode === 'fields') { + } + elseif ($this->translationMode === 'fields') { $localeValues = $this->getLocaleSaveValue($value); if ($value) { foreach ($value as $index => &$_data) { From 533285a5b6a360b5f69928a0fe176278ea66f5c7 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 22 Feb 2024 15:52:11 -0500 Subject: [PATCH 23/51] no need to hardcode model name here --- classes/EventRegistry.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/classes/EventRegistry.php b/classes/EventRegistry.php index 7ff190a1..699a7584 100644 --- a/classes/EventRegistry.php +++ b/classes/EventRegistry.php @@ -162,9 +162,7 @@ protected function getWidgetName($widget) $parentName = array_shift($nameArray); - if ($widget->model instanceof \Cms\Models\ThemeData) { - return $parentName; - } else { + if ($nameArray) { $parentName .= '[' . implode('][', $nameArray) . ']'; } From 1813c85486853ab60e8dce84ecddbdabc04c74ec Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Fri, 23 Feb 2024 09:14:46 -0500 Subject: [PATCH 24/51] fix for groups mode --- formwidgets/MLRepeater.php | 13 ++++++++++++- traits/MLControl.php | 5 ++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index ff610962..58364c2a 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -36,7 +36,9 @@ public function init() { $this->fillFromConfig(['translationMode']); // make the translationMode available to the repeater items formwidgets - $this->config->form['translationMode'] = $this->translationMode; + if (isset($this->config->form)) { + $this->config->form['translationMode'] = $this->translationMode; + } parent::init(); $this->initLocale(); @@ -77,6 +79,15 @@ public function prepareVars() $this->prepareLocaleVars(); } + // make the translationMode available to the repeater groups formwidgets + protected function getGroupFormFieldConfig($code) + { + $config = parent::getGroupFormFieldConfig($code); + $config['translationMode'] = $this->translationMode; + + return $config; + } + /** * Returns an array of translated values for this field * @return array diff --git a/traits/MLControl.php b/traits/MLControl.php index f2acd020..50bc2a26 100644 --- a/traits/MLControl.php +++ b/traits/MLControl.php @@ -260,7 +260,10 @@ public function isFieldParentJsonable() // $names[0] is the Model, $names[1] is the top array name $arrayName = $names[1]; - if (method_exists($this->model, 'isJsonable') && $this->model->isJsonable($arrayName)) { + if ($this->model->isClassExtendedWith('System\Behaviors\SettingsModel') || + method_exists($this->model, 'isJsonable') && $this->model->isJsonable($arrayName) + ) + { return true; } } From 681cafa492b8e4ddc6d6e4d3dbac9e1e06da2976 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Fri, 23 Feb 2024 12:59:44 -0500 Subject: [PATCH 25/51] account for nestedform field types in repeater --- classes/EventRegistry.php | 13 +++++++---- formwidgets/MLNestedForm.php | 43 +++++++++++++++++++++++++++++++----- formwidgets/MLRepeater.php | 25 +++++++++------------ 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/classes/EventRegistry.php b/classes/EventRegistry.php index 699a7584..ef7f2d47 100644 --- a/classes/EventRegistry.php +++ b/classes/EventRegistry.php @@ -156,10 +156,13 @@ public function registerModelTranslation($widget) protected function getWidgetName($widget) { $nameArray = HtmlHelper::nameToArray($widget->arrayName); + foreach ($nameArray as $index => $name) { + if (is_numeric($name)) { + unset($nameArray[$index]); + } + } array_shift($nameArray); // remove parent model - array_pop($nameArray); // remove repeater index - $parentName = array_shift($nameArray); if ($nameArray) { @@ -198,12 +201,14 @@ protected function processFormMLFields($fields, $model, $parent = null) } foreach ($fields as $name => $config) { - $fieldName = $parent ? sprintf("%s[%s]", $parent, $name) : $name; - + $fieldName = $name; if (str_contains($name, '@')) { // apply to fields with any context list($fieldName, $context) = explode('@', $name); } + + $fieldName = $parent ? sprintf("%s[%s]", $parent, $fieldName) : $fieldName; + if (!array_key_exists($fieldName, $translatable)) { continue; } diff --git a/formwidgets/MLNestedForm.php b/formwidgets/MLNestedForm.php index 3ad2e2a8..3bb72fc5 100644 --- a/formwidgets/MLNestedForm.php +++ b/formwidgets/MLNestedForm.php @@ -21,13 +21,37 @@ class MLNestedForm extends NestedForm */ protected $defaultAlias = 'mlnestedform'; + /** + * The repeater translation mode (default|fields) + */ + protected $translationMode = 'default'; + /** * {@inheritDoc} */ public function init() { + $this->fillFromConfig(['translationMode']); + + // make the translationMode available to the nestedform formwidgets + if (isset($this->config->form)) { + $this->config->form['translationMode'] = $this->translationMode; + } + parent::init(); $this->initLocale(); + + if ($this->translationMode === 'fields' && $this->model) { + $this->model->extend(function () { + $this->addDynamicMethod('getJsonAttributeTranslated', function ($key, $locale) { + $names = HtmlHelper::nameToArray($key); + array_shift($names); // remove model + if ($arrayName = array_shift($names)) { + return array_get($this->lang($locale)->{$arrayName}, implode('.', $names)); + } + }); + }); + } } /** @@ -39,7 +63,7 @@ public function render() $parentContent = parent::render(); $this->actAsParent(false); - if (!$this->isAvailable) { + if ($this->translationMode === 'fields' || !$this->isAvailable) { return $parentContent; } @@ -50,7 +74,9 @@ public function render() public function prepareVars() { parent::prepareVars(); - $this->prepareLocaleVars(); + if ($this->translationMode === 'default') { + $this->prepareLocaleVars(); + } } /** @@ -59,8 +85,15 @@ public function prepareVars() */ public function getSaveValue($value) { - $this->rewritePostValues(); - return $this->getLocaleSaveValue($value); + if ($this->translationMode === 'default') { + $this->rewritePostValues(); + $value = $this->getLocaleSaveValue($value); + } + elseif ($this->translationMode === 'fields') { + $localeValue = $this->getLocaleSaveValue($value); + $value = array_replace_recursive($value, $localeValue); + } + return $value; } /** @@ -72,7 +105,7 @@ protected function loadAssets() parent::loadAssets(); $this->actAsParent(false); - if (Locale::isAvailable()) { + if (Locale::isAvailable() && $this->translationMode === 'default') { $this->loadLocaleAssets(); $this->addJs('js/mlnestedform.js'); } diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index 58364c2a..1dcf5c30 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -25,9 +25,9 @@ class MLRepeater extends Repeater protected $defaultAlias = 'mlrepeater'; /** - * The repeater translation mode (repeater|fields) + * The repeater translation mode (default|fields) */ - protected $translationMode = 'repeater'; + protected $translationMode = 'default'; /** * {@inheritDoc} @@ -65,7 +65,7 @@ public function render() $parentContent = parent::render(); $this->actAsParent(false); - if (!$this->isAvailable || $this->translationMode === 'fields') { + if ($this->translationMode === 'fields' || !$this->isAvailable) { return $parentContent; } @@ -76,7 +76,9 @@ public function render() public function prepareVars() { parent::prepareVars(); - $this->prepareLocaleVars(); + if ($this->translationMode === 'default') { + $this->prepareLocaleVars(); + } } // make the translationMode available to the repeater groups formwidgets @@ -96,21 +98,14 @@ public function getSaveValue($value) { $value = is_array($value) ? array_values($value) : $value; - if ($this->translationMode === 'repeater') { + if ($this->translationMode === 'default') { $this->rewritePostValues(); $value = $this->getLocaleSaveValue($value); } elseif ($this->translationMode === 'fields') { - $localeValues = $this->getLocaleSaveValue($value); - if ($value) { - foreach ($value as $index => &$_data) { - $_data = array_merge($_data, $localeValues[$index]); - } - } else { - $value = $localeValues; - } + $localeValue = $this->getLocaleSaveValue($value); + $value = array_replace_recursive($value, $localeValue); } - return $value; } @@ -123,7 +118,7 @@ protected function loadAssets() parent::loadAssets(); $this->actAsParent(false); - if (Locale::isAvailable() && $this->translationMode === 'repeater') { + if (Locale::isAvailable() && $this->translationMode === 'default') { $this->loadLocaleAssets(); $this->addJs('js/mlrepeater.js'); } From e9e9c03d64e15ef971ea883ea72bc2f465271955 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Sat, 24 Feb 2024 08:49:07 -0500 Subject: [PATCH 26/51] make sure arrays are provided to array_replace_recursive() --- classes/EventRegistry.php | 4 ++-- formwidgets/MLNestedForm.php | 2 +- formwidgets/MLRepeater.php | 2 +- traits/mlcontrol/partials/_locale_values.htm | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/classes/EventRegistry.php b/classes/EventRegistry.php index ef7f2d47..55d0cfb8 100644 --- a/classes/EventRegistry.php +++ b/classes/EventRegistry.php @@ -135,7 +135,7 @@ public function registerModelTranslation($widget) if ($widget->isNested) { if (@$widget->config->translationMode === 'fields') { - $widget->fields = $this->processFormMLFields($widget->fields, $model, $this->getWidgetName($widget)); + $widget->fields = $this->processFormMLFields($widget->fields, $model, $this->getWidgetLongName($widget)); } return; } @@ -153,7 +153,7 @@ public function registerModelTranslation($widget) } } - protected function getWidgetName($widget) + protected function getWidgetLongName($widget) { $nameArray = HtmlHelper::nameToArray($widget->arrayName); foreach ($nameArray as $index => $name) { diff --git a/formwidgets/MLNestedForm.php b/formwidgets/MLNestedForm.php index 3bb72fc5..2239c13d 100644 --- a/formwidgets/MLNestedForm.php +++ b/formwidgets/MLNestedForm.php @@ -91,7 +91,7 @@ public function getSaveValue($value) } elseif ($this->translationMode === 'fields') { $localeValue = $this->getLocaleSaveValue($value); - $value = array_replace_recursive($value, $localeValue); + $value = array_replace_recursive($value ?? [], $localeValue ?? []); } return $value; } diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index 1dcf5c30..50471380 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -104,7 +104,7 @@ public function getSaveValue($value) } elseif ($this->translationMode === 'fields') { $localeValue = $this->getLocaleSaveValue($value); - $value = array_replace_recursive($value, $localeValue); + $value = array_replace_recursive($value ?? [], $localeValue ?? []); } return $value; } diff --git a/traits/mlcontrol/partials/_locale_values.htm b/traits/mlcontrol/partials/_locale_values.htm index d35accbd..642ee8be 100644 --- a/traits/mlcontrol/partials/_locale_values.htm +++ b/traits/mlcontrol/partials/_locale_values.htm @@ -2,7 +2,7 @@ $name): ?> getLocaleValue($code); - $value = $this->isLocaleFieldJsonable() ? json_encode($value) : $value; + $value = $this->isLocaleFieldJsonable() && is_array($value) ? json_encode($value) : $value; ?> Date: Sat, 24 Feb 2024 10:48:27 -0500 Subject: [PATCH 27/51] fix repeater in regular translation mode --- formwidgets/MLNestedForm.php | 9 ++++----- formwidgets/MLRepeater.php | 9 ++++----- traits/MLControl.php | 31 +++++++++++++++++++++++++++---- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/formwidgets/MLNestedForm.php b/formwidgets/MLNestedForm.php index 2239c13d..8a205157 100644 --- a/formwidgets/MLNestedForm.php +++ b/formwidgets/MLNestedForm.php @@ -85,13 +85,12 @@ public function prepareVars() */ public function getSaveValue($value) { - if ($this->translationMode === 'default') { - $this->rewritePostValues(); - $value = $this->getLocaleSaveValue($value); - } - elseif ($this->translationMode === 'fields') { + if ($this->translationMode === 'fields') { $localeValue = $this->getLocaleSaveValue($value); $value = array_replace_recursive($value ?? [], $localeValue ?? []); + } else { + $this->rewritePostValues(); + $value = $this->getLocaleSaveValue($value); } return $value; } diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index 50471380..5543a9fc 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -98,13 +98,12 @@ public function getSaveValue($value) { $value = is_array($value) ? array_values($value) : $value; - if ($this->translationMode === 'default') { - $this->rewritePostValues(); - $value = $this->getLocaleSaveValue($value); - } - elseif ($this->translationMode === 'fields') { + if ($this->translationMode === 'fields') { $localeValue = $this->getLocaleSaveValue($value); $value = array_replace_recursive($value ?? [], $localeValue ?? []); + } else { + $this->rewritePostValues(); + $value = $this->getLocaleSaveValue($value); } return $value; } diff --git a/traits/MLControl.php b/traits/MLControl.php index 50bc2a26..da23241c 100644 --- a/traits/MLControl.php +++ b/traits/MLControl.php @@ -188,8 +188,14 @@ protected function makeRenderFormField() public function getLocaleFieldName($code) { - $names = HtmlHelper::nameToArray($this->formField->arrayName); - return $this->formField->getName('RLTranslate[' . $code . '][' . implode('][', $names) . ']'); + if ($this->isLongFormNeeded()) { + $names = HtmlHelper::nameToArray($this->formField->arrayName); + $name = $this->formField->getName('RLTranslate[' . $code . '][' . implode('][', $names) . ']'); + } else { + $name = $this->formField->getName('RLTranslate['.$code.']'); + } + + return $name; } /** @@ -233,12 +239,16 @@ public function getLocaleSaveData() return $values; } - $fieldName = implode('.', HtmlHelper::nameToArray($this->formField->getName())); + if ($this->isLongFormNeeded()) { + $fieldName = implode('.', HtmlHelper::nameToArray($this->formField->getName())); + } else { + $fieldName = implode('.', HtmlHelper::nameToArray($this->fieldName)); + } $isJson = $this->isLocaleFieldJsonable(); foreach ($data as $locale => $_data) { $value = array_get($_data, $fieldName); - $values[$locale] = ($isJson && is_string($value)) ? json_decode($value, true) : $value; + $values[$locale] = $isJson && is_string($value) ? json_decode($value, true) : $value; } return $values; @@ -309,4 +319,17 @@ protected function objectMethodExists($object, $method) return method_exists($object, $method); } + + /** + * determine if fieldName needs long form + * + * @return boolean + */ + protected function isLongFormNeeded() + { + $type = array_get($this->formField->config, 'type'); + $mode = array_get($this->formField->config, 'translationMode', 'default'); + + return (!in_array($type, ['mlrepeater','mlnestedform']) || $mode === "fields"); + } } From d6ec62124a0d0fd30da5a70b20493762bbb86da1 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Sat, 24 Feb 2024 11:20:09 -0500 Subject: [PATCH 28/51] also apply to nestedform --- Plugin.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Plugin.php b/Plugin.php index 1a86f94a..ef1f68f0 100644 --- a/Plugin.php +++ b/Plugin.php @@ -218,10 +218,11 @@ protected function extendCmsModule(): void if (array_get($fieldConfig, 'translatable', false)) { $translatable[] = $fieldName; } - if (array_get($fieldConfig, 'type', 'text') === 'repeater') { - foreach (array_get($fieldConfig, 'form.fields', []) as $repeaterFieldName => $repeaterFieldConfig) { - if (array_get($repeaterFieldConfig, 'translatable', false)) { - $translatable[] = sprintf("%s[%s]", $fieldName, $repeaterFieldName); + $type = array_get($fieldConfig, 'type', 'text'); + if (in_array($type, ['repeater','nestedform'])) { + foreach (array_get($fieldConfig, 'form.fields', []) as $subFieldName => $subFieldConfig) { + if (array_get($subFieldConfig, 'translatable', false)) { + $translatable[] = sprintf("%s[%s]", $fieldName, $subFieldName); } } } From a7859ed70a56886090cf469219ba4605b8e1649c Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Sat, 24 Feb 2024 11:22:42 -0500 Subject: [PATCH 29/51] fix widget name --- formwidgets/MLNestedForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/formwidgets/MLNestedForm.php b/formwidgets/MLNestedForm.php index 8a205157..0c96eb8d 100644 --- a/formwidgets/MLNestedForm.php +++ b/formwidgets/MLNestedForm.php @@ -22,7 +22,7 @@ class MLNestedForm extends NestedForm protected $defaultAlias = 'mlnestedform'; /** - * The repeater translation mode (default|fields) + * The nestedform translation mode (default|fields) */ protected $translationMode = 'default'; From e2fb60b00176bf46211b90eb456423c9d0126605 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Sun, 25 Feb 2024 09:33:08 -0500 Subject: [PATCH 30/51] add nestedform to README example --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 392393ac..e07ce8dc 100644 --- a/README.md +++ b/README.md @@ -253,9 +253,9 @@ $user->getAttributeTranslated('name', 'fr'); $user->setAttributeTranslated('name', 'Jean-Claude', 'fr'); ``` -## Repeater formwidget internal fields translation +## Repeater/NestedForm formwidget internal fields translation -It is now possible to independently translate the fields defined within a repeater formwidget by setting its `translationMode` config to `fields` (see example below) +It is now possible to independently translate the fields defined within a repeater/nestedform formwidget by setting its `translationMode` config to `fields` (see example below) ```php class User @@ -275,7 +275,7 @@ models/user/fields.json: ```yaml fields: data[contacts]: - type: repeater + type: repeater (or nestedform) translationMode: fields form: fields: From 6047f8c3c1c3c8ab4ebf162965e9578f0b5b601b Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Sun, 25 Feb 2024 14:38:52 -0500 Subject: [PATCH 31/51] add top-level array to translatable --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e07ce8dc..4fe06a70 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,7 @@ class User public $jsonable = ['data']; public $translatable = [ + 'data', 'data[contacts]', 'data[contacts][title]', ]; From f98357e114103e2c04620baab98ca233ce7c99e6 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Mon, 26 Feb 2024 00:28:11 -0600 Subject: [PATCH 32/51] Update Plugin.php --- Plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugin.php b/Plugin.php index ef1f68f0..9973d30c 100644 --- a/Plugin.php +++ b/Plugin.php @@ -219,7 +219,7 @@ protected function extendCmsModule(): void $translatable[] = $fieldName; } $type = array_get($fieldConfig, 'type', 'text'); - if (in_array($type, ['repeater','nestedform'])) { + if (in_array($type, ['repeater', 'nestedform'])) { foreach (array_get($fieldConfig, 'form.fields', []) as $subFieldName => $subFieldConfig) { if (array_get($subFieldConfig, 'translatable', false)) { $translatable[] = sprintf("%s[%s]", $fieldName, $subFieldName); From c4d55bcb3cbc475694b533ff339e9f628433671f Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Mon, 26 Feb 2024 22:56:58 -0500 Subject: [PATCH 33/51] alternative to using @ Co-authored-by: Luke Towers --- classes/EventRegistry.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/EventRegistry.php b/classes/EventRegistry.php index 55d0cfb8..00ebbd9b 100644 --- a/classes/EventRegistry.php +++ b/classes/EventRegistry.php @@ -134,7 +134,7 @@ public function registerModelTranslation($widget) } if ($widget->isNested) { - if (@$widget->config->translationMode === 'fields') { + if (($widget->config->translationMode ?? 'default') === 'fields') { $widget->fields = $this->processFormMLFields($widget->fields, $model, $this->getWidgetLongName($widget)); } return; From aacb3d09b54d81c574edfaa7efdf6323a86edf66 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 27 Feb 2024 00:05:40 -0500 Subject: [PATCH 34/51] fix reordering issues --- formwidgets/MLRepeater.php | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index 5543a9fc..51358c09 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -108,6 +108,37 @@ public function getSaveValue($value) return $value; } + /** + * Returns an array of translated values for this field + * @return array + */ + public function getLocaleSaveData() + { + $values = []; + $data = post('RLTranslate'); + + if (!is_array($data)) { + return $values; + } + + if ($this->isLongFormNeeded()) { + $fieldName = implode('.', HtmlHelper::nameToArray($this->formField->getName())); + } else { + $fieldName = implode('.', HtmlHelper::nameToArray($this->fieldName)); + } + + foreach ($data as $locale => $_data) { + $i = 0; + $content = array_get($_data, $fieldName); + foreach ($content as $index => $value) { + // we reindex to fix item reordering index issues + $values[$locale][$i++] = $value; + } + } + + return $values; + } + /** * {@inheritDoc} */ From 700c28cf4a87207ea6614bb48fa11105f863e54d Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 27 Feb 2024 10:21:04 -0500 Subject: [PATCH 35/51] do not call with empty fields --- classes/EventRegistry.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/EventRegistry.php b/classes/EventRegistry.php index 00ebbd9b..6b062b60 100644 --- a/classes/EventRegistry.php +++ b/classes/EventRegistry.php @@ -133,7 +133,7 @@ public function registerModelTranslation($widget) return; } - if ($widget->isNested) { + if ($widget->isNested && !empty($widget->fields)) { if (($widget->config->translationMode ?? 'default') === 'fields') { $widget->fields = $this->processFormMLFields($widget->fields, $model, $this->getWidgetLongName($widget)); } From 6aef4982477b01427cfe9d9bc0fd0eef73a007ae Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 29 Feb 2024 15:09:43 -0500 Subject: [PATCH 36/51] make sure we have an array --- formwidgets/MLRepeater.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index 51358c09..9748f253 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -127,12 +127,18 @@ public function getLocaleSaveData() $fieldName = implode('.', HtmlHelper::nameToArray($this->fieldName)); } + $isJson = $this->isLocaleFieldJsonable(); + foreach ($data as $locale => $_data) { $i = 0; $content = array_get($_data, $fieldName); - foreach ($content as $index => $value) { - // we reindex to fix item reordering index issues - $values[$locale][$i++] = $value; + if (is_array($content)) { + foreach ($content as $index => $value) { + // we reindex to fix item reordering index issues + $values[$locale][$i++] = $value; + } + } else { + $values[$locale] = $isJson && is_string($content) ? json_decode($content, true) : $content; } } From c074a604613eff9f8470481c87f4bee2fcb6c994 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 29 Feb 2024 15:11:47 -0500 Subject: [PATCH 37/51] prefix plugin name to dynamic method --- formwidgets/MLNestedForm.php | 2 +- formwidgets/MLRepeater.php | 2 +- traits/MLControl.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/formwidgets/MLNestedForm.php b/formwidgets/MLNestedForm.php index 0c96eb8d..d0e02d6e 100644 --- a/formwidgets/MLNestedForm.php +++ b/formwidgets/MLNestedForm.php @@ -43,7 +43,7 @@ public function init() if ($this->translationMode === 'fields' && $this->model) { $this->model->extend(function () { - $this->addDynamicMethod('getJsonAttributeTranslated', function ($key, $locale) { + $this->addDynamicMethod('WinterTranslateGetJsonAttributeTranslated', function ($key, $locale) { $names = HtmlHelper::nameToArray($key); array_shift($names); // remove model if ($arrayName = array_shift($names)) { diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index 9748f253..72d9335a 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -45,7 +45,7 @@ public function init() if ($this->translationMode === 'fields' && $this->model) { $this->model->extend(function () { - $this->addDynamicMethod('getJsonAttributeTranslated', function ($key, $locale) { + $this->addDynamicMethod('WinterTranslateGetJsonAttributeTranslated', function ($key, $locale) { $names = HtmlHelper::nameToArray($key); array_shift($names); // remove model if ($arrayName = array_shift($names)) { diff --git a/traits/MLControl.php b/traits/MLControl.php index da23241c..ac66e135 100644 --- a/traits/MLControl.php +++ b/traits/MLControl.php @@ -156,7 +156,7 @@ public function getLocaleValue($locale) $value = $this->model->$mutateMethod($locale); } elseif ($this->defaultLocale->code != $locale && $this->isFieldParentJsonable() && - $this->objectMethodExists($this->model, 'getJsonAttributeTranslated') + $this->objectMethodExists($this->model, 'WinterTranslateGetJsonAttributeTranslated') ) { $value = $this->model->getJsonAttributeTranslated($this->formField->getName(), $locale); From 07c0a06e6d53585dc1684bd6fe0de14a95aa02fe Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 5 Mar 2024 13:36:54 -0500 Subject: [PATCH 38/51] rename last instance --- traits/MLControl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/traits/MLControl.php b/traits/MLControl.php index ac66e135..7242354f 100644 --- a/traits/MLControl.php +++ b/traits/MLControl.php @@ -159,7 +159,7 @@ public function getLocaleValue($locale) $this->objectMethodExists($this->model, 'WinterTranslateGetJsonAttributeTranslated') ) { - $value = $this->model->getJsonAttributeTranslated($this->formField->getName(), $locale); + $value = $this->model->WinterTranslateGetJsonAttributeTranslated($this->formField->getName(), $locale); } elseif ($this->objectMethodExists($this->model, 'getAttributeTranslated') && $this->defaultLocale->code != $locale) { $value = $this->model->noFallbackLocale()->getAttributeTranslated($key, $locale); From 51bcdaf14b5f4688ec938cfee0570874c1b7c740 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 5 Mar 2024 15:08:12 -0500 Subject: [PATCH 39/51] purge translations for deleted repeaters --- formwidgets/MLRepeater.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index 72d9335a..08b3686a 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -118,6 +118,12 @@ public function getLocaleSaveData() $data = post('RLTranslate'); if (!is_array($data)) { + if ($this->translationMode === 'fields') { + foreach (Locale::listEnabled() as $code => $name) { + // force translations removal from db + $values[$code] = []; + } + } return $values; } From 7bb6a863b2d2d64437e546e71eeedc70da47b452 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 19 Mar 2024 06:11:45 -0400 Subject: [PATCH 40/51] remove code duplication --- formwidgets/MLRepeater.php | 7 +------ traits/MLControl.php | 16 +++++++++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index 08b3686a..b41e12c9 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -127,12 +127,7 @@ public function getLocaleSaveData() return $values; } - if ($this->isLongFormNeeded()) { - $fieldName = implode('.', HtmlHelper::nameToArray($this->formField->getName())); - } else { - $fieldName = implode('.', HtmlHelper::nameToArray($this->fieldName)); - } - + $fieldName = $this->getFieldName(); $isJson = $this->isLocaleFieldJsonable(); foreach ($data as $locale => $_data) { diff --git a/traits/MLControl.php b/traits/MLControl.php index 25fcb861..29204940 100644 --- a/traits/MLControl.php +++ b/traits/MLControl.php @@ -239,11 +239,7 @@ public function getLocaleSaveData() return $values; } - if ($this->isLongFormNeeded()) { - $fieldName = implode('.', HtmlHelper::nameToArray($this->formField->getName())); - } else { - $fieldName = implode('.', HtmlHelper::nameToArray($this->fieldName)); - } + $fieldName = $this->getFieldName(); $isJson = $this->isLocaleFieldJsonable(); foreach ($data as $locale => $_data) { @@ -332,4 +328,14 @@ protected function isLongFormNeeded() return (!in_array($type, ['mlrepeater','mlnestedform']) || $mode === "fields"); } + + protected function getFieldName() + { + if ($this->isLongFormNeeded()) { + $fieldName = implode('.', HtmlHelper::nameToArray($this->formField->getName())); + } else { + $fieldName = implode('.', HtmlHelper::nameToArray($this->fieldName)); + } + return $fieldName; + } } From d617cbc18a1f0a7d67ef4ae7a6dea5cfbe576f9c Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 19 Mar 2024 06:18:01 -0400 Subject: [PATCH 41/51] fix method visibility and add doc string --- traits/MLControl.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/traits/MLControl.php b/traits/MLControl.php index 29204940..fc14a86f 100644 --- a/traits/MLControl.php +++ b/traits/MLControl.php @@ -321,7 +321,7 @@ protected function objectMethodExists($object, $method) * * @return boolean */ - protected function isLongFormNeeded() + public function isLongFormNeeded() { $type = array_get($this->formField->config, 'type'); $mode = array_get($this->formField->config, 'translationMode', 'default'); @@ -329,7 +329,12 @@ protected function isLongFormNeeded() return (!in_array($type, ['mlrepeater','mlnestedform']) || $mode === "fields"); } - protected function getFieldName() + /** + * get the proper field name + * + * @return string + */ + public function getFieldName() { if ($this->isLongFormNeeded()) { $fieldName = implode('.', HtmlHelper::nameToArray($this->formField->getName())); From e23bb1dbe83fa52c4984fd94b7c34c5279d150ed Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Wed, 27 Mar 2024 08:48:19 -0400 Subject: [PATCH 42/51] fix extension --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 646eafa7..33ecf816 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,7 @@ class User } ``` -models/user/fields.json: +models/user/fields.yaml: ```yaml fields: data[contacts]: From cc3aeef10054ac3b2ea072cd32da47706cc123d0 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Wed, 27 Mar 2024 09:16:26 -0400 Subject: [PATCH 43/51] add option to specify translatable fields in fields.yaml --- classes/EventRegistry.php | 13 +++++++------ classes/TranslatableBehavior.php | 4 ++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/classes/EventRegistry.php b/classes/EventRegistry.php index 6b062b60..f2edb8a2 100644 --- a/classes/EventRegistry.php +++ b/classes/EventRegistry.php @@ -128,11 +128,6 @@ public function registerModelTranslation($widget) return; } - - if (!$model->hasTranslatableAttributes()) { - return; - } - if ($widget->isNested && !empty($widget->fields)) { if (($widget->config->translationMode ?? 'default') === 'fields') { $widget->fields = $this->processFormMLFields($widget->fields, $model, $this->getWidgetLongName($widget)); @@ -202,6 +197,8 @@ protected function processFormMLFields($fields, $model, $parent = null) foreach ($fields as $name => $config) { $fieldName = $name; + $fieldTranslatable = false; + if (str_contains($name, '@')) { // apply to fields with any context list($fieldName, $context) = explode('@', $name); @@ -209,7 +206,11 @@ protected function processFormMLFields($fields, $model, $parent = null) $fieldName = $parent ? sprintf("%s[%s]", $parent, $fieldName) : $fieldName; - if (!array_key_exists($fieldName, $translatable)) { + if (array_get($config, 'translatable', false)) { + $model->addTranslatableAttributes($fieldName); + $fieldTranslatable = true; + } + if (!$fieldTranslatable && !array_key_exists($fieldName, $translatable)) { continue; } $type = array_get($config, 'type', 'text'); diff --git a/classes/TranslatableBehavior.php b/classes/TranslatableBehavior.php index b3134892..1c312524 100644 --- a/classes/TranslatableBehavior.php +++ b/classes/TranslatableBehavior.php @@ -61,6 +61,10 @@ public function __construct($model) $this->initTranslatableContext(); + if (! isset($model->translatable)) { + $model->addDynamicProperty('translatable', []); + } + $this->model->bindEvent('model.beforeGetAttribute', function ($key) use ($model) { if ($this->isTranslatable($key)) { $value = $this->getAttributeTranslated($key); From 0720e1dbd001ad3dcfd31f42c5d1abbf10a52cdf Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Wed, 27 Mar 2024 11:07:47 -0400 Subject: [PATCH 44/51] fix method name clash --- classes/TranslatableBehavior.php | 2 +- formwidgets/MLRepeater.php | 2 +- traits/MLControl.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/classes/TranslatableBehavior.php b/classes/TranslatableBehavior.php index 1c312524..2e37082b 100644 --- a/classes/TranslatableBehavior.php +++ b/classes/TranslatableBehavior.php @@ -355,7 +355,7 @@ public function translateContext($context = null): string * @param string|null $context * @return self */ - public function lang($context = null): self + public function lang($context = null) { $this->translateContext($context); diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index b41e12c9..5bc3cfa3 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -127,7 +127,7 @@ public function getLocaleSaveData() return $values; } - $fieldName = $this->getFieldName(); + $fieldName = $this->getLongFieldName(); $isJson = $this->isLocaleFieldJsonable(); foreach ($data as $locale => $_data) { diff --git a/traits/MLControl.php b/traits/MLControl.php index fc14a86f..ba3087f3 100644 --- a/traits/MLControl.php +++ b/traits/MLControl.php @@ -239,7 +239,7 @@ public function getLocaleSaveData() return $values; } - $fieldName = $this->getFieldName(); + $fieldName = $this->getLongFieldName(); $isJson = $this->isLocaleFieldJsonable(); foreach ($data as $locale => $_data) { @@ -334,7 +334,7 @@ public function isLongFormNeeded() * * @return string */ - public function getFieldName() + public function getLongFieldName() { if ($this->isLongFormNeeded()) { $fieldName = implode('.', HtmlHelper::nameToArray($this->formField->getName())); From 6ee2a556012ca8709c1064ed3d20a13754810ea4 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Wed, 27 Mar 2024 12:08:46 -0400 Subject: [PATCH 45/51] auto-add jsonable fields used by repeater/nested to translatable array --- classes/EventRegistry.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/classes/EventRegistry.php b/classes/EventRegistry.php index f2edb8a2..705beaee 100644 --- a/classes/EventRegistry.php +++ b/classes/EventRegistry.php @@ -176,6 +176,15 @@ protected function getWidgetLongName($widget) */ protected function processFormMLFields($fields, $model, $parent = null) { + if ($parent) { + $nameArray = HtmlHelper::nameToArray($parent); + $topArrayName = array_shift($nameArray); + if ($topArrayName && $model->isJsonable($topArrayName)) { + // make jsonable field translatable so its value can be localized + $model->addTranslatableAttributes($topArrayName); + } + } + $typesMap = [ 'markdown' => 'mlmarkdowneditor', 'mediafinder' => 'mlmediafinder', From 9d2fa6dd2e26854af4abe32fdb4b0daf7864079b Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Wed, 27 Mar 2024 12:17:29 -0400 Subject: [PATCH 46/51] no need to add dynamic property --- classes/TranslatableBehavior.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/classes/TranslatableBehavior.php b/classes/TranslatableBehavior.php index 2e37082b..afa79f94 100644 --- a/classes/TranslatableBehavior.php +++ b/classes/TranslatableBehavior.php @@ -61,10 +61,6 @@ public function __construct($model) $this->initTranslatableContext(); - if (! isset($model->translatable)) { - $model->addDynamicProperty('translatable', []); - } - $this->model->bindEvent('model.beforeGetAttribute', function ($key) use ($model) { if ($this->isTranslatable($key)) { $value = $this->getAttributeTranslated($key); @@ -380,7 +376,7 @@ public function getTranslatableAttributes() { $translatable = []; - foreach ($this->model->translatable as $attribute) { + foreach ($this->model->translatable ?? [] as $attribute) { $translatable[] = is_array($attribute) ? array_shift($attribute) : $attribute; } From fce0ccef45965ad9b5ac93ab12800b0e6619aebe Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Wed, 27 Mar 2024 12:48:04 -0400 Subject: [PATCH 47/51] document the new translatable field config --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 33ecf816..89b9a22c 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,8 @@ $user->setAttributeTranslated('name', 'Jean-Claude', 'fr'); It is now possible to independently translate the fields defined within a repeater/nestedform formwidget by setting its `translationMode` config to `fields` (see example below) +Note: fields can be marked as translatable either within the Model's `$translatable` array or within the fields.yaml file by adding `translatable: true` to any field config + ```php class User { @@ -265,7 +267,6 @@ class User public $jsonable = ['data']; public $translatable = [ - 'data', 'data[contacts]', 'data[contacts][title]', ]; @@ -277,6 +278,7 @@ models/user/fields.yaml: fields: data[contacts]: type: repeater (or nestedform) + translatable: true translationMode: fields form: fields: @@ -284,6 +286,7 @@ fields: label: Name title: label: Job Title + translatable: true phone: label: Phone number ``` From f7996c402f588aaec183cc27d21be45ebfd3513b Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 2 Apr 2024 09:08:11 -0400 Subject: [PATCH 48/51] build config for all cases --- formwidgets/MLNestedForm.php | 3 ++- formwidgets/MLRepeater.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/formwidgets/MLNestedForm.php b/formwidgets/MLNestedForm.php index d0e02d6e..23667ba6 100644 --- a/formwidgets/MLNestedForm.php +++ b/formwidgets/MLNestedForm.php @@ -35,7 +35,8 @@ public function init() // make the translationMode available to the nestedform formwidgets if (isset($this->config->form)) { - $this->config->form['translationMode'] = $this->translationMode; + $this->config->form = $this->makeConfig($this->config->form); + $this->config->form->translationMode = $this->translationMode; } parent::init(); diff --git a/formwidgets/MLRepeater.php b/formwidgets/MLRepeater.php index 5bc3cfa3..88105c91 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -37,7 +37,8 @@ public function init() $this->fillFromConfig(['translationMode']); // make the translationMode available to the repeater items formwidgets if (isset($this->config->form)) { - $this->config->form['translationMode'] = $this->translationMode; + $this->config->form = $this->makeConfig($this->config->form); + $this->config->form->translationMode = $this->translationMode; } parent::init(); From 1bcf104b5b7a80c191d33a1d655d5c9ecb59a087 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 4 Apr 2024 12:04:58 -0400 Subject: [PATCH 49/51] add css fix for nestedforms --- assets/css/multilingual.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/css/multilingual.css b/assets/css/multilingual.css index 3307979f..2d0b340d 100644 --- a/assets/css/multilingual.css +++ b/assets/css/multilingual.css @@ -20,7 +20,7 @@ .field-multilingual.field-multilingual-nestedform.is-empty .ml-btn{top:-10px;right:5px;text-align:right} .field-multilingual.field-multilingual-repeater.is-empty .ml-dropdown-menu, .field-multilingual.field-multilingual-nestedform.is-empty .ml-dropdown-menu{top:15px;right:-7px} -.fancy-layout .form-tabless-fields .field-multilingual .ml-btn{color:rgba(255,255,255,0.8)} -.fancy-layout .form-tabless-fields .field-multilingual .ml-btn:hover{color:#fff} +.fancy-layout *:not(.nested-form)>.form-widget>.layout-row>.form-tabless-fields .field-multilingual .ml-btn{color:rgba(255,255,255,0.8)} +.fancy-layout *:not(.nested-form)>.form-widget>.layout-row>.form-tabless-fields .field-multilingual .ml-btn:hover{color:#fff} .fancy-layout .field-multilingual-text input.form-control{padding-right:44px} -.help-block.before-field + .field-multilingual.field-multilingual-textarea .ml-btn{top:-41px} \ No newline at end of file +.help-block.before-field + .field-multilingual.field-multilingual-textarea .ml-btn{top:-41px} From 1f67fcff7879b3e7beb6d1aa18af0a898be68fa7 Mon Sep 17 00:00:00 2001 From: Naoki Miyazaki-Chapleau <104367522+nmiyazaki-chapleau@users.noreply.github.com> Date: Wed, 29 May 2024 02:52:27 -0400 Subject: [PATCH 50/51] Fix getLocaleFieldName behavior with empty formField arrayName (#83) --- traits/MLControl.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/traits/MLControl.php b/traits/MLControl.php index ba3087f3..31eba3a9 100644 --- a/traits/MLControl.php +++ b/traits/MLControl.php @@ -188,14 +188,14 @@ protected function makeRenderFormField() public function getLocaleFieldName($code) { - if ($this->isLongFormNeeded()) { + $suffix = ''; + + if ($this->isLongFormNeeded() && !empty($this->formField->arrayName)) { $names = HtmlHelper::nameToArray($this->formField->arrayName); - $name = $this->formField->getName('RLTranslate[' . $code . '][' . implode('][', $names) . ']'); - } else { - $name = $this->formField->getName('RLTranslate['.$code.']'); + $suffix = '[' . implode('][', $names) . ']'; } - return $name; + return $this->formField->getName('RLTranslate['.$code.']' . $suffix); } /** From 0e12827f8f5d4658afafc8f3adfc01bf8a31b035 Mon Sep 17 00:00:00 2001 From: Naoki Miyazaki-Chapleau <104367522+nmiyazaki-chapleau@users.noreply.github.com> Date: Thu, 1 Aug 2024 07:43:42 -0400 Subject: [PATCH 51/51] Fix issue with loading translated data with arrayNames --- traits/MLControl.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/traits/MLControl.php b/traits/MLControl.php index 31eba3a9..55cdbae4 100644 --- a/traits/MLControl.php +++ b/traits/MLControl.php @@ -146,6 +146,10 @@ public function getLocaleValue($locale) { $key = $this->valueFrom ?: $this->fieldName; + if (!empty($this->formField->arrayName)) { + $key = $this->formField->arrayName.'['.$key.']'; + } + /* * Get the translated values from the model */ @@ -206,6 +210,10 @@ public function getLocaleSaveValue($value) $localeData = $this->getLocaleSaveData(); $key = $this->valueFrom ?: $this->fieldName; + if (!empty($this->formField->arrayName)) { + $key = $this->formField->arrayName.'['.$key.']'; + } + /* * Set the translated values to the model */