Skip to content

Commit

Permalink
Merge pull request #12 from kdambekalns/translatable-forms
Browse files Browse the repository at this point in the history
!!! FEATURE: Translatable forms

This change allows for forms to be translated using XLIFF as elsewhere
in Flow. Form element labels, placeholders, option labels and more can
be translated.

Instructions are included in a new documentation chapter.

This is marked as breaking: if you changed the "translationPackage"
earlier to customize validation error messages, you will need to use the
new "validationErrorTranslationPackage" setting for that now. A code
migration to adjust that is included, run:

    ./flow flow:core:migrate  --version 20160601101500 --package-key ...
  • Loading branch information
kdambekalns committed Jun 21, 2016
2 parents b3cb4ac + d556cb7 commit 24b0020
Show file tree
Hide file tree
Showing 25 changed files with 424 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php
namespace TYPO3\Form\ViewHelpers;

/*
* This file is part of the TYPO3.Form package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use TYPO3\Flow\Annotations as Flow;
use TYPO3\Flow\I18n\Translator;
use TYPO3\Flow\Resource\Exception as ResourceException;
use TYPO3\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3\Fluid\Core\ViewHelper\Exception as ViewHelperException;
use TYPO3\Form\Core\Model\FormElementInterface;

/**
* ViewHelper to translate the property of a given form element based on its rendering options
*
* = Examples =
*
* <code>
* {element -> form:translateElementProperty(property: 'placeholder')}
* </code>
* <output>
* the translated placeholder, or the actual "placeholder" property if no translation was found
* </output>
*
*/
class TranslateElementPropertyViewHelper extends AbstractViewHelper
{
/**
* @Flow\Inject
* @var Translator
*/
protected $translator;

/**
* @var bool
*/
protected $escapeChildren = false;

/**
* @param string $property
* @param FormElementInterface $element
* @return string the rendered form head
*/
public function render($property, FormElementInterface $element = null)
{
if ($element === null) {
$element = $this->renderChildren();
}
if ($property === 'label') {
$defaultValue = $element->getLabel();
} else {
$defaultValue = isset($element->getProperties()[$property]) ? (string)$element->getProperties()[$property] : '';
}
$renderingOptions = $element->getRenderingOptions();
if (!isset($renderingOptions['translationPackage'])) {
return $defaultValue;
}
$translationId = sprintf('forms.elements.%s.%s', $element->getIdentifier(), $property);
try {
$translation = $this->translator->translateById($translationId, [], null, null, 'Main', $renderingOptions['translationPackage']);
} catch (ResourceException $exception) {
return $defaultValue;
}
return $translation === null ? $defaultValue : $translation;
}
}
7 changes: 5 additions & 2 deletions Configuration/Settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ TYPO3:
layoutPathPattern: 'resource://{@package}/Private/Form/Layouts/{@type}.html'
# set this to TRUE if you want to avoid exceptions for FormElements without definitions
skipUnknownElements: FALSE
# Package with XLIFF files for translatable messages (e.g. validation errors)
# Package with XLIFF files for translation of form labels, placeholders, ...
# If you need custom messages, copy Resources/Private/Translations/en/Main.xlf to your package and adjust this setting
translationPackage: 'TYPO3.Form'
# Package with XLIFF files for translatable error messages (validation errors)
# If you need custom messages, copy Resources/Private/Translations/en/ValidationErrors.xlf to your package and adjust this setting
translationPackage: 'TYPO3.Flow'
validationErrorTranslationPackage: 'TYPO3.Flow'

'TYPO3.Form:Form':
superTypes:
Expand Down
1 change: 1 addition & 0 deletions Documentation/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ reference, although there will be links to the in-depth API reference at various
quickstart
configuring-form-yaml
adjusting-form-output
translating-forms
extending-form-api
configuring-form-builder
extending-form-builder
Expand Down
204 changes: 204 additions & 0 deletions Documentation/translating-forms.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
.. _translating-forms:

Translating Forms
=================

If a form has been set up, all elements will use the labels, placeholders and so forth as configured.
To have the form translated depending on the current locale, you need to configure a package to load
the translations from and add the translations as XLIFF files.

Configuration
-------------

The package to load the translations from is configured in the form preset being used. The simplest
way to configure it is this:

.. code-block:: yaml
TYPO3:
Form:
presets:
default:
formElementTypes:
'TYPO3.Form:Base':
renderingOptions:
translationPackage: 'AcmeCom.SomePackage'
Of course it can be set in a custom preset in the same way.

The translation of validation error messages uses the TYPO3.Flow package by default, to avoid having to
copy the validation errors message catalog to all packages used for form translation. If you want to
adjust those error messages as well, copy ``ValidationErrors.xlf`` to your package and set the option
``validationErrorTranslationPackage`` to your package key.

XLIFF files
-----------

The XLIFF files follow the usual rules, the ``Main`` catalog is used. The Form package comes with the following
catalog (``Main.xlf``):

.. code-block:: xml
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file original="" product-name="TYPO3.Form" source-language="en" datatype="plaintext">
<body>
<trans-unit id="forms.navigation.previousPage" xml:space="preserve">
<source>Previous page</source>
</trans-unit>
<trans-unit id="forms.navigation.nextPage" xml:space="preserve">
<source>Next page</source>
</trans-unit>
<trans-unit id="forms.navigation.submit" xml:space="preserve">
<source>Submit</source>
</trans-unit>
</body>
</file>
</xliff>
It should be copied to make sure the three expected units are available and can then be amended by your own
units.

For most reliable translations, the units should be given id properties based on the form configuration.
The schema is as follows:

forms.navigation.nextPage
In multi-page forms this is used for the navigation.
forms.navigation.previousPage
In multi-page forms this is used for the navigation.
forms.navigation.submitButton
In forms this is used for the submit button.

Forms and sections can have their labels translated using this, where where ``{identifier}`` is the identifier
of the page or section itself:

forms.pages.{identifier}.label
The label used for a form page.
forms.sections.{identifier}.label
The label used for a form section.

The actual elements of a form have their id constructed by appending one of the following to
``forms.elements.{identifier}.``, where ``{identifier}`` is the identifier of the form element
itself:

label
The label for an element.
placeholder
The placeholder for an element, if applicable.
description
dkjsadhsajk
text
The text of a ``StaticText`` element.
confirmationLabel
Used in the ``PasswordWithConfirmation`` element.
passwordDescription
Used in the ``PasswordWithConfirmation`` element.

The labels of radio buttons and select field options can be translated using the following schema,
where ``{identifier}`` is the identifier of the form element itself and ``value`` is the value assigned
to the option:

forms.elements.{identifier}.options.{value}
Used to translate labels of radio buttons and select field entries.

Complete example
----------------

This is the example form used elsewhere in this documentation:

* Contact Form *(Form)*
* Page 01 *(Page)*
* Name *(Single-line Text)*
* Email *(Single-line Text)*
* Message *(Multi-line Text)*

Assume it is configured like this using YAML:

.. code-block:: yaml
type: 'TYPO3.Form:Form'
identifier: 'contact'
label: 'Contact form'
renderables:
-
type: 'TYPO3.Form:Page'
identifier: 'page-one'
renderables:
-
type: 'TYPO3.Form:SingleLineText'
identifier: name
label: 'Name'
validators:
- identifier: 'TYPO3.Flow:NotEmpty'
properties:
placeholder: 'Please enter your full name'
-
type: 'TYPO3.Form:SingleLineText'
identifier: email
label: 'Email'
validators:
- identifier: 'TYPO3.Flow:NotEmpty'
- identifier: 'TYPO3.Flow:EmailAddress'
properties:
placeholder: 'Enter a valid email address'
-
type: 'TYPO3.Form:MultiLineText'
identifier: message
label: 'Message'
validators:
- identifier: 'TYPO3.Flow:NotEmpty'
properties:
placeholder: 'Enter your message here'
.. note:: You may leave out ``label`` and ``placeholder`` if you use id-based matching for the translation.
Be aware though, that you will get empty labels and placeholders in case the translation fails or is not
available.

The following XLIFF would allow to translate the form:

.. code-block:: xml
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file original="" product-name="TYPO3.Form" source-language="en" datatype="plaintext">
<body>
<trans-unit id="forms.navigation.previousPage" xml:space="preserve">
<source>Previous page</source>
</trans-unit>
<trans-unit id="forms.navigation.nextPage" xml:space="preserve">
<source>Next page</source>
</trans-unit>
<trans-unit id="forms.navigation.submit" xml:space="preserve">
<source>Submit</source>
</trans-unit>
<trans-unit id="forms.pages.page-one" xml:space="preserve">
<source>Submit</source>
</trans-unit>
<trans-unit id="forms.elements.name.label" xml:space="preserve">
<source>Name</source>
</trans-unit>
<trans-unit id="forms.elements.name.placeholder" xml:space="preserve">
<source>Please enter your full name</source>
</trans-unit>
<trans-unit id="forms.elements.email.label" xml:space="preserve">
<source>Email</source>
</trans-unit>
<trans-unit id="forms.elements.email.placeholder" xml:space="preserve">
<source>Enter a valid email address</source>
</trans-unit>
<trans-unit id="forms.elements.message.label" xml:space="preserve">
<source>Message</source>
</trans-unit>
<trans-unit id="forms.elements.message.placeholder" xml:space="preserve">
<source>Enter your message here</source>
</trans-unit>
</body>
</file>
</xliff>
Copy it to your target language and add the ``target-language`` attribute as well as the needed
``<target>…</target>`` entries.
62 changes: 62 additions & 0 deletions Migrations/Code/Version20160601101500.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php
namespace TYPO3\Flow\Core\Migrations;

/*
* This file is part of the TYPO3.Flow package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use TYPO3\Flow\Configuration\ConfigurationManager;

/**
* Adjust "Settings.yaml" to use validationErrorTranslationPackage instead of translationPackage
*/
class Version20160601101500 extends AbstractMigration
{

/**
* @return void
*/
public function up()
{
$this->processConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS,
function (array &$configuration) {
$presetsConfiguration = \TYPO3\Flow\Reflection\ObjectAccess::getPropertyPath($configuration, 'TYPO3.Form.presets');
if (!is_array($presetsConfiguration)) {
return;
}

$presetsConfiguration = $this->renameTranslationPackage($presetsConfiguration);

$configuration['TYPO3']['Form']['presets'] = $presetsConfiguration;
},
true
);
}

/**
* Recurse into the given preset and rename translationPackage to validationErrorTranslationPackage
*
* @param array $preset
* @return array
*/
public function renameTranslationPackage(array &$preset)
{
foreach ($preset as $key => $value) {
if (is_array($value)) {
if (isset($value['renderingOptions']['translationPackage'])) {
$value['renderingOptions']['validationErrorTranslationPackage'] = $value['renderingOptions']['translationPackage'];
unset($value['renderingOptions']['translationPackage']);
}
$preset[$key] = $this->renameTranslationPackage($value);
}
}

return $preset;
}
}
2 changes: 1 addition & 1 deletion Resources/Private/Form/DatePicker.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{namespace form=TYPO3\Form\ViewHelpers}
<f:layout name="TYPO3.Form:Field" />
<f:section name="field">
<form:form.datePicker id="{element.uniqueIdentifier}" property="{element.identifier}" placeholder="{element.properties.placeholder}" dateFormat="{element.properties.dateFormat}" initialDate="{element.properties.initialDate}" enableDatePicker="{element.properties.enableDatePicker}" class="{element.properties.elementClassAttribute}" errorClass="{element.properties.elementErrorClassAttribute}" />
<form:form.datePicker id="{element.uniqueIdentifier}" property="{element.identifier}" placeholder="{element -> form:translateElementProperty(property: 'placeholder')}" dateFormat="{element.properties.dateFormat}" initialDate="{element.properties.initialDate}" enableDatePicker="{element.properties.enableDatePicker}" class="{element.properties.elementClassAttribute}" errorClass="{element.properties.elementErrorClassAttribute}" />
<f:if condition="{element.properties.displayTimeSelector}">
<form:form.timePicker id="{element.uniqueIdentifier}-time" property="{element.identifier}" initialDate="{element.properties.initialDate}" class="{element.properties.timeSelectorClassAttribute}" errorClass="{element.properties.elementErrorClassAttribute}" />
</f:if>
Expand Down
4 changes: 2 additions & 2 deletions Resources/Private/Form/FileUpload.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<f:form.upload property="{element.identifier}" id="{element.uniqueIdentifier}" class="{element.properties.elementClassAttribute}" />
<f:if condition="{resource}">
<div class="clearfix">
<a class="btn small" href="#" onclick="return !disableUpload('{element.uniqueIdentifier}')">Cancel</a>
<a class="btn small" href="#" onclick="return !disableUpload('{element.uniqueIdentifier}')"><f:translate id="forms.labels.cancel" package="{element.renderingOptions.translationPackage}">Cancel</f:translate></a>
</div>
</f:if>
</div>
Expand All @@ -16,7 +16,7 @@
{resource.filename}
</a>
<div class="clearfix">
<a class="btn small" href="#" onclick="return !enableUpload('{element.uniqueIdentifier}')">Replace File</a>
<a class="btn small" href="#" onclick="return !enableUpload('{element.uniqueIdentifier}')"><f:translate id="forms.labels.replaceFile" package="{element.renderingOptions.translationPackage}">Replace File</f:translate></a>
</div>
</div>
</f:if>
Expand Down

0 comments on commit 24b0020

Please sign in to comment.