diff --git a/CHANGELOG b/CHANGELOG index 42f3f3916..706d39f8b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -74,6 +74,7 @@ Core: - Added config for disabling header and footer in the projector [#3357]. - Updated CKEditor to 4.7 [#3375]. - Reduced ckeditor toolbar for inline editing [#3368]. +- Added custom translations in config [#3383]. Mediafiles: - Fixed reloading of PDF on page change [#3274]. diff --git a/openslides/core/config.py b/openslides/core/config.py index 7a7ed689c..f3683ad83 100644 --- a/openslides/core/config.py +++ b/openslides/core/config.py @@ -20,6 +20,7 @@ INPUT_TYPE_MAPPING = { 'datetimepicker': int, 'majorityMethod': str, 'logo': dict, + 'translations': list, } @@ -125,6 +126,22 @@ class ConfigHandler: if not isinstance(value[required_entry], str): raise ConfigError(_('{} has to be a string.'.format(required_entry))) + if config_variable.input_type == 'translations': + if not isinstance(value, list): + raise ConfigError(_('Translations has to be a list.')) + for entry in value: + if not isinstance(entry, dict): + raise ConfigError(_('Every value has to be a dict, not {}.'.format(type(entry)))) + whitelist = ( + 'original', + 'translation', + ) + for required_entry in whitelist: + if required_entry not in entry: + raise ConfigError(_('{} has to be given.'.format(required_entry))) + if not isinstance(entry[required_entry], str): + raise ConfigError(_('{} has to be a string.'.format(required_entry))) + # Save the new value to the database. db_value = ConfigStore.objects.get(key=key) db_value.value = value diff --git a/openslides/core/config_variables.py b/openslides/core/config_variables.py index 13e31e1a0..75f19d0e9 100644 --- a/openslides/core/config_variables.py +++ b/openslides/core/config_variables.py @@ -322,3 +322,12 @@ def get_config_variables(): weight=312, group='Logo', hidden=True) + + # Custom translations + yield ConfigVariable( + name='translations', + label='Custom translations', + default_value=[], + input_type='translations', + weight=1000, + group='Custom translations') diff --git a/openslides/core/static/css/app.css b/openslides/core/static/css/app.css index 28ebc0605..c339d20e4 100644 --- a/openslides/core/static/css/app.css +++ b/openslides/core/static/css/app.css @@ -1120,11 +1120,24 @@ img { /** Config **/ .input-comments > div { - margin-bottom: 5px + margin-bottom: 5px; } .config-checkbox { padding: 6px 12px; } +.config-translations > div { + margin-bottom: 5px; + padding-right: 15px; + width: 100%; +} +.config-translations .inputs input { + width: 47%; +} +.config-translations .inputs .arrow { + width: 6%; + float: left; + text-align: center; +} /** Pojector sidebar **/ .col2 .projectorSelector { diff --git a/openslides/core/static/js/core/base.js b/openslides/core/static/js/core/base.js index 21f1fe45c..7f30b67c7 100644 --- a/openslides/core/static/js/core/base.js +++ b/openslides/core/static/js/core/base.js @@ -254,6 +254,52 @@ angular.module('OpenSlidesApp.core', [ } ]) +// Hook into gettextCatalog to include custom translations by wrapping +// the getString method. The translations are stored in the config. +.decorator('gettextCatalog', [ + '$delegate', + '$rootScope', + function ($delegate, $rootScope) { + var oldGetString = $delegate.getString; + var customTranslations = {}; + + $delegate.getString = function () { + var translated = oldGetString.apply($delegate, arguments); + if (customTranslations[translated]) { + translated = customTranslations[translated]; + } + return translated; + }; + $delegate.setCustomTranslations = function (translations) { + customTranslations = translations; + $rootScope.$broadcast('gettextLanguageChanged'); + }; + + return $delegate; + } +]) + +.run([ + '$rootScope', + 'Config', + 'gettextCatalog', + function ($rootScope, Config, gettextCatalog) { + $rootScope.$watch(function () { + return Config.lastModified('translations'); + }, function () { + var translations = Config.get('translations'); + if (translations) { + var customTranslations = {}; + _.forEach(translations.value, function (entry) { + customTranslations[entry.original] = entry.translation; + }); + // Update all translate directives + gettextCatalog.setCustomTranslations(customTranslations); + } + }); + } +]) + // set browser language as default language for OpenSlides .run([ 'gettextCatalog', diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js index 947fea59a..e4cd2c079 100644 --- a/openslides/core/static/js/core/site.js +++ b/openslides/core/static/js/core/site.js @@ -825,6 +825,7 @@ angular.module('OpenSlidesApp.core.site', [ colorpicker: 'colorpicker', datetimepicker: 'datetimepicker', majorityMethod: 'choice', + translations: 'translations', }[type]; }; @@ -1134,6 +1135,19 @@ angular.module('OpenSlidesApp.core.site', [ $scope.save(configOption, parent.value); }; + // For custom translations input + $scope.addTranslation = function (configOption, parent) { + parent.value.push({ + original: gettextCatalog.getString('New'), + translation: gettextCatalog.getString('New'), + }); + $scope.save(configOption, parent.value); + }; + $scope.removeTranslation = function (configOption, parent, index) { + parent.value.splice(index, 1); + $scope.save(configOption, parent.value); + }; + // For majority method angular.forEach( _.filter($scope.configGroups, function (configGroup) { @@ -1912,6 +1926,7 @@ angular.module('OpenSlidesApp.core.site', [ gettext('PDF footer logo'); gettext('Web interface header logo'); gettext('PDF ballot paper logo'); + gettext('Custom translations'); // Mark the string 'Default projector' here, because it does not appear in the templates. gettext('Default projector'); diff --git a/openslides/core/static/templates/config-form-field.html b/openslides/core/static/templates/config-form-field.html index 67a8cc8e4..d192c55fa 100644 --- a/openslides/core/static/templates/config-form-field.html +++ b/openslides/core/static/templates/config-form-field.html @@ -95,6 +95,41 @@ ng-options="option.value as option.display_name | translate for option in choices"> + +
+
+
+ + + +
+ + + +
+
+ +
+
+