Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions _build/data/transport.settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
'vertical_tabs' => false,
'context_aware' => false,
'google_fonts_api_key' => '',
'lexicon_topics' => '',
);

$settings = array();
Expand Down
16 changes: 1 addition & 15 deletions assets/components/clientconfig/js/mgr/sections/home.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,24 +194,10 @@ Ext.extend(ClientConfig.page.Home,MODx.Component,{
}

if (field.xtype === 'modx-combo') {
var options = value.options.split('||');
var data = [];
Ext.each(options, function(option, index) {
option = option.split('==');
if (option[1]) {
data.push([
option[1],
option[0]
]);
} else {
data.push([option[0],option[0]]);
}
});

field.store = new Ext.data.ArrayStore({
mode: 'local',
fields: ['value','label'],
data: data
data: value.optionData
});
field.hiddenName = value.key;
field.valueField = 'value';
Expand Down
7 changes: 7 additions & 0 deletions core/components/clientconfig/controllers/home.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@ public function process(array $scriptProperties = []) {
$groups = $this->modx->getCollection('cgGroup', $c);
foreach ($groups as $group) {
$grp = $group->toArray();
$grp['label'] = $this->clientconfig->translate($grp['label']);
$grp['description'] = $this->clientconfig->translate($grp['description']);
$grp['items'] = [];

$c = $this->modx->newQuery('cgSetting');
$c->sortby('sortorder','ASC');
$c->sortby('label','ASC');
foreach ($group->getMany('Settings', $c) as $setting) {
$sa = $setting->toArray();
$sa['label'] = $this->clientconfig->translate($sa['label']);
$sa['description'] = $this->clientconfig->translate($sa['description']);
if (in_array($sa['xtype'], ['checkbox','xcheckbox'], true)) {
$sa['value'] = (bool)$sa['value'];
}
Expand All @@ -48,6 +52,9 @@ public function process(array $scriptProperties = []) {
$this->modx->parser->processElementTags('', $inputOpts, true, true);
$sa['options'] = $inputOpts;
}
if ($sa['xtype'] === 'modx-combo') {
$sa['optionData'] = $this->clientconfig->getOptionData($sa['options']);
}
$grp['items'][] = $sa;
}
$tabs[] = $grp;
Expand Down
5 changes: 4 additions & 1 deletion core/components/clientconfig/docs/readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ Possible uses include:
- Keep contact details updated in one central location
- Update the email-address a form sends notifications to.

Licensed under the MIT.
For translating configuration labels, descriptions and selectbox labels, see
translations.md in this directory.

Licensed under the MIT.
66 changes: 66 additions & 0 deletions core/components/clientconfig/docs/translations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Translating the configuration form

ClientConfig can display setting labels, setting descriptions, group titles,
group descriptions and selectbox labels in the manager user's language. The
language of the selected website context does not change these translations.

## Configure a lexicon

Create a MODX namespace, for example `website`, and add lexicon files for the
languages your editors use. With the namespace path
`{core_path}components/website/`, an example file is
`core/components/website/lexicon/en/settings.inc.php`:

```php
<?php
$_lang['website.appearance'] = 'Appearance';
$_lang['website.appearance.desc'] = 'Change the site appearance.';
$_lang['website.font'] = 'Font style';
$_lang['website.font.desc'] = 'Choose the font style.';
$_lang['website.font.normal'] = 'Normal';
$_lang['website.font.italic'] = 'Italic';
```

Create a corresponding `lexicon/de/settings.inc.php` with the same keys and
German text (for example `Kursiv` for `website.font.italic`). Add
`website:settings` to the `clientconfig.lexicon_topics` system setting. Multiple
topics are comma-separated, for example `website:default,website:settings`.
A topic without a namespace loads from `core`. Topics always load in the
manager's language, even if a language prefix was supplied.

MODX's normal lexicon fallback applies. If a key still cannot be found, its
original reference remains visible rather than becoming an empty label. Clear
the MODX cache after creating or updating lexicon files.

## Reference the entries

Use `%website.font` or `[[%website.font]]` as the **entire** setting label.
Use the same syntax for a description, group title or group description.
For example, a group can have label `%website.appearance` and description
`%website.appearance.desc`.

These are explicit ClientConfig references, not arbitrary MODX tag processing.
Inline tags, properties and output filters are not evaluated by this feature.
Plain text and bare keys such as `website.font` remain literal. Already loaded
topics work too, for example `[[%clientconfig.label]]`.

For a selectbox, put the reference on the label side:

```text
%website.font.normal==normal||%website.font.italic==italic
```

The displayed labels change with the editor's language, but the values saved
are always `normal` and `italic`. Without `==value`, the original option text
remains the submitted value, even if its display label is translated. Prefer
explicit values for translated choices.

You do not need **Process tags in options** for these references. If you enable
it for a dynamic option source, existing MODX tag processing runs first. Use
`%key` in that source's output so ClientConfig translates labels after splitting
the options. Native `[[%key]]` tags processed by MODX in this mode may introduce
`||` or `==` delimiters before ClientConfig receives the options.

Translations affect only the client-facing form and its required-field errors.
The admin grids/edit dialogs and imports/exports retain the original references;
setting keys, values and defaults are never translated by this feature.
3 changes: 3 additions & 0 deletions core/components/clientconfig/lexicon/en/default.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,6 @@
$_lang['clientconfig.xtype.line'] = 'Line (divider)';
$_lang['clientconfig.xtype.code'] = 'Code (requires Ace editor installed)';
$_lang['clientconfig.xtype.email'] = 'Email';

$_lang['setting_clientconfig.lexicon_topics'] = 'Additional lexicon topics';
$_lang['setting_clientconfig.lexicon_topics_desc'] = 'Comma-separated lexicon topics (for example website:default, website:settings) for translating setting/group labels, descriptions and selectbox labels in the manager. Use %key or [[%key]] as the complete label or description. Topics use the manager user\'s language; setting values are not translated.';
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ class ClientConfig {
*/
public $debug = false;

/** @var bool */
protected $lexiconTopicsLoaded = false;


/**
* @param \modX $modx
Expand Down Expand Up @@ -68,6 +71,58 @@ public function __construct(modX &$modx,array $config = array()) {
$this->debug = (bool)$this->modx->getOption('clientconfig.debug',null,false);
}

/**
* Translate an explicit lexicon reference for display in the manager.
* Plain text and unknown references are left intact. Do not use this on
* setting values or on the editable configuration returned by admin grids.
*
* @param string $text A %key or [[%key]] reference, or literal text.
* @return string
*/
public function translate($text) {
if (!is_string($text) || !preg_match('/^(?:%([a-zA-Z0-9_.-]+)|\[\[%([a-zA-Z0-9_.-]+)\]\])$/D', $text, $matches)) {
return $text;
}

// MODX resolves the manager user's language into cultureKey before
// invoking both manager controllers and connector processors.
$language = $this->modx->getOption('cultureKey', null, 'en', true);
if (!$this->lexiconTopicsLoaded) {
$topics = $this->modx->getOption('clientconfig.lexicon_topics', null, '');
foreach (explode(',', $topics) as $topic) {
$topic = trim($topic);
if ($topic !== '') {
$parts = explode(':', $topic);
$name = array_pop($parts);
$namespace = count($parts) ? array_pop($parts) : 'core';
$this->modx->lexicon->load($language . ':' . $namespace . ':' . $name);
}
}
$this->lexiconTopicsLoaded = true;
}

$key = $matches[1] !== '' ? $matches[1] : $matches[2];
return $this->modx->lexicon->exists($key, $language) ? $this->modx->lexicon($key, array(), $language) : $text;
}

/**
* Build selectbox data without translating the submitted option values.
* Keep labels separate from the option delimiters: translations may contain
* either || or ==. Match the existing manager's label==value parsing.
*
* @param string $options
* @return array
*/
public function getOptionData($options) {
$data = array();
foreach (explode('||', (string)$options) as $option) {
$parts = explode('==', $option);
$value = isset($parts[1]) && $parts[1] !== '' ? $parts[1] : $parts[0];
$data[] = array($value, $this->translate($parts[0]));
}
return $data;
}

/**
* Grab settings (from cache if possible) as key => value pairs.
* @param string $context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public function process() {

if (trim($value) === '' && $setting->get('is_required') &&
!in_array($setting->get('xtype'), array('checkbox', 'xcheckbox'), true)) {
$this->addFieldError($key, $setting->get('label') . ': ' . $this->modx->lexicon('clientconfig.field_is_required'));
$label = $this->modx->clientconfig->translate($setting->get('label'));
$this->addFieldError($key, $label . ': ' . $this->modx->lexicon('clientconfig.field_is_required'));
continue;
}

Expand Down
30 changes: 30 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Manager translation regression checks

```sh
php tests/manager-translations.php
node tests/manager-translations.js
```

The PHP checks run the actual ClientConfig service, home controller and save
processor against small MODX infrastructure fixtures. They cover two resolved
manager languages, custom topic loading, missing/literal labels, group and
setting text, option delimiters, unchanged configuration records, dynamic
options, and required-field validation/saving. The JavaScript checks run the
actual manager form builder with ExtJS infrastructure fixtures to verify its
display labels and submitted values.

These are standalone regression checks, not a full MODX installation test.
For integration testing on MODX 2 and 3:

1. Follow `core/components/clientconfig/docs/translations.md` to add English
and German entries and a translated group and selectbox.
2. Open the configuration form as English and German manager users. Confirm
that group and setting labels/descriptions use each user's language.
3. Enable context-aware mode and select a website context with a different
language. The form labels should still use the manager user's language.
4. Save each choice, reload, and check that the same stored value is selected
in both languages. Leave a required field empty to inspect its error label.
5. Reopen the admin edit dialogs and export the configuration. References,
raw option definitions and defaults should be unchanged.
6. Check a literal-only group and selectbox, an unknown lexicon key, and a
dynamic option source returning `%key==value` with tag processing enabled.
41 changes: 41 additions & 0 deletions tests/manager-translations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Run with: node tests/manager-translations.js
// Exercise the real manager form builder with the controller's data format.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const ClientConfig = {
page: {}, isAdmin: false, reqAsterisk: '*',
data: [{
id: 1, label: 'Darstellung', description: 'Darstellung bearbeiten',
items: [{
key: 'font', xtype: 'modx-combo', label: 'Schrift', description: 'Schrift auswählen',
is_required: true, value: 'italic', default: 'normal',
options: '%website.normal==normal||%website.italic==italic',
optionData: [['normal', 'Normal'], ['italic', 'Kursiv || mit == Zeichen']]
}]
}]
};
const context = {
ClientConfig, MODx: { Component: function () {}, config: {} },
Ext: {
extend(target, parent, methods) { Object.assign(target.prototype, methods); },
reg() {}, each(items, fn) { items.forEach(fn); }, iterate(items, fn) { items.forEach(fn); },
data: { ArrayStore: function (config) { Object.assign(this, config); } }
}
};
vm.runInNewContext(fs.readFileSync(require.resolve('../assets/components/clientconfig/js/mgr/sections/home.js'), 'utf8'), context);
const tabs = ClientConfig.page.Home.prototype.getTabs.call({});
const fields = tabs[0].items[1].items;
const select = fields[0];
assert.equal(tabs[0].title, 'Darstellung');
assert.equal(tabs[0].items[0].html, '<p>Darstellung bearbeiten</p>');
assert.equal(select.fieldLabel, 'Schrift*');
assert.equal(fields[1].text, 'Schrift auswählen');
assert.equal(select.name, 'font');
assert.equal(select.hiddenName, 'font');
assert.equal(select.value, 'italic');
assert.equal(select.valueField, 'value');
assert.equal(select.displayField, 'label');
assert.deepEqual(select.store.data, ClientConfig.data[0].items[0].optionData);
assert.equal(select.store.data.length, 2);
console.log('Passed manager form translation checks.');
Loading