laurent@580: #! /usr/bin/env python laurent@580: # -*- coding: iso-8859-1 -*- laurent@580: # laurent@580: # PYTHON MODULE: MKI18N.PY laurent@580: # ========= laurent@580: # laurent@580: # Abstract: Make Internationalization (i18n) files for an application. laurent@580: # laurent@580: # Copyright Pierre Rouleau. 2003. Released to public domain. laurent@580: # laurent@580: # Last update: Saturday, November 8, 2003. @ 15:55:18. laurent@580: # laurent@580: # File: ROUP2003N01::C:/dev/python/mki18n.py laurent@580: # laurent@580: # RCS $Header: //software/official/MKS/MKS_SI/TV_NT/dev/Python/rcs/mki18n.py 1.5 2003/11/05 19:40:04 PRouleau Exp $ laurent@580: # laurent@580: # Update history: laurent@580: # laurent@580: # - File created: Saturday, June 7, 2003. by Pierre Rouleau laurent@580: # - 10/06/03 rcs : RCS Revision 1.1 2003/06/10 10:06:12 PRouleau laurent@580: # - 10/06/03 rcs : RCS Initial revision laurent@580: # - 23/08/03 rcs : RCS Revision 1.2 2003/06/10 10:54:27 PRouleau laurent@580: # - 23/08/03 P.R.: [code:fix] : The strings encoded in this file are encode in iso-8859-1 format. Added the encoding laurent@580: # notification to Python to comply with Python's 2.3 PEP 263. laurent@580: # - 23/08/03 P.R.: [feature:new] : Added the '-e' switch which is used to force the creation of the empty English .mo file. laurent@580: # - 22/10/03 P.R.: [code] : incorporated utility functions in here to make script self sufficient. laurent@580: # - 05/11/03 rcs : RCS Revision 1.4 2003/10/22 06:39:31 PRouleau laurent@580: # - 05/11/03 P.R.: [code:fix] : included the unixpath() in this file. laurent@580: # - 08/11/03 rcs : RCS Revision 1.5 2003/11/05 19:40:04 PRouleau laurent@580: # laurent@580: # RCS $Log: $ laurent@580: # laurent@580: # laurent@580: # ----------------------------------------------------------------------------- laurent@580: """ laurent@580: mki18n allows you to internationalize your software. You can use it to laurent@580: create the GNU .po files (Portable Object) and the compiled .mo files laurent@580: (Machine Object). laurent@580: laurent@580: mki18n module can be used from the command line or from within a script (see laurent@580: the Usage at the end of this page). laurent@580: laurent@580: Table of Contents laurent@580: ----------------- laurent@580: laurent@580: makePO() -- Build the Portable Object file for the application -- laurent@580: catPO() -- Concatenate one or several PO files with the application domain files. -- laurent@580: makeMO() -- Compile the Portable Object files into the Machine Object stored in the right location. -- laurent@580: printUsage -- Displays how to use this script from the command line -- laurent@580: laurent@580: Scriptexecution -- Runs when invoked from the command line -- laurent@580: laurent@580: laurent@580: NOTE: this module uses GNU gettext utilities. laurent@580: laurent@580: You can get the gettext tools from the following sites: laurent@580: laurent@580: - `GNU FTP site for gettetx`_ where several versions (0.10.40, 0.11.2, 0.11.5 and 0.12.1) are available. laurent@580: Note that you need to use `GNU libiconv`_ to use this. Get it from the `GNU laurent@580: libiconv ftp site`_ and get version 1.9.1 or later. Get the Windows .ZIP laurent@580: files and install the packages inside c:/gnu. All binaries will be stored laurent@580: inside c:/gnu/bin. Just put c:/gnu/bin inside your PATH. You will need laurent@580: the following files: laurent@580: laurent@580: - `gettext-runtime-0.12.1.bin.woe32.zip`_ laurent@580: - `gettext-tools-0.12.1.bin.woe32.zip`_ laurent@580: - `libiconv-1.9.1.bin.woe32.zip`_ laurent@580: laurent@580: laurent@580: .. _GNU libiconv: http://www.gnu.org/software/libiconv/ laurent@580: .. _GNU libiconv ftp site: http://www.ibiblio.org/pub/gnu/libiconv/ laurent@580: .. _gettext-runtime-0.12.1.bin.woe32.zip: ftp://ftp.gnu.org/gnu/gettext/gettext-runtime-0.12.1.bin.woe32.zip laurent@580: .. _gettext-tools-0.12.1.bin.woe32.zip: ftp://ftp.gnu.org/gnu/gettext/gettext-tools-0.12.1.bin.woe32.zip laurent@580: .. _libiconv-1.9.1.bin.woe32.zip: http://www.ibiblio.org/pub/gnu/libiconv/libiconv-1.9.1.bin.woe32.zip laurent@580: laurent@580: """ laurent@580: # ----------------------------------------------------------------------------- laurent@580: # Module Import laurent@580: # ------------- laurent@580: # laurent@580: import os laurent@580: import sys laurent@580: import wx laurent@580: # ----------------------------------------------------------------------------- laurent@580: # Global variables laurent@580: # ---------------- laurent@580: # laurent@580: laurent@580: __author__ = "Pierre Rouleau" laurent@580: __version__= "$Revision: 1.5 $" laurent@580: laurent@580: # ----------------------------------------------------------------------------- laurent@580: laurent@580: def getlanguageDict(): laurent@580: languageDict = {} laurent@580: laurent@580: for lang in [x for x in dir(wx) if x.startswith("LANGUAGE")]: laurent@580: i = wx.Locale(wx.LANGUAGE_DEFAULT).GetLanguageInfo(getattr(wx, lang)) laurent@580: if i: laurent@580: languageDict[i.CanonicalName] = i.Description laurent@580: laurent@580: return languageDict laurent@580: laurent@580: # ----------------------------------------------------------------------------- laurent@580: # m a k e P O ( ) -- Build the Portable Object file for the application -- laurent@580: # ^^^^^^^^^^^^^^^ laurent@580: # laurent@580: def makePO(applicationDirectoryPath, applicationDomain=None, verbose=0) : laurent@580: """Build the Portable Object Template file for the application. laurent@580: laurent@580: makePO builds the .pot file for the application stored inside laurent@580: a specified directory by running xgettext for all application source laurent@580: files. It finds the name of all files by looking for a file called 'app.fil'. laurent@580: If this file does not exists, makePo raises an IOError exception. laurent@580: By default the application domain (the application laurent@580: name) is the same as the directory name but it can be overridden by the laurent@580: 'applicationDomain' argument. laurent@580: laurent@580: makePO always creates a new file called messages.pot. If it finds files laurent@580: of the form app_xx.po where 'app' is the application name and 'xx' is one laurent@580: of the ISO 639 two-letter language codes, makePO resynchronizes those laurent@580: files with the latest extracted strings (now contained in messages.pot). laurent@580: This process updates all line location number in the language-specific laurent@580: .po files and may also create new entries for translation (or comment out laurent@580: some). The .po file is not changed, instead a new file is created with laurent@580: the .new extension appended to the name of the .po file. laurent@580: laurent@580: By default the function does not display what it is doing. Set the laurent@580: verbose argument to 1 to force it to print its commands. laurent@580: """ laurent@580: laurent@580: if applicationDomain is None: laurent@580: applicationName = fileBaseOf(applicationDirectoryPath,withPath=0) laurent@580: else: laurent@580: applicationName = applicationDomain laurent@580: currentDir = os.getcwd() laurent@580: os.chdir(applicationDirectoryPath) laurent@580: if not os.path.exists('app.fil'): laurent@580: raise IOError(2,'No module file: app.fil') laurent@580: laurent@580: # Steps: laurent@580: # Use xgettext to parse all application modules laurent@580: # The following switches are used: laurent@580: # laurent@580: # -s : sort output by string content (easier to use when we need to merge several .po files) laurent@580: # --files-from=app.fil : The list of files is taken from the file: app.fil laurent@580: # --output= : specifies the name of the output file (using a .pot extension) laurent@580: cmd = 'xgettext -s --no-wrap --language=Python --files-from=app.fil --output=messages.pot' laurent@580: if verbose: print cmd laurent@580: os.system(cmd) laurent@580: laurent@580: languageDict = getlanguageDict() laurent@580: laurent@580: for langCode in languageDict.keys(): laurent@580: if langCode == 'en': laurent@580: pass laurent@580: else: laurent@580: langPOfileName = "%s_%s.po" % (applicationName , langCode) laurent@580: if os.path.exists(langPOfileName): laurent@580: cmd = 'msgmerge -s --no-wrap "%s" messages.pot > "%s.new"' % (langPOfileName, langPOfileName) laurent@580: if verbose: print cmd laurent@580: os.system(cmd) laurent@580: os.chdir(currentDir) laurent@580: laurent@580: # ----------------------------------------------------------------------------- laurent@580: # c a t P O ( ) -- Concatenate one or several PO files with the application domain files. -- laurent@580: # ^^^^^^^^^^^^^ laurent@580: # laurent@580: def catPO(applicationDirectoryPath, listOf_extraPo, applicationDomain=None, targetDir=None, verbose=0) : laurent@580: """Concatenate one or several PO files with the application domain files. laurent@580: """ laurent@580: laurent@580: if applicationDomain is None: laurent@580: applicationName = fileBaseOf(applicationDirectoryPath,withPath=0) laurent@580: else: laurent@580: applicationName = applicationDomain laurent@580: currentDir = os.getcwd() laurent@580: os.chdir(applicationDirectoryPath) laurent@580: laurent@580: languageDict = getlanguageDict() laurent@580: laurent@580: for langCode in languageDict.keys(): laurent@580: if langCode == 'en': laurent@580: pass laurent@580: else: laurent@580: langPOfileName = "%s_%s.po" % (applicationName , langCode) laurent@580: if os.path.exists(langPOfileName): laurent@580: fileList = '' laurent@580: for fileName in listOf_extraPo: laurent@580: fileList += ("%s_%s.po " % (fileName,langCode)) laurent@580: cmd = "msgcat -s --no-wrap %s %s > %s.cat" % (langPOfileName, fileList, langPOfileName) laurent@580: if verbose: print cmd laurent@580: os.system(cmd) laurent@580: if targetDir is None: laurent@580: pass laurent@580: else: laurent@580: mo_targetDir = "%s/%s/LC_MESSAGES" % (targetDir,langCode) laurent@580: cmd = "msgfmt --output-file=%s/%s.mo %s_%s.po.cat" % (mo_targetDir,applicationName,applicationName,langCode) laurent@580: if verbose: print cmd laurent@580: os.system(cmd) laurent@580: os.chdir(currentDir) laurent@580: laurent@580: # ----------------------------------------------------------------------------- laurent@580: # m a k e M O ( ) -- Compile the Portable Object files into the Machine Object stored in the right location. -- laurent@580: # ^^^^^^^^^^^^^^^ laurent@580: # laurent@580: def makeMO(applicationDirectoryPath,targetDir='./locale',applicationDomain=None, verbose=0, forceEnglish=0) : laurent@580: """Compile the Portable Object files into the Machine Object stored in the right location. laurent@580: laurent@580: makeMO converts all translated language-specific PO files located inside laurent@580: the application directory into the binary .MO files stored inside the laurent@580: LC_MESSAGES sub-directory for the found locale files. laurent@580: laurent@580: makeMO searches for all files that have a name of the form 'app_xx.po' laurent@580: inside the application directory specified by the first argument. The laurent@580: 'app' is the application domain name (that can be specified by the laurent@580: applicationDomain argument or is taken from the directory name). The 'xx' laurent@580: corresponds to one of the ISO 639 two-letter language codes. laurent@580: laurent@580: makeMo stores the resulting files inside a sub-directory of `targetDir` laurent@580: called xx/LC_MESSAGES where 'xx' corresponds to the 2-letter language laurent@580: code. laurent@580: """ laurent@580: if targetDir is None: laurent@580: targetDir = './locale' laurent@580: if verbose: laurent@580: print "Target directory for .mo files is: %s" % targetDir laurent@580: laurent@580: if applicationDomain is None: laurent@580: applicationName = fileBaseOf(applicationDirectoryPath,withPath=0) laurent@580: else: laurent@580: applicationName = applicationDomain laurent@580: currentDir = os.getcwd() laurent@580: os.chdir(applicationDirectoryPath) laurent@580: laurent@580: languageDict = getlanguageDict() laurent@580: laurent@580: for langCode in languageDict.keys(): laurent@580: if (langCode == 'en') and (forceEnglish==0): laurent@580: pass laurent@580: else: laurent@580: langPOfileName = "%s_%s.po" % (applicationName , langCode) laurent@580: if os.path.exists(langPOfileName): laurent@580: mo_targetDir = "%s/%s/LC_MESSAGES" % (targetDir,langCode) laurent@580: if not os.path.exists(mo_targetDir): laurent@580: mkdir(mo_targetDir) laurent@580: cmd = 'msgfmt --output-file="%s/%s.mo" "%s_%s.po"' % (mo_targetDir,applicationName,applicationName,langCode) laurent@580: if verbose: print cmd laurent@580: os.system(cmd) laurent@580: os.chdir(currentDir) laurent@580: laurent@580: # ----------------------------------------------------------------------------- laurent@580: # p r i n t U s a g e -- Displays how to use this script from the command line -- laurent@580: # ^^^^^^^^^^^^^^^^^^^ laurent@580: # laurent@580: def printUsage(errorMsg=None) : laurent@580: """Displays how to use this script from the command line.""" laurent@580: print """ laurent@580: ################################################################################## laurent@580: # mki18n : Make internationalization files. # laurent@580: # Uses the GNU gettext system to create PO (Portable Object) files # laurent@580: # from source code, coimpile PO into MO (Machine Object) files. # laurent@580: # Supports C,C++,Python source files. # laurent@580: # # laurent@580: # Usage: mki18n {OPTION} [appDirPath] # laurent@580: # # laurent@580: # Options: # laurent@580: # -e : When -m is used, forces English .mo file creation # laurent@580: # -h : prints this help # laurent@580: # -m : make MO from existing PO files # laurent@580: # -p : make PO, update PO files: Creates a new messages.pot # laurent@580: # file. Creates a dom_xx.po.new for every existing # laurent@580: # language specific .po file. ('xx' stands for the ISO639 # laurent@580: # two-letter language code and 'dom' stands for the # laurent@580: # application domain name). mki18n requires that you # laurent@580: # write a 'app.fil' file which contains the list of all # laurent@580: # source code to parse. # laurent@580: # -v : verbose (prints comments while running) # laurent@580: # --domain=appName : specifies the application domain name. By default # laurent@580: # the directory name is used. # laurent@580: # --moTarget=dir : specifies the directory where .mo files are stored. # laurent@580: # If not specified, the target is './locale' # laurent@580: # # laurent@580: # You must specify one of the -p or -m option to perform the work. You can # laurent@580: # specify the path of the target application. If you leave it out mki18n # laurent@580: # will use the current directory as the application main directory. # laurent@580: # # laurent@580: ##################################################################################""" laurent@580: if errorMsg: laurent@580: print "\n ERROR: %s" % errorMsg laurent@580: laurent@580: # ----------------------------------------------------------------------------- laurent@580: # f i l e B a s e O f ( ) -- Return base name of filename -- laurent@580: # ^^^^^^^^^^^^^^^^^^^^^^^ laurent@580: # laurent@580: def fileBaseOf(filename,withPath=0) : laurent@580: """fileBaseOf(filename,withPath) ---> string laurent@580: laurent@580: Return base name of filename. The returned string never includes the extension. laurent@580: Use os.path.basename() to return the basename with the extension. The laurent@580: second argument is optional. If specified and if set to 'true' (non zero) laurent@580: the string returned contains the full path of the file name. Otherwise the laurent@580: path is excluded. laurent@580: laurent@580: [Example] laurent@580: >>> fn = 'd:/dev/telepath/tvapp/code/test.html' laurent@580: >>> fileBaseOf(fn) laurent@580: 'test' laurent@580: >>> fileBaseOf(fn) laurent@580: 'test' laurent@580: >>> fileBaseOf(fn,1) laurent@580: 'd:/dev/telepath/tvapp/code/test' laurent@580: >>> fileBaseOf(fn,0) laurent@580: 'test' laurent@580: >>> fn = 'abcdef' laurent@580: >>> fileBaseOf(fn) laurent@580: 'abcdef' laurent@580: >>> fileBaseOf(fn,1) laurent@580: 'abcdef' laurent@580: >>> fn = "abcdef." laurent@580: >>> fileBaseOf(fn) laurent@580: 'abcdef' laurent@580: >>> fileBaseOf(fn,1) laurent@580: 'abcdef' laurent@580: """ laurent@580: pos = filename.rfind('.') laurent@580: if pos > 0: laurent@580: filename = filename[:pos] laurent@580: if withPath: laurent@580: return filename laurent@580: else: laurent@580: return os.path.basename(filename) laurent@580: # ----------------------------------------------------------------------------- laurent@580: # m k d i r ( ) -- Create a directory (and possibly the entire tree) -- laurent@580: # ^^^^^^^^^^^^^ laurent@580: # laurent@580: def mkdir(directory) : laurent@580: """Create a directory (and possibly the entire tree). laurent@580: laurent@580: The os.mkdir() will fail to create a directory if one of the laurent@580: directory in the specified path does not exist. mkdir() laurent@580: solves this problem. It creates every intermediate directory laurent@580: required to create the final path. Under Unix, the function laurent@580: only supports forward slash separator, but under Windows and MacOS laurent@580: the function supports the forward slash and the OS separator (backslash laurent@580: under windows). laurent@580: """ laurent@580: laurent@580: # translate the path separators laurent@580: directory = unixpath(directory) laurent@580: # build a list of all directory elements laurent@580: aList = filter(lambda x: len(x)>0, directory.split('/')) laurent@580: theLen = len(aList) laurent@580: # if the first element is a Windows-style disk drive laurent@580: # concatenate it with the first directory laurent@580: if aList[0].endswith(':'): laurent@580: if theLen > 1: laurent@580: aList[1] = aList[0] + '/' + aList[1] laurent@580: del aList[0] laurent@580: theLen -= 1 laurent@580: # if the original directory starts at root, laurent@580: # make sure the first element of the list laurent@580: # starts at root too laurent@580: if directory[0] == '/': laurent@580: aList[0] = '/' + aList[0] laurent@580: # Now iterate through the list, check if the laurent@580: # directory exists and if not create it laurent@580: theDir = '' laurent@580: for i in range(theLen): laurent@580: theDir += aList[i] laurent@580: if not os.path.exists(theDir): laurent@580: os.mkdir(theDir) laurent@580: theDir += '/' laurent@580: laurent@580: # ----------------------------------------------------------------------------- laurent@580: # u n i x p a t h ( ) -- Return a path name that contains Unix separator. -- laurent@580: # ^^^^^^^^^^^^^^^^^^^ laurent@580: # laurent@580: def unixpath(thePath) : laurent@580: r"""Return a path name that contains Unix separator. laurent@580: laurent@580: [Example] laurent@580: >>> unixpath(r"d:\test") laurent@580: 'd:/test' laurent@580: >>> unixpath("d:/test/file.txt") laurent@580: 'd:/test/file.txt' laurent@580: >>> laurent@580: """ laurent@580: thePath = os.path.normpath(thePath) laurent@580: if os.sep == '/': laurent@580: return thePath laurent@580: else: laurent@580: return thePath.replace(os.sep,'/') laurent@580: laurent@580: # ----------------------------------------------------------------------------- laurent@580: laurent@580: # S c r i p t e x e c u t i o n -- Runs when invoked from the command line -- laurent@580: # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ laurent@580: # laurent@580: if __name__ == "__main__": laurent@580: import getopt # command line parsing laurent@580: argc = len(sys.argv) laurent@580: if argc == 1: laurent@580: printUsage('Missing argument: specify at least one of -m or -p (or both).') laurent@580: sys.exit(1) laurent@580: # If there is some arguments, parse the command line laurent@580: validOptions = "ehmpv" laurent@580: validLongOptions = ['domain=', 'moTarget='] laurent@580: option = {} laurent@580: option['forceEnglish'] = 0 laurent@580: option['mo'] = 0 laurent@580: option['po'] = 0 laurent@580: option['verbose'] = 0 laurent@580: option['domain'] = None laurent@580: option['moTarget'] = None laurent@580: try: laurent@580: optionList,pargs = getopt.getopt(sys.argv[1:],validOptions,validLongOptions) laurent@580: except getopt.GetoptError, e: laurent@580: printUsage(e[0]) laurent@580: sys.exit(1) laurent@580: for (opt,val) in optionList: laurent@580: if (opt == '-h'): laurent@580: printUsage() laurent@580: sys.exit(0) laurent@580: elif (opt == '-e'): option['forceEnglish'] = 1 laurent@580: elif (opt == '-m'): option['mo'] = 1 laurent@580: elif (opt == '-p'): option['po'] = 1 laurent@580: elif (opt == '-v'): option['verbose'] = 1 laurent@580: elif (opt == '--domain'): option['domain'] = val laurent@580: elif (opt == '--moTarget'): option['moTarget'] = val laurent@580: if len(pargs) == 0: laurent@580: appDirPath = os.getcwd() laurent@580: if option['verbose']: laurent@580: print "No project directory given. Using current one: %s" % appDirPath laurent@580: elif len(pargs) == 1: laurent@580: appDirPath = pargs[0] laurent@580: else: laurent@580: printUsage('Too many arguments (%u). Use double quotes if you have space in directory name' % len(pargs)) laurent@580: sys.exit(1) laurent@580: if option['domain'] is None: laurent@580: # If no domain specified, use the name of the target directory laurent@580: option['domain'] = fileBaseOf(appDirPath) laurent@580: if option['verbose']: laurent@580: print "Application domain used is: '%s'" % option['domain'] laurent@580: if option['po']: laurent@580: try: laurent@580: makePO(appDirPath,option['domain'],option['verbose']) laurent@580: except IOError, e: laurent@580: printUsage(e[1] + '\n You must write a file app.fil that contains the list of all files to parse.') laurent@580: if option['mo']: laurent@580: makeMO(appDirPath,option['moTarget'],option['domain'],option['verbose'],option['forceEnglish']) laurent@580: sys.exit(1) laurent@580: laurent@580: laurent@580: # -----------------------------------------------------------------------------