]> git.lyx.org Git - features.git/blob - po/lyx_pot.py
3ac835d4095dbecce7fb5d4ee6c14bc02dc22a8a
[features.git] / po / lyx_pot.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 # file lyx_pot.py
5 # This file is part of LyX, the document processor.
6 # Licence details can be found in the file COPYING.
7 #
8 # \author Bo Peng
9 #
10 # Full author contact details are available in file CREDITS
11
12 # Usage: use
13 #     lyx_pot.py -h
14 # to get usage message
15
16 # This script will extract translatable strings from input files and write
17 # to output in gettext .pot format.
18 #
19 import sys, os, re, getopt
20 if sys.version_info < (2, 4, 0):
21     from sets import Set as set
22
23 def relativePath(path, base):
24     '''return relative path from top source dir'''
25     # full pathname of path
26     path1 = os.path.normpath(os.path.realpath(path)).split(os.sep)
27     path2 = os.path.normpath(os.path.realpath(base)).split(os.sep)
28     if path1[:len(path2)] != path2:
29         print "Path %s is not under top source directory" % path
30     path3 = os.path.join(*path1[len(path2):]);
31     # replace all \ by / such that we get the same comments on Windows and *nix
32     path3 = path3.replace('\\', '/')
33     return path3
34
35
36 def writeString(outfile, infile, basefile, lineno, string):
37     string = string.replace('\\', '\\\\').replace('"', '')
38     if string == "":
39         return
40     print >> outfile, '#: %s:%d\nmsgid "%s"\nmsgstr ""\n' % \
41         (relativePath(infile, basefile), lineno, string)
42
43
44 def ui_l10n(input_files, output, base):
45     '''Generate pot file from lib/ui/*'''
46     output = open(output, 'w')
47     Submenu = re.compile(r'^[^#]*Submenu\s+"([^"]*)"')
48     Popupmenu = re.compile(r'^[^#]*PopupMenu\s+"[^"]+"\s+"([^"]*)"')
49     IconPalette = re.compile(r'^[^#]*IconPalette\s+"[^"]+"\s+"([^"]*)"')
50     Toolbar = re.compile(r'^[^#]*Toolbar\s+"[^"]+"\s+"([^"]*)"')
51     Item = re.compile(r'[^#]*Item\s+"([^"]*)"')
52     TableInsert = re.compile(r'[^#]*TableInsert\s+"([^"]*)"')
53     for src in input_files:
54         input = open(src)
55         for lineno, line in enumerate(input.readlines()):
56             if Submenu.match(line):
57                 (string,) = Submenu.match(line).groups()
58                 string = string.replace('_', ' ')
59             elif Popupmenu.match(line):
60                 (string,) = Popupmenu.match(line).groups()
61             elif IconPalette.match(line):
62                 (string,) = IconPalette.match(line).groups()
63             elif Toolbar.match(line):
64                 (string,) = Toolbar.match(line).groups()
65             elif Item.match(line):
66                 (string,) = Item.match(line).groups()
67             elif TableInsert.match(line):
68                 (string,) = TableInsert.match(line).groups()
69             else:
70                 continue
71             string = string.replace('"', '')
72             if string != "":
73                 print >> output, '#: %s:%d\nmsgid "%s"\nmsgstr ""\n' % \
74                     (relativePath(src, base), lineno+1, string)
75         input.close()
76     output.close()
77
78
79 def layouts_l10n(input_files, output, base, layouttranslations):
80     '''Generate pot file from lib/layouts/*.{layout,inc,module}'''
81     out = open(output, 'w')
82     Style = re.compile(r'^Style\s+(.*)', re.IGNORECASE)
83     # include ???LabelString???, but exclude comment lines
84     LabelString = re.compile(r'^[^#]*LabelString\S*\s+(.*)')
85     GuiName = re.compile(r'\s*GuiName\s+(.*)')
86     ListName = re.compile(r'\s*ListName\s+(.*)')
87     CategoryName = re.compile(r'\s*Category\s+(.*)')
88     NameRE = re.compile(r'DeclareLyXModule.*{(.*)}')
89     InsetLayout = re.compile(r'^InsetLayout\s+\"?(.*)\"?')
90     FlexCheck = re.compile(r'^Flex:(.*)')
91     DescBegin = re.compile(r'#+\s*DescriptionBegin\s*$')
92     DescEnd = re.compile(r'#+\s*DescriptionEnd\s*$')
93     Category = re.compile(r'#Category: (.*)$')
94     I18nPreamble = re.compile(r'\s*(Lang)|(Babel)Preamble\s*$')
95     EndI18nPreamble = re.compile(r'\s*End(Lang)|(Babel)Preamble\s*$')
96     I18nString = re.compile(r'_\(([^\)]+)\)')
97     CounterFormat = re.compile(r'\s*PrettyFormat\s+"?(.*)"?')
98     CiteFormat = re.compile(r'\s*CiteFormat')
99     KeyVal = re.compile(r'^\s*_\w+\s+(.*)$')
100     Float = re.compile(r'\s*Float')
101     End = re.compile(r'\s*End')
102     Comment = re.compile(r'\s*#')
103
104     languages = []
105     keyset = set()
106     if layouttranslations:
107         linguas_file = os.path.join(base, 'po/LINGUAS')
108         for line in open(linguas_file).readlines():
109             if Comment.search(line) == None:
110                 languages.extend(line.split())
111     # walon is not a known document language
112     # FIXME: Do not hardcode, read from lib/languages!
113     if 'wa' in languages:
114         languages.remove('wa')
115
116     for src in input_files:
117         readingDescription = False
118         readingI18nPreamble = False
119         readingFloat = False
120         readingCiteFormats = False
121         descStartLine = -1
122         descLines = []
123         lineno = 0
124         for line in open(src).readlines():
125             lineno += 1
126             if readingDescription:
127                 res = DescEnd.search(line)
128                 if res != None:
129                     readingDescription = False
130                     desc = " ".join(descLines)
131                     if not layouttranslations:
132                         writeString(out, src, base, lineno + 1, desc)
133                     continue
134                 descLines.append(line[1:].strip())
135                 continue
136             res = DescBegin.search(line)
137             if res != None:
138                 readingDescription = True
139                 descStartLine = lineno
140                 continue
141             if readingI18nPreamble:
142                 res = EndI18nPreamble.search(line)
143                 if res != None:
144                     readingI18nPreamble = False
145                     continue
146                 res = I18nString.search(line)
147                 if res != None:
148                     string = res.group(1)
149                     if layouttranslations:
150                         keyset.add(string)
151                     else:
152                         writeString(out, src, base, lineno, string)
153                 continue
154             res = I18nPreamble.search(line)
155             if res != None:
156                 readingI18nPreamble = True
157                 continue
158             res = NameRE.search(line)
159             if res != None:
160                 string = res.group(1)
161                 if not layouttranslations:
162                     writeString(out, src, base, lineno + 1, string)
163                 continue
164             res = Style.search(line)
165             if res != None:
166                 string = res.group(1)
167                 string = string.replace('_', ' ')
168                 if not layouttranslations:
169                     writeString(out, src, base, lineno, string)
170                 continue
171             res = LabelString.search(line)
172             if res != None:
173                 string = res.group(1)
174                 if not layouttranslations:
175                     writeString(out, src, base, lineno, string)
176                 continue
177             res = GuiName.search(line)
178             if res != None:
179                 string = res.group(1)
180                 if layouttranslations:
181                     # gui name must only be added for floats
182                     if readingFloat:
183                         keyset.add(string)
184                 else:
185                     writeString(out, src, base, lineno, string)
186                 continue
187             res = CategoryName.search(line)
188             if res != None:
189                 string = res.group(1)
190                 if not layouttranslations:
191                     writeString(out, src, base, lineno, string)
192                 continue
193             res = ListName.search(line)
194             if res != None:
195                 string = res.group(1)
196                 if layouttranslations:
197                     keyset.add(string.strip('"'))
198                 else:
199                     writeString(out, src, base, lineno, string)
200                 continue
201             res = InsetLayout.search(line)
202             if res != None:
203                 string = res.group(1)
204                 string = string.replace('_', ' ')
205                 #Flex:xxx is not used in translation
206                 #if not layouttranslations:
207                 #    writeString(out, src, base, lineno, string)
208                 m = FlexCheck.search(string)
209                 if m:
210                     if not layouttranslations:
211                         writeString(out, src, base, lineno, m.group(1))
212                 continue
213             res = Category.search(line)
214             if res != None:
215                 string = res.group(1)
216                 if not layouttranslations:
217                     writeString(out, src, base, lineno, string)
218                 continue
219             res = CounterFormat.search(line)
220             if res != None:
221                 string = res.group(1)
222                 if not layouttranslations:
223                     writeString(out, src, base, lineno, string)
224                 continue
225             res = Float.search(line)
226             if res != None:
227                 readingFloat = True
228                 continue
229             res = CiteFormat.search(line)
230             if res != None:
231                 readingCiteFormats = True
232             res = End.search(line)
233             if res != None:
234                 readingCiteFormats = False
235                 readingFloat = False
236             if readingCiteFormats:
237                 res = KeyVal.search(line)
238                 if res != None:
239                     val = res.group(1)
240                     if not layouttranslations:
241                         writeString(out, src, base, lineno, val)
242
243     if layouttranslations:
244         # Extract translations of layout files
245         import polib
246
247         # Sort languages and key to minimize the diff between different runs
248         # with changed translations
249         languages.sort()
250         keys = []
251         for key in keyset:
252             keys.append(key)
253         keys.sort()
254
255         print >> out, '''# This file has been automatically generated by po/lyx_pot.py.
256 # PLEASE DO NOT MODIFY ANYTHING HERE! If you want to regenerate this file
257 # from the translations, run `make ../lib/layouttranslations' in po.'''
258         for lang in languages:
259             print >> out, '\nTranslation %s' % lang
260             poname = os.path.join(base, 'po/' + lang + '.po')
261             po = polib.pofile(poname)
262             # Iterate through po entries and not keys for speed reasons.
263             # FIXME: The code is still too slow
264             trans = dict()
265             for entry in po:
266                 if not entry.translated():
267                     continue
268                 if entry.msgid in keys:
269                     key = entry.msgid.replace('\\', '\\\\').replace('"', '\\"')
270                     val = entry.msgstr.replace('\\', '\\\\').replace('"', '\\"')
271                     # some translators keep untranslated entries
272                     if val != key:
273                         trans[key] = val
274             for key in keys:
275                 if key in trans.keys():
276                     val = trans[key]
277                     print >> out, '\t"%s" "%s"' % \
278                              (key.encode('utf-8'), val.encode('utf-8'))
279             print >> out, 'End'
280
281     out.close()
282
283
284 def qt4_l10n(input_files, output, base):
285     '''Generate pot file from src/frontends/qt4/ui/*.ui'''
286     output = open(output, 'w')
287     pat = re.compile(r'\s*<string>(.*)</string>')
288     prop = re.compile(r'\s*<property.*name.*=.*shortcut')
289     for src in input_files:
290         input = open(src)
291         skipNextLine = False
292         for lineno, line in enumerate(input.readlines()):
293             # skip the line after <property name=shortcut>
294             if skipNextLine:
295                 skipNextLine = False
296                 continue
297             if prop.match(line):
298                 skipNextLine = True
299                 continue
300             # get lines that match <string>...</string>
301             if pat.match(line):
302                 (string,) = pat.match(line).groups()
303                 string = string.replace('&amp;', '&').replace('&quot;', '"')
304                 string = string.replace('&lt;', '<').replace('&gt;', '>')
305                 string = string.replace('\\', '\\\\').replace('"', r'\"')
306                 string = string.replace('&#x0a;', r'\n')
307                 print >> output, '#: %s:%d\nmsgid "%s"\nmsgstr ""\n' % \
308                     (relativePath(src, base), lineno+1, string)
309         input.close()
310     output.close()
311
312
313 def languages_l10n(input_files, output, base):
314     '''Generate pot file from lib/languages'''
315     out = open(output, 'w')
316     GuiName = re.compile(r'^[^#]*GuiName\s+(.*)')
317     
318     for src in input_files:
319         descStartLine = -1
320         descLines = []
321         lineno = 0
322         for line in open(src).readlines():
323             lineno += 1
324             res = GuiName.search(line)
325             if res != None:
326                 string = res.group(1)
327                 writeString(out, src, base, lineno, string)
328                 continue
329                
330     out.close()
331
332
333 def external_l10n(input_files, output, base):
334     '''Generate pot file from lib/external_templates'''
335     output = open(output, 'w')
336     Template = re.compile(r'^Template\s+(.*)')
337     GuiName = re.compile(r'\s*GuiName\s+(.*)')
338     HelpTextStart = re.compile(r'\s*HelpText\s')
339     HelpTextSection = re.compile(r'\s*(\S.*)\s*$')
340     HelpTextEnd = re.compile(r'\s*HelpTextEnd\s')
341     i = -1
342     for src in input_files:
343         input = open(src)
344         inHelp = False
345         hadHelp = False
346         prev_help_string = ''
347         for lineno, line in enumerate(input.readlines()):
348             if Template.match(line):
349                 (string,) = Template.match(line).groups()
350             elif GuiName.match(line):
351                 (string,) = GuiName.match(line).groups()
352             elif inHelp:
353                 if HelpTextEnd.match(line):
354                     if hadHelp:
355                         print >> output, '\nmsgstr ""\n'
356                     inHelp = False
357                     hadHelp = False
358                     prev_help_string = ''
359                 elif HelpTextSection.match(line):
360                     (help_string,) = HelpTextSection.match(line).groups()
361                     help_string = help_string.replace('"', '')
362                     if help_string != "" and prev_help_string == '':
363                         print >> output, '#: %s:%d\nmsgid ""\n"%s\\n"' % \
364                             (relativePath(src, base), lineno+1, help_string)
365                         hadHelp = True
366                     elif help_string != "":
367                         print >> output, '"%s\\n"' % help_string
368                     prev_help_string = help_string
369             elif HelpTextStart.match(line):
370                 inHelp = True
371                 prev_help_string = ''
372             else:
373                 continue
374             string = string.replace('"', '')
375             if string != "" and not inHelp:
376                 print >> output, '#: %s:%d\nmsgid "%s"\nmsgstr ""\n' % \
377                     (relativePath(src, base), lineno+1, string)
378         input.close()
379     output.close()
380
381
382 def formats_l10n(input_files, output, base):
383     '''Generate pot file from configure.py'''
384     output = open(output, 'w')
385     GuiName = re.compile(r'.*\Format\s+\S+\s+\S+\s+"([^"]*)"\s+(\S*)\s+.*')
386     GuiName2 = re.compile(r'.*\Format\s+\S+\s+\S+\s+([^"]\S+)\s+(\S*)\s+.*')
387     input = open(input_files[0])
388     for lineno, line in enumerate(input.readlines()):
389         label = ""
390         labelsc = ""
391         if GuiName.match(line):
392             label = GuiName.match(line).group(1)
393             shortcut = GuiName.match(line).group(2).replace('"', '')
394         elif GuiName2.match(line):
395             label = GuiName2.match(line).group(1)
396             shortcut = GuiName2.match(line).group(2).replace('"', '')
397         else:
398             continue
399         label = label.replace('\\', '\\\\').replace('"', '')
400         if shortcut != "":
401             labelsc = label + "|" + shortcut
402         if label != "":
403             print >> output, '#: %s:%d\nmsgid "%s"\nmsgstr ""\n' % \
404                 (relativePath(input_files[0], base), lineno+1, label)
405         if labelsc != "":
406             print >> output, '#: %s:%d\nmsgid "%s"\nmsgstr ""\n' % \
407                 (relativePath(input_files[0], base), lineno+1, labelsc)
408     input.close()
409     output.close()
410
411
412 def encodings_l10n(input_files, output, base):
413     '''Generate pot file from lib/encodings'''
414     output = open(output, 'w')
415     # assuming only one encodings file
416     #                 Encoding utf8      utf8    "Unicode (utf8)" UTF-8    variable inputenc
417     reg = re.compile('Encoding [\w-]+\s+[\w-]+\s+"([\w \-\(\)]+)"\s+[\w-]+\s+(fixed|variable)\s+\w+.*')
418     input = open(input_files[0])
419     for lineno, line in enumerate(input.readlines()):
420         if not line.startswith('Encoding'):
421             continue
422         if reg.match(line):
423             print >> output, '#: %s:%d\nmsgid "%s"\nmsgstr ""\n' % \
424                 (relativePath(input_files[0], base), lineno+1, reg.match(line).groups()[0])
425         else:
426             print "Error: Unable to handle line:"
427             print line
428             # No need to abort if the parsing fails
429             # sys.exit(1)
430     input.close()
431     output.close()
432
433
434
435 Usage = '''
436 lyx_pot.py [-b|--base top_src_dir] [-o|--output output_file] [-h|--help] [-s|src_file filename] -t|--type input_type input_files
437
438 where
439     --base:
440         path to the top source directory. default to '.'
441     --output:
442         output pot file, default to './lyx.pot'
443     --src_file
444         filename that contains a list of input files in each line
445     --input_type can be
446         ui: lib/ui/*
447         layouts: lib/layouts/*
448         layouttranslations: create lib/layouttranslations from po/*.po and lib/layouts/*
449         qt4: qt4 ui files
450         languages: file lib/languages
451         encodings: file lib/encodings
452         external: external templates file
453         formats: formats predefined in lib/configure.py
454 '''
455
456 if __name__ == '__main__':
457     input_type = None
458     output = 'lyx.pot'
459     base = '.'
460     input_files = []
461     #
462     optlist, args = getopt.getopt(sys.argv[1:], 'ht:o:b:s:',
463         ['help', 'type=', 'output=', 'base=', 'src_file='])
464     for (opt, value) in optlist:
465         if opt in ['-h', '--help']:
466             print Usage
467             sys.exit(0)
468         elif opt in ['-o', '--output']:
469             output = value
470         elif opt in ['-b', '--base']:
471             base = value
472         elif opt in ['-t', '--type']:
473             input_type = value
474         elif opt in ['-s', '--src_file']:
475             input_files = [f.strip() for f in open(value)]
476
477     if input_type not in ['ui', 'layouts', 'layouttranslations', 'qt4', 'languages', 'encodings', 'external', 'formats'] or output is None:
478         print 'Wrong input type or output filename.'
479         sys.exit(1)
480
481     input_files += args
482
483     if input_type == 'ui':
484         ui_l10n(input_files, output, base)
485     elif input_type == 'layouts':
486         layouts_l10n(input_files, output, base, False)
487     elif input_type == 'layouttranslations':
488         layouts_l10n(input_files, output, base, True)
489     elif input_type == 'qt4':
490         qt4_l10n(input_files, output, base)
491     elif input_type == 'external':
492         external_l10n(input_files, output, base)
493     elif input_type == 'formats':
494         formats_l10n(input_files, output, base)
495     elif input_type == 'encodings':
496         encodings_l10n(input_files, output, base)
497     else:
498         languages_l10n(input_files, output, base)
499
500