Blueprint Developer Manual / Version 2406.0
Table Of ContentsYou may customize XLIFF handling at various extension points:
you may modify strategies to identify translatable properties, or
you may change representation in XLIFF export, or
you may adapt XLIFF import for special property types.
Assumptions
In here you will see a small use case which leads you through all required adaptations to take to your system. The use case is based on the following assumptions:
you introduced a markup property
xhtml
with XHTML grammar (http://www.w3.org/1999/xhtml
) to your content type model,you do not support attributes for XHTML elements, and
only markup properties named
xhtml
should be considered for translation (in other words: you do not use the translatable attribute inside your content type model).
In Section “Handling Attributes” this simple example is continued by adding the attributes, as this is an expert scenario which requires taking care of XLIFF validation.
Translatable Predicate
In order to add the property to the translation process, you have to mark it as translatable.
There are several ways of doing so, where the default one is to mark the property as
translatable within the content type model (using extension:translatable=true
).
As alternative to this you may add a custom bean of type
TranslatablePredicate
to your application context, which will then be ORed with all other existing
beans of this type. To completely override the behavior, you need to override
the bean named `translatablePredicate` in
TranslatablePredicateConfiguration.
The Blueprint contains a default predicate that can be configured with property
translateXliffTranslatableExpressions
and that makes it possible
to mark only certain nested properties of Struct properties as translatable.
See Section “XLIFF Configuration Beans” for details.
Translation Items
Having marked the property xhtml
as translatable, as described in
Section “Translatable Predicate” and trying to export a
content item containing the xhtml
property, you will see a
CapTranslateItemException
as in
Example 5.18, “Example for CapTranslateItemException”.
com.coremedia.translate.item.CapTranslateItemException: Property 'xhtml' (type: MARKUP) of content type 'MyDocument' is configured to be considered during translation but a corresponding transformer of type 'com.coremedia.translate.item.transform.TranslatePropertyTransformer' cannot be found. Please consider to add an appropriate transformer bean to your Spring context.
Example 5.18. Example for CapTranslateItemException
The exception tells you which steps to do next, in order to support the
xhtml
property during XLIFF export: You have to add a
TranslatePropertyTransformer
to your Spring context.
Some default transformers for standard CoreMedia property types are configured in TranslateItemConfiguration. One of them is the AtomicRichtextTranslatePropertyTransformer for CoreMedia RichText properties.
Just as the AtomicRichtextTranslatePropertyTransformer
the
transformer can be based on
AbstractAtomicMarkupTranslatePropertyTransformer
which ends up that you only have to specify how to match the grammar name and to add a
predicate to decide if the property should be considered empty or not.
public class AtomicXhtmlTranslatePropertyTransformer
extends AbstractAtomicMarkupTranslatePropertyTransformer {
private static final String XHTML_GRAMMAR_NAME =
"http://www.w3.org/1999/xhtml";
private AtomicXhtmlTranslatePropertyTransformer(
TranslatablePredicate translatablePredicate,
Predicate<? super Markup> emptyPredicate) {
super(
translatablePredicate,
AtomicXhtmlTranslatePropertyTransformer::isXhtml,
emptyPredicate
);
}
public AtomicXhtmlTranslatePropertyTransformer(
TranslatablePredicate translatablePredicate) {
this(translatablePredicate,
AtomicXhtmlTranslatePropertyTransformer::isEmpty);
}
private static boolean isEmpty(@Nullable Markup value) {
return !MarkupUtil.hasText(value, true);
}
private static boolean isXhtml(XmlGrammar grammar) {
return XHTML_GRAMMAR_NAME.equals(grammar.getName());
}
}
Example 5.19. TranslatePropertyTransformer for XHTML
Now you have a ready-to-use strategy to add your xhtml
property to your
translation items.
Property Hints
Transformed properties may hold meta information which are required for later processing. Typical scenarios are to tell the translation service how many characters are allowed within a String or to tell the XLIFF importer which grammar is used for the contained Markup.
For details have a look at TranslateProperty and PropertyHint.
XLIFF Export
As before, an exception provides further guidance how to continue as can be seen in Example 5.20, “Example for CapXliffExportException”.
com.coremedia.cap.translate.xliff.CapXliffExportException: Missing XLIFF export handler for property 'TranslateProperty[ id = property:markup:xml, propertyHints = [GrammarNameHint[value = http://www.w3.org/1999/xhtml]], value = <html xml:lang="en" ...]'. Please consider adding an appropriate handler of type 'com.coremedia.cap.translate.xliff.handler.XliffPropertyExportHandler' to your Spring context.
Example 5.20. Example for CapXliffExportException
You now have to implement a strategy to transform XHTML into a representation which contains
the texts to translate as XLIFF <trans-unit>
elements. The strategy is
created by implementing
XliffPropertyExportHandler.
public class XliffXhtmlPropertyExportHandler implements XliffPropertyExportHandler { private static final String XHTML_GRAMMAR_NAME = "http://www.w3.org/1999/xhtml"; @Override public boolean isApplicable(TranslateProperty property) { return property.getValue() instanceof Markup && property.getGrammarName() .map(XHTML_GRAMMAR_NAME::equals) .orElse(false); } @Override public Optional<Group> toGroup(TranslateProperty property, Supplier<String> idProvider, XliffExportOptions xliffExportOptions) { return Optional.ofNullable( toGroup(property.getId(), (Markup) property.getValue(), idProvider, xliffExportOptions ) ); } @Nullable private static Group toGroup(String id, Markup value, Supplier<String> idProvider, XliffExportOptions xliffExportOptions) { if (!MarkupUtil.hasText(value, true) && xliffExportOptions.isEmptyIgnore()) { // Don't add anything to translate for empty XHTML. return null; } Group markupGroup = new XhtmlToXliffConverter(idProvider, xliffExportOptions) .apply(value); Group result = new Group(); // Required: Set the property name. This is important for // the import process later on, to identify the property // to change. result.setResname(id); result.setDatatype(DatatypeValueList.XHTML.value()); result.getGroupOrTransUnitOrBinUnit().add(markupGroup); return result; } }
Example 5.21. PropertyExportHandler for XHTML
Example 5.21, “PropertyExportHandler for XHTML” contains the base for such a handler. It especially creates an XLIFF group object, which contains the property name. For mapping XHTML to XLIFF the handler delegates to another class in this case, as this is the most complex task during XLIFF export.
public class XhtmlToXliffConverter implements Function<Markup, Group> { private final Supplier<String> transUnitIdProvider; private final XliffExportOptions xliffExportOptions; public XhtmlToXliffConverter(Supplier<String> transUnitIdProvider, XliffExportOptions ) { this.transUnitIdProvider = transUnitIdProvider; this.xliffExportOptions = xliffExportOptions; } @Override public Group apply(Markup markup) { XhtmlContentHandler handler = new XhtmlContentHandler(); markup.writeOn(handler); return handler.getResult(); } private class XhtmlContentHandler extends DefaultHandler { // First element in stack is the currently modified element. private final Deque<Object> xliffStack = new LinkedList<>(); private Group result; @Override public void startElement(String uri, String localName, String qName, Attributes attributes) { String typeId = localName.toLowerCase(Locale.ROOT); Group elementWrapper = new Group(); if (result == null) { result = elementWrapper; } elementWrapper.setType(getType(typeId)); addElement(elementWrapper); } private void addElement(Object elementWrapper) { Object first = xliffStack.peekFirst(); if (first instanceof ContentHolder) { ContentHolder contentHolder = (ContentHolder) first; contentHolder.getContent().add(elementWrapper); } xliffStack.push(elementWrapper); } @Override public void endElement(String uri, String localName, String qName) { // remove element from xliffStack while (!xliffStack.isEmpty()) { Object object = xliffStack.pop(); if (object instanceof TypeHolder) { TypeHolder typeHolder = (TypeHolder) object; if (getType(localName).equals(typeHolder.getType())) { break; } } } } @Override public void characters(char[] ch, int start, int length) { String content = new String(ch, start, length); TransUnit transUnit = requireTransUnit(); transUnit.getSource().getContent().add(content); if (xliffExportOptions.getTargetOption() == XliffExportOptions.TargetOption.TARGET_SOURCE) { transUnit.getTarget().getContent().add(content); } } private TransUnit requireTransUnit() { Object first = xliffStack.peekFirst(); TransUnit transUnit; if (first instanceof TransUnit) { transUnit = (TransUnit) first; } else { transUnit = new TransUnit(); transUnit.setId(transUnitIdProvider.get()); transUnit.setSource(new Source()); addElement(transUnit); if (xliffExportOptions.getTargetOption() != XliffExportOptions.TargetOption.TARGET_OMIT) { transUnit.setTarget(new Target()); } } return transUnit; } private String getType(String typeId) { return "x-html-" + typeId.toLowerCase(Locale.ROOT); } private Group getResult() { // possibly check that the result is actually set return result; } } }
Example 5.22. XhtmlToXliffConverter
Example 5.22, “XhtmlToXliffConverter” is a very basic implementation of such a converter (it ignores for example any attributes). It contains the most relevant aspects, that you have to take into account while generating XLIFF structures.
Elements of XhtmlToXliffConverter
transUnitIdProvider
Each
<trans-unit>
has to get a unique ID. This ID is supplied by thetransUnitIdProvider
.xliffExportOptions
The options will especially tell you, how to deal with target nodes. You may for example have to create empty target nodes in
<trans-unit>
elements or you have to copy the value contained in the source node to the target node.XhtmlContentHandler
Markup can be written to a SAX
ContentHandler
. Use this handler, for going through the elements and creating a parallel XLIFF structure. For convenience you use the implementation SAXDefaultHandler
, so that you only have to implement the methods you are interested in.startElement
For any element in XHTML, you need to create a representation in XLIFF. Regarding the type identifier you can use any String value - you need this value later on to create the corresponding XHTML element. Thus, it makes sense, that the ID contains a reference to the corresponding XHTML element.
addElement
The XLIFF JAXB Classes contain some convenience methods and interfaces such as ContentHolder to easily identify XLIFF elements which may have some content.
endElement
This method just updates the element stack, so that new elements get added at the expected location.
characters
Obviously, characters are those elements of your XHTML which you want to translate. Thus, you need to add the characters to a
<trans-unit>
node. Depending on the configured XLIFF export options, you also need to add them to the target node.Do not forget to set the
<trans-unit>
ID here.
Marshalling Failures
If marshalling the XLIFF structure fails, ensure that you check that your generated XLIFF structure is valid. The JAXB wrapper classes do not provide much support there. Thus, it is always recommended having your XLIFF 1.2 specification at hand.
If everything is implemented correctly, an XHTML property as in Example 5.23, “XHTML Example Input” will be transformed to XLIFF as in Example 5.24, “XHTML as XLIFF Example Output”, which again will be part of the XLIFF file representing the content containing the property.
<html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>abcdefg</title> </head> <body> <p>abcdefg</p> </body> </html>
Example 5.23. XHTML Example Input
<group datatype="xhtml" resname="property:markup:xml"> <group restype="x-html-html"> <group restype="x-html-head"> <group restype="x-html-title"> <trans-unit id="12"> <source>abcdefg</source> <target>abcdefg</target> </trans-unit> </group> </group> <group restype="x-html-body"> <group restype="x-html-p"> <trans-unit id="13"> <source>abcdefg</source> <target>abcdefg</target> </trans-unit> </group> </group> </group> </group>
Example 5.24. XHTML as XLIFF Example Output
XLIFF Import
Having implemented all the above, let's handle the XLIFF import now. Without further changes, the just exported property will be ignored on XLIFF import, and no change will be applied to the target content item.
Extending XLIFF Schema
XLIFF 1.2 specification may be extended by custom attributes. If you have enabled XLIFF schema validation for XLIFF importer, and if you generated extended attributes you may have to add additional XLIFF schema sources. For details have a look at Section “Handling Attributes”.
In order to import the xhtml
property, you have to implement an
XliffPropertyImportHandler
and add it as bean to the Spring context.
public class XliffXhtmlPropertyImportHandler implements XliffPropertyImportHandler { private static final String XHTML_GRAMMAR_NAME = "http://www.w3.org/1999/xhtml"; @Override public boolean isAccepting(CapPropertyDescriptor property) { if (property.getType() != CapPropertyDescriptorType.MARKUP) { return false; } XmlGrammar grammar = ((MarkupPropertyDescriptor) property).getGrammar(); return grammar != null && XHTML_GRAMMAR_NAME.equals(grammar.getName()); } @Nullable @Override public Object convertXliffToProperty( CapPropertyDescriptor descriptor, Group group, @Nullable Object originalPropertyValue, String targetLocale, XliffImportResultCollector result) { Group markupGroup = (Group) group.getContent().get(0); Markup markup = new XliffToXhtmlConverter().apply(markupGroup); if (!descriptor.isValidValue(markup)) { result.addItem(XliffImportResultCode.INVALID_MARKUP); } return markup; } }
Example 5.25. XliffXhtmlPropertyImportHandler
Example 5.25, “XliffXhtmlPropertyImportHandler” shows a very simplistic implementation of an
import handler. It decides if it is responsible for importing into the given property, and if
it is, the method convertXliffToProperty
will be called. Many of the
arguments provide context information which may be used for further validation. You only use
the group
to parse the XLIFF and the descriptor
to
validate the resulting value. The group
handed over is the group node you
added before, which contains the reference to the property. Thus, the relevant content for you
to parse is the first group contained in it.
As you can see, it is important that XLIFF export and import go hand in hand, to understand each other. Adapting XLIFF export almost always means to adapt the XLIFF import, too.
The XliffXhtmlPropertyImportHandler
delegates further processing to a
converter which will now generate the Markup as can be seen in
Example 5.26, “XliffToXhtmlConverter”.
public class XliffToXhtmlConverter
implements Function<Group, Markup> {
private static final String XHTML_GRAMMAR_NAME =
"http://www.w3.org/1999/xhtml";
@Override
public Markup apply(Group group) {
Document document = MarkupFactory.newDocument(
XHTML_GRAMMAR_NAME,
elementName(group),
null
);
Element documentElement = document.getDocumentElement();
documentElement.setAttribute("xmlns", XHTML_GRAMMAR_NAME);
processContents(group, documentElement);
return MarkupFactory.fromDOM(document);
}
private static void processContents(ContentHolder contentHolder,
Element parent) {
for (Object content : contentHolder.getContent()) {
if (content instanceof String) {
processString(parent, (String) content);
} else if (content instanceof Group) {
processGroup(parent, (Group) content);
} else if (content instanceof TransUnit) {
TransUnit transUnit = (TransUnit) content;
processContents(transUnit.getTarget(), parent);
}
}
}
private static void processGroup(Node parent, Group group) {
Document document = parent.getOwnerDocument();
Element node = document.createElement(elementName(group));
parent.appendChild(node);
processContents(group, node);
}
private static void processString(Node parent, String text) {
Document document = parent.getOwnerDocument();
Node node = document.createTextNode(text);
parent.appendChild(node);
}
private static String elementName(TypeHolder group) {
return group.getType().replace("x-html-", "");
}
}
Example 5.26. XliffToXhtmlConverter
Methods of XliffXhtmlPropertyImportHandler
apply
Create a document from the main group which you may later transform to Markup.
processContents
Steps through the elements of a
ContentHolder
(again a convenience interface for XLIFF) and decides based on the (XLIFF) class type what to do. Strings will be the translated text, Groups represent nested elements and TransUnits contain the translated text in their target nodes.
The other methods just create the corresponding nodes for elements or texts.
Backward Compatibility
Especially for the import handler it is important to respect backward compatibility if necessary. The import handler may have to deal with XLIFF documents which were sent to a translation service weeks or month before.
Done
Having all this, you are done with this initial example and you got to know most of the API entry points you will need for customized XLIFF export. You learned about the initial filtering, the transformation to XLIFF and the transformation from XLIFF to your new translated property value.
Handling Attributes
In the previous sections, handling attributes was skipped in order to keep the example simple. But when it comes to attributes, you not only need to decide which of them should be translatable and which not, you also need to care of XLIFF validation, if you turned XLIFF validation on.
Start extending Example 5.22, “XhtmlToXliffConverter” by a method to map arguments.
public class XhtmlToXliffConverter implements Function<Markup, Group> { /* ... */ private class XhtmlContentHandler extends DefaultHandler { /* ... */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) { /* ... */ mapAttributes(attributes, elementWrapper); addElement(elementWrapper); } private void mapAttributes(Attributes attributes, Group elementWrapper) { for (int i = 0; i < attributes.getLength(); i++) { String localName = attributes.getLocalName(i); String attributeValue = attributes.getValue(i); switch (localName) { case "title": case "alt": case "summary": elementWrapper .getContent() .add( createTransUnit( "x-html-attr-" + localName, attributeValue ) ); break; default: // Wraps the attribute to a custom namespace. QName xliffAttrQName = new QName( "http://www.mycompany.com/custom-xliff-1.0", localName ); elementWrapper.getOtherAttributes().put(xliffAttrQName, attributeValue); } } } private TransUnit createTransUnit(String resType, String value) { TransUnit transUnit = new TransUnit(); transUnit.setId(transUnitIdProvider.get()); transUnit.setType(resType); Source source = new Source(); transUnit.setSource(source); source.getContent().add(value); if (xliffExportOptions.getTargetOption() != XliffExportOptions.TargetOption.TARGET_OMIT) { Target target = new Target(); transUnit.setTarget(target); if (xliffExportOptions.getTargetOption() == XliffExportOptions.TargetOption.TARGET_SOURCE) { target.getContent().add(value); } } return transUnit; } /* ... */ } }
Example 5.27. Attribute Export
In Example 5.27, “Attribute Export” a new method
mapAttributes
is added, which distinguishes between attributes to
translate, and attributes not to translate.
Translatable Attributes: For translatable attributes you just
add a <trans-unit>
element which also specifies a
restype
attribute. This attribute has to be resolved later on to a
property. The rest is very similar to the characters
method in the
initial example.
Non-Translatable Attributes: Non-translatable attributes,
are represented as custom attributes within XLIFF, as XLIFF Schema allows adding
such custom attributes at many places. In order to be prepared for XLIFF validation, CoreMedia
recommends adding a namespace URI to the attribute, here
http://www.mycompany.com/custom-xliff-1.0
.
<html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>abcdefg</title> </head> <body> <p class="some--class" title="some title">abcdefg</p> </body> </html>
Example 5.28. XHTML Example Input (Attributes)
<group restype="x-html-p" ns4:class="some--class" xmlns:ns4="http://www.mycompany.com/custom-xliff-1.0"> <trans-unit id="14" restype="x-html-attr-title"> <source>some title</source> <target>some title</target> </trans-unit> <trans-unit id="15"> <source>abcdefg</source> <target>abcdefg</target> </trans-unit> </group>
Example 5.29. XHTML as XLIFF Example Output (Attributes)
Given an example XHTML like in Example 5.28, “XHTML Example Input (Attributes)” the paragraph will be transformed as given in Example 5.29, “XHTML as XLIFF Example Output (Attributes)”. As you can see, the class attribute is added with a custom namespace, and the title attribute is added as trans-unit at first place within the group.
[Fatal Error] cvc-complex-type.3.2.2: Attribute 'ns4:class' is not allowed to appear in element 'group'. (23, 8)
Example 5.30. XLIFF Validation Error
XLIFF Validation: If XLIFF validation is turned on, the XLIFF import will fail to recognize the custom workspace and raises an error like in Example 5.30, “XLIFF Validation Error”. What you need to do now, is to create a schema defining your new attributes and to add the schema to the Spring application context.
<?xml version="1.0" encoding="UTF-8"?> <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://www.mycompany.com/custom-xliff-1.0" xml:lang="en"> <xsd:attribute name="class" type="xsd:string"/> </xsd:schema>
Example 5.31. Custom XLIFF XSD
@Bean @Scope(BeanDefinition.SCOPE_SINGLETON) public XliffSchemaSource<Source> customXliffSchema() { return () -> new StreamSource( getClass().getResourceAsStream("/custom-xliff-1.0.xsd") ); }
Example 5.32. Custom XLIFF XSD (Bean)
Example 5.31, “Custom XLIFF XSD” adds the class
attribute to the new
custom namespace. Note, that you have to do this for every attribute, you want to support. In
Example 5.32, “Custom XLIFF XSD (Bean)” you see, how to add the new schema to the application
context, so that it can be used for validation. Having this, you are prepared for the actual
XLIFF import handling.
For more details on XLIFF Validation Schemes, see the corresponding Javadoc of XliffSchemaSource and the configuration class adding standard schema required for validation XliffSchemaConfiguration. Refer to the 'See also' sections for links to the schemas.
XLIFF Import: Now you are going to extend the
XliffToXhtmlConverter
as given in
Example 5.26, “XliffToXhtmlConverter”.
public class XliffToXhtmlConverter implements Function<Group, Markup> { /* ... */ private static void processContents(ContentHolder contentHolder, Element parent) { for (Object content : contentHolder.getContent()) { if (content instanceof String) { processString(parent, (String) content); } else if (content instanceof Group) { processGroup(parent, (Group) content); } else if (content instanceof TransUnit) { TransUnit transUnit = (TransUnit) content; String type = attributeName(transUnit); if (type.isEmpty()) { processContents(transUnit.getTarget(), parent); } else { processTranslatableAttribute(type, transUnit.getTarget(), parent); } } } } private static void processTranslatableAttribute(String type, Target target, Element parent) { StringBuilder value = new StringBuilder(); for (Object content : target.getContent()) { value.append(content); } parent.setAttribute(type, value.toString()); } private static String attributeName(TypeHolder group) { String type = group.getType(); if (type == null || type.isEmpty()) { return ""; } return type.replace("x-html-attr-", ""); } /* ... */ }
Example 5.33. Importing Translatable Attributes
In Example 5.33, “Importing Translatable Attributes” add importing the
translatable attributes. Add a branch for <trans-unit>
elements, to detect that they represent a translatable attribute, if the type is set.
public class XliffToXhtmlConverter implements Function<Group, Markup> { /* ... */ private static void processGroup(Node parent, Group group) { Document document = parent.getOwnerDocument(); Element node = document.createElement(elementName(group)); processNonTranslatableAttributes(node, group); parent.appendChild(node); processContents(group, node); } private static void processNonTranslatableAttributes(Element node, Group group) { Map<QName, String> otherAttributes = group.getOtherAttributes(); for (Map.Entry<QName, String> entry : otherAttributes.entrySet()) { QName attributeQName = entry.getKey(); String localPart = attributeQName.getLocalPart(); node.setAttribute(localPart, entry.getValue()); } } /* ... */ }
Example 5.34. Importing Non-Translatable Attributes
In Example 5.34, “Importing Non-Translatable Attributes” the custom attributes handed over to the group element are parsed and set as corresponding attribute for the DOM element. Note, that in order to shorten the code, the namespace URI is not checked.
Now you are done, and can support custom attributes, translatable or non-translatable.