Kodi Community Forum
Internationalization script for addons - Printable Version

+- Kodi Community Forum (https://forum.kodi.tv)
+-- Forum: Development (https://forum.kodi.tv/forumdisplay.php?fid=32)
+--- Forum: Add-ons (https://forum.kodi.tv/forumdisplay.php?fid=26)
+--- Thread: Internationalization script for addons (/showthread.php?tid=268081)



Internationalization script for addons - metate - 2016-04-07

i18n (internationalization) in Python is usually easy, thanks to the gettext module.
However, once I started to develop addons for Kodi, I noticed addon i18n is quite hmm.. ugly.
Take a look at the following piece of code taken from the Kodi wiki:
Code:
import xbmcaddon
__settings__ = xbmcaddon.Addon(id='<your addons ID>')
__language__ = __settings__.getLocalizedString
__language__(30204)              # this will return localized string from resources/language/<name_of_language>/strings.xml

Using string ids leads to unreadable code. Furthermore, development is hard because you need to jump between your code and the English/strings.po file every time you want to add a new localized string. I wasn't sure how to make gettext work with xbmc (i.e., keep using xbmcaddon.getLocalizedString), so I wrote a small script instead and it worked perfectly fine for me in the Meta video search addon.

Requirements:
Python, grep and polib are needed on your development machine.
  • to install polib use pip (located in C:\Python27\Scripts\ on windows):
    Code:
    pip install polib
  • grep for windows is available here.

Usage:
  • Save the script in the root folder of your addon as language.py.
  • Make sure the root folder also contains a resources/language/English/strings.po file (can contain just metadata or even be empty)
  • From your addon code use the following import line
    Code:
    from language import get_string as _
  • From your addon code don't use "Some text" or getLocalizedString(some_id) for localized strings, instead use
    Code:
    _("Some text")
  • Run the language.py script with python and it will self generate to reflect your code changes + add missing text entries to the main strings.po file.

The script: (save as language.py)
Code:
#! /usr/bin/python

__strings = {}

if __name__ == "__main__":
    import polib
    po = polib.pofile('resources/language/English/strings.po')
    
    try:
        import re
        import subprocess
        r = subprocess.check_output(["grep", "-hnr", "_([\'\"]", "."])
        strings = re.compile("_\([\"'](.*?)[\"']\)", re.IGNORECASE).findall(r)
        translated = [m.msgid.lower().replace("'", "\\'") for m in po]        
        missing = set([s for s in strings if s.lower() not in translated])
        if missing:
            ids_range = range(30000, 31000)
            ids_reserved = [int(m.msgctxt[1:]) for m in po]
            ids_available = [x for x in ids_range if x not in ids_reserved]
            print "warning: missing translation for", missing
            for text in missing:
                id = ids_available.pop(0)
                entry = polib.POEntry(
                    msgid=text,
                    msgstr=u'',
                    msgctxt="#{0}".format(id)
                )
                po.append(entry)
            po.save('resources/language/English/strings.po')
    except:
        pass
        
    content = []
    with open(__file__, "r") as me:
        content = me.readlines()
        content = content[:content.index("#GENERATED\n")+1]
    
    with open(__file__, 'w') as f:
        f.writelines(content)
        for m in po:
            line = "__strings['{0}'] = {1}\n".format(m.msgid.lower().replace("'", "\\'"), m.msgctxt.replace('#', '').strip())
            f.write(line)
else:
    import xbmc, xbmcaddon
    __language__ = xbmcaddon.Addon().getLocalizedString
    
    def get_string(t):
        id = __strings.get(t.lower())
        if id:
            return __language__(id)
        xbmc.log("missing translation for " + t.lower())
        return t
    #setattr(__builtin__, '_', get_string)

#GENERATED

Other:
  • You can register _() as a builtin and then your imports can be cleaner (just import language). To do that just un-comment the setattr line in the script.
  • If you also use json-rpc in your addon check out another part I extracted from Meta last month: http://forum.kodi.tv/showthread.php?tid=264088



RE: Internationalization script for addons - Quihico - 2017-01-23

Hi Metate,

Excellent stuff: very easy to implement & use.
I liked it so much I tried to extend it's functionality to include system language strings.
The result is far from perfect but it suits my current needs pretty well.
Below is everything but the list of system-language strings.
For the whole file, including language strings, look here

Code:
#! /usr/bin/python

_strings = {}

if __name__ == "__main__":
    import polib
    po = polib.pofile("resources/language/English/strings.po")
    try:
        import re, subprocess
        r = subprocess.check_output(["grep", "-hnr", "_([\'\"]", "."])
        strings = re.compile("_\([\"'](.*?)[\"']\)", re.IGNORECASE).findall(r)
        translated = [m.msgid.lower().replace("'", "\\'") for m in po]
        missing = set([s for s in strings if s.lower() not in translated])
        if missing:
            ids_range = range(30000, 31000)
            ids_reserved = [int(m.msgctxt[1:]) for m in po]
            ids_available = [x for x in ids_range if x not in ids_reserved]
            print "WARNING: missing translation for '%s'" % missing
            for text in missing:
                id = ids_available.pop(0)
                entry = polib.POEntry(msgid=text, msgstr=u'', msgctxt="#{0}".format(id))
                po.append(entry)
            po.save("resources/language/English/strings.po")
    except: pass
    content = []
    with open(__file__, "r") as me:
        content = me.readlines()
        content = content[:content.index("#GENERATED\n")+1]
    with open(__file__, "w") as f:
        f.writelines(content)
        for m in po:
            line = "_strings['{0}'] = {1}\n".format(m.msgid.lower().replace("'", "\\'"), m.msgctxt.replace("#", "").strip())
            f.write(line)
else:
    def get_string(t):
        import xbmc, xbmcaddon
        ADDON = xbmcaddon.Addon()
        ADDON_ID = ADDON.getAddonInfo("id")
        id = _strings.get(t.lower())
        if not id:
            xbmc.log("LANGUAGE: missing translation for '%s'" % t.lower())
            return t
        elif id in range(30000, 31000) and ADDON_ID.startswith("plugin"): return ADDON.getLocalizedString(id)
        elif id in range(31000, 32000) and ADDON_ID.startswith("skin"): return ADDON.getLocalizedString(id)
        elif id in range(32000, 33000) and ADDON_ID.startswith("script"): return ADDON.getLocalizedString(id)
        elif not id in range(30000, 33000): return xbmc.getLocalizedString(id)
    #setattr(__builtin__, "_", get_string)
# SYSTEM LANGUAGE STRINGS: Only the ones that are available AND unchanged in ["Helix", "Isengard", "Jarvis", "Krypton", "Leia"]
"""list of system language strings not included in this example otherwise it would be too big"""
#GENERATED
"""here will be your addon's language strings"""