Description
openedon Sep 12, 2018
Problem: Most any sprintf
implementation, including that of @wordpress/i18n
, is expected to return a string value. In a React application, this can be problematic if the string contains HTML markup, as the HTML will be escaped, at least without using dangerouslySetInnerHTML, which is discouraged. This can sometimes lead to developers implementing workarounds using concatenation of partial localized strings (e.g. #5767 (comment)), which is not advisable as it cannot be translated accurately in many languages. This is also noted in the i18n for WordPress Developers guide:
Use format strings instead of string concatenation—
sprintf(__('Replace %1$s with %2$s'), $a, $b);
is always better than__('Replace ').$a.__(' with ').$b;
.
https://codex.wordpress.org/I18n_for_WordPress_Developers#Best_Practices
Task: Find a way to allow for React elements to be inserted within localized strings safely.
Prior Art:
- https://github.com/martinandert/react-interpolate-component
- https://github.com/Automattic/interpolate-components (Used by Calypso / WordPress.com)
- https://react.i18next.com/components/interpolate
- https://react.i18next.com/components/trans-component
Additional Considerations:
- We still need to be able to extract the string which would be surfaced to translators, presumably including placeholders where a
JSXExpressionContainer
occurs - There should be a reasonably-ergonomic offering for those developers who choose not to use Babel tooling
Brainstorming:
It would be nice if a developer could simply wrap their string and elements in a component which performs the localization:
<Interpolate>
Check out this link to my <a href={ url }>website</a>.
</Interpolate>
There are, of course, complications:
- How do we extract the string?
- Presumably it would take the form
Check out this link to my <a href="%s">website</a>
. We'd need to find a way to associate the JSX expression to the placeholder (and vice-versa, inject the variable value where the placeholder occurs when rendering into the UI).
- Presumably it would take the form
- Pluralization / Context
- Nested custom component elements
- For example, what if the website link was a
<WebsiteLink />
element?
- For example, what if the website link was a
- Encouraging best practices to avoid extracted strings being indecipherable
It might be simpler to associate placeholders via a known limited subset of expression values allowed. This could also be used for linting enforcement of best-practices:
<Interpolate>
{ ( getPlaceholder ) => (
<Fragment>Check out this link to my <a href={ getPlaceholder( '%s' ) }>website</a>.</Fragment>
) }
</Interpolate>
Aside: It is be feasible for a custom Babel plugin to transform an element like as written in the first snippet (which has arguably nicer semantics) to this latter format.
To address pluralization, perhaps having a special child component type mapping to the index of the plural form corresponding to the count (an advantage here is supporting >2 plural forms (correction below)):
<Interpolate count={ 2 }>
<Interpolate.PluralForm>
Check out this link to my <a href={ url }>website</a>.
</Interpolate.PluralForm>
<Interpolate.PluralForm>
Check out this link to my <a href={ url }>websites</a>.
</Interpolate.PluralForm>
</Interpolate>