Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/build_config/updateMessageIDs.py
169674 views
1
#!/usr/bin/env python
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2011-2025 German Aerospace Center (DLR) and others.
4
# This program and the accompanying materials are made available under the
5
# terms of the Eclipse Public License 2.0 which is available at
6
# https://www.eclipse.org/legal/epl-2.0/
7
# This Source Code may also be made available under the following Secondary
8
# Licenses when the conditions for such availability set forth in the Eclipse
9
# Public License 2.0 are satisfied: GNU General Public License, version 2
10
# or later which is available at
11
# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
12
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
13
14
# @file updateMessageIDs.py
15
# @author Mirko Barthauer
16
# @date 2023-06-26
17
18
"""
19
Change gettext message IDs and propagate the changes to the source code and translated messages.
20
"""
21
from __future__ import absolute_import
22
from __future__ import print_function
23
import os
24
import sys
25
import io
26
import re
27
import polib
28
import subprocess
29
import i18n
30
from glob import glob
31
from argparse import ArgumentParser
32
33
SUMO_HOME = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
34
SUMO_LIBRARIES = os.environ.get("SUMO_LIBRARIES", os.path.join(os.path.dirname(SUMO_HOME), "SUMOLibraries"))
35
36
37
def getOptions(args=None):
38
ap = ArgumentParser()
39
ap.add_argument("-l", "--lang", nargs='*',
40
help="languages to process (using the gettext short codes like en, fr, de)")
41
ap.add_argument("--replace", nargs='*',
42
help="search/replace commands to apply automatically, giving the search string on the uneven \
43
position and the replace string on the even position; cannot be used together with \
44
--numbered-placeholders")
45
ap.add_argument("--numbered-placeholders", dest="numberedPlaceholders", default=False, action="store_true",
46
help="add a running number to every placeholder in a msgid, starting with 0; cannot be \
47
used together with --replace")
48
ap.add_argument("--placeholder", default='%', help="string used for unnumbered placeholders")
49
ap.add_argument("--search-prefix", dest="searchPrefix", default=' ', type=str,
50
help="characters which can precede the actual search string in --replace (ignored with --strict)")
51
ap.add_argument("--search-suffix", dest="searchSuffix", default=' .?!', type=str,
52
help="characters which can succeed the actual search string in --replace (ignored with --strict)")
53
ap.add_argument("--strict", default=False, action="store_true",
54
help="compare the entire msgid to the search string")
55
ap.add_argument("--start", default=False, action="store_true",
56
help="create the English to English po file to edit manually")
57
ap.add_argument("--apply", default=False, action="store_true",
58
help="apply changes to source files and to msgid values in po files of languages to process")
59
ap.add_argument("--remove-obsolete", dest="removeObsolete", default=False, action="store_true",
60
help="remove obsolete msgid entries which have been superseded by replace strings")
61
ap.add_argument("--mark-fuzzy", dest="markFuzzy", default=False, action="store_true",
62
help="mark kept translations of changed msgid as fuzzy")
63
ap.add_argument("--process-languages", dest="processLanguages", default=False, action="store_true",
64
help="process msgstr values of language po files with the same rules as the original message")
65
ap.add_argument("--sumo-home", default=SUMO_HOME, help="SUMO root directory to use")
66
return ap.parse_args(args)
67
68
69
def main(args=None):
70
path = ""
71
if os.name == "nt":
72
paths = glob(os.path.join(SUMO_LIBRARIES, "gettext-*", "tools", "gettext", "bin"))
73
if paths:
74
path = paths[0] + os.path.sep
75
options = getOptions(args)
76
if not options.start and not options.apply:
77
sys.exit("At least one of the --start or --apply options has to be given to do something.")
78
if options.numberedPlaceholders and options.replace is not None:
79
print("Cannot apply both placeholder numbers and replace commands together. \
80
Replace commands will be neglected.")
81
options.replace.clear()
82
replaceRules = []
83
if options.replace is not None:
84
if len(options.replace) % 2 != 0:
85
print("The replace string for the search string '%s' is missing. The named search string will be neglected."
86
% options.replace[-1])
87
options.replace = options.replace[:-1]
88
# update newline chars in input
89
options.replace = [replaceInput.replace("\\n", "\n") for replaceInput in options.replace]
90
for i in range(0, len(options.replace), 2):
91
if options.strict:
92
replaceRules.append((options.replace[i], options.replace[i+1],
93
re.compile(re.escape(options.replace[i])), True, True))
94
else:
95
prefixes = list(options.searchPrefix)
96
if len(prefixes) == 0:
97
prefixes.append('')
98
suffixes = list(options.searchSuffix)
99
if len(suffixes) == 0:
100
suffixes.append('')
101
for prefix in prefixes:
102
for suffix in suffixes:
103
replaceRules.append((prefix + options.replace[i] + suffix,
104
prefix + options.replace[i+1] + suffix, None, False, False))
105
replaceRules.append((options.replace[i] + suffix, options.replace[i+1] + suffix, None, True, False))
106
replaceRules.append((prefix + options.replace[i], prefix + options.replace[i+1], None, False, True))
107
if options.lang is None:
108
options.lang = [os.path.basename(p)[:-8] for p in glob(options.sumo_home + "/data/po/*_sumo.po")]
109
if options.start:
110
args = ['--sumo-home', options.sumo_home, '--lang']
111
args.extend(options.lang)
112
print("Run i18n.py...")
113
i18n.main(args=args)
114
pot_file = options.sumo_home + "/data/po/sumo.pot"
115
gui_pot_file = options.sumo_home + "/data/po/gui.pot"
116
py_pot_file = options.sumo_home + "/data/po/py.pot"
117
potFiles = [pot_file, gui_pot_file, py_pot_file]
118
for potFile in potFiles:
119
print("Read pot file '%s'..." % potFile)
120
updatePotFile(path, potFile, replaceRules, options)
121
122
123
def updatePotFile(gettextPath, potFile, replaceRules, options):
124
uniLangPoFile = potFile[:-4] + "_en.po"
125
if options.start or options.replace is not None:
126
arguments = [gettextPath + "msgen", potFile, "-o", uniLangPoFile]
127
subprocess.check_call(arguments)
128
if options.replace or options.numberedPlaceholders:
129
processRules(uniLangPoFile, replaceRules, options, markObsolete=True)
130
if options.apply:
131
fileReplaceCommands = {}
132
# reload to discover which entries have changed and store for later application in source code
133
replaceIDs = []
134
po = polib.pofile(uniLangPoFile)
135
for entry in po:
136
if entry.msgid != entry.msgstr: # changes in msgid
137
for occurrence, lineNr in entry.occurrences:
138
if occurrence not in fileReplaceCommands:
139
fileReplaceCommands[occurrence] = []
140
# newline conversion between polib and source code
141
fileReplaceCommands[occurrence].append((entry.msgid.replace(
142
"\n", "\\n"), entry.msgstr.replace("\n", "\\n"), int(lineNr)))
143
replaceIDs.append(entry)
144
145
# apply the changes to the source code
146
for sourceFile, replaceCommands in fileReplaceCommands.items():
147
lines = []
148
with io.open(os.path.join(SUMO_HOME, sourceFile), "r", encoding="utf-8") as f:
149
lines.extend([line for line in f])
150
lineCount = len(lines)
151
updated = False
152
for search, replace, lineNr in replaceCommands:
153
if lineNr <= lineCount and replace not in lines[lineNr-1]:
154
lines[lineNr-1] = lines[lineNr-1].replace(search, replace)
155
updated = True
156
if updated:
157
print("\tUpdate %s" % sourceFile)
158
with io.open(os.path.join(SUMO_HOME, sourceFile), "w", encoding="utf-8", newline="\n") as f:
159
f.writelines(lines)
160
161
# change the msgid in other language files accordingly
162
for langCode in options.lang:
163
translatedPoFile = os.path.join(os.path.dirname(potFile), "%s_" % langCode +
164
os.path.basename(potFile)[:-4] + ".po")
165
if not os.path.exists(translatedPoFile):
166
print("Missing po translation file %s to update" % translatedPoFile)
167
continue
168
patchPoFile(translatedPoFile, replaceIDs, fuzzy=options.markFuzzy)
169
170
# optionally process the translated messages as well
171
if options.processLanguages:
172
processRules(translatedPoFile, replaceRules, options, translated=True,
173
filterIDs=[entry.msgstr for entry in replaceIDs])
174
if options.apply:
175
os.remove(uniLangPoFile)
176
177
178
def transferOccurrences(fromEntry, toEntry):
179
toEntry.occurrences.extend(fromEntry.occurrences)
180
fromEntry.occurrences.clear()
181
fromEntry.obsolete = True
182
183
184
def processRules(poFilePath, replaceRules, options, markObsolete=False, filterIDs=None):
185
# change the msgstr values according to the rules given in the options
186
poFile = polib.pofile(poFilePath)
187
checkFilter = filterIDs is not None
188
toRemove = []
189
for entry in poFile:
190
if checkFilter and entry.msgid not in filterIDs: # only process the wanted entries
191
continue
192
replaced = None
193
if options.numberedPlaceholders:
194
i = 0
195
placeholderIndex = entry.msgstr.find(options.placeholder)
196
if placeholderIndex > -1:
197
replaced = entry.msgstr
198
while placeholderIndex > -1:
199
replaced = replaced[:placeholderIndex + len(options.placeholder)] + str(i) +\
200
replaced[placeholderIndex + len(options.placeholder):]
201
placeholderIndex = replaced.find(options.placeholder, placeholderIndex +
202
len(options.placeholder) + len(str(i)))
203
i += 1
204
for replaceRule in replaceRules:
205
if options.strict and replaceRule[0] == entry.msgstr:
206
replaced = replaceRule[1]
207
elif replaceRule[3] and not replaceRule[4] and entry.msgstr.startswith(replaceRule[0]): # starts with ...
208
replaced = replaceRule[1] + entry.msgstr[len(replaceRule[0]):]
209
elif not replaceRule[3] and replaceRule[4] and entry.msgstr.endswith(replaceRule[0]): # ends with ...
210
replaced = entry.msgstr[:-len(replaceRule[0])] + replaceRule[1]
211
elif replaceRule[0] in entry.msgstr:
212
replaced = entry.msgstr.replace(replaceRule[0], replaceRule[1])
213
if replaced is not None:
214
# check if it already exists >> then transfer occurrences and remember it
215
print("Replace '%s' by '%s'" % (entry.msgstr, replaced))
216
match = poFile.find(replaced, msgctxt=entry.msgctxt)
217
if markObsolete and match is not None:
218
print("Transfer duplicate entries for msgid '%s' (was '%s')." % (replaced, entry.msgstr))
219
transferOccurrences(entry, match)
220
if options.removeObsolete:
221
print("Remove obsolete entry '%s' completely." % entry.msgstr)
222
toRemove.append(entry)
223
else:
224
entry.msgstr = replaced
225
for entry in toRemove:
226
poFile.remove(entry)
227
poFile.save(fpath=poFilePath)
228
229
230
def patchPoFile(filePath, replaceIDs, fuzzy=False):
231
po = polib.pofile(filePath)
232
for entry in replaceIDs:
233
search = entry.msgid
234
replace = entry.msgstr
235
searchEntry = po.find(search) # add context filter here
236
replaceEntry = po.find(replace)
237
if searchEntry is not None:
238
if replaceEntry is not None:
239
if len(replaceEntry.msgstr) == 0 and len(searchEntry.msgstr) > 0:
240
replaceEntry.msgstr = searchEntry.msgstr
241
transferOccurrences(searchEntry, replaceEntry)
242
else:
243
searchEntry.msgid = replace
244
if fuzzy:
245
searchEntry.fuzzy = True
246
po.save(fpath=filePath)
247
248
249
if __name__ == "__main__":
250
main()
251
252