i18n/mki18n.py
changeset 391 07447ee3538e
equal deleted inserted replaced
390:020420ad8914 391:07447ee3538e
       
     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 # -----------------------------------------------------------------------------
       
    85 # Global variables
       
    86 # ----------------
       
    87 #
       
    88 
       
    89 __author__ = "Pierre Rouleau"
       
    90 __version__= "$Revision: 1.5 $"
       
    91 
       
    92 # -----------------------------------------------------------------------------
       
    93 
       
    94 def getlanguageDict():
       
    95     languageDict = {}
       
    96     
       
    97     for lang in [x for x in dir(wx) if x.startswith("LANGUAGE")]:
       
    98         i = wx.Locale(wx.LANGUAGE_DEFAULT).GetLanguageInfo(getattr(wx, lang))
       
    99         if i:
       
   100             languageDict[i.CanonicalName] = i.Description
       
   101 
       
   102     return languageDict
       
   103 
       
   104 # -----------------------------------------------------------------------------
       
   105 # m a k e P O ( )         -- Build the Portable Object file for the application --
       
   106 # ^^^^^^^^^^^^^^^
       
   107 #
       
   108 def makePO(applicationDirectoryPath,  applicationDomain=None, verbose=0) :
       
   109     """Build the Portable Object Template file for the application.
       
   110 
       
   111     makePO builds the .pot file for the application stored inside 
       
   112     a specified directory by running xgettext for all application source 
       
   113     files.  It finds the name of all files by looking for a file called 'app.fil'. 
       
   114     If this file does not exists, makePo raises an IOError exception.
       
   115     By default the application domain (the application
       
   116     name) is the same as the directory name but it can be overridden by the
       
   117     'applicationDomain' argument.
       
   118 
       
   119     makePO always creates a new file called messages.pot.  If it finds files 
       
   120     of the form app_xx.po where 'app' is the application name and 'xx' is one 
       
   121     of the ISO 639 two-letter language codes, makePO resynchronizes those 
       
   122     files with the latest extracted strings (now contained in messages.pot). 
       
   123     This process updates all line location number in the language-specific
       
   124     .po files and may also create new entries for translation (or comment out 
       
   125     some).  The .po file is not changed, instead a new file is created with 
       
   126     the .new extension appended to the name of the .po file.
       
   127 
       
   128     By default the function does not display what it is doing.  Set the 
       
   129     verbose argument to 1 to force it to print its commands.
       
   130     """
       
   131 
       
   132     if applicationDomain is None:
       
   133         applicationName = fileBaseOf(applicationDirectoryPath,withPath=0)
       
   134     else:
       
   135         applicationName = applicationDomain
       
   136     currentDir = os.getcwd()
       
   137     os.chdir(applicationDirectoryPath)                    
       
   138     if not os.path.exists('app.fil'):
       
   139         raise IOError(2,'No module file: app.fil')
       
   140 
       
   141     # Steps:                                  
       
   142     #  Use xgettext to parse all application modules
       
   143     #  The following switches are used:
       
   144     #  
       
   145     #   -s                          : sort output by string content (easier to use when we need to merge several .po files)
       
   146     #   --files-from=app.fil        : The list of files is taken from the file: app.fil
       
   147     #   --output=                   : specifies the name of the output file (using a .pot extension)
       
   148     cmd = 'xgettext -s --no-wrap --language=Python --files-from=app.fil --output=messages.pot'
       
   149     if verbose: print cmd
       
   150     os.system(cmd)                                                
       
   151 
       
   152     languageDict = getlanguageDict()
       
   153 
       
   154     for langCode in languageDict.keys():
       
   155         if langCode == 'en':
       
   156             pass
       
   157         else:
       
   158             langPOfileName = "%s_%s.po" % (applicationName , langCode)
       
   159             if os.path.exists(langPOfileName):
       
   160                 cmd = 'msgmerge -s --no-wrap "%s" messages.pot > "%s.new"' % (langPOfileName, langPOfileName)
       
   161                 if verbose: print cmd
       
   162                 os.system(cmd)
       
   163     os.chdir(currentDir)
       
   164 
       
   165 # -----------------------------------------------------------------------------
       
   166 # c a t P O ( )         -- Concatenate one or several PO files with the application domain files. --
       
   167 # ^^^^^^^^^^^^^
       
   168 #
       
   169 def catPO(applicationDirectoryPath, listOf_extraPo, applicationDomain=None, targetDir=None, verbose=0) :
       
   170     """Concatenate one or several PO files with the application domain files.
       
   171     """
       
   172 
       
   173     if applicationDomain is None:
       
   174         applicationName = fileBaseOf(applicationDirectoryPath,withPath=0)
       
   175     else:
       
   176         applicationName = applicationDomain
       
   177     currentDir = os.getcwd()
       
   178     os.chdir(applicationDirectoryPath)
       
   179 
       
   180     languageDict = getlanguageDict()
       
   181 
       
   182     for langCode in languageDict.keys():
       
   183         if langCode == 'en':
       
   184             pass
       
   185         else:
       
   186             langPOfileName = "%s_%s.po" % (applicationName , langCode)
       
   187             if os.path.exists(langPOfileName):
       
   188                 fileList = ''
       
   189                 for fileName in listOf_extraPo:
       
   190                     fileList += ("%s_%s.po " % (fileName,langCode))
       
   191                 cmd = "msgcat -s --no-wrap %s %s > %s.cat" % (langPOfileName, fileList, langPOfileName)
       
   192                 if verbose: print cmd
       
   193                 os.system(cmd)
       
   194                 if targetDir is None:
       
   195                     pass
       
   196                 else:
       
   197                     mo_targetDir = "%s/%s/LC_MESSAGES" % (targetDir,langCode)
       
   198                     cmd = "msgfmt --output-file=%s/%s.mo %s_%s.po.cat" % (mo_targetDir,applicationName,applicationName,langCode)
       
   199                     if verbose: print cmd
       
   200                     os.system(cmd)
       
   201     os.chdir(currentDir)
       
   202 
       
   203 # -----------------------------------------------------------------------------
       
   204 # m a k e M O ( )         -- Compile the Portable Object files into the Machine Object stored in the right location. --
       
   205 # ^^^^^^^^^^^^^^^
       
   206 # 
       
   207 def makeMO(applicationDirectoryPath,targetDir='./locale',applicationDomain=None, verbose=0, forceEnglish=0) :
       
   208     """Compile the Portable Object files into the Machine Object stored in the right location.
       
   209 
       
   210     makeMO converts all translated language-specific PO files located inside 
       
   211     the  application directory into the binary .MO files stored inside the 
       
   212     LC_MESSAGES sub-directory for the found locale files.
       
   213 
       
   214     makeMO searches for all files that have a name of the form 'app_xx.po' 
       
   215     inside the application directory specified by the first argument.  The 
       
   216     'app' is the application domain name (that can be specified by the 
       
   217     applicationDomain argument or is taken from the directory name). The 'xx' 
       
   218     corresponds to one of the ISO 639 two-letter language codes.
       
   219 
       
   220     makeMo stores the resulting files inside a sub-directory of `targetDir`
       
   221     called xx/LC_MESSAGES where 'xx' corresponds to the 2-letter language
       
   222     code.
       
   223     """
       
   224     if targetDir is None:
       
   225         targetDir = './locale'
       
   226     if verbose:
       
   227         print "Target directory for .mo files is: %s" % targetDir
       
   228 
       
   229     if applicationDomain is None:
       
   230         applicationName = fileBaseOf(applicationDirectoryPath,withPath=0)
       
   231     else:
       
   232         applicationName = applicationDomain
       
   233     currentDir = os.getcwd()
       
   234     os.chdir(applicationDirectoryPath)
       
   235 
       
   236     languageDict = getlanguageDict()
       
   237 
       
   238     for langCode in languageDict.keys():
       
   239         if (langCode == 'en') and (forceEnglish==0):
       
   240             pass
       
   241         else:
       
   242             langPOfileName = "%s_%s.po" % (applicationName , langCode)
       
   243             if os.path.exists(langPOfileName):
       
   244                 mo_targetDir = "%s/%s/LC_MESSAGES" % (targetDir,langCode)
       
   245                 if not os.path.exists(mo_targetDir):
       
   246                     mkdir(mo_targetDir)
       
   247                 cmd = 'msgfmt --output-file="%s/%s.mo" "%s_%s.po"' % (mo_targetDir,applicationName,applicationName,langCode)
       
   248                 if verbose: print cmd
       
   249                 os.system(cmd)
       
   250     os.chdir(currentDir)
       
   251    
       
   252 # -----------------------------------------------------------------------------
       
   253 # p r i n t U s a g e         -- Displays how to use this script from the command line --
       
   254 # ^^^^^^^^^^^^^^^^^^^
       
   255 #
       
   256 def printUsage(errorMsg=None) :
       
   257     """Displays how to use this script from the command line."""
       
   258     print """
       
   259     ##################################################################################
       
   260     #   mki18n :   Make internationalization files.                                  #
       
   261     #              Uses the GNU gettext system to create PO (Portable Object) files  #
       
   262     #              from source code, coimpile PO into MO (Machine Object) files.     #
       
   263     #              Supports C,C++,Python source files.                               #
       
   264     #                                                                                #
       
   265     #   Usage: mki18n {OPTION} [appDirPath]                                          #
       
   266     #                                                                                #
       
   267     #   Options:                                                                     #
       
   268     #     -e               : When -m is used, forces English .mo file creation       #
       
   269     #     -h               : prints this help                                        #
       
   270     #     -m               : make MO from existing PO files                          #
       
   271     #     -p               : make PO, update PO files: Creates a new messages.pot    #
       
   272     #                        file. Creates a dom_xx.po.new for every existing        #
       
   273     #                        language specific .po file. ('xx' stands for the ISO639 #
       
   274     #                        two-letter language code and 'dom' stands for the       #
       
   275     #                        application domain name).  mki18n requires that you     #
       
   276     #                        write a 'app.fil' file  which contains the list of all  #
       
   277     #                        source code to parse.                                   #
       
   278     #     -v               : verbose (prints comments while running)                 #
       
   279     #     --domain=appName : specifies the application domain name.  By default      #
       
   280     #                        the directory name is used.                             #
       
   281     #     --moTarget=dir : specifies the directory where .mo files are stored.       #
       
   282     #                      If not specified, the target is './locale'                #
       
   283     #                                                                                #
       
   284     #   You must specify one of the -p or -m option to perform the work.  You can    #
       
   285     #   specify the path of the target application.  If you leave it out mki18n      #
       
   286     #   will use the current directory as the application main directory.            #        
       
   287     #                                                                                #
       
   288     ##################################################################################"""
       
   289     if errorMsg:
       
   290         print "\n   ERROR: %s" % errorMsg
       
   291 
       
   292 # -----------------------------------------------------------------------------
       
   293 # f i l e B a s e O f ( )         -- Return base name of filename --
       
   294 # ^^^^^^^^^^^^^^^^^^^^^^^
       
   295 # 
       
   296 def fileBaseOf(filename,withPath=0) :
       
   297    """fileBaseOf(filename,withPath) ---> string
       
   298 
       
   299    Return base name of filename.  The returned string never includes the extension.
       
   300    Use os.path.basename() to return the basename with the extension.  The 
       
   301    second argument is optional.  If specified and if set to 'true' (non zero) 
       
   302    the string returned contains the full path of the file name.  Otherwise the 
       
   303    path is excluded.
       
   304 
       
   305    [Example]
       
   306    >>> fn = 'd:/dev/telepath/tvapp/code/test.html'
       
   307    >>> fileBaseOf(fn)
       
   308    'test'
       
   309    >>> fileBaseOf(fn)
       
   310    'test'
       
   311    >>> fileBaseOf(fn,1)
       
   312    'd:/dev/telepath/tvapp/code/test'
       
   313    >>> fileBaseOf(fn,0)
       
   314    'test'
       
   315    >>> fn = 'abcdef'
       
   316    >>> fileBaseOf(fn)
       
   317    'abcdef'
       
   318    >>> fileBaseOf(fn,1)
       
   319    'abcdef'
       
   320    >>> fn = "abcdef."
       
   321    >>> fileBaseOf(fn)
       
   322    'abcdef'
       
   323    >>> fileBaseOf(fn,1)
       
   324    'abcdef'
       
   325    """            
       
   326    pos = filename.rfind('.')             
       
   327    if pos > 0:
       
   328       filename = filename[:pos]
       
   329    if withPath:
       
   330       return filename
       
   331    else:
       
   332       return os.path.basename(filename)
       
   333 # -----------------------------------------------------------------------------
       
   334 # m k d i r ( )         -- Create a directory (and possibly the entire tree) --
       
   335 # ^^^^^^^^^^^^^
       
   336 # 
       
   337 def mkdir(directory) :
       
   338    """Create a directory (and possibly the entire tree).
       
   339 
       
   340    The os.mkdir() will fail to create a directory if one of the
       
   341    directory in the specified path does not exist.  mkdir()
       
   342    solves this problem.  It creates every intermediate directory
       
   343    required to create the final path. Under Unix, the function 
       
   344    only supports forward slash separator, but under Windows and MacOS
       
   345    the function supports the forward slash and the OS separator (backslash
       
   346    under windows).
       
   347    """ 
       
   348 
       
   349    # translate the path separators
       
   350    directory = unixpath(directory)
       
   351    # build a list of all directory elements
       
   352    aList = filter(lambda x: len(x)>0, directory.split('/'))
       
   353    theLen = len(aList)                     
       
   354    # if the first element is a Windows-style disk drive
       
   355    # concatenate it with the first directory
       
   356    if aList[0].endswith(':'):
       
   357       if theLen > 1:
       
   358          aList[1] = aList[0] + '/' + aList[1]
       
   359          del aList[0]      
       
   360          theLen -= 1         
       
   361    # if the original directory starts at root,
       
   362    # make sure the first element of the list 
       
   363    # starts at root too
       
   364    if directory[0] == '/':     
       
   365       aList[0] = '/' + aList[0]
       
   366    # Now iterate through the list, check if the 
       
   367    # directory exists and if not create it
       
   368    theDir = ''
       
   369    for i in range(theLen):
       
   370       theDir += aList[i]
       
   371       if not os.path.exists(theDir):
       
   372          os.mkdir(theDir)
       
   373       theDir += '/'   
       
   374       
       
   375 # -----------------------------------------------------------------------------
       
   376 # u n i x p a t h ( )         -- Return a path name that contains Unix separator. --
       
   377 # ^^^^^^^^^^^^^^^^^^^
       
   378 # 
       
   379 def unixpath(thePath) :
       
   380    r"""Return a path name that contains Unix separator.
       
   381 
       
   382    [Example]
       
   383    >>> unixpath(r"d:\test")
       
   384    'd:/test'
       
   385    >>> unixpath("d:/test/file.txt")
       
   386    'd:/test/file.txt'
       
   387    >>> 
       
   388    """
       
   389    thePath = os.path.normpath(thePath)
       
   390    if os.sep == '/':
       
   391       return thePath
       
   392    else:
       
   393       return thePath.replace(os.sep,'/')
       
   394 
       
   395 # ----------------------------------------------------------------------------- 
       
   396 
       
   397 # S c r i p t   e x e c u t i o n               -- Runs when invoked from the command line --
       
   398 # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       
   399 # 
       
   400 if __name__ == "__main__":
       
   401     import getopt     # command line parsing
       
   402     argc = len(sys.argv)
       
   403     if argc == 1:
       
   404         printUsage('Missing argument: specify at least one of -m or -p (or both).')
       
   405         sys.exit(1)
       
   406     # If there is some arguments, parse the command line
       
   407     validOptions     = "ehmpv"
       
   408     validLongOptions = ['domain=', 'moTarget=']             
       
   409     option = {}
       
   410     option['forceEnglish'] = 0
       
   411     option['mo'] = 0
       
   412     option['po'] = 0        
       
   413     option['verbose'] = 0
       
   414     option['domain'] = None
       
   415     option['moTarget'] = None
       
   416     try:
       
   417         optionList,pargs = getopt.getopt(sys.argv[1:],validOptions,validLongOptions)
       
   418     except getopt.GetoptError, e:
       
   419         printUsage(e[0])
       
   420         sys.exit(1)       
       
   421     for (opt,val) in optionList:
       
   422         if  (opt == '-h'):    
       
   423             printUsage()
       
   424             sys.exit(0) 
       
   425         elif (opt == '-e'):         option['forceEnglish'] = 1
       
   426         elif (opt == '-m'):         option['mo'] = 1
       
   427         elif (opt == '-p'):         option['po'] = 1
       
   428         elif (opt == '-v'):         option['verbose'] = 1
       
   429         elif (opt == '--domain'):   option['domain'] = val
       
   430         elif (opt == '--moTarget'): option['moTarget'] = val
       
   431     if len(pargs) == 0:
       
   432         appDirPath = os.getcwd()
       
   433         if option['verbose']:
       
   434             print "No project directory given. Using current one:  %s" % appDirPath
       
   435     elif len(pargs) == 1:
       
   436         appDirPath = pargs[0]
       
   437     else:
       
   438         printUsage('Too many arguments (%u).  Use double quotes if you have space in directory name' % len(pargs))
       
   439         sys.exit(1)
       
   440     if option['domain'] is None:
       
   441         # If no domain specified, use the name of the target directory
       
   442         option['domain'] = fileBaseOf(appDirPath)
       
   443     if option['verbose']:
       
   444         print "Application domain used is: '%s'" % option['domain']
       
   445     if option['po']:
       
   446         try:
       
   447             makePO(appDirPath,option['domain'],option['verbose'])
       
   448         except IOError, e:
       
   449             printUsage(e[1] + '\n   You must write a file app.fil that contains the list of all files to parse.')
       
   450     if option['mo']:
       
   451         makeMO(appDirPath,option['moTarget'],option['domain'],option['verbose'],option['forceEnglish'])
       
   452     sys.exit(1)            
       
   453             
       
   454 
       
   455 # -----------------------------------------------------------------------------