Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

i18n: add new APIs for React bindings #28784

Merged
merged 2 commits into from
Feb 9, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`
*/
const I18N_HOOK_REGEXP = /^i18n\.(n?)gettext(_|$)/;
Copy link
Member

Choose a reason for hiding this comment

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

Does this need to match i18n.has_translation as well?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point 🙂 When I was adding this regexp and the code that uses it, the i18n.has_translation filter didn't exist yet. The filter cannot change the result of __(), but it can change result of hasTranslation() which is one of the functions that the React bindings expose. I'll add it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed in a245ae0


/**
* @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