#!/usr/bin/python # -*- coding: utf-8 -*- # # PYTHON MODULE: MKI18N.PY # ========= # # Abstract: Make Internationalization (i18n) files for an application. # # Copyright Pierre Rouleau. 2003. Released to public domain. # # Last update: Saturday, November 8, 2003. @ 15:55:18. # # File: ROUP2003N01::C:/dev/python/mki18n.py # # RCS $Header: //software/official/MKS/MKS_SI/TV_NT/dev/Python/rcs/mki18n.py 1.5 2003/11/05 19:40:04 PRouleau Exp $ # # Update history: # # - File created: Saturday, June 7, 2003. by Pierre Rouleau # - 10/06/03 rcs : RCS Revision 1.1 2003/06/10 10:06:12 PRouleau # - 10/06/03 rcs : RCS Initial revision # - 23/08/03 rcs : RCS Revision 1.2 2003/06/10 10:54:27 PRouleau # - 23/08/03 P.R.: [code:fix] : The strings encoded in this file are encode in iso-8859-1 format. Added the encoding # notification to Python to comply with Python's 2.3 PEP 263. # - 23/08/03 P.R.: [feature:new] : Added the '-e' switch which is used to force the creation of the empty English .mo file. # - 22/10/03 P.R.: [code] : incorporated utility functions in here to make script self sufficient. # - 05/11/03 rcs : RCS Revision 1.4 2003/10/22 06:39:31 PRouleau # - 05/11/03 P.R.: [code:fix] : included the unixpath() in this file. # - 08/11/03 rcs : RCS Revision 1.5 2003/11/05 19:40:04 PRouleau # # RCS $Log: $ # # # ----------------------------------------------------------------------------- """ mki18n allows you to internationalize your software. You can use it to create the GNU .po files (Portable Object) and the compiled .mo files (Machine Object). mki18n module can be used from the command line or from within a script (see the Usage at the end of this page). Table of Contents ----------------- makePO() -- Build the Portable Object file for the application -- catPO() -- Concatenate one or several PO files with the application domain files. -- makeMO() -- Compile the Portable Object files into the Machine Object stored in the right location. -- printUsage -- Displays how to use this script from the command line -- Scriptexecution -- Runs when invoked from the command line -- NOTE: this module uses GNU gettext utilities. You can get the gettext tools from the following sites: - `GNU FTP site for gettetx`_ where several versions (0.10.40, 0.11.2, 0.11.5 and 0.12.1) are available. Note that you need to use `GNU libiconv`_ to use this. Get it from the `GNU libiconv ftp site`_ and get version 1.9.1 or later. Get the Windows .ZIP files and install the packages inside c:/gnu. All binaries will be stored inside c:/gnu/bin. Just put c:/gnu/bin inside your PATH. You will need the following files: - `gettext-runtime-0.12.1.bin.woe32.zip`_ - `gettext-tools-0.12.1.bin.woe32.zip`_ - `libiconv-1.9.1.bin.woe32.zip`_ .. _GNU libiconv: http://www.gnu.org/software/libiconv/ .. _GNU libiconv ftp site: http://www.ibiblio.org/pub/gnu/libiconv/ .. _gettext-runtime-0.12.1.bin.woe32.zip: ftp://ftp.gnu.org/gnu/gettext/gettext-runtime-0.12.1.bin.woe32.zip .. _gettext-tools-0.12.1.bin.woe32.zip: ftp://ftp.gnu.org/gnu/gettext/gettext-tools-0.12.1.bin.woe32.zip .. _libiconv-1.9.1.bin.woe32.zip: http://www.ibiblio.org/pub/gnu/libiconv/libiconv-1.9.1.bin.woe32.zip """ # ----------------------------------------------------------------------------- # Module Import # ------------- # import os import sys import re import shutil # Try to work even with no python path try: from exe.application import Application except ImportError, error: if str(error) == "No module named exe.application": exePath = os.path.abspath(sys.argv[0]) exeDir = os.path.dirname(exePath) pythonPath = os.path.split(exeDir)[0] sys.path.insert(0, pythonPath) from exe.application import Application else: raise error from exe.engine.path import Path from xml.dom.ext.reader import Sax2 from xml.dom.NodeFilter import NodeFilter # ----------------------------------------------------------------------------- # Global variables # ---------------- # __author__ = "Pierre Rouleau" __version__= "$Revision: 1.5 $" # ----------------------------------------------------------------------------- # Public variables # ---------------- # # iso639 : natural # 2-letter : language # code : name iso639_languageDict = { 'aa' : 'Afar. ', 'ab' : 'Abkhazian. ', 'ae' : 'Avestan. ', 'af' : 'Afrikaans. ', 'am' : 'Amharic. ', 'ar' : 'Arabic. ', 'as' : 'Assamese. ', 'ay' : 'Aymara. ', 'az' : 'Azerbaijani. ', 'ba' : 'Bashkir. ', 'be' : 'Byelorussian; Belarusian. ', 'bg' : 'Bulgarian. ', 'bh' : 'Bihari. ', 'bi' : 'Bislama. ', 'bn' : 'Bengali; Bangla. ', 'bo' : 'Tibetan. ', 'br' : 'Breton. ', 'bs' : 'Bosnian. ', 'ca' : 'Catalan. ', 'ce' : 'Chechen. ', 'ch' : 'Chamorro. ', 'co' : 'Corsican. ', 'cs' : 'Czech. ', 'cu' : 'Church Slavic. ', 'cv' : 'Chuvash. ', 'cy' : 'Welsh. ', 'da' : 'Danish. ', 'de' : 'German. ', 'dz' : 'Dzongkha; Bhutani. ', 'el' : 'Greek. ', 'en' : 'English. ', 'eo' : 'Esperanto. ', 'es' : 'Spanish. ', 'et' : 'Estonian. ', 'eu' : 'Basque. ', 'fa' : 'Persian. ', 'fi' : 'Finnish. ', 'fj' : 'Fijian; Fiji. ', 'fo' : 'Faroese. ', 'fr' : 'French. ', 'fy' : 'Frisian. ', 'ga' : 'Irish. ', 'gd' : 'Scots; Gaelic. ', 'gl' : 'Gallegan; Galician. ', 'gn' : 'Guarani. ', 'gu' : 'Gujarati. ', 'gv' : 'Manx. ', 'ha' : 'Hausa (?). ', 'he' : 'Hebrew (formerly iw). ', 'hi' : 'Hindi. ', 'ho' : 'Hiri Motu. ', 'hr' : 'Croatian. ', 'hu' : 'Hungarian. ', 'hy' : 'Armenian. ', 'hz' : 'Herero. ', 'ia' : 'Interlingua. ', 'id' : 'Indonesian (formerly in). ', 'ie' : 'Interlingue. ', 'ik' : 'Inupiak. ', 'io' : 'Ido. ', 'is' : 'Icelandic. ', 'it' : 'Italian. ', 'iu' : 'Inuktitut. ', 'ja' : 'Japanese. ', 'jv' : 'Javanese. ', 'ka' : 'Georgian. ', 'ki' : 'Kikuyu. ', 'kj' : 'Kuanyama. ', 'kk' : 'Kazakh. ', 'kl' : 'Kalaallisut; Greenlandic. ', 'km' : 'Khmer; Cambodian. ', 'kn' : 'Kannada. ', 'ko' : 'Korean. ', 'ks' : 'Kashmiri. ', 'ku' : 'Kurdish. ', 'kv' : 'Komi. ', 'kw' : 'Cornish. ', 'ky' : 'Kirghiz. ', 'la' : 'Latin. ', 'lb' : 'Letzeburgesch. ', 'ln' : 'Lingala. ', 'lo' : 'Lao; Laotian. ', 'lt' : 'Lithuanian. ', 'lv' : 'Latvian; Lettish. ', 'mg' : 'Malagasy. ', 'mh' : 'Marshall. ', 'mi' : 'Maori. ', 'mk' : 'Macedonian. ', 'ml' : 'Malayalam. ', 'mn' : 'Mongolian. ', 'mo' : 'Moldavian. ', 'mr' : 'Marathi. ', 'ms' : 'Malay. ', 'mt' : 'Maltese. ', 'my' : 'Burmese. ', 'na' : 'Nauru. ', 'nb' : 'Norwegian Bokmål. ', 'nd' : 'Ndebele, North. ', 'ne' : 'Nepali. ', 'ng' : 'Ndonga. ', 'nl' : 'Dutch. ', 'nn' : 'Norwegian Nynorsk. ', 'no' : 'Norwegian. ', 'nr' : 'Ndebele, South. ', 'nv' : 'Navajo. ', 'ny' : 'Chichewa; Nyanja. ', 'oc' : 'Occitan; Provençal. ', 'om' : '(Afan) Oromo. ', 'or' : 'Oriya. ', 'os' : 'Ossetian; Ossetic. ', 'pa' : 'Panjabi; Punjabi. ', 'pi' : 'Pali. ', 'pl' : 'Polish. ', 'ps' : 'Pashto, Pushto. ', 'pt' : 'Portuguese. ', 'qu' : 'Quechua. ', 'rm' : 'Rhaeto-Romance. ', 'rn' : 'Rundi; Kirundi. ', 'ro' : 'Romanian. ', 'ru' : 'Russian. ', 'rw' : 'Kinyarwanda. ', 'sa' : 'Sanskrit. ', 'sc' : 'Sardinian. ', 'sd' : 'Sindhi. ', 'se' : 'Northern Sami. ', 'sg' : 'Sango; Sangro. ', 'si' : 'Sinhalese. ', 'sk' : 'Slovak. ', 'sl' : 'Slovenian. ', 'sm' : 'Samoan. ', 'sn' : 'Shona. ', 'so' : 'Somali. ', 'sq' : 'Albanian. ', 'sr' : 'Serbian. ', 'ss' : 'Swati; Siswati. ', 'st' : 'Sesotho; Sotho, Southern. ', 'su' : 'Sundanese. ', 'sv' : 'Swedish. ', 'sw' : 'Swahili. ', 'ta' : 'Tamil. ', 'te' : 'Telugu. ', 'tg' : 'Tajik. ', 'th' : 'Thai. ', 'ti' : 'Tigrinya. ', 'tk' : 'Turkmen. ', 'tl' : 'Tagalog. ', 'tn' : 'Tswana; Setswana. ', 'to' : 'Tonga (?). ', 'tr' : 'Turkish. ', 'ts' : 'Tsonga. ', 'tt' : 'Tatar. ', 'tw' : 'Twi. ', 'ty' : 'Tahitian. ', 'ug' : 'Uighur. ', 'uk' : 'Ukrainian. ', 'ur' : 'Urdu. ', 'uz' : 'Uzbek. ', 'qcv' : 'Valencian. ', 'vi' : 'Vietnamese. ', 'vo' : 'Volapük; Volapuk. ', 'wa' : 'Walloon. ', 'wo' : 'Wolof. ', 'xh' : 'Xhosa. ', 'yi' : 'Yiddish (formerly ji). ', 'yo' : 'Yoruba. ', 'za' : 'Zhuang. ', 'zh' : 'Chinese. ', 'zu' : 'Zulu.' } def generateAppFil(): """ Generates app.fil """ exe = Path('exe') toSearch = [ (exe, '*.py'), (exe/'engine', '*.py'), (exe/'export', '*.py'), (exe/'webui', '*.py'), (exe/'idevices', '*.py'), (exe/'xului', '*.py'), ] output = open('app.fil', 'w') for pth, glb in toSearch: for fn in Path(pth).glob(glb): output.write(fn + '\n') output.write('twisted/persisted/styles.py\n') output.close() def makePO(applicationDirectoryPath, applicationDomain=None, verbose=1) : """Build the Portable Object Template file for the application. makePO builds the .pot file for the application stored inside a specified directory by running xgettext for all application source files. It finds the name of all files by looking for a file called 'app.fil'. If this file does not exists, makePo raises an IOError exception. By default the application domain (the application name) is the same as the directory name but it can be overridden by the 'applicationDomain' argument. makePO always creates a new file called messages.pot. If it finds files of the form app_xx.po where 'app' is the application name and 'xx' is one of the ISO 639 two-letter language codes, makePO resynchronizes those files with the latest extracted strings (now contained in messages.pot). This process updates all line location number in the language-specific .po files and may also create new entries for translation (or comment out some). The .po file is not changed, instead a new file is created with the .new extension appended to the name of the .po file. By default the function does not display what it is doing. Set the verbose argument to 1 to force it to print its commands. """ if applicationDomain is None: applicationName = fileBaseOf(applicationDirectoryPath,withPath=0) else: applicationName = applicationDomain currentDir = os.getcwd() messages_pot = Path('exe/locale/messages.pot') # Use xgettext to make the base messages.pot (with header, etc.) if messages_pot.exists(): messages_pot.remove() messages_pot.touch() cmd = 'xgettext -kx_ -s -j --no-wrap --output=exe/locale/messages.pot --from-code=utf8 exe/engine/package.py' if verbose: print cmd os.system(cmd) if not os.path.exists('app.fil'): raise IOError(2,'No module file: app.fil') # Steps: # Use xgettext to parse all application modules # The following switches are used: # # -s : sort output by string content (easier to use when we need to merge several .po files) # --files-from=app.fil : The list of files is taken from the file: app.fil # --output= : specifies the name of the output file (using a .pot extension) cmd = 'xgettext -kx_ -s -j --no-wrap --output=exe/locale/messages.pot --from-code=utf8 %s' if verbose: print cmd for fn in open('app.fil'): print 'Extracting from', fn, os.system(cmd % fn[:-1]) makeXulPO(applicationDirectoryPath, applicationDomain, verbose) # Merge new pot with .po files localeDirs = Path('exe/locale') for filename in localeDirs.walkfiles('*_*.po'): cmd = "msgmerge -U --no-wrap %s exe/locale/messages.pot" % filename if verbose: print cmd os.system(cmd) def makeXulPO(applicationDirectoryPath, applicationDomain=None, verbose=0): """Searches through xul files and appends to messages.pot""" if verbose: print "Importing xul templates..." path = Path(applicationDirectoryPath) messages = pot2dict('exe/locale/messages.pot') messageCommentTemplate = '\n#: %s:%s\nmsgid "' seq = len(messages) skipPaths = ( applicationDirectoryPath/'exe/webui/firefox', ) for fn in path.walkfiles(): if fn.ext.lower() == '.xul': for skipPath in skipPaths: if fn.startswith(skipPath): print 'IGNORING', fn break else: if verbose: print "template: ", fn reader = Sax2.Reader() doc = reader.fromStream(file(fn, 'rb')) xul2dict(doc, messages, seq, fn.relpath()) pot = Path('exe/locale/messages.pot') if pot.exists(): pot.remove() pot.touch() dict2pot(messages, 'exe/locale/messages.pot') def doTranslate(node, toTranslate, messages, seq, filename): """Add message to be translated to the messages dict """ attributes = ' '.join(['%s="%s"' % (attr.name, attr.value) for attr in node.attributes.values()]) tagStr = '<%s %s>' % (node.nodeName, attributes) if toTranslate in messages: order, comments, msgStr = messages[toTranslate] comments += ('#: %s:%s' % (filename, tagStr),) messages[toTranslate] = order, comments, msgStr else: messages[toTranslate] = seq, \ ('#: %s:%s' % (filename, tagStr),), '' seq += 1 return messages, seq def xul2dict(doc, messages, seq, filename): """Recursively translates some "stan" contexts and fills out the messages dict, which should be passed to dict2pot later """ walker = doc.createTreeWalker(doc.documentElement, NodeFilter.SHOW_ELEMENT, None, 0) node = walker.currentNode while 1: # IF YOU CHANGE THE BELOW RULES, CHANGE THEIR COPY IN: # exe/webui/renderable.py # Here we have some rules: # 1. if a tag has a 'label' and an 'accesskey' attribute the # whole string is taken for translation like this: # 'label="english" accesskey="e"' # 2. If a tag has only a label attribute only that is taken # for translation: 'english' # 3. If a tag is a label tag, only its value is taken for # translation: