Repository: WeblateOrg/weblate

Issue:
Ability to update the source information labels via the API
**Is your feature request related to a problem? If so, please describe.**

We are sharing the same dictionary source for 3 different applications. We would like to easily distinguish in Weblate what application(s) a key appears in. Labels would be perfect but I haven't seen any API documentation regarding them. Am I missing something?

**Describe the solution you'd like**

Have the ability to update the source information labels via the API.



Discussion:
This issue has been added to the backlog. It is not scheduled on the Weblate roadmap, but it eventually might be implemented. In case you need this feature soon, please consider helping or push it by [funding the development](https://weblate.org/support/).
I'm asking this because we intend to translate only 1 of the 3 UIs our customers have access to for the moment. We will eventually translate the others.

Since a lot of keys are shared among the UIs, we decided when designing our i18n to have a single translation component in Weblate to maintain instead of 3. 

I've noticed that the following paths `[PUT|PATCH] api/units/:pk/` supports updating the `explanation` field, but then I can't do anything since both the Bulk Edit addon and the Search feature returns me this errors message when trying to search in the `explanation` field: 

```
Error in parameter q: Failed to parse query string: Unsupported field: explanation
```

@nijel sorry to bother you but, do I absolutely need to split my component into 3 distinct components (one per UI) or there's a workaround to this? 

Thanks in advance, Weblate is an amazing tool.
I've added searching for explanation in f4a2d00be2c25f7c427811f319ec2964bd024943.

Please keep the issue single-topic next time, requesting another features inside the existing ticket will make it hard to track in case it would not be implemented immediately. 
I second the request for label API.

Resolving patch:
diff --git a/weblate/api/serializers.py b/weblate/api/serializers.py
--- a/weblate/api/serializers.py
+++ b/weblate/api/serializers.py
@@ -35,6 +35,7 @@
     Change,
     Component,
     ComponentList,
+    Label,
     Project,
     Translation,
     Unit,
@@ -932,6 +933,35 @@ class Meta:
         )
 
 
+class LabelsSerializer(serializers.RelatedField):
+    class Meta:
+        model = Label
+
+    def get_queryset(self):
+        """
+        List of available labels for an unit.
+
+        The queryset argument is only ever required for writable relationship field,
+        in which case it is used for performing the model instance lookup, that maps
+        from the primitive user input, into a model instance.
+        """
+        unit = self.parent.parent.instance
+        project = unit.translation.component.project
+        return project.label_set.all()
+
+    def to_representation(self, value):
+        return value.name
+
+    def to_internal_value(self, data):
+        try:
+            label = self.get_queryset().get(name=data)
+        except Label.DoesNotExist as err:
+            raise serializers.ValidationError(
+                "Label with this name was not found."
+            ) from err
+        return label
+
+
 class UnitSerializer(serializers.ModelSerializer):
     web_url = AbsoluteURLField(source="get_absolute_url", read_only=True)
     translation = MultiFieldHyperlinkedIdentityField(
@@ -950,6 +980,7 @@ class UnitSerializer(serializers.ModelSerializer):
     target = PluralField()
     timestamp = serializers.DateTimeField(read_only=True)
     pending = serializers.BooleanField(read_only=True)
+    labels = LabelsSerializer(many=True, read_only=True)
 
     class Meta:
         model = Unit
@@ -964,6 +995,7 @@ class Meta:
             "context",
             "note",
             "flags",
+            "labels",
             "state",
             "fuzzy",
             "translated",
@@ -990,6 +1022,7 @@ class UnitWriteSerializer(serializers.ModelSerializer):
     """Serializer for updating source unit."""
 
     target = PluralField()
+    labels = LabelsSerializer(many=True)
 
     class Meta:
         model = Unit
@@ -998,6 +1031,7 @@ class Meta:
             "state",
             "explanation",
             "extra_flags",
+            "labels",
         )
 
     def to_internal_value(self, data):
diff --git a/weblate/api/views.py b/weblate/api/views.py
--- a/weblate/api/views.py
+++ b/weblate/api/views.py
@@ -1254,7 +1254,7 @@ def filter_queryset(self, queryset):
     def perform_update(self, serializer):
         data = serializer.validated_data
         do_translate = "target" in data or "state" in data
-        do_source = "extra_flags" in data or "explanation" in data
+        do_source = "extra_flags" in data or "explanation" in data or "labels" in data
         unit = serializer.instance
         translation = unit.translation
         request = self.request
@@ -1315,6 +1315,8 @@ def perform_update(self, serializer):
                     setattr(unit, name, data[name])
                 except KeyError:
                     continue
+            if "labels" in data:
+                unit.labels.set(data["labels"])
             unit.save(update_fields=fields)
 
         # Handle translate


Test patch:
diff --git a/weblate/api/tests.py b/weblate/api/tests.py
--- a/weblate/api/tests.py
+++ b/weblate/api/tests.py
@@ -2976,6 +2976,69 @@ def test_unit_flags(self):
         self.assertEqual(unit.all_flags.format(), "c-format, ignore-same")
         self.assertEqual(unit.all_checks_names, set())
 
+    def test_unit_labels(self):
+        other_project = Project.objects.create(
+            name="OtherProject",
+            slug="other-project",
+            access_control=Project.ACCESS_PRIVATE,
+        )
+
+        label1 = self.component.project.label_set.create(name="test", color="navy")
+        label2 = other_project.label_set.create(name="test_2", color="navy")
+
+        unit = Unit.objects.get(
+            translation__language_code="cs", source="Hello, world!\n"
+        )
+        unit.translate(self.user, "Hello, world!\n", STATE_TRANSLATED)
+
+        # Edit on translation will fail
+        self.do_request(
+            "api:unit-detail",
+            kwargs={"pk": unit.pk},
+            method="patch",
+            code=403,
+            superuser=True,
+            request={"labels": "test"},
+        )
+
+        # Edit on source will fail when label doesn't exist
+        # or is not in the same project
+        self.do_request(
+            "api:unit-detail",
+            kwargs={"pk": unit.pk},
+            method="patch",
+            code=400,
+            superuser=True,
+            request={"labels": "foo"},
+        )
+        self.do_request(
+            "api:unit-detail",
+            kwargs={"pk": unit.pk},
+            method="patch",
+            code=400,
+            superuser=True,
+            request={"labels": "test_2"},
+        )
+
+        # Edit on source will work when label exists
+        self.do_request(
+            "api:unit-detail",
+            kwargs={"pk": unit.source_unit.pk},
+            method="patch",
+            code=200,
+            superuser=True,
+            request={"labels": "test"},
+        )
+
+        # Label should be now updated
+        unit = Unit.objects.get(pk=unit.id)
+        self.assertEqual(len(unit.all_labels), 1)
+        self.assertEqual(unit.all_labels[0].name, "test")
+
+        label1.delete()
+        label2.delete()
+        other_project.delete()
+
     def test_translate_plural_unit(self):
         unit = Unit.objects.get(
             translation__language_code="cs", source__startswith="Orangutan has "