diff --git a/app/javascript/packages/i18n/CHANGELOG.md b/app/javascript/packages/i18n/CHANGELOG.md index fddaf37c74b..1a8789b667a 100644 --- a/app/javascript/packages/i18n/CHANGELOG.md +++ b/app/javascript/packages/i18n/CHANGELOG.md @@ -1,4 +1,6 @@ -# `Change Log` +## Unreleased + +- Output console error message when translation data missing for requested key. ## 1.0.1 diff --git a/app/javascript/packages/i18n/index.spec.ts b/app/javascript/packages/i18n/index.spec.ts index 96a07b0e933..2afc5b5b7c7 100644 --- a/app/javascript/packages/i18n/index.spec.ts +++ b/app/javascript/packages/i18n/index.spec.ts @@ -1,3 +1,4 @@ +import { useDefineProperty } from '@18f/identity-test-helpers'; import { I18n, replaceVariables } from './index'; describe('replaceVariables', () => { @@ -37,6 +38,23 @@ describe('I18n', () => { expect(t('unknown')).to.equal('unknown'); }); + context('in non-test environment', () => { + const defineProperty = useDefineProperty(); + beforeEach(() => { + defineProperty(process.env, 'NODE_ENV', { + value: 'production', + configurable: true, + writable: true, + enumerable: true, + }); + }); + + it('falls back to key value and logs to console', () => { + expect(t('unknown')).to.equal('unknown'); + expect(console).to.have.loggedError('Missing translation for key `unknown`.'); + }); + }); + describe('pluralization', () => { it('throws when count is not given', () => { expect(() => t('messages')).to.throw(TypeError); diff --git a/app/javascript/packages/i18n/index.ts b/app/javascript/packages/i18n/index.ts index 9183bdbd5a8..cd5c438226c 100644 --- a/app/javascript/packages/i18n/index.ts +++ b/app/javascript/packages/i18n/index.ts @@ -29,8 +29,21 @@ const getPluralizationKey = (count: number): keyof PluralizedEntry => * * @return Entry string or object. */ -const getEntry = (strings: Entries, key: string): Entry => - Object.hasOwn(strings, key) ? strings[key] : key; +function getEntry(strings: Entries, key: string): Entry { + if (Object.hasOwn(strings, key)) { + return strings[key]; + } + + if (process.env.NODE_ENV !== 'test') { + // String data is not populated in JavaScript tests, so falling back to the key is the expected + // behavior. In all other environments this is an unexpected behavior, so log accordingly. + + // eslint-disable-next-line no-console + console.error(`Missing translation for key \`${key}\`.`); + } + + return key; +} /** * Returns true if the given entry is a pluralization entry, or false otherwise. diff --git a/scripts/enforce-typescript-files.mjs b/scripts/enforce-typescript-files.mjs index df91c749c32..62860455c9a 100755 --- a/scripts/enforce-typescript-files.mjs +++ b/scripts/enforce-typescript-files.mjs @@ -26,7 +26,6 @@ const LEGACY_FILE_EXCEPTIONS = [ 'spec/javascript/packs/form-steps-wait-spec.js', 'spec/javascript/packs/state-guidance-spec.js', 'spec/javascript/packs/webauthn-setup-spec.js', - 'spec/javascript/support/console.js', 'spec/javascript/support/document-capture.jsx', 'spec/javascript/support/dom.js', 'spec/javascript/support/file.js', diff --git a/spec/javascript/support/console.js b/spec/javascript/support/console.ts similarity index 56% rename from spec/javascript/support/console.js rename to spec/javascript/support/console.ts index 539e7107b94..6f26cdc74aa 100644 --- a/spec/javascript/support/console.js +++ b/spec/javascript/support/console.ts @@ -1,7 +1,18 @@ /* eslint-disable no-console */ +import { format } from 'node:util'; import sinon from 'sinon'; -import { format } from 'util'; +import type Chai from 'chai'; + +declare global { + namespace Chai { + interface Assertion { + loggedError: (message: string | RegExp) => Chai.Assertion; + } + } +} + +let unverifiedCalls: string[] = []; /** * Chai plugin which adds chainable `logged` method, to be used in combination with @@ -10,33 +21,33 @@ import { format } from 'util'; * @see https://www.chaijs.com/guide/plugins/ * @see https://www.chaijs.com/api/plugins/ * - * @param {import('chai')} chai Chai object. - * @param {import('chai/lib/chai/utils')} utils Chai plugin utilities. + * @param chai Chai object. + * @param utils Chai plugin utilities. */ -export function chaiConsoleSpy(chai, utils) { +export const chaiConsoleSpy: Chai.ChaiPlugin = (chai, utils) => { utils.addChainableMethod( chai.Assertion.prototype, 'loggedError', - (message) => { + (message: string | RegExp) => { if (message) { - const index = console.unverifiedCalls.findIndex((calledMessage) => + const index = unverifiedCalls.findIndex((calledMessage) => message instanceof RegExp ? message.test(calledMessage) : message === calledMessage, ); let error = `Expected console to have logged: ${message}. `; - error += console.unverifiedCalls - ? `Console logged with: ${console.unverifiedCalls.join(', ')}` + error += unverifiedCalls + ? `Console logged with: ${unverifiedCalls.join(', ')}` : 'Console did not log.'; expect(index).to.not.equal(-1, error); - console.unverifiedCalls.splice(index, 1); + unverifiedCalls.splice(index, 1); } else { - console.unverifiedCalls = []; + unverifiedCalls = []; } }, undefined, ); -} +}; /** * Test lifecycle helper which stubs `console.error` and verifies that any logging which occurs to @@ -44,19 +55,25 @@ export function chaiConsoleSpy(chai, utils) { * `chaiConsoleSpy` Chai plugin. */ export function useConsoleLogSpy() { - let originalConsoleError; - beforeEach(() => { - console.unverifiedCalls = []; + let originalConsoleError: Console['error']; + before(() => { originalConsoleError = console.error; console.error = sinon.stub().callsFake((message, ...args) => { - console.unverifiedCalls = console.unverifiedCalls.concat(format(message, ...args)); + unverifiedCalls.push(format(message, ...args)); }); }); + beforeEach(() => { + unverifiedCalls = []; + }); + afterEach(() => { - console.error = originalConsoleError; - expect(console.unverifiedCalls).to.be.empty( - `Unexpected console logging: ${console.unverifiedCalls.join(', ')}`, + expect(unverifiedCalls).to.be.empty( + `Unexpected console logging: ${unverifiedCalls.join(', ')}`, ); }); + + after(() => { + console.error = originalConsoleError; + }); } diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 6c27735fd6d..e766747e17c 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -77,7 +77,7 @@ class Analytics # rubocop:enable Style/GlobalVars # rubocop:disable Rails/Output print ' Bundling JavaScript and stylesheets... ' - system 'yarn concurrently "yarn:build:*" > /dev/null 2>&1' + system 'NODE_ENV=production yarn concurrently "yarn:build:*" > /dev/null 2>&1' puts '✨ Done!' # rubocop:enable Rails/Output diff --git a/webpack.config.js b/webpack.config.js index eb46a9bd0b1..1c12ee0f964 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,7 +11,6 @@ const env = process.env.NODE_ENV || process.env.RAILS_ENV || 'development'; const host = process.env.HOST || 'localhost'; const isLocalhost = host === 'localhost'; const isProductionEnv = env === 'production'; -const isTestEnv = env === 'test'; const mode = isProductionEnv ? 'production' : 'development'; const hashSuffix = isProductionEnv ? '-[chunkhash:8].digested' : ''; const devServerPort = process.env.WEBPACK_PORT; @@ -88,9 +87,7 @@ module.exports = /** @type {import('webpack').Configuration} */ ({ }), new RailsI18nWebpackPlugin({ onMissingString(key, locale) { - if (isTestEnv) { - throw new Error(`Unexpected missing string for locale '${locale}': '${key}'`); - } + throw new Error(`Unexpected missing string for locale '${locale}': '${key}'`); }, }), new RailsAssetsWebpackPlugin(),