Skip to content
Merged
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
4 changes: 3 additions & 1 deletion app/javascript/packages/i18n/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# `Change Log`
## Unreleased

- Output console error message when translation data missing for requested key.

## 1.0.1

Expand Down
18 changes: 18 additions & 0 deletions app/javascript/packages/i18n/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useDefineProperty } from '@18f/identity-test-helpers';
import { I18n, replaceVariables } from './index';

describe('replaceVariables', () => {
Expand Down Expand Up @@ -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);
Expand Down
17 changes: 15 additions & 2 deletions app/javascript/packages/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 0 additions & 1 deletion scripts/enforce-typescript-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,53 +21,59 @@ 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
* this method is validated using the `logged` chainable assertion implemented by the
* `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;
});
}
2 changes: 1 addition & 1 deletion spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 1 addition & 4 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand Down