Skip to content

Commit

Permalink
Merge pull request #3526 from open-formulieren/feature/2958-component…
Browse files Browse the repository at this point in the history
…-level-translations

Component level translations
  • Loading branch information
sergei-maertens authored Oct 6, 2023
2 parents a1e5b11 + 227d46a commit 227bb54
Show file tree
Hide file tree
Showing 10 changed files with 1,152 additions and 11 deletions.
18 changes: 18 additions & 0 deletions src/openforms/formio/components/translations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from ..typing import OptionDict


def translate_options(
options: list[OptionDict],
language_code: str,
enabled: bool,
) -> None:
for option in options:
if not (translations := option.get("openForms", {}).get("translations")):
continue

translated_label = translations.get(language_code, {}).get("label", "")
if enabled and translated_label:
option["label"] = translated_label

# always clean up
del option["openForms"]["translations"] # type: ignore
39 changes: 33 additions & 6 deletions src/openforms/formio/components/vanilla.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@
TimeFormatter,
)
from ..registry import BasePlugin, register
from ..typing import Component, ContentComponent, FileComponent, RadioComponent
from ..typing import (
ContentComponent,
FileComponent,
RadioComponent,
SelectBoxesComponent,
SelectComponent,
)
from .translations import translate_options

if TYPE_CHECKING: # pragma: nocover
from openforms.submissions.models import Submission
Expand Down Expand Up @@ -115,21 +122,31 @@ class Checkbox(BasePlugin):


@register("selectboxes")
class SelectBoxes(BasePlugin):
class SelectBoxes(BasePlugin[SelectBoxesComponent]):
formatter = SelectBoxesFormatter

def mutate_config_dynamically(
self, component: Component, submission: "Submission", data: DataMapping
self,
component: SelectBoxesComponent,
submission: "Submission",
data: DataMapping,
) -> None:
add_options_to_config(component, data, submission)

def localize(
self, component: SelectBoxesComponent, language_code: str, enabled: bool
):
if not (options := component.get("values", [])):
return
translate_options(options, language_code, enabled)


@register("select")
class Select(BasePlugin):
class Select(BasePlugin[SelectComponent]):
formatter = SelectFormatter

def mutate_config_dynamically(
self, component: Component, submission: "Submission", data: DataMapping
self, component, submission: "Submission", data: DataMapping
) -> None:
add_options_to_config(
component,
Expand All @@ -138,21 +155,31 @@ def mutate_config_dynamically(
options_path="data.values",
)

def localize(self, component: SelectComponent, language_code: str, enabled: bool):
if not (options := component.get("data", {}).get("values", [])):
return
translate_options(options, language_code, enabled)


@register("currency")
class Currency(BasePlugin):
formatter = CurrencyFormatter


@register("radio")
class Radio(BasePlugin):
class Radio(BasePlugin[RadioComponent]):
formatter = RadioFormatter

def mutate_config_dynamically(
self, component: RadioComponent, submission: "Submission", data: DataMapping
) -> None:
add_options_to_config(component, data, submission)

def localize(self, component: SelectComponent, language_code: str, enabled: bool):
if not (options := component.get("values", [])):
return
translate_options(options, language_code, enabled)


@register("signature")
class Signature(BasePlugin):
Expand Down
16 changes: 16 additions & 0 deletions src/openforms/formio/dynamic_config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,19 @@ def get_translated_custom_error_messages(
component["errors"] = custom_error_messages[language]

return config_wrapper


def localize_components(
configuration_wrapper: FormioConfigurationWrapper,
language_code: str,
enabled: bool = True,
) -> None:
"""
Apply the configured translations for each component in the configuration.
.. note:: this function mutates the configuration.
"""
for component in configuration_wrapper:
register.localize_component(
component, language_code=language_code, enabled=enabled
)
45 changes: 42 additions & 3 deletions src/openforms/formio/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
smeared out across the codebase in similar but different implementations, while making
the public API better defined and smaller.
"""
from typing import TYPE_CHECKING, Any, Protocol
from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar

from django.utils.translation import gettext as _

Expand Down Expand Up @@ -45,7 +45,10 @@ def __call__(self, component: Component, request: Request) -> None:
...


class BasePlugin(AbstractBasePlugin):
T = TypeVar("T", bound=Component)


class BasePlugin(Generic[T], AbstractBasePlugin):
"""
Base class for Formio component plugins.
"""
Expand Down Expand Up @@ -73,10 +76,13 @@ def verbose_name(self):
return _("{type} component").format(type=self.identifier.capitalize())

def mutate_config_dynamically(
self, component: Component, submission: "Submission", data: DataMapping
self, component: T, submission: "Submission", data: DataMapping
) -> None: # pragma: nocover
...

def localize(self, component: T, language_code: str, enabled: bool):
pass # noop by default, specific component types can extend the base behaviour


class ComponentRegistry(BaseRegistry[BasePlugin]):
module = "formio_components"
Expand All @@ -85,6 +91,7 @@ def normalize(self, component: Component, value: Any) -> Any:
"""
Given a value from any source, normalize it according to the component rules.
"""
assert "type" in component
if (component_type := component["type"]) not in self:
return value
normalizer = self[component_type].normalizer
Expand All @@ -100,6 +107,7 @@ def format(self, component: Component, value: Any, as_html=False) -> str:
for the given component type, as it makes the best sense for that component
type.
"""
assert "type" in component
if (component_type := component["type"]) not in self:
component_type = "default"

Expand All @@ -120,6 +128,7 @@ def update_config(
for example) to work.
"""
# if there is no plugin registered for the component, return the input
assert "type" in component
if (component_type := component["type"]) not in self:
return

Expand All @@ -132,6 +141,7 @@ def update_config_for_request(self, component: Component, request: Request) -> N
Mutate the component in place for the given request context.
"""
# if there is no plugin registered for the component, return the input
assert "type" in component
if (component_type := component["type"]) not in self:
return

Expand All @@ -142,6 +152,35 @@ def update_config_for_request(self, component: Component, request: Request) -> N

rewriter(component, request)

def localize_component(
self, component: Component, language_code: str, enabled: bool
) -> None:
"""
Apply component translations for the provided language code.
:arg component: Form.io component definition to localize
:arg language_code: the language code of the language to translate to
:arg enabled: whether translations are enabled or not. If translations are not
enabled, the translation information should still be stripped from the
component definition(s).
"""
assert "type" in component
generic_translations = component.get("openForms", {}).get("translations", {})
# apply the generic translation behaviour even for unregistered components
if enabled and (translations := generic_translations.get(language_code, {})):
for prop, translation in translations.items():
if not translation:
continue
component[prop] = translation

if (component_type := component["type"]) in self:
component_plugin = self[component_type]
component_plugin.localize(component, language_code, enabled=enabled)

# always drop translation meta information
if generic_translations:
del component["openForms"]["translations"] # type: ignore


# Sentinel to provide the default registry. You can easily instantiate another
# :class:`Registry` object to use as dependency injection in tests.
Expand Down
6 changes: 6 additions & 0 deletions src/openforms/formio/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .datastructures import FormioConfigurationWrapper, FormioData
from .dynamic_config import (
get_translated_custom_error_messages,
localize_components,
rewrite_formio_components,
rewrite_formio_components_for_request,
)
Expand Down Expand Up @@ -88,6 +89,11 @@ def get_dynamic_configuration(

# Add to each component the custom errors in the current locale
get_translated_custom_error_messages(config_wrapper, submission)
localize_components(
config_wrapper,
submission.language_code,
enabled=submission.form.translation_enabled,
)

# prefill is still 'special' even though it uses variables, as we specifically
# set the `defaultValue` key to the resulting variable.
Expand Down
Loading

0 comments on commit 227bb54

Please sign in to comment.