Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
8b0ee0a
first draft of MLRepeaterFields
mjauvin Feb 19, 2024
faef2c3
merge new repeater field with internal fields translation
mjauvin Feb 19, 2024
2e30b7e
put all elements into the array then implode
mjauvin Feb 20, 2024
0ccb6c4
cleanup
mjauvin Feb 20, 2024
203c991
allow field translatable config for repeater forms in "fields" transl…
mjauvin Feb 20, 2024
685fa43
properly store repeater fields translations in attributes table
mjauvin Feb 22, 2024
1f73b76
save a call by determining if the field parent is jsonable
mjauvin Feb 22, 2024
a8724e5
simplify code
mjauvin Feb 22, 2024
f0da1c5
improve code robustness and add some comments
mjauvin Feb 22, 2024
ef184a3
remove translatable config support
mjauvin Feb 22, 2024
0b7d1bf
cleanup
mjauvin Feb 22, 2024
d942348
add docstring param & remove old code
mjauvin Feb 22, 2024
dddeef9
simplify code
mjauvin Feb 22, 2024
462b432
add example of translationMode: fields in README
mjauvin Feb 22, 2024
495726f
improve yaml syntax
mjauvin Feb 22, 2024
0c83533
improve readme syntax
mjauvin Feb 22, 2024
634e6ee
more readme syntax improvements
mjauvin Feb 22, 2024
579a9eb
improve translationMode section in readme
mjauvin Feb 22, 2024
e6a8df8
add repeater translatable fields from themedata; fix issue
mjauvin Feb 22, 2024
edbe849
properly merge values
mjauvin Feb 22, 2024
36353b0
value might be null
mjauvin Feb 22, 2024
0bed2fc
improve code readability
mjauvin Feb 22, 2024
533285a
no need to hardcode model name here
mjauvin Feb 22, 2024
1813c85
fix for groups mode
mjauvin Feb 23, 2024
681cafa
account for nestedform field types in repeater
mjauvin Feb 23, 2024
e9e9c03
make sure arrays are provided to array_replace_recursive()
mjauvin Feb 24, 2024
533d964
fix repeater in regular translation mode
mjauvin Feb 24, 2024
d6ec621
also apply to nestedform
mjauvin Feb 24, 2024
a7859ed
fix widget name
mjauvin Feb 24, 2024
e2fb60b
add nestedform to README example
mjauvin Feb 25, 2024
6047f8c
add top-level array to translatable
mjauvin Feb 25, 2024
f98357e
Update Plugin.php
LukeTowers Feb 26, 2024
c4d55bc
alternative to using @
mjauvin Feb 27, 2024
aacb3d0
fix reordering issues
mjauvin Feb 27, 2024
700c28c
do not call with empty fields
mjauvin Feb 27, 2024
6aef498
make sure we have an array
mjauvin Feb 29, 2024
c074a60
prefix plugin name to dynamic method
mjauvin Feb 29, 2024
07c0a06
rename last instance
mjauvin Mar 5, 2024
51bcdaf
purge translations for deleted repeaters
mjauvin Mar 5, 2024
5c6bf36
Merge branch 'main' into wip-ml-repeater-fields
mjauvin Mar 18, 2024
8b7f203
Merge branch 'main' into wip-ml-repeater-fields
mjauvin Mar 18, 2024
7bb6a86
remove code duplication
mjauvin Mar 19, 2024
d617cbc
fix method visibility and add doc string
mjauvin Mar 19, 2024
e23bb1d
fix extension
mjauvin Mar 27, 2024
cc3aeef
add option to specify translatable fields in fields.yaml
mjauvin Mar 27, 2024
0720e1d
fix method name clash
mjauvin Mar 27, 2024
6ee2a55
auto-add jsonable fields used by repeater/nested to translatable array
mjauvin Mar 27, 2024
9d2fa6d
no need to add dynamic property
mjauvin Mar 27, 2024
fce0cce
document the new translatable field config
mjauvin Mar 27, 2024
f7996c4
build config for all cases
mjauvin Apr 2, 2024
1bcf104
add css fix for nestedforms
mjauvin Apr 4, 2024
1f67fcf
Fix getLocaleFieldName behavior with empty formField arrayName (#83)
nmiyazaki-chapleau May 29, 2024
0e12827
Fix issue with loading translated data with arrayNames
nmiyazaki-chapleau Aug 1, 2024
f432fb5
Merge branch 'main' into wip-ml-repeater-fields
LukeTowers Feb 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 11 additions & 3 deletions assets/css/multilingual.css
Original file line number Diff line number Diff line change
Expand Up @@ -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}
51 changes: 45 additions & 6 deletions classes/EventRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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',
Expand All @@ -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];
}
Expand Down
2 changes: 1 addition & 1 deletion classes/TranslatableBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
43 changes: 38 additions & 5 deletions formwidgets/MLNestedForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
});
});
}
}

/**
Expand All @@ -39,7 +64,7 @@ public function render()
$parentContent = parent::render();
$this->actAsParent(false);

if (!$this->isAvailable) {
if ($this->translationMode === 'fields' || !$this->isAvailable) {
return $parentContent;
}

Expand All @@ -50,7 +75,9 @@ public function render()
public function prepareVars()
{
parent::prepareVars();
$this->prepareLocaleVars();
if ($this->translationMode === 'default') {
$this->prepareLocaleVars();
}
}

/**
Expand All @@ -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;
}

/**
Expand All @@ -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');
}
Expand Down
90 changes: 85 additions & 5 deletions formwidgets/MLRepeater.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
});
});
}
}

/**
Expand All @@ -42,7 +66,7 @@ public function render()
$parentContent = parent::render();
$this->actAsParent(false);

if (!$this->isAvailable) {
if ($this->translationMode === 'fields' || !$this->isAvailable) {
return $parentContent;
}

Expand All @@ -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;
}

/**
Expand All @@ -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;
}

/**
Expand All @@ -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');
}
Expand Down
Loading
Loading