Skip to content

Instantly share code, notes, and snippets.

@exalted
Last active October 13, 2015 03:08
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save exalted/4130087 to your computer and use it in GitHub Desktop.
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
#!/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__ = "asd@pittle.org"
__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