diff --git a/CHANGELOG.md b/CHANGELOG.md index f79b8c4..6cb3542 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Remove usage of Node core lib util module ([#1](https://github.com/Gandi/counterpart/pull/1)). - Remove usage of `except` helper package ([#1](https://github.com/Gandi/counterpart/pull/2)). + - **BREAKING** Replace usage of Node `EventEmitter` with native `EventTarget` ([#1](https://github.com/Gandi/counterpart/pull/3)). + This make the package able to run server of client side without requiring polyfills. + This change the signature of event listener callbacks. + They now receive a single `CustomEvent` object as argument, instead of unlimited + parameters. So for instance: + ``` + // Before + instance.onLocaleChange((locale, previous) => {}) + ``` + ``` + // After + instance.onLocaleChange((event) => { + // event.detail.locale + // event.detail.previous + }) + ``` [unreleased]: https://github.com/gandi/counterpart/compare/0.18.6...HEAD diff --git a/Makefile b/Makefile index fc4983f..d75f7e7 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ BIN = ./node_modules/.bin -test: lint +test: @$(BIN)/mocha -t 5000 -b -R spec spec.js lint: node_modules/ diff --git a/README.md b/README.md index f8935b8..c468498 100644 --- a/README.md +++ b/README.md @@ -149,8 +149,12 @@ translate('baz', { fallback: 'default' }) When a translation key cannot be resolved to a translation, regardless of whether a fallback is provided or not, `translate` will emit an event you can listen to: ```js -translate.onTranslationNotFound(function(locale, key, fallback, scope) { +translate.onTranslationNotFound(function(event) { // do important stuff here... + // event.detail.locale + // event.detail.key + // event.detail.fallback + // event.detail.scope }); ``` @@ -182,8 +186,10 @@ Note that it is advised to call `setLocale` only once at the start of the applic In case of a locale change, the `setLocale` function emits an event you can listen to: ```js -translate.onLocaleChange(function(newLocale, oldLocale) { +translate.onLocaleChange(function(event) { // do important stuff here... + // event.detail.locale + // event.detail.previous }, [callbackContext]); ``` @@ -341,8 +347,11 @@ instance.translate('foo'); When a translation fails, `translate` will emit an event you can listen to: ```js -translate.onError(function(err, entry, values) { +translate.onError(function(event) { // do some error handling here... + // event.detail.error + // event.detail.entry + // event.detail.values }); ``` diff --git a/index.js b/index.js index d32e345..16dffe1 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,6 @@ var extend = require('extend'); var sprintf = require("sprintf-js").sprintf; -var events = require('events'); var strftime = require('./strftime'); @@ -46,29 +45,72 @@ function getEntry(translations, keys) { }, translations); } -function Counterpart() { - events.EventEmitter.apply(this); - - this._registry = { - locale: 'en', - interpolate: true, - fallbackLocales: [], - scope: null, - translations: {}, - interpolations: {}, - normalizedKeys: {}, - separator: '.', - keepTrailingDot: false, - keyTransformer: function(key) { return key; }, - generateMissingEntry: function(key) { return 'missing translation: ' + key; } +class Counterpart extends EventTarget { + constructor() { + super(); + + this._registry = { + locale: 'en', + interpolate: true, + fallbackLocales: [], + scope: null, + translations: {}, + interpolations: {}, + normalizedKeys: {}, + separator: '.', + keepTrailingDot: false, + keyTransformer: function(key) { return key; }, + generateMissingEntry: function(key) { return 'missing translation: ' + key; } + }; + + this.registerTranslations('en', require('./locales/en')); + } + + // EventTarget does not (yet) have a native way to retrieve attached listeners. + // See https://github.com/whatwg/dom/issues/412 + + #events = {}; + + _addEventListener = (type, listener, options) => { + EventTarget.prototype.addEventListener.call(this, type, listener, options); + + (this.#events[type] ||= []).push(listener); + + return this; }; - this.registerTranslations('en', require('./locales/en')); - this.setMaxListeners(0); -} + _removeEventListener = (type, listener, options) => { + EventTarget.prototype.removeEventListener.call(this, type, listener, options); + + let list = this.#events[type]; + + if (!list) return this; + + let position = -1; + + for (let i = list.length - 1; i >= 0; i--) { + if (list[i] === listener) { + position = i; + break; + } + } -Counterpart.prototype = events.EventEmitter.prototype; -Counterpart.prototype.constructor = events.EventEmitter; + if (position < 0) return this; + + if (position === 0) { + list.shift(); + } + else { + list.splice(list, position); + } + + return this; + }; + + _listenerCount = (type) => { + return this.#events[type] ? this.#events[type].length : 0; + } +} Counterpart.prototype.getLocale = function() { return this._registry.locale; @@ -79,7 +121,7 @@ Counterpart.prototype.setLocale = function(value) { if (previous != value) { this._registry.locale = value; - this.emit('localechange', value, previous); + this.dispatchEvent(new CustomEvent('localechange', { detail: { locale: value, previous }})); } return previous; @@ -158,32 +200,32 @@ Counterpart.prototype.registerInterpolations = function(data) { Counterpart.prototype.onLocaleChange = Counterpart.prototype.addLocaleChangeListener = function(callback) { - this.addListener('localechange', callback); + this._addEventListener('localechange', callback); }; Counterpart.prototype.offLocaleChange = Counterpart.prototype.removeLocaleChangeListener = function(callback) { - this.removeListener('localechange', callback); + this._removeEventListener('localechange', callback); }; Counterpart.prototype.onTranslationNotFound = Counterpart.prototype.addTranslationNotFoundListener = function(callback) { - this.addListener('translationnotfound', callback); + this._addEventListener('translationnotfound', callback); }; Counterpart.prototype.offTranslationNotFound = Counterpart.prototype.removeTranslationNotFoundListener = function(callback) { - this.removeListener('translationnotfound', callback); + this._removeEventListener('translationnotfound', callback); }; Counterpart.prototype.onError = Counterpart.prototype.addErrorListener = function(callback) { - this.addListener('error', callback); + this._addEventListener('error', callback); }; Counterpart.prototype.offError = Counterpart.prototype.removeErrorListener = function(callback) { - this.removeListener('error', callback); + this._removeEventListener('error', callback); }; Counterpart.prototype.translate = function(key, options) { @@ -216,7 +258,7 @@ Counterpart.prototype.translate = function(key, options) { var entry = getEntry(this._registry.translations, keys); if (entry === null) { - this.emit('translationnotfound', locale, key, options.fallback, scope); + this.dispatchEvent(new CustomEvent('translationnotfound', { detail: { locale, key, fallback: options.fallback, scope }})); if (options.fallback) { entry = this._fallback(locale, scope, key, options.fallback, options); @@ -354,8 +396,8 @@ Counterpart.prototype._interpolate = function(entry, values) { try { return sprintf(entry, extend({}, this._registry.interpolations, values)); } catch (err) { - if (this.listenerCount('error') > 0) { - this.emit('error', err, entry, values); + if (this._listenerCount('error') > 0) { + this.dispatchEvent(new CustomEvent('error', { detail: { error: err, entry, values }})); } else { throw err; } diff --git a/package-lock.json b/package-lock.json index fd1cb10..1825201 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "counterpart", + "name": "@gandi/counterpart", "version": "0.18.6", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "counterpart", + "name": "@gandi/counterpart", "version": "0.18.6", "license": "MIT", "dependencies": { diff --git a/spec.js b/spec.js index 697d8d3..06e3c0e 100644 --- a/spec.js +++ b/spec.js @@ -607,9 +607,9 @@ describe('translate', function() { var oldLocale = instance.getLocale(); var newLocale = oldLocale + 'x'; - var handler = function(locale, previousLocale) { - assert.equal(locale, newLocale); - assert.equal(previousLocale, oldLocale); + var handler = function(evt) { + assert.equal(evt.detail.locale, newLocale); + assert.equal(evt.detail.previous, oldLocale); done(); }; @@ -619,7 +619,8 @@ describe('translate', function() { }); }); - describe('when called more than 10 times', function() { + // EventTarget does not have a native `setMaxListeners`. + describe.skip('when called more than 10 times', function() { it('does not let Node issue a warning about a possible memory leak', function() { var oldConsoleError = console.error; @@ -694,11 +695,11 @@ describe('translate', function() { describe('when called', function() { it('exposes the current locale, key, fallback and scope as arguments', function(done) { - var handler = function(locale, key, fallback, scope) { - assert.equal('yy', locale); - assert.equal('foo', key); - assert.equal('bar', fallback); - assert.equal('zz', scope); + var handler = function(evt) { + assert.equal('yy', evt.detail.locale); + assert.equal('foo', evt.detail.key); + assert.equal('bar', evt.detail.fallback); + assert.equal('zz', evt.detail.scope); done(); }; @@ -763,10 +764,10 @@ describe('translate', function() { describe('when called', function() { it('exposes the error, entry and values as arguments', function(done) { - var handler = function(error, entry, values) { - assert.notEqual(undefined, error); - assert.equal('Hello, %(name)s!', entry); - assert.deepEqual({}, values); + var handler = function(evt) { + assert.notEqual(undefined, evt.detail.error); + assert.equal('Hello, %(name)s!', evt.detail.entry); + assert.deepEqual({}, evt.detail.values); done(); };