From 7d22c0c5a56d6dea314ccd6a29d437ece686a993 Mon Sep 17 00:00:00 2001 From: joetsoi Date: Wed, 10 Jun 2015 13:48:22 +0100 Subject: [PATCH 1/9] [#959] add ITranslations interface This allows extensions to merge in their own translations with the main ckan mo file. Each extension should have an 'i18n' directory in the root directory of their repo containing its translations. The gettext domain (and hence filename) of the .mo files should be ckanext-{extname} --- ckan/lib/i18n.py | 16 ++++++++++++++-- ckan/lib/plugins.py | 28 ++++++++++++++++++++++++++++ ckan/plugins/interfaces.py | 20 ++++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/ckan/lib/i18n.py b/ckan/lib/i18n.py index 0d2d8e49499..eadf8d0a793 100644 --- a/ckan/lib/i18n.py +++ b/ckan/lib/i18n.py @@ -2,10 +2,14 @@ from babel import Locale, localedata from babel.core import LOCALE_ALIASES +from babel.support import Translations from pylons import config from pylons import i18n +import pylons import ckan.i18n +from ckan.plugins import PluginImplementations +from ckan.plugins.interfaces import ITranslations LOCALE_ALIASES['pt'] = 'pt_BR' # Default Portuguese language to # Brazilian territory, since @@ -121,9 +125,17 @@ def _set_lang(lang): if config.get('ckan.i18n_directory'): fake_config = {'pylons.paths': {'root': config['ckan.i18n_directory']}, 'pylons.package': config['pylons.package']} - i18n.set_lang(lang, pylons_config=fake_config) + i18n.set_lang(lang, pylons_config=fake_config, class_=Translations) else: - i18n.set_lang(lang) + i18n.set_lang(lang, class_=Translations) + + for plugin in PluginImplementations(ITranslations): + pylons.translator.merge(Translations.load( + dirname=plugin.directory(), + locales=plugin.locales(), + domain=plugin.domain() + )) + def handle_request(request, tmpl_context): ''' Set the language for the request ''' diff --git a/ckan/lib/plugins.py b/ckan/lib/plugins.py index abb7ffbeb4c..4939426685a 100644 --- a/ckan/lib/plugins.py +++ b/ckan/lib/plugins.py @@ -524,3 +524,31 @@ def activity_template(self): return 'organization/activity_stream.html' _default_organization_plugin = DefaultOrganizationForm() + + +class DefaultTranslation(object): + def directory(self): + '''Change the directory of the *.mo translation files + + The default implementation assumes the plugin is + ckanext/myplugin/plugin.py and the translations are stored in + i18n/ + ''' + import os + return os.path.abspath(os.path.join(os.path.dirname(__file__), + '../../', 'i18n')) + def locales(self): + '''Change the list of locales that this plugin handles + + By default the will assume any directory in subdirectory returned + by self.directory() is a locale handled by this plugin + ''' + import os + directory = self.directory() + return [ d for + d in os.listdir(directory) + if os.path.isdir(os.path.join(directory, d)) + ] + + def domain(self): + return 'ckanext-{name}'.format(name=self.name) diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py index bd151dbb279..5de911496f2 100644 --- a/ckan/plugins/interfaces.py +++ b/ckan/plugins/interfaces.py @@ -22,6 +22,7 @@ 'ITemplateHelpers', 'IFacets', 'IAuthenticator', + 'ITranslations', ] from inspect import isclass @@ -1432,3 +1433,22 @@ def abort(self, status_code, detail, headers, comment): '''called on abort. This allows aborts due to authorization issues to be overriden''' return (status_code, detail, headers, comment) + + +class ITranslations(Interface): + def directory(self): + '''Change the directory of the *.mo translation files + + The default implementation assumes the plugin is + ckanext/myplugin/plugin.py and the translations are stored in + i18n/ + ''' + def locales(self): + '''Change the list of locales that this plugin handles + + By default the will assume any directory in subdirectory returned + by self.directory() is a locale handled by this plugin + ''' + + def domain(self): + '''Change the gettext domain handled by this plugin''' From 5ffd13a36dc16dfbff76ba5bc29ab22e53c2f640 Mon Sep 17 00:00:00 2001 From: joetsoi Date: Mon, 15 Jun 2015 10:52:52 +0100 Subject: [PATCH 2/9] [#959] Add example ITranslation plugin --- ckan/lib/i18n.py | 4 +-- ckan/lib/plugins.py | 21 +++++++++++----- ckan/plugins/interfaces.py | 16 +++--------- ckanext/example_itranslation/__init__.py | 0 .../example_itranslation/babel_mapping.cfg | 5 ++++ .../i18n/ckanext-example_translation.pot | 23 ++++++++++++++++++ .../ckanext-example_itranslation.mo | Bin 0 -> 559 bytes .../ckanext-example_itranslation.po | 21 ++++++++++++++++ ckanext/example_itranslation/plugin.py | 11 +++++++++ .../templates/home/index.html | 5 ++++ .../example_itranslation/tests/__init__.py | 0 .../example_itranslation/tests/test_plugin.py | 5 ++++ setup.py | 1 + 13 files changed, 92 insertions(+), 20 deletions(-) create mode 100644 ckanext/example_itranslation/__init__.py create mode 100644 ckanext/example_itranslation/babel_mapping.cfg create mode 100644 ckanext/example_itranslation/i18n/ckanext-example_translation.pot create mode 100644 ckanext/example_itranslation/i18n/fr/LC_MESSAGES/ckanext-example_itranslation.mo create mode 100644 ckanext/example_itranslation/i18n/fr/LC_MESSAGES/ckanext-example_itranslation.po create mode 100644 ckanext/example_itranslation/plugin.py create mode 100644 ckanext/example_itranslation/templates/home/index.html create mode 100644 ckanext/example_itranslation/tests/__init__.py create mode 100644 ckanext/example_itranslation/tests/test_plugin.py diff --git a/ckan/lib/i18n.py b/ckan/lib/i18n.py index eadf8d0a793..8f58bdb2f9d 100644 --- a/ckan/lib/i18n.py +++ b/ckan/lib/i18n.py @@ -9,7 +9,7 @@ import ckan.i18n from ckan.plugins import PluginImplementations -from ckan.plugins.interfaces import ITranslations +from ckan.plugins.interfaces import ITranslation LOCALE_ALIASES['pt'] = 'pt_BR' # Default Portuguese language to # Brazilian territory, since @@ -129,7 +129,7 @@ def _set_lang(lang): else: i18n.set_lang(lang, class_=Translations) - for plugin in PluginImplementations(ITranslations): + for plugin in PluginImplementations(ITranslation): pylons.translator.merge(Translations.load( dirname=plugin.directory(), locales=plugin.locales(), diff --git a/ckan/lib/plugins.py b/ckan/lib/plugins.py index 4939426685a..b68f5ffa4b1 100644 --- a/ckan/lib/plugins.py +++ b/ckan/lib/plugins.py @@ -1,4 +1,6 @@ import logging +import os +import sys from pylons import c from ckan.lib import base @@ -534,16 +536,18 @@ def directory(self): ckanext/myplugin/plugin.py and the translations are stored in i18n/ ''' - import os - return os.path.abspath(os.path.join(os.path.dirname(__file__), - '../../', 'i18n')) + # assume plugin is called ckanext..<...>.PluginClass + extension_module_name = '.'.join(self.__module__.split('.')[0:2]) + module = sys.modules[extension_module_name] + return os.path.join(os.path.dirname(module.__file__), 'i18n') + def locales(self): '''Change the list of locales that this plugin handles - By default the will assume any directory in subdirectory returned - by self.directory() is a locale handled by this plugin + By default the will assume any directory in subdirectory in the + directory defined by self.directory() is a locale handled by this + plugin ''' - import os directory = self.directory() return [ d for d in os.listdir(directory) @@ -551,4 +555,9 @@ def locales(self): ] def domain(self): + '''Change the gettext domain handled by this plugin + + This implementation assumes the gettext domain is + ckanext-{extension name}, hence your pot, po and mo files should be + named ckanext-{extension name}.mo''' return 'ckanext-{name}'.format(name=self.name) diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py index 5de911496f2..696f316c5fa 100644 --- a/ckan/plugins/interfaces.py +++ b/ckan/plugins/interfaces.py @@ -22,7 +22,7 @@ 'ITemplateHelpers', 'IFacets', 'IAuthenticator', - 'ITranslations', + 'ITranslation', ] from inspect import isclass @@ -1435,20 +1435,12 @@ def abort(self, status_code, detail, headers, comment): return (status_code, detail, headers, comment) -class ITranslations(Interface): +class ITranslation(Interface): def directory(self): - '''Change the directory of the *.mo translation files + '''Change the directory of the *.mo translation files''' - The default implementation assumes the plugin is - ckanext/myplugin/plugin.py and the translations are stored in - i18n/ - ''' def locales(self): - '''Change the list of locales that this plugin handles - - By default the will assume any directory in subdirectory returned - by self.directory() is a locale handled by this plugin - ''' + '''Change the list of locales that this plugin handles ''' def domain(self): '''Change the gettext domain handled by this plugin''' diff --git a/ckanext/example_itranslation/__init__.py b/ckanext/example_itranslation/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ckanext/example_itranslation/babel_mapping.cfg b/ckanext/example_itranslation/babel_mapping.cfg new file mode 100644 index 00000000000..0896d123e89 --- /dev/null +++ b/ckanext/example_itranslation/babel_mapping.cfg @@ -0,0 +1,5 @@ +[extractors] +ckan = ckan.lib.extract:extract_ckan + +[ckan: **/templates/**.html] +encoding = utf-8 diff --git a/ckanext/example_itranslation/i18n/ckanext-example_translation.pot b/ckanext/example_itranslation/i18n/ckanext-example_translation.pot new file mode 100644 index 00000000000..5b69c3dde1d --- /dev/null +++ b/ckanext/example_itranslation/i18n/ckanext-example_translation.pot @@ -0,0 +1,23 @@ +# Translations template for PROJECT. +# Copyright (C) 2015 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2015. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2015-06-14 21:14+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: templates/home/index.html:4 +msgid "This is an untranslated string" +msgstr "" + diff --git a/ckanext/example_itranslation/i18n/fr/LC_MESSAGES/ckanext-example_itranslation.mo b/ckanext/example_itranslation/i18n/fr/LC_MESSAGES/ckanext-example_itranslation.mo new file mode 100644 index 0000000000000000000000000000000000000000..892133141ef4f31c2f1c0659f5a68a9af4a1631a GIT binary patch literal 559 zcmZuu%TB{E5DX7-$dNOkWNFr;>6J3w#z1i!~u@BwT9 zsUTQ+>|JT>S?|29ufBR1D~Jt5y()eL!+Z^qAhr-0;$_M6ES{T)cg(AM>&v(?or6?f zb|)?1;tERT3|F?`PK?1iBUSDVjJ_8mM|xv&BaD?=Q5dBC%ebAvO`HyU{VP8eGi@A6 zY%a&7RpyptnnIj3d+fZ~>7?;+=nwiCX&b>EzCb#tK%o{!2P8ZsQ5|X#jp}=06oyD7 zsd$u?(*L}JFCOF8k?gx)Z76iF8H1~462s{)=`){km6tphBoll}VPfEvvGYO(+T>^c zpePM5NLQPwrJ!b?rESzs;45wHDA;a5wCg9mBwkD*3PQiFl@kj4n9uM-PCUE)S;3_` zfzcf|R=7s#CgfJiYU++EuzzUx9 literal 0 HcmV?d00001 diff --git a/ckanext/example_itranslation/i18n/fr/LC_MESSAGES/ckanext-example_itranslation.po b/ckanext/example_itranslation/i18n/fr/LC_MESSAGES/ckanext-example_itranslation.po new file mode 100644 index 00000000000..57595f017fd --- /dev/null +++ b/ckanext/example_itranslation/i18n/fr/LC_MESSAGES/ckanext-example_itranslation.po @@ -0,0 +1,21 @@ +# This is an example translations file +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2015-06-14 21:14+0100\n" +"PO-Revision-Date: 2015-06-14 21:15+0100\n" +"Last-Translator: FULL NAME \n" +"Language-Team: fr \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +msgid "This is an untranslated string" +msgstr "This is a itranslated string" + +msgid "Log in" +msgstr "Overwritten string in ckan.mo" diff --git a/ckanext/example_itranslation/plugin.py b/ckanext/example_itranslation/plugin.py new file mode 100644 index 00000000000..c893ca1feef --- /dev/null +++ b/ckanext/example_itranslation/plugin.py @@ -0,0 +1,11 @@ +from ckan import plugins +from ckan.plugins import toolkit +from ckan.lib.plugins import DefaultTranslation + + +class ExampleITranslationPlugin(plugins.SingletonPlugin, DefaultTranslation): + plugins.implements(plugins.ITranslation) + plugins.implements(plugins.IConfigurer) + + def update_config(self, config): + toolkit.add_template_directory(config, 'templates') diff --git a/ckanext/example_itranslation/templates/home/index.html b/ckanext/example_itranslation/templates/home/index.html new file mode 100644 index 00000000000..b7b7c4a8753 --- /dev/null +++ b/ckanext/example_itranslation/templates/home/index.html @@ -0,0 +1,5 @@ +{% ckan_extends %} + +{% block primary_content %} +{% trans %}This is an untranslated string{% endtrans %} +{% endblock %} diff --git a/ckanext/example_itranslation/tests/__init__.py b/ckanext/example_itranslation/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ckanext/example_itranslation/tests/test_plugin.py b/ckanext/example_itranslation/tests/test_plugin.py new file mode 100644 index 00000000000..e22d21cb12b --- /dev/null +++ b/ckanext/example_itranslation/tests/test_plugin.py @@ -0,0 +1,5 @@ +"""Tests for plugin.py.""" +import ckanext.example_itranslation.plugin as plugin + +def test_plugin(): + pass diff --git a/setup.py b/setup.py index 2d48b908d99..26dbac762cd 100644 --- a/setup.py +++ b/setup.py @@ -127,6 +127,7 @@ 'example_iresourcecontroller = ckanext.example_iresourcecontroller.plugin:ExampleIResourceControllerPlugin', 'example_ivalidators = ckanext.example_ivalidators.plugin:ExampleIValidatorsPlugin', 'example_iconfigurer = ckanext.example_iconfigurer.plugin:ExampleIConfigurerPlugin', + 'example_itranslation = ckanext.example_itranslation.plugin:ExampleITranslationPlugin', ], 'ckan.system_plugins': [ 'domain_object_mods = ckan.model.modification:DomainObjectModificationExtension', From 4bb5b4291eaf8bd55316751c791b81d423cf3d2b Mon Sep 17 00:00:00 2001 From: joetsoi Date: Mon, 15 Jun 2015 12:03:21 +0100 Subject: [PATCH 3/9] [#959] Tests for example_itranslation plugin --- .../example_itranslation/tests/test_plugin.py | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/ckanext/example_itranslation/tests/test_plugin.py b/ckanext/example_itranslation/tests/test_plugin.py index e22d21cb12b..49256531fce 100644 --- a/ckanext/example_itranslation/tests/test_plugin.py +++ b/ckanext/example_itranslation/tests/test_plugin.py @@ -1,5 +1,34 @@ -"""Tests for plugin.py.""" -import ckanext.example_itranslation.plugin as plugin +from ckan import plugins +from ckan.tests import helpers -def test_plugin(): - pass +from nose.tools import assert_true, assert_false + + +class TestExampleITranslationPlugin(helpers.FunctionalTestBase): + @classmethod + def setup_class(cls): + super(TestExampleITranslationPlugin, cls).setup_class() + plugins.load('example_itranslation') + + @classmethod + def teardown_class(cls): + plugins.unload('example_itranslation') + super(TestExampleITranslationPlugin, cls).teardown_class() + + def test_translated_string_in_extensions_templates(self): + app = self._get_test_app() + response = app.get( + url=plugins.toolkit.url_for(controller='home', action='index', + locale='fr'), + ) + assert_true('This is a itranslated string' in response.body) + assert_false('This is a untranslated string' in response.body) + + def test_translated_string_in_core_templates(self): + app = self._get_test_app() + response = app.get( + url=plugins.toolkit.url_for(controller='home', action='index', + locale='fr'), + ) + assert_true('Overwritten string in ckan.mo' in response.body) + assert_false('Connexion' in response.body) From 0e9dfaac9648835620042dfb99b3bac2c3478e3d Mon Sep 17 00:00:00 2001 From: joetsoi Date: Mon, 15 Jun 2015 12:08:27 +0100 Subject: [PATCH 4/9] [#959] Add double check for example_itranslation tests --- .../example_itranslation/tests/test_plugin.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ckanext/example_itranslation/tests/test_plugin.py b/ckanext/example_itranslation/tests/test_plugin.py index 49256531fce..15fb8bf7e53 100644 --- a/ckanext/example_itranslation/tests/test_plugin.py +++ b/ckanext/example_itranslation/tests/test_plugin.py @@ -22,7 +22,14 @@ def test_translated_string_in_extensions_templates(self): locale='fr'), ) assert_true('This is a itranslated string' in response.body) - assert_false('This is a untranslated string' in response.body) + assert_false('This is an untranslated string' in response.body) + + # double check the untranslated strings + response = app.get( + url=plugins.toolkit.url_for(controller='home', action='index'), + ) + assert_true('This is an untranslated string' in response.body) + assert_false('This is a itranslated string' in response.body) def test_translated_string_in_core_templates(self): app = self._get_test_app() @@ -32,3 +39,10 @@ def test_translated_string_in_core_templates(self): ) assert_true('Overwritten string in ckan.mo' in response.body) assert_false('Connexion' in response.body) + + # double check the untranslated strings + response = app.get( + url=plugins.toolkit.url_for(controller='home', action='index'), + ) + assert_true('Log in' in response.body) + assert_false('Overwritten string in ckan.mo' in response.body) From 93d3a542e28cbfe0f729fa9cfafae7a618d85ba1 Mon Sep 17 00:00:00 2001 From: joetsoi Date: Tue, 16 Jun 2015 14:34:00 +0100 Subject: [PATCH 5/9] [#959] ITranslation interface fix Only add translations strings for the current locale (instead of all). Allow 'en' translation strings in extensions to overwrite core strings. --- ckan/lib/i18n.py | 28 +++++++++++++++---- .../example_itranslation/tests/test_plugin.py | 18 +++++++++++- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/ckan/lib/i18n.py b/ckan/lib/i18n.py index 8f58bdb2f9d..eecfc60b7c0 100644 --- a/ckan/lib/i18n.py +++ b/ckan/lib/i18n.py @@ -1,4 +1,5 @@ import os +import gettext from babel import Locale, localedata from babel.core import LOCALE_ALIASES @@ -129,12 +130,6 @@ def _set_lang(lang): else: i18n.set_lang(lang, class_=Translations) - for plugin in PluginImplementations(ITranslation): - pylons.translator.merge(Translations.load( - dirname=plugin.directory(), - locales=plugin.locales(), - domain=plugin.domain() - )) def handle_request(request, tmpl_context): @@ -143,6 +138,27 @@ def handle_request(request, tmpl_context): config.get('ckan.locale_default', 'en') if lang != 'en': set_lang(lang) + + for plugin in PluginImplementations(ITranslation): + if lang in plugin.locales(): + translator = Translations.load( + dirname=plugin.directory(), + locales=lang, + domain=plugin.domain() + ) + try: + pylons.translator.merge(translator) + except AttributeError: + # this occurs when an extension has 'en' translations that + # replace the default strings. As set_lang has not been run, + # pylons.translation is the NullTranslation, so we have to + # replace the StackedObjectProxy ourselves manually. + environ = pylons.request.environ + environ['pylons.pylons'].translator = translator + if 'paste.registry' in environ: + environ['paste.registry'].replace(pylons.translator, + translator) + tmpl_context.language = lang return lang diff --git a/ckanext/example_itranslation/tests/test_plugin.py b/ckanext/example_itranslation/tests/test_plugin.py index 15fb8bf7e53..71ed77fdf6c 100644 --- a/ckanext/example_itranslation/tests/test_plugin.py +++ b/ckanext/example_itranslation/tests/test_plugin.py @@ -31,6 +31,14 @@ def test_translated_string_in_extensions_templates(self): assert_true('This is an untranslated string' in response.body) assert_false('This is a itranslated string' in response.body) + # check that we have only overwritten 'fr' + response = app.get( + url=plugins.toolkit.url_for(controller='home', action='index', + locale='es'), + ) + assert_true('This is an untranslated string' in response.body) + assert_false('This is a itranslated string' in response.body) + def test_translated_string_in_core_templates(self): app = self._get_test_app() response = app.get( @@ -39,10 +47,18 @@ def test_translated_string_in_core_templates(self): ) assert_true('Overwritten string in ckan.mo' in response.body) assert_false('Connexion' in response.body) - + # double check the untranslated strings response = app.get( url=plugins.toolkit.url_for(controller='home', action='index'), ) assert_true('Log in' in response.body) assert_false('Overwritten string in ckan.mo' in response.body) + + # check that we have only overwritten 'fr' + response = app.get( + url=plugins.toolkit.url_for(controller='home', action='index', + locale='de'), + ) + assert_true('Einloggen' in response.body) + assert_false('Overwritten string in ckan.mo' in response.body) From 4f199d1a1c97b7ee318209e942bab90e45565350 Mon Sep 17 00:00:00 2001 From: joetsoi Date: Tue, 16 Jun 2015 14:55:49 +0100 Subject: [PATCH 6/9] [#959] extra 18n directory, rename interface names Add config options 'ckan.i18n.extra_directory' 'ckan.i18n.extra_gettext_domain' 'ckan.i18n.extra_locales' 'ckan.i18n.extra_directory' should be a fully qualified path 'ckan.i18n.extra_gettext_domain' should be the filename of mo files 'ckan.i18n.extra_locales' should be a list for each locale handled Rename the interfaces method names to reduce the risk of clashing 'domain' and 'directory' are too generic and might be used by other plugins in the future and there is no indication as a plugin writer that these methods are related to the ITranslation interface. So they have been changed to 'i18n_domain' etc. --- ckan/lib/i18n.py | 45 +++++++++++++++++++++++--------------- ckan/lib/plugins.py | 8 +++---- ckan/plugins/interfaces.py | 6 ++--- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/ckan/lib/i18n.py b/ckan/lib/i18n.py index eecfc60b7c0..a895a0910d0 100644 --- a/ckan/lib/i18n.py +++ b/ckan/lib/i18n.py @@ -4,6 +4,7 @@ from babel import Locale, localedata from babel.core import LOCALE_ALIASES from babel.support import Translations +from paste.deploy.converters import aslist from pylons import config from pylons import i18n import pylons @@ -139,29 +140,37 @@ def handle_request(request, tmpl_context): if lang != 'en': set_lang(lang) + extra_directory = config.get('ckan..i18n.extra_directory') + extra_domain = config.get('ckan.i18n.extra_gettext_domain') + extra_locales = aslist(config.get('ckan.i18n.extra_locales')) + if lang in extra_locales: + _add_extra_translations(extra_directory, lang, extra_domain) + for plugin in PluginImplementations(ITranslation): - if lang in plugin.locales(): - translator = Translations.load( - dirname=plugin.directory(), - locales=lang, - domain=plugin.domain() - ) - try: - pylons.translator.merge(translator) - except AttributeError: - # this occurs when an extension has 'en' translations that - # replace the default strings. As set_lang has not been run, - # pylons.translation is the NullTranslation, so we have to - # replace the StackedObjectProxy ourselves manually. - environ = pylons.request.environ - environ['pylons.pylons'].translator = translator - if 'paste.registry' in environ: - environ['paste.registry'].replace(pylons.translator, - translator) + if lang in plugin.i18n_locales(): + _add_extra_translations(plugin.i18n_directory(), lang, + plugin.i18n_domain()) tmpl_context.language = lang return lang +def _add_extra_translations(dirname, locales, domain): + translator = Translations.load(dirname=dirname, locales=locales, + domain=domain) + try: + pylons.translator.merge(translator) + except AttributeError: + # this occurs when an extension has 'en' translations that + # replace the default strings. As set_lang has not been run, + # pylons.translation is the NullTranslation, so we have to + # replace the StackedObjectProxy ourselves manually. + environ = pylons.request.environ + environ['pylons.pylons'].translator = translator + if 'paste.registry' in environ: + environ['paste.registry'].replace(pylons.translator, + translator) + + def get_lang(): ''' Returns the current language. Based on babel.i18n.get_lang but works when set_lang has not been run (i.e. still in English). ''' diff --git a/ckan/lib/plugins.py b/ckan/lib/plugins.py index b68f5ffa4b1..40495e01532 100644 --- a/ckan/lib/plugins.py +++ b/ckan/lib/plugins.py @@ -529,7 +529,7 @@ def activity_template(self): class DefaultTranslation(object): - def directory(self): + def i18n_directory(self): '''Change the directory of the *.mo translation files The default implementation assumes the plugin is @@ -541,20 +541,20 @@ def directory(self): module = sys.modules[extension_module_name] return os.path.join(os.path.dirname(module.__file__), 'i18n') - def locales(self): + def i18n_locales(self): '''Change the list of locales that this plugin handles By default the will assume any directory in subdirectory in the directory defined by self.directory() is a locale handled by this plugin ''' - directory = self.directory() + directory = self.i18n_directory() return [ d for d in os.listdir(directory) if os.path.isdir(os.path.join(directory, d)) ] - def domain(self): + def i18n_domain(self): '''Change the gettext domain handled by this plugin This implementation assumes the gettext domain is diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py index 696f316c5fa..e34f4b5b674 100644 --- a/ckan/plugins/interfaces.py +++ b/ckan/plugins/interfaces.py @@ -1436,11 +1436,11 @@ def abort(self, status_code, detail, headers, comment): class ITranslation(Interface): - def directory(self): + def i18n_directory(self): '''Change the directory of the *.mo translation files''' - def locales(self): + def i18n_locales(self): '''Change the list of locales that this plugin handles ''' - def domain(self): + def i18n_domain(self): '''Change the gettext domain handled by this plugin''' From c135d4c83ea0333ac6d38e352e19aaef5b0b8cc7 Mon Sep 17 00:00:00 2001 From: joetsoi Date: Tue, 16 Jun 2015 15:16:32 +0100 Subject: [PATCH 7/9] [#959]ITranslations, check config options and docs --- ckan/lib/i18n.py | 7 +++-- doc/maintaining/configuration.rst | 46 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/ckan/lib/i18n.py b/ckan/lib/i18n.py index a895a0910d0..a869ecffc0b 100644 --- a/ckan/lib/i18n.py +++ b/ckan/lib/i18n.py @@ -140,11 +140,12 @@ def handle_request(request, tmpl_context): if lang != 'en': set_lang(lang) - extra_directory = config.get('ckan..i18n.extra_directory') + extra_directory = config.get('ckan.i18n.extra_directory') extra_domain = config.get('ckan.i18n.extra_gettext_domain') extra_locales = aslist(config.get('ckan.i18n.extra_locales')) - if lang in extra_locales: - _add_extra_translations(extra_directory, lang, extra_domain) + if extra_directory and extra_domain and extra_locales: + if lang in extra_locales: + _add_extra_translations(extra_directory, lang, extra_domain) for plugin in PluginImplementations(ITranslation): if lang in plugin.i18n_locales(): diff --git a/doc/maintaining/configuration.rst b/doc/maintaining/configuration.rst index 1a60b115833..e8e9593f852 100644 --- a/doc/maintaining/configuration.rst +++ b/doc/maintaining/configuration.rst @@ -1492,6 +1492,52 @@ Default value: (none) By default, the locales are searched for in the ``ckan/i18n`` directory. Use this option if you want to use another folder. +.. _ckan.18n.extra_directory: + +ckan.i18n.extra_directory +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Example:: + + ckan.18n.extra_directory = /opt/ckan/extra_translations/ + +Default value: (none) + +If you wish to add extra translation strings and have them merged with the +default ckan translations at runtime you can specify the location of the extra +translations using this option. + +.. _ckan.18n.extra_gettext_domain: + +ckan.i18n.extra_gettext_domain +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Example:: + + ckan.18n.extra_gettext_domain = mydomain + +Default value: (none) + +You can specify the name of the gettext domain of the extra translations. For +example if your translations are stored as +``i18n//LC_MESSAGES/somedomain.mo`` you would want to set this option +to ``somedomain`` + +.. _ckan.18n.extra_locales: + +ckan.18n.extra_locales +^^^^^^^^^^^^^^^^^^^^^^ + +Example:: + + ckan.18n.extra_locales = fr es de + +Default value: (none) + +If you have set an extra i18n directory using ``ckan.18n.extra_directory``, you +should specify the locales that have been translated in that directory in this +option. + .. _ckan.root_path: ckan.root_path From 10b8db8f387cb518b27748250ed2c5319934e2f2 Mon Sep 17 00:00:00 2001 From: joetsoi Date: Tue, 16 Jun 2015 16:15:09 +0100 Subject: [PATCH 8/9] [#959] example_itranslation test for 'en' string replacements Add a test for checking that 'en' replacement strings which are handled as a special case work --- ...n.pot => ckanext-example_itranslation.pot} | 0 .../ckanext-example_itranslation.mo | Bin 0 -> 542 bytes .../ckanext-example_itranslation.po | 25 ++++++++++++++++++ .../ckanext-example_translation.po | 25 ++++++++++++++++++ .../example_itranslation/tests/test_plugin.py | 8 ++++++ 5 files changed, 58 insertions(+) rename ckanext/example_itranslation/i18n/{ckanext-example_translation.pot => ckanext-example_itranslation.pot} (100%) create mode 100644 ckanext/example_itranslation/i18n/en/LC_MESSAGES/ckanext-example_itranslation.mo create mode 100644 ckanext/example_itranslation/i18n/en/LC_MESSAGES/ckanext-example_itranslation.po create mode 100644 ckanext/example_itranslation/i18n/en/LC_MESSAGES/ckanext-example_translation.po diff --git a/ckanext/example_itranslation/i18n/ckanext-example_translation.pot b/ckanext/example_itranslation/i18n/ckanext-example_itranslation.pot similarity index 100% rename from ckanext/example_itranslation/i18n/ckanext-example_translation.pot rename to ckanext/example_itranslation/i18n/ckanext-example_itranslation.pot diff --git a/ckanext/example_itranslation/i18n/en/LC_MESSAGES/ckanext-example_itranslation.mo b/ckanext/example_itranslation/i18n/en/LC_MESSAGES/ckanext-example_itranslation.mo new file mode 100644 index 0000000000000000000000000000000000000000..9cb73ce133a2418ed1b3086dd0e3673553f7b955 GIT binary patch literal 542 zcmah`O;5ux3@w5K>X9=-<_1C;?X(RD46%=bhFUgtW%sa}5>%2YNfF%m6Z}1X23K$a ziH{3bp8RY**^d3Pw({(eEfdxWtvdK&G4WMGLf9b82v19%SCOytuf*&88+k9qLRyE$ z%Wk9vdO|^^oDs^F!eI{98L0|yV)QM}9UJ9rhQ>;*I84&3Z0!VL5Rj0i zcrWWte{~%Q+q}K^sf&emEc=>Q8xE(JaSRu|B!Z(~t&ja!sG<@DvJAx?2UKtr$0wz1 zXj2^fld>|RWT)EPSq^IP5!!C^5I$*lOTlIr!fihpCDHd5!bac^v~s9uj>iRk$l-(A zU6ewqLzv$PW6^ahcg^;{b@ja0Xjr7?I;Z_|*uRp_KSPD4UOVeQa_EaIEFowdG&(*V LiYm@A|L;)m?T?x9 literal 0 HcmV?d00001 diff --git a/ckanext/example_itranslation/i18n/en/LC_MESSAGES/ckanext-example_itranslation.po b/ckanext/example_itranslation/i18n/en/LC_MESSAGES/ckanext-example_itranslation.po new file mode 100644 index 00000000000..469f7133034 --- /dev/null +++ b/ckanext/example_itranslation/i18n/en/LC_MESSAGES/ckanext-example_itranslation.po @@ -0,0 +1,25 @@ +# English translations for PROJECT. +# Copyright (C) 2015 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2015. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2015-06-14 21:14+0100\n" +"PO-Revision-Date: 2015-06-16 15:57+0100\n" +"Last-Translator: FULL NAME \n" +"Language-Team: en \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: templates/home/index.html:4 +msgid "This is an untranslated string" +msgstr "" + +msgid "Register" +msgstr "Replaced" diff --git a/ckanext/example_itranslation/i18n/en/LC_MESSAGES/ckanext-example_translation.po b/ckanext/example_itranslation/i18n/en/LC_MESSAGES/ckanext-example_translation.po new file mode 100644 index 00000000000..469f7133034 --- /dev/null +++ b/ckanext/example_itranslation/i18n/en/LC_MESSAGES/ckanext-example_translation.po @@ -0,0 +1,25 @@ +# English translations for PROJECT. +# Copyright (C) 2015 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2015. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2015-06-14 21:14+0100\n" +"PO-Revision-Date: 2015-06-16 15:57+0100\n" +"Last-Translator: FULL NAME \n" +"Language-Team: en \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: templates/home/index.html:4 +msgid "This is an untranslated string" +msgstr "" + +msgid "Register" +msgstr "Replaced" diff --git a/ckanext/example_itranslation/tests/test_plugin.py b/ckanext/example_itranslation/tests/test_plugin.py index 71ed77fdf6c..d9598461591 100644 --- a/ckanext/example_itranslation/tests/test_plugin.py +++ b/ckanext/example_itranslation/tests/test_plugin.py @@ -62,3 +62,11 @@ def test_translated_string_in_core_templates(self): ) assert_true('Einloggen' in response.body) assert_false('Overwritten string in ckan.mo' in response.body) + + def test_english_translation_replaces_default_english_string(self): + app = self._get_test_app() + response = app.get( + url=plugins.toolkit.url_for(controller='home', action='index'), + ) + assert_true('Replaced' in response.body) + assert_false('Register' in response.body) From 2dbd53e59474a56186f85474c51c36710af2a1bf Mon Sep 17 00:00:00 2001 From: joetsoi Date: Tue, 15 Sep 2015 09:21:15 +0100 Subject: [PATCH 9/9] [#2461] Add custom translation directory after plugins --- ckan/lib/i18n.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ckan/lib/i18n.py b/ckan/lib/i18n.py index a869ecffc0b..033b86eeec9 100644 --- a/ckan/lib/i18n.py +++ b/ckan/lib/i18n.py @@ -140,6 +140,12 @@ def handle_request(request, tmpl_context): if lang != 'en': set_lang(lang) + + for plugin in PluginImplementations(ITranslation): + if lang in plugin.i18n_locales(): + _add_extra_translations(plugin.i18n_directory(), lang, + plugin.i18n_domain()) + extra_directory = config.get('ckan.i18n.extra_directory') extra_domain = config.get('ckan.i18n.extra_gettext_domain') extra_locales = aslist(config.get('ckan.i18n.extra_locales')) @@ -147,11 +153,6 @@ def handle_request(request, tmpl_context): if lang in extra_locales: _add_extra_translations(extra_directory, lang, extra_domain) - for plugin in PluginImplementations(ITranslation): - if lang in plugin.i18n_locales(): - _add_extra_translations(plugin.i18n_directory(), lang, - plugin.i18n_domain()) - tmpl_context.language = lang return lang