Extract UI and DataViews translations to a separate package - #81035
Extract UI and DataViews translations to a separate package#81035jsnajdr wants to merge 7 commits into
Conversation
`@wordpress/ui` is a bundled package: it is compiled into whichever script imports it rather than being registered as a WordPress script of its own. Gettext calls in it are therefore attributed to the handle of the consuming bundle, so WordPress has no handle to load translations for and every string is shown in English. Add an externalized companion package to hold those strings. It has a script handle of its own (`wp-ui-i18n`), so WordPress loads its translations, and `@wordpress/ui` can read finished strings out of it. The catalog is empty here; the messages and the call sites follow separately. Co-authored-by: Cursor <cursoragent@cursor.com>
One entry per string that `@wordpress/ui` displays. Entries are functions rather than plain strings so that each gettext call runs only when that message is needed, instead of resolving the whole catalog when the module loads. The indirection also leaves room to resolve messages through something other than the global `__` later. Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the gettext calls in `@wordpress/ui` with catalog entries, so the labels are translated instead of always being shown in English. `@wordpress/i18n` is no longer used here at all, so drop the dependency and the TypeScript project reference along with it. Co-authored-by: Cursor <cursoragent@cursor.com>
…kage `@wordpress/dataviews` was made a bundled package in #72319, which silently stopped its strings from being translated: gettext calls in a bundled package are attributed to the handle of the consuming bundle, so WordPress has no handle to load translations for. Add an externalized companion package to hold those strings, registered as the `wp-dataviews-i18n` script so WordPress loads its translations. The package's own bundler config has to treat message catalogs as singletons, like it already does for `@wordpress/data` and friends. Bundling a catalog would put the strings back inside the consuming bundle and undo the fix. The catalog is empty here; the messages and the call sites follow separately. Co-authored-by: Cursor <cursoragent@cursor.com>
…ssages One entry per string that DataViews and DataForms display. Entries are functions rather than plain strings so that each gettext call runs only when that message is needed: a consumer displays a handful of these 154 messages, and a catalog of constants would resolve all of them when the module loads. The indirection also leaves room to resolve messages through something other than the global `__` later. The catalog holds only what a translator needs — the source string, the context, the plural forms and the translator comment. Interpolation stays with the caller, so an entry with placeholders returns the format string. Plural entries are the exception, since gettext needs the count to pick a form. Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the gettext calls in `@wordpress/dataviews` with catalog entries, so DataViews and DataForms strings are translated again instead of being shown in English. Only the gettext call moves. `sprintf`, `createInterpolateElement` and `isRTL` stay where they were: the catalog returns the format string and interpolation continues to happen at the call site. Co-authored-by: Cursor <cursoragent@cursor.com>
Nothing stopped a new `__()` from being added back to `@wordpress/ui` or `@wordpress/dataviews`, where it would silently ship untranslated. Restrict the gettext imports in those packages and point at the catalog to use instead. `sprintf` and `isRTL` stay allowed: neither is a gettext call, and both are still used at the call sites. Stories and tests are exempt, since they ship nowhere. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Warning: Type of PR label mismatch To merge this PR, it requires exactly 1 label indicating the type of PR. Other labels are optional and not being checked here.
Read more about Type labels in Gutenberg. Don't worry if you don't have the required permissions to add labels; the PR reviewer should be able to help with the task. |
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
tyxla
left a comment
There was a problem hiding this comment.
To be honest, having to create extra packages for just the translations feels like too much. I'm worried by the precedent this is establishing - each bundled package needing its own corresponding i18n package - it looks like a major architectural flaw. And as you described, it comes with its own set of additional tradeoffs, some of which have serious downsides:
- Potential crashes - that's the one that worries me most. Especially considering that this is technically introducing a new set of public APIs. Missing translations or missing keys must never crash the packages IMO.
- Potentially missing translations - this is what we're trying to avoid, no?
- Creating new problems - like concurrency when multiple versions of the i18n packages are loaded
I wonder if we could try a "simpler" alternative. For example, have we considered keeping ordinary gettext calls beside their translator comments? During the package build, extract them, generate the Core catalog, and rewrite bundled output to a safe lookup containing the English fallback, which will help preserves locality and avoids the shallow interface introduced for DataViews.
Alternatively, as a short-term solution, a way is to rewrite into a "consumer" textdomain, similar to how some plugins like Jetpack implement it (see https://github.com/Automattic/jetpack/tree/trunk/projects/js-packages/babel-plugin-replace-textdomain). It works on old WordPress and requires no Core changes, but a downside is that it duplicates translations and permits inconsistent wording between plugins.
It also makes me think if it's a good time to explore a better long-term solution for those i18n problems. Especially considering that all plugins that use those bundled packages are also currently hitting those problems (for example, WooCommerce is one where we've already seen reports for). One way could be to extend the generated asset metadata with bundled package identity, version/hash etc. WordPress.org would use it to avoid attributing package strings to plugins; Core would use it to load multiple shared catalogs without treating them as hard JavaScript dependencies. Of course, this would require coordinated changes across Gutenberg tooling, WordPress.org, language-pack generation, and Core.
I wonder what other alternatives we've considered.
|
Did we consider a build tool solution for this instead? Like having a the wp-build, scripts tooling add the right plugin domain when bundling these packages? |
Not sure what you mean by this, but it's possible to keep the actual string as the key in the message catalog: import i18n from '@wordpress/ui-i18n';
i18n.__( 'Clear' );and the actually extracted translation is also in catalog[ 'Clear' ] = () => __( 'Clear' );The main change is that we're replacing the symbolic
Yes, this is an alternative solution that is already used in practice. Rewrite the textdomain and duplicate the string. Translation tools typically have a "translation memory" feature that lets you quickly match and reuse already translated strings.
This could be another viable solution. The bundled packages would use a custom textdomain for their translations: __( 'Clear', 'wordpress-ui' );
__( 'Optional', 'wordpress-dataviews' );These textdomains end up in the bundled JS asset, and are extracted by GlotPress/ When generating the language pack for the plugin and the JS asset, GlotPress could include the Then we need to patch the Core function that prints the translations ( |
Maybe something long those lines is the build tool solution @youknowriad has in mind? |
|
The problem with the "core" domain or the custom ones per package is the divergence between the strings available in Glotpress and the code, there's no easy solutions here. (you could have strings on glotpress not available in the version used by the plugin or vice versa). |
That sounds more like the Jetpack solution with |
Sure, yes, there's duplication but I still think it's the best tradeoff personally. |
Is this really different from any other situation where there are new Glotpress translations, and not everything is yet 100% translated? When releasing a new version of a bundled library, make sure that the Glotpress project is updated accordingly. Not really different from releasing a new version of a plugin. |
It's not just: not everything is 100% translated, it's also, some strings have been removed I think and will never be translated (using an old version with an old string) |

Implements @aduth's idea from #80714 (comment): extract the
wp-uiandwp-dataviewstranslations to a separate non-bundled packageswp-ui-i18nandwp-dataviews-i18n.When a plugin bundles one of these libraries, the
__()calls are no longer present in the bundled asset. They are in an external package that is provided by WordPress itself. Or someone else (?)Instead of:
we now write:
The
uianddataviewsi18n catalogs thus become part of Core translations.Open questions:
FOOmessage, callingi18n.FOO()will fail. This is solvable by exporting a proxy object that implements a fallback getter.wp-ui-i18n, and the best match wins at runtime?