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`) |
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`) ==== |
| 102 | Add some lines to `setup.cfg` or, if it doesn't exist by now, create it with the following content: |
| 103 | {{{#!ini |
| 104 | [extract_messages] |
| 105 | add_comments = TRANSLATOR: |
| 106 | msgid_bugs_address = |
| 107 | output_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. |
| 111 | keywords = _ N_ tag_ |
| 112 | # Other example: |
| 113 | #keywords = _ ngettext:1,2 N_ tag_ |
| 114 | width = 72 |
| 115 | |
| 116 | [init_catalog] |
| 117 | input_file = <path>/locale/messages.pot |
| 118 | output_dir = <path>/locale |
| 119 | domain = foo |
| 120 | |
| 121 | [compile_catalog] |
| 122 | directory = <path>/locale |
| 123 | domain = foo |
| 124 | |
| 125 | [update_catalog] |
| 126 | input_file = <path>/locale/messages.pot |
| 127 | output_dir = <path>/locale |
| 128 | domain = foo |
| 129 | }}} |
| 130 | Replace `<path>` as appropriate (i.e. the relative path to the folder containing the `locale` directory, for example `mytracplugin`). |
| 131 | |
| 132 | This will tell Babel where to look for and store message catalog files. |
| 133 | |
| 134 | |
| 135 | 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. |
| 136 | |
| 137 | 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. |
| 138 | |
| 139 | ==== Register message catalog files for packaging ==== |
| 140 | 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`: |
| 141 | {{{#!diff |
| 142 | diff -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 ==== |
| 158 | Pick a reasonably unique name for the domain, e.g. ** 'foo' ** (if your plugin is named 'foo', that is). |
| 159 | |
| 160 | This 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 | |
| 163 | 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`. |
| 164 | |
| 165 | This helper function should be called at module load time, like this: |
| 166 | {{{#!python |
| 167 | from trac.util.translation import domain_functions |
| 168 | |
| 169 | _, tag_, N_, add_domain = \ |
| 170 | domain_functions('foo', ('_', 'tag_', 'N_', 'add_domain')) |
| 171 | }}} |
| 172 | |
| 173 | The 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 | |
| 182 | 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))`. |
| 183 | |
| 184 | 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: |
| 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 | }}} |
| 192 | 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). |
| 193 | |
| 194 | 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: |
| 195 | {{{#!python |
| 196 | from api import _, tag_, N_ |
| 197 | }}} |
| 198 | |
| 199 | ==== Mark text for extraction ==== |
| 200 | In 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 | |
| 209 | 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). |
| 210 | |
| 211 | 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. |
| 212 | |
| 213 | === Make the Genshi templates translation-aware |
| 214 | |
| 215 | First, 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 ==== |
| 218 | 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. |
| 219 | |
| 220 | ==== Runtime support |
| 221 | Extraction 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 | |
| 223 | For 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 | |
| 239 | 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... |
| 240 | |
| 241 | 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. |
| 242 | |
| 243 | 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. |
| 244 | |
| 245 | The change to `setup.py` looks like this: |
| 246 | {{{#!diff |
| 247 | diff -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 | |
| 262 | That was the easiest part. |
| 263 | |
| 264 | 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: |
| 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 | |
| 280 | 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. |
| 281 | |
| 282 | {{{#!ini |
| 283 | [extract_messages_js] |
| 284 | add_comments = TRANSLATOR: |
| 285 | copyright_holder = <Your Name> |
| 286 | msgid_bugs_address = <Your E-Mail> |
| 287 | output_file = <path>/locale/messages-js.pot |
| 288 | keywords = _ ngettext:1,2 N_ |
| 289 | mapping_file = messages-js.cfg |
| 290 | |
| 291 | [init_catalog_js] |
| 292 | domain = foo-js |
| 293 | input_file = <path>/locale/messages-js.pot |
| 294 | output_dir = <path>/locale |
| 295 | |
| 296 | [compile_catalog_js] |
| 297 | domain = foo-js |
| 298 | directory = <path>/locale |
| 299 | |
| 300 | [update_catalog_js] |
| 301 | domain = foo-js |
| 302 | input_file = <path>/locale/messages-js.pot |
| 303 | output_dir = <path>/locale |
| 304 | |
| 305 | [generate_messages_js] |
| 306 | domain = foo-js |
| 307 | input_dir = <path>/locale |
| 308 | output_dir = <path>/htdocs/foo |
| 309 | }}} |
| 310 | |
| 311 | 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. |
| 312 | |
| 313 | We're nearly done yet... noticed the `mapping_file = messages-js.cfg` line? |
| 314 | We need to configure separately how to do the extraction for this `messages-js.pot` catalog template. |
| 315 | The `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] |
| 322 | javascript_script = trac.util.dist:extract_javascript_script |
| 323 | |
| 324 | [javascript_script: **.html] |
| 325 | }}} |
| 326 | |
| 327 | Bonus points for anyone who will manage to //simplify// a bit this procedure ;-) |
| 328 | |
| 329 | |
| 330 | === Announce new plugin version === |
| 331 | 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. |
| 332 | |
| 333 | 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`: |
| 334 | {{{ |
| 335 | install_requires = ['Trac >= 0.12'], |
| 336 | }}} |
| 337 | |
| 338 | 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. |
| 339 | |
| 340 | === Summing it up === |
| 341 | Here'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 | |
| 350 | 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. |
| 351 | |
| 352 | |
| 353 | |
| 354 | == Do translators work |
| 355 | General advice from [[TracL10N]] on making good translation for Trac applies here too. |
| 356 | |
| 357 | I.e. it's desirable to maintain a consistent wording across Trac and Trac plugins. |
| 358 | Since this is going beyond the scope of aforementioned [[TracL10N]], there might be the need for more coordination. |
| 359 | 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. |
| 360 | Trac has some language teams at Transifex as well, so this is a good chance for tight translator cooperation. |
| 361 | |
| 362 | For those who read previous parts, you do notice that we switch from talking about i18n to 'l10n' now, don't you? |
| 363 | 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. |
| 364 | |
| 365 | Switch to root directory of plugin's source, i.e.: |
| 366 | {{{ |
| 367 | cd /usr/src/trac_plugins/foo |
| 368 | }}} |
| 369 | |
| 370 | Extract the messages that where marked for translation before, or on case of Genshi templates are exposed by other means: |
| 371 | {{{ |
| 372 | python ./setup.py extract_messages |
| 373 | }}} |
| 374 | 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. |
| 375 | |
| 376 | If you attempt to do improvements on existing message catalogs you'll update the one for your desired language: |
| 377 | {{{ |
| 378 | python ./setup.py update_catalog -l de_DE |
| 379 | }}} |
| 380 | 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. |
| 381 | |
| 382 | 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: |
| 383 | {{{ |
| 384 | python ./setup.py init_catalog -l de_DE |
| 385 | }}} |
| 386 | 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. |
| 387 | |
| 388 | 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]^ |
| 389 | |
| 390 | 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. |
| 391 | |
| 392 | === Compile and use it === |
| 393 | Compile the `messages.po` catalog file with your translations into a machine readable `messages.mo` file. |
| 394 | {{{ |
| 395 | python ./setup.py compile_catalog -f -l de_DE |
| 396 | }}} |
| 397 | 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. |
| 398 | |
| 399 | 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. |
| 400 | |
| 401 | Make the python egg as usual: |
| 402 | {{{ |
| 403 | python ./setup.py bdist_egg |
| 404 | }}} |
| 405 | 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). |
| 406 | |
| 407 | 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. |
| 408 | |
| 409 | == Advanced stuff |
| 410 | |
| 411 | === Translating `Option*` documentation |
| 412 | |
| 413 | 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. |
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 |