Edgewall Software

Changes between Version 24 and Version 25 of CookBook/PluginL10N


Ignore:
Timestamp:
Sep 25, 2012, 11:30:05 PM (12 years ago)
Author:
Christian Boos
Comment:

restructured the page quite a bit: start with setup.*, continue with Python, Genshi, Javascript. Config* support in the advanced topics section.

Legend:

Unmodified
Added
Removed
Modified
  • CookBook/PluginL10N

    v24 v25  
    11= Adding i18n/l10n to Trac plugins ^(Trac >= 0.12)^ =
     2[[PageOutline(2-5)]]
     3
    24== Intro and Motivation ==
    35Are you a user of Trac and do your work with it, nothing more? Well, you may ignore this page and go on reading another subpage of [wiki:CookBook CookBook].
     
    911Ultimately, all plugin maintainers and developers in general, who are facing requests and are willing to take care for growing demand of their plugin to speak same (foreign) language(s) as Trac >= 0.12 should, just read on.
    1012
    11 == i18n, l10n, ... help! ==
     13=== i18n, l10n, ... help!
    1214In short '''i18n''' stands for '''`i`'''`nternationalizatio`'''`n`''' (count 18 chars between i and n) and is defined as software design for programs with translation support. '''`l`'''`ocalisatio`'''`n`''' that is abbreviated as '''l10n''' could be seen as a follow-up process providing data for one or more locales. It is taking care of feature differences between the original/default (that is English is most cases including Trac) and a given locale as well. Such features are i.e. sentence structure including punctuation and formatting of numbers, date/time strings, and currencies. Once you did some ground work at the source (i18n), what's remaining is proper translation work (l10n), putting more or less effort in preserving the sense of the original while looking as native locale as possible.^[#a1 1]^
    1315
    1416'''NLS''' (National Language Support or Native Language Support) is meant to be the sum of both. And there are more related terms that we could safely skip for now.^[#a1 1], [#a2 2]^
    1517
    16 == Background and concept of i18n/l10n support for Trac plugins ==
     18=== Background and concept of i18n/l10n support for Trac plugins
    1719
    1820It begun with adding [Babel:] to Trac. Babel is a very powerful translation framework. For one part, it is a message extraction tool: it can extract messages from source code files (in our case, Python and Javascript) as well as from Genshi templates, and create catalog templates (`.pot` files). It can also create and update the message catalogs (`.po` files), and compile those catalogs (`.mo` files). For the other part, as a Python library used within Trac, it provides the implementation of the message retrieval functions (`gettext` and related). And there's even more to it than that, if you're interested please visit the Babel project (also here on edgewall.org).
     
    4749Now, a detailed walk-through...
    4850
    49 === Prepare plugin code ===
    50 ==== Import i18n/l10n helper programs ====
    51 Pick a reasonably unique name for the domain, e.g. ** 'foo' ** (if your plugin is named 'foo', that is).
    52 
    53 This will be the basename for the various translation catalog files
    54 (e.g. `foo/locale/fr/LC_MESSAGES/foo.po` for the French catalog).
    55 
    56 At run-time, the translation functions (typically `_(...)`) have to know in which catalog the translation will be found. Specifying the 'foo' domain in every such call would be tedious, that's why there's a facility for creating partially instantiated domain-aware translation functions: `domain_functions`.
    57 
    58 This helper function should be called at module load time, like this:
    59 {{{#!python
    60 from trac.util.translation import domain_functions
    61 
    62 _, tag_, N_, add_domain = \
    63     domain_functions('foo', ('_', 'tag_', 'N_', 'add_domain'))
    64 }}}
    65 
    66 The translation functions which can be bound to a domain are:
    67  - `'_'`: extract and translate
    68  - `'ngettext'`: extract and translate (singular, plural, num)
    69  - `'tgettext'`, `'tag_'`: same as `'_'` but for Markup
    70  - `'tngettext'`, `'tagn_'`: same as `'ngettext'` but for Markup
    71  - `'gettext'`: translate //only//, don't extract
    72  - `'N_'`: extract //only//, don't translate
    73  - `'add_domain'`: register the catalog file for the bound domain
    74 
    75 Note: `N_` and `gettext()` are usually used in tandem. For example, when you have a global dict containing strings that need to extracted, you want to mark those strings for extraction but you don't want to put their //translation// in the dict: use `N_("the string")`; when you later use that dict and want to retrieve the translation for the string corresponding to some key, you don't want to mark anything here: use `gettext(mydict.get(key))`.
    76 
    77 To inform Trac about where the plugin's message catalogs can be found, you'll have to call the `add_domain` function obtained via `domain_functions` as shown above. One place to do this is in the `__init__` function of your plugin's main component, like this:
    78 {{{#!python
    79     def __init__(self):
    80         import pkg_resources # here or with the other imports
    81         # bind the 'foo' catalog to the specified locale directory
    82         locale_dir = pkg_resources.resource_filename(__name__, 'locale')
    83         add_domain(self.env.path, locale_dir)
    84 }}}
    85 assuming that folder `locale` will reside in the same folder as the file containing the code above, referred to as `<path>` below (as can be observed inside the Python egg after packaging).
    86 
    87 The i18n/l10n helper functions are available inside the plugin now, but if the plugin code contains several python script files and you encounter text for translation in one of them too, you need to import the functions from the main script, say its name is `api.py`, there:
    88 {{{#!python
    89 from api import _, tag_, N_
    90 }}}
    91 
    92 ==== Preset configuration for Babel commands ====
    93 Add some lines to `setup.cfg` or, if it doesn't exist by now, create it with the following content:
    94 {{{#!ini
    95 [extract_messages]
    96 add_comments = TRANSLATOR:
    97 msgid_bugs_address =
    98 output_file = <path>/locale/messages.pot
    99 # Note: specify as 'keywords' the functions for which the messages
    100 #       should be extracted. This should match the list of functions
    101 #       that you've listed in the `domain_functions()` call above.
    102 keywords = _ N_ tag_
    103 # Other example:
    104 #keywords = _ ngettext:1,2 N_ tag_
    105 width = 72
    106 
    107 [init_catalog]
    108 input_file = <path>/locale/messages.pot
    109 output_dir = <path>/locale
    110 domain = foo
    111 
    112 [compile_catalog]
    113 directory = <path>/locale
    114 domain = foo
    115 
    116 [update_catalog]
    117 input_file = <path>/locale/messages.pot
    118 output_dir = <path>/locale
    119 domain = foo
    120 }}}
    121 Replace `<path>` as appropriate (i.e. the relative path to the folder containing the `locale` directory, for example `mytracplugin`).
    122 
    123 This will tell Babel where to look for and store message catalog files.
    124 
    125 
    126 In the `extract_messages` section there is just one more lines you may like to change: `msgid_bugs_address`. To allow for direct feedback regarding your i18n work add a valid e-mail address or a mailing list dedicated to translation issues there.
    127 
    128 The `add_comments` line simply lists the tags in the comments surrounding the calls to the translation functions in the source code that have to be propagated to the catalogs (see [Babel:wiki:Documentation/0.9/setup.html#extract-messages extract_messages] in Babel's documentation). So you will want to leave that one untouched.
    129 
    130 ==== Mark text for extraction ====
    131 In python scripts you'll have to wrap text with the translation function `_()` to get it handled by translation helper programs.
    132 {{{#!diff
    133 --- a/<path>/api.py
    134 +++ b/<path>/api.py
    135 @@ -1,1 +1,1 @@
    136 -    msg = 'This is a msg text.'
    137 +    msg = _("This is a msg text.")
    138 }}}
    139 
    140 Note, that quoting of (i18n) message texts should really be done in double quotes. Single quotes are reserved for string constants (see commit note for r9751).
    141 
    142 This is a somewhat time consuming task depending on the size of the plugin's code. If you initially fail to find all desired texts you may notice this by missing them from the message catalog later and come back to this step again. If the plugin maintainer is unaware of your i18n work or unwilling to support it and he adds more message without the translation function call, remember that you have to do the wrapping of these new texts too.
    143 
    144 ==== Text extraction from Python code and Genshi templates ====
    145 Message extraction for Genshi templates should be done auto-magically. However there is the markup `i18n:msg` available to ensure extraction even from less common tags. For a real-world example have a look at [changeset:9542/trunk/trac/ticket/templates/ Trac SVN changeset r9542] for marking previously undetected text in templates.
    146 
    147 See Genshi documentation on this topic, [http://genshi.edgewall.org/wiki/Documentation/0.6.x/i18n.html Internationalization and Localization].
    148 
    149 
    150 ===== Extraction
     51=== Enable Babel support for your plugin ===
     52==== Add Babel commands to the setup (`setup.py`)
    15153
    15254Babel only does extract from Python scripts by default. To extract messages from Genshi templates as well, you'll have to declare the needed extractors in [=#setup `setup.py`]:
     
    19698}}}
    19799
    198 Note: Trac 1.0 added support for a special kind of `N_` marker, `cleandoc_`, which can be used to reformat multiline messages in a compact form. There's also support to apply this "cleandoc" transformation to the documentation of instances of `trac.config.Option` and its subclasses. However, this support is coming from a special Python extractor which has to be used instead of the default Python extractor from Babel.
     100
     101==== Preset configuration for Babel commands (`setup.cfg`) ====
     102Add some lines to `setup.cfg` or, if it doesn't exist by now, create it with the following content:
     103{{{#!ini
     104[extract_messages]
     105add_comments = TRANSLATOR:
     106msgid_bugs_address =
     107output_file = <path>/locale/messages.pot
     108# Note: specify as 'keywords' the functions for which the messages
     109#       should be extracted. This should match the list of functions
     110#       that you've listed in the `domain_functions()` call above.
     111keywords = _ N_ tag_
     112# Other example:
     113#keywords = _ ngettext:1,2 N_ tag_
     114width = 72
     115
     116[init_catalog]
     117input_file = <path>/locale/messages.pot
     118output_dir = <path>/locale
     119domain = foo
     120
     121[compile_catalog]
     122directory = <path>/locale
     123domain = foo
     124
     125[update_catalog]
     126input_file = <path>/locale/messages.pot
     127output_dir = <path>/locale
     128domain = foo
     129}}}
     130Replace `<path>` as appropriate (i.e. the relative path to the folder containing the `locale` directory, for example `mytracplugin`).
     131
     132This will tell Babel where to look for and store message catalog files.
     133
     134
     135In the `extract_messages` section there is just one more lines you may like to change: `msgid_bugs_address`. To allow for direct feedback regarding your i18n work add a valid e-mail address or a mailing list dedicated to translation issues there.
     136
     137The `add_comments` line simply lists the tags in the comments surrounding the calls to the translation functions in the source code that have to be propagated to the catalogs (see [Babel:wiki:Documentation/0.9/setup.html#extract-messages extract_messages] in Babel's documentation). So you will want to leave that one untouched.
     138
     139==== Register message catalog files for packaging ====
     140To include the translated messages into the packaged plugin you need to add the path to the catalog files to `package_data` in the call for function `setup()` in `setup.py`:
     141{{{#!diff
     142diff -u a/setup.py b/setup.py
     143--- a/setup.py
     144+++ b/setup.py
     145@@ -39,6 +39,7 @@
     146     package_data = {
     147         <path>: [
     148             'htdocs/css/*.css',
     149+            'locale/*/LC_MESSAGES/*.mo',
     150         ],
     151     },
     152     entry_points = {
     153}}}
     154
     155
     156=== Make the Python code translation-aware
     157==== Prepare domain-specific translation helper functions ====
     158Pick a reasonably unique name for the domain, e.g. ** 'foo' ** (if your plugin is named 'foo', that is).
     159
     160This will be the basename for the various translation catalog files
     161(e.g. `foo/locale/fr/LC_MESSAGES/foo.po` for the French catalog).
     162
     163At run-time, the translation functions (typically `_(...)`) have to know in which catalog the translation will be found. Specifying the 'foo' domain in every such call would be tedious, that's why there's a facility for creating partially instantiated domain-aware translation functions: `domain_functions`.
     164
     165This helper function should be called at module load time, like this:
     166{{{#!python
     167from trac.util.translation import domain_functions
     168
     169_, tag_, N_, add_domain = \
     170    domain_functions('foo', ('_', 'tag_', 'N_', 'add_domain'))
     171}}}
     172
     173The translation functions which can be bound to a domain are:
     174 - `'_'`: extract and translate
     175 - `'ngettext'`: extract and translate (singular, plural, num)
     176 - `'tgettext'`, `'tag_'`: same as `'_'` but for Markup
     177 - `'tngettext'`, `'tagn_'`: same as `'ngettext'` but for Markup
     178 - `'gettext'`: translate //only//, don't extract
     179 - `'N_'`: extract //only//, don't translate
     180 - `'add_domain'`: register the catalog file for the bound domain
     181
     182Note: `N_` and `gettext()` are usually used in tandem. For example, when you have a global dict containing strings that need to extracted, you want to mark those strings for extraction but you don't want to put their //translation// in the dict: use `N_("the string")`; when you later use that dict and want to retrieve the translation for the string corresponding to some key, you don't want to mark anything here: use `gettext(mydict.get(key))`.
     183
     184To inform Trac about where the plugin's message catalogs can be found, you'll have to call the `add_domain` function obtained via `domain_functions` as shown above. One place to do this is in the `__init__` function of your plugin's main component, like this:
     185{{{#!python
     186    def __init__(self):
     187        import pkg_resources # here or with the other imports
     188        # bind the 'foo' catalog to the specified locale directory
     189        locale_dir = pkg_resources.resource_filename(__name__, 'locale')
     190        add_domain(self.env.path, locale_dir)
     191}}}
     192assuming that folder `locale` will reside in the same folder as the file containing the code above, referred to as `<path>` below (as can be observed inside the Python egg after packaging).
     193
     194The i18n/l10n helper functions are available inside the plugin now, but if the plugin code contains several python script files and you encounter text for translation in one of them too, you need to import the functions from the main script, say its name is `api.py`, there:
     195{{{#!python
     196from api import _, tag_, N_
     197}}}
     198
     199==== Mark text for extraction ====
     200In python scripts you'll have to wrap text with the translation function `_()` to get it handled by translation helper programs.
     201{{{#!diff
     202--- a/<path>/api.py
     203+++ b/<path>/api.py
     204@@ -1,1 +1,1 @@
     205-    msg = 'This is a msg text.'
     206+    msg = _("This is a msg text.")
     207}}}
     208
     209Note, that quoting of (i18n) message texts should really be done in double quotes. Single quotes are reserved for string constants (see commit note for r9751).
     210
     211This is a somewhat time consuming task depending on the size of the plugin's code. If you initially fail to find all desired texts you may notice this by missing them from the message catalog later and come back to this step again. If the plugin maintainer is unaware of your i18n work or unwilling to support it and he adds more message without the translation function call, remember that you have to do the wrapping of these new texts too.
     212
     213=== Make the Genshi templates translation-aware
     214
     215First, keep an eye on the Genshi documentation on this topic, [http://genshi.edgewall.org/wiki/Documentation/0.6.x/i18n.html Internationalization and Localization].
     216
     217==== Text extraction from Python code and Genshi templates ====
     218Message extraction for Genshi templates should be done auto-magically. However there is the markup `i18n:msg` available to ensure extraction even from less common tags. For a real-world example have a look at [changeset:9542/trunk/trac/ticket/templates/ Trac SVN changeset r9542] for marking previously undetected text in templates.
     219
     220==== Runtime support
     221Extraction is auto-magical, however message retrieval at runtime is not. You have to make sure you've specified the appropriate //domain// in your template, by adding a `i18n:domain` directive. Usually you would put it in the top-level element, next to the mandatory `xmlns:i18n` namespace declaration.
     222
     223For example:
     224{{{#!xml
     225<!DOCTYPE html
     226    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     227    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
     228<html xmlns="http://www.w3.org/1999/xhtml"
     229      xmlns:xi="http://www.w3.org/2001/XInclude"
     230      xmlns:py="http://genshi.edgewall.org/"
     231      xmlns:i18n="http://genshi.edgewall.org/i18n" i18n:domain="foo">
     232...
     233</html>
     234}}}
     235
     236=== Make the Javascript code translation-aware
     237==== Text extraction from Javascript code ==== #Javascript
     238
     239Adding support for translating the marked strings in the Javascript code is a bit more involved, but if you made it to this point, that shouldn't scare you away...
     240
     241We currently support only statically deployed Javascript files, which means they can't be translated like template files on the server, but that the translation has to happen dynamically on the client side. To this end, we want to send an additional `.js` file containing a dictionary of the messages that have to be translated, and only those. In order to clearly identify which strings have to be present in this dictionary, we'll extract the messages marked for translation (the usual `_(...)` ways) from the Javascript code into a dedicated catalog template, and from there, we'll create dedicated catalogs for each locale. In the end, the translations present in each compiled catalog will be extracted and placed into a `.js` file containing the messages dictionary and some setup code.
     242
     243The first change is to use `get_l10n_js_cmdclass` in lieu of `get_l10n_cmdclass`. The former adds a few more setup commands for extracting messages strings from Javascript `.js` files and `<script type="text/javascript">` snippets in `.html` files, initialization and updating of dedicated catalog files, and finally compiling that catalog and creating a `.js` file containing the dictionary of strings, ready to be used by the `babel.js` support code already present in Trac pages.
     244
     245The change to `setup.py` looks like this:
     246{{{#!diff
     247diff -u a/setup.py b/setup.py
     248--- a/setup.py
     249+++ b/setup.py
     250@@ -5,8 +5,8 @@ from setuptools import setup
     251 extra = {}
     252 
     253-from trac.util.dist import get_l10n_cmdclass
     254-cmdclass = get_l10n_cmdclass()
     255+from trac.util.dist import get_l10n_js_cmdclass
     256+cmdclass = get_l10n_js_cmdclass()
     257 if cmdclass:
     258     extra['cmdclass'] = cmdclass
     259     extra['message_extractors'] = {
     260}}}
     261
     262That was the easiest part.
     263
     264Now, as you need to actually send that `.js` file containing the messages dictionary, call `add_script()` as appropriate in the `process_request()` method from your module:
     265
     266{{{#!diff
     267--- a/<path>/api.py
     268+++ b/<path>/api.py
     269@@ -243,6 +243,8 @@ class FooTracPlugin(Component):
     270         builds = self._extract_builds(self._get_info(start, stop))
     271         data = {'builds': list(builds)}
     272         add_script(req, '<path>/hudsontrac.js')
     273+        if req.locale is not None:
     274+            add_script(req, '<path>/foo/%s.js' % req.locale)
     275         add_stylesheet(req, '<path>/foo.css')
     276         return 'foo-build.html', data, None
     277 
     278}}}
     279
     280Now, you need to expand the `setup.cfg` file with the configuration that the new `cmdclass` dedicated to Javascript translation need. Those classes all end with an `_js` suffix.
     281
     282{{{#!ini
     283[extract_messages_js]
     284add_comments = TRANSLATOR:
     285copyright_holder = <Your Name>
     286msgid_bugs_address = <Your E-Mail>
     287output_file = <path>/locale/messages-js.pot
     288keywords = _ ngettext:1,2 N_
     289mapping_file = messages-js.cfg
     290
     291[init_catalog_js]
     292domain = foo-js
     293input_file = <path>/locale/messages-js.pot
     294output_dir = <path>/locale
     295
     296[compile_catalog_js]
     297domain = foo-js
     298directory = <path>/locale
     299
     300[update_catalog_js]
     301domain = foo-js
     302input_file = <path>/locale/messages-js.pot
     303output_dir = <path>/locale
     304
     305[generate_messages_js]
     306domain = foo-js
     307input_dir = <path>/locale
     308output_dir = <path>/htdocs/foo
     309}}}
     310
     311As before, replace `<path>` with what's appropriate for your plugin. Note that the domain name is now `foo-js`, not just `foo` as before. This is necessary as we want to have only the strings actually needed Javascript to be stored in the `.js` file containing the messages dictionary.
     312
     313We're nearly done yet... noticed the `mapping_file = messages-js.cfg` line?
     314We need to configure separately how to do the extraction for this `messages-js.pot` catalog template.
     315The `messages-js.cfg` file has the following content:
     316{{{#!ini
     317# mapping file for extracting messages from javascript files into
     318# <path>/locale/messages-js.pot (see setup.cfg)
     319[javascript: **.js]
     320
     321[extractors]
     322javascript_script = trac.util.dist:extract_javascript_script
     323
     324[javascript_script: **.html]
     325}}}
     326
     327Bonus points for anyone who will manage to //simplify// a bit this procedure ;-)
     328
     329
     330=== Announce new plugin version ===
     331The plugin will not work with any Trac version before 0.12, since import of the translation helper functions introduced for 0.12 will fail. It is possible to wrap the import with a '`try:`' and define dummy functions in a corresponding '`except ImportError:`' to allow the plugin to work with older versions of Trac, but there might already be a different version for 0.11 and 0.12, so this is not required in most cases. If it is strictly required for your plugin, have a look at `setup.py` of the [browser:plugins/0.12/mercurial-plugin/setup.py?rev=9133 Mercurial plugin] provided with Trac.
     332
     333In all other cases you'll just add a line like the following as another argument to the setup() function in plugin's `setup.py`:
     334{{{
     335install_requires = ['Trac >= 0.12'],
     336}}}
     337
     338All the work you did by now will go unnoticed, at least with regard to package naming. To help with identification of the new revision you should bump the plugin's version. This is done by changing the version/revision, typically in `setup.cfg` or `setup.py`. And you may wish to leave a note regarding your i18n work along the copyright notices as well.
     339
     340=== Summing it up ===
     341Here's an example of the changes required to add i18n support to the HudsonTrac plugin (`trac-0.12` branch):   
     342 - [[http://github.com/cboos/trachacks-hudsontracplugin/commit/1669cefa806614e62bd9ce5573750e30fa060408| Initial Python translation support ]]
     343   - [[http://github.com/cboos/trachacks-hudsontracplugin/commit/2f84cff6dec654f8d063289262cd1d6ea7b31648| ... better way to call add_domain ]]
     344 - [[http://github.com/cboos/trachacks-hudsontracplugin/commit/3c5b80e5ef3e70432b800c6a0c6504bc32ebe089| Template translation support (get_l10n_cmdclass) ]]
     345   - [[http://github.com/cboos/trachacks-hudsontracplugin/commit/0dd2ea455e57ef1134af394ec6ee1461d02018cb| ... don't forget to specify the domain!]]
     346 - [[http://github.com/cboos/trachacks-hudsontracplugin/commit/d5649ebc1433341f675bc7dcac29a9ee5e315f34| Javascript translation support (get_l10n_js_cmdclass) ]]
     347   - [[http://github.com/cboos/trachacks-hudsontracplugin/commit/b68a2514ad16d46e395de7994dbceabb878e84d9| ... don't forget to actually package the translation related files ]]
     348     - [[http://github.com/cboos/trachacks-hudsontracplugin/commit/775999a033b82654a85d862854d1f8c309d96c1e| and only the .mo files actually ]]
     349
     350You'll find another example attached to this page. That is [th:wiki:SubticketsPlugin Sub-Tickets plugin] v0.1.0 and a [attachment:trac-subtickets-plugin_i18n-l10n.patch diff] containing all i18n/l10n related work to produce a German translation based on that source.
     351
     352
     353
     354== Do translators work
     355General advice from [[TracL10N]] on making good translation for Trac applies here too.
     356
     357I.e. it's desirable to maintain a consistent wording across Trac and Trac plugins.
     358Since this is going beyond the scope of aforementioned [[TracL10N]], there might be the need for more coordination.
     359Consider joining the [th:TracPluginTranslation Trac plugin l10n project], that utilizes [http://www.transifex.net/projects/p/Trac_Plugin-L10N/ Transifex] for uniform access to message catalogs for multiple plugins backed by a dedicated (Mercurial) [http://bitbucket.org/hasienda/trac_plugins-l10n message catalog repository] at Bitbucket.org.
     360Trac has some language teams at Transifex as well, so this is a good chance for tight translator cooperation.
     361
     362For those who read previous parts, you do notice that we switch from talking about i18n to 'l10n' now, don't you?
     363No source code mangling. All code below is no meant to become part of the plugin source but meant to be put to the command line.
     364
     365Switch to root directory of plugin's source, i.e.:
     366{{{
     367cd /usr/src/trac_plugins/foo
     368}}}
     369
     370Extract the messages that where marked for translation before, or on case of Genshi templates are exposed by other means:
     371{{{
     372python ./setup.py extract_messages
     373}}}
     374The attentive reader will notice that the argument to `setup.py` has the same wording as a section in `setup.cfg`, that is not incidental. And this does apply to the following command lines as well.
     375
     376If you attempt to do improvements on existing message catalogs you'll update the one for your desired language:
     377{{{
     378python ./setup.py update_catalog -l de_DE
     379}}}
     380If you omit the language selection argument `-l` and identifier string, existing catalogs of all languages will be updated, what is acceptably fast (just seconds) on current hardware.
     381
     382But if you happen to do all the i18n work before, the you know you there's nothing to update right now. Well, so now it's time to create the message catalog for your desired language:
     383{{{
     384python ./setup.py init_catalog -l de_DE
     385}}}
     386As you may guess, there is not much to be done, if the helper programs don't know what language you'd like to work on, so the language selection argument `-l` and identifier string are mandatory here.
     387
     388Now fire up the editor of your choice. There are dedicated message catalog (.po) file editors that ensure for quick results as a beginner as well as make working on large message catalogs with few untranslated texts or translations marked 'fuzzy' much more convenient. See dedicated resources for details on choosing an editor program as well as for help on editing .po files.^[#a4 4], [#a5 5]^
     389
     390If not already taken care for by your (PO) editor, the place to announce yourself as the last translator is after the default `TRANSLATOR:` label at top of the message catalog file.
     391
     392=== Compile and use it ===
     393Compile the `messages.po` catalog file with your translations into a machine readable `messages.mo` file.
     394{{{
     395python ./setup.py compile_catalog -f -l de_DE
     396}}}
     397The argument `-f` is needed to include even the msgid's marked 'fuzzy'. If you have prepared only one translated catalog the final language selection argument `-l` and identifier string are superfluous. But as soon as there are several other translations that you don't care, it will help to select just your work for compilation.
     398
     399Now you've used all four configuration sections in `setup.cfg`, that are dedicated to i18n/l10n helper programs. You could finish your work by packaging the plugin.
     400
     401Make the python egg as usual:
     402{{{
     403python ./setup.py bdist_egg
     404}}}
     405Install the new egg and restart your web-server after you made sure to purge any former version of that plugin (without your latest work).
     406
     407Note that if the plugin's `setup.py` has installed the proper extra commands (`extra['cmdclass'] = cmdclass` like in the [#setup above]), then `bdist_egg` will automatically take care of the `compile_catalog` command, as well as the commands related to Javascript i18n if needed.
     408
     409== Advanced stuff
     410
     411=== Translating `Option*` documentation
     412
     413Trac 1.0 added support for a special kind of `N_` marker, `cleandoc_`, which can be used to reformat multiline messages in a compact form. There's also support to apply this "cleandoc" transformation to the documentation of instances of `trac.config.Option` and its subclasses. However, this support is coming from a special Python extractor which has to be used instead of the default Python extractor from Babel.
    199414
    200415The additional change is:
     
    229444}}}
    230445
    231 ==== Text extraction from Javascript code ==== #Javascript
    232 
    233 Adding support for translating the marked strings in the Javascript code is a bit more involved, but if you made it to this point, that shouldn't scare you away...
    234 
    235 We currently support only statically deployed Javascript files, which means they can't be translated like template files on the server, but that the translation has to happen dynamically on the client side. To this end, we want to send an additional `.js` file containing a dictionary of the messages that have to be translated, and only those. In order to clearly identify which strings have to be present in this dictionary, we'll extract the messages marked for translation (the usual `_(...)` ways) from the Javascript code into a dedicated catalog template, and from there, we'll create dedicated catalogs for each locale. In the end, the translations present in each compiled catalog will be extracted and placed into a `.js` file containing the messages dictionary and some setup code.
    236 
    237 The first change is to use `get_l10n_js_cmdclass` in lieu of `get_l10n_cmdclass`. The former adds a few more setup commands for extracting messages strings from Javascript `.js` files and `<script type="text/javascript">` snippets in `.html` files, initialization and updating of dedicated catalog files, and finally compiling that catalog and creating a `.js` file containing the dictionary of strings, ready to be used by the `babel.js` support code already present in Trac pages.
    238 
    239 The change to `setup.py` looks like this:
    240 {{{#!diff
    241 diff -u a/setup.py b/setup.py
    242 --- a/setup.py
    243 +++ b/setup.py
    244 @@ -5,8 +5,8 @@ from setuptools import setup
    245  extra = {}
    246  
    247 -from trac.util.dist import get_l10n_cmdclass
    248 -cmdclass = get_l10n_cmdclass()
    249 +from trac.util.dist import get_l10n_js_cmdclass
    250 +cmdclass = get_l10n_js_cmdclass()
    251  if cmdclass:
    252      extra['cmdclass'] = cmdclass
    253      extra['message_extractors'] = {
    254 }}}
    255 
    256 That was the easiest part.
    257 
    258 Now, as you need to actually send that `.js` file containing the messages dictionary, call `add_script()` as appropriate in the `process_request()` method from your module:
    259 
    260 {{{#!diff
    261 --- a/<path>/api.py
    262 +++ b/<path>/api.py
    263 @@ -243,6 +243,8 @@ class FooTracPlugin(Component):
    264          builds = self._extract_builds(self._get_info(start, stop))
    265          data = {'builds': list(builds)}
    266          add_script(req, '<path>/hudsontrac.js')
    267 +        if req.locale is not None:
    268 +            add_script(req, '<path>/foo/%s.js' % req.locale)
    269          add_stylesheet(req, '<path>/foo.css')
    270          return 'foo-build.html', data, None
    271  
    272 }}}
    273 
    274 Now, you need to expand the `setup.cfg` file with the configuration that the new `cmdclass` dedicated to Javascript translation need. Those classes all end with an `_js` suffix.
    275 
    276 {{{#!ini
    277 [extract_messages_js]
    278 add_comments = TRANSLATOR:
    279 copyright_holder = <Your Name>
    280 msgid_bugs_address = <Your E-Mail>
    281 output_file = <path>/locale/messages-js.pot
    282 keywords = _ ngettext:1,2 N_
    283 mapping_file = messages-js.cfg
    284 
    285 [init_catalog_js]
    286 domain = foo-js
    287 input_file = <path>/locale/messages-js.pot
    288 output_dir = <path>/locale
    289 
    290 [compile_catalog_js]
    291 domain = foo-js
    292 directory = <path>/locale
    293 
    294 [update_catalog_js]
    295 domain = foo-js
    296 input_file = <path>/locale/messages-js.pot
    297 output_dir = <path>/locale
    298 
    299 [generate_messages_js]
    300 domain = foo-js
    301 input_dir = <path>/locale
    302 output_dir = <path>/htdocs/foo
    303 }}}
    304 
    305 As before, replace `<path>` with what's appropriate for your plugin. Note that the domain name is now `foo-js`, not just `foo` as before. This is necessary as we want to have only the strings actually needed Javascript to be stored in the `.js` file containing the messages dictionary.
    306 
    307 We're nearly done yet... noticed the `mapping_file = messages-js.cfg` line?
    308 We need to configure separately how to do the extraction for this `messages-js.pot` catalog template.
    309 The `messages-js.cfg` file has the following content:
    310 {{{#!ini
    311 # mapping file for extracting messages from javascript files into
    312 # <path>/locale/messages-js.pot (see setup.cfg)
    313 [javascript: **.js]
    314 
    315 [extractors]
    316 javascript_script = trac.util.dist:extract_javascript_script
    317 
    318 [javascript_script: **.html]
    319 }}}
    320 
    321 Bonus points for anyone who will manage to //simplify// a bit this procedure ;-)
    322 
    323 ==== Register message catalog files for packaging ====
    324 To include the translated messages into the packaged plugin you need to add the path to the catalog files to `package_data` in the call for function `setup()` in `setup.py`:
    325 {{{#!diff
    326 diff -u a/setup.py b/setup.py
    327 --- a/setup.py
    328 +++ b/setup.py
    329 @@ -39,6 +39,7 @@
    330      package_data = {
    331          <path>: [
    332              'htdocs/css/*.css',
    333 +            'locale/*/LC_MESSAGES/*.mo',
    334          ],
    335      },
    336      entry_points = {
    337 }}}
    338 
    339 ==== Announce new plugin version ====
    340 The plugin will not work with any Trac version before 0.12, since import of the translation helper functions introduced for 0.12 will fail. It is possible to wrap the import with a '`try:`' and define dummy functions in a corresponding '`except ImportError:`' to allow the plugin to work with older versions of Trac, but there might already be a different version for 0.11 and 0.12, so this is not required in most cases. If it is strictly required for your plugin, have a look at `setup.py` of the [browser:plugins/0.12/mercurial-plugin/setup.py?rev=9133 Mercurial plugin] provided with Trac.
    341 
    342 In all other cases you'll just add a line like the following as another argument to the setup() function in plugin's `setup.py`:
    343 {{{
    344 install_requires = ['Trac >= 0.12'],
    345 }}}
    346 
    347 All the work you did by now will go unnoticed, at least with regard to package naming. To help with identification of the new revision you should bump the plugin's version. This is done by changing the version/revision, typically in `setup.cfg` or `setup.py`. And you may wish to leave a note regarding your i18n work along the copyright notices as well.
    348 
    349 ==== Summing it up ====
    350 Here's an example of the changes required to add i18n support to the HudsonTrac plugin (`trac-0.12` branch):   
    351  - [[http://github.com/cboos/trachacks-hudsontracplugin/commit/1669cefa806614e62bd9ce5573750e30fa060408| Initial Python translation support ]]
    352    - [[http://github.com/cboos/trachacks-hudsontracplugin/commit/2f84cff6dec654f8d063289262cd1d6ea7b31648| ... better way to call add_domain ]]
    353  - [[http://github.com/cboos/trachacks-hudsontracplugin/commit/3c5b80e5ef3e70432b800c6a0c6504bc32ebe089| Template translation support (get_l10n_cmdclass) ]]
    354    - [[http://github.com/cboos/trachacks-hudsontracplugin/commit/0dd2ea455e57ef1134af394ec6ee1461d02018cb| ... don't forget to specify the domain!]]
    355  - [[http://github.com/cboos/trachacks-hudsontracplugin/commit/d5649ebc1433341f675bc7dcac29a9ee5e315f34| Javascript translation support (get_l10n_js_cmdclass) ]]
    356    - [[http://github.com/cboos/trachacks-hudsontracplugin/commit/b68a2514ad16d46e395de7994dbceabb878e84d9| ... don't forget to actually package the translation related files ]]
    357      - [[http://github.com/cboos/trachacks-hudsontracplugin/commit/775999a033b82654a85d862854d1f8c309d96c1e| and only the .mo files actually ]]
    358 
    359 
    360 You'll find another example attached to this page. That is [th:wiki:SubticketsPlugin Sub-Tickets plugin] v0.1.0 and a [attachment:trac-subtickets-plugin_i18n-l10n.patch diff] containing all i18n/l10n related work to produce a German translation based on that source.
    361 
    362 === Do translators work ===
    363 General advice from [[TracL10N]] on making good translation for Trac applies here too.
    364 
    365 I.e. it's desirable to maintain a consistent wording across Trac and Trac plugins.
    366 Since this is going beyond the scope of aforementioned [[TracL10N]], there might be the need for more coordination.
    367 Consider joining the [th:TracPluginTranslation Trac plugin l10n project], that utilizes [http://www.transifex.net/projects/p/Trac_Plugin-L10N/ Transifex] for uniform access to message catalogs for multiple plugins backed by a dedicated (Mercurial) [http://bitbucket.org/hasienda/trac_plugins-l10n message catalog repository] at Bitbucket.org.
    368 Trac has some language teams at Transifex as well, so this is a good chance for tight translator cooperation.
    369 
    370 For those who read previous parts, you do notice that we switch from talking about i18n to 'l10n' now, don't you?
    371 No source code mangling. All code below is no meant to become part of the plugin source but meant to be put to the command line.
    372 
    373 Switch to root directory of plugin's source, i.e.:
    374 {{{
    375 cd /usr/src/trac_plugins/foo
    376 }}}
    377 
    378 Extract the messages that where marked for translation before, or on case of Genshi templates are exposed by other means:
    379 {{{
    380 python ./setup.py extract_messages
    381 }}}
    382 The attentive reader will notice that the argument to `setup.py` has the same wording as a section in `setup.cfg`, that is not incidental. And this does apply to the following command lines as well.
    383 
    384 If you attempt to do improvements on existing message catalogs you'll update the one for your desired language:
    385 {{{
    386 python ./setup.py update_catalog -l de_DE
    387 }}}
    388 If you omit the language selection argument `-l` and identifier string, existing catalogs of all languages will be updated, what is acceptably fast (just seconds) on current hardware.
    389 
    390 But if you happen to do all the i18n work before, the you know you there's nothing to update right now. Well, so now it's time to create the message catalog for your desired language:
    391 {{{
    392 python ./setup.py init_catalog -l de_DE
    393 }}}
    394 As you may guess, there is not much to be done, if the helper programs don't know what language you'd like to work on, so the language selection argument `-l` and identifier string are mandatory here.
    395 
    396 Now fire up the editor of your choice. There are dedicated message catalog (.po) file editors that ensure for quick results as a beginner as well as make working on large message catalogs with few untranslated texts or translations marked 'fuzzy' much more convenient. See dedicated resources for details on choosing an editor program as well as for help on editing .po files.^[#a4 4], [#a5 5]^
    397 
    398 If not already taken care for by your (PO) editor, the place to announce yourself as the last translator is after the default `TRANSLATOR:` label at top of the message catalog file.
    399 
    400 === Compile and use it ===
    401 Compile the `messages.po` catalog file with your translations into a machine readable `messages.mo` file.
    402 {{{
    403 python ./setup.py compile_catalog -f -l de_DE
    404 }}}
    405 The argument `-f` is needed to include even the msgid's marked 'fuzzy'. If you have prepared only one translated catalog the final language selection argument `-l` and identifier string are superfluous. But as soon as there are several other translations that you don't care, it will help to select just your work for compilation.
    406 
    407 Now you've used all four configuration sections in `setup.cfg`, that are dedicated to i18n/l10n helper programs. You could finish your work by packaging the plugin.
    408 
    409 Make the python egg as usual:
    410 {{{
    411 python ./setup.py bdist_egg
    412 }}}
    413 Install the new egg and restart your web-server after you made sure to purge any former version of that plugin (without your latest work).
    414 
    415 Note that if the plugin's `setup.py` has installed the proper extra commands (`extra['cmdclass'] = cmdclass` like in the [#setup above]), then `bdist_egg` will automatically take care of the `compile_catalog` command, as well as the commands related to Javascript i18n if needed.
    416 
    417 === Advanced stuff ===
    418 ==== About 'true' l10n ====
     446
     447=== About 'true' l10n
    419448A human translator will/should do better than an automatic translation,
    420449since good l10n has more of a transcription than pure translation word by word.