i18n: Robustify db-seed-i18n.py parsing
[working/Evergreen.git] / build / i18n / scripts / db-seed-i18n.py
1 #!/usr/bin/env python
2 # vim:et:ts=4:sw=4:
3 """
4 This class enables translation of Evergreen's seed database strings.
5
6 Requires polib from http://polib.googlecode.com
7 """
8 # Copyright 2007-2008 Dan Scott <dscott@laurentian.ca>
9 #
10 # This program is free software; you can redistribute it and/or
11 # modify it under the terms of the GNU General Public License
12 # as published by the Free Software Foundation; either version 2
13 # of the License, or (at your option) any later version.
14 #
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 # GNU General Public License for more details.
19
20 import basel10n
21 import codecs
22 import optparse
23 import polib
24 import re
25 import sys
26 import os.path
27
28 class SQL(basel10n.BaseL10N):
29     """
30     This class provides methods for extracting translatable strings from
31     Evergreen's database seed values, generating a translatable POT file,
32     reading translated PO files, and generating SQL for inserting the
33     translated values into the Evergreen database.
34     """
35
36     def __init__(self):
37         self.pot = None
38         basel10n.BaseL10N.__init__(self)
39         self.sql = []
40
41     def getstrings(self, source):
42         """
43         Each INSERT statement contains 0 or more oils_i18n_gettext()
44         markers for the en-US string that identify the string (which
45         we push into the POEntry.occurrences attribute), class hint,
46         and property. We concatenate the class hint and property and
47         use that for our msgid attribute.
48         
49         A sample INSERT string that we'll scan is as follows:
50
51             INSERT INTO foo.bar (key, value) VALUES 
52                 (99, oils_i18n_gettext(99, 'string', 'class hint', 'property'));
53         """
54         self.pothead()
55
56         num = 0
57         findi18n = re.compile(r'.*?oils_i18n_gettext\((.*?)\'\)')
58         intkey = re.compile(r'\s*(?P<id>\d+)\s*,\s*\'(?P<string>.+?)\',\s*\'(?P<class>.+?)\',\s*\'(?P<property>.+?)$')
59         textkey = re.compile(r'\s*\'(?P<id>.*?)\'\s*,\s*\'(?P<string>.+?)\',\s*\'(?P<class>.+?)\',\s*\'(?P<property>.+?)$')
60         serts = dict()
61
62         # Iterate through the source SQL grabbing table names and l10n strings
63         sourcefile = codecs.open(source, encoding='utf-8')
64         for line in sourcefile:
65             try:
66                 num = num + 1
67                 entry = findi18n.search(line)
68                 if entry is None:
69                     continue
70                 for parms in entry.groups():
71                     # Try for an integer-based primary key parameter first
72                     fi18n = intkey.search(parms)
73                     if fi18n is None:
74                         # Otherwise, it must be a text-based primary key parameter
75                         fi18n = textkey.search(parms)
76                     fq_field = "%s.%s" % (fi18n.group('class'), fi18n.group('property'))
77                     # Unescape escaped SQL single-quotes for translators' sanity
78                     msgid = re.compile(r'\'\'').sub("'", fi18n.group('string'))
79
80                     # Hmm, sometimes people use ":" in text identifiers and
81                     # polib doesn't seem to like that; urlencode the colon
82                     occurid = re.compile(r':').sub("%3A", fi18n.group('id'))
83
84                     if (msgid in serts):
85                         serts[msgid].occurrences.append((os.path.basename(source), num))
86                         serts[msgid].tcomment = ' '.join((serts[msgid].tcomment, 'id::%s__%s' % (fq_field, occurid)))
87                     else:
88                         poe = polib.POEntry()
89                         poe.tcomment = 'id::%s__%s' % (fq_field, occurid)
90                         poe.occurrences = [(os.path.basename(source), num)]
91                         poe.msgid = msgid
92                         serts[msgid] = poe
93             except Exception, exc:
94                 print "Error in line %d of SQL source file: %s" % (num, exc) 
95
96         for poe in serts.values():
97             self.pot.append(poe)
98
99     def create_sql(self, locale):
100         """
101         Creates a set of INSERT statements that place translated strings
102         into the config.i18n_core table.
103         """
104
105         insert = "INSERT INTO config.i18n_core (fq_field, identity_value," \
106             " translation, string) VALUES ('%s', '%s', '%s', '%s');"
107         idregex = re.compile(r'^id::(?P<class>.*?)__(?P<id>.*?)$')
108         for entry in self.pot:
109             for id_value in entry.tcomment.split():
110                 # Escape SQL single-quotes to avoid b0rkage
111                 msgstr = re.compile(r'\'').sub("''", entry.msgstr)
112
113                 identifier = idregex.search(id_value)
114                 if identifier is None:
115                     continue
116                 # And unescape any colons in the occurence ID
117                 occurid = re.compile(r'%3A').sub(':', identifier.group('id'))
118
119                 if msgstr == '':
120                     # Don't generate a stmt for an untranslated string
121                     break
122                 self.sql.append(insert % (identifier.group('class'), occurid, locale, msgstr))
123
124 def main():
125     """
126     Determine what action to take
127     """
128     opts = optparse.OptionParser()
129     opts.add_option('-p', '--pot', action='store', \
130         help='Generate POT from the specified source SQL file', metavar='FILE')
131     opts.add_option('-s', '--sql', action='store', \
132         help='Generate SQL from the specified source POT file', metavar='FILE')
133     opts.add_option('-l', '--locale', \
134         help='Locale of the SQL file that will be generated')
135     opts.add_option('-o', '--output', dest='outfile', \
136         help='Write output to FILE (defaults to STDOUT)', metavar='FILE')
137     (options, args) = opts.parse_args()
138
139     if options.pot:
140         pot = SQL()
141         pot.getstrings(options.pot)
142         if options.outfile:
143             pot.savepot(options.outfile)
144         else:
145             sys.stdout.write(pot.pot.__str__())
146     elif options.sql:
147         if not options.locale:
148             opts.error('Must specify an output locale')
149         pot = SQL()
150         pot.loadpo(options.sql)
151         pot.create_sql(options.locale)
152         if not options.outfile:
153             outfile = sys.stdout
154         else:
155             outfile = codecs.open(options.outfile, encoding='utf-8', mode='w')
156         for insert in pot.sql: 
157             outfile.write(insert + "\n")
158     else:
159         opts.print_help()
160
161 if __name__ == '__main__':
162     main()