diff --git a/src/openforms/formio/rendering/default.py b/src/openforms/formio/rendering/default.py index 8a77011666..1bfead46d9 100644 --- a/src/openforms/formio/rendering/default.py +++ b/src/openforms/formio/rendering/default.py @@ -311,7 +311,6 @@ def get_children(self) -> Iterator["ComponentNode"]: def __post_init__(self): self._base_label = self.component.get("groupLabel", self.default_label) - super().__post_init__() def apply_to_labels(self, f: Callable[[str], str]) -> None: super().apply_to_labels(f) diff --git a/src/openforms/formio/rendering/nodes.py b/src/openforms/formio/rendering/nodes.py index 3bbcde06f6..ce1821b76e 100644 --- a/src/openforms/formio/rendering/nodes.py +++ b/src/openforms/formio/rendering/nodes.py @@ -9,7 +9,7 @@ from openforms.submissions.rendering.constants import RenderModes from openforms.typing import DataMapping -from ..service import format_value, translate_function +from ..service import format_value from ..typing import Component from ..utils import ( is_layout_component, @@ -50,7 +50,6 @@ class ComponentNode(Node): "" # Path in the configuration tree, matching the path obtained with openforms/formio/utils.py `flatten_by_path` ) parent_node: Node | None = None - translate: Callable[[str], str] | None = None @staticmethod def build_node( @@ -62,7 +61,6 @@ def build_node( configuration_path: str = "", depth: int = 0, parent_node: Node | None = None, - translate: Callable[[str], str] | None = None, ) -> "ComponentNode": """ Instantiate the most specific node type for a given component type. @@ -80,17 +78,9 @@ def build_node( json_renderer_path=json_renderer_path, configuration_path=configuration_path, parent_node=parent_node, - translate=translate, ) return nested_node - def __post_init__(self): - # Value Formatters have no access to the translations; run all our - # labels through the translations, before further processing of logic - # etc. - if self.translate: - self.apply_to_labels(self.translate) - @property def is_visible(self) -> bool: """ @@ -298,18 +288,6 @@ def get_children(self) -> Iterator[ComponentNode]: assert self.step.form_step # nosec: intended use of B101 configuration = self.step.form_step.form_definition.configuration - # prepare for any potential translations to component definitions. The translate - # function is (for now) bound to the form step, as that's where the translations - # are stored. Possibly in the future we will store those in the component itself, - # but then the need to translate upfront is also gone as the value formatters do - # receive the component definition. - i18n_enabled = self.renderer.form.translation_enabled - do_translate = ( - translate_function(self.renderer.submission, self.step) - if i18n_enabled - else None - ) - for configuration_path, component in iterate_components_with_configuration_path( configuration, recursive=False ): @@ -318,6 +296,5 @@ def get_children(self) -> Iterator[ComponentNode]: component=component, renderer=self.renderer, configuration_path=configuration_path, - translate=do_translate, ) yield from child_node diff --git a/src/openforms/formio/service.py b/src/openforms/formio/service.py index 8a753cd6d8..1c6186c9da 100644 --- a/src/openforms/formio/service.py +++ b/src/openforms/formio/service.py @@ -9,13 +9,13 @@ submodules and only their 'public' API should be imported and used. """ -from typing import Any, Callable +from typing import Any import elasticapm from rest_framework.request import Request from openforms.prefill import inject_prefill -from openforms.submissions.models import Submission, SubmissionStep +from openforms.submissions.models import Submission from openforms.typing import DataMapping from .datastructures import FormioConfigurationWrapper, FormioData @@ -60,20 +60,6 @@ def normalize_value_for_component(component: Component, value: Any) -> Any: return register.normalize(component, value) -def translate_function( - submission: Submission, - step: SubmissionStep, -) -> Callable[[str], str]: - "Return a translate function for FormIO strings" - - assert step.form_step # nosec: intended use of B101 - lang = submission.language_code - - translations = step.form_step.form_definition.component_translations.get(lang, {}) - - return lambda s: translations.get(s) or s - - @elasticapm.capture_span(span_type="app.formio") def get_dynamic_configuration( config_wrapper: FormioConfigurationWrapper, diff --git a/src/openforms/formio/variables.py b/src/openforms/formio/variables.py index 1b8aeb8003..61da96ab2e 100644 --- a/src/openforms/formio/variables.py +++ b/src/openforms/formio/variables.py @@ -1,5 +1,5 @@ import logging -from typing import Callable, Iterator +from typing import Iterator from django.template import TemplateSyntaxError @@ -62,9 +62,7 @@ def validate_configuration(configuration: JSONObject) -> dict[str, str]: def inject_variables( - configuration: FormioConfigurationWrapper, - values: DataMapping, - translate: Callable[[str], str] = str, + configuration: FormioConfigurationWrapper, values: DataMapping ) -> None: """ Inject the variable values into the Formio configuration. @@ -88,20 +86,8 @@ def inject_variables( continue match property_value: - case str(): - property_value = translate(property_value) case [str(), *_]: - property_value = [ - translate(s) for s in property_value if isinstance(s, str) - ] - case [{"label": _}, *_]: - for item in property_value: - if "label" in item: - item["label"] = translate(item["label"]) - case {"values": [*defined_values]}: - for item in defined_values: - if "label" in item: - item["label"] = translate(item["label"]) + property_value = [s for s in property_value if isinstance(s, str)] try: templated_value = render(property_value, values) diff --git a/src/openforms/forms/api/parsers.py b/src/openforms/forms/api/parsers.py index 20b133e323..92dd6a6907 100644 --- a/src/openforms/forms/api/parsers.py +++ b/src/openforms/forms/api/parsers.py @@ -13,7 +13,7 @@ class IgnoreConfigurationFieldCamelCaseJSONParser(CamelCaseJSONParser): # variant can sometimes overwrite the camelCase variant, which breaks the pre-fill # functionality. This can happen because JSON objects DO NOT HAVE inherent ordering # and the spec is non-deterministic. - json_underscoreize = {"ignore_fields": ("configuration", "component_translations")} + json_underscoreize = {"ignore_fields": ("configuration",)} class FormVariableJSONParser(CamelCaseJSONParser): @@ -42,7 +42,7 @@ class IgnoreConfigurationFieldCamelCaseJSONRenderer(CamelCaseJSONRenderer): # This is needed for fields in the JSON configuration that have an underscore # For example: time_24hr in the date component. See github issue # https://github.com/open-formulieren/open-forms/issues/1255 - json_underscoreize = {"ignore_fields": ("configuration", "component_translations")} + json_underscoreize = {"ignore_fields": ("configuration",)} class FormCamelCaseJSONParser(FormCamelCaseMixin, CamelCaseJSONParser): diff --git a/src/openforms/forms/api/serializers/form_definition.py b/src/openforms/forms/api/serializers/form_definition.py index 586ddf5013..a163260c22 100644 --- a/src/openforms/forms/api/serializers/form_definition.py +++ b/src/openforms/forms/api/serializers/form_definition.py @@ -161,6 +161,14 @@ def validate(self, attrs): return attrs + def save(self, **kwargs): + # field 'component_translations' has been deprecated and it's not part of + # the 'form_definition' model anymore. We use it only in the serializer. + if "component_translations" in self.validated_data: + del self.validated_data["component_translations"] + + return super().save(**kwargs) + class FormDefinitionDetailSerializer(FormDefinitionSerializer): used_in = UsedInFormSerializer( diff --git a/src/openforms/forms/forms/form_definition.py b/src/openforms/forms/forms/form_definition.py index 1b8fc1ff6d..1581356acb 100644 --- a/src/openforms/forms/forms/form_definition.py +++ b/src/openforms/forms/forms/form_definition.py @@ -20,11 +20,5 @@ class Meta: "login_required", "is_reusable", "configuration", - "component_translations", ) - widgets = { - "uuid": forms.TextInput(attrs={"readonly": True}), - "component_translations": forms.HiddenInput( - attrs={"class": "form-builder__component-translations"} - ), - } + widgets = {"uuid": forms.TextInput(attrs={"readonly": True})} diff --git a/src/openforms/forms/migrations/0094_remove_formdefinition_component_translations.py b/src/openforms/forms/migrations/0094_remove_formdefinition_component_translations.py new file mode 100644 index 0000000000..c27414ec95 --- /dev/null +++ b/src/openforms/forms/migrations/0094_remove_formdefinition_component_translations.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.10 on 2024-03-12 14:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("forms", "0093_fix_prefill_bis"), + ] + + operations = [ + migrations.RemoveField( + model_name="formdefinition", + name="component_translations", + ), + ] diff --git a/src/openforms/forms/models/form_definition.py b/src/openforms/forms/models/form_definition.py index 490ebd413f..e9be4e0f9f 100644 --- a/src/openforms/forms/models/form_definition.py +++ b/src/openforms/forms/models/form_definition.py @@ -70,13 +70,6 @@ class FormDefinition(models.Model): help_text=_("The total number of Formio components used in the configuration"), ) - component_translations = models.JSONField( - verbose_name=_("Component translations"), - help_text=_("Translations for literals used in components"), - blank=True, - default=dict, - ) - class Meta: verbose_name = _("Form definition") verbose_name_plural = _("Form definitions") diff --git a/src/openforms/forms/tests/e2e_tests/test_form_designer.py b/src/openforms/forms/tests/e2e_tests/test_form_designer.py index 56dcc63f0b..49278a7471 100644 --- a/src/openforms/forms/tests/e2e_tests/test_form_designer.py +++ b/src/openforms/forms/tests/e2e_tests/test_form_designer.py @@ -221,7 +221,6 @@ def assertState(): textfield["openForms"]["translations"]["nl"]["label"], "Veldlabel", ) - self.assertEqual(fd.component_translations, {}) await assertState() diff --git a/src/openforms/forms/tests/e2e_tests/test_translation_warnings.py b/src/openforms/forms/tests/e2e_tests/test_translation_warnings.py index 3c9ed04267..9329cf1694 100644 --- a/src/openforms/forms/tests/e2e_tests/test_translation_warnings.py +++ b/src/openforms/forms/tests/e2e_tests/test_translation_warnings.py @@ -37,17 +37,15 @@ def setUpTestData(): "type": "textfield", "key": "someField", "label": "Some Field", + "openForms": { + "translations": { + "en": {"label": "Some EN Field"}, + "nl": {"label": "Een NL Veld"}, + } + }, } ], }, - formstep__form_definition__component_translations={ - "en": { - "Some Field": "Some EN Field", - }, - "nl": { - "Some Field": "Een NL Veld", - }, - }, translation_enabled=True, ) ConfirmationEmailTemplateFactory.create( @@ -100,17 +98,15 @@ def setUpTestData(): "type": "textfield", "key": "someField", "label": "Some Field", + "openForms": { + "translations": { + "en": {"label": ""}, + "nl": {"label": ""}, + } + }, } ], }, - formstep__form_definition__component_translations={ - "en": { - "Some Field": "", - }, - "nl": { - "Some Field": "", - }, - }, translation_enabled=True, ) ConfirmationEmailTemplateFactory.create( @@ -177,17 +173,15 @@ def setUpTestData(): "type": "textfield", "key": "someField", "label": "Some Field", + "openForms": { + "translations": { + "en": {"label": "Some EN Field"}, + "nl": {"label": "Een NL Veld"}, + } + }, } ], }, - formstep__form_definition__component_translations={ - "en": { - "Some Field": "Some EN Field", - }, - "nl": { - "Some Field": "Een NL Veld", - }, - }, translation_enabled=True, ) return form @@ -239,17 +233,15 @@ def setUpTestData(): "type": "textfield", "key": "someField", "label": "Some Field", + "openForms": { + "translations": { + "en": {"label": "Some EN Field"}, + "nl": {"label": "Een NL Veld"}, + } + }, } ], }, - formstep__form_definition__component_translations={ - "en": { - "Some Field": "Some EN Field", - }, - "nl": { - "Some Field": "Een NL Veld", - }, - }, translation_enabled=True, ) return form @@ -311,17 +303,15 @@ def setUpTestData(): "type": "textfield", "key": "someField", "label": "Some Field", + "openForms": { + "translations": { + "en": {"label": "Some EN Field"}, + "nl": {"label": "Een NL Veld"}, + } + }, } ], }, - formstep__form_definition__component_translations={ - "en": { - "Some Field": "Some EN Field", - }, - "nl": { - "Some Field": "Een NL Veld", - }, - }, translation_enabled=True, ) return form @@ -370,17 +360,15 @@ def setUpTestData(): "type": "textfield", "key": "someField", "label": "Some Field", + "openForms": { + "translations": { + "en": {"label": "Some EN Field"}, + "nl": {"label": "Een NL Veld"}, + } + }, } ], }, - formstep__form_definition__component_translations={ - "en": { - "Some Field": "Some EN Field", - }, - "nl": { - "Some Field": "Een NL Veld", - }, - }, translation_enabled=True, ) ConfirmationEmailTemplateFactory.create( @@ -437,17 +425,15 @@ def setUpTestData(): "type": "textfield", "key": "someField", "label": "Some Field", + "openForms": { + "translations": { + "en": {"label": "Some EN Field"}, + "nl": {"label": "Een NL Veld"}, + } + }, } ], }, - formstep__form_definition__component_translations={ - "en": { - "Some Field": "Some EN Field", - }, - "nl": { - "Some Field": "Een NL Veld", - }, - }, translation_enabled=True, ) ConfirmationEmailTemplateFactory.create( diff --git a/src/openforms/js/components/admin/form_design/FormStep.js b/src/openforms/js/components/admin/form_design/FormStep.js index 4dd4202c62..f9ae139820 100644 --- a/src/openforms/js/components/admin/form_design/FormStep.js +++ b/src/openforms/js/components/admin/form_design/FormStep.js @@ -21,7 +21,6 @@ const FormStep = ({data, onEdit, onComponentMutated, onFieldChange, onReplace}) isReusable, isNew, validationErrors = [], - componentTranslations, } = data; const previousFormDefinition = usePrevious(formDefinition); let forceBuilderUpdate = false; @@ -46,7 +45,6 @@ const FormStep = ({data, onEdit, onComponentMutated, onFieldChange, onReplace}) url={formDefinition} generatedId={_generatedId} translations={translations} - componentTranslations={componentTranslations} configuration={configuration} isApplicable={isApplicable} loginRequired={loginRequired} diff --git a/src/openforms/js/components/admin/form_design/FormStepDefinition.js b/src/openforms/js/components/admin/form_design/FormStepDefinition.js index 158b99e27b..0d17caf3ad 100644 --- a/src/openforms/js/components/admin/form_design/FormStepDefinition.js +++ b/src/openforms/js/components/admin/form_design/FormStepDefinition.js @@ -46,7 +46,6 @@ const FormStepDefinition = ({ loginRequired = false, isReusable = false, translations = {}, - componentTranslations, configuration = emptyConfiguration, onChange, onComponentMutated, @@ -391,10 +390,7 @@ const FormStepDefinition = ({ {translationEnabled ? ( - + ) : null}

Velden

@@ -406,7 +402,6 @@ const FormStepDefinition = ({ configuration={configuration} onChange={onChange} onComponentMutated={onComponentMutated.bind(null, url || generatedId)} - componentTranslations={componentTranslations} componentNamespace={componentNamespace} registrationBackendInfo={registrationBackends} {...props} @@ -435,7 +430,6 @@ FormStepDefinition.propTypes = { nextText: PropTypes.string.isRequired, }) ), - componentTranslations: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)).isRequired, }; const ConfigurationErrors = ({errors = []}) => { diff --git a/src/openforms/js/components/admin/form_design/MissingComponentTranslationsWarning.js b/src/openforms/js/components/admin/form_design/MissingComponentTranslationsWarning.js index 9f4869c9c5..95d96f7d8d 100644 --- a/src/openforms/js/components/admin/form_design/MissingComponentTranslationsWarning.js +++ b/src/openforms/js/components/admin/form_design/MissingComponentTranslationsWarning.js @@ -13,7 +13,7 @@ import { const LANGUAGES = getSupportedLanguages(); -const extractTranslatableValues = configuration => { +const extractTranslatableProperties = configuration => { let translatableValues = []; FormioUtils.eachComponent( configuration.components, @@ -21,9 +21,11 @@ const extractTranslatableValues = configuration => { const literals = extractComponentLiterals(component); literals.forEach(literal => { translatableValues.push({ + component: component, componentKey: component.key, componentLabel: component.label, - literal: literal, + literal: literal.literal, + property: literal.property, }); }); }, @@ -31,17 +33,19 @@ const extractTranslatableValues = configuration => { ); return translatableValues; }; - -const extractMissingComponentTranslations = (configuration, componentTranslations = {}) => { +const extractMissingComponentTranslations = configuration => { const languageCodeMapping = Object.fromEntries(LANGUAGES); - const translatableValues = extractTranslatableValues(configuration); + const translatableProperties = extractTranslatableProperties(configuration); let missingTranslations = []; - for (const entry of translatableValues) { + for (const entry of translatableProperties) { + const component = entry.component; + for (const [languageCode, _languageLabel] of LANGUAGES) { - let translations = componentTranslations?.[languageCode] || {}; - if (!translations[entry.literal]) + let translations = component.openForms?.translations?.[languageCode] || {}; + + if (!translations[entry.property]) missingTranslations.push({language: languageCodeMapping[languageCode], ...entry}); } } @@ -84,6 +88,7 @@ const MissingComponentTranslationsTable = ({children: missingTranslations}) => ( MissingComponentTranslationsTable.propTypes = { children: PropTypes.arrayOf( PropTypes.shape({ + component: PropTypes.object.isRequired, componentKey: PropTypes.string.isRequired, componentLabel: PropTypes.string.isRequired, language: PropTypes.string.isRequired, @@ -92,7 +97,7 @@ MissingComponentTranslationsTable.propTypes = { ), }; -const MissingComponentTranslationsWarning = ({configuration, componentTranslations}) => { +const MissingComponentTranslationsWarning = ({configuration}) => { const [modalOpen, setModalOpen] = useState(false); const onShowModal = event => { @@ -100,10 +105,7 @@ const MissingComponentTranslationsWarning = ({configuration, componentTranslatio setModalOpen(true); }; - const missingTranslations = extractMissingComponentTranslations( - configuration, - componentTranslations - ); + const missingTranslations = extractMissingComponentTranslations(configuration); const formattedWarning = ( { let formStepsMissingTranslations = []; for (const formStep of formSteps) { - if ( - extractMissingComponentTranslations(formStep.configuration, formStep.componentTranslations) - .length - ) { + if (extractMissingComponentTranslations(formStep.configuration).length) { formStepsMissingTranslations.push(formStep.name); } } diff --git a/src/openforms/js/components/admin/form_design/Warnings/Warnings.stories.js b/src/openforms/js/components/admin/form_design/Warnings/Warnings.stories.js index e4b5246a6d..ada023a639 100644 --- a/src/openforms/js/components/admin/form_design/Warnings/Warnings.stories.js +++ b/src/openforms/js/components/admin/form_design/Warnings/Warnings.stories.js @@ -81,10 +81,6 @@ export const FormWarningsWithoutTranslations = { name: '', }, }, - componentTranslations: { - nl: {}, - en: {}, - }, validationErrors: [], }, ], diff --git a/src/openforms/js/components/admin/form_design/data/steps.js b/src/openforms/js/components/admin/form_design/data/steps.js index f41d3d5f20..dfec698527 100644 --- a/src/openforms/js/components/admin/form_design/data/steps.js +++ b/src/openforms/js/components/admin/form_design/data/steps.js @@ -44,9 +44,6 @@ const updateOrCreateSingleFormStep = async ( loginRequired: step.loginRequired, isReusable: step.isReusable, translations: formDefinitionTranslations, - // the component translations need to be cleaned up and fully processed in the - // reducer/state management of the component calling the data functions. - componentTranslations: step.componentTranslations, }; try { diff --git a/src/openforms/js/components/admin/form_design/form-creation-form.js b/src/openforms/js/components/admin/form_design/form-creation-form.js index a92ab85ccb..1d54851ea9 100644 --- a/src/openforms/js/components/admin/form_design/form-creation-form.js +++ b/src/openforms/js/components/admin/form_design/form-creation-form.js @@ -148,7 +148,6 @@ const newStepData = { url: '', _generatedId: '', // Consumers should generate this if there is no form definition url isNew: true, - componentTranslations: {}, validationErrors: [], }; diff --git a/src/openforms/js/components/admin/form_design/types/FormStep.js b/src/openforms/js/components/admin/form_design/types/FormStep.js index c21ed0e88d..b7caef50d8 100644 --- a/src/openforms/js/components/admin/form_design/types/FormStep.js +++ b/src/openforms/js/components/admin/form_design/types/FormStep.js @@ -21,7 +21,6 @@ const FormStep = PropTypes.shape({ nextText: PropTypes.string.isRequired, }) ), - componentTranslations: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)).isRequired, }); export default FormStep; diff --git a/src/openforms/js/components/formio_builder/builder.js b/src/openforms/js/components/formio_builder/builder.js index 783fefb9af..4fc4e38558 100644 --- a/src/openforms/js/components/formio_builder/builder.js +++ b/src/openforms/js/components/formio_builder/builder.js @@ -260,7 +260,6 @@ const FormIOBuilder = ({ configuration, onChange, onComponentMutated, - componentTranslations = {}, // mapping of language code to (mapping of literal -> translation) componentNamespace = {}, registrationBackendInfo = [], forceUpdate = false, @@ -278,7 +277,6 @@ const FormIOBuilder = ({ // This approach effectively pins the FormBuilder.form prop reference. const formRef = useRef(clone); - const componentTranslationsRef = useRef(componentTranslations); const componentNamespaceRef = useRef(componentNamespace); const registrationBackendInfoRef = useRef(registrationBackendInfo); @@ -287,9 +285,6 @@ const FormIOBuilder = ({ // instance actually only knows about the very first one. This means our updated state/ // props that's checked in the callbacks is an outdated view, which we can fix by using // mutable refs :-) - useOnChanged(componentTranslations, () => { - componentTranslationsRef.current = componentTranslations; - }); useOnChanged(componentNamespace, () => { componentNamespaceRef.current = componentNamespace; }); @@ -303,7 +298,6 @@ const FormIOBuilder = ({ // props need to be immutable to not end up in infinite loops const [builderOptions] = useState(getBuilderOptions()); - set(builderOptions, 'openForms.componentTranslationsRef', componentTranslationsRef); set(builderOptions, 'openForms.componentNamespace', componentNamespaceRef.current); set(builderOptions, 'openForms.featureFlags', featureFlags); set(builderOptions, 'openForms.registrationBackendInfoRef', registrationBackendInfoRef); @@ -342,7 +336,6 @@ FormIOBuilder.propTypes = { configuration: PropTypes.object, onChange: PropTypes.func, onComponentMutated: PropTypes.func, - componentTranslations: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)).isRequired, componentNamespace: PropTypes.arrayOf(PropTypes.object), forceUpdate: PropTypes.bool, registrationBackendInfo: PropTypes.arrayOf(PropTypes.object), diff --git a/src/openforms/js/components/formio_builder/index.js b/src/openforms/js/components/formio_builder/index.js index 9bfca4502d..339c8d4b88 100644 --- a/src/openforms/js/components/formio_builder/index.js +++ b/src/openforms/js/components/formio_builder/index.js @@ -14,12 +14,8 @@ onLoaded(async () => { for (const node of nodes) { const configurationInput = node.querySelector('.form-builder__configuration-input'); - const componentTranslationsInput = node - .closest('form') - .querySelector('.form-builder__component-translations'); const configuration = JSON.parse(configurationInput.value) || {display: 'form'}; - let componentTranslations = JSON.parse(componentTranslationsInput.value) || {}; const onChange = newConfiguration => { // extract translations @@ -29,11 +25,7 @@ onLoaded(async () => { const root = createRoot(node.querySelector('.form-builder__container')); root.render( - + ); } diff --git a/src/openforms/js/components/formio_builder/translation.js b/src/openforms/js/components/formio_builder/translation.js index 039a658241..cc118142d8 100644 --- a/src/openforms/js/components/formio_builder/translation.js +++ b/src/openforms/js/components/formio_builder/translation.js @@ -52,25 +52,30 @@ export const extractComponentLiterals = component => { ]; const isDateOrDatetimeComponent = ['date', 'datetime'].includes(component.type); - const literals = allTranslatableProperties + const finalTranslatableProperties = allTranslatableProperties // Do not show warnings for date/datetime placeholders, they are not translatable: .filter(property => (isDateOrDatetimeComponent ? property !== 'placeholder' : property)) .filter(property => !isRegex(property)) - .map(property => get(component, property)); + .map(property => ({property, literal: get(component, property)})); // See ADDITIONAL_PROPERTIES_BY_COMPONENT_TYPE for the reverse check switch (component.type) { case 'select': { const values = component?.data?.values || []; - literals.push(...values.map(v => v.label)); + finalTranslatableProperties.push( + ...values.map(v => ({property: 'values_label', literal: v.label})) + ); break; } case 'selectboxes': case 'radio': { const values = component?.values || []; - literals.push(...values.map(v => v.label)); + finalTranslatableProperties.push( + ...values.map(v => ({property: 'values_label', literal: v.label})) + ); break; } } - return literals.filter(literal => !!literal); + + return finalTranslatableProperties.filter(property => !!property.literal); }; diff --git a/src/openforms/js/components/formio_builder/translation.spec.js b/src/openforms/js/components/formio_builder/translation.spec.js index d1427f86be..c72e6270fa 100644 --- a/src/openforms/js/components/formio_builder/translation.spec.js +++ b/src/openforms/js/components/formio_builder/translation.spec.js @@ -22,7 +22,7 @@ describe('Checking date/datetime translation literals', () => { format: 'dd-MM-yyyy HH:mm', }; - expect(extractComponentLiterals(dateComponent)).toStrictEqual(['Date']); - expect(extractComponentLiterals(dateTimeComponent)).toStrictEqual(['Date / Time']); + expect(extractComponentLiterals(dateComponent)[0].literal).toStrictEqual('Date'); + expect(extractComponentLiterals(dateTimeComponent)[0].literal).toStrictEqual('Date / Time'); }); }); diff --git a/src/openforms/submissions/form_logic.py b/src/openforms/submissions/form_logic.py index 70e3bd9fa4..3ebb7053a0 100644 --- a/src/openforms/submissions/form_logic.py +++ b/src/openforms/submissions/form_logic.py @@ -8,7 +8,6 @@ FormioData, get_dynamic_configuration, inject_variables, - translate_function, ) from openforms.formio.utils import get_component_empty_value from openforms.typing import DataMapping @@ -55,7 +54,7 @@ def evaluate_form_logic( 4. Apply any dirty data (unsaved) to the variable state 5. Evaluate the logic rules in order, for each action in each triggered rule: - 1. If a variable value is being update, update the variable state + 1. If a variable value is being updated, update the variable state 2. Else record the action to perform to the configuration (but don't apply it yet!) 6. The variables state is now completely resolved and can be used as input for the @@ -167,13 +166,7 @@ def evaluate_form_logic( data_diff[key] = empty_value # 7.2 Interpolate the component configuration with the variables. - translate = ( - # We need to interpolate the translated string - translate_function(submission, step) - if (submission.form.translation_enabled and step.form_step) - else str # a noop when translation is not enabled - ) - inject_variables(config_wrapper, data_container.data, translate) + inject_variables(config_wrapper, data_container.data) # 7.3 Handle custom formio types - TODO: this needs to be lifted out of # :func:`get_dynamic_configuration` so that it can use variables. diff --git a/src/openforms/submissions/tests/test_get_submission_step.py b/src/openforms/submissions/tests/test_get_submission_step.py index 5aa9a78a0d..a779e7ab39 100644 --- a/src/openforms/submissions/tests/test_get_submission_step.py +++ b/src/openforms/submissions/tests/test_get_submission_step.py @@ -402,8 +402,8 @@ def test_it_only_translates_appropriate_string_properties(self): language_code="de", ) form_step = submission.steps[0].form_step - form_step.form_definition.component_translations = { - "de": {"bar": "Kneipe", "Bar": "Kneipe"}, + form_step.form_definition.configuration["components"][0]["openForms"] = { + "translations": {"de": {"label": "Kneipe"}} } form_step.form_definition.save() self._add_submission_to_session(submission) @@ -435,35 +435,78 @@ def test_component_properties_are_translated(self): "tooltip": "Tip 1", "groupLabel": "Element", "components": [ - {"type": "textfield", "label": "Tekst 1", "key": "text1"}, + { + "type": "textfield", + "label": "Tekst 1", + "key": "text1", + "openForms": { + "translations": { + "en": {"label": "Text 1"}, + "nl": {"label": "Tekst 1"}, + } + }, + }, ], + "openForms": { + "translations": { + "en": { + "label": "Repeating group 1", + "tooltip": "First tip", + "groupLabel": "Item", + }, + "nl": { + "label": "Herhalende groep 1", + "tooltip": "Tip 1", + "groupLabel": "Element", + }, + } + }, }, { "type": "radio", "key": "radio1", "tooltip": "De uitsteekschijf van deze week", - "values": [{"value": 1, "label": "Een"}, {"value": 2}], + "values": [ + { + "value": 1, + "label": "Een", + "openForms": { + "translations": { + "en": {"label": "One"}, + "nl": {"label": "Een"}, + } + }, + }, + {"value": 2}, + ], + "openForms": { + "translations": { + "en": {"tooltip": "Radio Giraffe's tip of the week"}, + "nl": {"tooltip": "De uitsteekschijf van deze week"}, + } + }, }, { "type": "select", "key": "select1", "data": { - "values": [{"value": 1, "label": "Keuze 1"}, {"value": 2}] + "values": [ + { + "value": 1, + "label": "Keuze 1", + "openForms": { + "translations": { + "en": {"label": "1st Choice"}, + "nl": {"label": "Keuze 1"}, + } + }, + }, + {"value": 2}, + ] }, }, ], }, - form__formstep__form_definition__component_translations={ - "en": { - "Herhalende groep 1": "Repeating group 1", - "Element": "Item", - "Tekst 1": "Text 1", - "Een": "One", - "Keuze 1": "1st Choice", - "Tip 1": "First tip", - "De uitsteekschijf van deze week": "Radio Giraffe's tip of the week", - } - }, ) self._add_submission_to_session(submission) form_step = submission.steps[0].form_step diff --git a/src/openforms/submissions/tests/test_submission_report.py b/src/openforms/submissions/tests/test_submission_report.py index a3fd1f40e4..b657263c7b 100644 --- a/src/openforms/submissions/tests/test_submission_report.py +++ b/src/openforms/submissions/tests/test_submission_report.py @@ -138,6 +138,12 @@ def test_report_is_generated_in_same_language_as_submission(self): "label": f"Untranslated {component_type.title()} label", "showInPDF": True, "hidden": False, + "openForms": { + "translations": { + "en": {"label": f"Translated {component_type.title()} label"}, + "nl": {"label": f"Untranslated {component_type.title()} label"}, + } + }, } # add the component to the FormDefintion @@ -147,8 +153,26 @@ def test_report_is_generated_in_same_language_as_submission(self): match component: case {"type": "radio"}: component["values"] = [ - {"label": "Untranslated Radio option one", "value": "radioOne"}, - {"label": "Untranslated Radio option two", "value": "radioTwo"}, + { + "label": "Untranslated Radio option one", + "value": "radioOne", + "openForms": { + "translations": { + "en": {"label": "Radio number one"}, + "nl": {"label": "Untranslated Radio option one"}, + } + }, + }, + { + "label": "Untranslated Radio option two", + "value": "radioTwo", + "openForms": { + "translations": { + "en": {"label": "Radio number two"}, + "nl": {"label": "Untranslated Radio option two"}, + } + }, + }, ] case {"type": "select"}: component["data"] = { @@ -156,10 +180,26 @@ def test_report_is_generated_in_same_language_as_submission(self): { "label": "Untranslated Select option one", "value": "selectOne", + "openForms": { + "translations": { + "en": {"label": "A fine selection"}, + "nl": { + "label": "Untranslated Select option one" + }, + } + }, }, { "label": "Untranslated Select option two", "value": "selectTwo", + "openForms": { + "translations": { + "en": {"label": "Translated Select option two"}, + "nl": { + "label": "Untranslated Select option two" + }, + } + }, }, ] } @@ -168,18 +208,50 @@ def test_report_is_generated_in_same_language_as_submission(self): { "label": "Untranslated Selectboxes option one", "value": "selectboxesOne", + "openForms": { + "translations": { + "en": {"label": "The Deal"}, + "nl": { + "label": "Untranslated Selectboxes option one" + }, + } + }, }, { "label": "Untranslated Selectboxes option two", "value": "selectboxesTwo", + "openForms": { + "translations": { + "en": {"label": "This"}, + "nl": { + "label": "Untranslated Selectboxes option two" + }, + } + }, }, { "label": "Untranslated Selectboxes option three", "value": "selectboxesThree", + "openForms": { + "translations": { + "en": {"label": "That"}, + "nl": { + "label": "Untranslated Selectboxes option three" + }, + } + }, }, { "label": "Untranslated Selectboxes option four", "value": "selectboxesFour", + "openForms": { + "translations": { + "en": {"label": "The Other"}, + "nl": { + "label": "Untranslated Selectboxes option four" + }, + } + }, }, ] @@ -192,6 +264,12 @@ def test_report_is_generated_in_same_language_as_submission(self): "hidden": False, "label": "Untranslated Field Set label", "components": [components.pop()], + "openForms": { + "translations": { + "en": {"label": "Translated Field Set label"}, + "nl": {"label": "Untranslated Field Set label"}, + } + }, }, { "type": "columns", @@ -208,6 +286,12 @@ def test_report_is_generated_in_same_language_as_submission(self): "components": [components.pop()], }, ], + "openForms": { + "translations": { + "en": {"label": "Translated Columns label"}, + "nl": {"label": "Untranslated Columns label"}, + } + }, }, { "type": "editgrid", @@ -216,12 +300,32 @@ def test_report_is_generated_in_same_language_as_submission(self): "label": "Untranslated Repeating Group label", "groupLabel": "Untranslated Repeating Group Item label", "components": [components.pop()], + "openForms": { + "translations": { + "en": { + "label": "Translated Repeating Group label", + "groupLabel": "Translated Repeating Group Item label", + }, + "nl": { + "label": "Untranslated Repeating Group label", + "groupLabel": "Untranslated Repeating Group Item label", + }, + } + }, }, { "type": "checkbox", "key": "interpolkey", "hidden": False, "label": "Untranslated label using {{textfieldkey}} interpolation", + "openForms": { + "translations": { + "en": {"label": "Interpolated {{textfieldkey}} label"}, + "nl": { + "label": "Untranslated label using {{textfieldkey}} interpolation" + }, + } + }, }, ] ) @@ -234,45 +338,6 @@ def test_report_is_generated_in_same_language_as_submission(self): submission__form__name_en="Translated Form name", submission__form__formstep__form_definition__name_nl="Untranslated Form Step name", submission__form__formstep__form_definition__name_en="A Quickstep", - submission__form__formstep__form_definition__component_translations={ - "en": { - "Untranslated Bsn label": "Translated Bsn label", - "Untranslated Checkbox label": "Translated Checkbox label", - "Untranslated Currency label": "Translated Currency label", - "Untranslated Date label": "Translated Date label", - "Untranslated Email label": "Translated Email label", - "Untranslated Field Set label": "Translated Field Set label", - "Untranslated File label": "Translated File label", - "Untranslated Iban label": "Translated Iban label", - "Untranslated Licenseplate label": "Translated Licenseplate label", - "Untranslated Map label": "Translated Map label", - "Untranslated Number label": "Translated Number label", - "Untranslated Password label": "Translated Password label", - "Untranslated Phonenumber label": "Translated Phonenumber label", - "Untranslated Postcode label": "Translated Postcode label", - "Untranslated Radio label": "Translated Radio label", - "Untranslated Radio option one": "Radio number one", - "Untranslated Radio option two": "Translated Radio option two", - "Untranslated Repeating Group Item label": "Translated Repeating Gr" - "oup Item label", - "Untranslated Repeating Group label": "Translated Repeating Group l" - "abel", - "Untranslated Select label": "Translated Select label", - "Untranslated Select option one": "A fine selection", - "Untranslated Select option two": "Translated Select option two", - "Untranslated Selectboxes label": "Translated Selectboxes label", - "Untranslated Selectboxes option four": "The Other", - "Untranslated Selectboxes option one": "The Deal", - "Untranslated Selectboxes option three": "That", - "Untranslated Selectboxes option two": "This", - "Untranslated Signature label": "Translated Signature label", - "Untranslated Textarea label": "Translated Textarea label", - "Untranslated Textfield label": "Translated Textfield label", - "Untranslated Time label": "Translated Time label", - "Untranslated Updatenote label": "Translated Updatenote label", - "Untranslated label using {{textfieldkey}} interpolation": "Interpolated {{textfieldkey}} label", - } - }, submission__form__formstep__form_definition__configuration={ "components": components, }, diff --git a/src/openforms/submissions/tests/test_tasks_confirmation_emails.py b/src/openforms/submissions/tests/test_tasks_confirmation_emails.py index 092c7b75f2..2553de2951 100644 --- a/src/openforms/submissions/tests/test_tasks_confirmation_emails.py +++ b/src/openforms/submissions/tests/test_tasks_confirmation_emails.py @@ -557,15 +557,6 @@ def test_template_is_rendered_in_submission_language(self): form__translation_enabled=True, form__name="Translated form name", form__formstep__form_definition__name="A Quickstep", - form__formstep__form_definition__component_translations={ - "en": { - "Untranslated Repeating Group label": "Translated Repeating Group label", - "Untranslated Repeating Group Item label": "Translated Repeating Group Item label", - "Untranslated Radio option 1": "Translated Radio option 1", - "Untranslated Radio option 2": "Translated Radio option 2", - "Untranslated Select option 2": "Translated Select option 2", - }, - }, form__formstep__form_definition__configuration={ "components": [ { @@ -597,10 +588,30 @@ def test_template_is_rendered_in_submission_language(self): { "label": "Untranslated Radio option 1", "value": "radiov1", + "openForms": { + "translations": { + "en": { + "label": "Translated Radio option 1" + }, + "nl": { + "label": "Untranslated Radio option 1" + }, + } + }, }, { "label": "Untranslated Radio option 2", "value": "radiov2", + "openForms": { + "translations": { + "en": { + "label": "Translated Radio option 2" + }, + "nl": { + "label": "Untranslated Radio option 2" + }, + } + }, }, ], }, @@ -617,11 +628,33 @@ def test_template_is_rendered_in_submission_language(self): { "label": "Untranslated Select option 2", "value": "selectv2", + "openForms": { + "translations": { + "en": { + "label": "Translated Select option 2" + }, + "nl": { + "label": "Untranslated Select option 2" + }, + } + }, }, ], }, }, ], + "openForms": { + "translations": { + "en": { + "label": "Translated Repeating Group label", + "groupLabel": "Translated Repeating Group Item label", + }, + "nl": { + "label": "Untranslated Repeating Group label", + "groupLabel": "Untranslated Repeating Group Item label", + }, + } + }, }, ], }, diff --git a/src/openforms/submissions/tests/test_variables/test_variable_injection.py b/src/openforms/submissions/tests/test_variables/test_variable_injection.py index 8f15e8b3d2..9b4bdf4987 100644 --- a/src/openforms/submissions/tests/test_variables/test_variable_injection.py +++ b/src/openforms/submissions/tests/test_variables/test_variable_injection.py @@ -233,18 +233,40 @@ def setUpTestData(cls): "key": "naam", "type": "textfield", "label": "Naam", + "openForms": { + "translations": { + "en": {"label": "Name"}, + "nl": {"label": "Naam"}, + } + }, }, { "key": "geboortedatum", "type": "date", "label": "Geboortedatum", "format": "dd-MM-yyyy", + "openForms": { + "translations": { + "en": {"label": "Birthdate"}, + "nl": {"label": "Geboortedatum"}, + } + }, }, { "key": "ww", "type": "password", "label": "Wachtwoord", "description": 'Suggestie: #{{naam|title}}{{geboortedatum|date:"Y"}}', + "openForms": { + "translations": { + "en": { + "description": "Suggestion: #{{naam|title}}{{geboortedatum|date:'Y'}}" + }, + "nl": { + "description": "Suggestie: #{{naam|title}}{{geboortedatum|date:'Y'}}" + }, + } + }, }, { "key": "ww_check", @@ -252,6 +274,18 @@ def setUpTestData(cls): "label": "Bevestig uw wachtwoord: {{ww}}", "description": "Mooi hè? We laten u niet 2 maal hetzelfde " "typen. Gemak > veiligheid.", + "openForms": { + "translations": { + "en": { + "label": "Confirm your password: {{ww}}", + "description": "Nice, eh? We won't make you type the same thing twice. Convenience over safety.", + }, + "nl": { + "label": "Bevestig uw wachtwoord: {{ww}}", + "description": "Mooi hè? We laten u niet 2 maal hetzelfde typen. Gemak > veiligheid.", + }, + } + }, }, ], submitted_data={ @@ -261,16 +295,6 @@ def setUpTestData(cls): }, ) cls.form_step = cls.submission.steps[0].form_step - cls.form_step.form_definition.component_translations = { - "en": { - 'Suggestie: #{{naam|title}}{{geboortedatum|date:"Y"}}': 'Suggestion: #{{naam|title}}{{geboortedatum|date:"Y"}}', - "Naam": "Name", - "Geboortedatum": "Birthdate", - "Bevestig uw wachtwoord: {{ww}}": "Confirm your password: {{ww}}", - # Actually en-ca, eh? - "Mooi hè? We laten u niet 2 maal hetzelfde typen. Gemak > veiligheid.": "Nice, eh? We won't make you type the same thing twice. Convenience over safety.", - } - } cls.form_step.form_definition.save() def test_variable_interpolates_on_translated_definitions(self):