diff --git a/docs/changes.rst b/docs/changes.rst index 19198b0622ee..da54110b42f0 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -23,6 +23,7 @@ Not yet released. * :kbd:`?` now displays available :ref:`keyboard`. * Translation and language view in the project now include basic information about the language and plurals. * :ref:`bulk-edit` shows a preview of matched strings. +* :ref:`aresource` now supports translatable attribute in its strings. * Creating component via file upload (Translate document) now supports bilingual files. **Bug fixes** diff --git a/pyproject.toml b/pyproject.toml index c8b77ed766dc..0dda7b02653a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,7 @@ dependencies = [ "social-auth-app-django>=5.4.1,<6.0.0", "social-auth-core>=4.5.0,<5.0.0", "tesserocr>=2.6.1,<2.8.0", - "translate-toolkit>=3.13.1,<3.14", + "translate-toolkit>=3.13.4,<3.14", "translation-finder>=2.16,<3.0", "user-agents>=2.0,<2.3", "weblate-language-data>=2024.6", diff --git a/weblate/formats/ttkit.py b/weblate/formats/ttkit.py index a620e7710510..c0eed4d80491 100644 --- a/weblate/formats/ttkit.py +++ b/weblate/formats/ttkit.py @@ -56,6 +56,7 @@ STATE_APPROVED, STATE_EMPTY, STATE_FUZZY, + STATE_READONLY, STATE_TRANSLATED, ) @@ -1080,6 +1081,16 @@ def is_readonly(self) -> bool: return False +class AndroidUnit(MonolingualIDUnit): + """Wrapper unit for Android Resource.""" + + def set_state(self, state) -> None: + """Tag unit as translatable/readonly aside from fuzzy and approved flags.""" + super().set_state(state) + if state == STATE_READONLY: + self.unit.marktranslatable(False) + + class BasePoFormat(TTKitFormat): loader = pofile plural_preference = None @@ -1433,7 +1444,7 @@ class AndroidFormat(TTKitFormat): format_id = "aresource" loader = ("aresource", "AndroidResourceFile") monolingual = True - unit_class = MonolingualIDUnit + unit_class = AndroidUnit new_translation = '\n' autoload: tuple[str, ...] = ("strings*.xml", "values*.xml") language_format = "android" diff --git a/weblate/trans/models/translation.py b/weblate/trans/models/translation.py index b09e94086736..61dda519686e 100644 --- a/weblate/trans/models/translation.py +++ b/weblate/trans/models/translation.py @@ -1213,6 +1213,7 @@ def handle_add_upload( split_plural(unit.source), split_plural(unit.target) if not self.is_source else [], is_batch_update=True, + state=STATE_READONLY if unit.is_readonly() else None, ) existing.add(idkey) accepted += 1 diff --git a/weblate/trans/tests/data/strings-with-readonly.xml b/weblate/trans/tests/data/strings-with-readonly.xml new file mode 100644 index 000000000000..c61b776e8a61 --- /dev/null +++ b/weblate/trans/tests/data/strings-with-readonly.xml @@ -0,0 +1,6 @@ + + + String One + String Two + String Three + diff --git a/weblate/trans/tests/test_files.py b/weblate/trans/tests/test_files.py index fe4cee9d9cb2..7f73cf6ec027 100644 --- a/weblate/trans/tests/test_files.py +++ b/weblate/trans/tests/test_files.py @@ -16,6 +16,7 @@ from weblate.trans.models import Change, ComponentList from weblate.trans.tests.test_views import ViewTestCase from weblate.trans.tests.utils import get_test_file +from weblate.utils.state import STATE_READONLY TEST_PO = get_test_file("cs.po") TEST_CSV = get_test_file("cs.csv") @@ -29,6 +30,7 @@ TEST_MO = get_test_file("cs.mo") TEST_XLIFF = get_test_file("cs.poxliff") TEST_ANDROID = get_test_file("strings-cs.xml") +TEST_ANDROID_READONLY = get_test_file("strings-with-readonly.xml") TEST_XLSX = get_test_file("cs.xlsx") TEST_TBX = get_test_file("terms.tbx") @@ -380,6 +382,43 @@ def test_replace(self) -> None: translation.change_set.filter(action=Change.ACTION_REPLACE_UPLOAD).exists() ) + def test_readonly_upload_download(self) -> None: + """Test upload and download with a file containing a non-translatable string.""" + project = self.component.project + component = self.create_android(name="Component", project=project) + self.user.is_superuser = True + self.user.save() + with open(TEST_ANDROID_READONLY, "rb") as handle: + response = self.client.post( + reverse( + "upload", + kwargs={"path": component.source_translation.get_url_path()}, + ), + { + "file": handle, + "method": "replace", + "author_name": self.user.full_name, + "author_email": self.user.email, + }, + follow=True, + ) + messages = list(response.context["messages"]) + self.assertIn("updated: 3", messages[0].message) + unit = component.source_translation.unit_set.get(context="string_two") + self.assertEqual(unit.state, STATE_READONLY) + + response = self.client.get( + reverse( + "download", + kwargs={"path": component.source_translation.get_url_path()}, + ), + follow=True, + ) + self.assertIn( + 'name="string_two" translatable="false"', + response.getvalue().decode("utf-8"), + ) + class CSVImportTest(ViewTestCase): test_file = TEST_CSV