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