Make WordPress Core

Opened 18 months ago

Last modified 7 months ago

#22229 accepted enhancement

Plurals in JavaScript

Reported by: nacin Owned by: nbachiyski
Milestone: Future Release Priority: low
Severity: normal Version:
Component: I18N Keywords: has-patch
Focuses: Cc:


This is something koopersmith needs in the media modal, and I've seen a few other recent use cases.

Attachments (4)

22229.diff (2.0 KB) - added by nacin 18 months ago.
22229.2.diff (2.0 KB) - added by nacin 18 months ago.
Alternate script loader syntax
js-plurals.diff (10.1 KB) - added by nbachiyski 18 months ago.
22229.3.diff (11.2 KB) - added by nbachiyski 7 months ago.

Download all attachments as: .zip

Change History (30)

nacin18 months ago

nacin18 months ago

Alternate script loader syntax

comment:1 scribu18 months ago

  • Type changed from defect (bug) to enhancement

Why is wp_js_i18n_plural() even necessary? What does it do?

comment:2 nacin18 months ago

In WordPress, we register plural strings in a POT file like so:

_n( '%d string', '%d strings' );

English has two forms: a singular form (n = 1) and a plural form for n != 1. Other languages have anywhere from one (like Japanese) to four forms (like Slovenian). I'm sure you are familiar with Romanian's three forms.

Using _n() alone is not sufficient. You also have to pass the number of items, so the proper translation is chosen for the language to which you are translating. So, _n( '%d string', '%d strings', $number_of_strings );. And, you will get back a string with %d (or %s, if the number could be sufficiently large enough to possibly need comma separators with number_format_i18n()). This is why we then sprintf() the result. So:

printf( _n( '%d string', '%d strings', $number_of_strings ), $number_of_strings );


printf( _n( '%s string', '%s strings', $number_of_strings ), number_format_i18n( $number_of_strings ) );

For JavaScript, we obviously do not have access to core's i18n libraries. So we pass strings via the script-loader. For example, postL10n.publish is __( 'Publish' );. But in order to translate a plural, we need to know the count of items (n), which then gets passed to an expression to determine the plural form to be used based.

What wp_js_i18n_plural() does is provide a function in JS that can be called with the number of items, because these numbers are going to be dynamic. It does this by using the same plural expression (which takes n and returns the plural to use) and an array of all of the plural forms.

So rather than exampleL10n.numItems being '%d items', it can be this:

exampleL10n.numItems = function(n) {
	var i = (n != 1),
		t = {"0":"%d item","1":"%d items"};
	if (typeof i === 'boolean') i = i ? 1 : 0;
	return i > 1 ? t[1] : t[i];

And then instead of invoking exampleL10n.numItems, you invoke exampleL10n.numItems(5), and get back %d items'. Or, if called with (1)`, you'd get back '%d item'.

The patch needs some work. One, we need to decide whether the JS function should do the sprintf automatically (my example does not; the patch does). Two, some of this logic needs to make its way back into the pomo classes, as right now it is dependent on Gettext_Translations to function.

comment:3 follow-up: scribu18 months ago

Ok, so instead of generating a new JS function for each string, how about we make a single generic JS function that gets run for all the strings? Something like the following:

wp.i18n._n(exampleL10n.numItems, 5);

comment:4 scribu18 months ago

This would work, because the plural expression is the same for all the strings in a particular language.

We could even keep the pretty syntax, but implement the JS function generator in JS, rather than PHP. This would make the code both cleaner and more compact.

comment:5 nbachiyski18 months ago

Nacin, I like the idea.

A couple of notes on the implementation:

  • I would hide the big if in a Translations method like translate_entry_or_return_entry(), which instead of false returns the entry itself. This way you won't need the special case and the code will be easier to follow.
  • The default number of plurals and the default expression do not belong to this part of the code. We're dealing with JS here, not with gettext specifics. I have two ideas around this problem:
    1. Make nplurals_and_expression_from_header() and parenthesize_plural_exression() static. Then just call Gettext_Translations::nplurals_and_expression_from_header( $mo->get_header( 'Plural-Forms' ) );. Or better, create an instance method of Gettext_Translations called nplurals_and_expression(), which uses the header from the instance.
    2. We can create a NOOP_Gettext_Translations, which extends Gettext_Translations and all the functionality, except actually translating is still there. The note about nplurals_and_expression() still applies.
  • I agree with scribu that we shouldn't create a new function each time. I think we should keep the pretty syntax of numItems(5) (it's pretty and I don't need to remember any other function names) and we should have a function generator, building a single function and just varying on singular/plural.
Last edited 18 months ago by nbachiyski (previous) (diff)

comment:6 in reply to: ↑ 3 ; follow-up: nacin18 months ago

Replying to scribu:

Ok, so instead of generating a new JS function for each string, how about we make a single generic JS function that gets run for all the strings? Something like the following:

wp.i18n._n(exampleL10n.numItems, 5);

I've been playing around with different syntaxes but couldn't get it just right. This looks interesting, though I agree with nikolay that the pretty function syntax is quite nice. The patch was definitely written as a temporary rush-plurals-into-JS solution, not an API that would stand the test of time.

The main thing I couldn't figure out was where wp.i18n would live. A new file that is a dependency of common.js? If we print the plural expression through script-loader, we'd have to then eval() it, because it's an expression, not a string. So, we probably have to print it via some other means.

I'm curious what you see the role of NOOP_Gettext_Translations as. That would be used in place of NOOP_Translations, I imagine?

comment:7 follow-up: scribu18 months ago

I'm curious what you see the role of NOOP_Gettext_Translations as.

I don't think we've met. :)

The main thing I couldn't figure out was where wp.i18n would live. A new file that is a dependency of common.js?

It could be an inline script, as in the current patch, except it would be spit out only once (possibly in the header) and then used for all the strings.

comment:8 in reply to: ↑ 7 nacin18 months ago

Replying to scribu:

I'm curious what you see the role of NOOP_Gettext_Translations as.

I don't think we've met. :)

Sorry, that was in reply to nbachiyski.

comment:9 in reply to: ↑ 6 nbachiyski18 months ago

Replying to nacin:

I'm curious what you see the role of NOOP_Gettext_Translations as. That would be used in place of NOOP_Translations, I imagine?

Yes, it would contain gettext-specific methods, hence the name. Doesn't matter much, since I doubt somebody ever used this class outside of core.

comment:10 nbachiyski18 months ago

  • Owner set to nbachiyski
  • Status changed from new to accepted

comment:11 nbachiyski18 months ago

On our way back from the Summit we sat together with duck_ and thought a bit about that and wrote some of the code in planes and airports.


We have a couple of pieces:

  • Factory JS function. wp.i18n.make_plural(translations, domain). Returns a function, which accepts a number and returns the proper plural form. It needs the domain, because different domains, even using the same language, can be using different plural rules for some weird reason. Also, this easily allows us to have untranslated domains without introducing a special factory function for English.
  • Around the factory we need two structures for plural information (number of plurals and the ternary operator logic). These are wp.i18n.domains_plural_info and wp.i18n.english_plural_info. We need the latter, so that we can handle untranslated strings within a domain.
  • We need a way to output the make_plural() calls in the localization object for a script. Here it gets tricky.
    • First, we would like to hide it in a function, because the Nacin's l10n_print_after hack is very error-prone, because it's long and not obvious, makes it hard to add more than one plural, and includes the object's name, which we will for sure forget to update when we rename the object. Let's call this function _n_js(). I added it to makepot.php.
    • Then, we need to assign the literal JavaScript, not a string to the key in the localization object. This proved hard, because we run json_encode() on the whole localization object and we don't have a way to tell it not to encode some values.
    • Enter WP_JS_Literal. This is a small class (+ a convenience function), which represents a literal JS value, which shouldn't be encoded. The easiest way to use it is to do:
      wp_localize_script( 'handle', array( 'name' => 'string', 'code' => wp_js_make_literal( 'f()' ) ), 'l10n' )
      l10n = {
      	name: 'string',
      	code: f()
  • The next step is how to output the make_plural definition only once, without many hacks. Since the JS literal is essentially code, w can allow it to have a dependancy. Then, when used in localizing a script, we just add this dependancy to its parent script.
  • But we don't want another JS file, just for a couple lines of code, we want it inline. So, with a small change to the logic of printing scripts, we can make scripts with source of inline to only add its data and not try to load its src.

You can find tests in [UT1115] and [UT1116].

nbachiyski18 months ago

comment:12 nacin18 months ago

  • Type changed from enhancement to task (blessed)

comment:13 nbachiyski18 months ago

  • Keywords has-patch added

comment:14 nacin18 months ago

  • Priority changed from normal to low

This looks great. It's also quite a bit of extra API. Holding off here until we figure out exactly what we need plurals for in 3.5.

comment:15 nacin17 months ago

  • Milestone changed from 3.5 to Future Release
  • Type changed from task (blessed) to enhancement

We can pull this back in 3.5 if we have an urgent need.

comment:16 pavelevap17 months ago

There is for example string "selected". But in Czech it is 1 "zvolen", 2 "zvoleny" a 5 "zvoleno". We really need plurals in WP 3.5.

comment:17 pavelevap17 months ago

Is it possible to add plurals to string "selected"? Or change it to "%s selected" to allow playing with order?

comment:18 pavelevap17 months ago

Nice, my problem with "selected" string will be partially solved here: #22749

comment:19 knutsp17 months ago

Sorry, but lack of plurals is almost a blocker, at least seen from a translators view.

comment:20 nacin17 months ago

In 23075:

Media: Use '%d selected' for the selection string, and offer a comment to translators to help them find a workable solution with this would-be plural string. fixes #22749. see #22229.

comment:21 nbachiyski15 months ago

How about we get this early in 3.6?

comment:22 nbachiyski14 months ago

Ping. Let's commit this.

comment:23 adamsilverstein13 months ago

  • Cc adamsilverstein@… added

comment:24 johnbillion11 months ago

  • Cc johnbillion added

comment:25 nacin7 months ago

In 25711:

Remove tests for code still in development, see #22229. If accidentally run, they produce fatal errors.

props pauldewouters, no_fear_inc.

comment:26 nbachiyski7 months ago

Here is the previous patch, but with the tests included (and working on current trunk).

nbachiyski7 months ago

Note: See TracTickets for help on using tickets.