govuk_publishing_components 32.1.0 → 33.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/javascripts/component_guide/accessibility-test.js +0 -1
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-core.js +175 -0
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-ecommerce-tracker.js +4 -4
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.js +5 -13
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-link-tracker.js +80 -309
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-page-views.js +2 -2
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-specialist-link-tracker.js +140 -0
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.js +3 -0
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4.js +1 -0
- data/app/assets/javascripts/govuk_publishing_components/components/accordion.js +12 -1
- data/app/assets/javascripts/govuk_publishing_components/components/single-page-notification-button.js +24 -8
- data/app/assets/javascripts/govuk_publishing_components/components/step-by-step-nav.js +22 -1
- data/app/assets/javascripts/govuk_publishing_components/vendor/lux/lux-reporter.js +140 -191
- data/app/assets/stylesheets/govuk_publishing_components/components/_big-number.scss +2 -5
- data/app/assets/stylesheets/govuk_publishing_components/components/_image-card.scss +1 -5
- data/app/assets/stylesheets/govuk_publishing_components/components/_input.scss +3 -5
- data/app/assets/stylesheets/govuk_publishing_components/components/_layout-super-navigation-header.scss +10 -30
- data/app/assets/stylesheets/govuk_publishing_components/components/_search.scss +0 -7
- data/app/views/govuk_publishing_components/components/_accordion.html.erb +14 -1
- data/app/views/govuk_publishing_components/components/_error_summary.html.erb +27 -26
- data/app/views/govuk_publishing_components/components/_layout_super_navigation_header.html.erb +2 -2
- data/app/views/govuk_publishing_components/components/_phase_banner.html.erb +1 -1
- data/app/views/govuk_publishing_components/components/_share_links.html.erb +11 -13
- data/app/views/govuk_publishing_components/components/_single_page_notification_button.html.erb +1 -1
- data/app/views/govuk_publishing_components/components/_step_by_step_nav.html.erb +4 -1
- data/app/views/govuk_publishing_components/components/docs/accordion.yml +15 -3
- data/app/views/govuk_publishing_components/components/docs/button.yml +10 -0
- data/app/views/govuk_publishing_components/components/docs/share_links.yml +59 -30
- data/app/views/govuk_publishing_components/components/docs/single_page_notification_button.yml +10 -1
- data/app/views/govuk_publishing_components/components/docs/step_by_step_nav.yml +34 -0
- data/app/views/govuk_publishing_components/components/feedback/_problem_form.html.erb +1 -1
- data/app/views/govuk_publishing_components/components/feedback/_survey_signup_form.html.erb +1 -1
- data/app/views/govuk_publishing_components/components/feedback/_yes_no_banner.html.erb +3 -3
- data/lib/govuk_publishing_components/presenters/button_helper.rb +9 -2
- data/lib/govuk_publishing_components/presenters/single_page_notification_button_helper.rb +25 -1
- data/lib/govuk_publishing_components/version.rb +1 -1
- data/node_modules/axe-core/axe.js +4559 -4673
- data/node_modules/axe-core/axe.min.js +2 -2
- data/node_modules/axe-core/package.json +2 -2
- data/node_modules/axe-core/sri-history.json +4 -0
- data/node_modules/govuk-frontend/README.md +1 -2
- data/node_modules/govuk-frontend/govuk/all.js +1398 -273
- data/node_modules/govuk-frontend/govuk/common/closest-attribute-value.js +70 -0
- data/node_modules/govuk-frontend/govuk/common/index.js +172 -0
- data/node_modules/govuk-frontend/govuk/common/normalise-dataset.js +373 -0
- data/node_modules/govuk-frontend/govuk/common.js +138 -3
- data/node_modules/govuk-frontend/govuk/components/accordion/accordion.js +753 -25
- data/node_modules/govuk-frontend/govuk/components/accordion/fixtures.json +54 -22
- data/node_modules/govuk-frontend/govuk/components/accordion/macro-options.json +36 -0
- data/node_modules/govuk-frontend/govuk/components/accordion/template.njk +7 -1
- data/node_modules/govuk-frontend/govuk/components/back-link/fixtures.json +12 -12
- data/node_modules/govuk-frontend/govuk/components/breadcrumbs/fixtures.json +22 -22
- data/node_modules/govuk-frontend/govuk/components/button/_index.scss +23 -5
- data/node_modules/govuk-frontend/govuk/components/button/button.js +365 -107
- data/node_modules/govuk-frontend/govuk/components/button/fixtures.json +85 -66
- data/node_modules/govuk-frontend/govuk/components/button/template.njk +1 -1
- data/node_modules/govuk-frontend/govuk/components/character-count/_index.scss +9 -0
- data/node_modules/govuk-frontend/govuk/components/character-count/character-count.js +1033 -121
- data/node_modules/govuk-frontend/govuk/components/character-count/fixtures.json +112 -36
- data/node_modules/govuk-frontend/govuk/components/character-count/macro-options.json +42 -0
- data/node_modules/govuk-frontend/govuk/components/character-count/template.njk +27 -3
- data/node_modules/govuk-frontend/govuk/components/checkboxes/checkboxes.js +30 -2
- data/node_modules/govuk-frontend/govuk/components/checkboxes/fixtures.json +96 -93
- data/node_modules/govuk-frontend/govuk/components/cookie-banner/fixtures.json +46 -46
- data/node_modules/govuk-frontend/govuk/components/date-input/fixtures.json +50 -50
- data/node_modules/govuk-frontend/govuk/components/details/details.js +43 -13
- data/node_modules/govuk-frontend/govuk/components/details/fixtures.json +20 -20
- data/node_modules/govuk-frontend/govuk/components/error-message/fixtures.json +20 -20
- data/node_modules/govuk-frontend/govuk/components/error-summary/error-summary.js +268 -6
- data/node_modules/govuk-frontend/govuk/components/error-summary/fixtures.json +44 -35
- data/node_modules/govuk-frontend/govuk/components/error-summary/template.njk +25 -21
- data/node_modules/govuk-frontend/govuk/components/fieldset/fixtures.json +51 -39
- data/node_modules/govuk-frontend/govuk/components/file-upload/fixtures.json +26 -26
- data/node_modules/govuk-frontend/govuk/components/footer/_index.scss +1 -1
- data/node_modules/govuk-frontend/govuk/components/footer/fixtures.json +46 -46
- data/node_modules/govuk-frontend/govuk/components/footer/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/header/fixtures.json +93 -38
- data/node_modules/govuk-frontend/govuk/components/header/header.js +6 -0
- data/node_modules/govuk-frontend/govuk/components/header/macro-options.json +8 -2
- data/node_modules/govuk-frontend/govuk/components/header/template.njk +4 -2
- data/node_modules/govuk-frontend/govuk/components/hint/fixtures.json +12 -12
- data/node_modules/govuk-frontend/govuk/components/input/fixtures.json +80 -80
- data/node_modules/govuk-frontend/govuk/components/inset-text/fixtures.json +12 -12
- data/node_modules/govuk-frontend/govuk/components/label/fixtures.json +34 -34
- data/node_modules/govuk-frontend/govuk/components/notification-banner/fixtures.json +56 -46
- data/node_modules/govuk-frontend/govuk/components/notification-banner/notification-banner.js +252 -2
- data/node_modules/govuk-frontend/govuk/components/notification-banner/template.njk +1 -1
- data/node_modules/govuk-frontend/govuk/components/pagination/_index.scss +10 -7
- data/node_modules/govuk-frontend/govuk/components/pagination/fixtures.json +33 -26
- data/node_modules/govuk-frontend/govuk/components/panel/fixtures.json +18 -18
- data/node_modules/govuk-frontend/govuk/components/phase-banner/fixtures.json +14 -14
- data/node_modules/govuk-frontend/govuk/components/radios/fixtures.json +94 -91
- data/node_modules/govuk-frontend/govuk/components/radios/radios.js +30 -2
- data/node_modules/govuk-frontend/govuk/components/select/fixtures.json +32 -32
- data/node_modules/govuk-frontend/govuk/components/skip-link/fixtures.json +22 -20
- data/node_modules/govuk-frontend/govuk/components/skip-link/skip-link.js +10 -4
- data/node_modules/govuk-frontend/govuk/components/summary-list/fixtures.json +50 -50
- data/node_modules/govuk-frontend/govuk/components/table/_index.scss +1 -1
- data/node_modules/govuk-frontend/govuk/components/table/fixtures.json +40 -40
- data/node_modules/govuk-frontend/govuk/components/tabs/fixtures.json +29 -29
- data/node_modules/govuk-frontend/govuk/components/tabs/tabs.js +28 -0
- data/node_modules/govuk-frontend/govuk/components/tag/fixtures.json +28 -28
- data/node_modules/govuk-frontend/govuk/components/textarea/fixtures.json +34 -34
- data/node_modules/govuk-frontend/govuk/components/warning-text/fixtures.json +14 -14
- data/node_modules/govuk-frontend/govuk/core/_section-break.scss +1 -1
- data/node_modules/govuk-frontend/govuk/helpers/_colour.scss +2 -2
- data/node_modules/govuk-frontend/govuk/helpers/_links.scss +6 -6
- data/node_modules/govuk-frontend/govuk/i18n.js +390 -0
- data/node_modules/govuk-frontend/govuk/macros/i18n.njk +15 -0
- data/node_modules/govuk-frontend/govuk/settings/_all.scss +1 -0
- data/node_modules/govuk-frontend/govuk/settings/_colours-palette.scss +12 -0
- data/node_modules/govuk-frontend/govuk/settings/_compatibility.scss +26 -0
- data/node_modules/govuk-frontend/govuk/settings/_typography-font.scss +23 -0
- data/node_modules/govuk-frontend/govuk/settings/_typography-responsive.scss +12 -0
- data/node_modules/govuk-frontend/govuk/settings/_warnings.scss +53 -0
- data/node_modules/govuk-frontend/govuk/tools/_compatibility.scss +20 -6
- data/node_modules/govuk-frontend/govuk/vendor/polyfills/Date/now.js +21 -0
- data/node_modules/govuk-frontend/govuk/vendor/polyfills/Element/prototype/dataset.js +300 -0
- data/node_modules/govuk-frontend/govuk/vendor/polyfills/String/prototype/trim.js +21 -0
- data/node_modules/govuk-frontend/govuk-esm/all.mjs +50 -27
- data/node_modules/govuk-frontend/govuk-esm/common/closest-attribute-value.mjs +15 -0
- data/node_modules/govuk-frontend/govuk-esm/common/index.mjs +159 -0
- data/node_modules/govuk-frontend/govuk-esm/common/normalise-dataset.mjs +58 -0
- data/node_modules/govuk-frontend/govuk-esm/common.mjs +6 -28
- data/node_modules/govuk-frontend/govuk-esm/components/accordion/accordion.mjs +113 -43
- data/node_modules/govuk-frontend/govuk-esm/components/button/button.mjs +67 -30
- data/node_modules/govuk-frontend/govuk-esm/components/character-count/character-count.mjs +325 -123
- data/node_modules/govuk-frontend/govuk-esm/components/checkboxes/checkboxes.mjs +9 -3
- data/node_modules/govuk-frontend/govuk-esm/components/details/details.mjs +22 -8
- data/node_modules/govuk-frontend/govuk-esm/components/error-summary/error-summary.mjs +48 -6
- data/node_modules/govuk-frontend/govuk-esm/components/header/header.mjs +6 -0
- data/node_modules/govuk-frontend/govuk-esm/components/notification-banner/notification-banner.mjs +32 -2
- data/node_modules/govuk-frontend/govuk-esm/components/radios/radios.mjs +9 -3
- data/node_modules/govuk-frontend/govuk-esm/components/skip-link/skip-link.mjs +10 -4
- data/node_modules/govuk-frontend/govuk-esm/components/tabs/tabs.mjs +8 -2
- data/node_modules/govuk-frontend/govuk-esm/i18n.mjs +380 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Date/now.mjs +13 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/dataset.mjs +68 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/String/prototype/trim.mjs +13 -0
- data/node_modules/govuk-frontend/govuk-prototype-kit/init.js +7 -0
- data/node_modules/govuk-frontend/govuk-prototype-kit/init.scss +12 -0
- data/node_modules/govuk-frontend/govuk-prototype-kit.config.json +138 -7
- data/node_modules/govuk-frontend/package.json +1 -1
- metadata +22 -3
@@ -2,8 +2,42 @@ import '../../vendor/polyfills/Function/prototype/bind.mjs'
|
|
2
2
|
import '../../vendor/polyfills/Event.mjs' // addEventListener
|
3
3
|
import '../../vendor/polyfills/Element/prototype/closest.mjs'
|
4
4
|
|
5
|
-
|
5
|
+
import { mergeConfigs } from '../../common/index.mjs'
|
6
|
+
import { normaliseDataset } from '../../common/normalise-dataset.mjs'
|
7
|
+
|
8
|
+
/**
|
9
|
+
* JavaScript enhancements for the ErrorSummary
|
10
|
+
*
|
11
|
+
* Takes focus on initialisation for accessible announcement, unless disabled in configuration.
|
12
|
+
*
|
13
|
+
* @class
|
14
|
+
* @param {HTMLElement} $module - The element this component controls
|
15
|
+
* @param {ErrorSummaryConfig} config - Error summary config
|
16
|
+
*/
|
17
|
+
function ErrorSummary ($module, config) {
|
18
|
+
// Some consuming code may not be passing a module,
|
19
|
+
// for example if they initialise the component
|
20
|
+
// on their own by directly passing the result
|
21
|
+
// of `document.querySelector`.
|
22
|
+
// To avoid breaking further JavaScript initialisation
|
23
|
+
// we need to safeguard against this so things keep
|
24
|
+
// working the same now we read the elements data attributes
|
25
|
+
if (!$module) {
|
26
|
+
// Little safety in case code gets ported as-is
|
27
|
+
// into and ES6 class constructor, where the return value matters
|
28
|
+
return this
|
29
|
+
}
|
30
|
+
|
6
31
|
this.$module = $module
|
32
|
+
|
33
|
+
var defaultConfig = {
|
34
|
+
disableAutoFocus: false
|
35
|
+
}
|
36
|
+
this.config = mergeConfigs(
|
37
|
+
defaultConfig,
|
38
|
+
config || {},
|
39
|
+
normaliseDataset($module.dataset)
|
40
|
+
)
|
7
41
|
}
|
8
42
|
|
9
43
|
ErrorSummary.prototype.init = function () {
|
@@ -22,7 +56,7 @@ ErrorSummary.prototype.init = function () {
|
|
22
56
|
ErrorSummary.prototype.setFocus = function () {
|
23
57
|
var $module = this.$module
|
24
58
|
|
25
|
-
if (
|
59
|
+
if (this.config.disableAutoFocus) {
|
26
60
|
return
|
27
61
|
}
|
28
62
|
|
@@ -38,10 +72,10 @@ ErrorSummary.prototype.setFocus = function () {
|
|
38
72
|
}
|
39
73
|
|
40
74
|
/**
|
41
|
-
* Click event handler
|
42
|
-
*
|
43
|
-
* @param {MouseEvent} event - Click event
|
44
|
-
*/
|
75
|
+
* Click event handler
|
76
|
+
*
|
77
|
+
* @param {MouseEvent} event - Click event
|
78
|
+
*/
|
45
79
|
ErrorSummary.prototype.handleClick = function (event) {
|
46
80
|
var target = event.target
|
47
81
|
if (this.focusTarget(target)) {
|
@@ -166,3 +200,11 @@ ErrorSummary.prototype.getAssociatedLegendOrLabel = function ($input) {
|
|
166
200
|
}
|
167
201
|
|
168
202
|
export default ErrorSummary
|
203
|
+
|
204
|
+
/**
|
205
|
+
* Error summary config
|
206
|
+
*
|
207
|
+
* @typedef {object} ErrorSummaryConfig
|
208
|
+
* @property {boolean} [disableAutoFocus = false] -
|
209
|
+
* If set to `true` the error summary will not be focussed when the page loads.
|
210
|
+
*/
|
@@ -2,6 +2,12 @@ import '../../vendor/polyfills/Event.mjs'
|
|
2
2
|
import '../../vendor/polyfills/Element/prototype/classList.mjs'
|
3
3
|
import '../../vendor/polyfills/Function/prototype/bind.mjs'
|
4
4
|
|
5
|
+
/**
|
6
|
+
* Header component
|
7
|
+
*
|
8
|
+
* @class
|
9
|
+
* @param {HTMLElement} $module - HTML element to use for header
|
10
|
+
*/
|
5
11
|
function Header ($module) {
|
6
12
|
this.$module = $module
|
7
13
|
this.$menuButton = $module && $module.querySelector('.govuk-js-header-toggle')
|
data/node_modules/govuk-frontend/govuk-esm/components/notification-banner/notification-banner.mjs
CHANGED
@@ -1,7 +1,26 @@
|
|
1
1
|
import '../../vendor/polyfills/Event.mjs' // addEventListener
|
2
2
|
|
3
|
-
|
3
|
+
import { mergeConfigs } from '../../common/index.mjs'
|
4
|
+
import { normaliseDataset } from '../../common/normalise-dataset.mjs'
|
5
|
+
|
6
|
+
/**
|
7
|
+
* Notification Banner component
|
8
|
+
*
|
9
|
+
* @class
|
10
|
+
* @param {HTMLElement} $module - HTML element to use for notification banner
|
11
|
+
* @param {NotificationBannerConfig} config - Notification banner config
|
12
|
+
*/
|
13
|
+
function NotificationBanner ($module, config) {
|
4
14
|
this.$module = $module
|
15
|
+
|
16
|
+
var defaultConfig = {
|
17
|
+
disableAutoFocus: false
|
18
|
+
}
|
19
|
+
this.config = mergeConfigs(
|
20
|
+
defaultConfig,
|
21
|
+
config || {},
|
22
|
+
normaliseDataset($module.dataset)
|
23
|
+
)
|
5
24
|
}
|
6
25
|
|
7
26
|
/**
|
@@ -30,7 +49,7 @@ NotificationBanner.prototype.init = function () {
|
|
30
49
|
NotificationBanner.prototype.setFocus = function () {
|
31
50
|
var $module = this.$module
|
32
51
|
|
33
|
-
if (
|
52
|
+
if (this.config.disableAutoFocus) {
|
34
53
|
return
|
35
54
|
}
|
36
55
|
|
@@ -53,3 +72,14 @@ NotificationBanner.prototype.setFocus = function () {
|
|
53
72
|
}
|
54
73
|
|
55
74
|
export default NotificationBanner
|
75
|
+
|
76
|
+
/**
|
77
|
+
* Notification banner config
|
78
|
+
*
|
79
|
+
* @typedef {object} NotificationBannerConfig
|
80
|
+
* @property {boolean} [disableAutoFocus = false] -
|
81
|
+
* If set to `true` the notification banner will not be focussed when the page
|
82
|
+
* loads. This only applies if the component has a `role` of `alert` – in
|
83
|
+
* other cases the component will not be focused on page load, regardless of
|
84
|
+
* this option.
|
85
|
+
*/
|
@@ -2,8 +2,14 @@ import '../../vendor/polyfills/Function/prototype/bind.mjs'
|
|
2
2
|
// addEventListener, event.target normalization and DOMContentLoaded
|
3
3
|
import '../../vendor/polyfills/Event.mjs'
|
4
4
|
import '../../vendor/polyfills/Element/prototype/classList.mjs'
|
5
|
-
import { nodeListForEach } from '../../common.mjs'
|
5
|
+
import { nodeListForEach } from '../../common/index.mjs'
|
6
6
|
|
7
|
+
/**
|
8
|
+
* Radios component
|
9
|
+
*
|
10
|
+
* @class
|
11
|
+
* @param {HTMLElement} $module - HTML element to use for radios
|
12
|
+
*/
|
7
13
|
function Radios ($module) {
|
8
14
|
this.$module = $module
|
9
15
|
this.$inputs = $module.querySelectorAll('input[type="radio"]')
|
@@ -74,7 +80,7 @@ Radios.prototype.syncAllConditionalReveals = function () {
|
|
74
80
|
* Synchronise the visibility of the conditional reveal, and its accessible
|
75
81
|
* state, with the input's checked state.
|
76
82
|
*
|
77
|
-
* @param {HTMLInputElement} $input Radio input
|
83
|
+
* @param {HTMLInputElement} $input - Radio input
|
78
84
|
*/
|
79
85
|
Radios.prototype.syncConditionalRevealWithInputState = function ($input) {
|
80
86
|
var $target = document.getElementById($input.getAttribute('aria-controls'))
|
@@ -95,7 +101,7 @@ Radios.prototype.syncConditionalRevealWithInputState = function ($input) {
|
|
95
101
|
* with the same name (because checking one radio could have un-checked a radio
|
96
102
|
* in another $module)
|
97
103
|
*
|
98
|
-
* @param {MouseEvent} event Click event
|
104
|
+
* @param {MouseEvent} event - Click event
|
99
105
|
*/
|
100
106
|
Radios.prototype.handleClick = function (event) {
|
101
107
|
var $clickedInput = event.target
|
@@ -2,6 +2,12 @@ import '../../vendor/polyfills/Function/prototype/bind.mjs'
|
|
2
2
|
import '../../vendor/polyfills/Element/prototype/classList.mjs'
|
3
3
|
import '../../vendor/polyfills/Event.mjs' // addEventListener and event.target normalization
|
4
4
|
|
5
|
+
/**
|
6
|
+
* Skip link component
|
7
|
+
*
|
8
|
+
* @class
|
9
|
+
* @param {HTMLElement} $module - HTML element to use for skip link
|
10
|
+
*/
|
5
11
|
function SkipLink ($module) {
|
6
12
|
this.$module = $module
|
7
13
|
this.$linkedElement = null
|
@@ -27,10 +33,10 @@ SkipLink.prototype.init = function () {
|
|
27
33
|
}
|
28
34
|
|
29
35
|
/**
|
30
|
-
* Get linked element
|
31
|
-
*
|
32
|
-
* @returns {HTMLElement} $linkedElement - DOM element linked to from the skip link
|
33
|
-
*/
|
36
|
+
* Get linked element
|
37
|
+
*
|
38
|
+
* @returns {HTMLElement} $linkedElement - DOM element linked to from the skip link
|
39
|
+
*/
|
34
40
|
SkipLink.prototype.getLinkedElement = function () {
|
35
41
|
var linkedElementId = this.getFragmentFromUrl()
|
36
42
|
|
@@ -3,8 +3,14 @@ import '../../vendor/polyfills/Element/prototype/classList.mjs'
|
|
3
3
|
import '../../vendor/polyfills/Element/prototype/nextElementSibling.mjs'
|
4
4
|
import '../../vendor/polyfills/Element/prototype/previousElementSibling.mjs'
|
5
5
|
import '../../vendor/polyfills/Event.mjs' // addEventListener and event.target normaliziation
|
6
|
-
import { nodeListForEach } from '../../common.mjs'
|
7
|
-
|
6
|
+
import { nodeListForEach } from '../../common/index.mjs'
|
7
|
+
|
8
|
+
/**
|
9
|
+
* Tabs component
|
10
|
+
*
|
11
|
+
* @class
|
12
|
+
* @param {HTMLElement} $module - HTML element to use for tabs
|
13
|
+
*/
|
8
14
|
function Tabs ($module) {
|
9
15
|
this.$module = $module
|
10
16
|
this.$tabs = $module.querySelectorAll('.govuk-tabs__tab')
|
@@ -0,0 +1,380 @@
|
|
1
|
+
/**
|
2
|
+
* Internal support for selecting messages to render, with placeholder
|
3
|
+
* interpolation and locale-aware number formatting and pluralisation
|
4
|
+
*
|
5
|
+
* @class
|
6
|
+
* @private
|
7
|
+
* @param {TranslationsFlattened} translations - Key-value pairs of the translation strings to use.
|
8
|
+
* @param {object} [config] - Configuration options for the function.
|
9
|
+
* @param {string} config.locale - An overriding locale for the PluralRules functionality.
|
10
|
+
*/
|
11
|
+
export function I18n (translations, config) {
|
12
|
+
// Make list of translations available throughout function
|
13
|
+
this.translations = translations || {}
|
14
|
+
|
15
|
+
// The locale to use for PluralRules and NumberFormat
|
16
|
+
this.locale = (config && config.locale) || document.documentElement.lang || 'en'
|
17
|
+
}
|
18
|
+
|
19
|
+
/**
|
20
|
+
* The most used function - takes the key for a given piece of UI text and
|
21
|
+
* returns the appropriate string.
|
22
|
+
*
|
23
|
+
* @param {string} lookupKey - The lookup key of the string to use.
|
24
|
+
* @param {object} options - Any options passed with the translation string, e.g: for string interpolation.
|
25
|
+
* @returns {string} The appropriate translation string.
|
26
|
+
*/
|
27
|
+
I18n.prototype.t = function (lookupKey, options) {
|
28
|
+
if (!lookupKey) {
|
29
|
+
// Print a console error if no lookup key has been provided
|
30
|
+
throw new Error('i18n: lookup key missing')
|
31
|
+
}
|
32
|
+
|
33
|
+
// If the `count` option is set, determine which plural suffix is needed and
|
34
|
+
// change the lookupKey to match. We check to see if it's undefined instead of
|
35
|
+
// falsy, as this could legitimately be 0.
|
36
|
+
if (options && typeof options.count !== 'undefined') {
|
37
|
+
// Get the plural suffix
|
38
|
+
lookupKey = lookupKey + '.' + this.getPluralSuffix(lookupKey, options.count)
|
39
|
+
}
|
40
|
+
|
41
|
+
if (lookupKey in this.translations) {
|
42
|
+
// Fetch the translation string for that lookup key
|
43
|
+
var translationString = this.translations[lookupKey]
|
44
|
+
|
45
|
+
// Check for ${} placeholders in the translation string
|
46
|
+
if (translationString.match(/%{(.\S+)}/)) {
|
47
|
+
if (!options) {
|
48
|
+
throw new Error('i18n: cannot replace placeholders in string if no option data provided')
|
49
|
+
}
|
50
|
+
|
51
|
+
return this.replacePlaceholders(translationString, options)
|
52
|
+
} else {
|
53
|
+
return translationString
|
54
|
+
}
|
55
|
+
} else {
|
56
|
+
// If the key wasn't found in our translations object,
|
57
|
+
// return the lookup key itself as the fallback
|
58
|
+
return lookupKey
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
/**
|
63
|
+
* Takes a translation string with placeholders, and replaces the placeholders
|
64
|
+
* with the provided data
|
65
|
+
*
|
66
|
+
* @param {string} translationString - The translation string
|
67
|
+
* @param {object} options - Any options passed with the translation string, e.g: for string interpolation.
|
68
|
+
* @returns {string} The translation string to output, with ${} placeholders replaced
|
69
|
+
*/
|
70
|
+
I18n.prototype.replacePlaceholders = function (translationString, options) {
|
71
|
+
var formatter
|
72
|
+
|
73
|
+
if (this.hasIntlNumberFormatSupport()) {
|
74
|
+
formatter = new Intl.NumberFormat(this.locale)
|
75
|
+
}
|
76
|
+
|
77
|
+
return translationString.replace(/%{(.\S+)}/g, function (placeholderWithBraces, placeholderKey) {
|
78
|
+
if (Object.prototype.hasOwnProperty.call(options, placeholderKey)) {
|
79
|
+
var placeholderValue = options[placeholderKey]
|
80
|
+
|
81
|
+
// If a user has passed `false` as the value for the placeholder
|
82
|
+
// treat it as though the value should not be displayed
|
83
|
+
if (placeholderValue === false) {
|
84
|
+
return ''
|
85
|
+
}
|
86
|
+
|
87
|
+
// If the placeholder's value is a number, localise the number formatting
|
88
|
+
if (typeof placeholderValue === 'number' && formatter) {
|
89
|
+
return formatter.format(placeholderValue)
|
90
|
+
}
|
91
|
+
|
92
|
+
return placeholderValue
|
93
|
+
} else {
|
94
|
+
throw new Error('i18n: no data found to replace ' + placeholderWithBraces + ' placeholder in string')
|
95
|
+
}
|
96
|
+
})
|
97
|
+
}
|
98
|
+
|
99
|
+
/**
|
100
|
+
* Check to see if the browser supports Intl and Intl.PluralRules.
|
101
|
+
*
|
102
|
+
* It requires all conditions to be met in order to be supported:
|
103
|
+
* - The browser supports the Intl class (true in IE11)
|
104
|
+
* - The implementation of Intl supports PluralRules (NOT true in IE11)
|
105
|
+
* - The browser/OS has plural rules for the current locale (browser dependent)
|
106
|
+
*
|
107
|
+
* @returns {boolean} Returns true if all conditions are met. Returns false otherwise.
|
108
|
+
*/
|
109
|
+
I18n.prototype.hasIntlPluralRulesSupport = function () {
|
110
|
+
return Boolean(window.Intl && ('PluralRules' in window.Intl && Intl.PluralRules.supportedLocalesOf(this.locale).length))
|
111
|
+
}
|
112
|
+
|
113
|
+
/**
|
114
|
+
* Check to see if the browser supports Intl and Intl.NumberFormat.
|
115
|
+
*
|
116
|
+
* It requires all conditions to be met in order to be supported:
|
117
|
+
* - The browser supports the Intl class (true in IE11)
|
118
|
+
* - The implementation of Intl supports NumberFormat (also true in IE11)
|
119
|
+
* - The browser/OS has number formatting rules for the current locale (browser dependent)
|
120
|
+
*
|
121
|
+
* @returns {boolean} Returns true if all conditions are met. Returns false otherwise.
|
122
|
+
*/
|
123
|
+
I18n.prototype.hasIntlNumberFormatSupport = function () {
|
124
|
+
return Boolean(window.Intl && ('NumberFormat' in window.Intl && Intl.NumberFormat.supportedLocalesOf(this.locale).length))
|
125
|
+
}
|
126
|
+
|
127
|
+
/**
|
128
|
+
* Get the appropriate suffix for the plural form.
|
129
|
+
*
|
130
|
+
* Uses Intl.PluralRules (or our own fallback implementation) to get the
|
131
|
+
* 'preferred' form to use for the given count.
|
132
|
+
*
|
133
|
+
* Checks that a translation has been provided for that plural form – if it
|
134
|
+
* hasn't, it'll fall back to the 'other' plural form (unless that doesn't exist
|
135
|
+
* either, in which case an error will be thrown)
|
136
|
+
*
|
137
|
+
* @param {string} lookupKey - The lookup key of the string to use.
|
138
|
+
* @param {number} count - Number used to determine which pluralisation to use.
|
139
|
+
* @returns {PluralRule} The suffix associated with the correct pluralisation for this locale.
|
140
|
+
*/
|
141
|
+
I18n.prototype.getPluralSuffix = function (lookupKey, count) {
|
142
|
+
// Validate that the number is actually a number.
|
143
|
+
//
|
144
|
+
// Number(count) will turn anything that can't be converted to a Number type
|
145
|
+
// into 'NaN'. isFinite filters out NaN, as it isn't a finite number.
|
146
|
+
count = Number(count)
|
147
|
+
if (!isFinite(count)) { return 'other' }
|
148
|
+
|
149
|
+
var preferredForm
|
150
|
+
|
151
|
+
// Check to verify that all the requirements for Intl.PluralRules are met.
|
152
|
+
// If so, we can use that instead of our custom implementation. Otherwise,
|
153
|
+
// use the hardcoded fallback.
|
154
|
+
if (this.hasIntlPluralRulesSupport()) {
|
155
|
+
preferredForm = new Intl.PluralRules(this.locale).select(count)
|
156
|
+
} else {
|
157
|
+
preferredForm = this.selectPluralFormUsingFallbackRules(count)
|
158
|
+
}
|
159
|
+
|
160
|
+
// Use the correct plural form if provided
|
161
|
+
if (lookupKey + '.' + preferredForm in this.translations) {
|
162
|
+
return preferredForm
|
163
|
+
// Fall back to `other` if the plural form is missing, but log a warning
|
164
|
+
// to the console
|
165
|
+
} else if (lookupKey + '.other' in this.translations) {
|
166
|
+
if (console && 'warn' in console) {
|
167
|
+
console.warn('i18n: Missing plural form ".' + preferredForm + '" for "' +
|
168
|
+
this.locale + '" locale. Falling back to ".other".')
|
169
|
+
}
|
170
|
+
|
171
|
+
return 'other'
|
172
|
+
// If the required `other` plural form is missing, all we can do is error
|
173
|
+
} else {
|
174
|
+
throw new Error(
|
175
|
+
'i18n: Plural form ".other" is required for "' + this.locale + '" locale'
|
176
|
+
)
|
177
|
+
}
|
178
|
+
}
|
179
|
+
|
180
|
+
/**
|
181
|
+
* Get the plural form using our fallback implementation
|
182
|
+
*
|
183
|
+
* This is split out into a separate function to make it easier to test the
|
184
|
+
* fallback behaviour in an environment where Intl.PluralRules exists.
|
185
|
+
*
|
186
|
+
* @param {number} count - Number used to determine which pluralisation to use.
|
187
|
+
* @returns {PluralRule} The pluralisation form for count in this locale.
|
188
|
+
*/
|
189
|
+
I18n.prototype.selectPluralFormUsingFallbackRules = function (count) {
|
190
|
+
// Currently our custom code can only handle positive integers, so let's
|
191
|
+
// make sure our number is one of those.
|
192
|
+
count = Math.abs(Math.floor(count))
|
193
|
+
|
194
|
+
var ruleset = this.getPluralRulesForLocale()
|
195
|
+
|
196
|
+
if (ruleset) {
|
197
|
+
return I18n.pluralRules[ruleset](count)
|
198
|
+
}
|
199
|
+
|
200
|
+
return 'other'
|
201
|
+
}
|
202
|
+
|
203
|
+
/**
|
204
|
+
* Work out which pluralisation rules to use for the current locale
|
205
|
+
*
|
206
|
+
* The locale may include a regional indicator (such as en-GB), but we don't
|
207
|
+
* usually care about this part, as pluralisation rules are usually the same
|
208
|
+
* regardless of region. There are exceptions, however, (e.g. Portuguese) so
|
209
|
+
* this searches by both the full and shortened locale codes, just to be sure.
|
210
|
+
*
|
211
|
+
* @returns {PluralRuleName | undefined} The name of the pluralisation rule to use (a key for one
|
212
|
+
* of the functions in this.pluralRules)
|
213
|
+
*/
|
214
|
+
I18n.prototype.getPluralRulesForLocale = function () {
|
215
|
+
var locale = this.locale
|
216
|
+
var localeShort = locale.split('-')[0]
|
217
|
+
|
218
|
+
// Look through the plural rules map to find which `pluralRule` is
|
219
|
+
// appropriate for our current `locale`.
|
220
|
+
for (var pluralRule in I18n.pluralRulesMap) {
|
221
|
+
if (Object.prototype.hasOwnProperty.call(I18n.pluralRulesMap, pluralRule)) {
|
222
|
+
var languages = I18n.pluralRulesMap[pluralRule]
|
223
|
+
for (var i = 0; i < languages.length; i++) {
|
224
|
+
if (languages[i] === locale || languages[i] === localeShort) {
|
225
|
+
return pluralRule
|
226
|
+
}
|
227
|
+
}
|
228
|
+
}
|
229
|
+
}
|
230
|
+
}
|
231
|
+
|
232
|
+
/**
|
233
|
+
* Map of plural rules to languages where those rules apply.
|
234
|
+
*
|
235
|
+
* Note: These groups are named for the most dominant or recognisable language
|
236
|
+
* that uses each system. The groupings do not imply that the languages are
|
237
|
+
* related to one another. Many languages have evolved the same systems
|
238
|
+
* independently of one another.
|
239
|
+
*
|
240
|
+
* Code to support more languages can be found in the i18n spike:
|
241
|
+
* {@link https://github.com/alphagov/govuk-frontend/blob/spike-i18n-support/src/govuk/i18n.mjs}
|
242
|
+
*
|
243
|
+
* Languages currently supported:
|
244
|
+
*
|
245
|
+
* Arabic: Arabic (ar)
|
246
|
+
* Chinese: Burmese (my), Chinese (zh), Indonesian (id), Japanese (ja),
|
247
|
+
* Javanese (jv), Korean (ko), Malay (ms), Thai (th), Vietnamese (vi)
|
248
|
+
* French: Armenian (hy), Bangla (bn), French (fr), Gujarati (gu), Hindi (hi),
|
249
|
+
* Persian Farsi (fa), Punjabi (pa), Zulu (zu)
|
250
|
+
* German: Afrikaans (af), Albanian (sq), Azerbaijani (az), Basque (eu),
|
251
|
+
* Bulgarian (bg), Catalan (ca), Danish (da), Dutch (nl), English (en),
|
252
|
+
* Estonian (et), Finnish (fi), Georgian (ka), German (de), Greek (el),
|
253
|
+
* Hungarian (hu), Luxembourgish (lb), Norwegian (no), Somali (so),
|
254
|
+
* Swahili (sw), Swedish (sv), Tamil (ta), Telugu (te), Turkish (tr),
|
255
|
+
* Urdu (ur)
|
256
|
+
* Irish: Irish Gaelic (ga)
|
257
|
+
* Russian: Russian (ru), Ukrainian (uk)
|
258
|
+
* Scottish: Scottish Gaelic (gd)
|
259
|
+
* Spanish: European Portuguese (pt-PT), Italian (it), Spanish (es)
|
260
|
+
* Welsh: Welsh (cy)
|
261
|
+
*
|
262
|
+
* @type {Object<PluralRuleName, string[]>}
|
263
|
+
*/
|
264
|
+
I18n.pluralRulesMap = {
|
265
|
+
arabic: ['ar'],
|
266
|
+
chinese: ['my', 'zh', 'id', 'ja', 'jv', 'ko', 'ms', 'th', 'vi'],
|
267
|
+
french: ['hy', 'bn', 'fr', 'gu', 'hi', 'fa', 'pa', 'zu'],
|
268
|
+
german: [
|
269
|
+
'af', 'sq', 'az', 'eu', 'bg', 'ca', 'da', 'nl', 'en', 'et', 'fi', 'ka',
|
270
|
+
'de', 'el', 'hu', 'lb', 'no', 'so', 'sw', 'sv', 'ta', 'te', 'tr', 'ur'
|
271
|
+
],
|
272
|
+
irish: ['ga'],
|
273
|
+
russian: ['ru', 'uk'],
|
274
|
+
scottish: ['gd'],
|
275
|
+
spanish: ['pt-PT', 'it', 'es'],
|
276
|
+
welsh: ['cy']
|
277
|
+
}
|
278
|
+
|
279
|
+
/**
|
280
|
+
* Different pluralisation rule sets
|
281
|
+
*
|
282
|
+
* Returns the appropriate suffix for the plural form associated with `n`.
|
283
|
+
* Possible suffixes: 'zero', 'one', 'two', 'few', 'many', 'other' (the actual
|
284
|
+
* meaning of each differs per locale). 'other' should always exist, even in
|
285
|
+
* languages without plurals, such as Chinese.
|
286
|
+
* {@link https://cldr.unicode.org/index/cldr-spec/plural-rules}
|
287
|
+
*
|
288
|
+
* The count must be a positive integer. Negative numbers and decimals aren't accounted for
|
289
|
+
*
|
290
|
+
* @type {Object<string, function(number): PluralRule>}
|
291
|
+
*/
|
292
|
+
I18n.pluralRules = {
|
293
|
+
arabic: function (n) {
|
294
|
+
if (n === 0) { return 'zero' }
|
295
|
+
if (n === 1) { return 'one' }
|
296
|
+
if (n === 2) { return 'two' }
|
297
|
+
if (n % 100 >= 3 && n % 100 <= 10) { return 'few' }
|
298
|
+
if (n % 100 >= 11 && n % 100 <= 99) { return 'many' }
|
299
|
+
return 'other'
|
300
|
+
},
|
301
|
+
chinese: function () {
|
302
|
+
return 'other'
|
303
|
+
},
|
304
|
+
french: function (n) {
|
305
|
+
return n === 0 || n === 1 ? 'one' : 'other'
|
306
|
+
},
|
307
|
+
german: function (n) {
|
308
|
+
return n === 1 ? 'one' : 'other'
|
309
|
+
},
|
310
|
+
irish: function (n) {
|
311
|
+
if (n === 1) { return 'one' }
|
312
|
+
if (n === 2) { return 'two' }
|
313
|
+
if (n >= 3 && n <= 6) { return 'few' }
|
314
|
+
if (n >= 7 && n <= 10) { return 'many' }
|
315
|
+
return 'other'
|
316
|
+
},
|
317
|
+
russian: function (n) {
|
318
|
+
var lastTwo = n % 100
|
319
|
+
var last = lastTwo % 10
|
320
|
+
if (last === 1 && lastTwo !== 11) { return 'one' }
|
321
|
+
if (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)) { return 'few' }
|
322
|
+
if (last === 0 || (last >= 5 && last <= 9) || (lastTwo >= 11 && lastTwo <= 14)) { return 'many' }
|
323
|
+
// Note: The 'other' suffix is only used by decimal numbers in Russian.
|
324
|
+
// We don't anticipate it being used, but it's here for consistency.
|
325
|
+
return 'other'
|
326
|
+
},
|
327
|
+
scottish: function (n) {
|
328
|
+
if (n === 1 || n === 11) { return 'one' }
|
329
|
+
if (n === 2 || n === 12) { return 'two' }
|
330
|
+
if ((n >= 3 && n <= 10) || (n >= 13 && n <= 19)) { return 'few' }
|
331
|
+
return 'other'
|
332
|
+
},
|
333
|
+
spanish: function (n) {
|
334
|
+
if (n === 1) { return 'one' }
|
335
|
+
if (n % 1000000 === 0 && n !== 0) { return 'many' }
|
336
|
+
return 'other'
|
337
|
+
},
|
338
|
+
welsh: function (n) {
|
339
|
+
if (n === 0) { return 'zero' }
|
340
|
+
if (n === 1) { return 'one' }
|
341
|
+
if (n === 2) { return 'two' }
|
342
|
+
if (n === 3) { return 'few' }
|
343
|
+
if (n === 6) { return 'many' }
|
344
|
+
return 'other'
|
345
|
+
}
|
346
|
+
}
|
347
|
+
|
348
|
+
/**
|
349
|
+
* Supported languages for plural rules
|
350
|
+
*
|
351
|
+
* @typedef {'arabic' | 'chinese' | 'french' | 'german' | 'irish' | 'russian' | 'scottish' | 'spanish' | 'welsh'} PluralRuleName
|
352
|
+
*/
|
353
|
+
|
354
|
+
/**
|
355
|
+
* Plural rule category mnemonic tags
|
356
|
+
*
|
357
|
+
* @typedef {'zero' | 'one' | 'two' | 'few' | 'many' | 'other'} PluralRule
|
358
|
+
*/
|
359
|
+
|
360
|
+
/**
|
361
|
+
* Translated message by plural rule they correspond to.
|
362
|
+
*
|
363
|
+
* Allows to group pluralised messages under a single key when passing
|
364
|
+
* translations to a component's constructor
|
365
|
+
*
|
366
|
+
* @typedef {object} TranslationPluralForms
|
367
|
+
* @property {string} [other] - General plural form
|
368
|
+
* @property {string} [zero] - Plural form used with 0
|
369
|
+
* @property {string} [one] - Plural form used with 1
|
370
|
+
* @property {string} [two] - Plural form used with 2
|
371
|
+
* @property {string} [few] - Plural form used for a few
|
372
|
+
* @property {string} [many] - Plural form used for many
|
373
|
+
*/
|
374
|
+
|
375
|
+
/**
|
376
|
+
* Translated messages (flattened)
|
377
|
+
*
|
378
|
+
* @private
|
379
|
+
* @typedef {Object<string, string> | {}} TranslationsFlattened
|
380
|
+
*/
|
@@ -0,0 +1,13 @@
|
|
1
|
+
(function(undefined) {
|
2
|
+
|
3
|
+
// Detection from https://github.com/Financial-Times/polyfill-library/blob/v3.111.0/polyfills/Date/now/detect.js
|
4
|
+
var detect = ('Date' in self && 'now' in self.Date && 'getTime' in self.Date.prototype)
|
5
|
+
|
6
|
+
if (detect) return
|
7
|
+
|
8
|
+
// Polyfill from https://polyfill.io/v3/polyfill.js?version=3.111.0&features=Date.now&flags=always
|
9
|
+
Date.now = function () {
|
10
|
+
return new Date().getTime();
|
11
|
+
};
|
12
|
+
|
13
|
+
}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
|
@@ -0,0 +1,68 @@
|
|
1
|
+
import '../../Object/defineProperty.mjs'
|
2
|
+
import '../../Element.mjs'
|
3
|
+
|
4
|
+
(function(undefined) {
|
5
|
+
|
6
|
+
// Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-library/13cf7c340974d128d557580b5e2dafcd1b1192d1/polyfills/Element/prototype/dataset/detect.js
|
7
|
+
var detect = (function(){
|
8
|
+
if (!document.documentElement.dataset) {
|
9
|
+
return false;
|
10
|
+
}
|
11
|
+
var el = document.createElement('div');
|
12
|
+
el.setAttribute("data-a-b", "c");
|
13
|
+
return el.dataset && el.dataset.aB == "c";
|
14
|
+
}())
|
15
|
+
|
16
|
+
if (detect) return
|
17
|
+
|
18
|
+
// Polyfill derived from https://raw.githubusercontent.com/Financial-Times/polyfill-library/13cf7c340974d128d557580b5e2dafcd1b1192d1/polyfills/Element/prototype/dataset/polyfill.js
|
19
|
+
Object.defineProperty(Element.prototype, 'dataset', {
|
20
|
+
get: function() {
|
21
|
+
var element = this;
|
22
|
+
var attributes = this.attributes;
|
23
|
+
var map = {};
|
24
|
+
|
25
|
+
for (var i = 0; i < attributes.length; i++) {
|
26
|
+
var attribute = attributes[i];
|
27
|
+
|
28
|
+
// This regex has been edited from the original polyfill, to add
|
29
|
+
// support for period (.) separators in data-* attribute names. These
|
30
|
+
// are allowed in the HTML spec, but were not covered by the original
|
31
|
+
// polyfill's regex. We use periods in our i18n implementation.
|
32
|
+
if (attribute && attribute.name && (/^data-\w[.\w-]*$/).test(attribute.name)) {
|
33
|
+
var name = attribute.name;
|
34
|
+
var value = attribute.value;
|
35
|
+
|
36
|
+
var propName = name.substr(5).replace(/-./g, function (prop) {
|
37
|
+
return prop.charAt(1).toUpperCase();
|
38
|
+
});
|
39
|
+
|
40
|
+
// If this browser supports __defineGetter__ and __defineSetter__,
|
41
|
+
// continue using defineProperty. If not (like IE 8 and below), we use
|
42
|
+
// a hacky fallback which at least gives an object in the right format
|
43
|
+
if ('__defineGetter__' in Object.prototype && '__defineSetter__' in Object.prototype) {
|
44
|
+
Object.defineProperty(map, propName, {
|
45
|
+
enumerable: true,
|
46
|
+
get: function() {
|
47
|
+
return this.value;
|
48
|
+
}.bind({value: value || ''}),
|
49
|
+
set: function setter(name, value) {
|
50
|
+
if (typeof value !== 'undefined') {
|
51
|
+
this.setAttribute(name, value);
|
52
|
+
} else {
|
53
|
+
this.removeAttribute(name);
|
54
|
+
}
|
55
|
+
}.bind(element, name)
|
56
|
+
});
|
57
|
+
} else {
|
58
|
+
map[propName] = value
|
59
|
+
}
|
60
|
+
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
return map;
|
65
|
+
}
|
66
|
+
});
|
67
|
+
|
68
|
+
}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
|