mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-12 23:37:14 +03:00
Extend localisation support to plugins
This commit is contained in:
parent
fa76aa61e9
commit
a442d2cd14
@ -42,7 +42,7 @@ if __debug__:
|
|||||||
signal.signal(signal.SIGTERM, lambda sig, frame: pdb.Pdb().set_trace(frame))
|
signal.signal(signal.SIGTERM, lambda sig, frame: pdb.Pdb().set_trace(frame))
|
||||||
|
|
||||||
from l10n import Translations
|
from l10n import Translations
|
||||||
Translations().install(config.get('language') or None)
|
Translations.install(config.get('language') or None)
|
||||||
|
|
||||||
import companion
|
import companion
|
||||||
import commodity
|
import commodity
|
||||||
|
24
PLUGINS.md
24
PLUGINS.md
@ -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.
|
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
|
# 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.
|
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.
|
Other plugins can access features in a Package Plugin by `import`ing the package by name in the usual way.
|
||||||
|
|
||||||
|
|
||||||
# Distributing a Plugin
|
# Distributing a Plugin
|
||||||
|
|
||||||
To package your plugin for distribution simply create a `.zip` archive of your plugin's folder:
|
To package your plugin for distribution simply create a `.zip` archive of your plugin's folder:
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
from calendar import timegm
|
from calendar import timegm
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from os import listdir, stat
|
from os import listdir
|
||||||
from os.path import isdir, isfile, join
|
from os.path import isdir, isfile, join, getsize
|
||||||
from sys import platform
|
from sys import platform
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@ -111,7 +111,7 @@ class Dashboard(FileSystemEventHandler):
|
|||||||
|
|
||||||
def on_modified(self, event):
|
def on_modified(self, event):
|
||||||
# watchdog callback - DirModifiedEvent on macOS, FileModifiedEvent on Windows
|
# 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)
|
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.
|
# Can be called either in watchdog thread or, if polling, in main thread.
|
||||||
|
67
l10n.py
67
l10n.py
@ -7,17 +7,21 @@ import codecs
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
import numbers
|
import numbers
|
||||||
import os
|
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 re
|
||||||
import sys
|
import sys
|
||||||
from sys import platform
|
from sys import platform
|
||||||
|
from traceback import print_exc
|
||||||
import __builtin__
|
import __builtin__
|
||||||
|
|
||||||
import locale
|
import locale
|
||||||
locale.setlocale(locale.LC_ALL, '')
|
locale.setlocale(locale.LC_ALL, '')
|
||||||
|
|
||||||
|
from config import config
|
||||||
|
|
||||||
# Language name
|
# Language name
|
||||||
LANGUAGE_ID = '!Language'
|
LANGUAGE_ID = '!Language'
|
||||||
|
LOCALISATION_DIR = 'L10n'
|
||||||
|
|
||||||
|
|
||||||
if platform == 'darwin':
|
if platform == 'darwin':
|
||||||
@ -40,10 +44,6 @@ elif platform == 'win32':
|
|||||||
GetNumberFormatEx.restype = ctypes.c_int
|
GetNumberFormatEx.restype = ctypes.c_int
|
||||||
|
|
||||||
|
|
||||||
else: # POSIX
|
|
||||||
import locale
|
|
||||||
|
|
||||||
|
|
||||||
class Translations:
|
class Translations:
|
||||||
|
|
||||||
FALLBACK = 'en' # strings in this code are in English
|
FALLBACK = 'en' # strings in this code are in English
|
||||||
@ -81,13 +81,20 @@ class Translations:
|
|||||||
if lang not in self.available():
|
if lang not in self.available():
|
||||||
self.install_dummy()
|
self.install_dummy()
|
||||||
else:
|
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
|
__builtin__.__dict__['_'] = self.translate
|
||||||
|
|
||||||
def contents(self, lang):
|
def contents(self, lang, plugin_path=None):
|
||||||
assert lang in self.available()
|
assert lang in self.available()
|
||||||
translations = {}
|
translations = {}
|
||||||
with self.file(lang) as h:
|
h = self.file(lang, plugin_path)
|
||||||
|
if not h:
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
for line in h:
|
for line in h:
|
||||||
if line.strip():
|
if line.strip():
|
||||||
match = Translations.TRANS_RE.match(line)
|
match = Translations.TRANS_RE.match(line)
|
||||||
@ -99,11 +106,18 @@ class Translations:
|
|||||||
translations[LANGUAGE_ID] = unicode(lang) # Replace language name with code if missing
|
translations[LANGUAGE_ID] = unicode(lang) # Replace language name with code if missing
|
||||||
return translations
|
return translations
|
||||||
|
|
||||||
def translate(self, x):
|
def translate(self, x, context=None):
|
||||||
if __debug__:
|
if context:
|
||||||
if x not in self.translations:
|
context = context[len(config.plugin_dir)+1:].split(os.sep)[0]
|
||||||
print 'Missing translation: "%s"' % x
|
if __debug__:
|
||||||
return self.translations.get(x) or unicode(x).replace(ur'\"', u'"').replace(u'{CR}', u'\n')
|
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
|
# Returns list of available language codes
|
||||||
def available(self):
|
def available(self):
|
||||||
@ -129,14 +143,22 @@ class Translations:
|
|||||||
if platform=='darwin':
|
if platform=='darwin':
|
||||||
return normpath(join(dirname(sys.executable.decode(sys.getfilesystemencoding())), os.pardir, 'Resources'))
|
return normpath(join(dirname(sys.executable.decode(sys.getfilesystemencoding())), os.pardir, 'Resources'))
|
||||||
else:
|
else:
|
||||||
return join(dirname(sys.executable.decode(sys.getfilesystemencoding())), 'L10n')
|
return join(dirname(sys.executable.decode(sys.getfilesystemencoding())), LOCALISATION_DIR)
|
||||||
elif __file__:
|
elif __file__:
|
||||||
return join(dirname(__file__), 'L10n')
|
return join(dirname(__file__), LOCALISATION_DIR)
|
||||||
else:
|
else:
|
||||||
return 'L10n'
|
return LOCALISATION_DIR
|
||||||
|
|
||||||
def file(self, lang):
|
def file(self, lang, plugin_path=None):
|
||||||
if getattr(sys, 'frozen', False) and platform=='darwin':
|
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')
|
return codecs.open(join(self.respath(), '%s.lproj' % lang, 'Localizable.strings'), 'r', 'utf-16')
|
||||||
else:
|
else:
|
||||||
return codecs.open(join(self.respath(), '%s.strings' % lang), 'r', 'utf-8')
|
return codecs.open(join(self.respath(), '%s.strings' % lang), 'r', 'utf-8')
|
||||||
@ -218,8 +240,9 @@ class Locale:
|
|||||||
lang = locale.getlocale()[0]
|
lang = locale.getlocale()[0]
|
||||||
return lang and [lang.replace('_','-')]
|
return lang and [lang.replace('_','-')]
|
||||||
|
|
||||||
# singleton
|
# singletons
|
||||||
Locale = Locale()
|
Locale = Locale()
|
||||||
|
Translations = Translations()
|
||||||
|
|
||||||
|
|
||||||
# generate template strings file - like xgettext
|
# generate template strings file - like xgettext
|
||||||
@ -229,7 +252,7 @@ if __name__ == "__main__":
|
|||||||
regexp = re.compile(r'''_\([ur]?(['"])(((?<!\\)\\\1|.)+?)\1\)[^#]*(#.+)?''') # match a single line python literal
|
regexp = re.compile(r'''_\([ur]?(['"])(((?<!\\)\\\1|.)+?)\1\)[^#]*(#.+)?''') # match a single line python literal
|
||||||
seen = {}
|
seen = {}
|
||||||
for f in (sorted([x for x in os.listdir('.') if x.endswith('.py')]) +
|
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:
|
with codecs.open(f, 'r', 'utf-8') as h:
|
||||||
lineno = 0
|
lineno = 0
|
||||||
for line in h:
|
for line in h:
|
||||||
@ -238,7 +261,9 @@ if __name__ == "__main__":
|
|||||||
if match and not seen.get(match.group(2)): # only record first commented instance of a string
|
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)
|
seen[match.group(2)] = (match.group(4) and (match.group(4)[1:].strip()) + '. ' or '') + '[%s]' % basename(f)
|
||||||
if seen:
|
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'))
|
template.write('/* Language name */\n"%s" = "%s";\n\n' % (LANGUAGE_ID, 'English'))
|
||||||
for thing in sorted(seen, key=unicode.lower):
|
for thing in sorted(seen, key=unicode.lower):
|
||||||
if seen[thing]:
|
if seen[thing]:
|
||||||
|
4
prefs.py
4
prefs.py
@ -265,7 +265,7 @@ class PreferencesDialog(tk.Toplevel):
|
|||||||
notebook.add(configframe, text=_('Configuration')) # Tab heading in settings
|
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.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.always_ontop = tk.BooleanVar(value = config.getint('always_ontop'))
|
||||||
self.theme = tk.IntVar(value = config.getint('theme'))
|
self.theme = tk.IntVar(value = config.getint('theme'))
|
||||||
@ -590,7 +590,7 @@ class PreferencesDialog(tk.Toplevel):
|
|||||||
|
|
||||||
lang_codes = { v: k for k, v in self.languages.iteritems() } # Codes by name
|
lang_codes = { v: k for k, v in self.languages.iteritems() } # Codes by name
|
||||||
config.set('language', lang_codes.get(self.lang.get()) or '')
|
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('always_ontop', self.always_ontop.get())
|
||||||
config.set('theme', self.theme.get())
|
config.set('theme', self.theme.get())
|
||||||
|
Loading…
x
Reference in New Issue
Block a user