Skip to content

Commit

Permalink
Add support for custom key separator for JSON formats and exporters
Browse files Browse the repository at this point in the history
  • Loading branch information
mkarol-neurosys committed Apr 11, 2024
1 parent 0fe34ca commit 3fbb0fa
Show file tree
Hide file tree
Showing 23 changed files with 256 additions and 18 deletions.
2 changes: 1 addition & 1 deletion docs/formats/i18next.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ with (what is most often the) English strings.

Example file:

.. literalinclude:: ../../weblate/trans/tests/data/en.i18next.json
.. literalinclude:: ../../weblate/trans/tests/data/en.i18nextv4.json
:language: json

Weblate configuration
Expand Down
6 changes: 5 additions & 1 deletion docs/user/files.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@ are available via the :guilabel:`Files` ↓ :guilabel:`Customize download` menu:
* Excel Open XML (``xlsx``)
* JSON (only available for monolingual translations) (``json``)
* JSON nested structure file (only available for monolingual translations) (``json-nested``)
* I18Next V4 file (``i18nextv4``)
* Android String Resource (only available for monolingual translations) (``aresource``)
* iOS strings (only available for monolingual translations) (``strings``)

For JSON based format, key separator set in component settings will be used.

.. hint::

The content available in the converted files differs based on file format
Expand Down Expand Up @@ -83,7 +86,8 @@ Supported file formats

Any file in a supported file format can be uploaded, but it is still
recommended to use the same file format as the one used for translation, otherwise some
features might not be translated properly.
features might not be translated properly. For JSON based file formats, custom key separator
could be set in component settings or during setting up the component.

.. seealso::

Expand Down
8 changes: 6 additions & 2 deletions weblate/formats/auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
from weblate.formats.helpers import NamedBytesIO
from weblate.formats.models import FILE_FORMATS
from weblate.formats.ttkit import TTKitFormat
from weblate.trans.wrappers import file_format_custom_key_separator_wrapper

if TYPE_CHECKING:
from collections.abc import Generator

from weblate.formats.base import TranslationFormat
from weblate.trans.models.component import Component


def detect_filename(filename: str) -> type[TranslationFormat] | None:
Expand Down Expand Up @@ -81,13 +83,15 @@ def params_iter(
def try_load(
filename: str,
content: bytes,
original_format: type[TranslationFormat] | None,
component: Component | None,
template_store: TranslationFormat | None,
is_template: bool = False,
) -> TranslationFormat:
"""Try to load file by guessing type."""
failure = None
for file_format in formats_iter(filename, original_format):
for file_format in formats_iter(filename, component.file_format_cls):
file_format = file_format_custom_key_separator_wrapper(file_format, component.key_separator)

for kwargs, validate in params_iter(file_format, template_store, is_template):
handle = NamedBytesIO(filename, content)
try:
Expand Down
2 changes: 1 addition & 1 deletion weblate/formats/exporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
from translate.storage.aresource import AndroidResourceFile
from translate.storage.csvl10n import csvfile
from translate.storage.jsonl10n import (
I18NextV4File,
JsonFile,
JsonNestedFile,
I18NextV4File,
)
from translate.storage.mo import mofile
from translate.storage.po import pofile
Expand Down
22 changes: 22 additions & 0 deletions weblate/formats/tests/test_exporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
AndroidResourceExporter,
BaseExporter,
CSVExporter,
I18NextV4Exporter,
JSONExporter,
JSONNestedExporter,
MoExporter,
Expand All @@ -28,6 +29,7 @@
Unit,
)
from weblate.trans.tests.test_models import BaseTestCase
from weblate.trans.wrappers import exporter_custom_key_separator_wrapper
from weblate.utils.state import STATE_EMPTY, STATE_TRANSLATED


Expand Down Expand Up @@ -300,6 +302,26 @@ class JSONNestedExporterTest(JSONExporterTest):
_class = JSONNestedExporter


class I18NextExporterTest(JSONExporterTest):
_class = I18NextV4Exporter

def check_plurals(self, result) -> None:
self.assertIn(b"_one", result)
self.assertIn(b"_other", result)


class CustomKeySeparatorI18NextExporterTest(I18NextExporterTest):
_class = exporter_custom_key_separator_wrapper(I18NextExporterTest._class, ";")

def test_default_key_separator_skip(self) -> None:
output = self.check_unit(context="bar.foo", target="any")
self.assertIn(b"\"bar.foo\"", output)

def test_custom_key_separator(self) -> None:
output = self.check_unit(context="bar;foo", target="any")
self.assertNotIn(b"\"bar\"\n\t\t\"foo\"", output)


class StringsExporterTest(PoExporterTest):
_class = StringsExporter
_has_comments = False
Expand Down
21 changes: 21 additions & 0 deletions weblate/formats/tests/test_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
GoI18JSONFormat,
GoI18V2JSONFormat,
GWTFormat,
I18NextV4Format,
INIFormat,
InnoSetupINIFormat,
JoomlaFormat,
Expand Down Expand Up @@ -56,6 +57,7 @@
from weblate.lang.models import Language, Plural
from weblate.trans.tests.test_views import FixtureTestCase
from weblate.trans.tests.utils import TempDirMixin, get_test_file
from weblate.trans.wrappers import file_format_custom_key_separator_wrapper
from weblate.utils.state import STATE_FUZZY, STATE_TRANSLATED

TEST_PO = get_test_file("cs.po")
Expand All @@ -68,6 +70,7 @@
TEST_GO18N_V2_JSON = get_test_file("cs-go18n-v2.json")
TEST_NESTED_JSON = get_test_file("cs-nested.json")
TEST_WEBEXT_JSON = get_test_file("cs-webext.json")
TEST_I18NEXTV4_JSON = get_test_file("en.i18nextv4.json")
TEST_PHP = get_test_file("cs.php")
TEST_LARAVEL = get_test_file("laravel.php")
TEST_JOOMLA = get_test_file("cs.joomla.ini")
Expand Down Expand Up @@ -535,6 +538,24 @@ class WebExtesionJSONFormatTest(JSONFormatTest):
MONOLINGUAL = True


class I18NextV4JSONFormatTest(JSONFormatTest):
FORMAT = I18NextV4Format
FILE = TEST_I18NEXTV4_JSON
COUNT = 3
MASK = "i18nextv4/*.json"
EXPECTED_PATH = "i18nextv4/cs-CZ.json"
FIND_CONTEXT = "hello.world"
FIND_MATCH = "Hello"
NEW_UNIT_MATCH = b'\n "key": "Source string"\n'
MONOLINGUAL = True


class CustomKeySeparatorI18NextV4JSONFormatTest(I18NextV4JSONFormatTest):
FORMAT = file_format_custom_key_separator_wrapper(I18NextV4JSONFormatTest.FORMAT, ";")
FIND_CONTEXT = "hello;world"
FIND_MATCH = "Hello"


class GoI18NV1JSONFormatTest(JSONFormatTest):
FORMAT = GoI18JSONFormat
FILE = TEST_GO18N_V1_JSON
Expand Down
5 changes: 4 additions & 1 deletion weblate/formats/ttkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
)
from weblate.lang.data import FORMULA_WITH_ZERO, ZERO_PLURAL_TYPES
from weblate.lang.models import Plural
from weblate.trans.defines import DEFAULT_KEY_SEPARATOR
from weblate.trans.util import (
get_clean_env,
get_string,
Expand Down Expand Up @@ -927,10 +928,12 @@ def is_readonly(self) -> bool:


class JSONUnit(MonolingualSimpleUnit):
KEY_SEPARATOR = DEFAULT_KEY_SEPARATOR

@cached_property
def context(self):
context = super().context
if context.startswith("."):
if context.startswith(self.KEY_SEPARATOR):
return context[1:]
return context

Expand Down
1 change: 0 additions & 1 deletion weblate/templates/component.html
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@
{% if user_can_edit_component %}
<li><a href="{% url 'guide' path=object.get_url_path %}">{% trans "Community localization checklist" %}</a></li>
<li><a href="{% url 'addons' path=object.get_url_path %}">{% trans "Add-ons" %}</a></li>
<li><a href="{% url 'settings' path=object.get_url_path %}">{% trans "Settings" %}</a></li>
{% endif %}
{% if delete_form or rename_form or user_can_edit_component %}
<li role="separator" class="divider"></li>
Expand Down
3 changes: 3 additions & 0 deletions weblate/trans/defines.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@

# Maximal categories depth
CATEGORY_DEPTH = 3

# Default key separator for component
DEFAULT_KEY_SEPARATOR = "."
1 change: 1 addition & 0 deletions weblate/trans/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"commit_pending_age",
"edit_template",
"manage_units",
"key_separator",
"variant_regex",
"category_id",
)
Expand Down
10 changes: 10 additions & 0 deletions weblate/trans/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1489,6 +1489,7 @@ class Meta:
"auto_lock_error",
"links",
"manage_units",
"key_separator",
"is_glossary",
"glossary_color",
)
Expand Down Expand Up @@ -1548,6 +1549,7 @@ def __init__(self, request, *args, **kwargs) -> None:
gettext("Translation settings"),
"allow_translation_propagation",
"manage_units",
"key_separator",
"check_flags",
"variant_regex",
"enforced_checks",
Expand Down Expand Up @@ -1671,6 +1673,7 @@ class Meta:
"push_branch",
"repoweb",
"file_format",
"key_separator",
"filemask",
"template",
"edit_template",
Expand Down Expand Up @@ -1824,6 +1827,13 @@ class ComponentScratchCreateForm(ComponentProjectForm):
),
)

key_separator = forms.CharField(
label=Component.key_separator.field.verbose_name,
max_length=Component.key_separator.field.max_length,
help_text=Component.key_separator.field.help_text,
initial=Component.key_separator.field.default
)

def __init__(self, *args, **kwargs) -> None:
kwargs["auto_id"] = "id_scratchcreate_%s"
super().__init__(*args, **kwargs)
Expand Down
22 changes: 22 additions & 0 deletions weblate/trans/migrations/0015_component_key_separator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright © Michal Karol <m.karol@neurosys.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later

# Generated by Django 5.0.4 on 2024-04-05 10:23

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('trans', '0014_alter_component_repoweb_alter_project_web'),
]

operations = [
migrations.AddField(
model_name='component',
name='key_separator',
field=models.CharField(default='.', help_text='Customize key separator for JSON based formats.', max_length=1, verbose_name='Key separator'),
),
]
11 changes: 10 additions & 1 deletion weblate/trans/models/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from weblate.trans.defines import (
BRANCH_LENGTH,
COMPONENT_NAME_LENGTH,
DEFAULT_KEY_SEPARATOR,
FILENAME_LENGTH,
PROJECT_NAME_LENGTH,
REPO_LENGTH,
Expand Down Expand Up @@ -72,6 +73,7 @@
validate_filemask,
validate_language_code,
)
from weblate.trans.wrappers import file_format_custom_key_separator_wrapper
from weblate.utils import messages
from weblate.utils.celery import get_task_progress, is_task_ready
from weblate.utils.colors import COLOR_CHOICES
Expand Down Expand Up @@ -585,6 +587,12 @@ class Component(models.Model, PathMixin, CacheKeyMixin, ComponentCategoryMixin):
"probably want to keep it disabled."
),
)
key_separator = models.CharField(
verbose_name=gettext_lazy("Key separator"),
max_length=1,
default=DEFAULT_KEY_SEPARATOR,
help_text=gettext_lazy("Customize key separator for JSON based formats."),
)

# VCS config
merge_style = models.CharField(
Expand Down Expand Up @@ -1002,6 +1010,7 @@ def create_glossary(self) -> None:
has_template=False,
allow_translation_propagation=False,
license=self.license,
key_separator=self.key_separator,
)

@cached_property
Expand Down Expand Up @@ -3242,7 +3251,7 @@ def file_format_flags(self):
def file_format_cls(self):
"""Return file format object."""
if self._file_format is None or self._file_format.name != self.file_format:
self._file_format = FILE_FORMATS[self.file_format]
self._file_format = file_format_custom_key_separator_wrapper(FILE_FORMATS[self.file_format], self.key_separator)
return self._file_format

def has_template(self):
Expand Down
4 changes: 3 additions & 1 deletion weblate/trans/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from weblate.formats.models import FILE_FORMATS
from weblate.lang.models import Language
from weblate.memory.tasks import import_memory
from weblate.trans.defines import PROJECT_NAME_LENGTH
from weblate.trans.defines import DEFAULT_KEY_SEPARATOR, PROJECT_NAME_LENGTH
from weblate.trans.mixins import CacheKeyMixin, PathMixin
from weblate.utils.data import data_dir
from weblate.utils.site import get_site_url
Expand Down Expand Up @@ -552,6 +552,7 @@ def scratch_create_component(
file_format: str,
has_template: bool | None = None,
is_glossary: bool = False,
key_separator: str = DEFAULT_KEY_SEPARATOR,
**kwargs,
):
format_cls = FILE_FORMATS[file_format]
Expand All @@ -571,6 +572,7 @@ def scratch_create_component(
"source_language": source_language,
"manage_units": True,
"is_glossary": is_glossary,
"key_separator": key_separator
}
)
# Create component
Expand Down
4 changes: 2 additions & 2 deletions weblate/trans/models/translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1243,7 +1243,7 @@ def handle_upload( # noqa: C901
template_store = try_load(
fileobj.name,
filecopy,
component.file_format_cls,
component,
None,
is_template=True,
)
Expand All @@ -1256,7 +1256,7 @@ def handle_upload( # noqa: C901
store = try_load(
fileobj.name,
filecopy,
component.file_format_cls,
component,
template_store,
)
if isinstance(store, component.file_format_cls):
Expand Down
6 changes: 0 additions & 6 deletions weblate/trans/tests/data/en.i18next.json

This file was deleted.

10 changes: 10 additions & 0 deletions weblate/trans/tests/data/en.i18nextv4.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"main": {
"nested": "Test nested keys in i18next"
},
"plural_one": "Testing plural one",
"plural_other": "Testing plural other",
"hello": {
"world": "Hello"
}
}
1 change: 1 addition & 0 deletions weblate/trans/tests/test_categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ def test_create(self) -> None:
"language_regex": "^[^.]+$",
"source_language": get_default_lang(),
"category": category.pk,
"key_separator": "."
},
)
self.assertEqual(response.status_code, 302)
Expand Down

0 comments on commit 3fbb0fa

Please sign in to comment.