LP#1709932: recognize more strings from oils_i18n_gettext()
[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\((.*?)\'\s*\)', re.UNICODE+re.MULTILINE+re.DOTALL)
58         intkey = re.compile(r'\s*(?P<id>\d+)\s*,\s*E?\'(?P<string>.+?)\'\s*,\s*\'(?P<class>.+?)\'\s*,\s*\'(?P<property>.+?)$', re.UNICODE+re.MULTILINE+re.DOTALL)
59         textkey = re.compile(r'\s*\'(?P<id>.*?)\'\s*,\s*E?\'(?P<string>.+?)\'\s*,\s*\'(?P<class>.+?)\'\s*,\s*\'(?P<property>.+?)$', re.UNICODE+re.MULTILINE+re.DOTALL)
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         sourcelines = sourcefile.read()
65         try:
66             for match in findi18n.finditer(sourcelines):
67                 parms = match.group(1)
68                 num = sourcelines[:match.start()].count('\n') + 1  # ugh
69
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                 if fi18n is None:
76                     raise Exception("Cannot parse the source. Empty strings in there? String I cannot parse is %s" % parms)
77
78                 fq_field = "%s.%s" % (fi18n.group('class'), fi18n.group('property'))
79
80                 # strip sql string concatenation
81                 strx = re.sub(r'\'\s*\|\|\s*\'', '', fi18n.group('string'))
82
83                 # Unescape escaped SQL single-quotes for translators' sanity
84                 msgid = re.compile(r'\'\'').sub("'", strx)
85
86                 # Hmm, sometimes people use ":" in text identifiers and
87                 # polib doesn't seem to like that; urlencode the colon
88                 occurid = re.compile(r':').sub("%3A", fi18n.group('id'))
89
90                 if (msgid in serts):
91                     serts[msgid].occurrences.append((os.path.basename(source), num))
92                     serts[msgid].tcomment = ' '.join((serts[msgid].tcomment, 'id::%s__%s' % (fq_field, occurid)))
93                 else:
94                     poe = polib.POEntry()
95                     poe.tcomment = 'id::%s__%s' % (fq_field, occurid)
96                     poe.occurrences = [(os.path.basename(source), num)]
97                     poe.msgid = msgid
98                     serts[msgid] = poe
99         except Exception, exc:
100             print "Error in oils_i18n_gettext line %d of SQL source file: %s" % (num, exc)
101
102         for poe in serts.values():
103             self.pot.append(poe)
104
105     def create_sql(self, locale):
106         """
107         Creates a set of INSERT statements that place translated strings
108         into the config.i18n_core table.
109         """
110
111         insert = "INSERT INTO config.i18n_core (fq_field, identity_value," \
112             " translation, string) VALUES ('%s', '%s', '%s', '%s');"
113         idregex = re.compile(r'^id::(?P<class>.*?)__(?P<id>.*?)$')
114         for entry in self.pot:
115             for id_value in entry.tcomment.split():
116                 # Escape SQL single-quotes to avoid b0rkage
117                 msgstr = re.compile(r'\'').sub("''", entry.msgstr)
118
119                 identifier = idregex.search(id_value)
120                 if identifier is None:
121                     continue
122                 # And unescape any colons in the occurence ID
123                 occurid = re.compile(r'%3A').sub(':', identifier.group('id'))
124
125                 if msgstr == '':
126                     # Don't generate a stmt for an untranslated string
127                     break
128                 self.sql.append(insert % (identifier.group('class'), occurid, locale, msgstr))
129
130 def main():
131     """
132     Determine what action to take
133     """
134     opts = optparse.OptionParser()
135     opts.add_option('-p', '--pot', action='store', \
136         help='Generate POT from the specified source SQL file', metavar='FILE')
137     opts.add_option('-s', '--sql', action='store', \
138         help='Generate SQL from the specified source POT file', metavar='FILE')
139     opts.add_option('-l', '--locale', \
140         help='Locale of the SQL file that will be generated')
141     opts.add_option('-o', '--output', dest='outfile', \
142         help='Write output to FILE (defaults to STDOUT)', metavar='FILE')
143     (options, args) = opts.parse_args()
144
145     if options.pot:
146         pot = SQL()
147         pot.getstrings(options.pot)
148         if options.outfile:
149             pot.savepot(options.outfile)
150         else:
151             sys.stdout.write(pot.pot.__str__())
152     elif options.sql:
153         if not options.locale:
154             opts.error('Must specify an output locale')
155         pot = SQL()
156         pot.loadpo(options.sql)
157         pot.create_sql(options.locale)
158         if not options.outfile:
159             outfile = sys.stdout
160         else:
161             outfile = codecs.open(options.outfile, encoding='utf-8', mode='w')
162         for insert in pot.sql: 
163             outfile.write(insert + "\n")
164     else:
165         opts.print_help()
166
167 if __name__ == '__main__':
168     main()