-
Notifications
You must be signed in to change notification settings - Fork 14
Localization approach description
Extensions should be available to as much users as possible, without assuming that a user can use English to navigate through an extension or access documentation. This is the reason why localization(l10n) is considered an essential in extension development. Along this section, we are going to document the l10n relevant to several extension elements process.
An important aspect, and the welcoming part of almost every extension (that should include one) is its user interface. In terms of LibreOffice/OpenOffice, this interface is called a dialog and these suites have implemented a dedicated editor for such interfaces, the so called Dialog Editor. Among other l10n approaches, developers seem to follow two:
- Using l10n toolset as described in the wiki. Following this approach requires most elements to be statically included in the
.xdl
dialog description file. - Describing dialog elements giving them specific IDs and then defining details in main script. Following this approach allows for more flexibility mainly because elements are finally generated dynamically and sophisticated l10n libraries can be used.
During this project, we first followed the former approach. However, as the extensions started to scale, an approach with greater scalability and maintainability was essential so the latter was chosen.
The project extensions are developed in Python so the well known localization and iternationalization library called gettext is used. This module is bundled with Python and consists of an API and a toolset to help extract strings and create l10n catalogs. A brief introduction as well as a descriptive tutorial can be found at the PhraseApp blog.
Outlining the l10n process using gettext:
- Mark all those strings to be translated.
- Based on the marked strings, create localization templates that are plain text files filled by the developer (
.pot
files). - Use those templates to create specific translation files.
- Organize those translation files in specific directories per locale.
- Refer to those translations from the main extension code.
Initially, the gettext module for the Python script is included, and the marking function is defined:
import gettext
_ = gettext.gettext
Next, all translatable strings are marked using this marking function:
Doc = XSCRIPTCONTEXT.getDocument()
psm = uno.getComponentContext().ServiceManager
dp = psm.createInstance("com.sun.star.awt.DialogProvider")
dlg = dp.createDialog(
"vnd.sun.star.script:dialogs.PageNumberingDialog?location=application")
# Initialize the required fields
oDialog1Model = dlg.Model
oDialog1Model.Title = _("Page Numbering Title")
Previously, we mentioned that most of dialog control elements will be dynamically set during extension execution. The previous code segment represents that choice, by setting the dialog title during execution. More dialog control properties that are of string
type are defined in such way:
#Cancel and OK button Labels
CancelButton = oDialog1Model.getByName("CancelButton")
CancelButton.Label = _("Cancel")
OKButton = oDialog1Model.getByName("OKButton")
OKButton.Label = _("OK")
PositionLabel = oDialog1Model.getByName("PositionLabel")
PositionLabel.Label = _("Position")
PositionListBox = oDialog1Model.getByName("Position")
PositionListBox.StringItemList = [_("Header"), _("Footer")]
AlignmentLabel = oDialog1Model.getByName("AlignmentLabel")
AlignmentLabel.Label = _("Alignment")
AlignmentListBox = oDialog1Model.getByName("Alignment")
AlignmentListBox.StringItemList = [_("Left"), _("RightS"), _("Centered")]
#...#
FirstPageLabel = oDialog1Model.getByName("FirstPageLabel")
FirstPageLabel.Label = _("First Page")
#...#
PageOffsetLabel = oDialog1Model.getByName("PageOffsetLabel")
PageOffsetLabel.Label = _("Page Offset")
#...#
TypeLabel = oDialog1Model.getByName("TypeLabel")
TypeLabel.Label = _("Numbering Type")
#...#
DecorLabel = oDialog1Model.getByName("DecorLabel")
DecorLabel.Label = _("Decor")
#...#
In order to create template l10n files for the previous marked strings, the pygettext
tool is used:
pygettext.py -d base -o locales/base.pot Python/main_Python_script.py
This will create one template in the base domain for the main Python script. Strings inside _()
functions are used as id's for replaceable text. More info about domains can be found in GNU gawk manual.
The result template has the following format:
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-06-22 19:53+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
#: main.py:85
msgid "Page Numbering Title"
msgstr ""
#: main.py:89
msgid "Cancel"
msgstr ""
#: main.py:92
msgid "OK"
msgstr ""
#: main.py:95
msgid "Position"
msgstr ""
#: main.py:97
msgid "Footer"
msgstr ""
#: main.py:97
msgid "Header"
msgstr ""
#... Rest of translateable strings go here
More about the meta-data located at the start of template files can be found in gnu.org wiki
After acquiring the template, the l10n developer must create a specific directory structure that will host specific translations. We created two translations, one for Greek (el) and one for English (en)
├── locales
│ ├── el
│ │ └── LC_MESSAGES
│ │ └── base.po
│ ├── en
│ │ └── LC_MESSAGES
│ │ └── base.po
│ └── base.pot
└── main.py
Each of the LC_MESSAGES
includes a file named base.po
. This file is just a copy of the base.pot
template after filling each one of the translation strings (msgstr
field) and saving with .po
extension.
Although .po
files are human readable, they cannot be directly used by gettext. The msgfmt
tool is used to create gettext compatible catalogs (.mo
files):
cd locales/en/LC_MESSAGES
msgfmt.py -o base.mo base
After executing the previous commands for each locale the catalogs are created:
├── locales
│ ├── el
│ │ └── LC_MESSAGES
│ │ ├── base.mo
│ │ └── base.po
│ ├── en
│ │ └── LC_MESSAGES
│ │ ├── base.mo
│ │ └── base.po
│ └── base.pot
└── main.py
Although we have created the required catalogs, we still have to select the right one depending on the running locale. Generally, selecting a locale in Python requires the following:
import gettext
locale = gettext.translation('base', localedir='locales', languages=['locale_string']) #locale_string = en, el, ...
locale.install()
_ = locale.gettext # Greek
To implement full l10n features, we have to decide which is the right locale to use. This is done by getting the right property info from the LO PropertySet:
def getLanguage():
oProvider = "com.sun.star.configuration.ConfigurationProvider"
oAccess = "com.sun.star.configuration.ConfigurationAccess"
oConfigProvider = get_instance(oProvider)
oProp = PropertyValue()
oProp.Name = "nodepath"
oProp.Value = "org.openoffice.Office.Linguistic/General"
properties = (oProp,)
key = "UILocale"
oSet = oConfigProvider.createInstanceWithArguments(oAccess, properties)
if oSet and (oSet.hasByName(key)):
ooLang = oSet.getPropertyValue(key)
if not (ooLang and not ooLang.isspace()):
oProp.Value = "/org.openoffice.Setup/L10N"
properties = (oProp,)
key = "ooLocale"
oSet = oConfigProvider.createInstanceWithArguments(oAccess, properties)
if oSet and (oSet.hasByName(key)):
ooLang = oSet.getPropertyValue(key)
return ooLang
def get_instance(service_name):
""" gets a service from Uno """
sm = uno.getComponentContext()
ctx = sm.getServiceManager()
try:
service = ctx.createInstance(service_name)
except:
service = NONE
return service
getLanguage()
returns the string value of the currently used locale in LibreOffice. Then, setting the correct locale and catalog is based on this string value:
from urllib.parse import urlparse
def get_main_directory(module_name): #com.addon.pagenumbering
ctx = uno.getComponentContext()
srv = ctx.getByName("/singletons/com.sun.star.deployment.PackageInformationProvider")
return urlparse(srv.getPackageLocation(module_name)).path + "/"
def main(*args):
try:
ui_locale = gettext.translation('base', localedir=get_main_directory("com.addon.pagenumbering")+'Python/locales', languages=[getLanguage()])
except Exception as e:
ui_locale = gettext.translation('base', localedir=get_main_directory("com.addon.pagenumbering")+'Python/locales', languages=["en"])
ui_locale.install()
_ = ui_locale.gettext
Although dialog controls include the majority of translatable strings, the previous methodology applies to other elements:
def main(*args):
try:
ui_locale = gettext.translation('base', localedir=get_main_directory("com.addon.pagenumbering")+'Python/locales', languages=[getLanguage()])
except Exception as e:
ui_locale = gettext.translation('base', localedir=get_main_directory("com.addon.pagenumbering")+'Python/locales', languages=["en"])
ui_locale.install()
_ = ui_locale.gettext
# get the doc from the scripting context which is made available to all scripts
Doc = XSCRIPTCONTEXT.getDocument()
UndoManager = Doc.getUndoManager()
UndoManager.enterUndoContext(_("Page Numbering")) #There should be included all those changing operations that should be put in undo stack
# Operations for undomanager go here
UndoManager.leaveUndoContext()
Generating help pages is described extensively in Extension Compiler during extension packaging. LibreOffice/OpenOffice handles the display of the correct locale help pages provided that these pages are included in the Extension Compiler .odt
file.
The steps required to create a new localization catalog for librecust extensions is outlined in the following steps:
-
Choose extension to l10n.
-
Generate most recent
.pot
template:pygettext.py -d base -o locales/base.pot Python/main_Python_script.py
-
Create new folder for the locale in the directory tree:
mkdir locales/new_locale/LC_MESSAGES
├── locales │ ├── el │ │ └── LC_MESSAGES │ │ ├── base.mo │ │ └── base.po │ ├── en │ │ └── LC_MESSAGES │ │ ├── base.mo │ │ └── base.po │ ├── new_locale │ │ └── LC_MESSAGES │ └── base.pot └── main.py
-
Fill template and create
.po
file: Based on the.pot
template, themsgstr
fields are filled and the.po
file is saved in thelocales/new_locale/LC_MESSAGES
-
Generate locale catalog file:
cd locales/new_locale/LC_MESSAGES msgfmt.py -o base.mo base