diff --git a/docs/content/en/latest/workspace-content/localization/_index.md b/docs/content/en/latest/workspace-content/localization/_index.md
new file mode 100644
index 000000000..3c1c3bc02
--- /dev/null
+++ b/docs/content/en/latest/workspace-content/localization/_index.md
@@ -0,0 +1,67 @@
+---
+title: "Metadata Localization"
+linkTitle: "Metadata Localization"
+weight: 20
+---
+
+Manage metadata localization for workspaces.
+
+## Methods
+
+* [get_metadata_localization](./get_metadata_localization/)
+* [set_metadata_localization](./set_metadata_localization/)
+* [clean_metadata_localization](./clean_metadata_localization/)
+* [add_metadata_locale](./add_metadata_locale/)
+* [save_metadata_locale_to_disk](./save_metadata_locale_to_disk/)
+* [set_metadata_locale_from_disk](./set_metadata_locale_from_disk/)
+
+## Example
+
+```python
+from gooddata_sdk import GoodDataSdk
+from pathlib import Path
+
+# GoodData base URL, e.g. "https://www.example.com"
+host = "https://www.example.com"
+# GoodData user token
+token = "some_user_token"
+sdk = GoodDataSdk.create(host, token)
+
+# Example usage for getting metadata localization
+localization = sdk.catalog_workspace.get_metadata_localization(
+ workspace_id="123",
+ target_language="de-DE"
+)
+
+# Example usage for setting metadata localization
+sdk.catalog_workspace.set_metadata_localization(
+ workspace_id="123",
+ encoded_xml=b"..."
+)
+
+# Example usage for cleaning metadata localization
+sdk.catalog_workspace.clean_metadata_localization(
+ workspace_id="123",
+ target_language="de-DE"
+)
+
+# Example usage for adding metadata locale
+sdk.catalog_workspace.add_metadata_locale(
+ workspace_id="123",
+ target_language="de-DE",
+ translator_func=my_translation_function,
+ set_locale=True
+)
+
+# Example usage for saving metadata locale to disk
+sdk.catalog_workspace.save_metadata_locale_to_disk(
+ workspace_id="123",
+ target_language="de-DE",
+ file_path=Path("/path/to/file.xliff")
+)
+
+# Example usage for setting metadata locale from disk
+sdk.catalog_workspace.set_metadata_locale_from_disk(
+ workspace_id="123",
+ file_path=Path("/path/to/file.xliff")
+)
diff --git a/docs/content/en/latest/workspace-content/localization/add_metadata_locale.md b/docs/content/en/latest/workspace-content/localization/add_metadata_locale.md
new file mode 100644
index 000000000..2709b8a0f
--- /dev/null
+++ b/docs/content/en/latest/workspace-content/localization/add_metadata_locale.md
@@ -0,0 +1,39 @@
+---
+title: "add_metadata_locale"
+linkTitle: "add_metadata_locale"
+weight: 25
+superheading: "catalog_workspace."
+---
+
+``add_metadata_locale(workspace_id: str, target_language: str, translator_func: Callable, set_locale: bool = True) -> None``
+
+Add and optionally set the metadata localization for a workspace in a target language.
+
+{{% parameters-block title="Parameters" %}}
+{{< parameter p_name="workspace_id" p_type="string" >}}
+The ID of the workspace.
+{{< /parameter >}}
+{{< parameter p_name="target_language" p_type="string" >}}
+The target language for the metadata localization.
+{{< /parameter >}}
+{{< parameter p_name="translator_func" p_type="Callable" >}}
+A function to translate the source text.
+{{< /parameter >}}
+{{< parameter p_name="set_locale" p_type="bool" >}}
+Flag to indicate if the locale settings should be updated in the workspace.
+{{< /parameter >}}
+{{% /parameters-block %}}
+
+{{% parameters-block title="Returns" None="yes" %}}
+{{% /parameters-block %}}
+
+## Example
+
+```python
+# Add and set the metadata localization for a workspace using a translation function.
+sdk.catalog_workspace.add_metadata_locale(
+ workspace_id="123",
+ target_language="de-DE",
+ translator_func=my_translation_function,
+ set_locale=True
+)
diff --git a/docs/content/en/latest/workspace-content/localization/clean_metadata_localization.md b/docs/content/en/latest/workspace-content/localization/clean_metadata_localization.md
new file mode 100644
index 000000000..caad71e87
--- /dev/null
+++ b/docs/content/en/latest/workspace-content/localization/clean_metadata_localization.md
@@ -0,0 +1,39 @@
+---
+title: "add_metadata_locale"
+linkTitle: "add_metadata_locale"
+weight: 55
+superheading: "catalog_workspace."
+---
+
+``add_metadata_locale(workspace_id: str, target_language: str, translator_func: Callable, set_locale: bool = True) -> None``
+
+Add and optionally set the metadata localization for a workspace in a target language.
+
+{{% parameters-block title="Parameters" %}}
+{{< parameter p_name="workspace_id" p_type="string" >}}
+The ID of the workspace.
+{{< /parameter >}}
+{{< parameter p_name="target_language" p_type="string" >}}
+The target language for the metadata localization.
+{{< /parameter >}}
+{{< parameter p_name="translator_func" p_type="Callable" >}}
+A function to translate the source text.
+{{< /parameter >}}
+{{< parameter p_name="set_locale" p_type="bool" >}}
+Flag to indicate if the locale settings should be updated in the workspace.
+{{< /parameter >}}
+{{% /parameters-block %}}
+
+{{% parameters-block title="Returns" None="yes" %}}
+{{% /parameters-block %}}
+
+## Example
+
+```python
+# Add and set the metadata localization for a workspace using a translation function.
+sdk.catalog_workspace.add_metadata_locale(
+ workspace_id="123",
+ target_language="de-DE",
+ translator_func=my_translation_function,
+ set_locale=True
+)
diff --git a/docs/content/en/latest/workspace-content/localization/get_metadata_localization.md b/docs/content/en/latest/workspace-content/localization/get_metadata_localization.md
new file mode 100644
index 000000000..c192d7ace
--- /dev/null
+++ b/docs/content/en/latest/workspace-content/localization/get_metadata_localization.md
@@ -0,0 +1,34 @@
+---
+title: "get_metadata_localization"
+linkTitle: "get_metadata_localization"
+weight: 52
+superheading: "catalog_workspace."
+---
+
+``get_metadata_localization(workspace_id: str, target_language: str) -> bytes``
+
+Retrieve the metadata localization for a workspace.
+
+{{% parameters-block title="Parameters" %}}
+{{< parameter p_name="workspace_id" p_type="string" >}}
+The ID of the workspace for which to retrieve the metadata localization.
+{{< /parameter >}}
+{{< parameter p_name="target_language" p_type="string" >}}
+The target language code for the localization.
+{{< /parameter >}}
+{{% /parameters-block %}}
+
+{{% parameters-block title="Returns" %}}
+{{< parameter p_type="bytes" >}}
+Object Containing declarative Analytical Model.
+{{< /parameter >}}The encoded metadata localization in the target language.
+{{% /parameters-block %}}
+
+## Example
+
+```python
+# Retrieve metadata localization for a workspace in the specified language.
+localization = sdk.catalog_workspace.get_metadata_localization(
+ workspace_id="123",
+ target_language="de-DE"
+)
diff --git a/docs/content/en/latest/workspace-content/localization/save_metadata_locale_to_disk.md b/docs/content/en/latest/workspace-content/localization/save_metadata_locale_to_disk.md
new file mode 100644
index 000000000..4f878e0bc
--- /dev/null
+++ b/docs/content/en/latest/workspace-content/localization/save_metadata_locale_to_disk.md
@@ -0,0 +1,35 @@
+---
+title: "save_metadata_locale_to_disk"
+linkTitle: "save_metadata_locale_to_disk"
+weight: 56
+superheading: "catalog_workspace."
+---
+
+``save_metadata_locale_to_disk(workspace_id: str, target_language: str, file_path: Path) -> None``
+
+Save the metadata localization for a workspace to a file.
+
+{{% parameters-block title="Parameters" %}}
+{{< parameter p_name="workspace_id" p_type="string" >}}
+The ID of the workspace.
+{{< /parameter >}}
+{{< parameter p_name="target_language" p_type="string" >}}
+The target language for the metadata localization.
+{{< /parameter >}}
+{{< parameter p_name="file_path" p_type="Path" >}}
+The path to the file where the XLIFF content will be saved.
+{{< /parameter >}}
+{{% /parameters-block %}}
+
+{{% parameters-block title="Returns" None="yes" %}}
+{{% /parameters-block %}}
+
+## Example
+
+```python
+# Save the metadata localization for a workspace to a file.
+sdk.catalog_workspace.save_metadata_locale_to_disk(
+ workspace_id="123",
+ target_language="de-DE",
+ file_path=Path("/path/to/file.xliff")
+)
diff --git a/docs/content/en/latest/workspace-content/localization/set_metadata_locale_from_disk.md b/docs/content/en/latest/workspace-content/localization/set_metadata_locale_from_disk.md
new file mode 100644
index 000000000..514d29009
--- /dev/null
+++ b/docs/content/en/latest/workspace-content/localization/set_metadata_locale_from_disk.md
@@ -0,0 +1,33 @@
+---
+title: "set_metadata_locale_from_disk"
+linkTitle: "set_metadata_locale_from_disk"
+weight: 57
+superheading: "catalog_workspace."
+---
+
+``set_metadata_locale_from_disk(workspace_id: str, file_path: Path) -> None``
+
+Load and set the metadata localization for a workspace from a file.
+
+{{% parameters-block title="Parameters" %}}
+{{< parameter p_name="workspace_id" p_type="string" >}}
+The ID of the workspace to which the metadata localization applies.
+{{< /parameter >}}
+{{< parameter p_name="file_path" p_type="Path" >}}
+The path to the file containing the encoded XML metadata.
+{{< /parameter >}}
+{{% /parameters-block %}}
+
+{{% parameters-block title="Returns" None="yes" %}}
+{{% /parameters-block %}}
+
+## Example
+
+```python
+# Load and set the metadata localization for a workspace from a file.
+from pathlib import Path
+
+sdk.catalog_workspace.set_metadata_locale_from_disk(
+ workspace_id="123",
+ file_path=Path("/path/to/file.xliff")
+)
diff --git a/docs/content/en/latest/workspace-content/localization/set_metadata_localization.md b/docs/content/en/latest/workspace-content/localization/set_metadata_localization.md
new file mode 100644
index 000000000..7260b85cc
--- /dev/null
+++ b/docs/content/en/latest/workspace-content/localization/set_metadata_localization.md
@@ -0,0 +1,31 @@
+---
+title: "set_metadata_localization"
+linkTitle: "set_metadata_localization"
+weight: 53
+superheading: "catalog_workspace."
+---
+
+``set_metadata_localization(workspace_id: str, encoded_xml: bytes) -> None``
+
+Set the metadata localization for a workspace.
+
+{{% parameters-block title="Parameters" %}}
+{{< parameter p_name="workspace_id" p_type="string" >}}
+The ID of the workspace to which the metadata localization applies.
+{{< /parameter >}}
+{{< parameter p_name="encoded_xml" p_type="bytes" >}}
+The encoded XML metadata to be set.
+{{< /parameter >}}
+{{% /parameters-block %}}
+
+{{% parameters-block title="Returns" None="yes" %}}
+{{% /parameters-block %}}
+
+## Example
+
+```python
+# Set the metadata localization for a workspace using encoded XML.
+sdk.catalog_workspace.set_metadata_localization(
+ workspace_id="123",
+ encoded_xml=b"..."
+)
diff --git a/gooddata-sdk/gooddata_sdk/catalog/workspace/service.py b/gooddata-sdk/gooddata_sdk/catalog/workspace/service.py
index 82175dc27..b4f5e81cd 100644
--- a/gooddata-sdk/gooddata_sdk/catalog/workspace/service.py
+++ b/gooddata-sdk/gooddata_sdk/catalog/workspace/service.py
@@ -10,8 +10,10 @@
from pathlib import Path
from time import time
from typing import Any, Callable, Dict, List, Optional, Set
+from xml.etree import ElementTree as ET
import attrs
+from gooddata_api_client.api.translations_api import LocaleRequest
from gooddata_api_client.exceptions import NotFoundException
from gooddata_api_client.model.resolve_settings_request import ResolveSettingsRequest
@@ -32,7 +34,9 @@
from gooddata_sdk.catalog.workspace.entity_model.workspace import CatalogWorkspace
from gooddata_sdk.client import GoodDataApiClient
from gooddata_sdk.utils import (
+ HttpMethod,
create_directory,
+ get_namespace_from_xliff,
load_all_entities,
load_all_entities_dict,
read_layout_from_file,
@@ -806,6 +810,182 @@ def set_translated_texts(
if "description" in section["header"]:
section["header"]["description"] = translated.get(section["header"]["description"])
+ @staticmethod
+ def _add_target_tags(xliff_content: str, translate_func: Callable) -> bytes:
+ """Add target tags to the XLIFF content for translation purposes.
+
+ Args:
+ xliff_content (str): The XLIFF content as a string.
+ translate_func (Optional[Callable]):
+ A function that translates the source text. It can take an optional argument
+ `already_translated` and `old_translation` for updating existing translations.
+
+ Returns:
+ bytes: The modified XLIFF content with target tags, encoded as UTF-8.
+ """
+ namespace = get_namespace_from_xliff(xliff_content)
+
+ ET.register_namespace("", namespace["ns"])
+ tree = ET.ElementTree(ET.fromstring(xliff_content))
+ root = tree.getroot()
+
+ # Segment is always parent of source/target - no need to find parents
+ for segment in root.findall(".//ns:segment", namespaces=namespace):
+ source = segment.find("ns:source", namespaces=namespace)
+ if source is not None and "".join(source.itertext()).strip():
+ to_translate = "".join(source.itertext()).strip()
+ target = segment.find("ns:target", namespaces=namespace)
+ if target is None:
+ target = ET.Element("target")
+ segment.append(target)
+ if not target.text or not target.text.strip():
+ target.text = translate_func(to_translate)
+ else:
+ old_translation = "".join(target.itertext()).strip()
+ target.text = translate_func(
+ to_translate=to_translate,
+ already_translated=True,
+ old_translation=old_translation,
+ )
+
+ return ET.tostring(root, encoding="utf-8", xml_declaration=True)
+
+ def get_metadata_localization(
+ self,
+ workspace_id: str,
+ target_language: str,
+ ) -> bytes:
+ """Retrieve the metadata localization for a workspace.
+
+ Args:
+ workspace_id (str): The ID of the workspace for which to retrieve the metadata localization.
+ target_language (str): The target language code for the localization.
+
+ Returns:
+ bytes: The encoded metadata localization in the target language.
+ """
+ ans = self._actions_api.retrieve_translations(
+ workspace_id=workspace_id,
+ locale_request=LocaleRequest(locale=target_language),
+ _preload_content=False,
+ )
+ return ans.data
+
+ def set_metadata_localization(
+ self,
+ workspace_id: str,
+ encoded_xml: bytes,
+ ) -> None:
+ """Set the metadata localization for a workspace.
+
+ Args:
+ workspace_id (str): The ID of the workspace to which the metadata localization applies.
+ encoded_xml (bytes): The encoded XML metadata to be set.
+
+ Returns:
+ None
+ """
+ self._client.do_request(
+ method=HttpMethod.POST,
+ endpoint=f"api/v1/actions/workspaces/{workspace_id}/translations/set",
+ content_type="application/xml",
+ data=encoded_xml,
+ )
+
+ def clean_metadata_localization(
+ self,
+ workspace_id: str,
+ target_language: str,
+ ) -> None:
+ """Clean the metadata localization for a workspace.
+
+ Args:
+ workspace_id (str): The ID of the workspace for which to clean the metadata localization.
+ target_language (str): The target language code for the localization to be cleaned.
+
+ Returns:
+ None
+ """
+ self._client.actions_api.clean_translations(
+ workspace_id=workspace_id, locale_request=LocaleRequest(target_language)
+ )
+
+ def add_metadata_locale(
+ self,
+ workspace_id: str,
+ target_language: str,
+ translator_func: Callable,
+ set_locale: bool = True,
+ ) -> None:
+ """Add and optionally set the metadata localization for a workspace in a target language.
+
+ Args:
+ workspace_id (str): The ID of the workspace.
+ target_language (str): The target language for the metadata localization.
+ translator_func (Optional[Callable]): A function to translate the source text.
+ set_locale (bool): Flag to indicate if the locale settings should be updated in the workspace.
+
+ Returns:
+ None
+ """
+ ans = self._actions_api.retrieve_translations(
+ workspace_id=workspace_id,
+ locale_request=LocaleRequest(locale=target_language),
+ _preload_content=False,
+ )
+
+ encoded_xml = self._add_target_tags(ans.data.decode(), translator_func)
+
+ self.set_metadata_localization(workspace_id=workspace_id, encoded_xml=encoded_xml)
+ if set_locale:
+ metadata_locale = "METADATA_LOCALE"
+ locale = "LOCALE"
+
+ self.create_or_update_workspace_setting(
+ workspace_id,
+ CatalogWorkspaceSetting(
+ id=metadata_locale, setting_type=metadata_locale, content={"value": target_language}
+ ),
+ )
+ self.create_or_update_workspace_setting(
+ workspace_id,
+ CatalogWorkspaceSetting(id=locale, setting_type=locale, content={"value": target_language}),
+ )
+
+ def save_metadata_locale_to_disk(self, workspace_id: str, target_language: str, file_path: Path) -> None:
+ """Save the metadata localization for a workspace to a file.
+
+ Args:
+ workspace_id (str): The ID of the workspace.
+ target_language (str): The target language for the metadata localization.
+ file_path (Path): The path to the file where the XLIFF content will be saved.
+
+ Returns:
+ None
+ """
+ xliff_content = self.get_metadata_localization(workspace_id, target_language)
+
+ ns = get_namespace_from_xliff(xliff_content.decode())
+
+ ET.register_namespace("", ns["ns"])
+ tree = ET.ElementTree(ET.fromstring(xliff_content))
+
+ tree.write(file_path, "utf-8")
+
+ def set_metadata_locale_from_disk(self, workspace_id: str, file_path: Path) -> None:
+ """Load and set the metadata localization for a workspace from a file.
+
+ Args:
+ workspace_id (str): The ID of the workspace to which the metadata localization applies.
+ file_path (Path): The path to the file containing the encoded XML metadata.
+
+ Returns:
+ None
+ """
+ with open(file_path, "rb") as f:
+ encoded_xml = f.read()
+ self.set_metadata_localization(workspace_id=workspace_id, encoded_xml=encoded_xml)
+
# Declarative methods - workspace data filters
def get_declarative_workspace_data_filters(self) -> CatalogDeclarativeWorkspaceDataFilters:
diff --git a/gooddata-sdk/gooddata_sdk/client.py b/gooddata-sdk/gooddata_sdk/client.py
index 617898c0b..d1eae14a4 100644
--- a/gooddata-sdk/gooddata_sdk/client.py
+++ b/gooddata-sdk/gooddata_sdk/client.py
@@ -3,12 +3,15 @@
from __future__ import annotations
+from builtins import bytes
from typing import Optional
import gooddata_api_client as api_client
+import requests
from gooddata_api_client import apis
from gooddata_sdk import __version__
+from gooddata_sdk.utils import HttpMethod
USER_AGENT = f"gooddata-python-sdk/{__version__}"
@@ -56,6 +59,62 @@ def __init__(
self._actions_api = apis.ActionsApi(self._api_client)
self._user_management_api = apis.UserManagementApi(self._api_client)
+ def _do_post_request(
+ self,
+ data: bytes,
+ endpoint: str,
+ content_type: str,
+ ) -> requests.Response:
+ """Perform a POST request to a specified endpoint.
+
+ Args:
+ data (bytes): The data to be sent in the POST request.
+ endpoint (str): The endpoint URL to which the request is made.
+ content_type (str): The content type of the data being sent.
+
+ Returns:
+ None
+ """
+ if not self._hostname.endswith("/"):
+ endpoint = f"/{endpoint}"
+
+ response = requests.post(
+ url=f"{self._hostname}{endpoint}",
+ headers={
+ "Content-Type": content_type,
+ "Authorization": f"Bearer {self._token}",
+ },
+ data=data,
+ )
+
+ return response
+
+ def do_request(
+ self,
+ data: bytes,
+ endpoint: str,
+ content_type: str,
+ method: HttpMethod,
+ ) -> requests.Response:
+ """Perform an HTTP request using the specified method.
+
+ Args:
+ data (bytes): The data to be sent in the request.
+ endpoint (str): The endpoint URL to which the request is made.
+ content_type (str): The content type of the data being sent.
+ method (HttpMethod): The HTTP method to be used for the request.
+
+ Returns:
+ None
+
+ Raises:
+ NotImplementedError: If the specified HTTP method is not supported.
+ """
+ if method == HttpMethod.POST:
+ self._do_post_request(data, endpoint, content_type)
+ else:
+ raise NotImplementedError("Currently only supports the POST method.")
+
@staticmethod
def _set_default_headers(headers: dict) -> None:
headers["X-Requested-With"] = "XMLHttpRequest"
diff --git a/gooddata-sdk/gooddata_sdk/utils.py b/gooddata-sdk/gooddata_sdk/utils.py
index 03923d08d..d0b6eab08 100644
--- a/gooddata-sdk/gooddata_sdk/utils.py
+++ b/gooddata-sdk/gooddata_sdk/utils.py
@@ -5,9 +5,11 @@
import os
import re
from collections.abc import KeysView
+from enum import Enum, auto
from pathlib import Path
from shutil import rmtree
from typing import Any, Callable, Dict, List, NamedTuple, Tuple, Union, cast, no_type_check
+from xml.etree import ElementTree as ET
import yaml
from gooddata_api_client import ApiAttributeError
@@ -25,6 +27,16 @@
SDK_PROFILE_KEYS = SDK_PROFILE_MANDATORY_KEYS + ["custom_headers", "extra_user_agent"]
+class HttpMethod(Enum):
+ """Enum representing HTTP methods."""
+
+ GET = auto()
+ POST = auto()
+ PUT = auto()
+ DELETE = auto()
+ PATCH = auto()
+
+
def id_obj_to_key(id_obj: IdObjType) -> str:
"""
Given an object containing an id+type pair, this function will return a string key.
@@ -306,3 +318,17 @@ def safeget_list(var: Any, path: List[str]) -> List[Any]:
return []
else:
raise Exception("safeget_list: result is not iterable! result={0}".format(result))
+
+
+def get_namespace_from_xliff(xliff_content: str) -> Dict:
+ """Extract the XML namespace from the given XLIFF content.
+
+ Args:
+ xliff_content (str): The XLIFF content as bytes.
+
+ Returns:
+ dict: A dictionary containing the namespace with the key 'ns'.
+ """
+ tree = ET.ElementTree(ET.fromstring(xliff_content))
+ root = tree.getroot()
+ return {"ns": root.tag.split("}")[0].strip("{")}
diff --git a/gooddata-sdk/mypy.ini b/gooddata-sdk/mypy.ini
index 60d2a0406..1ff031b52 100644
--- a/gooddata-sdk/mypy.ini
+++ b/gooddata-sdk/mypy.ini
@@ -11,3 +11,6 @@ ignore_missing_imports = True
[mypy-urllib3.*]
ignore_missing_imports = True
+
+[mypy-requests.*]
+ignore_missing_imports = True
diff --git a/gooddata-sdk/requirements.txt b/gooddata-sdk/requirements.txt
index e4bb49f1b..8c43c3b1b 100644
--- a/gooddata-sdk/requirements.txt
+++ b/gooddata-sdk/requirements.txt
@@ -3,3 +3,4 @@ pyyaml>=5.1
attrs>=21.4.0,<=23.2.0
cattrs>=22.1.0,<=23.2.3
brotli==1.1.0
+requests~=2.31.0
diff --git a/gooddata-sdk/setup.py b/gooddata-sdk/setup.py
index 25d9d575b..7b970d010 100644
--- a/gooddata-sdk/setup.py
+++ b/gooddata-sdk/setup.py
@@ -13,6 +13,7 @@
"attrs>=21.4.0,<=23.2.0",
"cattrs>=22.1.0,<=23.2.3",
"brotli==1.1.0",
+ "requests~=2.31.0",
]
setup(