i18n/mki18n.py
changeset 361 331d698e1118
child 814 5743cbdff669
equal deleted inserted replaced
360:32339ad7d9ae 361:331d698e1118
       
     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 # -----------------------------------------------------------------------------