diff --git a/Plugin.php b/Plugin.php index b14e5fc0..72861a37 100644 --- a/Plugin.php +++ b/Plugin.php @@ -219,9 +219,17 @@ 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; + } + $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); + } + } } } $this->extendModel($model, 'model', $translatable); diff --git a/README.md b/README.md index 0c958a23..e3a6d331 100644 --- a/README.md +++ b/README.md @@ -133,8 +133,8 @@ name: My Theme # [...] translate: -en: config/lang-en.yaml -fr: config/lang-fr.yaml + en: config/lang-en.yaml + fr: config/lang-fr.yaml ``` This is an example for the **config/lang-en.yaml** file: @@ -476,7 +476,7 @@ 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. -```js +```javascript document.querySelector('#languageSelect').addEventListener('change', function () { const details = { _session_key: document.querySelector('input[name="_session_key"]').value, diff --git a/assets/css/multilingual.css b/assets/css/multilingual.css index 1b2bc38c..7c4d3eda 100644 --- a/assets/css/multilingual.css +++ b/assets/css/multilingual.css @@ -13,6 +13,14 @@ .field-multilingual.field-multilingual-markdowneditor .ml-btn{border-radius:0;border-bottom-left-radius:.375rem} .field-multilingual.field-multilingual-markdowneditor .ml-dropdown-menu{top:28px;right:1px} .field-multilingual.field-multilingual-repeater .ml-dropdown-menu, -.field-multilingual.field-multilingual-nestedform .ml-dropdown-menu{top:2px;right:0} -.field-multilingual.field-multilingual-mediafinder .ml-btn{border-radius:.375rem} -.fancy-layout .form-tabless-fields .field-multilingual .ml-btn{background-color:rgba(248,246,243,0.75)} +.field-multilingual.field-multilingual-nestedform .ml-dropdown-menu{top:0;right:0} +.field-multilingual.field-multilingual-repeater.is-empty, +.field-multilingual.field-multilingual-nestedform.is-empty{padding-top:5px} +.field-multilingual.field-multilingual-repeater.is-empty .ml-btn, +.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 *: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} diff --git a/classes/EventRegistry.php b/classes/EventRegistry.php index 31480670..705beaee 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; @@ -127,8 +128,10 @@ public function registerModelTranslation($widget) return; } - - if (!$model->hasTranslatableAttributes() || $widget->isNested) { + if ($widget->isNested && !empty($widget->fields)) { + if (($widget->config->translationMode ?? 'default') === 'fields') { + $widget->fields = $this->processFormMLFields($widget->fields, $model, $this->getWidgetLongName($widget)); + } return; } @@ -145,14 +148,43 @@ public function registerModelTranslation($widget) } } + protected function getWidgetLongName($widget) + { + $nameArray = HtmlHelper::nameToArray($widget->arrayName); + foreach ($nameArray as $index => $name) { + if (is_numeric($name)) { + unset($nameArray[$index]); + } + } + + array_shift($nameArray); // remove parent model + $parentName = array_shift($nameArray); + + if ($nameArray) { + $parentName .= '[' . implode('][', $nameArray) . ']'; + } + + return $parentName; + } + /** * 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) + 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', @@ -174,16 +206,23 @@ protected function processFormMLFields($fields, $model) foreach ($fields as $name => $config) { $fieldName = $name; + $fieldTranslatable = false; + if (str_contains($name, '@')) { // apply to fields with any context list($fieldName, $context) = explode('@', $name); } - if (!array_key_exists($fieldName, $translatable)) { + + $fieldName = $parent ? sprintf("%s[%s]", $parent, $fieldName) : $fieldName; + + 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'); - if (array_key_exists($type, $typesMap)) { $fields[$name]['type'] = $typesMap[$type]; } diff --git a/classes/TranslatableBehavior.php b/classes/TranslatableBehavior.php index 5ba9b728..afa79f94 100644 --- a/classes/TranslatableBehavior.php +++ b/classes/TranslatableBehavior.php @@ -376,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; } diff --git a/formwidgets/MLNestedForm.php b/formwidgets/MLNestedForm.php index 3ad2e2a8..23667ba6 100644 --- a/formwidgets/MLNestedForm.php +++ b/formwidgets/MLNestedForm.php @@ -21,13 +21,38 @@ class MLNestedForm extends NestedForm */ protected $defaultAlias = 'mlnestedform'; + /** + * The nestedform 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 = $this->makeConfig($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('WinterTranslateGetJsonAttributeTranslated', 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 +64,7 @@ public function render() $parentContent = parent::render(); $this->actAsParent(false); - if (!$this->isAvailable) { + if ($this->translationMode === 'fields' || !$this->isAvailable) { return $parentContent; } @@ -50,7 +75,9 @@ public function render() public function prepareVars() { parent::prepareVars(); - $this->prepareLocaleVars(); + if ($this->translationMode === 'default') { + $this->prepareLocaleVars(); + } } /** @@ -59,8 +86,14 @@ public function prepareVars() */ public function getSaveValue($value) { - $this->rewritePostValues(); - return $this->getLocaleSaveValue($value); + if ($this->translationMode === 'fields') { + $localeValue = $this->getLocaleSaveValue($value); + $value = array_replace_recursive($value ?? [], $localeValue ?? []); + } else { + $this->rewritePostValues(); + $value = $this->getLocaleSaveValue($value); + } + 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 460463fd..88105c91 100644 --- a/formwidgets/MLRepeater.php +++ b/formwidgets/MLRepeater.php @@ -24,13 +24,37 @@ class MLRepeater extends Repeater */ protected $defaultAlias = 'mlrepeater'; + /** + * The repeater translation mode (default|fields) + */ + protected $translationMode = 'default'; + /** * {@inheritDoc} */ public function init() { + $this->fillFromConfig(['translationMode']); + // make the translationMode available to the repeater items formwidgets + if (isset($this->config->form)) { + $this->config->form = $this->makeConfig($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('WinterTranslateGetJsonAttributeTranslated', 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)); + } + }); + }); + } } /** @@ -42,7 +66,7 @@ public function render() $parentContent = parent::render(); $this->actAsParent(false); - if (!$this->isAvailable) { + if ($this->translationMode === 'fields' || !$this->isAvailable) { return $parentContent; } @@ -53,7 +77,18 @@ 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 + protected function getGroupFormFieldConfig($code) + { + $config = parent::getGroupFormFieldConfig($code); + $config['translationMode'] = $this->translationMode; + + return $config; } /** @@ -62,9 +97,54 @@ public function prepareVars() */ public function getSaveValue($value) { - $this->rewritePostValues(); + $value = is_array($value) ? array_values($value) : $value; + + if ($this->translationMode === 'fields') { + $localeValue = $this->getLocaleSaveValue($value); + $value = array_replace_recursive($value ?? [], $localeValue ?? []); + } else { + $this->rewritePostValues(); + $value = $this->getLocaleSaveValue($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)) { + if ($this->translationMode === 'fields') { + foreach (Locale::listEnabled() as $code => $name) { + // force translations removal from db + $values[$code] = []; + } + } + return $values; + } + + $fieldName = $this->getLongFieldName(); + $isJson = $this->isLocaleFieldJsonable(); + + foreach ($data as $locale => $_data) { + $i = 0; + $content = array_get($_data, $fieldName); + 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; + } + } - return $this->getLocaleSaveValue(is_array($value) ? array_values($value) : $value); + return $values; } /** @@ -76,7 +156,7 @@ protected function loadAssets() parent::loadAssets(); $this->actAsParent(false); - if (Locale::isAvailable()) { + if (Locale::isAvailable() && $this->translationMode === 'default') { $this->loadLocaleAssets(); $this->addJs('js/mlrepeater.js'); } diff --git a/traits/MLControl.php b/traits/MLControl.php index a95e9e85..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 */ @@ -155,6 +159,12 @@ public function getLocaleValue($locale) if ($this->objectMethodExists($this->model, $mutateMethod)) { $value = $this->model->$mutateMethod($locale); } + elseif ($this->defaultLocale->code != $locale && $this->isFieldParentJsonable() && + $this->objectMethodExists($this->model, 'WinterTranslateGetJsonAttributeTranslated') + ) + { + $value = $this->model->WinterTranslateGetJsonAttributeTranslated($this->formField->getName(), $locale); + } elseif ($this->objectMethodExists($this->model, 'getAttributeTranslated') && $this->defaultLocale->code != $locale) { $value = $this->model->setTranslatableUseFallback(false)->getAttributeTranslated($key, $locale); } @@ -180,6 +190,18 @@ protected function makeRenderFormField() return $field; } + public function getLocaleFieldName($code) + { + $suffix = ''; + + if ($this->isLongFormNeeded() && !empty($this->formField->arrayName)) { + $names = HtmlHelper::nameToArray($this->formField->arrayName); + $suffix = '[' . implode('][', $names) . ']'; + } + + return $this->formField->getName('RLTranslate['.$code.']' . $suffix); + } + /** * {@inheritDoc} */ @@ -188,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 */ @@ -221,12 +247,12 @@ public function getLocaleSaveData() return $values; } - $fieldName = implode('.', HtmlHelper::nameToArray($this->fieldName)); + $fieldName = $this->getLongFieldName(); $isJson = $this->isLocaleFieldJsonable(); 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; @@ -241,6 +267,23 @@ public function getFallbackType() return defined('static::FALLBACK_TYPE') ? static::FALLBACK_TYPE : 'text'; } + public function isFieldParentJsonable() + { + $names = HtmlHelper::nameToArray($this->formField->arrayName); + if (count($names) >= 2) { + // $names[0] is the Model, $names[1] is the top array name + $arrayName = $names[1]; + + if ($this->model->isClassExtendedWith('System\Behaviors\SettingsModel') || + method_exists($this->model, 'isJsonable') && $this->model->isJsonable($arrayName) + ) + { + return true; + } + } + return false; + } + /** * Returns true if widget is a repeater, or the field is specified * as jsonable in the model. @@ -280,4 +323,32 @@ protected function objectMethodExists($object, $method) return method_exists($object, $method); } + + /** + * determine if fieldName needs long form + * + * @return boolean + */ + public 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"); + } + + /** + * get the proper field name + * + * @return string + */ + public function getLongFieldName() + { + if ($this->isLongFormNeeded()) { + $fieldName = implode('.', HtmlHelper::nameToArray($this->formField->getName())); + } else { + $fieldName = implode('.', HtmlHelper::nameToArray($this->fieldName)); + } + return $fieldName; + } } diff --git a/traits/mlcontrol/partials/_locale_values.htm b/traits/mlcontrol/partials/_locale_values.htm index 29c9fb41..21f8efaa 100644 --- a/traits/mlcontrol/partials/_locale_values.htm +++ b/traits/mlcontrol/partials/_locale_values.htm @@ -2,11 +2,11 @@ $name): ?> getLocaleValue($code); - $value = $this->isLocaleFieldJsonable() ? json_encode($value) : $value; + $value = $this->isLocaleFieldJsonable() && is_array($value) ? json_encode($value) : $value; ?>