diff --git a/api/src/shared/domain/constants.js b/api/src/shared/domain/constants.js index d5aa5ee6934..d11baa5ecac 100644 --- a/api/src/shared/domain/constants.js +++ b/api/src/shared/domain/constants.js @@ -12,7 +12,7 @@ const LOCALE = { SPANISH_SPOKEN: 'es', }; -const SUPPORTED_LOCALES = ['en', 'es', 'fr', 'fr-BE', 'fr-FR', 'nl-BE']; +const SUPPORTED_LOCALES = ['en', 'es', 'fr', 'fr-BE', 'fr-FR', 'nl-BE', 'nl']; const ORGANIZATION_FEATURE = { MISSIONS_MANAGEMENT: { diff --git a/api/src/shared/infrastructure/i18n/i18n.js b/api/src/shared/infrastructure/i18n/i18n.js new file mode 100644 index 00000000000..2e0dd8eaae3 --- /dev/null +++ b/api/src/shared/infrastructure/i18n/i18n.js @@ -0,0 +1,54 @@ +import path from 'node:path'; + +import { I18n } from 'i18n'; + +import { SUPPORTED_LOCALES } from '../../domain/constants.js'; +import { logger } from '../utils/logger.js'; + +const __dirname = import.meta.dirname; +const translationsFolder = path.resolve(path.join(__dirname, '../../../../translations')); + +const supportedLocales = SUPPORTED_LOCALES.map((supportedLocale) => supportedLocale.toLowerCase()); + +const DEFAULT_LOCALE = 'fr'; + +export const options = { + locales: ['en', 'fr', 'es', 'nl'], + fallbacks: { 'en-*': 'en', 'fr-*': 'fr', 'es-*': 'es', 'nl-*': 'nl' }, + defaultLocale: DEFAULT_LOCALE, + directory: translationsFolder, + queryParameter: 'lang', + languageHeaderField: 'Accept-Language', + objectNotation: true, + updateFiles: false, + mustacheConfig: { + tags: ['{', '}'], + disable: false, + }, +}; + +// This is an optimization to avoid settings a new instance each time +// we need to use i18n. +const i18nInstances = {}; + +/** + * @param {string} locale a locale (language or BCP 47 format) + * @returns i18n instance correctly setup with the language + */ +export function getI18n(locale) { + if (!locale || !supportedLocales.includes(locale?.toLowerCase())) { + return getI18n(DEFAULT_LOCALE); + } + + if (!i18nInstances[locale]) { + const i18n = new I18n(options); + i18n.setLocale(locale); + // we freeze the setLocale to avoid changing i18n locale for an instance + i18n.setLocale = () => { + logger.warn('Cannot change i18n locale instance, use getI18n(locale) instead.'); + }; + i18nInstances[locale] = i18n; + } + + return i18nInstances[locale]; +} diff --git a/api/src/shared/infrastructure/plugins/i18n.js b/api/src/shared/infrastructure/plugins/i18n.js index 7c23140c6e8..5e140e6e9c8 100644 --- a/api/src/shared/infrastructure/plugins/i18n.js +++ b/api/src/shared/infrastructure/plugins/i18n.js @@ -1,20 +1,7 @@ -import * as url from 'node:url'; - import hapiI18n from 'hapi-i18n'; -const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); + +import { options } from '../i18n/i18n.js'; + const plugin = hapiI18n; -const options = { - locales: ['en', 'fr', 'es', 'nl'], - directory: __dirname + '../../../../translations', - defaultLocale: 'fr', - queryParameter: 'lang', - languageHeaderField: 'Accept-Language', - objectNotation: true, - updateFiles: false, - mustacheConfig: { - tags: ['{', '}'], - disable: false, - }, -}; export { options, plugin }; diff --git a/api/tests/shared/unit/infrastructure/i18n/i18n_test.js b/api/tests/shared/unit/infrastructure/i18n/i18n_test.js new file mode 100644 index 00000000000..4bb8f4f26c7 --- /dev/null +++ b/api/tests/shared/unit/infrastructure/i18n/i18n_test.js @@ -0,0 +1,59 @@ +import { getI18n, options } from '../../../../../src/shared/infrastructure/i18n/i18n.js'; +import { expect } from '../../../../test-helper.js'; + +describe('Unit | Shared | Infrastucture | i18n', function () { + describe('default i18n options', function () { + it('returns i18n options', function () { + expect(options).to.have.property('locales').that.includes('en', 'fr', 'es', 'nl'); + expect(options).to.have.property('directory').that.is.a('string'); + expect(options).to.have.property('defaultLocale', 'fr'); + expect(options).to.have.property('queryParameter', 'lang'); + expect(options).to.have.property('languageHeaderField', 'Accept-Language'); + expect(options).to.have.property('objectNotation', true); + expect(options).to.have.property('updateFiles', false); + expect(options) + .to.have.property('mustacheConfig') + .to.deep.equal({ + tags: ['{', '}'], + disable: false, + }); + }); + }); + + describe('getI18n', function () { + it('returns an instance of i18n with default locale', function () { + const i18n = getI18n(); + expect(i18n.getLocale()).to.equal('fr'); + }); + + it('returns an instance of i18n with the specified locale', function () { + const locale = 'es'; + const i18n = getI18n(locale); + expect(i18n.getLocale()).to.equal(locale); + }); + + context('when the locale is BCP 47 format', function () { + it('returns the correct locale instance of i18n', function () { + const locale = 'nl-BE'; + const i18n = getI18n(locale); + expect(i18n.getLocale()).to.equal('nl'); + }); + }); + + context('when the locale is not supported', function () { + it('returns the default locale instance of i18n', function () { + const locale = 'foo'; + const i18n = getI18n(locale); + expect(i18n.getLocale()).to.equal('fr'); + }); + }); + + context('when the i18n setLocale is called on an i18n instance', function () { + it('does not change the instance locale', function () { + const i18n1 = getI18n('fr'); + i18n1.setLocale('en'); + expect(i18n1.getLocale()).to.equal('fr'); + }); + }); + }); +}); diff --git a/api/tests/tooling/i18n/i18n.js b/api/tests/tooling/i18n/i18n.js deleted file mode 100644 index 55e1c607e54..00000000000 --- a/api/tests/tooling/i18n/i18n.js +++ /dev/null @@ -1,20 +0,0 @@ -import path from 'node:path'; -import * as url from 'node:url'; - -import { I18n } from 'i18n'; - -import { options } from '../../../src/shared/infrastructure/plugins/i18n.js'; -const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); - -function getI18n() { - const directory = path.resolve(path.join(__dirname, '../../../translations')); - - const i18n = new I18n(); - i18n.configure({ - ...options, - directory, - }); - return i18n; -} - -export { getI18n }; diff --git a/api/tests/tooling/i18n/i18n_test.js b/api/tests/tooling/i18n/i18n_test.js deleted file mode 100644 index 3cacd02d2ca..00000000000 --- a/api/tests/tooling/i18n/i18n_test.js +++ /dev/null @@ -1,9 +0,0 @@ -import { expect } from '../../test-helper.js'; -import { getI18n } from './i18n.js'; - -describe('Unit | Tooling | i18n', function () { - it('should translate by default to fr', function () { - const currentLang = getI18n().getLocale(); - expect(currentLang).to.equal('fr'); - }); -});