diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md
index e9b5255dae5328..cf038c0a4fef9c 100644
--- a/packages/i18n/CHANGELOG.md
+++ b/packages/i18n/CHANGELOG.md
@@ -4,7 +4,7 @@
- Add `isRTL` function ([#20298](https://github.com/WordPress/gutenberg/pull/20298))
- Include TypeScript type declarations ([#18942](https://github.com/WordPress/gutenberg/pull/18942))
-
+- Add `createI18n` method to allow creation of multiple i18n instances ([#21182](https://github.com/WordPress/gutenberg/pull/21182))
## 3.1.0 (2018-11-15)
diff --git a/packages/i18n/README.md b/packages/i18n/README.md
index 21847b5f3f0172..f57ce4124c191b 100644
--- a/packages/i18n/README.md
+++ b/packages/i18n/README.md
@@ -27,6 +27,19 @@ For a complete example, see the [Internationalization section of the Block Edito
+# **createI18n**
+
+Create an i18n instance
+
+_Parameters_
+
+- _initialData_ `[LocaleData]`: Locale data configuration.
+- _initialDomain_ `[string]`: Domain for which configuration applies.
+
+_Returns_
+
+- `I18n`: I18n instance
+
# **isRTL**
Check if current locale is RTL.
diff --git a/packages/i18n/src/create-i18n.js b/packages/i18n/src/create-i18n.js
new file mode 100644
index 00000000000000..0b0b4cd1826d14
--- /dev/null
+++ b/packages/i18n/src/create-i18n.js
@@ -0,0 +1,200 @@
+/**
+ * External dependencies
+ */
+import Tannin from 'tannin';
+
+/**
+ * @typedef {Record} LocaleData
+ */
+
+/**
+ * Default locale data to use for Tannin domain when not otherwise provided.
+ * Assumes an English plural forms expression.
+ *
+ * @type {LocaleData}
+ */
+const DEFAULT_LOCALE_DATA = {
+ '': {
+ /** @param {number} n */
+ plural_forms( n ) {
+ return n === 1 ? 0 : 1;
+ },
+ },
+};
+
+/**
+ * An i18n instance
+ *
+ * @typedef {Object} I18n
+ * @property {Function} setLocaleData Merges locale data into the Tannin instance by domain. Accepts data in a
+ * Jed-formatted JSON object shape.
+ * @property {Function} __ Retrieve the translation of text.
+ * @property {Function} _x Retrieve translated string with gettext context.
+ * @property {Function} _n Translates and retrieves the singular or plural form based on the supplied
+ * number.
+ * @property {Function} _nx Translates and retrieves the singular or plural form based on the supplied
+ * number, with gettext context.
+ * @property {Function} isRTL Check if current locale is RTL.
+ */
+
+/**
+ * Create an i18n instance
+ *
+ * @param {LocaleData} [initialData] Locale data configuration.
+ * @param {string} [initialDomain] Domain for which configuration applies.
+ * @return {I18n} I18n instance
+ */
+export const createI18n = ( initialData, initialDomain ) => {
+ /**
+ * The underlying instance of Tannin to which exported functions interface.
+ *
+ * @type {Tannin}
+ */
+ const tannin = new Tannin( {} );
+
+ /**
+ * Merges locale data into the Tannin instance by domain. Accepts data in a
+ * Jed-formatted JSON object shape.
+ *
+ * @see http://messageformat.github.io/Jed/
+ *
+ * @param {LocaleData} [data] Locale data configuration.
+ * @param {string} [domain] Domain for which configuration applies.
+ */
+ const setLocaleData = ( data, domain = 'default' ) => {
+ tannin.data[ domain ] = {
+ ...DEFAULT_LOCALE_DATA,
+ ...tannin.data[ domain ],
+ ...data,
+ };
+
+ // Populate default domain configuration (supported locale date which omits
+ // a plural forms expression).
+ tannin.data[ domain ][ '' ] = {
+ ...DEFAULT_LOCALE_DATA[ '' ],
+ ...tannin.data[ domain ][ '' ],
+ };
+ };
+
+ /**
+ * Wrapper for Tannin's `dcnpgettext`. Populates default locale data if not
+ * otherwise previously assigned.
+ *
+ * @param {string|undefined} domain Domain to retrieve the translated text.
+ * @param {string|undefined} context Context information for the translators.
+ * @param {string} single Text to translate if non-plural. Used as
+ * fallback return value on a caught error.
+ * @param {string} [plural] The text to be used if the number is
+ * plural.
+ * @param {number} [number] The number to compare against to use
+ * either the singular or plural form.
+ *
+ * @return {string} The translated string.
+ */
+ const dcnpgettext = (
+ domain = 'default',
+ context,
+ single,
+ plural,
+ number
+ ) => {
+ if ( ! tannin.data[ domain ] ) {
+ setLocaleData( undefined, domain );
+ }
+
+ return tannin.dcnpgettext( domain, context, single, plural, number );
+ };
+
+ /**
+ * Retrieve the translation of text.
+ *
+ * @see https://developer.wordpress.org/reference/functions/__/
+ *
+ * @param {string} text Text to translate.
+ * @param {string} [domain] Domain to retrieve the translated text.
+ *
+ * @return {string} Translated text.
+ */
+ const __ = ( text, domain ) => {
+ return dcnpgettext( domain, undefined, text );
+ };
+
+ /**
+ * Retrieve translated string with gettext context.
+ *
+ * @see https://developer.wordpress.org/reference/functions/_x/
+ *
+ * @param {string} text Text to translate.
+ * @param {string} context Context information for the translators.
+ * @param {string} [domain] Domain to retrieve the translated text.
+ *
+ * @return {string} Translated context string without pipe.
+ */
+ const _x = ( text, context, domain ) => {
+ return dcnpgettext( domain, context, text );
+ };
+
+ /**
+ * Translates and retrieves the singular or plural form based on the supplied
+ * number.
+ *
+ * @see https://developer.wordpress.org/reference/functions/_n/
+ *
+ * @param {string} single The text to be used if the number is singular.
+ * @param {string} plural The text to be used if the number is plural.
+ * @param {number} number The number to compare against to use either the
+ * singular or plural form.
+ * @param {string} [domain] Domain to retrieve the translated text.
+ *
+ * @return {string} The translated singular or plural form.
+ */
+ const _n = ( single, plural, number, domain ) => {
+ return dcnpgettext( domain, undefined, single, plural, number );
+ };
+
+ /**
+ * Translates and retrieves the singular or plural form based on the supplied
+ * number, with gettext context.
+ *
+ * @see https://developer.wordpress.org/reference/functions/_nx/
+ *
+ * @param {string} single The text to be used if the number is singular.
+ * @param {string} plural The text to be used if the number is plural.
+ * @param {number} number The number to compare against to use either the
+ * singular or plural form.
+ * @param {string} context Context information for the translators.
+ * @param {string} [domain] Domain to retrieve the translated text.
+ *
+ * @return {string} The translated singular or plural form.
+ */
+ const _nx = ( single, plural, number, context, domain ) => {
+ return dcnpgettext( domain, context, single, plural, number );
+ };
+
+ /**
+ * Check if current locale is RTL.
+ *
+ * **RTL (Right To Left)** is a locale property indicating that text is written from right to left.
+ * For example, the `he` locale (for Hebrew) specifies right-to-left. Arabic (ar) is another common
+ * language written RTL. The opposite of RTL, LTR (Left To Right) is used in other languages,
+ * including English (`en`, `en-US`, `en-GB`, etc.), Spanish (`es`), and French (`fr`).
+ *
+ * @return {boolean} Whether locale is RTL.
+ */
+ const isRTL = () => {
+ return 'rtl' === _x( 'ltr', 'text direction' );
+ };
+
+ if ( initialData ) {
+ setLocaleData( initialData, initialDomain );
+ }
+
+ return {
+ setLocaleData,
+ __,
+ _x,
+ _n,
+ _nx,
+ isRTL,
+ };
+};
diff --git a/packages/i18n/src/default-i18n.js b/packages/i18n/src/default-i18n.js
new file mode 100644
index 00000000000000..c5fdc8c06548be
--- /dev/null
+++ b/packages/i18n/src/default-i18n.js
@@ -0,0 +1,96 @@
+/**
+ * Internal dependencies
+ */
+import { createI18n } from './create-i18n';
+
+const i18n = createI18n();
+
+/*
+ * Comments in this file are duplicated from ./i18n due to
+ * https://github.com/WordPress/gutenberg/pull/20318#issuecomment-590837722
+ */
+
+/**
+ * @typedef {import('./create-i18n').LocaleData} LocaleData
+ */
+
+/**
+ * Merges locale data into the Tannin instance by domain. Accepts data in a
+ * Jed-formatted JSON object shape.
+ *
+ * @see http://messageformat.github.io/Jed/
+ *
+ * @param {LocaleData} [data] Locale data configuration.
+ * @param {string} [domain] Domain for which configuration applies.
+ */
+export const setLocaleData = i18n.setLocaleData.bind( i18n );
+
+/**
+ * Retrieve the translation of text.
+ *
+ * @see https://developer.wordpress.org/reference/functions/__/
+ *
+ * @param {string} text Text to translate.
+ * @param {string} [domain] Domain to retrieve the translated text.
+ *
+ * @return {string} Translated text.
+ */
+export const __ = i18n.__.bind( i18n );
+
+/**
+ * Retrieve translated string with gettext context.
+ *
+ * @see https://developer.wordpress.org/reference/functions/_x/
+ *
+ * @param {string} text Text to translate.
+ * @param {string} context Context information for the translators.
+ * @param {string} [domain] Domain to retrieve the translated text.
+ *
+ * @return {string} Translated context string without pipe.
+ */
+export const _x = i18n._x.bind( i18n );
+
+/**
+ * Translates and retrieves the singular or plural form based on the supplied
+ * number.
+ *
+ * @see https://developer.wordpress.org/reference/functions/_n/
+ *
+ * @param {string} single The text to be used if the number is singular.
+ * @param {string} plural The text to be used if the number is plural.
+ * @param {number} number The number to compare against to use either the
+ * singular or plural form.
+ * @param {string} [domain] Domain to retrieve the translated text.
+ *
+ * @return {string} The translated singular or plural form.
+ */
+export const _n = i18n._n.bind( i18n );
+
+/**
+ * Translates and retrieves the singular or plural form based on the supplied
+ * number, with gettext context.
+ *
+ * @see https://developer.wordpress.org/reference/functions/_nx/
+ *
+ * @param {string} single The text to be used if the number is singular.
+ * @param {string} plural The text to be used if the number is plural.
+ * @param {number} number The number to compare against to use either the
+ * singular or plural form.
+ * @param {string} context Context information for the translators.
+ * @param {string} [domain] Domain to retrieve the translated text.
+ *
+ * @return {string} The translated singular or plural form.
+ */
+export const _nx = i18n._nx.bind( i18n );
+
+/**
+ * Check if current locale is RTL.
+ *
+ * **RTL (Right To Left)** is a locale property indicating that text is written from right to left.
+ * For example, the `he` locale (for Hebrew) specifies right-to-left. Arabic (ar) is another common
+ * language written RTL. The opposite of RTL, LTR (Left To Right) is used in other languages,
+ * including English (`en`, `en-US`, `en-GB`, etc.), Spanish (`es`), and French (`fr`).
+ *
+ * @return {boolean} Whether locale is RTL.
+ */
+export const isRTL = i18n.isRTL.bind( i18n );
diff --git a/packages/i18n/src/index.js b/packages/i18n/src/index.js
index 111ea03e555719..e5e04852a3c227 100644
--- a/packages/i18n/src/index.js
+++ b/packages/i18n/src/index.js
@@ -1,189 +1,3 @@
-/**
- * External dependencies
- */
-import Tannin from 'tannin';
-import memoize from 'memize';
-import sprintfjs from 'sprintf-js';
-
-/**
- * @typedef {Record} LocaleData
- */
-
-/**
- * Default locale data to use for Tannin domain when not otherwise provided.
- * Assumes an English plural forms expression.
- *
- * @type {LocaleData}
- */
-const DEFAULT_LOCALE_DATA = {
- '': {
- /** @param {number} n */
- plural_forms( n ) {
- return n === 1 ? 0 : 1;
- },
- },
-};
-
-/**
- * Log to console, once per message; or more precisely, per referentially equal
- * argument set. Because Jed throws errors, we log these to the console instead
- * to avoid crashing the application.
- *
- * @param {...*} args Arguments to pass to `console.error`
- */
-const logErrorOnce = memoize( console.error ); // eslint-disable-line no-console
-
-/**
- * The underlying instance of Tannin to which exported functions interface.
- *
- * @type {Tannin}
- */
-const i18n = new Tannin( {} );
-
-/**
- * Merges locale data into the Tannin instance by domain. Accepts data in a
- * Jed-formatted JSON object shape.
- *
- * @see http://messageformat.github.io/Jed/
- *
- * @param {LocaleData} [data] Locale data configuration.
- * @param {string} [domain] Domain for which configuration applies.
- */
-export function setLocaleData( data, domain = 'default' ) {
- i18n.data[ domain ] = {
- ...DEFAULT_LOCALE_DATA,
- ...i18n.data[ domain ],
- ...data,
- };
-
- // Populate default domain configuration (supported locale date which omits
- // a plural forms expression).
- i18n.data[ domain ][ '' ] = {
- ...DEFAULT_LOCALE_DATA[ '' ],
- ...i18n.data[ domain ][ '' ],
- };
-}
-
-/**
- * Wrapper for Tannin's `dcnpgettext`. Populates default locale data if not
- * otherwise previously assigned.
- *
- * @param {string|undefined} domain Domain to retrieve the translated text.
- * @param {string|undefined} context Context information for the translators.
- * @param {string} single Text to translate if non-plural. Used as
- * fallback return value on a caught error.
- * @param {string} [plural] The text to be used if the number is
- * plural.
- * @param {number} [number] The number to compare against to use
- * either the singular or plural form.
- *
- * @return {string} The translated string.
- */
-function dcnpgettext( domain = 'default', context, single, plural, number ) {
- if ( ! i18n.data[ domain ] ) {
- setLocaleData( undefined, domain );
- }
-
- return i18n.dcnpgettext( domain, context, single, plural, number );
-}
-
-/**
- * Retrieve the translation of text.
- *
- * @see https://developer.wordpress.org/reference/functions/__/
- *
- * @param {string} text Text to translate.
- * @param {string} [domain] Domain to retrieve the translated text.
- *
- * @return {string} Translated text.
- */
-export function __( text, domain ) {
- return dcnpgettext( domain, undefined, text );
-}
-
-/**
- * Retrieve translated string with gettext context.
- *
- * @see https://developer.wordpress.org/reference/functions/_x/
- *
- * @param {string} text Text to translate.
- * @param {string} context Context information for the translators.
- * @param {string} [domain] Domain to retrieve the translated text.
- *
- * @return {string} Translated context string without pipe.
- */
-export function _x( text, context, domain ) {
- return dcnpgettext( domain, context, text );
-}
-
-/**
- * Translates and retrieves the singular or plural form based on the supplied
- * number.
- *
- * @see https://developer.wordpress.org/reference/functions/_n/
- *
- * @param {string} single The text to be used if the number is singular.
- * @param {string} plural The text to be used if the number is plural.
- * @param {number} number The number to compare against to use either the
- * singular or plural form.
- * @param {string} [domain] Domain to retrieve the translated text.
- *
- * @return {string} The translated singular or plural form.
- */
-export function _n( single, plural, number, domain ) {
- return dcnpgettext( domain, undefined, single, plural, number );
-}
-
-/**
- * Translates and retrieves the singular or plural form based on the supplied
- * number, with gettext context.
- *
- * @see https://developer.wordpress.org/reference/functions/_nx/
- *
- * @param {string} single The text to be used if the number is singular.
- * @param {string} plural The text to be used if the number is plural.
- * @param {number} number The number to compare against to use either the
- * singular or plural form.
- * @param {string} context Context information for the translators.
- * @param {string} [domain] Domain to retrieve the translated text.
- *
- * @return {string} The translated singular or plural form.
- */
-export function _nx( single, plural, number, context, domain ) {
- return dcnpgettext( domain, context, single, plural, number );
-}
-
-/**
- * Check if current locale is RTL.
- *
- * **RTL (Right To Left)** is a locale property indicating that text is written from right to left.
- * For example, the `he` locale (for Hebrew) specifies right-to-left. Arabic (ar) is another common
- * language written RTL. The opposite of RTL, LTR (Left To Right) is used in other languages,
- * including English (`en`, `en-US`, `en-GB`, etc.), Spanish (`es`), and French (`fr`).
- *
- * @return {boolean} Whether locale is RTL.
- */
-export function isRTL() {
- return 'rtl' === _x( 'ltr', 'text direction' );
-}
-
-/**
- * Returns a formatted string. If an error occurs in applying the format, the
- * original format string is returned.
- *
- * @param {string} format The format of the string to generate.
- * @param {...string} args Arguments to apply to the format.
- *
- * @see http://www.diveintojavascript.com/projects/javascript-sprintf
- *
- * @return {string} The formatted string.
- */
-export function sprintf( format, ...args ) {
- try {
- return sprintfjs.sprintf( format, ...args );
- } catch ( error ) {
- logErrorOnce( 'sprintf error: \n\n' + error.toString() );
-
- return format;
- }
-}
+export { sprintf } from './sprintf';
+export * from './create-i18n';
+export { setLocaleData, __, _x, _n, _nx, isRTL } from './default-i18n';
diff --git a/packages/i18n/src/sprintf.js b/packages/i18n/src/sprintf.js
new file mode 100644
index 00000000000000..397fe7abe4e40f
--- /dev/null
+++ b/packages/i18n/src/sprintf.js
@@ -0,0 +1,35 @@
+/**
+ * External dependencies
+ */
+import memoize from 'memize';
+import sprintfjs from 'sprintf-js';
+
+/**
+ * Log to console, once per message; or more precisely, per referentially equal
+ * argument set. Because Jed throws errors, we log these to the console instead
+ * to avoid crashing the application.
+ *
+ * @param {...*} args Arguments to pass to `console.error`
+ */
+const logErrorOnce = memoize( console.error ); // eslint-disable-line no-console
+
+/**
+ * Returns a formatted string. If an error occurs in applying the format, the
+ * original format string is returned.
+ *
+ * @param {string} format The format of the string to generate.
+ * @param {...string} args Arguments to apply to the format.
+ *
+ * @see http://www.diveintojavascript.com/projects/javascript-sprintf
+ *
+ * @return {string} The formatted string.
+ */
+export function sprintf( format, ...args ) {
+ try {
+ return sprintfjs.sprintf( format, ...args );
+ } catch ( error ) {
+ logErrorOnce( 'sprintf error: \n\n' + error.toString() );
+
+ return format;
+ }
+}
diff --git a/packages/i18n/src/test/create-i18n.js b/packages/i18n/src/test/create-i18n.js
new file mode 100644
index 00000000000000..75e05c1f434a74
--- /dev/null
+++ b/packages/i18n/src/test/create-i18n.js
@@ -0,0 +1,190 @@
+/**
+ * Internal dependencies
+ */
+import { createI18n } from '..';
+
+const strayaLocale = {
+ hello: [ 'gday' ],
+};
+
+const frenchLocale = {
+ hello: [ 'bonjour' ],
+};
+
+const localeData = {
+ '': {
+ // Domain name
+ domain: 'test_domain',
+ lang: 'fr',
+ // Plural form function for language
+ plural_forms: 'nplurals=2; plural=(n != 1);',
+ },
+
+ hello: [ 'bonjour' ],
+
+ 'verb\u0004feed': [ 'nourrir' ],
+
+ 'hello %s': [ 'bonjour %s' ],
+
+ '%d banana': [ '%d banane', '%d bananes' ],
+
+ 'fruit\u0004%d apple': [ '%d pomme', '%d pommes' ],
+};
+
+const additionalLocaleData = {
+ cheeseburger: [ 'hamburger au fromage' ],
+ '%d cat': [ '%d chat', '%d chats' ],
+};
+
+const createTestLocale = () => createI18n( localeData, 'test_domain' );
+const createTestLocaleWithAdditionalData = () => {
+ const locale = createI18n( localeData, 'test_domain' );
+ locale.setLocaleData( additionalLocaleData, 'test_domain' );
+ return locale;
+};
+
+describe( 'createI18n', () => {
+ test( 'instantiated with locale data', () => {
+ const straya = createI18n( strayaLocale );
+ expect( straya.__( 'hello' ) ).toEqual( 'gday' );
+ } );
+
+ test( 'multiple instances maintain their own distinct locale data', () => {
+ const straya = createI18n();
+ const french = createI18n();
+
+ straya.setLocaleData( strayaLocale );
+ french.setLocaleData( frenchLocale );
+
+ expect( straya.__( 'hello' ) ).toEqual( 'gday' );
+ expect( french.__( 'hello' ) ).toEqual( 'bonjour' );
+ } );
+
+ describe( '__', () => {
+ it( 'use the translation', () => {
+ const locale = createTestLocale();
+ expect( locale.__( 'hello', 'test_domain' ) ).toBe( 'bonjour' );
+ } );
+ } );
+
+ describe( '_x', () => {
+ it( 'use the translation with context', () => {
+ const locale = createTestLocale();
+ expect( locale._x( 'feed', 'verb', 'test_domain' ) ).toBe(
+ 'nourrir'
+ );
+ } );
+ } );
+
+ describe( '_n', () => {
+ it( 'use the plural form', () => {
+ const locale = createTestLocale();
+ expect(
+ locale._n( '%d banana', '%d bananas', 3, 'test_domain' )
+ ).toBe( '%d bananes' );
+ } );
+
+ it( 'use the singular form', () => {
+ const locale = createTestLocale();
+ expect(
+ locale._n( '%d banana', '%d bananas', 1, 'test_domain' )
+ ).toBe( '%d banane' );
+ } );
+ } );
+
+ describe( '_nx', () => {
+ it( 'use the plural form', () => {
+ const locale = createTestLocale();
+ expect(
+ locale._nx( '%d apple', '%d apples', 3, 'fruit', 'test_domain' )
+ ).toBe( '%d pommes' );
+ } );
+
+ it( 'use the singular form', () => {
+ const locale = createTestLocale();
+ expect(
+ locale._nx( '%d apple', '%d apples', 1, 'fruit', 'test_domain' )
+ ).toBe( '%d pomme' );
+ } );
+ } );
+
+ describe( 'isRTL', () => {
+ const ARLocaleData = {
+ '': {
+ plural_forms:
+ 'nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;',
+ language: 'ar',
+ localeSlug: 'ar',
+ },
+ 'text direction\u0004ltr': [ 'rtl' ],
+ Back: [ 'رجوع' ],
+ };
+
+ it( 'is false for non-rtl', () => {
+ const locale = createI18n();
+ expect( locale.isRTL() ).toBe( false );
+ } );
+
+ it( 'is true for rtl', () => {
+ const locale = createI18n( ARLocaleData );
+ expect( locale.isRTL() ).toBe( true );
+ } );
+ } );
+
+ describe( 'setLocaleData', () => {
+ it( 'supports omitted plural forms expression', () => {
+ const locale = createTestLocaleWithAdditionalData();
+ locale.setLocaleData(
+ {
+ '': {
+ domain: 'test_domain2',
+ lang: 'fr',
+ },
+
+ '%d banana': [ '%d banane', '%d bananes' ],
+ },
+ 'test_domain2'
+ );
+ expect(
+ locale._n( '%d banana', '%d bananes', 2, 'test_domain2' )
+ ).toBe( '%d bananes' );
+ } );
+
+ describe( '__', () => {
+ it( 'existing translation still available', () => {
+ const locale = createTestLocaleWithAdditionalData();
+ expect( locale.__( 'hello', 'test_domain' ) ).toBe( 'bonjour' );
+ } );
+
+ it( 'new translation available.', () => {
+ const locale = createTestLocaleWithAdditionalData();
+ expect( locale.__( 'cheeseburger', 'test_domain' ) ).toBe(
+ 'hamburger au fromage'
+ );
+ } );
+ } );
+
+ describe( '_n', () => {
+ it( 'existing plural form still works', () => {
+ const locale = createTestLocaleWithAdditionalData();
+ expect(
+ locale._n( '%d banana', '%d bananas', 3, 'test_domain' )
+ ).toBe( '%d bananes' );
+ } );
+
+ it( 'new singular form was added', () => {
+ const locale = createTestLocaleWithAdditionalData();
+ expect(
+ locale._n( '%d cat', '%d cats', 1, 'test_domain' )
+ ).toBe( '%d chat' );
+ } );
+
+ it( 'new plural form was added', () => {
+ const locale = createTestLocaleWithAdditionalData();
+ expect(
+ locale._n( '%d cat', '%d cats', 3, 'test_domain' )
+ ).toBe( '%d chats' );
+ } );
+ } );
+ } );
+} );
diff --git a/packages/i18n/src/test/index.js b/packages/i18n/src/test/index.js
deleted file mode 100644
index 5246bc8f3659b1..00000000000000
--- a/packages/i18n/src/test/index.js
+++ /dev/null
@@ -1,187 +0,0 @@
-// Mock memoization as identity function. Inline since Jest errors on out-of-
-// scope references in a mock callback.
-jest.mock( 'memize', () => ( fn ) => fn );
-
-const localeData = {
- '': {
- // Domain name
- domain: 'test_domain',
- lang: 'fr',
- // Plural form function for language
- plural_forms: 'nplurals=2; plural=(n != 1);',
- },
-
- hello: [ 'bonjour' ],
-
- 'verb\u0004feed': [ 'nourrir' ],
-
- 'hello %s': [ 'bonjour %s' ],
-
- '%d banana': [ '%d banane', '%d bananes' ],
-
- 'fruit\u0004%d apple': [ '%d pomme', '%d pommes' ],
-};
-const additionalLocaleData = {
- cheeseburger: [ 'hamburger au fromage' ],
- '%d cat': [ '%d chat', '%d chats' ],
-};
-
-// Get clean locale data
-let sprintf, __, _x, _n, _nx, isRTL, setLocaleData;
-beforeEach( () => {
- const module = require.resolve( '..' );
- delete require.cache[ module ];
- ( { sprintf, __, _x, _n, _nx, isRTL, setLocaleData } = require( '..' ) );
-} );
-
-describe( 'i18n', () => {
- describe( '__', () => {
- beforeEach( setDefaultLocalData );
-
- it( 'use the translation', () => {
- expect( __( 'hello', 'test_domain' ) ).toBe( 'bonjour' );
- } );
- } );
-
- describe( '_x', () => {
- beforeEach( setDefaultLocalData );
-
- it( 'use the translation with context', () => {
- expect( _x( 'feed', 'verb', 'test_domain' ) ).toBe( 'nourrir' );
- } );
- } );
-
- describe( '_n', () => {
- beforeEach( setDefaultLocalData );
-
- it( 'use the plural form', () => {
- expect( _n( '%d banana', '%d bananas', 3, 'test_domain' ) ).toBe(
- '%d bananes'
- );
- } );
-
- it( 'use the singular form', () => {
- expect( _n( '%d banana', '%d bananas', 1, 'test_domain' ) ).toBe(
- '%d banane'
- );
- } );
- } );
-
- describe( '_nx', () => {
- beforeEach( setDefaultLocalData );
-
- it( 'use the plural form', () => {
- expect(
- _nx( '%d apple', '%d apples', 3, 'fruit', 'test_domain' )
- ).toBe( '%d pommes' );
- } );
-
- it( 'use the singular form', () => {
- expect(
- _nx( '%d apple', '%d apples', 1, 'fruit', 'test_domain' )
- ).toBe( '%d pomme' );
- } );
- } );
-
- describe( 'isRTL', () => {
- const ARLocaleData = {
- '': {
- plural_forms:
- 'nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;',
- language: 'ar',
- localeSlug: 'ar',
- },
- 'text direction\u0004ltr': [ 'rtl' ],
- Back: [ 'رجوع' ],
- };
-
- it( 'is false for non-rtl', () => {
- expect( isRTL() ).toBe( false );
- } );
-
- it( 'is true for rtl', () => {
- setLocaleData( ARLocaleData );
- expect( isRTL() ).toBe( true );
- } );
- } );
-
- describe( 'sprintf', () => {
- beforeEach( setDefaultLocalData );
-
- it( 'absorbs errors', () => {
- // Disable reason: Failing case is the purpose of the test.
- // eslint-disable-next-line @wordpress/valid-sprintf
- const result = sprintf( 'Hello %(placeholder-not-provided)s' );
-
- expect( console ).toHaveErrored();
- expect( result ).toBe( 'Hello %(placeholder-not-provided)s' );
- } );
-
- it( 'replaces placeholders', () => {
- const result = sprintf( __( 'hello %s', 'test_domain' ), 'Riad' );
-
- expect( result ).toBe( 'bonjour Riad' );
- } );
- } );
-
- describe( 'setLocaleData', () => {
- beforeAll( () => {
- setDefaultLocalData();
- setLocaleData( additionalLocaleData, 'test_domain' );
- } );
-
- it( 'supports omitted plural forms expression', () => {
- setLocaleData(
- {
- '': {
- domain: 'test_domain2',
- lang: 'fr',
- },
-
- '%d banana': [ '%d banane', '%d bananes' ],
- },
- 'test_domain2'
- );
-
- expect( _n( '%d banana', '%d bananes', 2, 'test_domain2' ) ).toBe(
- '%d bananes'
- );
- } );
-
- describe( '__', () => {
- it( 'existing translation still available', () => {
- expect( __( 'hello', 'test_domain' ) ).toBe( 'bonjour' );
- } );
-
- it( 'new translation available.', () => {
- expect( __( 'cheeseburger', 'test_domain' ) ).toBe(
- 'hamburger au fromage'
- );
- } );
- } );
-
- describe( '_n', () => {
- it( 'existing plural form still works', () => {
- expect(
- _n( '%d banana', '%d bananas', 3, 'test_domain' )
- ).toBe( '%d bananes' );
- } );
-
- it( 'new singular form was added', () => {
- expect( _n( '%d cat', '%d cats', 1, 'test_domain' ) ).toBe(
- '%d chat'
- );
- } );
-
- it( 'new plural form was added', () => {
- expect( _n( '%d cat', '%d cats', 3, 'test_domain' ) ).toBe(
- '%d chats'
- );
- } );
- } );
- } );
-} );
-
-function setDefaultLocalData() {
- setLocaleData( localeData, 'test_domain' );
-}
diff --git a/packages/i18n/src/test/sprintf.js b/packages/i18n/src/test/sprintf.js
new file mode 100644
index 00000000000000..035d3b3a4b3d41
--- /dev/null
+++ b/packages/i18n/src/test/sprintf.js
@@ -0,0 +1,27 @@
+// Mock memoization as identity function. Inline since Jest errors on
+// out-of-scope references in a mock callback.
+jest.mock( 'memize', () => ( fn ) => fn );
+
+/**
+ * Internal dependencies
+ */
+import { sprintf } from '../sprintf';
+
+describe( 'i18n', () => {
+ describe( 'sprintf', () => {
+ it( 'absorbs errors', () => {
+ // Disable reason: Failing case is the purpose of the test.
+ // eslint-disable-next-line @wordpress/valid-sprintf
+ const result = sprintf( 'Hello %(placeholder-not-provided)s' );
+
+ expect( console ).toHaveErrored();
+ expect( result ).toBe( 'Hello %(placeholder-not-provided)s' );
+ } );
+
+ it( 'replaces placeholders', () => {
+ const result = sprintf( 'bonjour %s', 'Riad' );
+
+ expect( result ).toBe( 'bonjour Riad' );
+ } );
+ } );
+} );