Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
A simple script to find missing translations in an Android project
This script finds missing string translations in Android applicaitons.
Author: Kostya Vasilyev. License: Creative Commons Attribution.
The output format is, I believe, more suitable to working with external
translators than the output of Lint from the Android SDK.
usage: [-h] [-i LANG] res [res ...]
positional arguments:
res Resource directory to check
optional arguments:
-h, --help show this help message and exit
-i LANG, --ignore LANG
Ignore a particular language code
Run it like this from your project's directory:
> res
Or do this to check a library at the same time:
> res ../MyLibrary/res
The output is sorted by language code, so it can be easily collected and sent to your translator(s)
all at once. For example:
Checking language 2, de
***** Found 44 missing translations for language de
<!-- res/values/strings_account_list.xml -->
<string name=account_list_menu_uilock_now>Lock now</string>
<!-- res/values/strings_account_options.xml -->
<string name=account_options_folder_sync_type_spam>Sync as spam</string>
<string name=account_options_prefs_preload_inlines_mobile>Embedded images, mobile</string>
<string name=account_options_prefs_preload_inlines_wifi>Embedded images, WiFi</string>
<string name=account_options_prefs_signature_auto>Add signature automatically</string>
Checking language 1, uk
***** Found 47 missing translations for language uk
... all missing translations for Ukrainian are printed here
If you have a language whose translation is out of date, and not going to be updated, you can
exclude it from checking by using the "-i" switch:
> -i zh res
This will ignore directories such as "values-zh-rCN", "values-zh-rHK", just "values-zh", and so on,
making the output cleaner for the languages you care about.
Strings marked with translatable="false" or redirecting with "@string" are ignored.
import sys
import argparse
import os
import re
import xml.parsers.expat
SAX parse event handler
class XmlHandler:
def __init__(self):
self.insideString = False
self.insideStringArray = False
self.tag = None = None = None
self.childCount = 0
self.stringCount = 0
self.stringArrayCount = 0;
self.StringCallback = None
self.StringArrayCallback = None
Start XML element
def start_element(self, name, attrs):
self.insideString = False
trans = attrs.get('translatable')
if trans == 'false':
#print u'String not translatable: {0}'.format(attrs['name']) = None
elif name == u'string': = None
self.tag = u'' + name = u'' + attrs['name']
self.insideString = True
elif name == u'string-array': = None
self.tag = u'' + name = u'' + attrs['name']
self.childCount = 0
self.insideStringArray = True
End XML element
def end_element(self, name):
if name == u'string' and self.insideString:
if and not'@string/'):
self.stringCount += 1
self.insideString = False
elif name == u'item' and self.insideStringArray:
self.childCount += 1
elif name == u'string-array' and self.insideStringArray:
print u'string-array: {0} {1}'.format(, self.childCount)
self.stringArrayCount += 1
self.StringArrayCallback(self.tag,,, self.childCount)
self.insideStringArray = False
Character data
def char_data(self, data):
if += data
else: = u'' + data
Check state for one string.
Keeps track of language bits (one bit for each language, except the default),
the string name, the default value, and orignal resource file where declared
class StringState:
def __init__(self, tag, name, data, resfile, childCount = 0):
self.langBits = 0;
self.tag = tag = name = data
self.resfile = resfile
self.childCount = childCount
class StringList:
def __init__(self, langIgnoreList):
self.stringDict = {}
self.langIgnoreList = langIgnoreList if langIgnoreList else {}
Scans a res directory
def scanResDirectory(self, res):
print 'Scanning:', res
self.scanValuesDirectory(os.path.join(res, 'values'), 0, 'Default')
langBit = 0
langList = {}
for top, dirs, files in os.walk(res):
for nm in sorted(dirs):
if nm != 'values':
m = re.match('^values-(([a-z]{2})(-r[a-zA-Z]{2})?)$', nm)
if m != None:
langFull =
langShort =
langName = langFull
if not langShort in self.langIgnoreList and not langFull in self.langIgnoreList:
if langBit == 0:
langBit = 1
langBit = langBit << 1
langList[langBit] = langName
self.scanValuesDirectory(os.path.join(top, nm), langBit, langName)
while langBit != 0:
langName = langList.get(langBit)
self.findMissingTranslations(langBit, langName)
langBit = langBit >> 1
Scans a res/values{-lang} directory
def scanValuesDirectory(self, values, langBit, langName):
print 'Scanning:', values, 'langBit =', langBit, 'langName =', langName
for top, dirs, files in os.walk(values):
for nm in sorted(files):
if not nm.endswith("non_nls.xml"):
self.scanValuesFile(os.path.join(top, nm), langBit, langName)
Scans a res/values{-lang}/foo.xml resource file
def scanValuesFile(self, resfile, langBit, langName):
print 'File:', resfile,
with open(resfile, 'r') as f:
self.dontNeedTranslationString = []
self.dontNeedTranslationStringArray = []
def stringCallback(tag, name, data):
if langBit is 0:
ss = StringState(tag, name, data, resfile)
self.stringDict[name] = ss
ss = self.stringDict.get(name)
if ss:
ss.langBits = ss.langBits | langBit
def stringArrayCallback(tag, name, data, childCount):
if langBit is 0:
ss = StringState(tag, name, data, resfile, childCount)
self.stringDict[name] = ss
ss = self.stringDict.get(name)
if ss:
if ss.childCount != childCount:
print(u'*** Fatal error: in {0}, string-array "{1}" size mismatch: {2}, {3}' \
.format (resfile, name, ss.childCount, childCount))
ss.langBits = ss.langBits | langBit
h = XmlHandler()
h.StringCallback = stringCallback
h.StringArrayCallback = stringArrayCallback
p = xml.parsers.expat.ParserCreate()
p.StartElementHandler = h.start_element
p.EndElementHandler = h.end_element
p.CharacterDataHandler = h.char_data
print '{0}, {1}'.format(h.stringCount, h.stringArrayCount)
for name in self.dontNeedTranslationString:
print '*** string "', name, '" does not need to be translated'
for name in self.dontNeedTranslationStringArray:
print '*** string-array "', name, '" does not need to be translated'
Looks for missing translations
def findMissingTranslations(self, langBit, langName):
print u'\n***** Checking language {0}, {1}'.format(langBit, langName)
missing = []
for ss in self.stringDict.values():
if (ss.langBits & langBit) == 0:
if missing:
print '\n***** Found', len(missing), 'missing translations for language', langName
resfile = None
for ss in sorted(missing, key = lambda ss: ss.resfile +
if not resfile or resfile != ss.resfile:
print '\n<!--', ss.resfile, '-->\n'
resfile = ss.resfile
print u'<{0} name="{1}">{2}</{0}>'.format(ss.tag,,, ss.tag)
if __name__ == "__main__":
Parse command line arguments
parser = argparse.ArgumentParser(description='Finds missing string translations in Android applicaitons')
parser.add_argument('-i', dest='LANG', action='append', help='ignore a particular language code')
parser.add_argument('res', nargs='+', help='resource directory to check (one or more)')
args = parser.parse_args()
sl = StringList(args.LANG)
for resdir in args.res:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.