Skip to content

Commit

Permalink
Update I18n to use “unflattened” nested configs
Browse files Browse the repository at this point in the history
  • Loading branch information
colinrotherham committed Feb 23, 2024
1 parent 4f25f29 commit de0937d
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
})
</script>
Expand Down
6 changes: 3 additions & 3 deletions packages/govuk-frontend/src/govuk/common/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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('.')

Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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')
})
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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

Expand Down
40 changes: 28 additions & 12 deletions packages/govuk-frontend/src/govuk/i18n.jsdom.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -245,8 +247,10 @@ describe('I18n', () => {

const i18n = new I18n(
{
'test.one': 'test',
'test.other': 'test'
test: {
one: 'test',
other: 'test'
}
},
{
locale: 'en'
Expand All @@ -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'
Expand All @@ -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'
Expand All @@ -304,7 +312,9 @@ describe('I18n', () => {
({ count }) => {
const i18n = new I18n(
{
'test.other': 'test'
test: {
other: 'test'
}
},
{
locale: 'cy'
Expand All @@ -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'
Expand Down Expand Up @@ -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'
Expand All @@ -363,7 +377,9 @@ describe('I18n', () => {
it('returns `other` for non-numbers', () => {
const i18n = new I18n(
{
'test.other': 'test'
test: {
other: 'test'
}
},
{
locale: 'en'
Expand Down
53 changes: 30 additions & 23 deletions packages/govuk-frontend/src/govuk/i18n.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down

0 comments on commit de0937d

Please sign in to comment.