Fix SQL translation script and associated unit tests
[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 optparse
22 import polib
23 import re
24 import sys
25 import os.path
26
27 class SQL(basel10n.BaseL10N):
28     """
29     This class provides methods for extracting translatable strings from
30     Evergreen's database seed values, generating a translatable POT file,
31     reading translated PO files, and generating SQL for inserting the
32     translated values into the Evergreen database.
33     """
34
35     def __init__(self):
36         self.pot = None
37         basel10n.BaseL10N.__init__(self)
38         self.sql = []
39
40     def getstrings(self, source):
41         """
42         Each INSERT statement contains 0 or more oils_i18n_gettext()
43         markers for the en-US string that identify the string (which
44         we push into the POEntry.occurrences attribute), class hint,
45         and property. We concatenate the class hint and property and
46         use that for our msgid attribute.
47         
48         A sample INSERT string that we'll scan is as follows:
49
50             INSERT INTO foo.bar (key, value) VALUES 
51                 (99, oils_i18n_gettext(99, 'string', 'class hint', 'property'));
52         """
53         self.pothead()
54
55         num = 0
56         findi18n = re.compile(r'.*?oils_i18n_gettext\((.*?)\'\)')
57         intkey = re.compile(r'\s*(?P<id>\d+),\s*\'(?P<string>.+?)\',\s*\'(?P<class>.+?)\',\s*\'(?P<property>.+?)$')
58         textkey = re.compile(r'\s*\'(?P<id>.*?)\',\s*\'(?P<string>.+?)\',\s*\'(?P<class>.+?)\',\s*\'(?P<property>.+?)$')
59         serts = dict()
60
61         # Iterate through the source SQL grabbing table names and l10n strings
62         sourcefile = open(source)
63         for line in sourcefile:
64             try:
65                 num = num + 1
66                 entry = findi18n.search(line)
67                 if entry is None:
68                     continue
69                 for parms in entry.groups():
70                     # Try for an integer-based primary key parameter first
71                     fi18n = intkey.search(parms)
72                     if fi18n is None:
73                         # Otherwise, it must be a text-based primary key parameter
74                         fi18n = textkey.search(parms)
75                     fq_field = "%s.%s" % (fi18n.group('class'), fi18n.group('property'))
76                     # Unescape escaped SQL single-quotes for translators' sanity
77                     msgid = re.compile(r'\'\'').sub("'", fi18n.group('string'))
78
79                     # Hmm, sometimes people use ":" in text identifiers and
80                     # polib doesn't seem to like that; urlencode the colon
81                     occurid = re.compile(r':').sub("%3A", fi18n.group('id'))
82
83                     if (msgid in serts):
84                         serts[msgid].occurrences.append((os.path.basename(source), num))
85                         serts[msgid].tcomment = ' '.join((serts[msgid].tcomment, 'id::%s__%s' % (fq_field, occurid)))
86                     else:
87                         poe = polib.POEntry()
88                         poe.tcomment = 'id::%s__%s' % (fq_field, occurid)
89                         poe.occurrences = [(os.path.basename(source), num)]
90                         poe.msgid = msgid
91                         serts[msgid] = poe
92             except Exception as exc:
93                 print "Error in line %d of SQL source file: %s" % (num, exc) 
94
95         for poe in serts.values():
96             self.pot.append(poe)
97
98     def create_sql(self, locale):
99         """
100         Creates a set of INSERT statements that place translated strings
101         into the config.i18n_core table.
102         """
103
104         insert = "INSERT INTO config.i18n_core (fq_field, identity_value," \
105             " translation, string) VALUES ('%s', '%s', '%s', '%s');"
106         idregex = re.compile(r'^id::(?P<class>.*?)__(?P<id>.*?)$')
107         for entry in self.pot:
108             for id_value in entry.tcomment.split():
109                 # Escape SQL single-quotes to avoid b0rkage
110                 msgstr = re.compile(r'\'').sub("''", entry.msgstr)
111
112                 identifier = idregex.search(id_value)
113                 if identifier is None:
114                     continue
115                 # And unescape any colons in the occurence ID
116                 occurid = re.compile(r'%3A').sub(':', identifier.group('id'))
117
118                 if msgstr == '':
119                     # Don't generate a stmt for an untranslated string
120                     break
121                 self.sql.append(insert % (identifier.group('class'), occurid, locale, msgstr))
122
123 def main():
124     """
125     Determine what action to take
126     """
127     opts = optparse.OptionParser()
128     opts.add_option('-p', '--pot', action='store', \
129         help='Generate POT from the specified source SQL file', metavar='FILE')
130     opts.add_option('-s', '--sql', action='store', \
131         help='Generate SQL from the specified source POT file', metavar='FILE')
132     opts.add_option('-l', '--locale', \
133         help='Locale of the SQL file that will be generated')
134     opts.add_option('-o', '--output', dest='outfile', \
135         help='Write output to FILE (defaults to STDOUT)', metavar='FILE')
136     (options, args) = opts.parse_args()
137
138     if options.pot:
139         pot = SQL()
140         pot.getstrings(options.pot)
141         if options.outfile:
142             pot.savepot(options.outfile)
143         else:
144             sys.stdout.write(pot.pot.__str__())
145     elif options.sql:
146         if not options.locale:
147             opts.error('Must specify an output locale')
148         pot = SQL()
149         pot.loadpo(options.sql)
150         pot.create_sql(options.locale)
151         if not options.outfile:
152             outfile = sys.stdout
153         else:
154             outfile = open(options.outfile, 'w')
155         for insert in pot.sql: 
156             outfile.write(insert + "\n")
157     else:
158         opts.print_help()
159
160 if __name__ == '__main__':
161     main()