Skip to content

Commit

Permalink
Merge pull request #2461 from joetsoi/itranslation-interface
Browse files Browse the repository at this point in the history
add ITranslations interface
  • Loading branch information
wardi committed Sep 15, 2015
2 parents 50f5e37 + 2d26a84 commit 76f2c48
Show file tree
Hide file tree
Showing 17 changed files with 324 additions and 2 deletions.
43 changes: 41 additions & 2 deletions ckan/lib/i18n.py
@@ -1,11 +1,17 @@
import os
import gettext

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

import ckan.i18n
from ckan.plugins import PluginImplementations
from ckan.plugins.interfaces import ITranslation

LOCALE_ALIASES['pt'] = 'pt_BR' # Default Portuguese language to
# Brazilian territory, since
Expand Down Expand Up @@ -121,19 +127,52 @@ 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)



def handle_request(request, tmpl_context):
''' Set the language for the request '''
lang = request.environ.get('CKAN_LANG') or \
config.get('ckan.locale_default', 'en')
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'))
if extra_directory and extra_domain and extra_locales:
if lang in extra_locales:
_add_extra_translations(extra_directory, lang, extra_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). '''
Expand Down
37 changes: 37 additions & 0 deletions ckan/lib/plugins.py
@@ -1,4 +1,6 @@
import logging
import os
import sys

from pylons import c
from ckan.lib import base
Expand Down Expand Up @@ -522,3 +524,38 @@ def activity_template(self):
return 'organization/activity_stream.html'

_default_organization_plugin = DefaultOrganizationForm()


class DefaultTranslation(object):
def i18n_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/
'''
# assume plugin is called ckanext.<myplugin>.<...>.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 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.i18n_directory()
return [ d for
d in os.listdir(directory)
if os.path.isdir(os.path.join(directory, d))
]

def i18n_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)
12 changes: 12 additions & 0 deletions ckan/plugins/interfaces.py
Expand Up @@ -22,6 +22,7 @@
'ITemplateHelpers',
'IFacets',
'IAuthenticator',
'ITranslation',
'IUploader'
]

Expand Down Expand Up @@ -1462,6 +1463,17 @@ def abort(self, status_code, detail, headers, comment):
return (status_code, detail, headers, comment)


class ITranslation(Interface):
def i18n_directory(self):
'''Change the directory of the *.mo translation files'''

def i18n_locales(self):
'''Change the list of locales that this plugin handles '''

def i18n_domain(self):
'''Change the gettext domain handled by this plugin'''


class IUploader(Interface):
'''
Extensions implementing this interface can provide custom uploaders to
Expand Down
Empty file.
5 changes: 5 additions & 0 deletions ckanext/example_itranslation/babel_mapping.cfg
@@ -0,0 +1,5 @@
[extractors]
ckan = ckan.lib.extract:extract_ckan

[ckan: **/templates/**.html]
encoding = utf-8
23 changes: 23 additions & 0 deletions ckanext/example_itranslation/i18n/ckanext-example_itranslation.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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 ""

Binary file not shown.
@@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language-Team: en <LL@li.org>\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"
@@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language-Team: en <LL@li.org>\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"
Binary file not shown.
@@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: fr <LL@li.org>\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"
11 changes: 11 additions & 0 deletions 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')
5 changes: 5 additions & 0 deletions ckanext/example_itranslation/templates/home/index.html
@@ -0,0 +1,5 @@
{% ckan_extends %}

{% block primary_content %}
{% trans %}This is an untranslated string{% endtrans %}
{% endblock %}
Empty file.
72 changes: 72 additions & 0 deletions ckanext/example_itranslation/tests/test_plugin.py
@@ -0,0 +1,72 @@
from ckan import plugins
from ckan.tests import helpers

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 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)

# 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(
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)

# 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)

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)
46 changes: 46 additions & 0 deletions doc/maintaining/configuration.rst
Expand Up @@ -1558,6 +1558,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/<locale>/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.display_timezone:

ckan.display_timezone
Expand Down

0 comments on commit 76f2c48

Please sign in to comment.