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
=== modified file 'Makefile'
--- Makefile 2011-04-18 00:44:08 +0000
+++ Makefile 2011-05-15 17:12:40 +0000
@@ -420,6 +420,26 @@
420 $(PYTHON) tools/win32/ostools.py remove dist420 $(PYTHON) tools/win32/ostools.py remove dist
421421
422422
423# i18n targets
424
425.PHONY: update-pot po/bzr.pot
426update-pot: po/bzr.pot
427
428TRANSLATABLE_PYFILES:=$(shell find bzrlib -name '*.py' \
429 | grep -v 'bzrlib/tests/' \
430 | grep -v 'bzrlib/doc' \
431 )
432
433po/bzr.pot: $(PYFILES) $(DOCFILES)
434 $(PYTHON) ./bzr export-pot > po/bzr.pot
435 echo $(TRANSLATABLE_PYFILES) | xargs \
436 xgettext --package-name "bzr" \
437 --msgid-bugs-address "<bazaar@canonical.com>" \
438 --copyright-holder "Canonical" \
439 --from-code ISO-8859-1 --join --sort-by-file --add-comments=i18n: \
440 -d bzr -p po -o bzr.pot
441
442
423### Packaging Targets ###443### Packaging Targets ###
424444
425.PHONY: dist check-dist-tarball445.PHONY: dist check-dist-tarball
426446
=== modified file 'bzrlib/builtins.py'
--- bzrlib/builtins.py 2011-05-03 13:53:46 +0000
+++ bzrlib/builtins.py 2011-05-15 17:12:40 +0000
@@ -6154,6 +6154,16 @@
6154 self.outf.write('%s %s\n' % (path, location))6154 self.outf.write('%s %s\n' % (path, location))
61556155
61566156
6157class cmd_export_pot(Command):
6158 __doc__ = """Export command helps and error messages in po format."""
6159
6160 hidden = True
6161
6162 def run(self):
6163 from bzrlib.export_pot import export_pot
6164 export_pot(self.outf)
6165
6166
6157def _register_lazy_builtins():6167def _register_lazy_builtins():
6158 # register lazy builtins from other modules; called at startup and should6168 # register lazy builtins from other modules; called at startup and should
6159 # be only called once.6169 # be only called once.
61606170
=== added file 'bzrlib/export_pot.py'
--- bzrlib/export_pot.py 1970-01-01 00:00:00 +0000
+++ bzrlib/export_pot.py 2011-05-15 17:12:40 +0000
@@ -0,0 +1,241 @@
1# Copyright (C) 2011 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17# The normalize function is taken from pygettext which is distributed
18# with Python under the Python License, which is GPL compatible.
19
20"""Extract docstrings from Bazaar commands.
21"""
22
23import inspect
24import os
25
26from bzrlib import (
27 commands as _mod_commands,
28 errors,
29 help_topics,
30 plugin,
31 )
32from bzrlib.trace import (
33 mutter,
34 note,
35 )
36
37
38def _escape(s):
39 s = (s.replace('\\', '\\\\')
40 .replace('\n', '\\n')
41 .replace('\r', '\\r')
42 .replace('\t', '\\t')
43 .replace('"', '\\"')
44 )
45 return s
46
47def _normalize(s):
48 # This converts the various Python string types into a format that
49 # is appropriate for .po files, namely much closer to C style.
50 lines = s.split('\n')
51 if len(lines) == 1:
52 s = '"' + _escape(s) + '"'
53 else:
54 if not lines[-1]:
55 del lines[-1]
56 lines[-1] = lines[-1] + '\n'
57 lines = map(_escape, lines)
58 lineterm = '\\n"\n"'
59 s = '""\n"' + lineterm.join(lines) + '"'
60 return s
61
62
63_FOUND_MSGID = None # set by entry function.
64
65def _poentry(outf, path, lineno, s, comment=None):
66 if s in _FOUND_MSGID:
67 return
68 _FOUND_MSGID.add(s)
69 if comment is None:
70 comment = ''
71 else:
72 comment = "# %s\n" % comment
73 mutter("Exporting msg %r at line %d in %r", s[:20], lineno, path)
74 print >>outf, ('#: %s:%d\n' % (path, lineno) +
75 comment+
76 'msgid %s\n' % _normalize(s) +
77 'msgstr ""\n')
78
79def _poentry_per_paragraph(outf, path, lineno, msgid):
80 # TODO: How to split long help?
81 paragraphs = msgid.split('\n\n')
82 for p in paragraphs:
83 _poentry(outf, path, lineno, p)
84 lineno += p.count('\n') + 2
85
86_LAST_CACHE = _LAST_CACHED_SRC = None
87
88def _offsets_of_literal(src):
89 global _LAST_CACHE, _LAST_CACHED_SRC
90 if src == _LAST_CACHED_SRC:
91 return _LAST_CACHE.copy()
92
93 import ast
94 root = ast.parse(src)
95 offsets = {}
96 for node in ast.walk(root):
97 if not isinstance(node, ast.Str):
98 continue
99 offsets[node.s] = node.lineno - node.s.count('\n')
100
101 _LAST_CACHED_SRC = src
102 _LAST_CACHE = offsets.copy()
103 return offsets
104
105def _standard_options(outf):
106 from bzrlib.option import Option
107 src = inspect.findsource(Option)[0]
108 src = ''.join(src)
109 path = 'bzrlib/option.py'
110 offsets = _offsets_of_literal(src)
111
112 for name in sorted(Option.OPTIONS.keys()):
113 opt = Option.OPTIONS[name]
114 if getattr(opt, 'hidden', False):
115 continue
116 if getattr(opt, 'title', None):
117 lineno = offsets.get(opt.title, 9999)
118 if lineno == 9999:
119 note("%r is not found in bzrlib/option.py" % opt.title)
120 _poentry(outf, path, lineno, opt.title,
121 'title of %r option' % name)
122 if getattr(opt, 'help', None):
123 lineno = offsets.get(opt.help, 9999)
124 if lineno == 9999:
125 note("%r is not found in bzrlib/option.py" % opt.help)
126 _poentry(outf, path, lineno, opt.help,
127 'help of %r option' % name)
128
129def _command_options(outf, path, cmd):
130 src, default_lineno = inspect.findsource(cmd.__class__)
131 offsets = _offsets_of_literal(''.join(src))
132 for opt in cmd.takes_options:
133 if isinstance(opt, str):
134 continue
135 if getattr(opt, 'hidden', False):
136 continue
137 name = opt.name
138 if getattr(opt, 'title', None):
139 lineno = offsets.get(opt.title, default_lineno)
140 _poentry(outf, path, lineno, opt.title,
141 'title of %r option of %r command' % (name, cmd.name()))
142 if getattr(opt, 'help', None):
143 lineno = offsets.get(opt.help, default_lineno)
144 _poentry(outf, path, lineno, opt.help,
145 'help of %r option of %r command' % (name, cmd.name()))
146
147
148def _write_command_help(outf, cmd_name, cmd):
149 path = inspect.getfile(cmd.__class__)
150 if path.endswith('.pyc'):
151 path = path[:-1]
152 path = os.path.relpath(path)
153 src, lineno = inspect.findsource(cmd.__class__)
154 offsets = _offsets_of_literal(''.join(src))
155 lineno = offsets[cmd.__doc__]
156 doc = inspect.getdoc(cmd)
157
158 _poentry_per_paragraph(outf, path, lineno, doc)
159 _command_options(outf, path, cmd)
160
161def _command_helps(outf):
162 """Extract docstrings from path.
163
164 This respects the Bazaar cmdtable/table convention and will
165 only extract docstrings from functions mentioned in these tables.
166 """
167 from glob import glob
168
169 # builtin commands
170 for cmd_name in _mod_commands.builtin_command_names():
171 command = _mod_commands.get_cmd_object(cmd_name, False)
172 if command.hidden:
173 continue
174 note("Exporting messages from builtin command: %s", cmd_name)
175 _write_command_help(outf, cmd_name, command)
176
177 plugin_path = plugin.get_core_plugin_path()
178 core_plugins = glob(plugin_path + '/*/__init__.py')
179 core_plugins = [os.path.basename(os.path.dirname(p))
180 for p in core_plugins]
181 # core plugins
182 for cmd_name in _mod_commands.plugin_command_names():
183 command = _mod_commands.get_cmd_object(cmd_name, False)
184 if command.hidden:
185 continue
186 if command.plugin_name() not in core_plugins:
187 # skip non-core plugins
188 # TODO: Support extracting from third party plugins.
189 continue
190 note("Exporting messages from plugin command: %s in %s",
191 cmd_name, command.plugin_name())
192 _write_command_help(outf, cmd_name, command)
193
194
195def _error_messages(outf):
196 """Extract fmt string from bzrlib.errors."""
197 path = errors.__file__
198 if path.endswith('.pyc'):
199 path = path[:-1]
200 offsets = _offsets_of_literal(open(path).read())
201
202 base_klass = errors.BzrError
203 for name in dir(errors):
204 klass = getattr(errors, name)
205 if not inspect.isclass(klass):
206 continue
207 if not issubclass(klass, base_klass):
208 continue
209 if klass is base_klass:
210 continue
211 if klass.internal_error:
212 continue
213 fmt = getattr(klass, "_fmt", None)
214 if fmt:
215 note("Exporting message from error: %s", name)
216 _poentry(outf, 'bzrlib/errors.py',
217 offsets.get(fmt, 9999), fmt)
218
219def _help_topics(outf):
220 topic_registry = help_topics.topic_registry
221 for key in topic_registry.keys():
222 doc = topic_registry.get(key)
223 if isinstance(doc, str):
224 _poentry_per_paragraph(
225 outf,
226 'dummy/help_topics/'+key+'/detail.txt',
227 1, doc)
228
229 summary = topic_registry.get_summary(key)
230 if summary is not None:
231 _poentry(outf, 'dummy/help_topics/'+key+'/summary.txt',
232 1, summary)
233
234def export_pot(outf):
235 global _FOUND_MSGID
236 _FOUND_MSGID = set()
237 _standard_options(outf)
238 _command_helps(outf)
239 _error_messages(outf)
240 # disable exporting help topics until we decide how to translate it.
241 #_help_topics(outf)
0242
=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py 2011-05-13 12:51:05 +0000
+++ bzrlib/tests/__init__.py 2011-05-15 17:12:40 +0000
@@ -3783,6 +3783,7 @@
3783 'bzrlib.tests.test_eol_filters',3783 'bzrlib.tests.test_eol_filters',
3784 'bzrlib.tests.test_errors',3784 'bzrlib.tests.test_errors',
3785 'bzrlib.tests.test_export',3785 'bzrlib.tests.test_export',
3786 'bzrlib.tests.test_export_pot',
3786 'bzrlib.tests.test_extract',3787 'bzrlib.tests.test_extract',
3787 'bzrlib.tests.test_fetch',3788 'bzrlib.tests.test_fetch',
3788 'bzrlib.tests.test_fixtures',3789 'bzrlib.tests.test_fixtures',
37893790
=== added file 'bzrlib/tests/test_export_pot.py'
--- bzrlib/tests/test_export_pot.py 1970-01-01 00:00:00 +0000
+++ bzrlib/tests/test_export_pot.py 2011-05-15 17:12:40 +0000
@@ -0,0 +1,147 @@
1# Copyright (C) 2011 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17from cStringIO import StringIO
18import textwrap
19
20from bzrlib import (
21 export_pot,
22 tests,
23 )
24
25class TestEscape(tests.TestCase):
26
27 def test_simple_escape(self):
28 self.assertEqual(
29 export_pot._escape('foobar'),
30 'foobar')
31
32 s = '''foo\nbar\r\tbaz\\"spam"'''
33 e = '''foo\\nbar\\r\\tbaz\\\\\\"spam\\"'''
34 self.assertEqual(export_pot._escape(s), e)
35
36 def test_complex_escape(self):
37 s = '''\\r \\\n'''
38 e = '''\\\\r \\\\\\n'''
39 self.assertEqual(export_pot._escape(s), e)
40
41
42class TestNormalize(tests.TestCase):
43
44 def test_single_line(self):
45 s = 'foobar'
46 e = '"foobar"'
47 self.assertEqual(export_pot._normalize(s), e)
48
49 s = 'foo"bar'
50 e = '"foo\\"bar"'
51 self.assertEqual(export_pot._normalize(s), e)
52
53 def test_multi_lines(self):
54 s = 'foo\nbar\n'
55 e = '""\n"foo\\n"\n"bar\\n"'
56 self.assertEqual(export_pot._normalize(s), e)
57
58 s = '\nfoo\nbar\n'
59 e = ('""\n'
60 '"\\n"\n'
61 '"foo\\n"\n'
62 '"bar\\n"')
63 self.assertEqual(export_pot._normalize(s), e)
64
65
66class PoEntryTestCase(tests.TestCase):
67
68 def setUp(self):
69 self.overrideAttr(export_pot, '_FOUND_MSGID', set())
70 self._outf = StringIO()
71 super(PoEntryTestCase, self).setUp()
72
73 def check_output(self, expected):
74 self.assertEqual(
75 self._outf.getvalue(),
76 textwrap.dedent(expected)
77 )
78
79class TestPoEntry(PoEntryTestCase):
80
81 def test_simple(self):
82 export_pot._poentry(self._outf, 'dummy', 1, "spam")
83 export_pot._poentry(self._outf, 'dummy', 2, "ham", 'EGG')
84 self.check_output('''\
85 #: dummy:1
86 msgid "spam"
87 msgstr ""
88
89 #: dummy:2
90 # EGG
91 msgid "ham"
92 msgstr ""
93
94 ''')
95
96 def test_duplicate(self):
97 export_pot._poentry(self._outf, 'dummy', 1, "spam")
98 # This should be ignored.
99 export_pot._poentry(self._outf, 'dummy', 2, "spam", 'EGG')
100
101 self.check_output('''\
102 #: dummy:1
103 msgid "spam"
104 msgstr ""\n
105 ''')
106
107
108class TestPoentryPerPergraph(PoEntryTestCase):
109
110 def test_single(self):
111 export_pot._poentry_per_paragraph(
112 self._outf,
113 'dummy',
114 10,
115 '''foo\nbar\nbaz\n'''
116 )
117 self.check_output('''\
118 #: dummy:10
119 msgid ""
120 "foo\\n"
121 "bar\\n"
122 "baz\\n"
123 msgstr ""\n
124 ''')
125
126 def test_multi(self):
127 export_pot._poentry_per_paragraph(
128 self._outf,
129 'dummy',
130 10,
131 '''spam\nham\negg\n\nSPAM\nHAM\nEGG\n'''
132 )
133 self.check_output('''\
134 #: dummy:10
135 msgid ""
136 "spam\\n"
137 "ham\\n"
138 "egg"
139 msgstr ""
140
141 #: dummy:14
142 msgid ""
143 "SPAM\\n"
144 "HAM\\n"
145 "EGG\\n"
146 msgstr ""\n
147 ''')
0148
=== added directory 'po'
=== modified file 'setup.py'
--- setup.py 2011-05-11 16:35:34 +0000
+++ setup.py 2011-05-15 17:12:40 +0000
@@ -69,7 +69,8 @@
69 'tests/ssl_certs/ca.crt',69 'tests/ssl_certs/ca.crt',
70 'tests/ssl_certs/server_without_pass.key',70 'tests/ssl_certs/server_without_pass.key',
71 'tests/ssl_certs/server_with_pass.key',71 'tests/ssl_certs/server_with_pass.key',
72 'tests/ssl_certs/server.crt'72 'tests/ssl_certs/server.crt',
73 'locale/*/LC_MESSAGES/*.mo',
73 ]},74 ]},
74 }75 }
7576
@@ -152,6 +153,10 @@
152 Generate bzr.1.153 Generate bzr.1.
153 """154 """
154155
156 sub_commands = build.sub_commands + [
157 ('build_mo', lambda _: True),
158 ]
159
155 def run(self):160 def run(self):
156 build.run(self)161 build.run(self)
157162
@@ -163,8 +168,12 @@
163## Setup168## Setup
164########################169########################
165170
171from tools.build_mo import build_mo
172
166command_classes = {'install_scripts': my_install_scripts,173command_classes = {'install_scripts': my_install_scripts,
167 'build': bzr_build}174 'build': bzr_build,
175 'build_mo': build_mo,
176 }
168from distutils import log177from distutils import log
169from distutils.errors import CCompilerError, DistutilsPlatformError178from distutils.errors import CCompilerError, DistutilsPlatformError
170from distutils.extension import Extension179from distutils.extension import Extension
171180
=== added file 'tools/build_mo.py'
--- tools/build_mo.py 1970-01-01 00:00:00 +0000
+++ tools/build_mo.py 2011-05-15 17:12:40 +0000
@@ -0,0 +1,125 @@
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2007 Lukáš Lalinský <lalinsky@gmail.com>
4# Copyright (C) 2007,2009 Alexander Belchenko <bialix@ukr.net>
5# Copyright 2011 Canonical Ltd.
6#
7# This program is free software; you can redistribute it and/or
8# modify it under the terms of the GNU General Public License
9# as published by the Free Software Foundation; either version 2
10# of the License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
21# This code is bring from bzr-explorer and modified for bzr.
22
23"""build_mo command for setup.py"""
24
25from distutils import log
26from distutils.command.build import build
27from distutils.core import Command
28from distutils.dep_util import newer
29from distutils.spawn import find_executable
30import os
31import re
32
33import msgfmt
34
35class build_mo(Command):
36 """Subcommand of build command: build_mo"""
37
38 description = 'compile po files to mo files'
39
40 # List of options:
41 # - long name,
42 # - short name (None if no short name),
43 # - help string.
44 user_options = [('build-dir=', 'd', 'Directory to build locale files'),
45 ('output-base=', 'o', 'mo-files base name'),
46 ('source-dir=', None, 'Directory with sources po files'),
47 ('force', 'f', 'Force creation of mo files'),
48 ('lang=', None, 'Comma-separated list of languages '
49 'to process'),
50 ]
51
52 boolean_options = ['force']
53
54 def initialize_options(self):
55 self.build_dir = None
56 self.output_base = None
57 self.source_dir = None
58 self.force = None
59 self.lang = None
60
61 def finalize_options(self):
62 self.set_undefined_options('build', ('force', 'force'))
63 self.prj_name = self.distribution.get_name()
64 if self.build_dir is None:
65 self.build_dir = 'bzrlib/locale'
66 if not self.output_base:
67 self.output_base = self.prj_name or 'messages'
68 if self.source_dir is None:
69 self.source_dir = 'po'
70 if self.lang is None:
71 if self.prj_name:
72 re_po = re.compile(r'^(?:%s-)?([a-zA-Z_]+)\.po$' % self.prj_name)
73 else:
74 re_po = re.compile(r'^([a-zA-Z_]+)\.po$')
75 self.lang = []
76 for i in os.listdir(self.source_dir):
77 mo = re_po.match(i)
78 if mo:
79 self.lang.append(mo.group(1))
80 else:
81 self.lang = [i.strip() for i in self.lang.split(',') if i.strip()]
82
83 def run(self):
84 """Run msgfmt for each language"""
85 if not self.lang:
86 return
87
88 if 'en' in self.lang:
89 if find_executable('msginit') is None:
90 log.warn("GNU gettext msginit utility not found!")
91 log.warn("Skip creating English PO file.")
92 else:
93 log.info('Creating English PO file...')
94 pot = (self.prj_name or 'messages') + '.pot'
95 if self.prj_name:
96 en_po = '%s-en.po' % self.prj_name
97 else:
98 en_po = 'en.po'
99 self.spawn(['msginit',
100 '--no-translator',
101 '-l', 'en',
102 '-i', os.path.join(self.source_dir, pot),
103 '-o', os.path.join(self.source_dir, en_po),
104 ])
105
106 basename = self.output_base
107 if not basename.endswith('.mo'):
108 basename += '.mo'
109
110 po_prefix = ''
111 if self.prj_name:
112 po_prefix = self.prj_name + '-'
113 for lang in self.lang:
114 po = os.path.join('po', lang + '.po')
115 if not os.path.isfile(po):
116 po = os.path.join('po', po_prefix + lang + '.po')
117 dir_ = os.path.join(self.build_dir, lang, 'LC_MESSAGES')
118 self.mkpath(dir_)
119 mo = os.path.join(dir_, basename)
120 if self.force or newer(po, mo):
121 log.info('Compile: %s -> %s' % (po, mo))
122 msgfmt.make(po, mo)
123
124
125build.sub_commands.insert(0, ('build_mo', None))
0126
=== added file 'tools/msgfmt.py'
--- tools/msgfmt.py 1970-01-01 00:00:00 +0000
+++ tools/msgfmt.py 2011-05-15 17:12:40 +0000
@@ -0,0 +1,208 @@
1#! /usr/bin/env python
2# -*- coding: utf-8 -*-
3# Written by Martin v. Lowis <loewis@informatik.hu-berlin.de>
4
5# This script is copied from Python/Tools/i18n/msgfmt.py
6# It licensed under PSF license which compatible with GPL.
7#
8# ChangeLog
9# * Convert encoding from iso-8859-1 to utf-8
10# * Fix http://bugs.python.org/issue9741
11
12"""Generate binary message catalog from textual translation description.
13
14This program converts a textual Uniforum-style message catalog (.po file) into
15a binary GNU catalog (.mo file). This is essentially the same function as the
16GNU msgfmt program, however, it is a simpler implementation.
17
18Usage: msgfmt.py [OPTIONS] filename.po
19
20Options:
21 -o file
22 --output-file=file
23 Specify the output file to write to. If omitted, output will go to a
24 file named filename.mo (based off the input file name).
25
26 -h
27 --help
28 Print this message and exit.
29
30 -V
31 --version
32 Display version information and exit.
33"""
34
35import sys
36import os
37import getopt
38import struct
39import array
40
41__version__ = "1.1"
42
43MESSAGES = {}
44
45
46def usage(code, msg=''):
47 print >> sys.stderr, __doc__
48 if msg:
49 print >> sys.stderr, msg
50 sys.exit(code)
51
52
53def add(id, str, fuzzy):
54 "Add a non-fuzzy translation to the dictionary."
55 global MESSAGES
56 if not fuzzy and str:
57 MESSAGES[id] = str
58
59
60def generate():
61 "Return the generated output."
62 global MESSAGES
63 keys = MESSAGES.keys()
64 # the keys are sorted in the .mo file
65 keys.sort()
66 offsets = []
67 ids = strs = ''
68 for id in keys:
69 # For each string, we need size and file offset. Each string is NUL
70 # terminated; the NUL does not count into the size.
71 offsets.append((len(ids), len(id), len(strs), len(MESSAGES[id])))
72 ids += id + '\0'
73 strs += MESSAGES[id] + '\0'
74 output = ''
75 # The header is 7 32-bit unsigned integers. We don't use hash tables, so
76 # the keys start right after the index tables.
77 # translated string.
78 keystart = 7*4+16*len(keys)
79 # and the values start after the keys
80 valuestart = keystart + len(ids)
81 koffsets = []
82 voffsets = []
83 # The string table first has the list of keys, then the list of values.
84 # Each entry has first the size of the string, then the file offset.
85 for o1, l1, o2, l2 in offsets:
86 koffsets += [l1, o1+keystart]
87 voffsets += [l2, o2+valuestart]
88 offsets = koffsets + voffsets
89 output = struct.pack("Iiiiiii",
90 0x950412deL, # Magic
91 0, # Version
92 len(keys), # # of entries
93 7*4, # start of key index
94 7*4+len(keys)*8, # start of value index
95 0, 0) # size and offset of hash table
96 output += array.array("i", offsets).tostring()
97 output += ids
98 output += strs
99 return output
100
101
102def make(filename, outfile):
103 # Fix http://bugs.python.org/issue9741
104 global MESSAGES
105 MESSAGES.clear()
106 ID = 1
107 STR = 2
108
109 # Compute .mo name from .po name and arguments
110 if filename.endswith('.po'):
111 infile = filename
112 else:
113 infile = filename + '.po'
114 if outfile is None:
115 outfile = os.path.splitext(infile)[0] + '.mo'
116
117 try:
118 lines = open(infile).readlines()
119 except IOError, msg:
120 print >> sys.stderr, msg
121 sys.exit(1)
122
123 section = None
124 fuzzy = 0
125
126 # Parse the catalog
127 lno = 0
128 for l in lines:
129 lno += 1
130 # If we get a comment line after a msgstr, this is a new entry
131 if l[0] == '#' and section == STR:
132 add(msgid, msgstr, fuzzy)
133 section = None
134 fuzzy = 0
135 # Record a fuzzy mark
136 if l[:2] == '#,' and 'fuzzy' in l:
137 fuzzy = 1
138 # Skip comments
139 if l[0] == '#':
140 continue
141 # Now we are in a msgid section, output previous section
142 if l.startswith('msgid'):
143 if section == STR:
144 add(msgid, msgstr, fuzzy)
145 section = ID
146 l = l[5:]
147 msgid = msgstr = ''
148 # Now we are in a msgstr section
149 elif l.startswith('msgstr'):
150 section = STR
151 l = l[6:]
152 # Skip empty lines
153 l = l.strip()
154 if not l:
155 continue
156 # XXX: Does this always follow Python escape semantics?
157 l = eval(l)
158 if section == ID:
159 msgid += l
160 elif section == STR:
161 msgstr += l
162 else:
163 print >> sys.stderr, 'Syntax error on %s:%d' % (infile, lno), \
164 'before:'
165 print >> sys.stderr, l
166 sys.exit(1)
167 # Add last entry
168 if section == STR:
169 add(msgid, msgstr, fuzzy)
170
171 # Compute output
172 output = generate()
173
174 try:
175 open(outfile,"wb").write(output)
176 except IOError,msg:
177 print >> sys.stderr, msg
178
179
180def main():
181 try:
182 opts, args = getopt.getopt(sys.argv[1:], 'hVo:',
183 ['help', 'version', 'output-file='])
184 except getopt.error, msg:
185 usage(1, msg)
186
187 outfile = None
188 # parse options
189 for opt, arg in opts:
190 if opt in ('-h', '--help'):
191 usage(0)
192 elif opt in ('-V', '--version'):
193 print >> sys.stderr, "msgfmt.py", __version__
194 sys.exit(0)
195 elif opt in ('-o', '--output-file'):
196 outfile = arg
197 # do it
198 if not args:
199 print >> sys.stderr, 'No input file given'
200 print >> sys.stderr, "Try `msgfmt --help' for more information."
201 return
202
203 for filename in args:
204 make(filename, outfile)
205
206
207if __name__ == '__main__':
208 main()