Skip to content

Instantly share code, notes, and snippets.

@yoichitgy
Last active July 9, 2022 23:59
Show Gist options
  • Star 43 You must be signed in to star a gist
  • Fork 19 You must be signed in to fork a gist
  • Save yoichitgy/29bdd71c3556c2055cc0 to your computer and use it in GitHub Desktop.
Save yoichitgy/29bdd71c3556c2055cc0 to your computer and use it in GitHub Desktop.
A script to generate .strings file for .swift, .m, .storyboard and .xib files by genstrings and ibtool commands, and merge them with existing translations.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Localize.py - Incremental localization on XCode projects
# João Moreno 2009
# http://joaomoreno.com/
# Modified by Steve Streeting 2010 http://www.stevestreeting.com
# Changes
# - Use .strings files encoded as UTF-8
# This is useful because Mercurial and Git treat UTF-16 as binary and can't
# diff/merge them. For use on iPhone you can run an iconv script during build to
# convert back to UTF-16 (Mac OS X will happily use UTF-8 .strings files).
# - Clean up .old and .new files once we're done
# Modified by Yoichi Tagaya 2015 http://github.com/yoichitgy
# Changes
# - Use command line arguments to execute as `mergegenstrings.py path routine`
# path: Path to the directory containing source files and lproj directories.
# routine: Routine argument for genstrings command specified with '-s' option.
# - Support both .swift and .m files.
# - Support .storyboard and .xib files.
from sys import argv
from codecs import open
from re import compile
from copy import copy
import os
re_translation = compile(r'^"(.+)" = "(.+)";$')
re_comment_single = compile(r'^/\*.*\*/$')
re_comment_start = compile(r'^/\*.*$')
re_comment_end = compile(r'^.*\*/$')
class LocalizedString():
def __init__(self, comments, translation):
self.comments, self.translation = comments, translation
self.key, self.value = re_translation.match(self.translation).groups()
def __unicode__(self):
return u'%s%s\n' % (u''.join(self.comments), self.translation)
class LocalizedFile():
def __init__(self, fname=None, auto_read=False):
self.fname = fname
self.strings = []
self.strings_d = {}
if auto_read:
self.read_from_file(fname)
def read_from_file(self, fname=None):
fname = self.fname if fname == None else fname
try:
f = open(fname, encoding='utf_8', mode='r')
except:
print 'File %s does not exist.' % fname
exit(-1)
line = f.readline()
while line:
comments = [line]
if not re_comment_single.match(line):
while line and not re_comment_end.match(line):
line = f.readline()
comments.append(line)
line = f.readline()
if line and re_translation.match(line):
translation = line
else:
raise Exception('invalid file')
line = f.readline()
while line and line == u'\n':
line = f.readline()
string = LocalizedString(comments, translation)
self.strings.append(string)
self.strings_d[string.key] = string
f.close()
def save_to_file(self, fname=None):
fname = self.fname if fname == None else fname
try:
f = open(fname, encoding='utf_8', mode='w')
except:
print 'Couldn\'t open file %s.' % fname
exit(-1)
for string in self.strings:
f.write(string.__unicode__())
f.close()
def merge_with(self, new):
merged = LocalizedFile()
for string in new.strings:
if self.strings_d.has_key(string.key):
new_string = copy(self.strings_d[string.key])
new_string.comments = string.comments
string = new_string
merged.strings.append(string)
merged.strings_d[string.key] = string
return merged
def merge(merged_fname, old_fname, new_fname):
try:
old = LocalizedFile(old_fname, auto_read=True)
new = LocalizedFile(new_fname, auto_read=True)
merged = old.merge_with(new)
merged.save_to_file(merged_fname)
except:
print 'Error: input files have invalid format.'
STRINGS_FILE = 'Localizable.strings'
def localizeCode(path, routine):
print 'Localize source code...'
languages = [lang for lang in [os.path.join(path, name) for name in os.listdir(path)]
if lang.endswith('.lproj') and os.path.isdir(lang)]
for language in languages:
print language
original = merged = os.path.join(language, STRINGS_FILE)
old = original + '.old'
new = original + '.new'
if os.path.isfile(original):
os.rename(original, old)
os.system('genstrings -q -s %s -o "%s" `find %s -name "*.swift" -o -name "*.m"`' % (routine, language, path))
os.system('iconv -f UTF-16 -t UTF-8 "%s" > "%s"' % (original, new))
merge(merged, old, new)
else:
os.system('genstrings -q -s %s -o "%s" `find %s -name "*.swift" -o -name "*.m"`' % (routine, language, path))
os.rename(original, old)
os.system('iconv -f UTF-16 -t UTF-8 "%s" > "%s"' % (old, original))
if os.path.isfile(old):
os.remove(old)
if os.path.isfile(new):
os.remove(new)
def localizeInterface(path, developmentLanguage):
baseDir = os.path.join(path, "Base.lproj")
developmentLanguage = os.path.splitext(developmentLanguage)[0] + ".lproj" # Add the extension if not exists
print developmentLanguage
if os.path.isdir(baseDir):
print 'Localize interface...'
ibFileNames = [name for name in os.listdir(baseDir) if name.endswith('.storyboard') or name.endswith('.xib')]
languages = [lang for lang in [os.path.join(path, name) for name in os.listdir(path)]
if lang.endswith('.lproj') and not lang.endswith('Base.lproj') and os.path.isdir(lang)]
for language in languages:
print language
for ibFileName in ibFileNames:
ibFilePath = os.path.join(baseDir, ibFileName)
stringsFileName = os.path.splitext(ibFileName)[0] + ".strings"
print ' ' + stringsFileName
original = merged = os.path.join(language, stringsFileName)
old = original + '.old'
new = original + '.new'
if os.path.isfile(original) and not language.endswith(developmentLanguage):
os.rename(original, old)
os.system('ibtool --export-strings-file %s %s' % (original, ibFilePath))
os.system('iconv -f UTF-16 -t UTF-8 "%s" > "%s"' % (original, new))
merge(merged, old, new)
else:
os.system('ibtool --export-strings-file %s %s' % (original, ibFilePath))
os.rename(original, old)
os.system('iconv -f UTF-16 -t UTF-8 "%s" > "%s"' % (old, original))
if os.path.isfile(old):
os.remove(old)
if os.path.isfile(new):
os.remove(new)
if __name__ == '__main__':
argc = len(argv)
if (argc <= 1 or 4 < argc):
print 'Usage: %s path_to_source_directory [routine] [development_language]' % argv[0]
quit()
path = os.path.abspath(argv[1])
routine = argv[2] if argc > 2 else 'NSLocalizedString'
developmentLanguage = argv[3] if argc > 3 else 'en'
localizeCode(path, routine)
localizeInterface(path, developmentLanguage)
@yoichitgy
Copy link
Author

@yoichitgy
Copy link
Author

To run the script automatically when you build a project, put the script into the project root directory, and add a Run Script phase with the following line to a target build phases in your project settings.

./mergegenstrings.py PathToSourceDir

if you use a custom routine for genstrings command, specify the routine after the path.

./mergegenstrings.py PathToSourceDir MyLocalizedStringRoutine

if your development language (especially for storyboards and xibs) is not English, specify your language after the routine.

./mergegenstrings.py PathToSourceDir MyLocalizedStringRoutine ja

@yoichitgy
Copy link
Author

The script assumes your project has the default directory layout created by Xcode 6 as shown in 'screenshot-finder.png', which has a project named 'MySample'.

To try the script, place the script to your project root directory as shown in the screenshot image, and run the script in Terminal. If your project name is 'MySample':

./mergegenstrings.py MySample

If the script does not have the right to execute, run the command below to add the right:

chmod +x mergegenstrings.py

Please note that both the project root and source root has the same name 'MySample' by default, and the script argument is the source root directory name. If your source root has a different name, give the name as the script argument.

If you want to run the script automatically when you build your project, add Run Script phase as shown in 'screenshot-runscript.png' image.

@maxykato
Copy link

I followed the instruction, but it did not really work...

I added a button on the base storyboard, built the project with the script running, but the button was not added on neither of English storyboard nor Japanese storyboard.

I see that the character on the right of storyboard changing from M to A, then A to M, so the script seems to be doing something, but not in the way I want....

Would you give me a help?

@rajohns08
Copy link

This script is overwriting my existing translations in Localizable.strings, but seems to work fine for my storyboard strings files. P.s. I'm using Xcode 7.

@yoichitgy
Copy link
Author

@rajohns08, thanks for the report. As far as I checked script with Xcode 7 beta 5 and 6, it works as intended. It might a problem of earlier beta versions of Xcode 7.

@yoichitgy
Copy link
Author

@maxykato, sorry that I didn't notice your question. The following project has the script set up. Hope it helps.

https://github.com/Swinject/SwinjectMVVMExample

@lammertw
Copy link

The script seems to remove all translations for which it cannot find the keys in the code. I have many keys that are generated in the code, eg:

let key = "MenuItem\(index)"

And then in my Localizable.strings:

"MenuItem1" = "Menu item 1";
"MenuItem2" = "Menu item 2";

These items are not recognized and thus removed. Is there anything to solve this? Perhaps a comment like this and then keep everything as is after that comment:

#nongenerated

Or would you suggest placing these kind of localizations in another file?

@klundberg
Copy link

@lammertw, the genstrings tool on its own is unable to recognize string interpolation and generate strings from that, so I presume that this is intended. These tools work on static code, and cannot know what values are possible. I'd recommend being explicit with your string keys, or putting them in a file you generate manually like you suggested. That said, it would be nice if keys that are not found would be preserved so that others in your situation don't hit any unexpected issues.

@muzammil-triffort
Copy link

@maxykato have you find any fixes for you?

@juliofruta
Copy link

For anyone having issues with iconv while converting your files. I recommend modifying the script to use vim instead.
That worked for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment