Localization

From Dabo Wiki
Jump to: navigation, search

Localization is the ability to customize an app for the local language without having to change the source code. In the Dabo framework, we've strived to wrap all strings that can be displayed to users in a localization function, which by convention in the Python world at least is named simply with the underscore character: _().

Here is a post from the dabo-dev list that helps explain our approach:


> Since you both appear to be working - could someone explain the
> following code:
raise dException.NoRecordsException, _("No rows to sort.")
> The part I don't understand is the underscore?

In the Python world, _() is the standard name for the internationalization function: it takes a string and, depending on the settings, returns the proper localized translation of the original. In Dabo, it is nothing more than a placeholder for now; if we get some international developers involved in the future, they can write translations and hook them into the dLocalize program, allowing for localized apps without having to mess with the source code.

Look at the top of any file where you see the underscore function; it will have a line like:

from dabo.dLocalize import _

New attempt...

We are writing a small dabo app, that needs a German and English locale (German is default) and is packaged on OSX with py2app (perhaps on Windows using py2exe later).

We use Babel and POedit for translation.

The project tree looks like (fat names are directories):

setup.py (see below) ez_setup.py (from distutils) translate.sh (see below) makeit.sh (see below) build (py2app's build tree) dist (py2app's app target) src

   main.py
   biz (our "functional" code, not shown)
   gui (our GUI code, not shown)
   icons (our application icon)
       appIcon.icns
       appIcon.png
   locale
       de
           LC_MESSAGES
           myapp.po
           myapp.mo
       en
           LC_MESSAGES
           myapp.po
           myapp.mo
       myapp.pot

The main script contains:

import os
import dabo

# i18n/i10n stuff
# better get the default locale setting from the OS! (doesn't work for me, will add later)
dabo.settings.defaultLanguage = 'de'
dabo.settings.firstDayOfWeek = 'Monday'

from dabo.dLocalize import _
dabo.dLocalize.install('myapp', os.path.join(os.path.dirname(__file__), 'locale'), True)
dabo.dLocalize.setLanguage('de', 'utf-8')

Beware: order of the above commands matters! You'll still get some wrong settings (e.g. date format), while http://trac.dabodev.com/ticket/1298 is unresolved.

Of course all output strings are i18n-prepared like

print _(u'Foo bar')

The setup script looks like:

import ez_setup
ez_setup.use_setuptools()


import os, sys, glob
import dabo, dabo.icons
from setuptools import setup


# locales:
daboDir = os.path.dirname(dabo.__file__)
localeDir = os.path.join(daboDir, 'locale')
locales = [
        ('lib/python2.6/dabo.locale/de/LC_MESSAGES', (os.path.join(localeDir, 'de', 'LC_MESSAGES', 'dabo.mo'),)),
        ('lib/python2.6/dabo.locale/en/LC_MESSAGES', (os.path.join(localeDir, 'en', 'LC_MESSAGES', 'dabo.mo'),)),
        ('locale/de/LC_MESSAGES', ('src/locale/de/LC_MESSAGES/myapp.mo',)),
        ('locale/en/LC_MESSAGES', ('src/locale/en/LC_MESSAGES/myapp.mo',)),
        ]


# this would include all of dabo's locales:
"""
#locales = [("dabo.locale", (os.path.join(daboDir, "locale", "dabo.pot"),))]
locales = []
def getLocales(arg, dirname, fnames):
    if ".svn" not in dirname and dirname[-1] != "\\":
        mo_files = tuple(glob.glob(os.path.join(dirname, "*.mo")))
        if mo_files:
            subdir = os.path.join("dabo.locale", dirname[len(arg)+1:])
            locales.append((subdir, mo_files))
os.path.walk(localeDir, getLocales, localeDir)
"""


icons = [
         #('lib/python2.6/dabo.icons', tuple(glob.glob(os.path.join(daboDir, 'icons', 'dabo*.png')))) # if you need dabo's icons
         ('icons', tuple(glob.glob('src/icons/appIcon.*')))
         ]


mainscript = 'src/main.py'
NAME = 'My App'
VERSION = '0.1.0'
APP = [mainscript]
DATA_FILES = locales + icons
APP_OPTIONS = {
               'argv_emulation': True,
               'includes' : ['wx', 'wx.gizmos', 'wx.lib.calendar', 'dabo', 'Growl', 'ftputil'], #, 'dabo.icons',
               'excludes' : ['kinterbasdb', 'psycopg2', 'MySQLdb', 'numpy', 'wx.tools.Editra', 'wx.tools.XRCed'], # in this case, we don't need database support
               'iconfile' : 'src/icons/appIcon.icns',
}
# included packages must not be zipped eggs, use easy_install -Z foo or pip unzip foo


EXE_OPTIONS = {
}


plist = dict(
                          CFBundleName                  = NAME,
                          CFBundleShortVersionString    = VERSION,
                          CFBundleGetInfoString                = '%s %s' % (NAME, VERSION),
                          CFBundleExecutable                    = NAME,
                          CFBundleIdentifier                    = 'com.example.myapp',
                         # CFBundleDocumentTypes        = [
                         #                                 {'CFBundleTypeExtensions': ['jpg', 'tif', 'bmp'],
                        #                                  'CFBundleTypeName':'Image File',
                        #                                   'CFBundleTypeRole':'Viewer'}
                        #                                      ]
          


# BEWARE Windows py2exe code is untested!
manifest = """
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
manifestVersion="1.0">
<assemblyIdentity
    version="0.64.1.0"
    processorArchitecture="x86"
    name="Controls"
    type="win32"
/>
<description>edNet Exporter</description>
<dependency>
    <dependentAssembly>
        <assemblyIdentity
            type="win32"
            name="Microsoft.Windows.Common-Controls"
            version="6.0.0.0"
            processorArchitecture="X86"
            publicKeyToken="6595b64144ccf1df"
            language="*"
        />
    </dependentAssembly>
</dependency>
</assembly>
"""


if sys.platform == 'darwin':
    APP_OPTIONS['plist'] = plist
    extra_options = dict(
        # Cross-platform applications generally expect sys.argv to
        # be used for opening files.
        setup_requires=['py2app'],
        app=APP,
        options={'py2app': APP_OPTIONS},
   
elif sys.platform == 'win32':
    """
    windows = [
        {
            "script": mainscript,
            "icon_resources": [(1, "appIcon.png")],
            "other_resources": [(24,1,manifest)]
        }
    ],
      data_files=["src/icons/appIcon.png"]
    """
    extra_options = dict(
         script = mainscript,
         setup_requires=['py2exe'],
         app=APP,
     )
else:
     extra_options = dict(
         # Normally unix-like platforms will use "setup.py install"
         # and install the main script as such
         scripts=APP,
     )


setup(
    name=NAME,
    data_files=DATA_FILES,
    **extra_options
)

translate.sh controls pybabel

#!/bin/sh
# Extract strings from Python files (make .pot) and update translation source (.po)
# use POedit to edit .po files and to compile translations to .mo!
LD='./src/locale'
DOM=myapp


if [ ! -d "$LD" ]
then
        mkdir $LD
fi
for L
do
        if [ ! -d "$LD/$L" ]
        then
                mkdir $LD/$L
        fi
        if [ ! -d "$LD/$L/LC_MESSAGES" ]
        then
                mkdir $LD/$L/LC_MESSAGES
        fi
done


echo "Extracting messages..."
pybabel extract -o $LD/$DOM.pot src

for LOC in de en
do
        if [ ! -f $LD/$LOC/LC_MESSAGES/$DOM.po ]
        then
                echo "Making new locale $LOC"
                pybabel init -D $DOM -i $LD/$DOM.pot -l $LOC -d $LD
        else
                echo "Updating locale $LOC"
                pybabel update -D $DOM -i $LD/$DOM.pot -l $LOC -d $LD
        fi
        #echo "Compiling locale $LOC"
        #pybabel compile -D $DOM -i $LD/$LOC/LC_MESSAGES/$DOM.po -l $LOC -d $LD
done

After that open the .po files with POedit (saving creates .mo files). If you don't like POedit, edit the .po files in your favourite editor and run pybabel compile (commented in the above script).

makeit.sh cleans the previous build files and creates a new application in "dist"

#!/bin/bash
rm -R dist
rm -R build
mkdir build
echo "Making My App"
python setup.py py2app -O2 -b build/myapp > build/build.log