]> git.lyx.org Git - lyx.git/blob - development/tools/mergepo.py
0ed3b49aff15b8d44a793a7d2e21d660e6a76047
[lyx.git] / development / tools / mergepo.py
1 #! /usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 # file mergepo.py
5 # This file is part of LyX, the document processor.
6 # Licence details can be found in the file COPYING.
7
8 # author Georg Baum
9
10 # Full author contact details are available in file CREDITS
11
12 # This script takes missing translations from another set of po files and
13 # merges them into the po files in this source tree.
14
15
16 import os, re, string, sys
17 import io
18 import polib
19 from optparse import OptionParser
20
21
22 def trim_eol(line):
23     " Remove end of line char."
24     if line[-1:] == '\n':
25         return line[:-1]
26     else:
27         # file with no EOL in last line
28         return line
29
30
31 def read(input):
32     " Read utf8 input file and strip lineendings."
33     lines = list()
34     while 1:
35         line = input.readline()
36         if not line:
37             break
38         line = trim_eol(line)
39         lines.append(line)
40     return lines
41
42
43 def parse_msg(lines):
44     " Extracts msgid or msgstr from lines."
45     if len(lines) < 1:
46         return ''
47     i = lines[0].find('"')
48     if i < 0:
49         return ''
50     msg = lines[0][i:].strip('"')
51     for i in range(1, len(lines)):
52         j = lines[i].find('"')
53         if j < 0:
54             return ''
55         msg = msg + lines[i][j:].strip('"')
56     return polib.unescape(msg)
57
58
59 def translate(msgid, msgstr_lines, po2, options):
60     msgstr = parse_msg(msgstr_lines)
61     if options.overwrite:
62         other = po2.find(msgid)
63         if not other:
64             return 0
65         if not other.translated():
66             return 0
67         if msgstr == other.msgstr:
68             return 0
69     else:
70         if msgstr != '':
71             return 0
72         other = po2.find(msgid)
73         if not other:
74             return 0
75         if not other.translated():
76             return 0
77     if options.nonnull and other.msgstr == other.msgid:
78         return 0
79     msgstr = other.msgstr
80     obsolete = (msgstr_lines[0].find('#~') == 0)
81     j = msgstr_lines[0].find('"')
82     # must not assign to msgstr_lines, because that would not be seen by our caller
83     new_lines = polib.wrap(msgstr_lines[0][0:j+1] + polib.escape(msgstr), 76, drop_whitespace = False)
84     del msgstr_lines[:]
85     for i in range(0, len(new_lines)):
86         if i == 0:
87             msgstr_lines.append(new_lines[i] + '"')
88         elif obsolete:
89             msgstr_lines.append('#~ "' + new_lines[i] + '"')
90         else:
91             msgstr_lines.append('"' + new_lines[i] + '"')
92     return 1
93
94
95 def mergepo_polib(target, source, options):
96     changed = 0
97     po1 = polib.pofile(target)
98     po2 = polib.pofile(source)
99     if options.overwrite:
100         for entry in po1.entries():
101             other = po2.find(entry.msgid, include_obsolete_entries=True)
102             if not other:
103                 continue
104             if options.nonnull and other.msgstr == other.msgid:
105                 continue
106             if other.translated() and other.msgstr != entry.msgstr:
107                 entry.msgstr = other.msgstr
108                 changed = changed + 1
109     else:
110         for entry in po1.untranslated_entries():
111             other = po2.find(entry.msgid, include_obsolete_entries=True)
112             if not other:
113                 continue
114             if options.nonnull and other.msgstr == other.msgid:
115                 continue
116             if other.translated():
117                 entry.msgstr = other.msgstr
118                 changed = changed + 1
119     if changed > 0:
120         po1.save(target)
121     return changed
122
123
124 def mergepo_minimaldiff(target, source, options):
125     changed = 0
126     po2 = polib.pofile(source)
127     target_enc = polib.detect_encoding(target)
128     # for utf8 files we can use our self written parser to minimize diffs,
129     # otherwise we need to use polib
130     if not target_enc in ['UTF-8', 'utf-8', 'utf_8']:
131         raise
132     # open file with universal newlines, since it can happen that we are
133     # on unix, but the file has been written on windows or vice versa.
134     po1 = io.open(target, 'r', encoding='utf_8', newline=None)
135     oldlines = read(po1)
136     po1.close()
137     newlines = []
138     in_msgid = False
139     in_msgstr = False
140     msgstr_lines = []
141     msgid_lines = []
142     msgid = ''
143     for line in oldlines:
144         if in_msgid:
145             if line.find('"') == 0 or line.find('#~ "') == 0:
146                 msgid_lines.append(line)
147             else:
148                 in_msgid = False
149                 msgid = parse_msg(msgid_lines)
150                 newlines.extend(msgid_lines)
151                 msgid_lines = []
152         elif in_msgstr:
153             if line.find('"') == 0 or line.find('#~ "') == 0:
154                 msgstr_lines.append(line)
155             else:
156                 in_msgstr = False
157                 changed = changed + translate(msgid, msgstr_lines, po2, options)
158                 newlines.extend(msgstr_lines)
159                 msgstr_lines = []
160                 msgid = ''
161         if not in_msgid and not in_msgstr:
162             if line.find('msgid') == 0 or line.find('#~ msgid') == 0:
163                 msgid_lines.append(line)
164                 in_msgid = True
165             elif line.find('msgstr') == 0 or line.find('#~ msgstr') == 0:
166                 if line.find('msgstr[') == 0 or line.find('#~ msgstr[') == 0:
167                     # plural forms are not implemented
168                     raise
169                 msgstr_lines.append(line)
170                 in_msgstr = True
171             else:
172                 newlines.append(line)
173     if msgid != '':
174         # the file ended with a msgstr
175         changed = changed + translate(msgid, msgstr_lines, po2, options)
176         newlines.extend(msgstr_lines)
177         msgstr_lines = []
178         msgid = ''
179     if changed > 0:
180         # we store .po files with unix line ends in git,
181         # so do always write them even on windows
182         po1 = io.open(target, 'w', encoding='utf_8', newline='\n')
183         for line in newlines:
184             po1.write(line + '\n')
185     return changed
186
187
188 def mergepo(target, source, options):
189     if not os.path.exists(source):
190         sys.stderr.write('Skipping %s since %s does not exist.\n' % (target, source))
191         return
192     if not os.path.exists(target):
193         sys.stderr.write('Skipping %s since %s does not exist.\n' % (target, target))
194         return
195     sys.stderr.write('Merging %s into %s: ' % (source, target))
196     try:
197         changed = mergepo_minimaldiff(target, source, options)
198         sys.stderr.write('Updated %d translations with minimal diff.\n' % changed)
199     except:
200         changed = mergepo_polib(target, source, options)
201         sys.stderr.write('Updated %d translations using polib.\n' % changed)
202
203
204 def main(argv):
205
206     parser = OptionParser(description = """This script reads translations from .po files in the given source directory
207 and adds all translations that do not already exist to the corresponding .po
208 files in the target directory. It is recommended to remerge strings from the
209 source code before running this script. Otherwise translations that are not
210 yet in the target .po files are not updated.""", usage = "Usage: %prog [options] sourcedir")
211     parser.add_option("-t", "--target", dest="target",
212                       help="target directory containing .po files. If missing, it is determined from the script location.")
213     parser.add_option("-l", "--language", dest="language",
214                       help="language for which translations are merged (if missing, all languages are merged)")
215     parser.add_option("-o", "--overwrite", action="store_true", dest="overwrite", default=False,
216                       help="overwrite existing target translations with source translations (if missing, only new translations are added)")
217     parser.add_option("-n", "--nonnull", action="store_true", dest="nonnull", default=False,
218                       help="do not update target translations with source translations that are identical to the untranslated text)")
219     (options, args) = parser.parse_args(argv)
220     if len(args) <= 1:
221         parser.print_help()
222         return 0
223
224     toolsdir = os.path.dirname(args[0])
225     if options.target:
226         podir1 = os.path.abspath(options.target)
227     else:
228         podir1 = os.path.normpath(os.path.join(toolsdir, '../../po'))
229     podir2 = os.path.abspath(args[1])
230
231     if options.language:
232         name = options.language + '.po'
233         mergepo(os.path.join(podir1, name), os.path.join(podir2, name), options)
234     else:
235         for i in os.listdir(podir1):
236             (base, ext) = os.path.splitext(i)
237             if ext != ".po":
238                 continue
239             mergepo(os.path.join(podir1, i), os.path.join(podir2, i), options)
240
241     return 0
242
243
244 if __name__ == "__main__":
245     main(sys.argv)