Repository: WeblateOrg/weblate

Issue:
Checks for Java MessageFormat
**Is your feature request related to a problem? Please describe.**
Java MessageFormat has quite specific rules for formatting and those could be covered by quality checks.

**Describe the solution you'd like**
The errors in markup should be reported similarly to other formats.

**Additional context**
https://docs.oracle.com/javase/7/docs/api/java/text/MessageFormat.html


Discussion:

Resolving patch:
diff --git a/weblate/checks/format.py b/weblate/checks/format.py
--- a/weblate/checks/format.py
+++ b/weblate/checks/format.py
@@ -138,6 +138,21 @@
     re.VERBOSE
 )
 
+JAVA_MESSAGE_MATCH = re.compile(
+    r'''
+    {                                   # initial {
+        (?P<arg>\d+)                    # variable order
+        \s*
+        (
+        ,\s*(?P<format>[a-z]+)          # format type
+        (,\s*(?P<style>\S+))?            # format style
+        )?
+        \s*
+    }                                   # Ending }
+    ''',
+    re.VERBOSE
+)
+
 
 class BaseFormatCheck(TargetCheck):
     """Base class for fomat string checks."""
@@ -326,3 +341,38 @@ class JavaFormatCheck(BaseFormatCheck):
 
     def is_position_based(self, string):
         return '$' not in string and string != '%'
+
+
+class JavaMessageFormatCheck(BaseFormatCheck):
+    """Check for Java MessageFormat string"""
+    check_id = 'java_messageformat'
+    name = _('Java MessageFormat')
+    description = _('Java MessageFormat string does not match source')
+    regexp = JAVA_MESSAGE_MATCH
+
+    def is_position_based(self, string):
+        return False
+
+    def should_skip(self, unit):
+        if ('auto-java-messageformat' in unit.all_flags and
+                '{0' in unit.source):
+            return False
+
+        return super(JavaMessageFormatCheck, self).should_skip(unit)
+
+    def cleanup_string(self, text):
+        """No cleanups here"""
+        return text
+
+    def check_format(self, source, target, ignore_missing):
+        """Generic checker for format strings."""
+        if not target or not source:
+            return False
+
+        # Even number of quotes
+        if target.count("'") % 2 != 0:
+            return True
+
+        return super(JavaMessageFormatCheck, self).check_format(
+            source, target, ignore_missing
+        )
diff --git a/weblate/trans/autofixes/custom.py b/weblate/trans/autofixes/custom.py
new file mode 100644
--- /dev/null
+++ b/weblate/trans/autofixes/custom.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2012 - 2018 Michal Čihař <michal@cihar.com>
+#
+# This file is part of Weblate <https://weblate.org/>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+#
+"""
+Auto fixes implemeted for specific environments and not enabled by default.
+"""
+
+from __future__ import unicode_literals
+
+import re
+
+from django.utils.translation import ugettext_lazy as _
+
+from weblate.trans.autofixes.base import AutoFix
+
+QUOTE_PARAM = re.compile(r"'(\{[^}]+\})'")
+SINGLE_APO = re.compile(r"'{1,3}")
+DOUBLE_APO = re.compile(r"'{4,}")
+REPLACEMENT = '__weblate:quote__'
+REPLACE_STRING = r'{0}\1{0}'.format(REPLACEMENT)
+
+
+class DoubleApostrophes(AutoFix):
+    """Ensures apostrophes are escaped in Java Properties MessageFormat string
+
+    - all apostrophes except ones around {} vars are doubled
+
+    Note: This fix is not really generically applicable in all cases, that's
+    why it's not enabled by default.
+    """
+
+    fix_id = 'java-messageformat'
+    name = _('Apostrophes in Java MessageFormat')
+
+    def fix_single_target(self, target, source, unit):
+        flags = unit.all_flags
+        if (('auto-java-messageformat' not in flags or '{0' not in source) and
+                ('java-messageformat' not in flags)):
+            return target, False
+        # Split on apostrophe
+        new = SINGLE_APO.sub("''", DOUBLE_APO.sub(
+            "''''", QUOTE_PARAM.sub(REPLACE_STRING, target)
+        )).replace(REPLACEMENT, "'")
+        return new, new != target
diff --git a/weblate/trans/models/conf.py b/weblate/trans/models/conf.py
--- a/weblate/trans/models/conf.py
+++ b/weblate/trans/models/conf.py
@@ -86,6 +86,7 @@ class WeblateConf(AppConf):
         'weblate.checks.format.JavascriptFormatCheck',
         'weblate.checks.format.CSharpFormatCheck',
         'weblate.checks.format.JavaFormatCheck',
+        'weblate.checks.format.JavaMessageFormatCheck',
         'weblate.checks.angularjs.AngularJSInterpolationCheck',
         'weblate.checks.consistency.PluralsCheck',
         'weblate.checks.consistency.SamePluralsCheck',


Test patch:
diff --git a/weblate/checks/tests/test_format_checks.py b/weblate/checks/tests/test_format_checks.py
--- a/weblate/checks/tests/test_format_checks.py
+++ b/weblate/checks/tests/test_format_checks.py
@@ -23,10 +23,11 @@
 """
 
 from __future__ import unicode_literals
-from weblate.checks.tests.test_checks import CheckTestCase
+from weblate.checks.tests.test_checks import CheckTestCase, MockUnit
 from weblate.checks.format import (
     PythonFormatCheck, PHPFormatCheck, CFormatCheck, PythonBraceFormatCheck,
     PerlFormatCheck, CSharpFormatCheck, JavaFormatCheck,
+    JavaMessageFormatCheck,
 )
 
 
@@ -622,3 +623,147 @@ def test_reordered_format(self):
             '%2d string %1s',
             False
         ))
+
+
+class JavaMessageFormatCheckTest(CheckTestCase):
+    check = JavaMessageFormatCheck()
+
+    def setUp(self):
+        super(JavaMessageFormatCheckTest, self).setUp()
+        self.test_highlight = (
+            'java-messageformat',
+            '{0}string{1}',
+            [(0, 3, u'{0}'), (9, 12, u'{1}')],
+        )
+
+    def test_no_format(self):
+        self.assertFalse(self.check.check_format(
+            'strins',
+            'string',
+            False
+        ))
+
+    def test_escaping_no_position(self):
+        self.assertFalse(self.check.check_format(
+            '{{ string }}',
+            'string',
+            False
+        ))
+
+    def test_simple_format(self):
+        self.assertFalse(self.check.check_format(
+            '{0} strins',
+            '{0} string',
+            False
+        ))
+
+    def test_format_with_width(self):
+        self.assertFalse(self.check.check_format(
+            '{0,1} strins',
+            '{0,1} string',
+            False
+        ))
+
+    def test_format_with_flag(self):
+        self.assertFalse(self.check.check_format(
+            '{0:C2} strins',
+            '{0:C2} string',
+            False
+        ))
+
+    def test_full_format(self):
+        self.assertFalse(self.check.check_format(
+            '{0,1:N0} strins',
+            '{0,1:N0} string',
+            False
+        ))
+
+    def test_missing_format(self):
+        self.assertTrue(self.check.check_format(
+            '{0} strins',
+            'string',
+            False
+        ))
+
+    def test_missing_type_format(self):
+        self.assertTrue(self.check.check_format(
+            '{0,number} strins',
+            'string',
+            False
+        ))
+
+    def test_missing_flag_format(self):
+        self.assertTrue(self.check.check_format(
+            '{0} strins',
+            'string',
+            False
+        ))
+
+    def test_missing_full_format(self):
+        self.assertTrue(self.check.check_format(
+            '{0,number,integer} strins',
+            'string',
+            False
+        ))
+
+    def test_wrong_format(self):
+        self.assertTrue(self.check.check_format(
+            '{0} string',
+            '{1} string',
+            False
+        ))
+
+    def test_missing_named_format_ignore(self):
+        self.assertFalse(self.check.check_format(
+            '{0} string',
+            'string',
+            True
+        ))
+
+    def test_escaping_with_position(self):
+        self.assertFalse(self.check.check_format(
+            '{{ 0 }}',
+            'string',
+            False
+        ))
+
+    def test_wrong_attribute_format(self):
+        self.assertTrue(self.check.check_format(
+            '{0} string',
+            '{1} string',
+            False
+        ))
+
+    def test_reordered_format(self):
+        self.assertFalse(self.check.check_format(
+            '{0} string {1}',
+            '{1} string {0}',
+            False
+        ))
+
+    def test_skip(self):
+        unit = MockUnit(source='source')
+        self.assertTrue(self.check.should_skip(unit))
+        unit.flags = 'java-messageformat'
+        self.assertFalse(self.check.should_skip(unit))
+        unit.flags = 'auto-java-messageformat'
+        self.assertTrue(self.check.should_skip(unit))
+        unit.source = '{0}'
+        self.assertFalse(self.check.should_skip(unit))
+
+    def test_quotes(self):
+        self.assertFalse(self.check.check_format(
+            '{0} string {1}',
+            "'{1}' strin''g '{0}'",
+            False
+        ))
+        self.assertTrue(self.check.check_format(
+            '{0} string {1}',
+            "'{1}' strin''g '{0}",
+            False
+        ))
+        self.assertTrue(self.check.check_format(
+            '{0} string {1}',
+            "'{1}' strin'g '{0}'",
+            False
+        ))
diff --git a/weblate/trans/tests/test_autofix.py b/weblate/trans/tests/test_autofix.py
--- a/weblate/trans/tests/test_autofix.py
+++ b/weblate/trans/tests/test_autofix.py
@@ -30,6 +30,7 @@
 from weblate.trans.autofixes.chars import (
     ReplaceTrailingDotsWithEllipsis, RemoveZeroSpace, RemoveControlChars,
 )
+from weblate.trans.autofixes.custom import DoubleApostrophes
 from weblate.trans.autofixes.whitespace import SameBookendingWhitespace
 
 
@@ -162,3 +163,49 @@ def test_fix_target(self):
         self.assertEqual(fixed, ['Bar…'])
         self.assertEqual(len(fixups), 1)
         self.assertEqual(force_text(fixups[0]), 'Trailing ellipsis')
+
+    def test_apostrophes(self):
+        unit = MockUnit(source='Foo')
+        fix = DoubleApostrophes()
+        # No flags
+        self.assertEqual(fix.fix_target(['Bar'], unit), (['Bar'], False))
+        # No format string, but forced
+        unit.flags = 'java-messageformat'
+        self.assertEqual(fix.fix_target(['Bar'], unit), (['Bar'], False))
+        # No format string
+        unit.flags = 'auto-java-messageformat'
+        self.assertEqual(fix.fix_target(['Bar'], unit), (['Bar'], False))
+        unit.source = 'test {0}'
+        # Nothing to fix
+        self.assertEqual(fix.fix_target(['r {0}'], unit), (['r {0}'], False))
+        # Correct string
+        self.assertEqual(
+            fix.fix_target(["''r'' {0}"], unit),
+            (["''r'' {0}"], False)
+        )
+        # String with quoted format string
+        self.assertEqual(
+            fix.fix_target(["''r'' '{0}'"], unit),
+            (["''r'' '{0}'"], False)
+        )
+        # Fixes
+        self.assertEqual(
+            fix.fix_target(["'r''' {0}"], unit),
+            (["''r'' {0}"], True)
+        )
+        # Fixes keeping double ones
+        self.assertEqual(
+            fix.fix_target(["'''''''r'''' {0}"], unit),
+            (["''''r'''' {0}"], True)
+        )
+        # Quoted format
+        self.assertEqual(
+            fix.fix_target(["'r''' {0}"], unit),
+            (["''r'' {0}"], True)
+        )
+        unit.source = 'foo'
+        unit.flags = 'java-messageformat'
+        self.assertEqual(
+            fix.fix_target(["bar'"], unit),
+            (["bar''"], True)
+        )