Skip to content

i18n-calypso: Replace EventEmitter with simplified subscriber callbacks #103304

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

Open
wants to merge 5 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 1 addition & 5 deletions client/components/calypso-i18n-provider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,7 @@ const CalypsoI18nProvider: FunctionComponent< { i18n?: I18N; children?: React.Re
setLocaleSlug( i18n.getLocaleSlug() );
};

i18n.on( 'change', onChange );

return () => {
i18n.off( 'change', onChange );
};
return i18n.subscribe( onChange );
}, [ i18n ] );

useEffect( () => {
Expand Down
4 changes: 2 additions & 2 deletions client/components/community-translator/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ class CommunityTranslator extends Component {
// callback when translated component changes.
// the callback is overwritten by the translator on load/unload, so we're returning it within an anonymous function.
i18n.registerComponentUpdateHook( () => {} );
i18n.on( 'change', this.refresh );
this.i18nUnsubscribe = i18n.subscribe( this.refresh );
}

componentWillUnmount() {
i18n.off( 'change', this.refresh );
this.i18nUnsubscribe();
}

setLanguage() {
Expand Down
4 changes: 2 additions & 2 deletions client/layout/community-translator/launcher.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ class TranslatorLauncher extends Component {
highlightRef = createRef();

componentDidMount() {
i18n.on( 'change', this.onI18nChange );
this.i18nUnsubscribe = i18n.subscribe( this.onI18nChange );
window.addEventListener( 'keydown', this.handleKeyDown );
}

componentWillUnmount() {
i18n.off( 'change', this.onI18nChange );
this.i18nUnsubscribe();
window.removeEventListener( 'keydown', this.handleKeyDown );
}

Expand Down
2 changes: 1 addition & 1 deletion client/lib/i18n-utils/empathy-mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import i18n, { I18N, translate } from 'i18n-calypso';
let defaultUntranslatedPlacehoder = translate( "I don't understand" );

// keep `defaultUntranslatedPlacehoder` in sync with i18n changes
i18n.on( 'change', () => {
i18n.subscribe( () => {
defaultUntranslatedPlacehoder = translate( "I don't understand" );
} );

Expand Down
2 changes: 1 addition & 1 deletion client/lib/translator-jumpstart/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,6 @@ export function trackTranslatorStatus( isTranslatorEnabled ) {
}

// re-initialize when new locale data is loaded
i18n.on( 'change', communityTranslatorJumpstart.init.bind( communityTranslatorJumpstart ) );
i18n.subscribe( communityTranslatorJumpstart.init.bind( communityTranslatorJumpstart ) );

export default communityTranslatorJumpstart;
6 changes: 6 additions & 0 deletions packages/i18n-calypso/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Trunk

- Breaking: Emitter object replaced with subscriber
- Before: `i18n.on( 'change', callback ); i18n.off( 'change', callback );`
- After: `const unsubscribe = i18n.subscribe( callback ); unsubscribe();`

## 7.5.0

- Add `translationOptions` to fixMe so it can work on translations with context.
Expand Down
1 change: 0 additions & 1 deletion packages/i18n-calypso/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
"@tannin/sprintf": "^1.1.0",
"@wordpress/compose": "^7.23.0",
"debug": "^4.4.0",
"events": "^3.3.0",
"hash.js": "^1.1.7",
"lru": "^3.1.0",
"tannin": "^1.2.0",
Expand Down
44 changes: 27 additions & 17 deletions packages/i18n-calypso/src/i18n.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { EventEmitter } from 'events';
import interpolateComponents from '@automattic/interpolate-components';
import sprintf from '@tannin/sprintf';
import debugFactory from 'debug';
Expand Down Expand Up @@ -27,6 +26,24 @@ const translationLookup = [

const hashCache = {};

/**
* Efficiently removes an item from an array, with the assumption that the item appears only once.
* @param {Array<I>} array The array to remove the item from.
* @param {I} item The item to remove from the array.
* @template I
*/
function removeOne( array, item ) {
for ( let i = 0; i < array.length; i++ ) {
if ( array[ i ] === item ) {
for ( ; i < array.length - 1; i++ ) {
array[ i ] = array[ i + 1 ];
}
array.pop();
return;
}
}
}

// raise a console warning
function warn() {
if ( ! I18N.throwErrors ) {
Expand Down Expand Up @@ -145,11 +162,7 @@ function I18N() {
};
this.componentUpdateHooks = [];
this.translateHooks = [];
this.stateObserver = new EventEmitter();
// Because the higher-order component can wrap a ton of React components,
// we need to bump the number of listeners to infinity and beyond
// FIXME: still valid?
this.stateObserver.setMaxListeners( 0 );
this.subscribers = [];
// default configuration
this.configure();
}
Expand Down Expand Up @@ -191,16 +204,13 @@ I18N.prototype.geolocateCurrencySymbol = async function ( callback ) {
callback?.( 'string' === typeof geoData?.country_short ? geoData.country_short : '' );
};

I18N.prototype.on = function ( ...args ) {
this.stateObserver.on( ...args );
};

I18N.prototype.off = function ( ...args ) {
this.stateObserver.off( ...args );
I18N.prototype.subscribe = function ( callback ) {
this.subscribers.push( callback );
return () => removeOne( this.subscribers, callback );
};

I18N.prototype.emit = function ( ...args ) {
this.stateObserver.emit( ...args );
I18N.prototype.emitChange = function () {
this.subscribers.forEach( ( callback ) => callback() );
};

/**
Expand Down Expand Up @@ -304,7 +314,7 @@ I18N.prototype.setLocale = function ( localeData ) {

this.state.tannin = new Tannin( { [ domain_key ]: this.state.locale } );

this.stateObserver.emit( 'change' );
this.emitChange();
};

I18N.prototype.getLocale = function () {
Expand Down Expand Up @@ -347,7 +357,7 @@ I18N.prototype.addTranslations = function ( localeData ) {
}
}

this.stateObserver.emit( 'change' );
this.emitChange();
};

/**
Expand Down Expand Up @@ -452,7 +462,7 @@ I18N.prototype.translate = function () {
*/
I18N.prototype.reRenderTranslations = function () {
debug( 'Re-rendering all translations due to external request' );
this.stateObserver.emit( 'change' );
this.emitChange();
};

I18N.prototype.registerComponentUpdateHook = function ( callback ) {
Expand Down
4 changes: 0 additions & 4 deletions packages/i18n-calypso/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ export const reRenderTranslations = i18n.reRenderTranslations.bind( i18n );
export const registerComponentUpdateHook = i18n.registerComponentUpdateHook.bind( i18n );
export const registerTranslateHook = i18n.registerTranslateHook.bind( i18n );
export const state = i18n.state;
export const stateObserver = i18n.stateObserver;
export const on = i18n.on.bind( i18n );
export const off = i18n.off.bind( i18n );
export const emit = i18n.emit.bind( i18n );
export const fixMe = i18n.fixMe.bind( i18n );

export type * from './types';
3 changes: 1 addition & 2 deletions packages/i18n-calypso/src/localize.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ export default function localize( ComposedComponent ) {
const [ counter, setCounter ] = useState( 0 );
useEffect( () => {
const onChange = () => setCounter( ( c ) => c + 1 );
i18n.on( 'change', onChange );
return () => i18n.off( 'change', onChange );
return i18n.subscribe( onChange );
}, [ i18n ] );

const i18nProps = useMemo( () => bindI18nProps( i18n, counter ), [ i18n, counter ] );
Expand Down
5 changes: 2 additions & 3 deletions packages/i18n-calypso/src/rtl.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ export function useRtl() {
return i18n.isRtl();
},
subscribe( callback ) {
i18n.on( 'change', callback );
return () => i18n.off( 'change', callback );
return i18n.subscribe( callback );
},
} ),
[ i18n ]
Expand All @@ -24,7 +23,7 @@ export function useRtl() {

export const withRtl = createHigherOrderComponent(
( WrappedComponent ) =>
forwardRef( ( props, ref ) => {
forwardRef( function WrappedRtlComponent( props, ref ) {
const isRtl = useRtl();
return <WrappedComponent { ...props } isRtl={ isRtl } ref={ ref } />;
} ),
Expand Down
85 changes: 85 additions & 0 deletions packages/i18n-calypso/src/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ describe( 'I18n', function () {
} );

describe( 'setLocale()', function () {
it( 'should emit a change event', () => {
const callback = jest.fn();
i18n.subscribe( callback );

i18n.setLocale();

expect( callback ).toHaveBeenCalled();
} );

describe( 'adding a new locale source from the same language', function () {
beforeEach( function () {
i18n.setLocale( {
Expand Down Expand Up @@ -191,13 +200,23 @@ describe( 'I18n', function () {

expect( translate( 'test-does-not-exist' ) ).toBe( 'translation3' );
} );

it( 'should return the new translation if it has been overwritten', function () {
i18n.addTranslations( {
'test-will-overwrite': [ 'not-translation1' ],
} );

expect( translate( 'test-will-overwrite' ) ).toBe( 'not-translation1' );
} );

it( 'should emit a change event', () => {
const callback = jest.fn();
i18n.subscribe( callback );

i18n.addTranslations( {} );

expect( callback ).toHaveBeenCalled();
} );
} );
} );

Expand Down Expand Up @@ -266,6 +285,72 @@ describe( 'I18n', function () {
} );
} );

describe( 'emitChange()', () => {
it( 'should call all subscribed callbacks', () => {
const callback1 = jest.fn();
const callback2 = jest.fn();

i18n.subscribe( callback1 );
i18n.subscribe( callback2 );

i18n.emitChange();

expect( callback1 ).toHaveBeenCalled();
expect( callback2 ).toHaveBeenCalled();
} );

it( 'should not call unsubscribed callbacks', () => {
const callback1 = jest.fn();
const callback2 = jest.fn();
const callback3 = jest.fn();

i18n.subscribe( callback1 );
const unsubscribe2 = i18n.subscribe( callback2 );
i18n.subscribe( callback3 );

unsubscribe2();
i18n.emitChange();

expect( callback1 ).toHaveBeenCalled();
expect( callback2 ).not.toHaveBeenCalled();
expect( callback3 ).toHaveBeenCalled();
} );
} );

describe( 'reRenderTranslations()', () => {
it( 'should call subscriber callback', () => {
const callback = jest.fn();

i18n.subscribe( callback );

i18n.reRenderTranslations();

expect( callback ).toHaveBeenCalled();
} );
} );

describe( 'subscribe()', () => {
it( 'should call the callback whenever a change is emitted', () => {
const callback = jest.fn();

i18n.subscribe( callback );
i18n.setLocale();

expect( callback ).toHaveBeenCalled();
} );

it( 'should return an unsubscribe function', () => {
const callback = jest.fn();

const unsubscribe = i18n.subscribe( callback );
unsubscribe();

i18n.setLocale();

expect( callback ).not.toHaveBeenCalled();
} );
} );

describe( 'hashed locale data', function () {
it( 'should find keys when looked up by simple hash', function () {
i18n.setLocale( {
Expand Down
8 changes: 2 additions & 6 deletions packages/i18n-calypso/src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,8 @@ export interface I18N {
registerTranslateHook( hook: TranslateHook ): void;
registerComponentUpdateHook( hook: ComponentUpdateHook ): void;

on( eventName: string, listener: EventListener ): void;
off( eventName: string, listener: EventListener ): void;
emit( eventName: string, ...payload: any ): void;
subscribe( callback: () => any ): () => void;
emitChange(): void;

/**
* Returns `newCopy` if given `text` is translated or locale is English, otherwise returns the `oldCopy`.
Expand Down Expand Up @@ -180,9 +179,6 @@ export declare const isRtl: typeof i18n.isRtl;
export declare const defaultLocaleSlug: typeof i18n.defaultLocaleSlug;
export declare const registerTranslateHook: typeof i18n.registerTranslateHook;
export declare const registerComponentUpdateHook: typeof i18n.registerComponentUpdateHook;
export declare const on: typeof i18n.on;
export declare const off: typeof i18n.off;
export declare const emit: typeof i18n.emit;
export declare const fixMe: typeof i18n.fixMe;

export interface LocalizeProps {
Expand Down
3 changes: 1 addition & 2 deletions packages/i18n-calypso/src/use-translate.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ export default function useTranslate() {

useEffect( () => {
const onChange = () => setCounter( ( c ) => c + 1 );
i18n.on( 'change', onChange );
return () => i18n.off( 'change', onChange );
return i18n.subscribe( onChange );
}, [ i18n ] );

return useMemo( () => bindTranslate( i18n, counter ), [ i18n, counter ] );
Expand Down
1 change: 0 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -21885,7 +21885,6 @@ __metadata:
"@types/react": "npm:^18.3.20"
"@wordpress/compose": "npm:^7.23.0"
debug: "npm:^4.4.0"
events: "npm:^3.3.0"
hash.js: "npm:^1.1.7"
lru: "npm:^3.1.0"
react: "npm:^18.3.1"
Expand Down