diff --git a/packages/govuk-frontend-review/src/views/examples/translated/index.njk b/packages/govuk-frontend-review/src/views/examples/translated/index.njk index b8fc294af3..3ba09e3c52 100644 --- a/packages/govuk-frontend-review/src/views/examples/translated/index.njk +++ b/packages/govuk-frontend-review/src/views/examples/translated/index.njk @@ -954,11 +954,11 @@ i18n: { showAllSections: 'Dangos adrannau', hideAllSections: 'Cuddio adrannau', + showSection: 'Dangos', + showSectionAriaLabel: 'Dangos adran', + hideSection: 'Cuddio', + hideSectionAriaLabel: 'Cuddio adran' }, - 'i18n.showSection': 'Dangos', - 'i18n.showSectionAriaLabel': 'Dangos adran', - 'i18n.hideSection': 'Cuddio', - 'i18n.hideSectionAriaLabel': 'Cuddio adran' } }) diff --git a/packages/govuk-frontend/src/govuk/common/index.mjs b/packages/govuk-frontend/src/govuk/common/index.mjs index edd37de3c6..c5181b315d 100644 --- a/packages/govuk-frontend/src/govuk/common/index.mjs +++ b/packages/govuk-frontend/src/govuk/common/index.mjs @@ -50,14 +50,14 @@ export function mergeConfigs(...configObjects) { * object, removing the namespace in the process, normalising all values * * @internal - * @param {ObjectNested} configObject - The object to extract key-value pairs from + * @param {DOMStringMap} dataset - The object to extract key-value pairs from * @param {string} namespace - The namespace to filter keys with * @returns {ObjectNested} Nested object with dot-separated key namespace removed */ -export function extractConfigByNamespace(configObject, namespace) { +export function extractConfigByNamespace(dataset, namespace) { const newObject = /** @type {ObjectNested} */ ({}) - for (const [key, value] of Object.entries(configObject)) { + for (const [key, value] of Object.entries(dataset)) { // Split the key into parts, using . as our namespace separator const keyParts = key.split('.') diff --git a/packages/govuk-frontend/src/govuk/components/accordion/accordion.mjs b/packages/govuk-frontend/src/govuk/components/accordion/accordion.mjs index fc20549fcf..27806f4866 100644 --- a/packages/govuk-frontend/src/govuk/components/accordion/accordion.mjs +++ b/packages/govuk-frontend/src/govuk/components/accordion/accordion.mjs @@ -1,4 +1,4 @@ -import { mergeConfigs, extractConfigByNamespace } from '../../common/index.mjs' +import { mergeConfigs } from '../../common/index.mjs' import { normaliseDataset } from '../../common/normalise-dataset.mjs' import { ElementError } from '../../errors/index.mjs' import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs' @@ -135,7 +135,7 @@ export class Accordion extends GOVUKFrontendComponent { normaliseDataset($module.dataset, Accordion.schema) ) - this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n')) + this.i18n = new I18n(this.config.i18n) const $sections = this.$module.querySelectorAll(`.${this.sectionClass}`) if (!$sections.length) { diff --git a/packages/govuk-frontend/src/govuk/components/character-count/character-count.mjs b/packages/govuk-frontend/src/govuk/components/character-count/character-count.mjs index c208c7a178..3a1f4fdc15 100644 --- a/packages/govuk-frontend/src/govuk/components/character-count/character-count.mjs +++ b/packages/govuk-frontend/src/govuk/components/character-count/character-count.mjs @@ -1,9 +1,5 @@ import { closestAttributeValue } from '../../common/closest-attribute-value.mjs' -import { - extractConfigByNamespace, - mergeConfigs, - validateConfig -} from '../../common/index.mjs' +import { mergeConfigs, validateConfig } from '../../common/index.mjs' import { normaliseDataset } from '../../common/normalise-dataset.mjs' import { ConfigError, ElementError } from '../../errors/index.mjs' import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs' @@ -125,7 +121,7 @@ export class CharacterCount extends GOVUKFrontendComponent { throw new ConfigError(`Character count: ${errors[0]}`) } - this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'), { + this.i18n = new I18n(this.config.i18n, { // Read the fallback if necessary rather than have it set in the defaults locale: closestAttributeValue($module, 'lang') }) diff --git a/packages/govuk-frontend/src/govuk/components/exit-this-page/exit-this-page.mjs b/packages/govuk-frontend/src/govuk/components/exit-this-page/exit-this-page.mjs index 1c3e4ea935..8cd5060dd1 100644 --- a/packages/govuk-frontend/src/govuk/components/exit-this-page/exit-this-page.mjs +++ b/packages/govuk-frontend/src/govuk/components/exit-this-page/exit-this-page.mjs @@ -1,4 +1,4 @@ -import { mergeConfigs, extractConfigByNamespace } from '../../common/index.mjs' +import { mergeConfigs } from '../../common/index.mjs' import { normaliseDataset } from '../../common/normalise-dataset.mjs' import { ElementError } from '../../errors/index.mjs' import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs' @@ -105,7 +105,7 @@ export class ExitThisPage extends GOVUKFrontendComponent { normaliseDataset($module.dataset, ExitThisPage.schema) ) - this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n')) + this.i18n = new I18n(this.config.i18n) this.$module = $module this.$button = $button diff --git a/packages/govuk-frontend/src/govuk/i18n.jsdom.test.mjs b/packages/govuk-frontend/src/govuk/i18n.jsdom.test.mjs index 8c68a952f7..5b8a26a991 100644 --- a/packages/govuk-frontend/src/govuk/i18n.jsdom.test.mjs +++ b/packages/govuk-frontend/src/govuk/i18n.jsdom.test.mjs @@ -208,8 +208,10 @@ describe('I18n', () => { it('interpolates the count variable into the correct plural form', () => { const i18n = new I18n( { - 'test.one': '%{count} test', - 'test.other': '%{count} tests' + test: { + one: '%{count} test', + other: '%{count} tests' + } }, { locale: 'en' @@ -245,8 +247,10 @@ describe('I18n', () => { const i18n = new I18n( { - 'test.one': 'test', - 'test.other': 'test' + test: { + one: 'test', + other: 'test' + } }, { locale: 'en' @@ -260,8 +264,10 @@ describe('I18n', () => { it('falls back to internal fallback rules', () => { const i18n = new I18n( { - 'test.one': 'test', - 'test.other': 'test' + test: { + one: 'test', + other: 'test' + } }, { locale: 'en' @@ -284,8 +290,10 @@ describe('I18n', () => { it('returns the preferred plural form for the locale if a translation exists', () => { const i18n = new I18n( { - 'test.one': 'test', - 'test.other': 'test' + test: { + one: 'test', + other: 'test' + } }, { locale: 'en' @@ -304,7 +312,9 @@ describe('I18n', () => { ({ count }) => { const i18n = new I18n( { - 'test.other': 'test' + test: { + other: 'test' + } }, { locale: 'cy' @@ -318,7 +328,9 @@ describe('I18n', () => { it('logs a console warning when falling back to `other`', () => { const i18n = new I18n( { - 'test.other': 'test' + test: { + other: 'test' + } }, { locale: 'en' @@ -348,7 +360,9 @@ describe('I18n', () => { it('throws an error if a plural form is not provided and neither is `other`', () => { const i18n = new I18n( { - 'test.one': 'test' + test: { + one: 'test' + } }, { locale: 'en' @@ -363,7 +377,9 @@ describe('I18n', () => { it('returns `other` for non-numbers', () => { const i18n = new I18n( { - 'test.other': 'test' + test: { + other: 'test' + } }, { locale: 'en' diff --git a/packages/govuk-frontend/src/govuk/i18n.mjs b/packages/govuk-frontend/src/govuk/i18n.mjs index dcac3063c6..2c1200feda 100644 --- a/packages/govuk-frontend/src/govuk/i18n.mjs +++ b/packages/govuk-frontend/src/govuk/i18n.mjs @@ -10,7 +10,7 @@ export class I18n { /** * @internal - * @param {{ [key: string]: unknown }} translations - Key-value pairs of the translation strings to use. + * @param {{ [key: string]: string | TranslationPluralForms }} translations - Key-value pairs of the translation strings to use. * @param {object} [config] - Configuration options for the function. * @param {string | null} [config.locale] - An overriding locale for the PluralRules functionality. */ @@ -39,33 +39,35 @@ export class I18n { throw new Error('i18n: lookup key missing') } + // Fetch the translation for that lookup key + let translation = this.translations[lookupKey] + // If the `count` option is set, determine which plural suffix is needed and // change the lookupKey to match. We check to see if it's numeric instead of // falsy, as this could legitimately be 0. - if (typeof options?.count === 'number') { - // Get the plural suffix - lookupKey = `${lookupKey}.${this.getPluralSuffix( - lookupKey, - options.count - )}` - } + if (typeof options?.count === 'number' && typeof translation === 'object') { + const translationPluralForm = + translation[this.getPluralSuffix(lookupKey, options.count)] - // Fetch the translation string for that lookup key - const translationString = this.translations[lookupKey] + // Update translation with plural suffix + if (translationPluralForm) { + translation = translationPluralForm + } + } - if (typeof translationString === 'string') { + if (typeof translation === 'string') { // Check for ${} placeholders in the translation string - if (translationString.match(/%{(.\S+)}/)) { + if (translation.match(/%{(.\S+)}/)) { if (!options) { throw new Error( 'i18n: cannot replace placeholders in string if no option data provided' ) } - return this.replacePlaceholders(translationString, options) + return this.replacePlaceholders(translation, options) } - return translationString + return translation } // If the key wasn't found in our translations object, @@ -174,6 +176,9 @@ export class I18n { return 'other' } + // Fetch the translation for that lookup key + const translation = this.translations[lookupKey] + // Check to verify that all the requirements for Intl.PluralRules are met. // If so, we can use that instead of our custom implementation. Otherwise, // use the hardcoded fallback. @@ -182,16 +187,18 @@ export class I18n { : this.selectPluralFormUsingFallbackRules(count) // Use the correct plural form if provided - if (`${lookupKey}.${preferredForm}` in this.translations) { - return preferredForm - // Fall back to `other` if the plural form is missing, but log a warning - // to the console - } else if (`${lookupKey}.other` in this.translations) { - console.warn( - `i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".` - ) + if (typeof translation === 'object') { + if (preferredForm in translation) { + return preferredForm + // Fall back to `other` if the plural form is missing, but log a warning + // to the console + } else if ('other' in translation) { + console.warn( + `i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".` + ) - return 'other' + return 'other' + } } // If the required `other` plural form is missing, all we can do is error