UNPKG

govuk-frontend

Version:

GOV.UK Frontend contains the code you need to start building a user interface for government platforms and services.

1 lines 12.3 kB
{"version":3,"file":"i18n.mjs","sources":["../../src/govuk/i18n.mjs"],"sourcesContent":["import { isObject } from './common/index.mjs'\n\n/**\n * Internal support for selecting messages to render, with placeholder\n * interpolation and locale-aware number formatting and pluralisation\n *\n * @internal\n */\nexport class I18n {\n translations\n locale\n\n /**\n * @internal\n * @param {{ [key: string]: string | TranslationPluralForms }} translations - Key-value pairs of the translation strings to use.\n * @param {object} [config] - Configuration options for the function.\n * @param {string | null} [config.locale] - An overriding locale for the PluralRules functionality.\n */\n constructor(translations = {}, config = {}) {\n // Make list of translations available throughout function\n this.translations = translations\n\n // The locale to use for PluralRules and NumberFormat\n this.locale = config.locale ?? (document.documentElement.lang || 'en')\n }\n\n /**\n * The most used function - takes the key for a given piece of UI text and\n * returns the appropriate string.\n *\n * @internal\n * @param {string} lookupKey - The lookup key of the string to use.\n * @param {{ [key: string]: unknown }} [options] - Any options passed with the translation string, e.g: for string interpolation.\n * @returns {string} The appropriate translation string.\n * @throws {Error} Lookup key required\n * @throws {Error} Options required for `${}` placeholders\n */\n t(lookupKey, options) {\n if (!lookupKey) {\n // Print a console error if no lookup key has been provided\n throw new Error('i18n: lookup key missing')\n }\n\n // Fetch the translation for that lookup key\n let translation = this.translations[lookupKey]\n\n // If the `count` option is set, determine which plural suffix is needed and\n // change the lookupKey to match. We check to see if it's numeric instead of\n // falsy, as this could legitimately be 0.\n if (typeof options?.count === 'number' && isObject(translation)) {\n const translationPluralForm =\n translation[this.getPluralSuffix(lookupKey, options.count)]\n\n // Update translation with plural suffix\n if (translationPluralForm) {\n translation = translationPluralForm\n }\n }\n\n if (typeof translation === 'string') {\n // Check for ${} placeholders in the translation string\n // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec\n if (translation.match(/%{(.\\S+)}/)) {\n if (!options) {\n throw new Error(\n 'i18n: cannot replace placeholders in string if no option data provided'\n )\n }\n\n return this.replacePlaceholders(translation, options)\n }\n\n return translation\n }\n\n // If the key wasn't found in our translations object,\n // return the lookup key itself as the fallback\n return lookupKey\n }\n\n /**\n * Takes a translation string with placeholders, and replaces the placeholders\n * with the provided data\n *\n * @internal\n * @param {string} translationString - The translation string\n * @param {{ [key: string]: unknown }} options - Any options passed with the translation string, e.g: for string interpolation.\n * @returns {string} The translation string to output, with $\\{\\} placeholders replaced\n */\n replacePlaceholders(translationString, options) {\n const formatter = Intl.NumberFormat.supportedLocalesOf(this.locale).length\n ? new Intl.NumberFormat(this.locale)\n : undefined\n\n return translationString.replace(\n /%{(.\\S+)}/g,\n\n /**\n * Replace translation string placeholders\n *\n * @internal\n * @param {string} placeholderWithBraces - Placeholder with braces\n * @param {string} placeholderKey - Placeholder key\n * @returns {string} Placeholder value\n */\n function (placeholderWithBraces, placeholderKey) {\n if (Object.prototype.hasOwnProperty.call(options, placeholderKey)) {\n const placeholderValue = options[placeholderKey]\n\n // If a user has passed `false` as the value for the placeholder\n // treat it as though the value should not be displayed\n if (\n placeholderValue === false ||\n (typeof placeholderValue !== 'number' &&\n typeof placeholderValue !== 'string')\n ) {\n return ''\n }\n\n // If the placeholder's value is a number, localise the number formatting\n if (typeof placeholderValue === 'number') {\n return formatter\n ? formatter.format(placeholderValue)\n : `${placeholderValue}`\n }\n\n return placeholderValue\n }\n\n throw new Error(\n `i18n: no data found to replace ${placeholderWithBraces} placeholder in string`\n )\n }\n )\n }\n\n /**\n * Check to see if the browser supports Intl.PluralRules\n *\n * It requires all conditions to be met in order to be supported:\n * - The implementation of Intl supports PluralRules (NOT true in Safari 10–12)\n * - The browser/OS has plural rules for the current locale (browser dependent)\n *\n * {@link https://browsersl.ist/#q=supports+es6-module+and+not+supports+intl-pluralrules}\n *\n * @internal\n * @returns {boolean} Returns true if all conditions are met. Returns false otherwise.\n */\n hasIntlPluralRulesSupport() {\n return Boolean(\n 'PluralRules' in window.Intl &&\n Intl.PluralRules.supportedLocalesOf(this.locale).length\n )\n }\n\n /**\n * Get the appropriate suffix for the plural form.\n *\n * Uses Intl.PluralRules (or our own fallback implementation) to get the\n * 'preferred' form to use for the given count.\n *\n * Checks that a translation has been provided for that plural form – if it\n * hasn't, it'll fall back to the 'other' plural form (unless that doesn't exist\n * either, in which case an error will be thrown)\n *\n * @internal\n * @param {string} lookupKey - The lookup key of the string to use.\n * @param {number} count - Number used to determine which pluralisation to use.\n * @returns {PluralRule} The suffix associated with the correct pluralisation for this locale.\n * @throws {Error} Plural form `.other` required when preferred plural form is missing\n */\n getPluralSuffix(lookupKey, count) {\n // Validate that the number is actually a number.\n //\n // Number(count) will turn anything that can't be converted to a Number type\n // into 'NaN'. isFinite filters out NaN, as it isn't a finite number.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion\n count = Number(count)\n if (!isFinite(count)) {\n return 'other'\n }\n\n // Fetch the translation for that lookup key\n const translation = this.translations[lookupKey]\n\n // Check to verify that all the requirements for Intl.PluralRules are met.\n // If so, we can use that instead of our custom implementation. Otherwise,\n // use the hardcoded fallback.\n const preferredForm = this.hasIntlPluralRulesSupport()\n ? new Intl.PluralRules(this.locale).select(count)\n : 'other'\n\n // Use the correct plural form if provided\n if (isObject(translation)) {\n if (preferredForm in translation) {\n return preferredForm\n // Fall back to `other` if the plural form is missing, but log a warning\n // to the console\n } else if ('other' in translation) {\n console.warn(\n `i18n: Missing plural form \".${preferredForm}\" for \"${this.locale}\" locale. Falling back to \".other\".`\n )\n\n return 'other'\n }\n }\n\n // If the required `other` plural form is missing, all we can do is error\n throw new Error(\n `i18n: Plural form \".other\" is required for \"${this.locale}\" locale`\n )\n }\n}\n\n/**\n * Plural rule category mnemonic tags\n *\n * @internal\n * @typedef {'zero' | 'one' | 'two' | 'few' | 'many' | 'other'} PluralRule\n */\n\n/**\n * Translated message by plural rule they correspond to.\n *\n * Allows to group pluralised messages under a single key when passing\n * translations to a component's constructor\n *\n * @internal\n * @typedef {object} TranslationPluralForms\n * @property {string} [other] - General plural form\n * @property {string} [zero] - Plural form used with 0\n * @property {string} [one] - Plural form used with 1\n * @property {string} [two] - Plural form used with 2\n * @property {string} [few] - Plural form used for a few\n * @property {string} [many] - Plural form used for many\n */\n"],"names":["I18n","constructor","translations","config","_config$locale","locale","document","documentElement","lang","t","lookupKey","options","Error","translation","count","isObject","translationPluralForm","getPluralSuffix","match","replacePlaceholders","translationString","formatter","Intl","NumberFormat","supportedLocalesOf","length","undefined","replace","placeholderWithBraces","placeholderKey","Object","prototype","hasOwnProperty","call","placeholderValue","format","hasIntlPluralRulesSupport","Boolean","window","PluralRules","Number","isFinite","preferredForm","select","console","warn"],"mappings":"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"}