Last active
October 13, 2015 03:08
-
-
Save exalted/4130087 to your computer and use it in GitHub Desktop.
Update by merging .strings file(s) of an Xcode project. Probably similar to, but much better — obviously: * https://github.com/ndfred/xcode-tools/blob/master/update_strings.py * https://github.com/dulaccc/pygenstrings
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: UTF-8 -*- | |
"""Update by merging .strings file(s) of an Xcode project.""" | |
import os | |
import sys | |
import shlex | |
import shutil | |
import tempfile | |
import re | |
from string import Template | |
from subprocess import Popen, PIPE | |
__author__ = "Ali Servet Donmez" | |
__email__ = "[email protected]" | |
__version__ = "0.9.1" | |
# ============================================================================== | |
# SETTINGS | |
# ============================================================================== | |
GENSTRING_SEARCH_PATHS = [ | |
] | |
"""Recursive search paths where Objective-C source file(s) will be searched.""" | |
BASE_LANG = 'en' | |
"""Base localization language which will be overridden at all times.""" | |
OTHER_LANGS = [ | |
] | |
"""List of additional localization languages which will be updated.""" | |
BASE_RESOURCES = '' | |
"""Base resources directory which will be overridden at all times.""" | |
OTHER_RESOURCES = [ | |
] | |
"""List of additional resources that will be updated.""" | |
# ============================================================================== | |
# DO NOT TOUCH BELOW HERE | |
# ============================================================================== | |
class LocalizedString(): | |
def __init__(self, key, value, comment=None): | |
self.key = key | |
self.value = value | |
self.comment = comment | |
self.todoc = False | |
def __str__(self): | |
return Template('/* $todoc$comment */\n"$key" = "$value";').substitute( | |
key=self.key, | |
value=self.value, | |
comment=self.comment or "No comment provided by engineer.", | |
todoc='TODOC ' if self.todoc else '', | |
) | |
def __lt__(self, other): | |
return self.key.lower() < other.key.lower() | |
def check_and_setup_settings(): | |
global OTHER_LANGS | |
# Remove duplicate entries | |
OTHER_LANGS = list(set(OTHER_LANGS)) | |
if BASE_LANG in OTHER_LANGS: | |
sys.stderr.write("OTHER_LANGS must not include base language: %s.\n" % BASE_LANG) | |
return False | |
return True | |
def check_xcode_setup(): | |
for res in [BASE_RESOURCES] + OTHER_RESOURCES: | |
for lang in [BASE_LANG] + OTHER_LANGS: | |
lang_dirname = os.path.join(res, '%s.lproj' % lang) | |
if not os.path.isdir(lang_dirname): | |
sys.stderr.write('Missing directory: %s.\n' % lang_dirname) | |
return False | |
return True | |
def genstrings(output_path): | |
"""Recursively search current working directory for Objective-C source code | |
file(s) and return output generated by internal genstrings utility for lines | |
containing text of the form NSLocalizedString("key", comment) or | |
CFCopyLocalizedString("key", comment). | |
""" | |
find_cmd = r"find -E %s -iregex '.*\.(h|m|mm)' -print0" % ' '.join(GENSTRING_SEARCH_PATHS) | |
genstrings_cmd = 'xargs -0 genstrings -o "%s"' % output_path | |
try: | |
p1 = Popen(shlex.split(find_cmd), stdout=PIPE) | |
p2 = Popen(shlex.split(genstrings_cmd), stdin=p1.stdout, stdout=PIPE) | |
p1.stdout.close() # Allow p1 to receive a SIGPIPE if p2 exits. | |
p2.communicate() | |
except OSError: | |
sys.stderr.write("Error (e.g., trying to execute a non-existent file).\n") | |
sys.exit() | |
except ValueError: | |
sys.stderr.write("Invalid arguments.\n") | |
sys.exit() | |
with open(os.path.join(output_path, 'Localizable.strings'), 'r') as f: | |
return f.read().decode('utf16').strip() | |
def parse_strings_file(data): | |
"""Parse .strings file and return a list of LocalizedString objects. | |
Keyword arguments: | |
data -- .strings file content | |
""" | |
return [LocalizedString(*parse_localized_string(s)) for s in data.split('\n\n')] | |
re_comment = re.compile(r'^/\* (.*) \*/$') | |
re_l10n = re.compile(r'^"(.+)" = "(.+)";$') | |
def parse_localized_string(text): | |
"""Parse text and return key, value and comment tuple. | |
Keyword arguments: | |
text -- localized strings text, leading and trailing characters will be removed if necessary. | |
""" | |
split = text.strip().split('\n') | |
if len(split) == 2: | |
return re_l10n.match(split[1]).groups() + (re_comment.match(split[0]).groups()[0],) | |
def save_strings(strings, base_path): | |
"""Write Localizable.strings to disk. | |
Keyword arguments: | |
strings -- list of LocalizedString objects. | |
base_path -- where Localizable.strings file will be written. | |
""" | |
write_data = unicode() | |
for s in strings: | |
write_data += "%s\n\n" % unicode(s) | |
with open(os.path.join(base_path, 'Localizable.strings'), 'wb') as f: | |
f.write(write_data.encode('utf16')) | |
def merge_strings(base_strings, other_strings): | |
"""Merge two list of localized strings. | |
Keyword arguments: | |
base_strings -- more up-to-date list of localized strings. | |
other_strings -- previous list of localized strings. | |
""" | |
# Local copy of base_strings since we don't want to change it outside too | |
merged = base_strings[:] | |
for s in merged: | |
s.todoc = True | |
for i, base in enumerate(merged): | |
for other in other_strings: | |
if base.key == other.key: | |
merged[i] = other | |
return merged | |
def main(): | |
filename = os.path.split(__file__)[1] | |
# Check if script is configured okay | |
if not check_and_setup_settings(): | |
sys.stderr.write("%s: configuration error.\n" % filename) | |
return os.EX_CONFIG | |
# Check if Xcode project is setup ready for localizations to take place | |
if not check_xcode_setup(): | |
sys.stderr.write("%s: project is not setup correctly.\n" % filename) | |
return os.EX_CONFIG | |
# All checks went well, here comes the good part... | |
# Generate latest string table from source code | |
dtemp_path = tempfile.mkdtemp() | |
latest_strings = parse_strings_file(genstrings(dtemp_path)) | |
shutil.rmtree(dtemp_path) | |
# Save base .strings file as it is by overwriting the old one | |
save_strings(latest_strings, os.path.join(BASE_RESOURCES, '%s.lproj' % BASE_LANG)) | |
# For any other languages do the merge-magic and write to disk | |
for lang in OTHER_LANGS: | |
merged = None | |
try: | |
read_data = None | |
with open(os.path.join(BASE_RESOURCES, '%s.lproj' % lang, 'Localizable.strings'), 'r') as f: | |
read_data = f.read().decode('utf16') | |
merged = merge_strings(latest_strings, parse_strings_file(read_data.strip())) | |
except IOError: | |
merged = latest_strings | |
save_strings(sorted(merged), os.path.join(BASE_RESOURCES, '%s.lproj' % lang)) | |
# For all other languages for all other resources do the merge-magic and write to disk | |
# FIXME This code block is almost identical to one above, wrap these two | |
for res in OTHER_RESOURCES: | |
for lang in [BASE_LANG] + OTHER_LANGS: | |
read_data = None | |
with open(os.path.join(res, '%s.lproj' % lang, 'Localizable.strings'), 'r') as f: | |
read_data = f.read().decode('utf16') | |
merged = merge_strings(latest_strings, parse_strings_file(read_data.strip())) | |
save_strings(sorted(merged), os.path.join(res, '%s.lproj' % lang)) | |
return os.EX_OK | |
if __name__ == '__main__': | |
status = main() | |
sys.exit(status) | |
# End of file |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment