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