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 483125a6..89b9a22c 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: @@ -147,7 +156,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,94 +198,147 @@ 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'); + +// Sets a single translated attribute for a language +$user->setAttributeTranslated('name', 'Jean-Claude', 'fr'); +``` + +## Repeater/NestedForm formwidget internal fields translation + +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) - // Gets a single translated attribute for a language - $user->getAttributeTranslated('name', 'fr'); +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 - // Sets a single translated attribute for a language - $user->setAttributeTranslated('name', 'Jean-Claude', 'fr'); - +```php +class User +{ + public $implement = ['Winter.Translate.Behaviors.TranslatableModel']; + + public $jsonable = ['data']; + + public $translatable = [ + 'data[contacts]', + 'data[contacts][title]', + ]; +} +``` + +models/user/fields.yaml: +```yaml +fields: + data[contacts]: + type: repeater (or nestedform) + translatable: true + translationMode: fields + form: + fields: + name: + label: Name + title: + label: Job Title + translatable: true + 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: - 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 `setTranslatableUseFallback()` method. - $user = User::first(); +```php +$user = User::first(); - $user->setTranslatableUseFallback(false)->lang('fr'); +$user->setTranslatableUseFallback(false)->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 @@ -305,102 +367,109 @@ 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. - - Event::listen('translate.localePicker.translateParams', function($page, $params, $oldLocale, $newLocale) { - if ($page->baseFileName == 'your-page-filename') { - return YourModel::translateParams($params, $oldLocale, $newLocale); - } - }); +```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: - - 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; - } +```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; } - 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. - - Event::listen('translate.localePicker.translateQuery', function($page, $params, $oldLocale, $newLocale) { - if ($page->baseFileName == 'your-page-filename') { - return YourModel::translateParams($params, $oldLocale, $newLocale); - } - }); +```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 - - Event::listen('winter.translate.themeScanner.afterScan', function (ThemeScanner $scanner) { - ... - }); +```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: - - Settings::instance()->getAttributeTranslated('your_attribute_name'); +```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 + */ +class Post extends Model +{ + + [...] /** - * Blog Post Model + * Softly implement the TranslatableModel behavior. */ - class Post extends Model - { - - [...] - - /** - * Softly implement the TranslatableModel behavior. - */ - public $implement = ['@Winter.Translate.Behaviors.TranslatableModel']; + 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. #### 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. - - /** - * 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'], - ] - ]; - } +```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'], + ] + ]; +} +``` # User Interface @@ -411,48 +480,50 @@ 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, + _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('&') - 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)) + 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: - - {{ form_open() }} - - {{ form_close() }} +```twig +{{ form_open() }} + +{{ form_close() }} +``` 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} 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 b3134892..afa79f94 100644 --- a/classes/TranslatableBehavior.php +++ b/classes/TranslatableBehavior.php @@ -351,7 +351,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); @@ -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; ?>