Commit 8795e17b authored by Michal Čihař's avatar Michal Čihař

Merge remote-tracking branch 'origin/master'

parents f552641c b29bd8d4
...@@ -142,3 +142,70 @@ The changes are in this mode committed once one of following conditions happen: ...@@ -142,3 +142,70 @@ The changes are in this mode committed once one of following conditions happen:
* merge from upstream occurs * merge from upstream occurs
* import of translation happens * import of translation happens
* translation for a language is completed * translation for a language is completed
.. _custom-checks:
Customizing checks
------------------
Weblate comes with wide range of consistency checks (see :ref:`checks`), though
they might not 100% cover all you want to check. The list of performed checks
can be adjusted using :envvar:`CHECK_LIST` and you can also add custom checks.
All you need to do is to subclass :class:`trans.checks.Check`, set few
attributes and implement either ``check`` or ``check_single`` methods (first
one if you want to deal with plurals in your code, the latter one does this for
you). You will find below some examples.
Checking translation text does not contain "foo"
++++++++++++++++++++++++++++++++++++++++++++++++
This is pretty simple check which just checks whether translation does not
contain string "foo".
.. code-block:: python
from trans.checks import Check
from django.utils.translation import ugettext_lazy as _
class FooCheck(Check):
# Used as identifier for check, should be unique
check_id = 'foo'
# Short name used to display failing check
name = _('Foo check')
# Description for failing check
description = _('Your translation is foo')
# Real check code
def check_single(self, source, target, flags, language, unit):
return 'foo' in target
Checking Czech translation text plurals differ
++++++++++++++++++++++++++++++++++++++++++++++
Check using language information to verify that two plural forms in Czech
language are not same.
.. code-block:: python
from trans.checks import Check
from django.utils.translation import ugettext_lazy as _
class PluralCzechCheck(Check):
# Used as identifier for check, should be unique
check_id = 'foo'
# Short name used to display failing check
name = _('Foo check')
# Description for failing check
description = _('Your translation is foo')
# Real check code
def check(self, sources, targets, flags, language, unit):
if self.is_language(language, ['cs']):
return targets[1] == targets[2]
return False
...@@ -3,6 +3,12 @@ Configuration ...@@ -3,6 +3,12 @@ Configuration
All settings are stored in :file:`settings.py` (as usual for Django). All settings are stored in :file:`settings.py` (as usual for Django).
.. envvar:: CHECK_LIST
List of consistency checks to perform on translation.
.. seealso:: :ref:`checks`, :ref:`custom-checks`
.. envvar:: COMMIT_MESSAGE .. envvar:: COMMIT_MESSAGE
Message used on each commit Weblate does. Message used on each commit Weblate does.
......
...@@ -59,6 +59,13 @@ preferences, translation to these languages will be shown. ...@@ -59,6 +59,13 @@ preferences, translation to these languages will be shown.
Bellow translation can be also shown suggestions from other users, which you Bellow translation can be also shown suggestions from other users, which you
can accept or delete. can accept or delete.
Dictionary
----------
Each project can have assigned dictionary for any language. This could be used
for storing terminology for given project, so that translations are consistent.
You can display terms from currently translated string in bottom tabs.
Suggestions Suggestions
----------- -----------
...@@ -101,6 +108,8 @@ Machine translation service provided by Microsoft. ...@@ -101,6 +108,8 @@ Machine translation service provided by Microsoft.
http://www.microsofttranslator.com/ http://www.microsofttranslator.com/
.. _checks:
Checks Checks
------ ------
......
...@@ -263,3 +263,21 @@ LAZY_COMMITS = True ...@@ -263,3 +263,21 @@ LAZY_COMMITS = True
# Where to put Whoosh index # Where to put Whoosh index
WHOOSH_INDEX = os.path.join(WEB_ROOT, 'whoosh-index') WHOOSH_INDEX = os.path.join(WEB_ROOT, 'whoosh-index')
# List of consistency checks
CHECK_LIST = (
'trans.checks.SameCheck',
'trans.checks.BeginNewlineCheck',
'trans.checks.EndNewlineCheck',
'trans.checks.EndSpaceCheck',
'trans.checks.EndStopCheck',
'trans.checks.EndColonCheck',
'trans.checks.EndQuestionCheck',
'trans.checks.EndExclamationCheck',
'trans.checks.PythonFormatCheck',
'trans.checks.PHPFormatCheck',
'trans.checks.CFormatCheck',
'trans.checks.PluralsCheck',
'trans.checks.ConsistencyCheck',
)
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
import re import re
PYTHON_PRINTF_MATCH = re.compile(''' PYTHON_PRINTF_MATCH = re.compile('''
...@@ -85,34 +87,107 @@ SAME_BLACKLIST = frozenset(( ...@@ -85,34 +87,107 @@ SAME_BLACKLIST = frozenset((
'zip', 'zip',
)) ))
CHECKS = {} DEFAULT_CHECK_LIST = (
'trans.checks.SameCheck',
'trans.checks.BeginNewlineCheck',
'trans.checks.EndNewlineCheck',
'trans.checks.EndSpaceCheck',
'trans.checks.EndStopCheck',
'trans.checks.EndColonCheck',
'trans.checks.EndQuestionCheck',
'trans.checks.EndExclamationCheck',
'trans.checks.PythonFormatCheck',
'trans.checks.PHPFormatCheck',
'trans.checks.CFormatCheck',
'trans.checks.PluralsCheck',
'trans.checks.ConsistencyCheck',
)
class Check(object):
'''
Basic class for checks.
'''
check_id = ''
name = ''
description = ''
def plural_check(f): def check(self, sources, targets, flags, language, unit):
''' '''
Generic decorator for working with plural translations. Checks single unit, handling plurals.
''' '''
def _plural_check(sources, targets, flags, language, unit): # Check singular
if f(sources[0], targets[0], flags, language, unit): if self.check_single(sources[0], targets[0], flags, language, unit):
return True return True
# Do we have more to check?
if len(sources) == 1: if len(sources) == 1:
return False return False
for t in targets[1:]: # Check plurals against plural from source
if f(sources[1], t, flags, language, unit): for target in targets[1:]:
if self.check_single(sources[1], target, flags, language, unit):
return True
# Check did not fire
return False
def check_single(self, source, target, flags, language, unit):
'''
Check for single phrase, not dealing with plurals.
'''
return False
def check_chars(self, source, target, pos, chars):
'''
Generic checker for chars presence.
'''
if len(target) == 0:
return False
s = source[pos]
t = target[pos]
return (s in chars and t not in chars) or (s not in chars and t in chars)
def check_format_strings(self, source, target, regex):
'''
Generic checker for format strings.
'''
if len(target) == 0:
return False
src_matches = set([x[0] for x in regex.findall(source)])
tgt_matches = set([x[0] for x in regex.findall(target)])
# We ignore %% as this is really not relevant. However it needs
# to be matched to prevent handling %%s as %s.
if '%' in src_matches:
src_matches.remove('%')
if '%' in tgt_matches:
tgt_matches.remove('%')
if src_matches != tgt_matches:
return True return True
return False return False
return _plural_check def is_language(self, language, vals):
'''
Detects whether language is in given list, ignores language
variants.
'''
return language.code.split('_')[0] in vals
# Check for not translated entries
@plural_check class SameCheck(Check):
def check_same(source, target, flags, language, unit): '''
Check for not translated entries.
'''
check_id = 'same'
name = _('Not translated')
description = _('Source and translated strings are same')
def check_single(self, source, target, flags, language, unit):
# One letter things are usually labels or decimal/thousand separators # One letter things are usually labels or decimal/thousand separators
if len(source) == 1 and len(target) == 1: if len(source) == 1 and len(target) == 1:
return False return False
# English variants will have most things not translated # English variants will have most things not translated
if language.code.split('_')[0] == 'en': if self.is_language(language, ['en']):
return False return False
# Probably shortcut # Probably shortcut
...@@ -125,158 +200,174 @@ def check_same(source, target, flags, language, unit): ...@@ -125,158 +200,174 @@ def check_same(source, target, flags, language, unit):
return (source == target) return (source == target)
CHECKS['same'] = (_('Not translated'), check_same, _('Source and translated strings are same')) class BeginNewlineCheck(Check):
def check_chars(source, target, pos, chars):
''' '''
Generic checker for chars presence. Checks for newlines at beginning.
''' '''
if len(target) == 0: check_id = 'begin_newline'
return False name = _('Starting newline')
s = source[pos] description = _('Source and translated do not both start with a newline')
t = target[pos]
return (s in chars and t not in chars) or (s not in chars and t in chars)
# Checks for newlines at beginning/end
@plural_check
def check_begin_newline(source, target, flags, language, unit):
return check_chars(source, target, 0, ['\n'])
CHECKS['begin_newline'] = (_('Starting newline'), check_begin_newline, _('Source and translated do not both start with a newline')) def check_single(self, source, target, flags, language, unit):
return self.check_chars(source, target, 0, ['\n'])
@plural_check class EndNewlineCheck(Check):
def check_end_newline(source, target, flags, language, unit): '''
return check_chars(source, target, -1, ['\n']) Checks for newlines at end.
'''
check_id = 'end_newline'
name = _('Trailing newline')
description = _('Source and translated do not both end with a newline')
CHECKS['end_newline'] = (_('Trailing newline'), check_end_newline, _('Source and translated do not both end with a newline')) def check_single(self, source, target, flags, language, unit):
return self.check_chars(source, target, -1, ['\n'])
# Whitespace check class EndSpaceCheck(Check):
'''
Whitespace check
'''
check_id = 'end_space'
name = _('Trailing space')
description = _('Source and translated do not both end with a space')
@plural_check def check_single(self, source, target, flags, language, unit):
def check_end_space(source, target, flags, language, unit):
# One letter things are usually decimal/thousand separators # One letter things are usually decimal/thousand separators
if len(source) == 1 and len(target) <= 1: if len(source) == 1 and len(target) <= 1:
return False return False
if language.code.split('_')[0] in ['fr', 'br']: if self.is_language(language, ['fr', 'br']):
if len(target) == 0: if len(target) == 0:
return False return False
if source[-1] in [':', '!', '?'] and target[-1] == ' ': if source[-1] in [':', '!', '?'] and target[-1] == ' ':
return False return False
return check_chars(source, target, -1, [' ']) return self.check_chars(source, target, -1, [' '])
CHECKS['end_space'] = (_('Trailing space'), check_end_space, _('Source and translated do not both end with a space')) class EndStopCheck(Check):
'''
# Check for punctation Check for final stop
'''
check_id = 'end_stop'
name = _('Trailing stop')
description = _('Source and translated do not both end with a full stop')
@plural_check def check_single(self, source, target, flags, language, unit):
def check_end_stop(source, target, flags, language, unit):
if len(source) == 1 and len(target) == 1: if len(source) == 1 and len(target) == 1:
return False return False
return check_chars(source, target, -1, [u'.', u'。', u'।', u'۔']) return self.check_chars(source, target, -1, [u'.', u'。', u'।', u'۔'])
CHECKS['end_stop'] = (_('Trailing stop'), check_end_stop, _('Source and translated do not both end with a full stop'))
@plural_check class EndColonCheck(Check):
def check_end_colon(source, target, flags, language, unit): '''
if language.code.split('_')[0] in ['fr', 'br']: Check for final colon
'''
check_id = 'end_colon'
name = _('Trailing colon')
description = _('Source and translated do not both end with a colon or colon is not correctly spaced')
def check_single(self, source, target, flags, language, unit):
if self.is_language(language, ['fr', 'br']):
if len(target) == 0: if len(target) == 0:
return False return False
if source[-1] == ':': if source[-1] == ':':
if target[-3:] not in [' : ', '&nbsp;: ', u' : ']: if target[-3:] not in [' : ', '&nbsp;: ', u' : ']:
return True return True
return False return False
if language.code.split('_')[0] in ['ja']: if self.is_language(language, ['ja']):
# Japanese sentence might need to end with full stop # Japanese sentence might need to end with full stop
# in case it's used before list. # in case it's used before list.
if source[-1] == ':': if source[-1] == ':':
return check_chars(source, target, -1, [u':', u':', u'.', u'。']) return self.check_chars(source, target, -1, [u':', u':', u'.', u'。'])
return False return False
return check_chars(source, target, -1, [u':', u':']) return self.check_chars(source, target, -1, [u':', u':'])
CHECKS['end_colon'] = (_('Trailing colon'), check_end_colon, _('Source and translated do not both end with a colon or colon is not correctly spaced'))
@plural_check class EndQuestionCheck(Check):
def check_end_question(source, target, flags, language, unit): '''
if language.code.split('_')[0] in ['fr', 'br']: Check for final question mark
'''
check_id = 'end_question'
name = _('Trailing question')
description = _('Source and translated do not both end with a question mark or it is not correctly spaced')
def check_single(self, source, target, flags, language, unit):
if self.is_language(language, ['fr', 'br']):
if len(target) == 0: if len(target) == 0:
return False return False
if source[-1] == '?': if source[-1] == '?':
if target[-2:] not in [' ?', '&nbsp;?', u' ?']: if target[-2:] not in [' ?', '&nbsp;?', u' ?']:
return True return True
return False return False
return check_chars(source, target, -1, [u'?', u'՞', u'؟', u'⸮', u'?', u'፧', u'꘏', u'⳺']) return self.check_chars(source, target, -1, [u'?', u'՞', u'؟', u'⸮', u'?', u'፧', u'꘏', u'⳺'])
CHECKS['end_question'] = (_('Trailing question'), check_end_question, _('Source and translated do not both end with a question mark or it is not correctly spaced')) class EndExclamationCheck(Check):
'''
Check for final exclamation mark
'''
check_id = 'end_exclamation'
name = _('Trailing exclamation')
description = _('Source and translated do not both end with an exclamation mark or it is not correctly spaced')
@plural_check def check_single(self, source, target, flags, language, unit):
def check_end_exclamation(source, target, flags, language, unit): if self.is_language(language, ['fr', 'br']):
if language.code.split('_')[0] in ['fr', 'br']:
if len(target) == 0: if len(target) == 0:
return False return False
if source[-1] == '!': if source[-1] == '!':
if target[-2:] not in [' !', '&nbsp;!', u' !']: if target[-2:] not in [' !', '&nbsp;!', u' !']:
return True return True
return False return False
return check_chars(source, target, -1, [u'!', u'!', u'՜', u'᥄', u'႟', u'߹']) return self.check_chars(source, target, -1, [u'!', u'!', u'՜', u'᥄', u'႟', u'߹'])
CHECKS['end_exclamation'] = (_('Trailing exclamation'), check_end_exclamation, _('Source and translated do not both end with an exclamation mark or it is not correctly spaced'))
# For now all format string checks use generic implementation, but # For now all format string checks use generic implementation, but
# it should be switched to language specific # it should be switched to language specific
def check_format_strings(source, target, regex):
class PythonFormatCheck(Check):
''' '''
Generic checker for format strings. Check for Python format string
''' '''
if len(target) == 0: check_id = 'python_format'
return False name = _('Python format')
src_matches = set([x[0] for x in regex.findall(source)]) description = _('Format string does not match source')
tgt_matches = set([x[0] for x in regex.findall(target)])
# We ignore %% as this is really not relevant. However it needs
# to be matched to prevent handling %%s as %s.
if '%' in src_matches:
src_matches.remove('%')
if '%' in tgt_matches:
tgt_matches.remove('%')
if src_matches != tgt_matches:
return True
return False def check_single(self, source, target, flags, language, unit):
# Check for Python format string
@plural_check
def check_python_format(source, target, flags, language, unit):
if not 'python-format' in flags: if not 'python-format' in flags:
return False return False
return check_format_strings(source, target, PYTHON_PRINTF_MATCH) return self.check_format_strings(source, target, PYTHON_PRINTF_MATCH)
CHECKS['python_format'] = (_('Python format'), check_python_format, _('Format string does not match source')) class PHPFormatCheck(Check):
'''
# Check for PHP format string Check for PHP format string
'''
check_id = 'php_format'
name = _('PHP format')
description = _('Format string does not match source')
@plural_check def check_single(self, source, target, flags, language, unit):
def check_php_format(source, target, flags, language, unit):
if not 'php-format' in flags: if not 'php-format' in flags:
return False return False
return check_format_strings(source, target, PHP_PRINTF_MATCH) return self.check_format_strings(source, target, PHP_PRINTF_MATCH)
CHECKS['php_format'] = (_('PHP format'), check_php_format, _('Format string does not match source')) class CFormatCheck(Check):
'''
# Check for C format string Check for C format string
'''
check_id = 'c_format'
name = _('C format')
description = _('Format string does not match source')
@plural_check def check_single(self, source, target, flags, language, unit):
def check_c_format(source, target, flags, language, unit):
if not 'c-format' in flags: if not 'c-format' in flags:
return False return False
return check_format_strings(source, target, C_PRINTF_MATCH) return self.check_format_strings(source, target, C_PRINTF_MATCH)
CHECKS['c_format'] = (_('C format'), check_c_format, _('Format string does not match source'))
# Check for incomplete plural forms class PluralsCheck(Check):
'''
Check for incomplete plural forms
'''
check_id = 'plurals'
name = _('Missing plurals')
description = _('Some plural forms are not translated')
def check_plurals(sources, targets, flags, language, unit): def check(self, sources, targets, flags, language, unit):
# Is this plural? # Is this plural?
if len(sources) == 1: if len(sources) == 1:
return False return False
...@@ -286,11 +377,15 @@ def check_plurals(sources, targets, flags, language, unit): ...@@ -286,11 +377,15 @@ def check_plurals(sources, targets, flags, language, unit):
# Check for empty translation # Check for empty translation
return ('' in targets) return ('' in targets)
CHECKS['plurals'] = (_('Missing plurals'), check_plurals, _('Some plural forms are not translated')) class ConsistencyCheck(Check):
'''
# Check for inconsistent translations Check for inconsistent translations
'''
check_id = 'inconsistent'
name = _('Inconsistent')
description = _('This message has more than one translation in this project')
def check_consistency(sources, targets, flags, language, unit): def check(self, sources, targets, flags, language, unit):
from trans.models import Unit from trans.models import Unit
related = Unit.objects.filter( related = Unit.objects.filter(
translation__language = language, translation__language = language,
...@@ -305,4 +400,20 @@ def check_consistency(sources, targets, flags, language, unit): ...@@ -305,4 +400,20 @@ def check_consistency(sources, targets, flags, language, unit):
return False return False
CHECKS['inconsistent'] = (_('Inconsistent'), check_consistency, _('This message has more than one translation in this project'))
# Initialize checks list
CHECKS = {}
for path in getattr(settings, 'CHECK_LIST', DEFAULT_CHECK_LIST):
i = path.rfind('.')
module, attr = path[:i], path[i+1:]
try:
mod = __import__(module, {}, {}, [attr])
except ImportError, e:
raise ImproperlyConfigured('Error importing translation check module %s: "%s"' % (module, e))
try:
cls = getattr(mod, attr)
except AttributeError:
raise ImproperlyConfigured('Module "%s" does not define a "%s" callable check' % (module, attr))
CHECKS[cls.check_id] = cls()
...@@ -616,7 +616,7 @@ class Translation(models.Model): ...@@ -616,7 +616,7 @@ class Translation(models.Model):
for check in trans.checks.CHECKS: for check in trans.checks.CHECKS:
cnt = self.unit_set.filter_type(check).count() cnt = self.unit_set.filter_type(check).count()
if cnt > 0: if cnt > 0:
desc = trans.checks.CHECKS[check][2] + (' (%d)' % cnt) desc = trans.checks.CHECKS[check].description + (' (%d)' % cnt)
result.append((check, desc)) result.append((check, desc))
return result return result
...@@ -850,7 +850,7 @@ class Unit(models.Model): ...@@ -850,7 +850,7 @@ class Unit(models.Model):
tgt = self.get_target_plurals() tgt = self.get_target_plurals()
failing = [] failing = []
for check in trans.checks.CHECKS: for check in trans.checks.CHECKS:
if trans.checks.CHECKS[check][1](src, tgt, self.flags, self.translation.language, self): if trans.checks.CHECKS[check].check(src, tgt, self.flags, self.translation.language, self):
failing.append(check) failing.append(check)
for check in self.checks(): for check in self.checks():
...@@ -901,7 +901,7 @@ class Suggestion(models.Model): ...@@ -901,7 +901,7 @@ class Suggestion(models.Model):
unit.fuzzy = False unit.fuzzy = False
unit.save_backend(request, False) unit.save_backend(request, False)
CHECK_CHOICES = [(x, trans.checks.CHECKS[x][0]) for x in trans.checks.CHECKS] CHECK_CHOICES = [(x, trans.checks.CHECKS[x].name) for x in trans.checks.CHECKS]
class Check(models.Model): class Check(models.Model):
checksum = models.CharField(max_length = 40, default = '', blank = True, db_index = True) checksum = models.CharField(max_length = 40, default = '', blank = True, db_index = True)
...@@ -923,7 +923,7 @@ class Check(models.Model): ...@@ -923,7 +923,7 @@ class Check(models.Model):
) )
def get_description(self): def get_description(self):
return trans.checks.CHECKS[self.check][2] return trans.checks.CHECKS[self.check].description
def get_doc_url(self): def get_doc_url(self):
return 'http://weblate.readthedocs.org/en/weblate-%s/usage.html#check-%s' % ( return 'http://weblate.readthedocs.org/en/weblate-%s/usage.html#check-%s' % (
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment