forked from facebookarchive/three20
-
Notifications
You must be signed in to change notification settings - Fork 0
/
diffstrings.py
executable file
·335 lines (259 loc) · 12.8 KB
/
diffstrings.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
#!/usr/bin/env python
usage = """usage: %prog [options] path1 path2 ...
diffstrings compares your primary locale with all your other locales to help you determine which new strings need to be translated. It can also merge translated strings back into the project.
The path arguments supplied to this script should be the path containing your source files. The file system will be searched to find the .lproj directories containing the Localizable.strings files that the script will read and write.
"""
import os.path, codecs, optparse, re
###################################################################################################
reLprojFileName = re.compile(r'(.+?)\.lproj')
reComment = re.compile(r'/\*(.*?)\*/')
reString = re.compile(r'\s*"((\\.|.)+?)"\s*=\s*"(.+?)";')
reTranslatedString = re.compile(r'\w{2}:\s*"(.*?)"')
defaultComment = "No comment provided by engineer."
stringsFileName = "Localizable.strings"
###################################################################################################
# Diffing
def diffProjectLocales(projectDirPaths, primaryLocaleName):
primaryLocale, otherLocales = openAllLocales(projectDirPaths, primaryLocaleName)
print " * %s has %d total strings." % (primaryLocaleName, len(primaryLocale))
for localeName, locale in otherLocales.iteritems():
writeLocaleDiff(localeName, locale, primaryLocaleName, primaryLocale)
def compareLocales(locale1, locale2, compareStrings=False):
newStrings = {}
existingStrings = {}
for originalString, (translatedString, comment) in locale1.iteritems():
if originalString in locale2:
translatedString, comment2 = locale2[originalString]
if compareStrings and translatedString == originalString:
newStrings[originalString] = ("", comment)
else:
existingStrings[originalString] = (translatedString, comment)
else:
newStrings[originalString] = ("", comment)
return newStrings, existingStrings
def writeLocaleDiff(localeName, locale, projectLocaleName, projectLocale):
""" Writes a <locale>.txt file for a locale."""
newStrings, existingStrings = compareLocales(projectLocale, locale, True)
deletedStrings, ignore = compareLocales(locale, projectLocale)
if not len(newStrings):
if len(deletedStrings):
print " * %s is fully translated, but has %s obsolute strings."\
% (localeName, len(deletedStrings))
else:
print " * %s is fully translated." % localeName
else:
fileName = "%s.txt" % localeName
stringsPath = os.path.abspath(os.path.join(".", fileName))
f = codecs.open(stringsPath, 'w', 'utf-16')
allStrings = dict(newStrings)
allStrings.update(existingStrings)
sortedKeys = allStrings.keys()
sortedKeys.sort(key=unicode.lower)
for originalString in sortedKeys:
translatedString, comment = allStrings[originalString]
if translatedString == originalString:
translatedString = ""
f.write("%s: \"%s\"\n" % (projectLocaleName, originalString))
if comment:
f.write(" (%s)\n" % comment)
f.write("%s: \"%s\"\n\n" % (localeName, translatedString))
f.close()
if len(deletedStrings):
print " * Writing %s.txt: %s new strings, %s already translated, and %s obsolete."\
% (localeName, len(newStrings), len(existingStrings), len(deletedStrings))
else:
print " * Writing %s.txt: %s new strings, with %s already translated."\
% (localeName, len(newStrings), len(existingStrings))
###################################################################################################
## Merging
def mergeProjectLocales(projectDirPaths, primaryLocaleName):
primaryLocale, otherLocales = openAllLocales(projectDirPaths, primaryLocaleName)
translations = openAllTranslations(otherLocales.keys())
mergeTranslations(primaryLocale, otherLocales, translations)
for projectDirPath in projectDirPaths:
writeProjectTranslations(projectDirPath, primaryLocaleName, translations)
def mergeTranslations(primaryLocale, otherLocales, translations):
for localeName, locale in otherLocales.iteritems():
if localeName not in translations:
print "WARNING: No translation exist for %s" % localeName
else:
print " * Merging %s.txt" % localeName
translation = translations[localeName]
for originalString in primaryLocale:
if originalString in translation:
translatedString = translation[originalString]
if originalString in locale:
(localeTranslatedString, comment) = locale[originalString]
else:
(localeTranslatedString, comment) = ("", None)
if localeTranslatedString != translatedString:
locale[originalString] = (translatedString, comment)
def writeProjectTranslations(projectDirPath, primaryLocaleName, translations):
"""Writes translations back to the strings file for each locale in a project."""
primaryStringsPath = pathForLocaleStrings(primaryLocaleName, projectDirPath)
projectPrimaryLocale = parseLocaleFile(primaryStringsPath)
stringsDirPath = os.path.abspath(os.path.join(primaryStringsPath, "..", ".."))
for localeName,translation in translations.iteritems():
lines = []
for originalString in projectPrimaryLocale:
translatedString = translation[originalString]
(ignore, comment) = projectPrimaryLocale[originalString]
if comment:
lines.append("/* %s */" % comment)
if not translatedString:
line = '"%s" = "%s";\n' % (originalString, originalString)
else:
line = '"%s" = "%s";\n' % (originalString, translatedString)
lines.append(line)
localeDirName = "%s.lproj" % localeName
stringsPath = os.path.join(stringsDirPath, localeDirName, stringsFileName)
print " * Writing %s" % stringsPath
f = codecs.open(stringsPath, 'w', 'utf-16')
f.write("\n".join(lines))
f.close()
###################################################################################################
## Opening Locales
def openLocale(localeName, projectDirPath="."):
localeStringsPath = pathForLocaleStrings(localeName, projectDirPath)
return parseLocaleFile(localeStringsPath)
def openAllLocales(projectDirPaths, primaryLocaleName):
primaryLocale = None
allLocales = {}
for projectDirPath in projectDirPaths:
localeDirPath = findLocaleDirPath(primaryLocaleName, projectDirPath)
if not localeDirPath:
print "WARNING: %s does not have a '%s' locale."\
% (projectDirPath, primaryLocaleName)
continue
for localeName, locale in iterLocales(os.path.dirname(localeDirPath)):
if not localeName in allLocales:
allLocales[localeName] = {}
allLocales[localeName].update(locale)
if not primaryLocaleName in allLocales:
primaryLocale = {}
else:
primaryLocale = allLocales[primaryLocaleName]
del allLocales[primaryLocaleName]
return primaryLocale, allLocales
def iterLocales(projectDirPath):
for dirName in os.listdir(projectDirPath):
m = reLprojFileName.match(dirName)
if m:
localeName = m.groups()[0]
yield localeName, openLocale(localeName, projectDirPath)
def parseLocaleFile(stringsPath):
strings = {}
lastComment = None
for line in openWithProperEncoding(stringsPath):
m = reString.search(line)
if m:
originalString = m.groups()[0]
translatedString = m.groups()[2]
strings[originalString] = (translatedString, lastComment)
lastComment = None
else:
m = reComment.search(line)
if m:
comment = m.groups()[0].strip()
if comment != defaultComment:
lastComment = comment
return strings
###################################################################################################
## Opening translations
def openTranslation(localeName):
translationFileName = "%s.txt" % localeName
translationFilePath = os.path.abspath(os.path.join(".", translationFileName))
return parseTranslationsFile(translationFilePath)
def openAllTranslations(localeNames):
translations = {}
for localeName in localeNames:
translations[localeName] = openTranslation(localeName)
return translations
def parseTranslationsFile(stringsPath):
strings = {}
originalString = None
for line in openWithProperEncoding(stringsPath):
m = reTranslatedString.search(line)
if m:
string = m.groups()[0]
if not originalString:
originalString = string
else:
strings[originalString] = string
originalString = None
return strings
###################################################################################################
## File System Helpers
def openWithProperEncoding(path):
if not os.path.isfile(path):
return []
try:
f = codecs.open(path, 'r', 'utf-16')
lines = f.read().splitlines()
f.close()
except UnicodeError,exc:
f = codecs.open(path, 'r', 'utf-8')
lines = f.read().splitlines()
f.close()
return lines
def pathForLocaleStrings(localeName, projectDirPath, fileName=stringsFileName):
localeDirPath = findLocaleDirPath(localeName, projectDirPath)
return os.path.join(localeDirPath, fileName)
def findLocaleDirPath(localeName, projectDirPath):
localeDirName = "%s.lproj" % localeName
localeDirPath = os.path.join(projectDirPath, localeDirName)
if os.path.isdir(localeDirPath):
return localeDirPath
for name in os.listdir(projectDirPath):
path = os.path.join(projectDirPath, name)
if os.path.isdir(path):
localeDirPath = findLocaleDirPath(localeName, path)
if localeDirPath:
return localeDirPath
return None
###################################################################################################
## genstrings
def runGenstrings(projectDirPaths, primaryLocaleName):
cwd = os.getcwd()
for projectDirPath in projectDirPaths:
os.chdir(projectDirPath)
localeDirPath = findLocaleDirPath(primaryLocaleName, projectDirPath)
command = "genstrings *.m -o %s" % localeDirPath
print " ", command
os.system(command)
os.chdir(cwd)
###################################################################################################
## Main
def parseOptions():
parser = optparse.OptionParser(usage)
parser.set_defaults(locale="en", genstrings=False, merge=False, diff=False)
parser.add_option("-l", "--locale", dest="locale", type="str",
help = "The name of your primary locale. The default is 'en'.")
parser.add_option("-g", "--genstrings", dest="genstrings", action="store_true",
help = "Runs 'genstrings *.m' on each project before diffing or merging. WARNING: This will overwrite the Localized.strings file in your primary locale.")
parser.add_option("-d", "--diff", dest="diff", action="store_true",
help="Generates a diff of each locale against the primary locale. Each locale's diff will be stored in a file in the working directory named <locale>.txt.")
parser.add_option("-m", "--merge", dest="merge", action="store_true",
help="Merges strings from the <locale>.txt file in the working directory back into the Localized.strings files in each locale.")
options, arguments = parser.parse_args()
paths = ["."] if not len(arguments) else arguments
if not options.merge:
options.diff = True
return options, paths
def main():
options, projectPaths = parseOptions()
projectPaths = [os.path.abspath(os.path.expanduser(path)) for path in projectPaths]
if options.genstrings:
print "*** Generating strings"
runGenstrings(projectPaths, options.locale)
print ""
if options.merge:
print "*** Merging"
mergeProjectLocales(projectPaths, options.locale)
print ""
if options.diff:
print "*** Diffing"
diffProjectLocales(projectPaths, options.locale)
print ""
if __name__ == "__main__":
main()