|
1 #! /usr/bin/env python |
|
2 # -*- coding: iso-8859-1 -*- |
|
3 # |
|
4 # PYTHON MODULE: MKI18N.PY |
|
5 # ========= |
|
6 # |
|
7 # Abstract: Make Internationalization (i18n) files for an application. |
|
8 # |
|
9 # Copyright Pierre Rouleau. 2003. Released to public domain. |
|
10 # |
|
11 # Last update: Saturday, November 8, 2003. @ 15:55:18. |
|
12 # |
|
13 # File: ROUP2003N01::C:/dev/python/mki18n.py |
|
14 # |
|
15 # RCS $Header: //software/official/MKS/MKS_SI/TV_NT/dev/Python/rcs/mki18n.py 1.5 2003/11/05 19:40:04 PRouleau Exp $ |
|
16 # |
|
17 # Update history: |
|
18 # |
|
19 # - File created: Saturday, June 7, 2003. by Pierre Rouleau |
|
20 # - 10/06/03 rcs : RCS Revision 1.1 2003/06/10 10:06:12 PRouleau |
|
21 # - 10/06/03 rcs : RCS Initial revision |
|
22 # - 23/08/03 rcs : RCS Revision 1.2 2003/06/10 10:54:27 PRouleau |
|
23 # - 23/08/03 P.R.: [code:fix] : The strings encoded in this file are encode in iso-8859-1 format. Added the encoding |
|
24 # notification to Python to comply with Python's 2.3 PEP 263. |
|
25 # - 23/08/03 P.R.: [feature:new] : Added the '-e' switch which is used to force the creation of the empty English .mo file. |
|
26 # - 22/10/03 P.R.: [code] : incorporated utility functions in here to make script self sufficient. |
|
27 # - 05/11/03 rcs : RCS Revision 1.4 2003/10/22 06:39:31 PRouleau |
|
28 # - 05/11/03 P.R.: [code:fix] : included the unixpath() in this file. |
|
29 # - 08/11/03 rcs : RCS Revision 1.5 2003/11/05 19:40:04 PRouleau |
|
30 # |
|
31 # RCS $Log: $ |
|
32 # |
|
33 # |
|
34 # ----------------------------------------------------------------------------- |
|
35 """ |
|
36 mki18n allows you to internationalize your software. You can use it to |
|
37 create the GNU .po files (Portable Object) and the compiled .mo files |
|
38 (Machine Object). |
|
39 |
|
40 mki18n module can be used from the command line or from within a script (see |
|
41 the Usage at the end of this page). |
|
42 |
|
43 Table of Contents |
|
44 ----------------- |
|
45 |
|
46 makePO() -- Build the Portable Object file for the application -- |
|
47 catPO() -- Concatenate one or several PO files with the application domain files. -- |
|
48 makeMO() -- Compile the Portable Object files into the Machine Object stored in the right location. -- |
|
49 printUsage -- Displays how to use this script from the command line -- |
|
50 |
|
51 Scriptexecution -- Runs when invoked from the command line -- |
|
52 |
|
53 |
|
54 NOTE: this module uses GNU gettext utilities. |
|
55 |
|
56 You can get the gettext tools from the following sites: |
|
57 |
|
58 - `GNU FTP site for gettetx`_ where several versions (0.10.40, 0.11.2, 0.11.5 and 0.12.1) are available. |
|
59 Note that you need to use `GNU libiconv`_ to use this. Get it from the `GNU |
|
60 libiconv ftp site`_ and get version 1.9.1 or later. Get the Windows .ZIP |
|
61 files and install the packages inside c:/gnu. All binaries will be stored |
|
62 inside c:/gnu/bin. Just put c:/gnu/bin inside your PATH. You will need |
|
63 the following files: |
|
64 |
|
65 - `gettext-runtime-0.12.1.bin.woe32.zip`_ |
|
66 - `gettext-tools-0.12.1.bin.woe32.zip`_ |
|
67 - `libiconv-1.9.1.bin.woe32.zip`_ |
|
68 |
|
69 |
|
70 .. _GNU libiconv: http://www.gnu.org/software/libiconv/ |
|
71 .. _GNU libiconv ftp site: http://www.ibiblio.org/pub/gnu/libiconv/ |
|
72 .. _gettext-runtime-0.12.1.bin.woe32.zip: ftp://ftp.gnu.org/gnu/gettext/gettext-runtime-0.12.1.bin.woe32.zip |
|
73 .. _gettext-tools-0.12.1.bin.woe32.zip: ftp://ftp.gnu.org/gnu/gettext/gettext-tools-0.12.1.bin.woe32.zip |
|
74 .. _libiconv-1.9.1.bin.woe32.zip: http://www.ibiblio.org/pub/gnu/libiconv/libiconv-1.9.1.bin.woe32.zip |
|
75 |
|
76 """ |
|
77 # ----------------------------------------------------------------------------- |
|
78 # Module Import |
|
79 # ------------- |
|
80 # |
|
81 import os |
|
82 import sys |
|
83 import wx |
|
84 import re |
|
85 |
|
86 # ----------------------------------------------------------------------------- |
|
87 # Global variables |
|
88 # ---------------- |
|
89 # |
|
90 |
|
91 __author__ = "Pierre Rouleau" |
|
92 __version__= "$Revision: 1.5 $" |
|
93 |
|
94 # ----------------------------------------------------------------------------- |
|
95 |
|
96 def getlanguageDict(): |
|
97 languageDict = {} |
|
98 |
|
99 for lang in [x for x in dir(wx) if x.startswith("LANGUAGE")]: |
|
100 i = wx.Locale(wx.LANGUAGE_DEFAULT).GetLanguageInfo(getattr(wx, lang)) |
|
101 if i: |
|
102 languageDict[i.CanonicalName] = i.Description |
|
103 |
|
104 return languageDict |
|
105 |
|
106 XSD_STRING_MODEL = re.compile("<xsd\:(?:element|attribute) name=\"([^\"]*)\"[^\>]*\>") |
|
107 |
|
108 # ----------------------------------------------------------------------------- |
|
109 # m a k e P O ( ) -- Build the Portable Object file for the application -- |
|
110 # ^^^^^^^^^^^^^^^ |
|
111 # |
|
112 def makePO(applicationDirectoryPath, applicationDomain=None, verbose=0) : |
|
113 """Build the Portable Object Template file for the application. |
|
114 |
|
115 makePO builds the .pot file for the application stored inside |
|
116 a specified directory by running xgettext for all application source |
|
117 files. It finds the name of all files by looking for a file called 'app.fil'. |
|
118 If this file does not exists, makePo raises an IOError exception. |
|
119 By default the application domain (the application |
|
120 name) is the same as the directory name but it can be overridden by the |
|
121 'applicationDomain' argument. |
|
122 |
|
123 makePO always creates a new file called messages.pot. If it finds files |
|
124 of the form app_xx.po where 'app' is the application name and 'xx' is one |
|
125 of the ISO 639 two-letter language codes, makePO resynchronizes those |
|
126 files with the latest extracted strings (now contained in messages.pot). |
|
127 This process updates all line location number in the language-specific |
|
128 .po files and may also create new entries for translation (or comment out |
|
129 some). The .po file is not changed, instead a new file is created with |
|
130 the .new extension appended to the name of the .po file. |
|
131 |
|
132 By default the function does not display what it is doing. Set the |
|
133 verbose argument to 1 to force it to print its commands. |
|
134 """ |
|
135 |
|
136 if applicationDomain is None: |
|
137 applicationName = fileBaseOf(applicationDirectoryPath,withPath=0) |
|
138 else: |
|
139 applicationName = applicationDomain |
|
140 currentDir = os.getcwd() |
|
141 os.chdir(applicationDirectoryPath) |
|
142 if not os.path.exists('app.fil'): |
|
143 raise IOError(2,'No module file: app.fil') |
|
144 |
|
145 # Steps: |
|
146 # Use xgettext to parse all application modules |
|
147 # The following switches are used: |
|
148 # |
|
149 # -s : sort output by string content (easier to use when we need to merge several .po files) |
|
150 # --files-from=app.fil : The list of files is taken from the file: app.fil |
|
151 # --output= : specifies the name of the output file (using a .pot extension) |
|
152 cmd = 'xgettext -s --no-wrap --language=Python --files-from=app.fil --output=messages.pot' |
|
153 if verbose: print cmd |
|
154 os.system(cmd) |
|
155 |
|
156 appfil_file = open("app.fil", 'r') |
|
157 messages_file = open("messages.pot", 'a') |
|
158 messages_file.write(""" |
|
159 #: Extra XSD strings |
|
160 """) |
|
161 words_found = {} |
|
162 for filepath in appfil_file.xreadlines(): |
|
163 code_file = open(filepath.strip(), 'r') |
|
164 for match in XSD_STRING_MODEL.finditer(code_file.read()): |
|
165 word = match.group(1) |
|
166 if not words_found.get(word, False): |
|
167 words_found[word] = True |
|
168 messages_file.write(""" |
|
169 msgid "%s" |
|
170 msgstr "" |
|
171 """%word) |
|
172 code_file.close() |
|
173 messages_file.close() |
|
174 appfil_file.close() |
|
175 |
|
176 languageDict = getlanguageDict() |
|
177 |
|
178 for langCode in languageDict.keys(): |
|
179 if langCode == 'en': |
|
180 pass |
|
181 else: |
|
182 langPOfileName = "%s_%s.po" % (applicationName , langCode) |
|
183 if os.path.exists(langPOfileName): |
|
184 cmd = 'msgmerge -s --no-wrap "%s" messages.pot > "%s.new"' % (langPOfileName, langPOfileName) |
|
185 if verbose: print cmd |
|
186 os.system(cmd) |
|
187 os.chdir(currentDir) |
|
188 |
|
189 # ----------------------------------------------------------------------------- |
|
190 # c a t P O ( ) -- Concatenate one or several PO files with the application domain files. -- |
|
191 # ^^^^^^^^^^^^^ |
|
192 # |
|
193 def catPO(applicationDirectoryPath, listOf_extraPo, applicationDomain=None, targetDir=None, verbose=0) : |
|
194 """Concatenate one or several PO files with the application domain files. |
|
195 """ |
|
196 |
|
197 if applicationDomain is None: |
|
198 applicationName = fileBaseOf(applicationDirectoryPath,withPath=0) |
|
199 else: |
|
200 applicationName = applicationDomain |
|
201 currentDir = os.getcwd() |
|
202 os.chdir(applicationDirectoryPath) |
|
203 |
|
204 languageDict = getlanguageDict() |
|
205 |
|
206 for langCode in languageDict.keys(): |
|
207 if langCode == 'en': |
|
208 pass |
|
209 else: |
|
210 langPOfileName = "%s_%s.po" % (applicationName , langCode) |
|
211 if os.path.exists(langPOfileName): |
|
212 fileList = '' |
|
213 for fileName in listOf_extraPo: |
|
214 fileList += ("%s_%s.po " % (fileName,langCode)) |
|
215 cmd = "msgcat -s --no-wrap %s %s > %s.cat" % (langPOfileName, fileList, langPOfileName) |
|
216 if verbose: print cmd |
|
217 os.system(cmd) |
|
218 if targetDir is None: |
|
219 pass |
|
220 else: |
|
221 mo_targetDir = "%s/%s/LC_MESSAGES" % (targetDir,langCode) |
|
222 cmd = "msgfmt --output-file=%s/%s.mo %s_%s.po.cat" % (mo_targetDir,applicationName,applicationName,langCode) |
|
223 if verbose: print cmd |
|
224 os.system(cmd) |
|
225 os.chdir(currentDir) |
|
226 |
|
227 # ----------------------------------------------------------------------------- |
|
228 # m a k e M O ( ) -- Compile the Portable Object files into the Machine Object stored in the right location. -- |
|
229 # ^^^^^^^^^^^^^^^ |
|
230 # |
|
231 def makeMO(applicationDirectoryPath,targetDir='./locale',applicationDomain=None, verbose=0, forceEnglish=0) : |
|
232 """Compile the Portable Object files into the Machine Object stored in the right location. |
|
233 |
|
234 makeMO converts all translated language-specific PO files located inside |
|
235 the application directory into the binary .MO files stored inside the |
|
236 LC_MESSAGES sub-directory for the found locale files. |
|
237 |
|
238 makeMO searches for all files that have a name of the form 'app_xx.po' |
|
239 inside the application directory specified by the first argument. The |
|
240 'app' is the application domain name (that can be specified by the |
|
241 applicationDomain argument or is taken from the directory name). The 'xx' |
|
242 corresponds to one of the ISO 639 two-letter language codes. |
|
243 |
|
244 makeMo stores the resulting files inside a sub-directory of `targetDir` |
|
245 called xx/LC_MESSAGES where 'xx' corresponds to the 2-letter language |
|
246 code. |
|
247 """ |
|
248 if targetDir is None: |
|
249 targetDir = './locale' |
|
250 if verbose: |
|
251 print "Target directory for .mo files is: %s" % targetDir |
|
252 |
|
253 if applicationDomain is None: |
|
254 applicationName = fileBaseOf(applicationDirectoryPath,withPath=0) |
|
255 else: |
|
256 applicationName = applicationDomain |
|
257 currentDir = os.getcwd() |
|
258 os.chdir(applicationDirectoryPath) |
|
259 |
|
260 languageDict = getlanguageDict() |
|
261 |
|
262 for langCode in languageDict.keys(): |
|
263 if (langCode == 'en') and (forceEnglish==0): |
|
264 pass |
|
265 else: |
|
266 langPOfileName = "%s_%s.po" % (applicationName , langCode) |
|
267 if os.path.exists(langPOfileName): |
|
268 mo_targetDir = "%s/%s/LC_MESSAGES" % (targetDir,langCode) |
|
269 if not os.path.exists(mo_targetDir): |
|
270 mkdir(mo_targetDir) |
|
271 cmd = 'msgfmt --output-file="%s/%s.mo" "%s_%s.po"' % (mo_targetDir,applicationName,applicationName,langCode) |
|
272 if verbose: print cmd |
|
273 os.system(cmd) |
|
274 os.chdir(currentDir) |
|
275 |
|
276 # ----------------------------------------------------------------------------- |
|
277 # p r i n t U s a g e -- Displays how to use this script from the command line -- |
|
278 # ^^^^^^^^^^^^^^^^^^^ |
|
279 # |
|
280 def printUsage(errorMsg=None) : |
|
281 """Displays how to use this script from the command line.""" |
|
282 print """ |
|
283 ################################################################################## |
|
284 # mki18n : Make internationalization files. # |
|
285 # Uses the GNU gettext system to create PO (Portable Object) files # |
|
286 # from source code, coimpile PO into MO (Machine Object) files. # |
|
287 # Supports C,C++,Python source files. # |
|
288 # # |
|
289 # Usage: mki18n {OPTION} [appDirPath] # |
|
290 # # |
|
291 # Options: # |
|
292 # -e : When -m is used, forces English .mo file creation # |
|
293 # -h : prints this help # |
|
294 # -m : make MO from existing PO files # |
|
295 # -p : make PO, update PO files: Creates a new messages.pot # |
|
296 # file. Creates a dom_xx.po.new for every existing # |
|
297 # language specific .po file. ('xx' stands for the ISO639 # |
|
298 # two-letter language code and 'dom' stands for the # |
|
299 # application domain name). mki18n requires that you # |
|
300 # write a 'app.fil' file which contains the list of all # |
|
301 # source code to parse. # |
|
302 # -v : verbose (prints comments while running) # |
|
303 # --domain=appName : specifies the application domain name. By default # |
|
304 # the directory name is used. # |
|
305 # --moTarget=dir : specifies the directory where .mo files are stored. # |
|
306 # If not specified, the target is './locale' # |
|
307 # # |
|
308 # You must specify one of the -p or -m option to perform the work. You can # |
|
309 # specify the path of the target application. If you leave it out mki18n # |
|
310 # will use the current directory as the application main directory. # |
|
311 # # |
|
312 ##################################################################################""" |
|
313 if errorMsg: |
|
314 print "\n ERROR: %s" % errorMsg |
|
315 |
|
316 # ----------------------------------------------------------------------------- |
|
317 # f i l e B a s e O f ( ) -- Return base name of filename -- |
|
318 # ^^^^^^^^^^^^^^^^^^^^^^^ |
|
319 # |
|
320 def fileBaseOf(filename,withPath=0) : |
|
321 """fileBaseOf(filename,withPath) ---> string |
|
322 |
|
323 Return base name of filename. The returned string never includes the extension. |
|
324 Use os.path.basename() to return the basename with the extension. The |
|
325 second argument is optional. If specified and if set to 'true' (non zero) |
|
326 the string returned contains the full path of the file name. Otherwise the |
|
327 path is excluded. |
|
328 |
|
329 [Example] |
|
330 >>> fn = 'd:/dev/telepath/tvapp/code/test.html' |
|
331 >>> fileBaseOf(fn) |
|
332 'test' |
|
333 >>> fileBaseOf(fn) |
|
334 'test' |
|
335 >>> fileBaseOf(fn,1) |
|
336 'd:/dev/telepath/tvapp/code/test' |
|
337 >>> fileBaseOf(fn,0) |
|
338 'test' |
|
339 >>> fn = 'abcdef' |
|
340 >>> fileBaseOf(fn) |
|
341 'abcdef' |
|
342 >>> fileBaseOf(fn,1) |
|
343 'abcdef' |
|
344 >>> fn = "abcdef." |
|
345 >>> fileBaseOf(fn) |
|
346 'abcdef' |
|
347 >>> fileBaseOf(fn,1) |
|
348 'abcdef' |
|
349 """ |
|
350 pos = filename.rfind('.') |
|
351 if pos > 0: |
|
352 filename = filename[:pos] |
|
353 if withPath: |
|
354 return filename |
|
355 else: |
|
356 return os.path.basename(filename) |
|
357 # ----------------------------------------------------------------------------- |
|
358 # m k d i r ( ) -- Create a directory (and possibly the entire tree) -- |
|
359 # ^^^^^^^^^^^^^ |
|
360 # |
|
361 def mkdir(directory) : |
|
362 """Create a directory (and possibly the entire tree). |
|
363 |
|
364 The os.mkdir() will fail to create a directory if one of the |
|
365 directory in the specified path does not exist. mkdir() |
|
366 solves this problem. It creates every intermediate directory |
|
367 required to create the final path. Under Unix, the function |
|
368 only supports forward slash separator, but under Windows and MacOS |
|
369 the function supports the forward slash and the OS separator (backslash |
|
370 under windows). |
|
371 """ |
|
372 |
|
373 # translate the path separators |
|
374 directory = unixpath(directory) |
|
375 # build a list of all directory elements |
|
376 aList = filter(lambda x: len(x)>0, directory.split('/')) |
|
377 theLen = len(aList) |
|
378 # if the first element is a Windows-style disk drive |
|
379 # concatenate it with the first directory |
|
380 if aList[0].endswith(':'): |
|
381 if theLen > 1: |
|
382 aList[1] = aList[0] + '/' + aList[1] |
|
383 del aList[0] |
|
384 theLen -= 1 |
|
385 # if the original directory starts at root, |
|
386 # make sure the first element of the list |
|
387 # starts at root too |
|
388 if directory[0] == '/': |
|
389 aList[0] = '/' + aList[0] |
|
390 # Now iterate through the list, check if the |
|
391 # directory exists and if not create it |
|
392 theDir = '' |
|
393 for i in range(theLen): |
|
394 theDir += aList[i] |
|
395 if not os.path.exists(theDir): |
|
396 os.mkdir(theDir) |
|
397 theDir += '/' |
|
398 |
|
399 # ----------------------------------------------------------------------------- |
|
400 # u n i x p a t h ( ) -- Return a path name that contains Unix separator. -- |
|
401 # ^^^^^^^^^^^^^^^^^^^ |
|
402 # |
|
403 def unixpath(thePath) : |
|
404 r"""Return a path name that contains Unix separator. |
|
405 |
|
406 [Example] |
|
407 >>> unixpath(r"d:\test") |
|
408 'd:/test' |
|
409 >>> unixpath("d:/test/file.txt") |
|
410 'd:/test/file.txt' |
|
411 >>> |
|
412 """ |
|
413 thePath = os.path.normpath(thePath) |
|
414 if os.sep == '/': |
|
415 return thePath |
|
416 else: |
|
417 return thePath.replace(os.sep,'/') |
|
418 |
|
419 # ----------------------------------------------------------------------------- |
|
420 |
|
421 # S c r i p t e x e c u t i o n -- Runs when invoked from the command line -- |
|
422 # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
|
423 # |
|
424 if __name__ == "__main__": |
|
425 import getopt # command line parsing |
|
426 argc = len(sys.argv) |
|
427 if argc == 1: |
|
428 printUsage('Missing argument: specify at least one of -m or -p (or both).') |
|
429 sys.exit(1) |
|
430 # If there is some arguments, parse the command line |
|
431 validOptions = "ehmpv" |
|
432 validLongOptions = ['domain=', 'moTarget='] |
|
433 option = {} |
|
434 option['forceEnglish'] = 0 |
|
435 option['mo'] = 0 |
|
436 option['po'] = 0 |
|
437 option['verbose'] = 0 |
|
438 option['domain'] = None |
|
439 option['moTarget'] = None |
|
440 try: |
|
441 optionList,pargs = getopt.getopt(sys.argv[1:],validOptions,validLongOptions) |
|
442 except getopt.GetoptError, e: |
|
443 printUsage(e[0]) |
|
444 sys.exit(1) |
|
445 for (opt,val) in optionList: |
|
446 if (opt == '-h'): |
|
447 printUsage() |
|
448 sys.exit(0) |
|
449 elif (opt == '-e'): option['forceEnglish'] = 1 |
|
450 elif (opt == '-m'): option['mo'] = 1 |
|
451 elif (opt == '-p'): option['po'] = 1 |
|
452 elif (opt == '-v'): option['verbose'] = 1 |
|
453 elif (opt == '--domain'): option['domain'] = val |
|
454 elif (opt == '--moTarget'): option['moTarget'] = val |
|
455 if len(pargs) == 0: |
|
456 appDirPath = os.getcwd() |
|
457 if option['verbose']: |
|
458 print "No project directory given. Using current one: %s" % appDirPath |
|
459 elif len(pargs) == 1: |
|
460 appDirPath = pargs[0] |
|
461 else: |
|
462 printUsage('Too many arguments (%u). Use double quotes if you have space in directory name' % len(pargs)) |
|
463 sys.exit(1) |
|
464 if option['domain'] is None: |
|
465 # If no domain specified, use the name of the target directory |
|
466 option['domain'] = fileBaseOf(appDirPath) |
|
467 if option['verbose']: |
|
468 print "Application domain used is: '%s'" % option['domain'] |
|
469 if option['po']: |
|
470 try: |
|
471 makePO(appDirPath,option['domain'],option['verbose']) |
|
472 except IOError, e: |
|
473 printUsage(e[1] + '\n You must write a file app.fil that contains the list of all files to parse.') |
|
474 if option['mo']: |
|
475 makeMO(appDirPath,option['moTarget'],option['domain'],option['verbose'],option['forceEnglish']) |
|
476 sys.exit(1) |
|
477 |
|
478 |
|
479 # ----------------------------------------------------------------------------- |