Skip to content

Commit

Permalink
i18n: add new APIs for React bindings (#28784)
Browse files Browse the repository at this point in the history
* i18n: add new APIs: defaultI18n, getLocaleData, subscribe, hasTranslation

* Notify listeners also on has_translation filter add/remove
  • Loading branch information
jsnajdr authored Feb 9, 2021
1 parent ce9f6a9 commit 0ad5a14
Show file tree
Hide file tree
Showing 7 changed files with 404 additions and 42 deletions.
7 changes: 7 additions & 0 deletions packages/i18n/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## Unreleased

### Enhancements

- Export the default `I18n` instance as `defaultI18n`, in addition to already exported bound methods.
- Add new `getLocaleData` method to get the internal Tannin locale data object.
- Add new `subscribe` method to subscribe to changes in the internal locale data.
- Add new `hasTranslation` method to determine whether a translation for a string is available.

## 3.17.0 (2020-12-17)

### Enhancements
Expand Down
48 changes: 47 additions & 1 deletion packages/i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,46 @@ _Parameters_

- _initialData_ `[LocaleData]`: Locale data configuration.
- _initialDomain_ `[string]`: Domain for which configuration applies.
- _hooks_ `[ApplyFiltersInterface]`: Hooks implementation.
- _hooks_ `[Hooks]`: Hooks implementation.

_Returns_

- `I18n`: I18n instance

<a name="defaultI18n" href="#defaultI18n">#</a> **defaultI18n**

Default, singleton instance of `I18n`.

<a name="getLocaleData" href="#getLocaleData">#</a> **getLocaleData**

Returns locale data by domain in a Jed-formatted JSON object shape.

_Related_

- <http://messageformat.github.io/Jed/>

_Parameters_

- _domain_ `[string]`: Domain for which to get the data.

_Returns_

- `LocaleData`: Locale data.

<a name="hasTranslation" href="#hasTranslation">#</a> **hasTranslation**

Check if there is a translation for a given string (in singular form).

_Parameters_

- _single_ `string`: Singular form of the string to look up.
- _context_ `[string]`: Context information for the translators.
- _domain_ `[string]`: Domain to retrieve the translated text.

_Returns_

- `boolean`: Whether the translation exists or not.

<a name="isRTL" href="#isRTL">#</a> **isRTL**

Check if current locale is RTL.
Expand Down Expand Up @@ -86,6 +120,18 @@ _Returns_

- `string`: The formatted string.

<a name="subscribe" href="#subscribe">#</a> **subscribe**

Subscribes to changes of locale data

_Parameters_

- _callback_ `SubscribeCallback`: Subscription callback

_Returns_

- `UnsubscribeCallback`: Unsubscribe callback

<a name="_n" href="#_n">#</a> **\_n**

Translates and retrieves the singular or plural form based on the supplied
Expand Down
121 changes: 116 additions & 5 deletions packages/i18n/src/create-i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,35 @@ const DEFAULT_LOCALE_DATA = {
},
};

/*
* Regular expression that matches i18n hooks like `i18n.gettext`, `i18n.ngettext`,
* `i18n.gettext_domain` or `i18n.ngettext_with_context` or `i18n.has_translation`.
*/
const I18N_HOOK_REGEXP = /^i18n\.(n?gettext|has_translation)(_|$)/;

/**
* @typedef {(domain?: string) => LocaleData} GetLocaleData
*
* Returns locale data by domain in a
* Jed-formatted JSON object shape.
*
* @see http://messageformat.github.io/Jed/
*/
/**
* @typedef {(data?: LocaleData, domain?: string) => void} SetLocaleData
*
* Merges locale data into the Tannin instance by domain. Accepts data in a
* Jed-formatted JSON object shape.
*
* @see http://messageformat.github.io/Jed/
*/
/** @typedef {() => void} SubscribeCallback */
/** @typedef {() => void} UnsubscribeCallback */
/**
* @typedef {(callback: SubscribeCallback) => UnsubscribeCallback} Subscribe
*
* Subscribes to changes of locale data
*/
/**
* @typedef {(domain?: string) => string} GetFilterDomain
* Retrieve the domain to use when calling domain-specific filters.
Expand Down Expand Up @@ -74,30 +96,36 @@ const DEFAULT_LOCALE_DATA = {
* including English (`en`, `en-US`, `en-GB`, etc.), Spanish (`es`), and French (`fr`).
*/
/**
* @typedef {{ applyFilters: (hookName:string, ...args: unknown[]) => unknown}} ApplyFiltersInterface
* @typedef {(single: string, context?: string, domain?: string) => boolean} HasTranslation
*
* Check if there is a translation for a given string in singular form.
*/
/** @typedef {import('@wordpress/hooks').Hooks} Hooks */

/**
* An i18n instance
*
* @typedef I18n
* @property {GetLocaleData} getLocaleData Returns locale data by domain in a Jed-formatted JSON object shape.
* @property {SetLocaleData} setLocaleData Merges locale data into the Tannin instance by domain. Accepts data in a
* Jed-formatted JSON object shape.
* @property {Subscribe} subscribe Subscribes to changes of Tannin locale data.
* @property {__} __ Retrieve the translation of text.
* @property {_x} _x Retrieve translated string with gettext context.
* @property {_n} _n Translates and retrieves the singular or plural form based on the supplied
* number.
* @property {_nx} _nx Translates and retrieves the singular or plural form based on the supplied
* number, with gettext context.
* @property {IsRtl} isRTL Check if current locale is RTL.
* @property {HasTranslation} hasTranslation Check if there is a translation for a given string.
*/

/**
* Create an i18n instance
*
* @param {LocaleData} [initialData] Locale data configuration.
* @param {string} [initialDomain] Domain for which configuration applies.
* @param {ApplyFiltersInterface} [hooks] Hooks implementation.
* @param {Hooks} [hooks] Hooks implementation.
* @return {I18n} I18n instance
*/
export const createI18n = ( initialData, initialDomain, hooks ) => {
Expand All @@ -108,8 +136,31 @@ export const createI18n = ( initialData, initialDomain, hooks ) => {
*/
const tannin = new Tannin( {} );

/** @type {SetLocaleData} */
const setLocaleData = ( data, domain = 'default' ) => {
const listeners = new Set();

const notifyListeners = () => {
listeners.forEach( ( listener ) => listener() );
};

/**
* Subscribe to changes of locale data.
*
* @param {SubscribeCallback} callback Subscription callback.
* @return {UnsubscribeCallback} Unsubscribe callback.
*/
const subscribe = ( callback ) => {
listeners.add( callback );
return () => listeners.delete( callback );
};

/** @type {GetLocaleData} */
const getLocaleData = ( domain = 'default' ) => tannin.data[ domain ];

/**
* @param {LocaleData} [data]
* @param {string} [domain]
*/
const doSetLocaleData = ( data, domain = 'default' ) => {
tannin.data[ domain ] = {
...DEFAULT_LOCALE_DATA,
...tannin.data[ domain ],
Expand All @@ -124,6 +175,12 @@ export const createI18n = ( initialData, initialDomain, hooks ) => {
};
};

/** @type {SetLocaleData} */
const setLocaleData = ( data, domain ) => {
doSetLocaleData( data, domain );
notifyListeners();
};

/**
* Wrapper for Tannin's `dcnpgettext`. Populates default locale data if not
* otherwise previously assigned.
Expand All @@ -147,7 +204,8 @@ export const createI18n = ( initialData, initialDomain, hooks ) => {
number
) => {
if ( ! tannin.data[ domain ] ) {
setLocaleData( undefined, domain );
// use `doSetLocaleData` to set silently, without notifying listeners
doSetLocaleData( undefined, domain );
}

return tannin.dcnpgettext( domain, context, single, plural, number );
Expand Down Expand Up @@ -320,16 +378,69 @@ export const createI18n = ( initialData, initialDomain, hooks ) => {
return 'rtl' === _x( 'ltr', 'text direction' );
};

/** @type {HasTranslation} */
const hasTranslation = ( single, context, domain ) => {
const key = context ? context + '\u0004' + single : single;
let result = !! tannin.data?.[ domain ?? 'default' ]?.[ key ];
if ( hooks ) {
/**
* Filters the presence of a translation in the locale data.
*
* @param {boolean} hasTranslation Whether the translation is present or not..
* @param {string} single The singular form of the translated text (used as key in locale data)
* @param {string} context Context information for the translators.
* @param {string} domain Text domain. Unique identifier for retrieving translated strings.
*/
result = /** @type { boolean } */ (
/** @type {*} */ hooks.applyFilters(
'i18n.has_translation',
result,
single,
context,
domain
)
);

result = /** @type { boolean } */ (
/** @type {*} */ hooks.applyFilters(
'i18n.has_translation_' + getFilterDomain( domain ),
result,
single,
context,
domain
)
);
}
return result;
};

if ( initialData ) {
setLocaleData( initialData, initialDomain );
}

if ( hooks ) {
/**
* @param {string} hookName
*/
const onHookAddedOrRemoved = ( hookName ) => {
if ( I18N_HOOK_REGEXP.test( hookName ) ) {
notifyListeners();
}
};

hooks.addAction( 'hookAdded', 'core/i18n', onHookAddedOrRemoved );
hooks.addAction( 'hookRemoved', 'core/i18n', onHookAddedOrRemoved );
}

return {
getLocaleData,
setLocaleData,
subscribe,
__,
_x,
_n,
_nx,
isRTL,
hasTranslation,
};
};
45 changes: 40 additions & 5 deletions packages/i18n/src/default-i18n.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
/**
* WordPress dependencies
* Internal dependencies
*/
import { applyFilters } from '@wordpress/hooks';
import { createI18n } from './create-i18n';

/**
* Internal dependencies
* WordPress dependencies
*/
import { createI18n } from './create-i18n';
import { defaultHooks } from '@wordpress/hooks';

const i18n = createI18n( undefined, undefined, { applyFilters } );
const i18n = createI18n( undefined, undefined, defaultHooks );

/**
* Default, singleton instance of `I18n`.
*/
export default i18n;

/*
* Comments in this file are duplicated from ./i18n due to
Expand All @@ -17,7 +22,19 @@ const i18n = createI18n( undefined, undefined, { applyFilters } );

/**
* @typedef {import('./create-i18n').LocaleData} LocaleData
* @typedef {import('./create-i18n').SubscribeCallback} SubscribeCallback
* @typedef {import('./create-i18n').UnsubscribeCallback} UnsubscribeCallback
*/

/**
* Returns locale data by domain in a Jed-formatted JSON object shape.
*
* @see http://messageformat.github.io/Jed/
*
* @param {string} [domain] Domain for which to get the data.
* @return {LocaleData} Locale data.
*/
export const getLocaleData = i18n.getLocaleData.bind( i18n );

/**
* Merges locale data into the Tannin instance by domain. Accepts data in a
Expand All @@ -30,6 +47,14 @@ const i18n = createI18n( undefined, undefined, { applyFilters } );
*/
export const setLocaleData = i18n.setLocaleData.bind( i18n );

/**
* Subscribes to changes of locale data
*
* @param {SubscribeCallback} callback Subscription callback
* @return {UnsubscribeCallback} Unsubscribe callback
*/
export const subscribe = i18n.subscribe.bind( i18n );

/**
* Retrieve the translation of text.
*
Expand Down Expand Up @@ -99,3 +124,13 @@ export const _nx = i18n._nx.bind( i18n );
* @return {boolean} Whether locale is RTL.
*/
export const isRTL = i18n.isRTL.bind( i18n );

/**
* Check if there is a translation for a given string (in singular form).
*
* @param {string} single Singular form of the string to look up.
* @param {string} [context] Context information for the translators.
* @param {string} [domain] Domain to retrieve the translated text.
* @return {boolean} Whether the translation exists or not.
*/
export const hasTranslation = i18n.hasTranslation.bind( i18n );
13 changes: 12 additions & 1 deletion packages/i18n/src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
export { sprintf } from './sprintf';
export * from './create-i18n';
export { setLocaleData, __, _x, _n, _nx, isRTL } from './default-i18n';
export {
default as defaultI18n,
setLocaleData,
getLocaleData,
subscribe,
__,
_x,
_n,
_nx,
isRTL,
hasTranslation,
} from './default-i18n';
Loading

0 comments on commit 0ad5a14

Please sign in to comment.