Merge lp:~songofacandy/bzr/i18n-msgfmt into lp:bzr

Proposed by methane
Status: Superseded
Proposed branch: lp:~songofacandy/bzr/i18n-msgfmt
Merge into: lp:bzr
Diff against target: 844 lines (+763/-2)
8 files modified
Makefile (+20/-0)
bzrlib/builtins.py (+10/-0)
bzrlib/export_pot.py (+241/-0)
bzrlib/tests/__init__.py (+1/-0)
bzrlib/tests/test_export_pot.py (+147/-0)
setup.py (+11/-2)
tools/build_mo.py (+125/-0)
tools/msgfmt.py (+208/-0)
To merge this branch: bzr merge lp:~songofacandy/bzr/i18n-msgfmt
Reviewer Review Type Date Requested Status
bzr-core Pending
Review via email: mp+61030@code.launchpad.net

This proposal has been superseded by a proposal from 2011-05-16.

Description of the change

This merge proposal adds build_mo command to setup.py
This command builds bzrlib/locale/<LANG>/LC_MESSAGES/*.mo files from po/*.po.

The directory format draft is described at http://wiki.bazaar.canonical.com/DraftSpecs/I18nSupport.

To post a comment you must log in.
Revision history for this message
methane (songofacandy) wrote :

Uh, this mp is based on lp:~songofacandy/bzr/i18n-msgextract.
Extra diffs may be removed after that branch is merged.

Revision history for this message
John A Meinel (jameinel) wrote :

Do you know that you can set a Prerequisite branch as part of submitting the merge proposal? Changes in the prerequisite branch won't be shown in the diff. I think you can resubmit this and add the appropriate prereq. I'll try.

Revision history for this message
methane (songofacandy) wrote :

Oh, I didn't know that. Thank you.

On Mon, May 16, 2011 at 6:48 PM, John A Meinel <email address hidden> wrote:
> Do you know that you can set a Prerequisite branch as part of submitting the merge proposal? Changes in the prerequisite branch won't be shown in the diff. I think you can resubmit this and add the appropriate prereq. I'll try.
> --
> https://code.launchpad.net/~songofacandy/bzr/i18n-msgfmt/+merge/61030
> You are the owner of lp:~songofacandy/bzr/i18n-msgfmt.
>

--
INADA Naoki  <email address hidden>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2011-04-18 00:44:08 +0000
3+++ Makefile 2011-05-15 17:12:40 +0000
4@@ -420,6 +420,26 @@
5 $(PYTHON) tools/win32/ostools.py remove dist
6
7
8+# i18n targets
9+
10+.PHONY: update-pot po/bzr.pot
11+update-pot: po/bzr.pot
12+
13+TRANSLATABLE_PYFILES:=$(shell find bzrlib -name '*.py' \
14+ | grep -v 'bzrlib/tests/' \
15+ | grep -v 'bzrlib/doc' \
16+ )
17+
18+po/bzr.pot: $(PYFILES) $(DOCFILES)
19+ $(PYTHON) ./bzr export-pot > po/bzr.pot
20+ echo $(TRANSLATABLE_PYFILES) | xargs \
21+ xgettext --package-name "bzr" \
22+ --msgid-bugs-address "<bazaar@canonical.com>" \
23+ --copyright-holder "Canonical" \
24+ --from-code ISO-8859-1 --join --sort-by-file --add-comments=i18n: \
25+ -d bzr -p po -o bzr.pot
26+
27+
28 ### Packaging Targets ###
29
30 .PHONY: dist check-dist-tarball
31
32=== modified file 'bzrlib/builtins.py'
33--- bzrlib/builtins.py 2011-05-03 13:53:46 +0000
34+++ bzrlib/builtins.py 2011-05-15 17:12:40 +0000
35@@ -6154,6 +6154,16 @@
36 self.outf.write('%s %s\n' % (path, location))
37
38
39+class cmd_export_pot(Command):
40+ __doc__ = """Export command helps and error messages in po format."""
41+
42+ hidden = True
43+
44+ def run(self):
45+ from bzrlib.export_pot import export_pot
46+ export_pot(self.outf)
47+
48+
49 def _register_lazy_builtins():
50 # register lazy builtins from other modules; called at startup and should
51 # be only called once.
52
53=== added file 'bzrlib/export_pot.py'
54--- bzrlib/export_pot.py 1970-01-01 00:00:00 +0000
55+++ bzrlib/export_pot.py 2011-05-15 17:12:40 +0000
56@@ -0,0 +1,241 @@
57+# Copyright (C) 2011 Canonical Ltd
58+#
59+# This program is free software; you can redistribute it and/or modify
60+# it under the terms of the GNU General Public License as published by
61+# the Free Software Foundation; either version 2 of the License, or
62+# (at your option) any later version.
63+#
64+# This program is distributed in the hope that it will be useful,
65+# but WITHOUT ANY WARRANTY; without even the implied warranty of
66+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
67+# GNU General Public License for more details.
68+#
69+# You should have received a copy of the GNU General Public License
70+# along with this program; if not, write to the Free Software
71+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
72+
73+# The normalize function is taken from pygettext which is distributed
74+# with Python under the Python License, which is GPL compatible.
75+
76+"""Extract docstrings from Bazaar commands.
77+"""
78+
79+import inspect
80+import os
81+
82+from bzrlib import (
83+ commands as _mod_commands,
84+ errors,
85+ help_topics,
86+ plugin,
87+ )
88+from bzrlib.trace import (
89+ mutter,
90+ note,
91+ )
92+
93+
94+def _escape(s):
95+ s = (s.replace('\\', '\\\\')
96+ .replace('\n', '\\n')
97+ .replace('\r', '\\r')
98+ .replace('\t', '\\t')
99+ .replace('"', '\\"')
100+ )
101+ return s
102+
103+def _normalize(s):
104+ # This converts the various Python string types into a format that
105+ # is appropriate for .po files, namely much closer to C style.
106+ lines = s.split('\n')
107+ if len(lines) == 1:
108+ s = '"' + _escape(s) + '"'
109+ else:
110+ if not lines[-1]:
111+ del lines[-1]
112+ lines[-1] = lines[-1] + '\n'
113+ lines = map(_escape, lines)
114+ lineterm = '\\n"\n"'
115+ s = '""\n"' + lineterm.join(lines) + '"'
116+ return s
117+
118+
119+_FOUND_MSGID = None # set by entry function.
120+
121+def _poentry(outf, path, lineno, s, comment=None):
122+ if s in _FOUND_MSGID:
123+ return
124+ _FOUND_MSGID.add(s)
125+ if comment is None:
126+ comment = ''
127+ else:
128+ comment = "# %s\n" % comment
129+ mutter("Exporting msg %r at line %d in %r", s[:20], lineno, path)
130+ print >>outf, ('#: %s:%d\n' % (path, lineno) +
131+ comment+
132+ 'msgid %s\n' % _normalize(s) +
133+ 'msgstr ""\n')
134+
135+def _poentry_per_paragraph(outf, path, lineno, msgid):
136+ # TODO: How to split long help?
137+ paragraphs = msgid.split('\n\n')
138+ for p in paragraphs:
139+ _poentry(outf, path, lineno, p)
140+ lineno += p.count('\n') + 2
141+
142+_LAST_CACHE = _LAST_CACHED_SRC = None
143+
144+def _offsets_of_literal(src):
145+ global _LAST_CACHE, _LAST_CACHED_SRC
146+ if src == _LAST_CACHED_SRC:
147+ return _LAST_CACHE.copy()
148+
149+ import ast
150+ root = ast.parse(src)
151+ offsets = {}
152+ for node in ast.walk(root):
153+ if not isinstance(node, ast.Str):
154+ continue
155+ offsets[node.s] = node.lineno - node.s.count('\n')
156+
157+ _LAST_CACHED_SRC = src
158+ _LAST_CACHE = offsets.copy()
159+ return offsets
160+
161+def _standard_options(outf):
162+ from bzrlib.option import Option
163+ src = inspect.findsource(Option)[0]
164+ src = ''.join(src)
165+ path = 'bzrlib/option.py'
166+ offsets = _offsets_of_literal(src)
167+
168+ for name in sorted(Option.OPTIONS.keys()):
169+ opt = Option.OPTIONS[name]
170+ if getattr(opt, 'hidden', False):
171+ continue
172+ if getattr(opt, 'title', None):
173+ lineno = offsets.get(opt.title, 9999)
174+ if lineno == 9999:
175+ note("%r is not found in bzrlib/option.py" % opt.title)
176+ _poentry(outf, path, lineno, opt.title,
177+ 'title of %r option' % name)
178+ if getattr(opt, 'help', None):
179+ lineno = offsets.get(opt.help, 9999)
180+ if lineno == 9999:
181+ note("%r is not found in bzrlib/option.py" % opt.help)
182+ _poentry(outf, path, lineno, opt.help,
183+ 'help of %r option' % name)
184+
185+def _command_options(outf, path, cmd):
186+ src, default_lineno = inspect.findsource(cmd.__class__)
187+ offsets = _offsets_of_literal(''.join(src))
188+ for opt in cmd.takes_options:
189+ if isinstance(opt, str):
190+ continue
191+ if getattr(opt, 'hidden', False):
192+ continue
193+ name = opt.name
194+ if getattr(opt, 'title', None):
195+ lineno = offsets.get(opt.title, default_lineno)
196+ _poentry(outf, path, lineno, opt.title,
197+ 'title of %r option of %r command' % (name, cmd.name()))
198+ if getattr(opt, 'help', None):
199+ lineno = offsets.get(opt.help, default_lineno)
200+ _poentry(outf, path, lineno, opt.help,
201+ 'help of %r option of %r command' % (name, cmd.name()))
202+
203+
204+def _write_command_help(outf, cmd_name, cmd):
205+ path = inspect.getfile(cmd.__class__)
206+ if path.endswith('.pyc'):
207+ path = path[:-1]
208+ path = os.path.relpath(path)
209+ src, lineno = inspect.findsource(cmd.__class__)
210+ offsets = _offsets_of_literal(''.join(src))
211+ lineno = offsets[cmd.__doc__]
212+ doc = inspect.getdoc(cmd)
213+
214+ _poentry_per_paragraph(outf, path, lineno, doc)
215+ _command_options(outf, path, cmd)
216+
217+def _command_helps(outf):
218+ """Extract docstrings from path.
219+
220+ This respects the Bazaar cmdtable/table convention and will
221+ only extract docstrings from functions mentioned in these tables.
222+ """
223+ from glob import glob
224+
225+ # builtin commands
226+ for cmd_name in _mod_commands.builtin_command_names():
227+ command = _mod_commands.get_cmd_object(cmd_name, False)
228+ if command.hidden:
229+ continue
230+ note("Exporting messages from builtin command: %s", cmd_name)
231+ _write_command_help(outf, cmd_name, command)
232+
233+ plugin_path = plugin.get_core_plugin_path()
234+ core_plugins = glob(plugin_path + '/*/__init__.py')
235+ core_plugins = [os.path.basename(os.path.dirname(p))
236+ for p in core_plugins]
237+ # core plugins
238+ for cmd_name in _mod_commands.plugin_command_names():
239+ command = _mod_commands.get_cmd_object(cmd_name, False)
240+ if command.hidden:
241+ continue
242+ if command.plugin_name() not in core_plugins:
243+ # skip non-core plugins
244+ # TODO: Support extracting from third party plugins.
245+ continue
246+ note("Exporting messages from plugin command: %s in %s",
247+ cmd_name, command.plugin_name())
248+ _write_command_help(outf, cmd_name, command)
249+
250+
251+def _error_messages(outf):
252+ """Extract fmt string from bzrlib.errors."""
253+ path = errors.__file__
254+ if path.endswith('.pyc'):
255+ path = path[:-1]
256+ offsets = _offsets_of_literal(open(path).read())
257+
258+ base_klass = errors.BzrError
259+ for name in dir(errors):
260+ klass = getattr(errors, name)
261+ if not inspect.isclass(klass):
262+ continue
263+ if not issubclass(klass, base_klass):
264+ continue
265+ if klass is base_klass:
266+ continue
267+ if klass.internal_error:
268+ continue
269+ fmt = getattr(klass, "_fmt", None)
270+ if fmt:
271+ note("Exporting message from error: %s", name)
272+ _poentry(outf, 'bzrlib/errors.py',
273+ offsets.get(fmt, 9999), fmt)
274+
275+def _help_topics(outf):
276+ topic_registry = help_topics.topic_registry
277+ for key in topic_registry.keys():
278+ doc = topic_registry.get(key)
279+ if isinstance(doc, str):
280+ _poentry_per_paragraph(
281+ outf,
282+ 'dummy/help_topics/'+key+'/detail.txt',
283+ 1, doc)
284+
285+ summary = topic_registry.get_summary(key)
286+ if summary is not None:
287+ _poentry(outf, 'dummy/help_topics/'+key+'/summary.txt',
288+ 1, summary)
289+
290+def export_pot(outf):
291+ global _FOUND_MSGID
292+ _FOUND_MSGID = set()
293+ _standard_options(outf)
294+ _command_helps(outf)
295+ _error_messages(outf)
296+ # disable exporting help topics until we decide how to translate it.
297+ #_help_topics(outf)
298
299=== modified file 'bzrlib/tests/__init__.py'
300--- bzrlib/tests/__init__.py 2011-05-13 12:51:05 +0000
301+++ bzrlib/tests/__init__.py 2011-05-15 17:12:40 +0000
302@@ -3783,6 +3783,7 @@
303 'bzrlib.tests.test_eol_filters',
304 'bzrlib.tests.test_errors',
305 'bzrlib.tests.test_export',
306+ 'bzrlib.tests.test_export_pot',
307 'bzrlib.tests.test_extract',
308 'bzrlib.tests.test_fetch',
309 'bzrlib.tests.test_fixtures',
310
311=== added file 'bzrlib/tests/test_export_pot.py'
312--- bzrlib/tests/test_export_pot.py 1970-01-01 00:00:00 +0000
313+++ bzrlib/tests/test_export_pot.py 2011-05-15 17:12:40 +0000
314@@ -0,0 +1,147 @@
315+# Copyright (C) 2011 Canonical Ltd
316+#
317+# This program is free software; you can redistribute it and/or modify
318+# it under the terms of the GNU General Public License as published by
319+# the Free Software Foundation; either version 2 of the License, or
320+# (at your option) any later version.
321+#
322+# This program is distributed in the hope that it will be useful,
323+# but WITHOUT ANY WARRANTY; without even the implied warranty of
324+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
325+# GNU General Public License for more details.
326+#
327+# You should have received a copy of the GNU General Public License
328+# along with this program; if not, write to the Free Software
329+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
330+
331+from cStringIO import StringIO
332+import textwrap
333+
334+from bzrlib import (
335+ export_pot,
336+ tests,
337+ )
338+
339+class TestEscape(tests.TestCase):
340+
341+ def test_simple_escape(self):
342+ self.assertEqual(
343+ export_pot._escape('foobar'),
344+ 'foobar')
345+
346+ s = '''foo\nbar\r\tbaz\\"spam"'''
347+ e = '''foo\\nbar\\r\\tbaz\\\\\\"spam\\"'''
348+ self.assertEqual(export_pot._escape(s), e)
349+
350+ def test_complex_escape(self):
351+ s = '''\\r \\\n'''
352+ e = '''\\\\r \\\\\\n'''
353+ self.assertEqual(export_pot._escape(s), e)
354+
355+
356+class TestNormalize(tests.TestCase):
357+
358+ def test_single_line(self):
359+ s = 'foobar'
360+ e = '"foobar"'
361+ self.assertEqual(export_pot._normalize(s), e)
362+
363+ s = 'foo"bar'
364+ e = '"foo\\"bar"'
365+ self.assertEqual(export_pot._normalize(s), e)
366+
367+ def test_multi_lines(self):
368+ s = 'foo\nbar\n'
369+ e = '""\n"foo\\n"\n"bar\\n"'
370+ self.assertEqual(export_pot._normalize(s), e)
371+
372+ s = '\nfoo\nbar\n'
373+ e = ('""\n'
374+ '"\\n"\n'
375+ '"foo\\n"\n'
376+ '"bar\\n"')
377+ self.assertEqual(export_pot._normalize(s), e)
378+
379+
380+class PoEntryTestCase(tests.TestCase):
381+
382+ def setUp(self):
383+ self.overrideAttr(export_pot, '_FOUND_MSGID', set())
384+ self._outf = StringIO()
385+ super(PoEntryTestCase, self).setUp()
386+
387+ def check_output(self, expected):
388+ self.assertEqual(
389+ self._outf.getvalue(),
390+ textwrap.dedent(expected)
391+ )
392+
393+class TestPoEntry(PoEntryTestCase):
394+
395+ def test_simple(self):
396+ export_pot._poentry(self._outf, 'dummy', 1, "spam")
397+ export_pot._poentry(self._outf, 'dummy', 2, "ham", 'EGG')
398+ self.check_output('''\
399+ #: dummy:1
400+ msgid "spam"
401+ msgstr ""
402+
403+ #: dummy:2
404+ # EGG
405+ msgid "ham"
406+ msgstr ""
407+
408+ ''')
409+
410+ def test_duplicate(self):
411+ export_pot._poentry(self._outf, 'dummy', 1, "spam")
412+ # This should be ignored.
413+ export_pot._poentry(self._outf, 'dummy', 2, "spam", 'EGG')
414+
415+ self.check_output('''\
416+ #: dummy:1
417+ msgid "spam"
418+ msgstr ""\n
419+ ''')
420+
421+
422+class TestPoentryPerPergraph(PoEntryTestCase):
423+
424+ def test_single(self):
425+ export_pot._poentry_per_paragraph(
426+ self._outf,
427+ 'dummy',
428+ 10,
429+ '''foo\nbar\nbaz\n'''
430+ )
431+ self.check_output('''\
432+ #: dummy:10
433+ msgid ""
434+ "foo\\n"
435+ "bar\\n"
436+ "baz\\n"
437+ msgstr ""\n
438+ ''')
439+
440+ def test_multi(self):
441+ export_pot._poentry_per_paragraph(
442+ self._outf,
443+ 'dummy',
444+ 10,
445+ '''spam\nham\negg\n\nSPAM\nHAM\nEGG\n'''
446+ )
447+ self.check_output('''\
448+ #: dummy:10
449+ msgid ""
450+ "spam\\n"
451+ "ham\\n"
452+ "egg"
453+ msgstr ""
454+
455+ #: dummy:14
456+ msgid ""
457+ "SPAM\\n"
458+ "HAM\\n"
459+ "EGG\\n"
460+ msgstr ""\n
461+ ''')
462
463=== added directory 'po'
464=== modified file 'setup.py'
465--- setup.py 2011-05-11 16:35:34 +0000
466+++ setup.py 2011-05-15 17:12:40 +0000
467@@ -69,7 +69,8 @@
468 'tests/ssl_certs/ca.crt',
469 'tests/ssl_certs/server_without_pass.key',
470 'tests/ssl_certs/server_with_pass.key',
471- 'tests/ssl_certs/server.crt'
472+ 'tests/ssl_certs/server.crt',
473+ 'locale/*/LC_MESSAGES/*.mo',
474 ]},
475 }
476
477@@ -152,6 +153,10 @@
478 Generate bzr.1.
479 """
480
481+ sub_commands = build.sub_commands + [
482+ ('build_mo', lambda _: True),
483+ ]
484+
485 def run(self):
486 build.run(self)
487
488@@ -163,8 +168,12 @@
489 ## Setup
490 ########################
491
492+from tools.build_mo import build_mo
493+
494 command_classes = {'install_scripts': my_install_scripts,
495- 'build': bzr_build}
496+ 'build': bzr_build,
497+ 'build_mo': build_mo,
498+ }
499 from distutils import log
500 from distutils.errors import CCompilerError, DistutilsPlatformError
501 from distutils.extension import Extension
502
503=== added file 'tools/build_mo.py'
504--- tools/build_mo.py 1970-01-01 00:00:00 +0000
505+++ tools/build_mo.py 2011-05-15 17:12:40 +0000
506@@ -0,0 +1,125 @@
507+# -*- coding: utf-8 -*-
508+#
509+# Copyright (C) 2007 Lukáš Lalinský <lalinsky@gmail.com>
510+# Copyright (C) 2007,2009 Alexander Belchenko <bialix@ukr.net>
511+# Copyright 2011 Canonical Ltd.
512+#
513+# This program is free software; you can redistribute it and/or
514+# modify it under the terms of the GNU General Public License
515+# as published by the Free Software Foundation; either version 2
516+# of the License, or (at your option) any later version.
517+#
518+# This program is distributed in the hope that it will be useful,
519+# but WITHOUT ANY WARRANTY; without even the implied warranty of
520+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
521+# GNU General Public License for more details.
522+#
523+# You should have received a copy of the GNU General Public License
524+# along with this program; if not, write to the Free Software
525+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
526+
527+# This code is bring from bzr-explorer and modified for bzr.
528+
529+"""build_mo command for setup.py"""
530+
531+from distutils import log
532+from distutils.command.build import build
533+from distutils.core import Command
534+from distutils.dep_util import newer
535+from distutils.spawn import find_executable
536+import os
537+import re
538+
539+import msgfmt
540+
541+class build_mo(Command):
542+ """Subcommand of build command: build_mo"""
543+
544+ description = 'compile po files to mo files'
545+
546+ # List of options:
547+ # - long name,
548+ # - short name (None if no short name),
549+ # - help string.
550+ user_options = [('build-dir=', 'd', 'Directory to build locale files'),
551+ ('output-base=', 'o', 'mo-files base name'),
552+ ('source-dir=', None, 'Directory with sources po files'),
553+ ('force', 'f', 'Force creation of mo files'),
554+ ('lang=', None, 'Comma-separated list of languages '
555+ 'to process'),
556+ ]
557+
558+ boolean_options = ['force']
559+
560+ def initialize_options(self):
561+ self.build_dir = None
562+ self.output_base = None
563+ self.source_dir = None
564+ self.force = None
565+ self.lang = None
566+
567+ def finalize_options(self):
568+ self.set_undefined_options('build', ('force', 'force'))
569+ self.prj_name = self.distribution.get_name()
570+ if self.build_dir is None:
571+ self.build_dir = 'bzrlib/locale'
572+ if not self.output_base:
573+ self.output_base = self.prj_name or 'messages'
574+ if self.source_dir is None:
575+ self.source_dir = 'po'
576+ if self.lang is None:
577+ if self.prj_name:
578+ re_po = re.compile(r'^(?:%s-)?([a-zA-Z_]+)\.po$' % self.prj_name)
579+ else:
580+ re_po = re.compile(r'^([a-zA-Z_]+)\.po$')
581+ self.lang = []
582+ for i in os.listdir(self.source_dir):
583+ mo = re_po.match(i)
584+ if mo:
585+ self.lang.append(mo.group(1))
586+ else:
587+ self.lang = [i.strip() for i in self.lang.split(',') if i.strip()]
588+
589+ def run(self):
590+ """Run msgfmt for each language"""
591+ if not self.lang:
592+ return
593+
594+ if 'en' in self.lang:
595+ if find_executable('msginit') is None:
596+ log.warn("GNU gettext msginit utility not found!")
597+ log.warn("Skip creating English PO file.")
598+ else:
599+ log.info('Creating English PO file...')
600+ pot = (self.prj_name or 'messages') + '.pot'
601+ if self.prj_name:
602+ en_po = '%s-en.po' % self.prj_name
603+ else:
604+ en_po = 'en.po'
605+ self.spawn(['msginit',
606+ '--no-translator',
607+ '-l', 'en',
608+ '-i', os.path.join(self.source_dir, pot),
609+ '-o', os.path.join(self.source_dir, en_po),
610+ ])
611+
612+ basename = self.output_base
613+ if not basename.endswith('.mo'):
614+ basename += '.mo'
615+
616+ po_prefix = ''
617+ if self.prj_name:
618+ po_prefix = self.prj_name + '-'
619+ for lang in self.lang:
620+ po = os.path.join('po', lang + '.po')
621+ if not os.path.isfile(po):
622+ po = os.path.join('po', po_prefix + lang + '.po')
623+ dir_ = os.path.join(self.build_dir, lang, 'LC_MESSAGES')
624+ self.mkpath(dir_)
625+ mo = os.path.join(dir_, basename)
626+ if self.force or newer(po, mo):
627+ log.info('Compile: %s -> %s' % (po, mo))
628+ msgfmt.make(po, mo)
629+
630+
631+build.sub_commands.insert(0, ('build_mo', None))
632
633=== added file 'tools/msgfmt.py'
634--- tools/msgfmt.py 1970-01-01 00:00:00 +0000
635+++ tools/msgfmt.py 2011-05-15 17:12:40 +0000
636@@ -0,0 +1,208 @@
637+#! /usr/bin/env python
638+# -*- coding: utf-8 -*-
639+# Written by Martin v. Lowis <loewis@informatik.hu-berlin.de>
640+
641+# This script is copied from Python/Tools/i18n/msgfmt.py
642+# It licensed under PSF license which compatible with GPL.
643+#
644+# ChangeLog
645+# * Convert encoding from iso-8859-1 to utf-8
646+# * Fix http://bugs.python.org/issue9741
647+
648+"""Generate binary message catalog from textual translation description.
649+
650+This program converts a textual Uniforum-style message catalog (.po file) into
651+a binary GNU catalog (.mo file). This is essentially the same function as the
652+GNU msgfmt program, however, it is a simpler implementation.
653+
654+Usage: msgfmt.py [OPTIONS] filename.po
655+
656+Options:
657+ -o file
658+ --output-file=file
659+ Specify the output file to write to. If omitted, output will go to a
660+ file named filename.mo (based off the input file name).
661+
662+ -h
663+ --help
664+ Print this message and exit.
665+
666+ -V
667+ --version
668+ Display version information and exit.
669+"""
670+
671+import sys
672+import os
673+import getopt
674+import struct
675+import array
676+
677+__version__ = "1.1"
678+
679+MESSAGES = {}
680+
681+
682+def usage(code, msg=''):
683+ print >> sys.stderr, __doc__
684+ if msg:
685+ print >> sys.stderr, msg
686+ sys.exit(code)
687+
688+
689+def add(id, str, fuzzy):
690+ "Add a non-fuzzy translation to the dictionary."
691+ global MESSAGES
692+ if not fuzzy and str:
693+ MESSAGES[id] = str
694+
695+
696+def generate():
697+ "Return the generated output."
698+ global MESSAGES
699+ keys = MESSAGES.keys()
700+ # the keys are sorted in the .mo file
701+ keys.sort()
702+ offsets = []
703+ ids = strs = ''
704+ for id in keys:
705+ # For each string, we need size and file offset. Each string is NUL
706+ # terminated; the NUL does not count into the size.
707+ offsets.append((len(ids), len(id), len(strs), len(MESSAGES[id])))
708+ ids += id + '\0'
709+ strs += MESSAGES[id] + '\0'
710+ output = ''
711+ # The header is 7 32-bit unsigned integers. We don't use hash tables, so
712+ # the keys start right after the index tables.
713+ # translated string.
714+ keystart = 7*4+16*len(keys)
715+ # and the values start after the keys
716+ valuestart = keystart + len(ids)
717+ koffsets = []
718+ voffsets = []
719+ # The string table first has the list of keys, then the list of values.
720+ # Each entry has first the size of the string, then the file offset.
721+ for o1, l1, o2, l2 in offsets:
722+ koffsets += [l1, o1+keystart]
723+ voffsets += [l2, o2+valuestart]
724+ offsets = koffsets + voffsets
725+ output = struct.pack("Iiiiiii",
726+ 0x950412deL, # Magic
727+ 0, # Version
728+ len(keys), # # of entries
729+ 7*4, # start of key index
730+ 7*4+len(keys)*8, # start of value index
731+ 0, 0) # size and offset of hash table
732+ output += array.array("i", offsets).tostring()
733+ output += ids
734+ output += strs
735+ return output
736+
737+
738+def make(filename, outfile):
739+ # Fix http://bugs.python.org/issue9741
740+ global MESSAGES
741+ MESSAGES.clear()
742+ ID = 1
743+ STR = 2
744+
745+ # Compute .mo name from .po name and arguments
746+ if filename.endswith('.po'):
747+ infile = filename
748+ else:
749+ infile = filename + '.po'
750+ if outfile is None:
751+ outfile = os.path.splitext(infile)[0] + '.mo'
752+
753+ try:
754+ lines = open(infile).readlines()
755+ except IOError, msg:
756+ print >> sys.stderr, msg
757+ sys.exit(1)
758+
759+ section = None
760+ fuzzy = 0
761+
762+ # Parse the catalog
763+ lno = 0
764+ for l in lines:
765+ lno += 1
766+ # If we get a comment line after a msgstr, this is a new entry
767+ if l[0] == '#' and section == STR:
768+ add(msgid, msgstr, fuzzy)
769+ section = None
770+ fuzzy = 0
771+ # Record a fuzzy mark
772+ if l[:2] == '#,' and 'fuzzy' in l:
773+ fuzzy = 1
774+ # Skip comments
775+ if l[0] == '#':
776+ continue
777+ # Now we are in a msgid section, output previous section
778+ if l.startswith('msgid'):
779+ if section == STR:
780+ add(msgid, msgstr, fuzzy)
781+ section = ID
782+ l = l[5:]
783+ msgid = msgstr = ''
784+ # Now we are in a msgstr section
785+ elif l.startswith('msgstr'):
786+ section = STR
787+ l = l[6:]
788+ # Skip empty lines
789+ l = l.strip()
790+ if not l:
791+ continue
792+ # XXX: Does this always follow Python escape semantics?
793+ l = eval(l)
794+ if section == ID:
795+ msgid += l
796+ elif section == STR:
797+ msgstr += l
798+ else:
799+ print >> sys.stderr, 'Syntax error on %s:%d' % (infile, lno), \
800+ 'before:'
801+ print >> sys.stderr, l
802+ sys.exit(1)
803+ # Add last entry
804+ if section == STR:
805+ add(msgid, msgstr, fuzzy)
806+
807+ # Compute output
808+ output = generate()
809+
810+ try:
811+ open(outfile,"wb").write(output)
812+ except IOError,msg:
813+ print >> sys.stderr, msg
814+
815+
816+def main():
817+ try:
818+ opts, args = getopt.getopt(sys.argv[1:], 'hVo:',
819+ ['help', 'version', 'output-file='])
820+ except getopt.error, msg:
821+ usage(1, msg)
822+
823+ outfile = None
824+ # parse options
825+ for opt, arg in opts:
826+ if opt in ('-h', '--help'):
827+ usage(0)
828+ elif opt in ('-V', '--version'):
829+ print >> sys.stderr, "msgfmt.py", __version__
830+ sys.exit(0)
831+ elif opt in ('-o', '--output-file'):
832+ outfile = arg
833+ # do it
834+ if not args:
835+ print >> sys.stderr, 'No input file given'
836+ print >> sys.stderr, "Try `msgfmt --help' for more information."
837+ return
838+
839+ for filename in args:
840+ make(filename, outfile)
841+
842+
843+if __name__ == '__main__':
844+ main()