diff --git a/__tests__/client/prerender.spec.js b/__tests__/client/prerender.spec.js index c4ff07abc..52462df39 100644 --- a/__tests__/client/prerender.spec.js +++ b/__tests__/client/prerender.spec.js @@ -128,6 +128,17 @@ describe('loadPrerenderScripts', () => { expect(getLocalePack).not.toHaveBeenCalled(); }) ); + + it('should still resolve if useNativeIntl is set to true but not call getLocalePack', async () => { + window.useNativeIntl = true; + const { getLocalePack } = require('@americanexpress/one-app-ducks'); + const initialState = fromJS({ intl: { activeLocale: 'es-ES' } }); + getLocalePack.mockClear(); + + await loadPrerenderScripts(initialState); + + expect(getLocalePack).not.toHaveBeenCalled(); + }); }); describe('moveHelmetScripts', () => { diff --git a/__tests__/integration/helpers/testRunner.js b/__tests__/integration/helpers/testRunner.js index 9e724608b..30ad916ae 100644 --- a/__tests__/integration/helpers/testRunner.js +++ b/__tests__/integration/helpers/testRunner.js @@ -95,7 +95,7 @@ const setUpTestRunner = async ({ })); // uncomment this line in order to view full logs for debugging - // logWatcherDuplex.pipe(process.stdout); + logWatcherDuplex.pipe(process.stdout); try { await Promise.all([ diff --git a/__tests__/server/config/env/runTime.spec.js b/__tests__/server/config/env/runTime.spec.js index 1ae7f49ee..31f012a4d 100644 --- a/__tests__/server/config/env/runTime.spec.js +++ b/__tests__/server/config/env/runTime.spec.js @@ -525,4 +525,20 @@ describe('runTime', () => { expect(() => otelTraceCollectorUrl.validate()).not.toThrow(); }); }); + + describe('ONE_CONFIG_USE_NATIVE_INTL', () => { + const useNativeIntl = getEnvVarConfig('ONE_CONFIG_USE_NATIVE_INTL'); + + it('should have a default value of false', () => { + expect(useNativeIntl.defaultValue).toBe('false'); + }); + + it('should normalise the value to false when not explicitly true', () => { + expect(useNativeIntl.normalize('Value')).toBe('false'); + expect(useNativeIntl.normalize('VALUE')).toBe('false'); + expect(useNativeIntl.normalize('true')).toBe('true'); + expect(useNativeIntl.normalize('TRUE')).toBe('true'); + expect(useNativeIntl.normalize('FALSE')).toBe('false'); + }); + }); }); diff --git a/__tests__/server/plugins/reactHtml/index.spec.jsx b/__tests__/server/plugins/reactHtml/index.spec.jsx index b21b7b4b4..b47928a31 100644 --- a/__tests__/server/plugins/reactHtml/index.spec.jsx +++ b/__tests__/server/plugins/reactHtml/index.spec.jsx @@ -23,6 +23,7 @@ import reactHtml, { renderModuleScripts, renderExternalFallbacks, checkStateForRedirectAndStatusCode, + renderEnvironmentVariables, } from '../../../../src/server/plugins/reactHtml'; import { getClientStateConfig } from '../../../../src/server/utils/stateConfig'; import transit from '../../../../src/universal/utils/transit'; @@ -301,6 +302,8 @@ describe('reactHtml', () => { cdnUrl: '/cdnUrl/', rootModuleName: 'test-root', })); + + process.env.ONE_CONFIG_USE_NATIVE_INTL = 'false'; }); function removeInitialState(body) { @@ -448,6 +451,15 @@ describe('reactHtml', () => { expect(reply.send.mock.calls[0][0]).not.toContain('src="/cdnUrl/app/1.2.3-rc.4-abc123/i18n/'); }); + it('sends a rendered page without the locale data script tag when te ONE_CONFIG_USE_NATIVE_INTL environment variable is true', () => { + process.env.ONE_CONFIG_USE_NATIVE_INTL = 'true'; + setFullMap(); + sendHtml(request, reply); + + expect(reply.send).toHaveBeenCalledTimes(1); + expect(reply.send.mock.calls[0][0]).not.toContain('src="/cdnUrl/app/1.2.3-rc.4-abc123/i18n/'); + }); + it('sends a rendered page with the module styles and scripts', () => { sendHtml(request, reply); expect(reply.send).toHaveBeenCalledTimes(1); @@ -1349,4 +1361,23 @@ describe('reactHtml', () => { expect(reply.send.mock.calls[0][0]).toContain('One App'); }); }); + + describe('renderEnvironmentVariables', () => { + it('should set useNativeIntl to false if the environment variable is not true', () => { + expect(renderEnvironmentVariables('')).toMatchInlineSnapshot(` + "" + `); + }); + + it('should set useNativeIntl to true is the environment variable is true', () => { + process.env.ONE_CONFIG_USE_NATIVE_INTL = 'true'; + expect(renderEnvironmentVariables('')).toMatchInlineSnapshot(` + "" + `); + }); + }); }); diff --git a/docs/api/server/Environment-Variables.md b/docs/api/server/Environment-Variables.md index ea0823053..4387e678f 100644 --- a/docs/api/server/Environment-Variables.md +++ b/docs/api/server/Environment-Variables.md @@ -31,6 +31,7 @@ One App can be configured via Environment Variables: * [`ONE_CLIENT_ROOT_MODULE_NAME`](#one_client_root_module_name) ⚠️ * [`ONE_CLIENT_CDN_URL`](#one_client_cdn_url) ⚠️ * [`ONE_CONFIG_ENV`](#one_config_env) ⚠️ + * [`ONE_CONFIG_USE_NATIVE_INTL`](#one_config_use_native_intl) * Running in Development * [`NODE_ENV`](#node_env) ⚠️ * [`ONE_CLIENT_ROOT_MODULE_NAME`](#one_client_root_module_name) ⚠️ @@ -38,6 +39,7 @@ One App can be configured via Environment Variables: * [`ONE_DANGEROUSLY_ACCEPT_BREAKING_EXTERNALS`](#ONE_DANGEROUSLY_ACCEPT_BREAKING_EXTERNALS) * [`ONE_CSP_ALLOW_INLINE_SCRIPTS`](#ONE_CSP_ALLOW_INLINE_SCRIPTS) * [`ONE_DANGEROUSLY_DISABLE_CSP`](#ONE_DANGEROUSLY_DISABLE_CSP) + * [`ONE_CONFIG_USE_NATIVE_INTL`](#one_config_use_native_intl) * Server Settings * [`HOLOCRON_SERVER_MAX_MODULES_RETRY`](#holocron_server_max_modules_retry) * [`HOLOCRON_SERVER_MAX_SIM_MODULES_FETCH`](#holocron_server_max_sim_modules_fetch) @@ -79,6 +81,7 @@ One App can be configured via Environment Variables: * [`ONE_CLIENT_REPORTING_URL`](#one_client_reporting_url) ⚠️ * [`ONE_CLIENT_ROOT_MODULE_NAME`](#one_client_root_module_name) ⚠️ * [`ONE_CONFIG_ENV`](#one_config_env) ⚠️ + * [`ONE_CONFIG_USE_NATIVE_INTL`](#one_config_use_native_intl) * [`ONE_ENABLE_POST_TO_MODULE_ROUTES`](#one_enable_post_to_module_routes) * [`ONE_ENABLE_REQUEST_LOGGING_WHILE_TRACING`](#one_enable_request_logging_while_tracing) * [`ONE_MAP_POLLING_MAX`](#one_map_polling_max) @@ -550,6 +553,27 @@ ONE_CONFIG_ENV=staging ONE_CONFIG_ENV=undefined ``` +## `ONE_CONFIG_USE_NATIVE_INTL` +**Runs In** +* ✅ Production +* ✅ Development + +Feature flag to disable lean-intl polyfill. +This allows you to use modern intl features such as timezones, but will result in your application supporting fewer older browsers. + +**Shape** +```bash +ONE_CONFIG_USE_NATIVE_INTL=Boolean +``` +**Example** +```bash +ONE_CONFIG_USE_NATIVE_INTL=true +``` +**Default Value** +```bash +ONE_CONFIG_USE_NATIVE_INTL=false +``` + ## `ONE_DANGEROUSLY_ACCEPT_BREAKING_EXTERNALS` **Runs In** @@ -903,4 +927,4 @@ ONE_ENABLE_REQUEST_LOGGING_WHILE_TRACING=true [`HTTPS_PRIVATE_KEY_PASS_FILE_PATH`]: #https_private_key_pass_file_path [`HTTPS_PORT`]: #https_port [OTel Environment Variable Specification]: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/ -[OTel Resource SDK documentation]: https://opentelemetry.io/docs/specs/otel/resource/sdk/#specifying-resource-information-via-an-environment-variable \ No newline at end of file +[OTel Resource SDK documentation]: https://opentelemetry.io/docs/specs/otel/resource/sdk/#specifying-resource-information-via-an-environment-variable diff --git a/package-lock.json b/package-lock.json index 527885733..1c60db0ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "@americanexpress/env-config-utils": "^2.0.4", "@americanexpress/fetch-enhancers": "^1.1.5", - "@americanexpress/one-app-ducks": "^4.4.4", + "@americanexpress/one-app-ducks": "^4.5.1", "@americanexpress/one-app-router": "^1.2.1", "@americanexpress/one-app-server-bundler": "^1.0.2", "@americanexpress/vitruvius": "^3.0.1", @@ -314,9 +314,9 @@ } }, "node_modules/@americanexpress/one-app-ducks": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@americanexpress/one-app-ducks/-/one-app-ducks-4.4.4.tgz", - "integrity": "sha512-KlIcfAnMdThI3r+xBihJrDAjfgA51GR7xO5cuMbL965JEdlWNFhw/f2vsWiSID7RKMcMbkhZ3UsbwrVNBnLVow==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@americanexpress/one-app-ducks/-/one-app-ducks-4.5.1.tgz", + "integrity": "sha512-yAMNxEduc5+WLImNkolBvhaeO3WS5ZMBifP8W6VoJxunba14rFDRwI4K31jndQ2ktapA/0lLmK0DRcsmGI/NPg==", "dependencies": { "holocron": "^1.0.0", "immutable": "^4.0.0-rc.12", diff --git a/package.json b/package.json index 61f427dc5..ae582577b 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "dependencies": { "@americanexpress/env-config-utils": "^2.0.4", "@americanexpress/fetch-enhancers": "^1.1.5", - "@americanexpress/one-app-ducks": "^4.4.4", + "@americanexpress/one-app-ducks": "^4.5.1", "@americanexpress/one-app-router": "^1.2.1", "@americanexpress/one-app-server-bundler": "^1.0.2", "@americanexpress/vitruvius": "^3.0.1", diff --git a/src/client/polyfill/Intl.js b/src/client/polyfill/Intl.js index 8cbb70143..234499492 100644 --- a/src/client/polyfill/Intl.js +++ b/src/client/polyfill/Intl.js @@ -19,5 +19,7 @@ import Intl from 'lean-intl'; -global.Intl = Intl; -global.IntlPolyfill = Intl; +if (!(window && window.useNativeIntl)) { + global.Intl = Intl; + global.IntlPolyfill = Intl; +} diff --git a/src/client/prerender.js b/src/client/prerender.js index a68ee5984..9a2fb12e0 100644 --- a/src/client/prerender.js +++ b/src/client/prerender.js @@ -42,7 +42,7 @@ export function initializeClientStore() { export function loadPrerenderScripts(initialState) { const locale = initialState && initialState.getIn(['intl', 'activeLocale']); - return locale ? getLocalePack(locale) : Promise.resolve(); + return locale && !window?.useNativeIntl ? getLocalePack(locale) : Promise.resolve(); } export function moveHelmetScripts() { diff --git a/src/server/config/env/runTime.js b/src/server/config/env/runTime.js index c3dc079f4..18e4bde0f 100644 --- a/src/server/config/env/runTime.js +++ b/src/server/config/env/runTime.js @@ -186,6 +186,16 @@ const runTime = [ validate: (value) => { if (!value) { throw new Error('The `ONE_CLIENT_ROOT_MODULE_NAME` environment variable must be defined.'); } }, defaultValue: () => (process.env.NODE_ENV === 'development' ? argv.rootModuleName : undefined), }, + { + name: 'ONE_CONFIG_USE_NATIVE_INTL', + defaultValue: 'false', + normalize: (input) => { + if (input?.toLowerCase() === 'true') { + return 'true'; + } + return 'false'; + }, + }, { name: 'ONE_REFERRER_POLICY_OVERRIDE', defaultValue: () => '', diff --git a/src/server/plugins/reactHtml/index.jsx b/src/server/plugins/reactHtml/index.jsx index 7206885f7..72b95fcf0 100644 --- a/src/server/plugins/reactHtml/index.jsx +++ b/src/server/plugins/reactHtml/index.jsx @@ -65,6 +65,10 @@ const legacyBrowserChunkAssets = getChunkAssets(readJsonFile('../../../.build-me .map((chunkAsset) => `legacy/${chunkAsset}`); function renderI18nScript(clientInitialState, appBundlesURLPrefix) { + if (process.env.ONE_CONFIG_USE_NATIVE_INTL === 'true') { + return ''; + } + const i18nFile = getI18nFileFromState(clientInitialState); if (!i18nFile) { return ''; @@ -236,6 +240,12 @@ export function getHead({ `; } +export function renderEnvironmentVariables(nonce) { + return ``; +} + export function getBody({ isLegacy, helmetInfo, @@ -253,13 +263,14 @@ export function getBody({ const bundle = isLegacy ? 'legacyBrowser' : 'browser'; const { bodyAttributes, script } = helmetInfo; const bundlePrefixForBrowser = isLegacy ? `${appBundlesURLPrefix}/legacy` : appBundlesURLPrefix; + const nonce = scriptNonce ? `nonce="${scriptNonce}"` : ''; return `
${appHtml || ''}
${disableScripts ? '' : ` - + ${renderEnvironmentVariables(nonce)} ${assets} ${renderI18nScript(clientInitialState, bundlePrefixForBrowser)} ${renderExternalFallbacks({ diff --git a/src/server/polyfill/intl.js b/src/server/polyfill/intl.js index 4d78166a8..2c5c0ca9b 100644 --- a/src/server/polyfill/intl.js +++ b/src/server/polyfill/intl.js @@ -14,4 +14,8 @@ * permissions and limitations under the License. */ -global.Intl = require('lean-intl'); +import Intl from 'lean-intl'; + +if (process.env.ONE_CONFIG_USE_NATIVE_INTL !== 'true') { + global.Intl = Intl; +}