Skip to content

Commit

Permalink
Extend localisation support to plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
Marginal committed May 12, 2018
1 parent fa76aa6 commit a442d2c
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 27 deletions.
2 changes: 1 addition & 1 deletion EDMarketConnector.py
Expand Up @@ -42,7 +42,7 @@
signal.signal(signal.SIGTERM, lambda sig, frame: pdb.Pdb().set_trace(frame))

from l10n import Translations
Translations().install(config.get('language') or None)
Translations.install(config.get('language') or None)

import companion
import commodity
Expand Down
24 changes: 24 additions & 0 deletions PLUGINS.md
Expand Up @@ -160,12 +160,36 @@ You can display an error in EDMC's status area by returning a string from your `

The status area is shared between EDMC itself and all other plugins, so your message won't be displayed for very long. Create a dedicated widget if you need to display routine status information.

## Localisation

You can localise your plugin to one of the languages that EDMC itself supports. Add the following boilerplate near the top of each source file that contains strings that needs translating:

```python
import l10n
import functools
_ = functools.partial(l10n.Translations.translate, context=__file__)
```

Wrap each string that needs translating with the `_()` function, e.g.:

```python
this.status["text"] = _('Happy!') # Main window status
```

If you display localized strings in EDMC's main window you should refresh them in your `prefs_changed` function in case the user has changed their preferred language.

Translation files should reside in folder named `L10n` inside your plugin's folder. Files must be in macOS/iOS ".strings" format, encoded as UTF-8. You can generate a starting template file for your translations by invoking `l10n.py` in your plugin's folder. This extracts all the translatable strings from Python files in your plugin's folder and places them in a file named `en.template` in the `L10n` folder. Rename this file as `<language_code>.strings` and edit it.

See EDMC's own [`L10n`](https://github.com/Marginal/EDMarketConnector/tree/master/L10n) folder for the list of supported language codes and for example translation files.


# Python Package Plugins

A _Package Plugin_ is both a standard Python package (i.e. contains an `__init__.py` file) and an EDMC plugin (i.e. contains a `load.py` file providing at minimum a `plugin_start()` function). These plugins are loaded before any non-Package plugins.

Other plugins can access features in a Package Plugin by `import`ing the package by name in the usual way.


# Distributing a Plugin

To package your plugin for distribution simply create a `.zip` archive of your plugin's folder:
Expand Down
6 changes: 3 additions & 3 deletions dashboard.py
@@ -1,8 +1,8 @@
import json
from calendar import timegm
from operator import itemgetter
from os import listdir, stat
from os.path import isdir, isfile, join
from os import listdir
from os.path import isdir, isfile, join, getsize
from sys import platform
import time

Expand Down Expand Up @@ -111,7 +111,7 @@ def poll(self, first_time=False):

def on_modified(self, event):
# watchdog callback - DirModifiedEvent on macOS, FileModifiedEvent on Windows
if event.is_directory or (isfile(event.src_path) and stat(event.src_path).st_size): # Can get on_modified events when the file is emptied
if event.is_directory or (isfile(event.src_path) and getsize(event.src_path)): # Can get on_modified events when the file is emptied
self.process(event.src_path if not event.is_directory else None)

# Can be called either in watchdog thread or, if polling, in main thread.
Expand Down
67 changes: 46 additions & 21 deletions l10n.py
Expand Up @@ -7,17 +7,21 @@
from collections import OrderedDict
import numbers
import os
from os.path import basename, dirname, isfile, join, normpath
from os.path import basename, dirname, exists, isfile, isdir, join, normpath
import re
import sys
from sys import platform
from traceback import print_exc
import __builtin__

import locale
locale.setlocale(locale.LC_ALL, '')

from config import config

# Language name
LANGUAGE_ID = '!Language'
LOCALISATION_DIR = 'L10n'


if platform == 'darwin':
Expand All @@ -40,10 +44,6 @@
GetNumberFormatEx.restype = ctypes.c_int


else: # POSIX
import locale


class Translations:

FALLBACK = 'en' # strings in this code are in English
Expand Down Expand Up @@ -81,13 +81,20 @@ def install(self, lang=None):
if lang not in self.available():
self.install_dummy()
else:
self.translations = self.contents(lang)
self.translations = { None: self.contents(lang) }
for plugin in os.listdir(config.plugin_dir):
plugin_path = join(config.plugin_dir, plugin, LOCALISATION_DIR)
if isdir(plugin_path):
self.translations[plugin] = self.contents(lang, plugin_path)
__builtin__.__dict__['_'] = self.translate

def contents(self, lang):
def contents(self, lang, plugin_path=None):
assert lang in self.available()
translations = {}
with self.file(lang) as h:
h = self.file(lang, plugin_path)
if not h:
return {}
else:
for line in h:
if line.strip():
match = Translations.TRANS_RE.match(line)
Expand All @@ -99,11 +106,18 @@ def contents(self, lang):
translations[LANGUAGE_ID] = unicode(lang) # Replace language name with code if missing
return translations

def translate(self, x):
if __debug__:
if x not in self.translations:
print 'Missing translation: "%s"' % x
return self.translations.get(x) or unicode(x).replace(ur'\"', u'"').replace(u'{CR}', u'\n')
def translate(self, x, context=None):
if context:
context = context[len(config.plugin_dir)+1:].split(os.sep)[0]
if __debug__:
if context not in self.translations:
print 'No translations for "%s"' % context
return self.translations.get(context, {}).get(x) or self.translate(x)
else:
if __debug__:
if x not in self.translations[None]:
print 'Missing translation: "%s"' % x
return self.translations[None].get(x) or unicode(x).replace(ur'\"', u'"').replace(u'{CR}', u'\n')

# Returns list of available language codes
def available(self):
Expand All @@ -129,14 +143,22 @@ def respath(self):
if platform=='darwin':
return normpath(join(dirname(sys.executable.decode(sys.getfilesystemencoding())), os.pardir, 'Resources'))
else:
return join(dirname(sys.executable.decode(sys.getfilesystemencoding())), 'L10n')
return join(dirname(sys.executable.decode(sys.getfilesystemencoding())), LOCALISATION_DIR)
elif __file__:
return join(dirname(__file__), 'L10n')
return join(dirname(__file__), LOCALISATION_DIR)
else:
return 'L10n'
return LOCALISATION_DIR

def file(self, lang):
if getattr(sys, 'frozen', False) and platform=='darwin':
def file(self, lang, plugin_path=None):
if plugin_path:
f = join(plugin_path, '%s.strings' % lang)
if exists(f):
try:
return codecs.open(f, 'r', 'utf-8')
except:
print_exc()
return None
elif getattr(sys, 'frozen', False) and platform=='darwin':
return codecs.open(join(self.respath(), '%s.lproj' % lang, 'Localizable.strings'), 'r', 'utf-16')
else:
return codecs.open(join(self.respath(), '%s.strings' % lang), 'r', 'utf-8')
Expand Down Expand Up @@ -218,8 +240,9 @@ def wszarray_to_list(array):
lang = locale.getlocale()[0]
return lang and [lang.replace('_','-')]

# singleton
# singletons
Locale = Locale()
Translations = Translations()


# generate template strings file - like xgettext
Expand All @@ -229,7 +252,7 @@ def wszarray_to_list(array):
regexp = re.compile(r'''_\([ur]?(['"])(((?<!\\)\\\1|.)+?)\1\)[^#]*(#.+)?''') # match a single line python literal
seen = {}
for f in (sorted([x for x in os.listdir('.') if x.endswith('.py')]) +
sorted([join('plugins', x) for x in os.listdir('plugins') if x.endswith('.py')])):
sorted([join('plugins', x) for x in isdir('plugins') and os.listdir('plugins') or [] if x.endswith('.py')])):
with codecs.open(f, 'r', 'utf-8') as h:
lineno = 0
for line in h:
Expand All @@ -238,7 +261,9 @@ def wszarray_to_list(array):
if match and not seen.get(match.group(2)): # only record first commented instance of a string
seen[match.group(2)] = (match.group(4) and (match.group(4)[1:].strip()) + '. ' or '') + '[%s]' % basename(f)
if seen:
template = codecs.open('L10n/en.template', 'w', 'utf-8')
if not isdir(LOCALISATION_DIR):
os.mkdir(LOCALISATION_DIR)
template = codecs.open(join(LOCALISATION_DIR, 'en.template'), 'w', 'utf-8')
template.write('/* Language name */\n"%s" = "%s";\n\n' % (LANGUAGE_ID, 'English'))
for thing in sorted(seen, key=unicode.lower):
if seen[thing]:
Expand Down
4 changes: 2 additions & 2 deletions prefs.py
Expand Up @@ -265,7 +265,7 @@ def __init__(self, parent, callback):
notebook.add(configframe, text=_('Configuration')) # Tab heading in settings


self.languages = Translations().available_names()
self.languages = Translations.available_names()
self.lang = tk.StringVar(value = self.languages.get(config.get('language'), _('Default'))) # Appearance theme and language setting
self.always_ontop = tk.BooleanVar(value = config.getint('always_ontop'))
self.theme = tk.IntVar(value = config.getint('theme'))
Expand Down Expand Up @@ -590,7 +590,7 @@ def apply(self):

lang_codes = { v: k for k, v in self.languages.iteritems() } # Codes by name
config.set('language', lang_codes.get(self.lang.get()) or '')
Translations().install(config.get('language') or None)
Translations.install(config.get('language') or None)

config.set('always_ontop', self.always_ontop.get())
config.set('theme', self.theme.get())
Expand Down

0 comments on commit a442d2c

Please sign in to comment.