close
Skip to content

Extract UI and DataViews translations to a separate package - #81035

Open
jsnajdr wants to merge 7 commits into
trunkfrom
add/bundled-i18n-catalogs
Open

Extract UI and DataViews translations to a separate package#81035
jsnajdr wants to merge 7 commits into
trunkfrom
add/bundled-i18n-catalogs

Conversation

@jsnajdr

@jsnajdr jsnajdr commented Jul 31, 2026

Copy link
Copy Markdown
Member

Implements @aduth's idea from #80714 (comment): extract the wp-ui and wp-dataviews translations to a separate non-bundled packages wp-ui-i18n and wp-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:

import { __ } from '@wordpress/i18n';
__( 'Clear' );

we now write:

import i18n from '@wordpress/ui-i18n';
i18n.CLEAR();

The ui and dataviews i18n catalogs thus become part of Core translations.

Open questions:

  1. If the translations are now part of Core, then a plugin that bundles a new version of a library, and runs on an old version, can miss some new translations. They are not part of the old language pack. Are we OK with that?
  2. Missing translations are currently going to crash. If there is no FOO message, calling i18n.FOO() will fail. This is solvable by exporting a proxy object that implements a fallback getter.
  3. Could we possibly ship the translations also with the plugin? Like, registering multiple versions of wp-ui-i18n, and the best match wins at runtime?

jsnajdr and others added 7 commits July 31, 2026 18:45
`@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>
@github-actions

Copy link
Copy Markdown

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.

  • Required label: Any label starting with [Type].
  • Labels found: .

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.

@github-actions github-actions Bot added [Package] DataViews /packages/dataviews [Package] UI /packages/ui labels Jul 31, 2026
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

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 props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: jsnajdr <jsnajdr@git.wordpress.org>
Co-authored-by: tyxla <tyxla@git.wordpress.org>
Co-authored-by: youknowriad <youknowriad@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@tyxla tyxla left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@youknowriad

Copy link
Copy Markdown
Contributor

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?

@jsnajdr

jsnajdr commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

For example, have we considered keeping ordinary gettext calls beside their translator comments?

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 @wordpress/ui-i18n:

catalog[ 'Clear' ] = () => __( 'Clear' );

The main change is that we're replacing the symbolic CLEAR constant with the actual string. That way, no information is really lost, it's still at both places and is potentially extractable.

rewrite into a "consumer" textdomain, similar to how some plugins like Jetpack implement it

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.

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.

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/make-pot. The tool drops the textdomain info, but the extraction is performed.

When generating the language pack for the plugin and the JS asset, GlotPress could include the wordpress-ui translations there, in the JSON chunk. The JSON format has support for multiple domains in one file.

Then we need to patch the Core function that prints the translations (print_translations in class-wp-scripts.php) to print translations not only for the default plugin domain, but also for additional domains in the same chunk.

@jsnajdr jsnajdr added the [Type] Enhancement A suggestion for improvement. label Aug 3, 2026
@tyxla

tyxla commented Aug 3, 2026

Copy link
Copy Markdown
Member

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.

This could be another viable solution. The bundled packages would use a custom textdomain for their translations:

Maybe something long those lines is the build tool solution @youknowriad has in mind?

@youknowriad

Copy link
Copy Markdown
Contributor

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).

@jsnajdr

jsnajdr commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Maybe something long those lines is the build tool solution @youknowriad has in mind?

That sounds more like the Jetpack solution with babel-plugin-replace-textdomain. That leads to each plugin domain having translation strings that the plugin author never added, and that need to be translated again over and over. Unless copying the shared translations is somehow automated.

@youknowriad

Copy link
Copy Markdown
Contributor

That sounds more like the Jetpack solution with babel-plugin-replace-textdomain. That leads to each plugin domain having translation strings that the plugin author never added, and that need to be translated again over and over.

Sure, yes, there's duplication but I still think it's the best tradeoff personally.

@jsnajdr

jsnajdr commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

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.

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.

@youknowriad

Copy link
Copy Markdown
Contributor

not everything is yet 100% translated?

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] DataViews /packages/dataviews [Package] UI /packages/ui [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants