class Localizer(LanguageManager, Folder): """ The Localizer meta type lets you customize the language negotiation policy. """ meta_type = 'Localizer' implements(ILocalizer) id = 'Localizer' _properties = ( { 'id': 'title', 'type': 'string' }, { 'id': 'accept_methods', 'type': 'tokens' }, { 'id': 'user_defined_languages', 'type': 'lines' }, ) accept_methods = ('accept_path', 'accept_cookie', 'accept_url') security = ClassSecurityInfo() manage_options = \ (Folder.manage_options[0],) \ + LanguageManager.manage_options \ + Folder.manage_options[1:] user_defined_languages = () def __init__(self, title, languages): self.title = title self._languages = languages self._default_language = languages[0] ####################################################################### # API / Private ####################################################################### def _getCopy(self, container): return Localizer.inheritedAttribute('_getCopy')(self, container) def _needs_upgrade(self): return not self.hooked() def _upgrade(self): # Upgrade to 0.9 if not self.hooked(): self.manage_hook(1) ####################################################################### # API / Public ####################################################################### # Get some data security.declarePublic('get_supported_languages') def get_supported_languages(self): """ Get the supported languages, that is the languages that the are being working so the site is or will provide to the public. """ return self._languages security.declarePublic('get_selected_language') def get_selected_language(self): """ """ return lang_negotiator(self._languages) \ or self._default_language # Hooking the traversal machinery # Fix this! a new permission needed? ## security.declareProtected('View management screens', 'manage_hookForm') ## manage_hookForm = LocalDTMLFile('ui/Localizer_hook', globals()) ## security.declareProtected('Manage properties', 'manage_hook') security.declarePrivate('manage_hook') def manage_hook(self, hook=0): """ """ if hook != self.hooked(): if hook: hook = NameCaller(self.id) registerBeforeTraverse(aq_parent(self), hook, self.meta_type) else: unregisterBeforeTraverse(aq_parent(self), self.meta_type) security.declarePublic('hooked') def hooked(self): """ """ if queryBeforeTraverse(aq_parent(self), self.meta_type): return 1 return 0 # New code to control the language policy def accept_cookie(self, accept_language): """Add the language from a cookie.""" lang = self.REQUEST.cookies.get('LOCALIZER_LANGUAGE', None) if lang is not None: accept_language.set(lang, 2.0) def accept_path(self, accept_language): """Add the language from the path.""" stack = self.REQUEST['TraversalRequestNameStack'] if stack and (stack[-1] in self._languages): lang = stack.pop() accept_language.set(lang, 3.0) def accept_url(self, accept_language): """Add the language from the URL.""" lang = self.REQUEST.form.get('LOCALIZER_LANGUAGE') if lang is not None: accept_language.set(lang, 2.0) def __call__(self, container, REQUEST): """Hooks the traversal path.""" try: accept_language = REQUEST['AcceptLanguage'] except KeyError: return for id in self.accept_methods: try: method = getattr(self, id) method(accept_language) except: LOG(self.meta_type, PROBLEM, 'method "%s" raised an exception.' % id, error=True) # Changing the language, useful snippets security.declarePublic('get_languages_map') def get_languages_map(self): """ Return a list of dictionaries, each dictionary has the language id, its title and a boolean value to indicate wether it's the user preferred language, for example: [{'id': 'en', 'title': 'English', 'selected': 1}] Used in changeLanguageForm. """ # For now only LPM instances are considered to be containers of # multilingual data. try: ob = self.getLocalPropertyManager() except AttributeError: ob = self ob_language = ob.get_selected_language() ob_languages = ob.get_available_languages() langs = [] for x in ob_languages: langs.append({ 'id': x, 'title': self.get_language_name(x), 'selected': x == ob_language }) return langs security.declarePublic('changeLanguage') changeLanguageForm = LocalDTMLFile('ui/changeLanguageForm', globals()) def changeLanguage(self, lang, goto=None, expires=None): """Change the user language to `lang`. This method will set a cookie and redirect to `goto` URL. """ request = self.REQUEST response = request.RESPONSE # Changes the cookie (it could be something different) parent = aq_parent(self) path = parent.absolute_url()[len(request['SERVER_URL']):] or '/' if expires is None: response.setCookie('LOCALIZER_LANGUAGE', lang, path=path) else: response.setCookie('LOCALIZER_LANGUAGE', lang, path=path, expires=unquote(expires)) # Comes back if goto is None: goto = request['HTTP_REFERER'] response.redirect(goto) security.declarePublic('translationContext') @contextmanager def translationContext(self, lang): """Context manager to temporarily change the current language. """ class ForcedLanguage: __allow_access_to_unprotected_subobjects__ = 1 def __init__(self, lang): self.lang = lang def select_language(self, available_languages): return self.lang def set(self, lang, priority): if lang != self.lang: LOG('Localizer', PROBLEM, 'Cannot change language inside a translationContext', error=1) MARKER = [] from patches import get_request # late import, as this is patched by # unit tests request = get_request() # Localizer always use this request internally old_accept_language = request.get('AcceptLanguage', MARKER) request.set('AcceptLanguage', ForcedLanguage(lang)) try: assert self.get_selected_language() == lang yield finally: request.other.pop('AcceptLanguage') if old_accept_language is not MARKER: request.set('AcceptLanguage', old_accept_language) security.declarePublic('translate') def translate(self, domain, msgid, lang=None, *args, **kw): """ backward compatibility shim over zope.i18n.translate. Please avoid. """ # parameter reordering/mangling necessary assert not args if lang is not None: kw['target_language'] = lang return translate(to_unicode(msgid), domain=domain, **kw)
class LanguageManager(Tabs): """ """ security = ClassSecurityInfo() # TODO For backwards compatibility with Python 2.1 the variable # _languages is a tuple. Change it to a frozenset. _languages = () _default_language = None ######################################################################## # API ######################################################################## def get_languages(self): """Returns all the object languages. """ return self._languages def set_languages(self, languages): """Sets the object languages. """ self._languages = tuple(languages) def add_language(self, language): """Adds a new language. """ if language not in self._languages: # Sort the language list, else selected languages # can be nearly random. new_language_list = tuple(self._languages) + (language, ) new_language_list = tuple(sorted(new_language_list)) self._languages = new_language_list def del_language(self, language): """Removes a language. """ if language in self._languages: languages = [x for x in self._languages if x != language] self._languages = tuple(languages) def get_languages_mapping(self): """Returns a list of dictionary, one for each objects language. The dictionary contains the language code, its name and a boolean value that tells wether the language is the default one or not. """ return [{ 'code': x, 'name': self.get_language_name(x), 'default': x == self._default_language } for x in self._languages] def get_available_languages(self, **kw): """Returns the langauges available. For example, a language could be considered as available only if there is some data associated to it. This method is used by the language negotiation code (see 'get_selected_language'), sometimes you will want to redefine it in your classes. """ return self._languages def get_default_language(self): """Returns the default language. This method is used by the language negotiation code (see 'get_selected_language'), sometimes you will want to redefine it in your classes. For example, maybe you will want to define it to return always a default language, even when internally it is None. """ return self._default_language ######################################################################## # Web API ######################################################################## # Security settings security.declarePublic('get_languages') security.declareProtected('Manage languages', 'set_languages') security.declareProtected('Manage languages', 'add_language') security.declareProtected('Manage languages', 'del_language') security.declarePublic('get_languages_mapping') security.declarePublic('get_language_name') def get_language_name(self, id=None): """ Returns the name of the given language code. XXX Kept here for backwards compatibility only """ if id is None: id = self.get_default_language() language_name = get_language_name(id) if language_name == '???': return self.get_user_defined_language_name(id) or language_name else: return language_name security.declarePublic('get_available_languages') security.declarePublic('get_default_language') # XXX Kept here temporarily, further refactoring needed security.declarePublic('get_selected_language') def get_selected_language(self, **kw): """ Returns the selected language. Here the language negotiation takes place. Accepts keyword arguments which will be passed to 'get_available_languages'. """ available_languages = apply(self.get_available_languages, (), kw) return lang_negotiator(available_languages) \ or self.get_default_language() ######################################################################## # ZMI ######################################################################## manage_options = ({ 'action': 'manage_languages', 'label': u'Languages', 'help': ('Localizer', 'LM_languages.stx') }, ) def filtered_manage_options(self, REQUEST=None): options = Tabs.filtered_manage_options(self, REQUEST=REQUEST) # Insert the upgrade form if needed if self._needs_upgrade(): options.insert( 0, { 'action': 'manage_upgradeForm', 'label': u'Upgrade', 'help': ('Localizer', 'LM_upgrade.stx') }) # Translate the labels r = [] for option in options: option = option.copy() option['label'] = _(option['label']) r.append(option) # Ok return r security.declareProtected('View management screens', 'manage_languages') manage_languages = LocalDTMLFile('ui/LM_languages', globals()) security.declarePublic('get_all_languages') def get_all_languages(self): """ Returns all ISO languages, used by 'manage_languages'. """ return get_languages() + self.get_user_defined_languages() security.declareProtected('Manage languages', 'manage_addLanguage') def manage_addLanguage(self, language, REQUEST=None, RESPONSE=None): """ """ self.add_language(language) if RESPONSE is not None: RESPONSE.redirect("%s/manage_languages" % REQUEST['URL1']) security.declareProtected('Manage languages', 'manage_delLanguages') def manage_delLanguages(self, languages, REQUEST, RESPONSE): """ """ for language in languages: self.del_language(language) RESPONSE.redirect("%s/manage_languages" % REQUEST['URL1']) security.declareProtected('Manage languages', 'manage_changeDefaultLang') def manage_changeDefaultLang(self, language, REQUEST=None, RESPONSE=None): """ """ self._default_language = language if REQUEST is not None: RESPONSE.redirect("%s/manage_languages" % REQUEST['URL1']) # Unicode support, custom ZMI manage_page_header = LocalDTMLFile('ui/manage_page_header', globals()) ######################################################################## # Upgrade def _needs_upgrade(self): return False def _upgrade(self): pass security.declarePublic('need_upgrade') def need_upgrade(self): """ """ return self._needs_upgrade() security.declareProtected('Manage Access Rules', 'manage_upgradeForm', 'manage_upgrade') manage_upgradeForm = LocalDTMLFile('ui/LM_upgrade', globals()) def manage_upgrade(self, REQUEST, RESPONSE): """ """ self._upgrade() RESPONSE.redirect('manage_main') # Add a feature which allows users to be able to add a new language. security.declarePublic('get_user_defined_language_name') def get_user_defined_language_name(self, id=None): """ Returns the name of the given user defined language code. """ for language_dict in self.get_user_defined_languages(): if language_dict['code'] == id: return language_dict['name'] security.declarePublic('get_user_defined_languages') def get_user_defined_languages(self): user_define_language_dict_list = [] localizer = getattr(self, 'Localizer', None) if localizer is not None: for value in getattr(self, 'user_defined_languages', ()): splitted_value = value.split(' ', 1) if len(splitted_value) == 2: user_define_language_dict_list.append({ 'name': splitted_value[0].strip(), 'code': splitted_value[1].strip(), }) return user_define_language_dict_list def _add_user_defined_language(self, language_name, language_code): self.user_defined_languages = (getattr(self, 'user_defined_languages', ()) + ('%s %s' % (language_name, language_code), )) self._p_changed = True def _del_user_defined_language(self, language_code): user_defined_languages = [] for language_dict in self.get_user_defined_languages(): if language_dict['code'] != language_code: user_defined_languages.append( '%s %s' % (language_dict['name'], language_dict['code'])) self.user_defined_languages = tuple(user_defined_languages) self._p_changed = True
from OFS.Folder import Folder from zLOG import LOG, ERROR, INFO, PROBLEM from zope.interface import implements from zope.i18n import translate from ZPublisher.BeforeTraverse import registerBeforeTraverse, \ unregisterBeforeTraverse, queryBeforeTraverse, NameCaller # Import Localizer modules from interfaces import ILocalizer from LocalFiles import LocalDTMLFile from MessageCatalog import MessageCatalog, to_unicode from utils import lang_negotiator from LanguageManager import LanguageManager # Constructors manage_addLocalizerForm = LocalDTMLFile('ui/Localizer_add', globals()) def manage_addLocalizer(self, title, languages, REQUEST=None, RESPONSE=None): """ Add a new Localizer instance. """ self._setObject('Localizer', Localizer(title, languages)) if REQUEST is not None: RESPONSE.redirect('manage_main') class Localizer(LanguageManager, Folder): """ The Localizer meta type lets you customize the language negotiation
return url + '?' + '&'.join(params) # Empty header information for PO files (UTF-8 is the default encoding) empty_po_header = { 'last_translator_name': '', 'last_translator_email': '', 'language_team': '', 'charset': 'UTF-8' } ########################################################################### # The Message Catalog class, and ZMI constructor ########################################################################### manage_addMessageCatalogForm = LocalDTMLFile('ui/MC_add', globals()) def manage_addMessageCatalog(self, id, title, languages, sourcelang=None, REQUEST=None): """ """ if sourcelang is None: sourcelang = languages[0] self._setObject(id, MessageCatalog(id, title, sourcelang, languages)) if REQUEST is not None:
class MessageCatalog(LanguageManager, ObjectManager, SimpleItem): """Stores messages and their translations... """ meta_type = 'MessageCatalog' implements(IMessageCatalog) security = ClassSecurityInfo() POLICY_ADD_FALSE = 0 POLICY_ADD_TRUE = 1 POLICY_ADD_LOG = 2 def __init__(self, id, title, sourcelang, languages): self.id = id self.title = title self.policy = self.POLICY_ADD_TRUE # Language Manager data self._languages = tuple(languages) self._default_language = sourcelang # Here the message translations are stored self._messages = PersistentMapping() # Data for the PO files headers self._po_headers = PersistentMapping() for lang in self._languages: self._po_headers[lang] = empty_po_header ####################################################################### # ITranslationDomain interface # zope.i18n.interfaces.ITranslationDomain ####################################################################### @property def domain(self): """ """ return unicode(self.id) def translate(self, msgid, mapping=None, context=None, target_language=None, default=None): """ """ msgstr = self.gettext(msgid, lang=target_language, default=default) # BBB support str in mapping by converting to unicode for # backward compatibility. if mapping: mapping = dict([to_unicode(k), to_unicode(v)] for k, v in mapping.iteritems()) return interpolate(msgstr, mapping) ####################################################################### # Private API ####################################################################### def get_message_key(self, message): if message in self._messages: return message # A message may be stored as unicode or byte string encoding = HTTPRequest.default_encoding if isinstance(message, unicode): message = message.encode(encoding) else: message = unicode(message, encoding) if message in self._messages: return message def get_translations(self, message): message = self.get_message_key(message) return self._messages[message] def get_tabs_message(self, REQUEST): message = REQUEST.get('manage_tabs_message') if message is None: return None return unicode(message, 'utf-8') ####################################################################### # Public API ####################################################################### security.declarePublic('message_exists') def message_exists(self, message): """ """ # BBB call get_message_key to support both (old) str key and # (new) unicode key. return bool(self.get_message_key(message)) security.declareProtected('Manage messages', 'message_edit') def message_edit(self, message, language, translation, note): """ """ # BBB call get_message_key to support both (old) str key and # (new) unicode key. message = self.get_message_key(message) or message self._messages[message][language] = translation self._messages[message]['note'] = note security.declareProtected('Manage messages', 'message_del') def message_del(self, message): """ """ # BBB call get_message_key to support both (old) str key and # (new) unicode key. message = self.get_message_key(message) or message del self._messages[message] security.declarePublic('gettext') def gettext(self, message, lang=None, add=None, default=None): """Returns the message translation from the database if available. If add=1, add any unknown message to the database. If a default is provided, use it instead of the message id as a translation for unknown messages. """ if not isinstance(message, basestring): raise TypeError('only strings can be translated, not: %r' % (message, )) if default is None: default = message message = message.strip() # BBB call get_message_key to support both (old) str key and # (new) unicode key. message = self.get_message_key(message) or to_unicode(message) # Add it if it's not in the dictionary if add is None: add = getattr(self, 'policy', self.POLICY_ADD_TRUE) if add != self.POLICY_ADD_FALSE and not self._messages.has_key( message) and message: if add == self.POLICY_ADD_LOG: LOG( 'New entry added to message catalog %s :' % self.id, INFO, '%s\n%s' % (message, ''.join(format_list(extract_stack()[:-1])))) self._messages[message] = PersistentMapping() # Get the string if self._messages.has_key(message): m = self._messages[message] if lang is None: # Builds the list of available languages # should the empty translations be filtered? available_languages = list(self._languages) # Imagine that the default language is 'en'. There is no # translation from 'en' to 'en' in the message catalog # The user has the preferences 'en' and 'nl' in that order # The next two lines make certain 'en' is shown, not 'nl' if not self._default_language in available_languages: available_languages.append(self._default_language) # Get the language! lang = lang_negotiator(available_languages) # Is it None? use the default if lang is None: lang = self._default_language if lang is not None: return m.get(lang) or default return default __call__ = gettext ####################################################################### # Management screens ####################################################################### manage_options = ( {'label': u'Messages', 'action': 'manage_messages', 'help': ('Localizer', 'MC_messages.stx')}, {'label': u'Properties', 'action': 'manage_propertiesForm'}, {'label': u'Import', 'action': 'manage_Import_form', 'help': ('Localizer', 'MC_importExport.stx')}, {'label': u'Export', 'action': 'manage_Export_form', 'help': ('Localizer', 'MC_importExport.stx')}) \ + LanguageManager.manage_options \ + SimpleItem.manage_options ####################################################################### # Management screens -- Messages ####################################################################### security.declareProtected('Manage messages', 'manage_messages') manage_messages = LocalDTMLFile('ui/MC_messages', globals()) security.declarePublic('get_namespace') def get_namespace(self, REQUEST): """For the management interface, allows to filter the messages to show. """ # Check whether there are languages or not languages = self.get_languages_mapping() if not languages: return {} # Input batch_start = REQUEST.get('batch_start', 0) batch_size = REQUEST.get('batch_size', 15) empty = REQUEST.get('empty', 0) regex = REQUEST.get('regex', '') message = REQUEST.get('msg', None) # Build the namespace namespace = {} namespace['batch_size'] = batch_size namespace['empty'] = empty namespace['regex'] = regex # The language lang = REQUEST.get('lang', None) or languages[0]['code'] namespace['language'] = lang # Filter the messages query = regex.strip() try: query = compile(query) except: query = compile('') messages = [] for m, t in self._messages.items(): if query.search(m) and (not empty or not t.get(lang, '').strip()): messages.append(m) messages.sort(filter_sort) # How many messages n = len(messages) namespace['n_messages'] = n # Calculate the start while batch_start >= n: batch_start = batch_start - batch_size if batch_start < 0: batch_start = 0 namespace['batch_start'] = batch_start # Select the batch to show batch_end = batch_start + batch_size messages = messages[batch_start:batch_end] # Batch links namespace['previous'] = get_url(REQUEST.URL, batch_start - batch_size, batch_size, regex, lang, empty) namespace['next'] = get_url(REQUEST.URL, batch_start + batch_size, batch_size, regex, lang, empty) # Get the message message_encoded = None translations = {} if message is None: if messages: message = messages[0] translations = self.get_translations(message) message = to_unicode(message) message_encoded = message_encode(message) else: message_encoded = message message = message_decode(message_encoded) translations = self.get_translations(message) message = to_unicode(message) namespace['message'] = message namespace['message_encoded'] = message_encoded namespace['translations'] = translations namespace['translation'] = translations.get(lang, '') namespace['note'] = translations.get('note', '') # Calculate the current message namespace['messages'] = [] for x in messages: x = to_unicode(x) x_encoded = message_encode(x) url = get_url(REQUEST.URL, batch_start, batch_size, regex, lang, empty, msg=x_encoded) namespace['messages'].append({ 'message': x, 'message_encoded': x_encoded, 'current': x == message, 'url': url }) # The languages for language in languages: code = language['code'] language['name'] = _(language['name'], language=code) language['url'] = get_url(REQUEST.URL, batch_start, batch_size, regex, code, empty, msg=message_encoded) namespace['languages'] = languages return namespace security.declareProtected('Manage messages', 'manage_editMessage') def manage_editMessage(self, message, language, translation, note, REQUEST, RESPONSE): """Modifies a message. """ message_encoded = message message = message_decode(message_encoded) message_key = self.get_message_key(message) self.message_edit(message_key, language, translation, note) url = get_url(REQUEST.URL1 + '/manage_messages', REQUEST['batch_start'], REQUEST['batch_size'], REQUEST['regex'], REQUEST.get('lang', ''), REQUEST.get('empty', 0), msg=message_encoded, manage_tabs_message=_(u'Saved changes.')) RESPONSE.redirect(url) security.declareProtected('Manage messages', 'manage_delMessage') def manage_delMessage(self, message, REQUEST, RESPONSE): """ """ message = message_decode(message) message_key = self.get_message_key(message) self.message_del(message_key) url = get_url(REQUEST.URL1 + '/manage_messages', REQUEST['batch_start'], REQUEST['batch_size'], REQUEST['regex'], REQUEST.get('lang', ''), REQUEST.get('empty', 0), manage_tabs_message=_(u'Saved changes.')) RESPONSE.redirect(url) ####################################################################### # Management screens -- Properties # Management screens -- Import/Export # FTP access ####################################################################### security.declareProtected('View management screens', 'manage_propertiesForm') manage_propertiesForm = LocalDTMLFile('ui/MC_properties', globals()) security.declareProtected('View management screens', 'manage_properties') def manage_properties(self, title, policy, REQUEST=None, RESPONSE=None): """Change the Message Catalog properties. """ self.title = title self.policy = int(policy) if RESPONSE is not None: RESPONSE.redirect('manage_propertiesForm') # Properties management screen security.declareProtected('View management screens', 'get_po_header') def get_po_header(self, lang): """ """ # For backwards compatibility if not hasattr(aq_base(self), '_po_headers'): self._po_headers = PersistentMapping() return self._po_headers.get(lang, empty_po_header) security.declareProtected('View management screens', 'update_po_header') def update_po_header(self, lang, last_translator_name=None, last_translator_email=None, language_team=None, charset=None, REQUEST=None, RESPONSE=None): """ """ header = self.get_po_header(lang) if last_translator_name is None: last_translator_name = header['last_translator_name'] if last_translator_email is None: last_translator_email = header['last_translator_email'] if language_team is None: language_team = header['language_team'] if charset is None: charset = header['charset'] header = { 'last_translator_name': last_translator_name, 'last_translator_email': last_translator_email, 'language_team': language_team, 'charset': charset } self._po_headers[lang] = header if RESPONSE is not None: RESPONSE.redirect('manage_propertiesForm') security.declareProtected('View management screens', 'manage_Import_form') manage_Import_form = LocalDTMLFile('ui/MC_Import_form', globals()) security.declarePublic('get_policies') def get_policies(self): """ """ if not hasattr(self, 'policy'): self.policy = self.POLICY_ADD_TRUE policies = [ [self.POLICY_ADD_FALSE, "Never add new entries automatically"], [self.POLICY_ADD_TRUE, "Add new entries automatically if missing"], [ self.POLICY_ADD_LOG, "Add new entries automatically if missing and log the backtrace" ], ] return policies security.declarePublic('get_charsets') def get_charsets(self): """ """ return charsets[:] security.declarePublic('manage_export') def manage_export(self, x, REQUEST=None, RESPONSE=None): """Exports the content of the message catalog either to a template file (locale.pot) or to an language specific PO file (<x>.po). """ # Get the PO header info header = self.get_po_header(x) last_translator_name = header['last_translator_name'] last_translator_email = header['last_translator_email'] language_team = header['language_team'] charset = header['charset'] # PO file header, empty message. po_revision_date = strftime('%Y-%m-%d %H:%m+%Z', gmtime(time())) pot_creation_date = po_revision_date last_translator = '%s <%s>' % (last_translator_name, last_translator_email) if x == 'locale.pot': language_team = 'LANGUAGE <*****@*****.**>' else: language_team = '%s <%s>' % (x, language_team) r = [ 'msgid ""', 'msgstr "Project-Id-Version: %s\\n"' % self.title, '"POT-Creation-Date: %s\\n"' % pot_creation_date, '"PO-Revision-Date: %s\\n"' % po_revision_date, '"Last-Translator: %s\\n"' % last_translator, '"Language-Team: %s\\n"' % language_team, '"MIME-Version: 1.0\\n"', '"Content-Type: text/plain; charset=%s\\n"' % charset, '"Content-Transfer-Encoding: 8bit\\n"', '', '' ] # Get the messages, and perhaps its translations. # Convert keys to unicode for proper sorting. d = {} if x == 'locale.pot': filename = x for k in self._messages.keys(): d[to_unicode(k, encoding=charset)] = u"" else: filename = '%s.po' % x for k, v in self._messages.items(): k = to_unicode(k, encoding=charset) d[k] = to_unicode(v.get(x, ""), encoding=charset) # Generate the file def backslashescape(x): x = to_str(x) quote_esc = compile(r'"') x = quote_esc.sub('\\"', x) trans = [('\n', '\\n'), ('\r', '\\r'), ('\t', '\\t')] for a, b in trans: x = x.replace(a, b) return x # Generate sorted msgids to simplify diffs dkeys = d.keys() dkeys.sort() for k in dkeys: r.append('msgid "%s"' % backslashescape(k)) v = d[k] r.append('msgstr "%s"' % backslashescape(v)) r.append('') if RESPONSE is not None: RESPONSE.setHeader('Content-type', 'application/data') RESPONSE.setHeader('Content-Disposition', 'inline;filename=%s' % filename) return '\n'.join(r) security.declareProtected('Manage messages', 'po_import') def po_import(self, lang, data): """ """ messages = self._messages # Load the data po = polib.pofile(data) encoding = to_str(po.encoding) for entry in po: msgid = to_unicode(entry.msgid, encoding=encoding) if msgid: msgstr = to_unicode(entry.msgstr or '', encoding=encoding) translation_map = messages.get(msgid) if translation_map is None: # convert old non-unicode translations if they exist: translation_map = messages.pop(self.get_message_key(msgid), None) if translation_map is None: translation_map = PersistentMapping() messages[msgid] = translation_map translation_map[lang] = msgstr # Set the encoding (the full header should be loaded XXX) self.update_po_header(lang, charset=encoding) security.declareProtected('Manage messages', 'manage_import') def manage_import(self, lang, file, REQUEST=None, RESPONSE=None): """ """ # XXX For backwards compatibility only, use "po_import" instead. if isinstance(file, str): content = file else: content = file.read() self.po_import(lang, content) if RESPONSE is not None: RESPONSE.redirect('manage_messages') def objectItems(self, spec=None): """ """ for lang in self._languages: if not hasattr(aq_base(self), lang): self._setObject(lang, POFile(lang)) r = MessageCatalog.inheritedAttribute('objectItems')(self, spec) return r ####################################################################### # TMX support security.declareProtected('View management screens', 'manage_Export_form') manage_Export_form = LocalDTMLFile('ui/MC_Export_form', globals()) ####################################################################### # Backwards compatibility (XXX) ####################################################################### security.declarePublic('hasmsg') hasmsg = message_exists security.declarePublic('hasLS') hasLS = message_exists # CMFLocalizer uses it
class MessageCatalog(LanguageManager, ObjectManager, SimpleItem): """Stores messages and their translations... """ meta_type = 'MessageCatalog' implements(IMessageCatalog) security = ClassSecurityInfo() def __init__(self, id, title, sourcelang, languages): self.id = id self.title = title # Language Manager data self._languages = tuple(languages) self._default_language = sourcelang # Here the message translations are stored self._messages = PersistentMapping() # Data for the PO files headers self._po_headers = PersistentMapping() for lang in self._languages: self._po_headers[lang] = empty_po_header ####################################################################### # ITranslationDomain interface # zope.i18n.interfaces.ITranslationDomain ####################################################################### @property def domain(self): """ """ return unicode(self.id) def translate(self, msgid, mapping=None, context=None, target_language=None, default=None): """ """ msgstr = self.gettext(msgid, lang=target_language, default=default) return interpolate(msgstr, mapping) ####################################################################### # Private API ####################################################################### def get_message_key(self, message): if message in self._messages: return message # A message may be stored as unicode or byte string encoding = HTTPRequest.default_encoding if isinstance(message, unicode): message = message.encode(encoding) else: message = unicode(message, encoding) if message in self._messages: return message def get_translations(self, message): message = self.get_message_key(message) return self._messages[message] def get_tabs_message(self, REQUEST): message = REQUEST.get('manage_tabs_message') if message is None: return None return unicode(message, 'utf-8') ####################################################################### # Public API ####################################################################### security.declarePublic('message_exists') def message_exists(self, message): """ """ return self._messages.has_key(message) security.declareProtected('Manage messages', 'message_edit') def message_edit(self, message, language, translation, note): """ """ self._messages[message][language] = translation self._messages[message]['note'] = note security.declareProtected('Manage messages', 'message_del') def message_del(self, message): """ """ del self._messages[message] security.declarePublic('gettext') def gettext(self, message, lang=None, add=1, default=None): """Returns the message translation from the database if available. If add=1, add any unknown message to the database. If a default is provided, use it instead of the message id as a translation for unknown messages. """ if not isinstance(message, (str, unicode)): raise TypeError, 'only strings can be translated.' message = message.strip() if default is None: default = message # Add it if it's not in the dictionary if add and not self._messages.has_key(message) and message: self._messages[message] = PersistentMapping() # Get the string if self._messages.has_key(message): m = self._messages[message] if lang is None: # Builds the list of available languages # should the empty translations be filtered? available_languages = list(self._languages) # Imagine that the default language is 'en'. There is no # translation from 'en' to 'en' in the message catalog # The user has the preferences 'en' and 'nl' in that order # The next two lines make certain 'en' is shown, not 'nl' if not self._default_language in available_languages: available_languages.append(self._default_language) # Get the language! lang = lang_negotiator(available_languages) # Is it None? use the default if lang is None: lang = self._default_language if lang is not None: return m.get(lang) or default return default __call__ = gettext ####################################################################### # Management screens ####################################################################### manage_options = ( {'label': u'Messages', 'action': 'manage_messages', 'help': ('Localizer', 'MC_messages.stx')}, {'label': u'Properties', 'action': 'manage_propertiesForm'}, {'label': u'Import', 'action': 'manage_Import_form', 'help': ('Localizer', 'MC_importExport.stx')}, {'label': u'Export', 'action': 'manage_Export_form', 'help': ('Localizer', 'MC_importExport.stx')}) \ + LanguageManager.manage_options \ + SimpleItem.manage_options ####################################################################### # Management screens -- Messages ####################################################################### security.declareProtected('Manage messages', 'manage_messages') manage_messages = LocalDTMLFile('ui/MC_messages', globals()) security.declarePublic('get_namespace') def get_namespace(self, REQUEST): """For the management interface, allows to filter the messages to show. """ # Check whether there are languages or not languages = self.get_languages_mapping() if not languages: return {} # Input batch_start = REQUEST.get('batch_start', 0) batch_size = REQUEST.get('batch_size', 15) empty = REQUEST.get('empty', 0) regex = REQUEST.get('regex', '') message = REQUEST.get('msg', None) # Build the namespace namespace = {} namespace['batch_size'] = batch_size namespace['empty'] = empty namespace['regex'] = regex # The language lang = REQUEST.get('lang', None) or languages[0]['code'] namespace['language'] = lang # Filter the messages query = regex.strip() try: query = compile(query) except: query = compile('') messages = [] for m, t in self._messages.items(): if query.search(m) and (not empty or not t.get(lang, '').strip()): messages.append(m) messages.sort(filter_sort) # How many messages n = len(messages) namespace['n_messages'] = n # Calculate the start while batch_start >= n: batch_start = batch_start - batch_size if batch_start < 0: batch_start = 0 namespace['batch_start'] = batch_start # Select the batch to show batch_end = batch_start + batch_size messages = messages[batch_start:batch_end] # Batch links namespace['previous'] = get_url(REQUEST.URL, batch_start - batch_size, batch_size, regex, lang, empty) namespace['next'] = get_url(REQUEST.URL, batch_start + batch_size, batch_size, regex, lang, empty) # Get the message message_encoded = None translations = {} if message is None: if messages: message = messages[0] translations = self.get_translations(message) message = to_unicode(message) message_encoded = message_encode(message) else: message_encoded = message message = message_decode(message_encoded) translations = self.get_translations(message) message = to_unicode(message) namespace['message'] = message namespace['message_encoded'] = message_encoded namespace['translations'] = translations namespace['translation'] = translations.get(lang, '') namespace['note'] = translations.get('note', '') # Calculate the current message namespace['messages'] = [] for x in messages: x = to_unicode(x) x_encoded = message_encode(x) url = get_url(REQUEST.URL, batch_start, batch_size, regex, lang, empty, msg=x_encoded) namespace['messages'].append({ 'message': x, 'message_encoded': x_encoded, 'current': x == message, 'url': url }) # The languages for language in languages: code = language['code'] language['name'] = _(language['name'], language=code) language['url'] = get_url(REQUEST.URL, batch_start, batch_size, regex, code, empty, msg=message_encoded) namespace['languages'] = languages return namespace security.declareProtected('Manage messages', 'manage_editMessage') def manage_editMessage(self, message, language, translation, note, REQUEST, RESPONSE): """Modifies a message. """ message_encoded = message message = message_decode(message_encoded) message_key = self.get_message_key(message) self.message_edit(message_key, language, translation, note) url = get_url(REQUEST.URL1 + '/manage_messages', REQUEST['batch_start'], REQUEST['batch_size'], REQUEST['regex'], REQUEST.get('lang', ''), REQUEST.get('empty', 0), msg=message_encoded, manage_tabs_message=_(u'Saved changes.')) RESPONSE.redirect(url) security.declareProtected('Manage messages', 'manage_delMessage') def manage_delMessage(self, message, REQUEST, RESPONSE): """ """ message = message_decode(message) message_key = self.get_message_key(message) self.message_del(message_key) url = get_url(REQUEST.URL1 + '/manage_messages', REQUEST['batch_start'], REQUEST['batch_size'], REQUEST['regex'], REQUEST.get('lang', ''), REQUEST.get('empty', 0), manage_tabs_message=_(u'Saved changes.')) RESPONSE.redirect(url) ####################################################################### # Management screens -- Properties # Management screens -- Import/Export # FTP access ####################################################################### security.declareProtected('View management screens', 'manage_propertiesForm') manage_propertiesForm = LocalDTMLFile('ui/MC_properties', globals()) security.declareProtected('View management screens', 'manage_properties') def manage_properties(self, title, REQUEST=None, RESPONSE=None): """Change the Message Catalog properties. """ self.title = title if RESPONSE is not None: RESPONSE.redirect('manage_propertiesForm') # Properties management screen security.declareProtected('View management screens', 'get_po_header') def get_po_header(self, lang): """ """ # For backwards compatibility if not hasattr(aq_base(self), '_po_headers'): self._po_headers = PersistentMapping() return self._po_headers.get(lang, empty_po_header) security.declareProtected('View management screens', 'update_po_header') def update_po_header(self, lang, last_translator_name=None, last_translator_email=None, language_team=None, charset=None, REQUEST=None, RESPONSE=None): """ """ header = self.get_po_header(lang) if last_translator_name is None: last_translator_name = header['last_translator_name'] if last_translator_email is None: last_translator_email = header['last_translator_email'] if language_team is None: language_team = header['language_team'] if charset is None: charset = header['charset'] header = { 'last_translator_name': last_translator_name, 'last_translator_email': last_translator_email, 'language_team': language_team, 'charset': charset } self._po_headers[lang] = header if RESPONSE is not None: RESPONSE.redirect('manage_propertiesForm') security.declareProtected('View management screens', 'manage_Import_form') manage_Import_form = LocalDTMLFile('ui/MC_Import_form', globals()) security.declarePublic('get_charsets') def get_charsets(self): """ """ return charsets[:] security.declarePublic('manage_export') def manage_export(self, x, REQUEST=None, RESPONSE=None): """Exports the content of the message catalog either to a template file (locale.pot) or to an language specific PO file (<x>.po). """ # Get the PO header info header = self.get_po_header(x) last_translator_name = header['last_translator_name'] last_translator_email = header['last_translator_email'] language_team = header['language_team'] charset = header['charset'] # PO file header, empty message. po_revision_date = strftime('%Y-%m-%d %H:%m+%Z', gmtime(time())) pot_creation_date = po_revision_date last_translator = '%s <%s>' % (last_translator_name, last_translator_email) if x == 'locale.pot': language_team = 'LANGUAGE <*****@*****.**>' else: language_team = '%s <%s>' % (x, language_team) r = [ 'msgid ""', 'msgstr "Project-Id-Version: %s\\n"' % self.title, '"POT-Creation-Date: %s\\n"' % pot_creation_date, '"PO-Revision-Date: %s\\n"' % po_revision_date, '"Last-Translator: %s\\n"' % last_translator, '"Language-Team: %s\\n"' % language_team, '"MIME-Version: 1.0\\n"', '"Content-Type: text/plain; charset=%s\\n"' % charset, '"Content-Transfer-Encoding: 8bit\\n"', '', '' ] # Get the messages, and perhaps its translations. d = {} if x == 'locale.pot': filename = x for k in self._messages.keys(): d[k] = "" else: filename = '%s.po' % x for k, v in self._messages.items(): try: d[k] = v[x] except KeyError: d[k] = "" # Generate the file def backslashescape(x): quote_esc = compile(r'"') x = quote_esc.sub('\\"', x) trans = [('\n', '\\n'), ('\r', '\\r'), ('\t', '\\t')] for a, b in trans: x = x.replace(a, b) return x # Generate sorted msgids to simplify diffs dkeys = d.keys() dkeys.sort() for k in dkeys: r.append('msgid "%s"' % backslashescape(k)) v = d[k] r.append('msgstr "%s"' % backslashescape(v)) r.append('') if RESPONSE is not None: RESPONSE.setHeader('Content-type', 'application/data') RESPONSE.setHeader('Content-Disposition', 'inline;filename=%s' % filename) r2 = [] for x in r: if isinstance(x, unicode): r2.append(x.encode(charset)) else: r2.append(x) return '\n'.join(r2) security.declareProtected('Manage messages', 'po_import') def po_import(self, lang, data): """ """ messages = self._messages # Load the data po = itools.gettext.POFile(string=data) for msgid in po.get_msgids(): # TODO Keep the context if any _context, msgid = msgid if msgid: msgstr = po.get_msgstr(msgid) or '' if not messages.has_key(msgid): messages[msgid] = PersistentMapping() messages[msgid][lang] = msgstr # Set the encoding (the full header should be loaded XXX) self.update_po_header(lang, charset=po.get_encoding()) security.declareProtected('Manage messages', 'manage_import') def manage_import(self, lang, file, REQUEST=None, RESPONSE=None): """ """ # XXX For backwards compatibility only, use "po_import" instead. if isinstance(file, str): content = file else: content = file.read() self.po_import(lang, content) if RESPONSE is not None: RESPONSE.redirect('manage_messages') def objectItems(self, spec=None): """ """ for lang in self._languages: if not hasattr(aq_base(self), lang): self._setObject(lang, POFile(lang)) r = MessageCatalog.inheritedAttribute('objectItems')(self, spec) return r ####################################################################### # TMX support security.declareProtected('View management screens', 'manage_Export_form') manage_Export_form = LocalDTMLFile('ui/MC_Export_form', globals()) security.declareProtected('Manage messages', 'tmx_export') def tmx_export(self, REQUEST, RESPONSE=None): """Exports the content of the message catalog to a TMX file """ src_lang = self._default_language # Get the header info header = self.get_po_header(src_lang) charset = header['charset'] # Init the TMX handler tmx = TMXFile() tmx.header['creationtool'] = u'Localizer' tmx.header['creationtoolversion'] = u'1.x' tmx.header['datatype'] = u'plaintext' tmx.header['segtype'] = u'paragraph' tmx.header['adminlang'] = src_lang tmx.header['srclang'] = src_lang tmx.header['o-encoding'] = u'%s' % charset.lower() # handle messages for msgkey, transunit in self._messages.items(): unit = TMXUnit({}) for lang in transunit.keys(): if lang != 'note': sentence = Sentence({'lang': lang}) sentence.text = transunit[lang] unit.msgstr[lang] = sentence if src_lang not in transunit.keys(): sentence = Sentence({'lang': src_lang}) sentence.text = msgkey unit.msgstr[src_lang] = sentence if transunit.has_key('note'): note = TMXNote(transunit.get('note')) unit.notes.append(note) tmx.messages[msgkey] = unit if RESPONSE is not None: RESPONSE.setHeader('Content-type', 'application/data') RESPONSE.setHeader('Content-Disposition', 'attachment; filename="%s.tmx"' % self.id) return tmx.to_str() security.declareProtected('Manage messages', 'tmx_import') def tmx_import(self, howmuch, file, REQUEST=None, RESPONSE=None): """Imports a TMX level 1 file. """ try: data = file.read() tmx = TMXFile(string=data) except: return MessageDialog( title='Parse error', message=_('impossible to parse the file'), action='manage_Import_form', ) num_notes = 0 num_trans = 0 if howmuch == 'clear': # Clear the message catalogue prior to import self._messages = {} self._languages = () self._default_language = tmx.get_srclang() for id, msg in tmx.messages.items(): if not self._messages.has_key(id) and howmuch == 'existing': continue msg.msgstr.pop(self._default_language) if not self._messages.has_key(id): self._messages[id] = {} for lang in msg.msgstr.keys(): # normalize the languageTag and extract the core (core, local) = LanguageTag.decode(lang) lang = LanguageTag.encode((core, local)) if lang not in self._languages: self._languages += (lang, ) if msg.msgstr[lang].text: self._messages[id][lang] = msg.msgstr[lang].text if core != lang and core != self._default_language: if core not in self._languages: self._languages += (core, ) if not msg.msgstr.has_key(core): self._messages[id][core] = msg.msgstr[lang].text if msg.notes: ns = [m.text for m in msg.notes] self._messages[id]['note'] = u' '.join(ns) num_notes += 1 num_trans += 1 if REQUEST is not None: message = _(u'Imported %d messages and %d notes') return MessageDialog(title=_(u'Messages imported'), message=message % (num_trans, num_notes), action='manage_messages') ####################################################################### # Backwards compatibility (XXX) ####################################################################### hasmsg = message_exists hasLS = message_exists # CMFLocalizer uses it security.declareProtected('Manage messages', 'xliff_export') def xliff_export(self, dst_lang, export_all=1, REQUEST=None, RESPONSE=None): """Exports the content of the message catalog to an XLIFF file """ from DateTime import DateTime src_lang = self._default_language export_all = int(export_all) # Init the XLIFF handler xliff = XLFFile() # Add the translation units original = '/%s' % self.absolute_url(1) for msgkey, transunit in self._messages.items(): target = transunit.get(dst_lang, '') # If 'export_all' is true export all messages, otherwise export # only untranslated messages if export_all or not target: unit = xliff.add_unit(original, msgkey, None) unit.attributes['id'] = md5text(msgkey) if target: unit.target = target # Add note note = transunit.get('note') if note: unit.notes.append(XLFNote(note)) # build the data-stucture for the File tag file = xliff.files[original] attributes = file.attributes attributes['original'] = original attributes['product-name'] = u'Localizer' attributes['product-version'] = u'1.1.x' attributes['data-type'] = u'plaintext' attributes['source-language'] = src_lang attributes['target-language'] = dst_lang attributes['date'] = DateTime().HTML4() # Serialize xliff = xliff.to_str() # Set response headers RESPONSE.setHeader('Content-Type', 'text/xml; charset=UTF-8') filename = '%s_%s_%s.xlf' % (self.id, src_lang, dst_lang) RESPONSE.setHeader('Content-Disposition', 'attachment; filename="%s"' % filename) # Ok return xliff security.declareProtected('Manage messages', 'xliff_import') def xliff_import(self, howmuch, file, REQUEST=None): """XLIFF is the XML Localization Interchange File Format designed by a group of software providers. It is specified by www.oasis-open.org """ try: data = file.read() xliff = XLFFile(string=data) except: return MessageDialog( title='Parse error', message=_('impossible to parse the file'), action='manage_Import_form', ) num_notes = 0 num_trans = 0 (file_ids, sources, targets) = xliff.get_languages() if howmuch == 'clear': # Clear the message catalogue prior to import self._messages = {} self._languages = () self._default_language = sources[0] # update languages if len(sources) > 1 or sources[0] != self._default_language: return MessageDialog( title='Language error', message=_('incompatible language sources'), action='manage_Import_form', ) for lang in targets: if lang != self._default_language and lang not in self._languages: self._languages += (lang, ) # get messages for file in xliff.files: cur_target = file.attributes.get('target-language', '') for msg in file.body.keys(): if not self._messages.has_key(msg) and howmuch == 'existing': pass else: if not self._messages.has_key(msg): self._messages[msg] = {} if cur_target and file.body[msg].target: self._messages[msg][cur_target] = file.body[msg].target num_trans += 1 if file.body[msg].notes: ns = [n.text for n in file.body[msg].notes] comment = ' '.join(ns) self._messages[msg]['note'] = comment num_notes += 1 if REQUEST is not None: return MessageDialog( title = _(u'Messages imported'), message = (_(u'Imported %d messages and %d notes to %s') % \ (num_trans, num_notes, ' '.join(targets))), action = 'manage_messages')
class Localizer(LanguageManager, Folder): """ The Localizer meta type lets you customize the language negotiation policy. """ meta_type = 'Localizer' implements(ILocalizer) id = 'Localizer' _properties = ({ 'id': 'title', 'type': 'string' }, { 'id': 'accept_methods', 'type': 'tokens' }) accept_methods = ('accept_path', 'accept_cookie', 'accept_url') security = ClassSecurityInfo() manage_options = \ (Folder.manage_options[0],) \ + LanguageManager.manage_options \ + Folder.manage_options[1:] def __init__(self, title, languages): self.title = title self._languages = languages self._default_language = languages[0] ####################################################################### # API / Private ####################################################################### def _getCopy(self, container): return Localizer.inheritedAttribute('_getCopy')(self, container) def _needs_upgrade(self): return not self.hooked() def _upgrade(self): # Upgrade to 0.9 if not self.hooked(): self.manage_hook(1) ####################################################################### # API / Public ####################################################################### # Get some data security.declarePublic('get_supported_languages') def get_supported_languages(self): """ Get the supported languages, that is the languages that the are being working so the site is or will provide to the public. """ return self._languages security.declarePublic('get_selected_language') def get_selected_language(self): """ """ return lang_negotiator(self._languages) \ or self._default_language # Hooking the traversal machinery # Fix this! a new permission needed? ## security.declareProtected('View management screens', 'manage_hookForm') ## manage_hookForm = LocalDTMLFile('ui/Localizer_hook', globals()) ## security.declareProtected('Manage properties', 'manage_hook') security.declarePrivate('manage_hook') def manage_hook(self, hook=0): """ """ if hook != self.hooked(): if hook: hook = NameCaller(self.id) registerBeforeTraverse(aq_parent(self), hook, self.meta_type) else: unregisterBeforeTraverse(aq_parent(self), self.meta_type) security.declarePublic('hooked') def hooked(self): """ """ if queryBeforeTraverse(aq_parent(self), self.meta_type): return 1 return 0 # New code to control the language policy def accept_cookie(self, accept_language): """Add the language from a cookie.""" lang = self.REQUEST.cookies.get('LOCALIZER_LANGUAGE', None) if lang is not None: accept_language.set(lang, 2.0) def accept_path(self, accept_language): """Add the language from the path.""" stack = self.REQUEST['TraversalRequestNameStack'] if stack and (stack[-1] in self._languages): lang = stack.pop() accept_language.set(lang, 3.0) def accept_url(self, accept_language): """Add the language from the URL.""" lang = self.REQUEST.form.get('LOCALIZER_LANGUAGE') if lang is not None: accept_language.set(lang, 2.0) def __call__(self, container, REQUEST): """Hooks the traversal path.""" try: accept_language = REQUEST['AcceptLanguage'] except KeyError: return for id in self.accept_methods: try: method = getattr(self, id) method(accept_language) except: LOG(self.meta_type, PROBLEM, 'method "%s" raised an exception.' % id) # Changing the language, useful snippets security.declarePublic('get_languages_map') def get_languages_map(self): """ Return a list of dictionaries, each dictionary has the language id, its title and a boolean value to indicate wether it's the user preferred language, for example: [{'id': 'en', 'title': 'English', 'selected': 1}] Used in changeLanguageForm. """ # For now only LPM instances are considered to be containers of # multilingual data. try: ob = self.getLocalPropertyManager() except AttributeError: ob = self ob_language = ob.get_selected_language() ob_languages = ob.get_available_languages() langs = [] for x in ob_languages: langs.append({ 'id': x, 'title': get_language_name(x), 'selected': x == ob_language }) return langs security.declarePublic('changeLanguage') changeLanguageForm = LocalDTMLFile('ui/changeLanguageForm', globals()) def changeLanguage(self, lang, goto=None, expires=None): """ """ request = self.REQUEST response = request.RESPONSE # Changes the cookie (it could be something different) parent = aq_parent(self) path = parent.absolute_url()[len(request['SERVER_URL']):] or '/' if expires is None: response.setCookie('LOCALIZER_LANGUAGE', lang, path=path) else: response.setCookie('LOCALIZER_LANGUAGE', lang, path=path, expires=unquote(expires)) # Comes back if goto is None: goto = request['HTTP_REFERER'] response.redirect(goto)
class LocalPropertyManager(LanguageManager, LocalAttributesBase): """ Mixin class that allows to manage localized properties. Somewhat similar to OFS.PropertyManager. """ security = ClassSecurityInfo() # Metadata for local properties # Example: ({'id': 'title', 'type': 'string'},) _local_properties_metadata = () # Local properties are stored here # Example: {'title': {'en': ('Title', timestamp), 'es': ('Títul', timestamp)}} _local_properties = {} # Useful to find or index all LPM instances isLocalPropertyManager = 1 def getLocalPropertyManager(self): """ Returns the instance, useful to get the object through acquisition. """ return self manage_options = ( {'action': 'manage_localPropertiesForm', 'label': u'Local properties', 'help': ('Localizer', 'LPM_properties.stx')}, {'action': 'manage_transPropertiesForm', 'label': u'Translate properties', 'help': ('Localizer', 'LPM_translate.stx')}) \ + LanguageManager.manage_options security.declarePublic('hasLocalProperty') def hasLocalProperty(self, id): """Return true if object has a property 'id'""" for property in self._local_properties_metadata: if property['id'] == id: return 1 return 0 security.declareProtected('View management screens', 'manage_localPropertiesForm') manage_localPropertiesForm = LocalDTMLFile('ui/LPM_properties', globals()) security.declareProtected('View management screens', 'manage_transPropertiesForm') manage_transPropertiesForm = LocalDTMLFile('ui/LPM_translations', globals()) security.declareProtected('Manage properties', 'set_localpropvalue') def set_localpropvalue(self, id, lang, value): # Get previous value old_value, timestamp = self.get_localproperty(id, lang) if old_value is None: old_value = '' # Update value only if it is different if value != old_value: properties = self._local_properties.copy() if not properties.has_key(id): properties[id] = {} properties[id][lang] = (value, time()) self._local_properties = properties def get_localproperty(self, name, language): if name not in self._local_properties: return None, None property = self._local_properties[name] if language not in property: return None, None value = property[language] if isinstance(value, tuple): return value return value, None security.declareProtected('Manage properties', 'set_localproperty') def set_localproperty(self, id, type, lang=None, value=None): """Adds a new local property""" if not self.hasLocalProperty(id): self._local_properties_metadata += ({'id': id, 'type': type}, ) setattr(self, id, LocalAttribute(id)) if lang is not None: self.set_localpropvalue(id, lang, value) security.declareProtected('Manage properties', 'del_localproperty') def del_localproperty(self, id): """Deletes a property""" # Update properties metadata p = [x for x in self._local_properties_metadata if x['id'] != id] self._local_properties_metadata = tuple(p) # delete attribute try: del self._local_properties[id] except KeyError: pass try: delattr(self, id) except KeyError: pass security.declareProtected('Manage properties', 'manage_addLocalProperty') def manage_addLocalProperty(self, id, type, REQUEST=None, RESPONSE=None): """Adds a new local property""" self.set_localproperty(id, type) if RESPONSE is not None: url = "%s/manage_localPropertiesForm?manage_tabs_message=Saved changes." % REQUEST[ 'URL1'] RESPONSE.redirect(url) security.declareProtected('Manage properties', 'manage_editLocalProperty') def manage_editLocalProperty(self, REQUEST, RESPONSE=None): """Edit a property""" def_lang = self.get_default_language() form = REQUEST.form for prop in self.getLocalProperties(): name = prop['id'] if form.has_key(name): value = form[name].strip() self.set_localpropvalue(name, def_lang, value) if REQUEST is not None: url = "%s/%s?manage_tabs_message=Saved changes." \ % (REQUEST['URL1'], REQUEST['destination']) REQUEST.RESPONSE.redirect(url) security.declareProtected('Manage properties', 'manage_delLocalProperty') def manage_delLocalProperty(self, ids=[], REQUEST=None, RESPONSE=None): """Deletes a property""" for id in ids: self.del_localproperty(id) if RESPONSE is not None: url = "%s/manage_localPropertiesForm?manage_tabs_message=Saved changes." % REQUEST[ 'URL1'] RESPONSE.redirect(url) security.declareProtected('Manage properties', 'manage_transLocalProperty') def manage_transLocalProperty(self, id, code, value, REQUEST, RESPONSE=None): """Translate a property.""" self.set_localpropvalue(id, code, value.strip()) if RESPONSE is not None: url = "%s/%s?lang=%s&prop=%s&manage_tabs_message=Saved changes." \ % (REQUEST['URL1'], REQUEST['destination'], code, id) RESPONSE.redirect(url) security.declareProtected('Manage properties', 'is_obsolete') def is_obsolete(self, prop, lang): default_language = self.get_default_language() value, t0 = self.get_localproperty(prop, default_language) value, t1 = self.get_localproperty(prop, lang) if t0 is None: return False if t1 is None: return True return t1 < t0 security.declarePublic('getTargetLanguages') def get_targetLanguages(self): """Get all languages except the default one.""" def_lang = self.get_default_language() all_langs = self.get_languages_mapping() for record in all_langs: if def_lang == record['code']: all_langs.remove(record) return all_langs security.declarePublic('getLocalProperties') def getLocalProperties(self): """Returns a copy of the properties metadata.""" return tuple([x.copy() for x in self._local_properties_metadata]) security.declarePublic('getLocalAttribute') def getLocalAttribute(self, id, lang=None): """Returns a local property""" # No language, look for the first non-empty available version if lang is None: lang = self.get_selected_language(property=id) value, timestamp = self.get_localproperty(id, lang) if value is None: return '' return value # Languages logic security.declarePublic('get_available_languages') def get_available_languages(self, **kw): """ """ languages = self.get_languages() id = kw.get('property', None) if id is None: # Is this thing right?? return languages else: if id in self._local_properties: property = self._local_properties[id] return [x for x in languages if property.get(x, None)] else: return [] security.declarePublic('get_default_language') def get_default_language(self): """ """ if self._default_language: return self._default_language languages = self.get_languages() if languages: return languages[0] return None # Upgrading.. def _needs_upgrade(self): return hasattr(aq_base(self), 'original_language') def _upgrade(self): # In version 0.7 the language management logic moved to the # mixin class LanguageManager, as a consequence the attribute # "original_language" changes its name to "_default_language". if hasattr(aq_base(self), 'original_languge'): self._default_language = self.original_language del self.original_language # XXX With version 1.1.0b5 (as of patch 14) the '_local_properties' # data structure keeps a timestamp to mark obsolete translations. # The upgrade code below must be activated once the new upgrade # framework is deployed, something that should happen for the 1.2 # release. ## for k, v in self._local_properties.items(): ## for i, j in v.items(): ## if type(j) is not tuple: ## # XXX add the timestamp for every property ## self._local_properties[k][i] = (j, time()) ## self._p_changed = 1 # Define <id>_<lang> attributes, useful for example to catalog def __getattr__(self, name): try: index = name.rfind('_') id, lang = name[:index], name[index + 1:] property = self._local_properties[id] except: raise AttributeError, "%s instance has no attribute '%s'" \ % (self.__class__.__name__, name) return self.getLocalAttribute(id, lang)
class LocalContent(CatalogAware, LocalPropertyManager, PropertyManager, SimpleItem): """ """ meta_type = 'LocalContent' security = ClassSecurityInfo() # Properties metadata _local_properties_metadata = ({'id': 'title', 'type': 'string'}, {'id': 'body', 'type': 'text'}) _properties = () title = LocalAttribute('title') # Override title from SimpleItem body = LocalAttribute('body') manage_options = \ LocalPropertyManager.manage_options \ + PropertyManager.manage_options[:1] \ + ({'action': 'manage_import', 'label': u'Import', 'help': ('Localizer', 'MC_importExport.stx')}, {'action': 'manage_export', 'label': u'Export', 'help': ('Localizer', 'MC_importExport.stx')}) \ + PropertyManager.manage_options[1:] \ + SimpleItem.manage_options def __init__(self, id, sourcelang, languages): self.id = id self._default_language = sourcelang self._languages = languages index_html = None # Prevent accidental acquisition def __call__(self, client=None, REQUEST=None, RESPONSE=None, **kw): if REQUEST is None: REQUEST = self.REQUEST # Get the template to use template_id = 'default_template' if hasattr(aq_base(self), 'default_template'): template_id = self.default_template # Render the object template = getattr(aq_parent(self), template_id) template = template.__of__(self) return apply(template, ((client, self), REQUEST), kw) # Override some methods to be sure that LocalContent objects are # reindexed when changed. def set_localpropvalue(self, id, lang, value): LocalContent.inheritedAttribute('set_localpropvalue')(self, id, lang, value) self.reindex_object() def del_localproperty(self, id): LocalContent.inheritedAttribute('del_localproperty')(self, id) self.reindex_object() security.declareProtected('View management screens', 'manage_import') manage_import = LocalDTMLFile('ui/LC_import_form', globals()) ####################################################################### # TMX support security.declareProtected('View management screens', 'manage_export') manage_export = LocalDTMLFile('ui/LC_export_form', globals()) security.declareProtected('Manage messages', 'tmx_export') def tmx_export(self, REQUEST, RESPONSE): """Exports the content of the message catalog to a TMX file. """ src_lang = self._default_language # Init the TMX handler tmx = TMXFile() tmx.header['creationtool'] = u'Localizer' tmx.header['creationtoolversion'] = u'1.x' tmx.header['datatype'] = u'plaintext' tmx.header['segtype'] = u'paragraph' tmx.header['adminlang'] = src_lang tmx.header['srclang'] = src_lang tmx.header['o-encoding'] = u'utf-8' # Add the translation units for key in self._local_properties.keys(): unit = TMXUnit({}) for lang in self._languages: sentence = Sentence({'lang': lang}) trans, fuzzy = self.get_localproperty(key, lang) sentence.text = trans unit.msgstr[lang] = sentence tmx.messages[self.get_localproperty(key, src_lang)[0]] = unit # Serialize data = tmx.to_str() # Set response headers RESPONSE.setHeader('Content-type','application/data') RESPONSE.setHeader('Content-Disposition', 'attachment; filename="%s.tmx"' % self.id) # Ok return data security.declareProtected('Manage messages', 'tmx_import') def tmx_import(self, file, REQUEST=None, RESPONSE=None): """Imports a TMX level 1 file. """ try: data = file.read() tmx = TMXFile(string=data) except: return MessageDialog(title = 'Parse error', message = _('impossible to parse the file') , action = 'manage_import',) for id, msg in tmx.messages.items(): for prop, d in self._local_properties.items(): if d[self._default_language][0] == id: msg.msgstr.pop(self._default_language) for lang in msg.msgstr.keys(): # normalize the languageTag and extract the core (core, local) = LanguageTag.decode(lang) lang = LanguageTag.encode((core, local)) if lang not in self._languages: self._languages += (lang,) texte = msg.msgstr[lang].text if texte: self.set_localpropvalue(prop, lang, texte) if core != lang and core != self._default_language: if core not in self._languages: self._languages += (core,) if not msg.msgstr.has_key(core): self.set_localpropvalue(prop, lang, texte) if REQUEST is not None: RESPONSE.redirect('manage_localPropertiesForm') security.declareProtected('Manage messages', 'xliff_export') def xliff_export(self, dst_lang, export_all=1, REQUEST=None, RESPONSE=None): """ Exports the content of the message catalog to an XLIFF file """ from DateTime import DateTime src_lang = self._default_language export_all = int(export_all) # Init the XLIFF handler xliff = XLFFile() # Add the translation units original = '/%s' % self.absolute_url(1) for prop in self._local_properties.keys(): target, fuzzy = self.get_localproperty(prop, dst_lang) msgkey, fuzzy = self.get_localproperty(prop, src_lang) # If 'export_all' is true export all messages, otherwise export # only untranslated messages if export_all or not target: unit = xliff.add_unit(original, msgkey, None) unit.attributes['id'] = md5text(msgkey) if target: unit.target = target # Set the file attributes file = xliff.files[original] attributes = file.attributes attributes['original'] = original attributes['product-name'] = u'Localizer' attributes['product-version'] = u'1.1.x' attributes['data-type'] = u'plaintext' attributes['source-language'] = src_lang attributes['target-language'] = dst_lang attributes['date'] = DateTime().HTML4() # Serialize xliff = xliff.to_str() # Set response headers RESPONSE.setHeader('Content-Type', 'text/xml; charset=UTF-8') filename = '%s_%s_%s.xlf' % (self.id, src_lang, dst_lang) RESPONSE.setHeader('Content-Disposition', 'attachment; filename="%s"' % filename) # Ok return xliff security.declareProtected('Manage messages', 'xliff_import') def xliff_import(self, file, REQUEST=None): """ XLIFF is the XML Localization Interchange File Format designed by a group of software providers. It is specified by www.oasis-open.org """ try: data = file.read() xliff = XLFFile(string=data) except: return MessageDialog(title = 'Parse error', message = _('impossible to parse the file') , action = 'manage_import',) num_trans = 0 (file_ids, sources, targets) = xliff.get_languages() # update languages if len(sources) > 1 or sources[0] != self._default_language: return MessageDialog(title = 'Language error', message = _('incompatible language sources') , action = 'manage_import',) for lang in targets: if lang != self._default_language and lang not in self._languages: self._languages += (lang,) # get messages for file in xliff.files: cur_target = file.attributes.get('target-language', '') for msg in file.body.keys(): for (prop, val) in self._local_properties.items(): if val[self._default_language][0] == msg: if cur_target and file.body[msg].target: texte = file.body[msg].target self.set_localpropvalue(prop, cur_target, texte) num_trans += 1 if REQUEST is not None: return MessageDialog( title = _(u'Messages imported'), message = (_(u'Imported %d messages to %s') % (num_trans, ' '.join(targets))), action = 'manage_localPropertiesForm')
# Import from Localizer from LocalAttributes import LocalAttribute from LocalFiles import LocalDTMLFile from LocalPropertyManager import LocalPropertyManager from utils import _ def md5text(str): """Create an MD5 sum (or hash) of a text. It is guaranteed to be 32 bytes long. """ return md5(str.encode('utf-8')).hexdigest() manage_addLocalContentForm = LocalDTMLFile('ui/LocalContent_add', globals()) def manage_addLocalContent(self, id, sourcelang, languages, REQUEST=None): """ """ languages.append(sourcelang) # Make sure source is one of the target langs self._setObject(id, LocalContent(id, sourcelang, tuple(languages))) if REQUEST is not None: return self.manage_main(self, REQUEST) class LocalContent(CatalogAware, LocalPropertyManager, PropertyManager, SimpleItem): """ """ meta_type = 'LocalContent'