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