diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md index 700b3b520eefb..62c453cbfd9ab 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -3,6 +3,7 @@ ### New Feature - Add `isRTL` function (#20298) +- Add `createI18n` method to allow creation of multiple i18n instances. (#20318) ## 3.1.0 (2018-11-15) diff --git a/packages/i18n/README.md b/packages/i18n/README.md index 21847b5f3f017..f57ce4124c191 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 0000000000000..f8473bff00734 --- /dev/null +++ b/packages/i18n/src/create-i18n.js @@ -0,0 +1,195 @@ +/** + * External dependencies + */ +import Tannin from 'tannin'; + +/** + * @typedef {{[key: string]: any}} 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 = { + '': { + plural_forms: ( n ) => ( 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' ); + }; + + 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 0000000000000..760ad5240342a --- /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 {{[key: string]: any}} 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 2b4286819c9ca..9e83b23e77971 100644 --- a/packages/i18n/src/index.js +++ b/packages/i18n/src/index.js @@ -1,186 +1,3 @@ -/** - * External dependencies - */ -import Tannin from 'tannin'; -import memoize from 'memize'; -import sprintfjs from 'sprintf-js'; - -/** - * @typedef {{[key: string]: any}} 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 = { - '': { - plural_forms: ( n ) => ( 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 { createI18n } 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 0000000000000..397fe7abe4e40 --- /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 0000000000000..75e05c1f434a7 --- /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 5246bc8f3659b..0000000000000 --- 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 0000000000000..035d3b3a4b3d4 --- /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' ); + } ); + } ); +} );