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.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/component_guide/accessibility-test.js +0 -1
  3. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-core.js +175 -0
  4. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-ecommerce-tracker.js +4 -4
  5. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.js +5 -13
  6. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-link-tracker.js +80 -309
  7. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-page-views.js +2 -2
  8. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-specialist-link-tracker.js +140 -0
  9. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.js +3 -0
  10. data/app/assets/javascripts/govuk_publishing_components/analytics-ga4.js +1 -0
  11. data/app/assets/javascripts/govuk_publishing_components/components/accordion.js +12 -1
  12. data/app/assets/javascripts/govuk_publishing_components/components/single-page-notification-button.js +24 -8
  13. data/app/assets/javascripts/govuk_publishing_components/components/step-by-step-nav.js +22 -1
  14. data/app/assets/javascripts/govuk_publishing_components/vendor/lux/lux-reporter.js +140 -191
  15. data/app/assets/stylesheets/govuk_publishing_components/components/_big-number.scss +2 -5
  16. data/app/assets/stylesheets/govuk_publishing_components/components/_image-card.scss +1 -5
  17. data/app/assets/stylesheets/govuk_publishing_components/components/_input.scss +3 -5
  18. data/app/assets/stylesheets/govuk_publishing_components/components/_layout-super-navigation-header.scss +10 -30
  19. data/app/assets/stylesheets/govuk_publishing_components/components/_search.scss +0 -7
  20. data/app/views/govuk_publishing_components/components/_accordion.html.erb +14 -1
  21. data/app/views/govuk_publishing_components/components/_error_summary.html.erb +27 -26
  22. data/app/views/govuk_publishing_components/components/_layout_super_navigation_header.html.erb +2 -2
  23. data/app/views/govuk_publishing_components/components/_phase_banner.html.erb +1 -1
  24. data/app/views/govuk_publishing_components/components/_share_links.html.erb +11 -13
  25. data/app/views/govuk_publishing_components/components/_single_page_notification_button.html.erb +1 -1
  26. data/app/views/govuk_publishing_components/components/_step_by_step_nav.html.erb +4 -1
  27. data/app/views/govuk_publishing_components/components/docs/accordion.yml +15 -3
  28. data/app/views/govuk_publishing_components/components/docs/button.yml +10 -0
  29. data/app/views/govuk_publishing_components/components/docs/share_links.yml +59 -30
  30. data/app/views/govuk_publishing_components/components/docs/single_page_notification_button.yml +10 -1
  31. data/app/views/govuk_publishing_components/components/docs/step_by_step_nav.yml +34 -0
  32. data/app/views/govuk_publishing_components/components/feedback/_problem_form.html.erb +1 -1
  33. data/app/views/govuk_publishing_components/components/feedback/_survey_signup_form.html.erb +1 -1
  34. data/app/views/govuk_publishing_components/components/feedback/_yes_no_banner.html.erb +3 -3
  35. data/lib/govuk_publishing_components/presenters/button_helper.rb +9 -2
  36. data/lib/govuk_publishing_components/presenters/single_page_notification_button_helper.rb +25 -1
  37. data/lib/govuk_publishing_components/version.rb +1 -1
  38. data/node_modules/axe-core/axe.js +4559 -4673
  39. data/node_modules/axe-core/axe.min.js +2 -2
  40. data/node_modules/axe-core/package.json +2 -2
  41. data/node_modules/axe-core/sri-history.json +4 -0
  42. data/node_modules/govuk-frontend/README.md +1 -2
  43. data/node_modules/govuk-frontend/govuk/all.js +1398 -273
  44. data/node_modules/govuk-frontend/govuk/common/closest-attribute-value.js +70 -0
  45. data/node_modules/govuk-frontend/govuk/common/index.js +172 -0
  46. data/node_modules/govuk-frontend/govuk/common/normalise-dataset.js +373 -0
  47. data/node_modules/govuk-frontend/govuk/common.js +138 -3
  48. data/node_modules/govuk-frontend/govuk/components/accordion/accordion.js +753 -25
  49. data/node_modules/govuk-frontend/govuk/components/accordion/fixtures.json +54 -22
  50. data/node_modules/govuk-frontend/govuk/components/accordion/macro-options.json +36 -0
  51. data/node_modules/govuk-frontend/govuk/components/accordion/template.njk +7 -1
  52. data/node_modules/govuk-frontend/govuk/components/back-link/fixtures.json +12 -12
  53. data/node_modules/govuk-frontend/govuk/components/breadcrumbs/fixtures.json +22 -22
  54. data/node_modules/govuk-frontend/govuk/components/button/_index.scss +23 -5
  55. data/node_modules/govuk-frontend/govuk/components/button/button.js +365 -107
  56. data/node_modules/govuk-frontend/govuk/components/button/fixtures.json +85 -66
  57. data/node_modules/govuk-frontend/govuk/components/button/template.njk +1 -1
  58. data/node_modules/govuk-frontend/govuk/components/character-count/_index.scss +9 -0
  59. data/node_modules/govuk-frontend/govuk/components/character-count/character-count.js +1033 -121
  60. data/node_modules/govuk-frontend/govuk/components/character-count/fixtures.json +112 -36
  61. data/node_modules/govuk-frontend/govuk/components/character-count/macro-options.json +42 -0
  62. data/node_modules/govuk-frontend/govuk/components/character-count/template.njk +27 -3
  63. data/node_modules/govuk-frontend/govuk/components/checkboxes/checkboxes.js +30 -2
  64. data/node_modules/govuk-frontend/govuk/components/checkboxes/fixtures.json +96 -93
  65. data/node_modules/govuk-frontend/govuk/components/cookie-banner/fixtures.json +46 -46
  66. data/node_modules/govuk-frontend/govuk/components/date-input/fixtures.json +50 -50
  67. data/node_modules/govuk-frontend/govuk/components/details/details.js +43 -13
  68. data/node_modules/govuk-frontend/govuk/components/details/fixtures.json +20 -20
  69. data/node_modules/govuk-frontend/govuk/components/error-message/fixtures.json +20 -20
  70. data/node_modules/govuk-frontend/govuk/components/error-summary/error-summary.js +268 -6
  71. data/node_modules/govuk-frontend/govuk/components/error-summary/fixtures.json +44 -35
  72. data/node_modules/govuk-frontend/govuk/components/error-summary/template.njk +25 -21
  73. data/node_modules/govuk-frontend/govuk/components/fieldset/fixtures.json +51 -39
  74. data/node_modules/govuk-frontend/govuk/components/file-upload/fixtures.json +26 -26
  75. data/node_modules/govuk-frontend/govuk/components/footer/_index.scss +1 -1
  76. data/node_modules/govuk-frontend/govuk/components/footer/fixtures.json +46 -46
  77. data/node_modules/govuk-frontend/govuk/components/footer/macro-options.json +2 -2
  78. data/node_modules/govuk-frontend/govuk/components/header/fixtures.json +93 -38
  79. data/node_modules/govuk-frontend/govuk/components/header/header.js +6 -0
  80. data/node_modules/govuk-frontend/govuk/components/header/macro-options.json +8 -2
  81. data/node_modules/govuk-frontend/govuk/components/header/template.njk +4 -2
  82. data/node_modules/govuk-frontend/govuk/components/hint/fixtures.json +12 -12
  83. data/node_modules/govuk-frontend/govuk/components/input/fixtures.json +80 -80
  84. data/node_modules/govuk-frontend/govuk/components/inset-text/fixtures.json +12 -12
  85. data/node_modules/govuk-frontend/govuk/components/label/fixtures.json +34 -34
  86. data/node_modules/govuk-frontend/govuk/components/notification-banner/fixtures.json +56 -46
  87. data/node_modules/govuk-frontend/govuk/components/notification-banner/notification-banner.js +252 -2
  88. data/node_modules/govuk-frontend/govuk/components/notification-banner/template.njk +1 -1
  89. data/node_modules/govuk-frontend/govuk/components/pagination/_index.scss +10 -7
  90. data/node_modules/govuk-frontend/govuk/components/pagination/fixtures.json +33 -26
  91. data/node_modules/govuk-frontend/govuk/components/panel/fixtures.json +18 -18
  92. data/node_modules/govuk-frontend/govuk/components/phase-banner/fixtures.json +14 -14
  93. data/node_modules/govuk-frontend/govuk/components/radios/fixtures.json +94 -91
  94. data/node_modules/govuk-frontend/govuk/components/radios/radios.js +30 -2
  95. data/node_modules/govuk-frontend/govuk/components/select/fixtures.json +32 -32
  96. data/node_modules/govuk-frontend/govuk/components/skip-link/fixtures.json +22 -20
  97. data/node_modules/govuk-frontend/govuk/components/skip-link/skip-link.js +10 -4
  98. data/node_modules/govuk-frontend/govuk/components/summary-list/fixtures.json +50 -50
  99. data/node_modules/govuk-frontend/govuk/components/table/_index.scss +1 -1
  100. data/node_modules/govuk-frontend/govuk/components/table/fixtures.json +40 -40
  101. data/node_modules/govuk-frontend/govuk/components/tabs/fixtures.json +29 -29
  102. data/node_modules/govuk-frontend/govuk/components/tabs/tabs.js +28 -0
  103. data/node_modules/govuk-frontend/govuk/components/tag/fixtures.json +28 -28
  104. data/node_modules/govuk-frontend/govuk/components/textarea/fixtures.json +34 -34
  105. data/node_modules/govuk-frontend/govuk/components/warning-text/fixtures.json +14 -14
  106. data/node_modules/govuk-frontend/govuk/core/_section-break.scss +1 -1
  107. data/node_modules/govuk-frontend/govuk/helpers/_colour.scss +2 -2
  108. data/node_modules/govuk-frontend/govuk/helpers/_links.scss +6 -6
  109. data/node_modules/govuk-frontend/govuk/i18n.js +390 -0
  110. data/node_modules/govuk-frontend/govuk/macros/i18n.njk +15 -0
  111. data/node_modules/govuk-frontend/govuk/settings/_all.scss +1 -0
  112. data/node_modules/govuk-frontend/govuk/settings/_colours-palette.scss +12 -0
  113. data/node_modules/govuk-frontend/govuk/settings/_compatibility.scss +26 -0
  114. data/node_modules/govuk-frontend/govuk/settings/_typography-font.scss +23 -0
  115. data/node_modules/govuk-frontend/govuk/settings/_typography-responsive.scss +12 -0
  116. data/node_modules/govuk-frontend/govuk/settings/_warnings.scss +53 -0
  117. data/node_modules/govuk-frontend/govuk/tools/_compatibility.scss +20 -6
  118. data/node_modules/govuk-frontend/govuk/vendor/polyfills/Date/now.js +21 -0
  119. data/node_modules/govuk-frontend/govuk/vendor/polyfills/Element/prototype/dataset.js +300 -0
  120. data/node_modules/govuk-frontend/govuk/vendor/polyfills/String/prototype/trim.js +21 -0
  121. data/node_modules/govuk-frontend/govuk-esm/all.mjs +50 -27
  122. data/node_modules/govuk-frontend/govuk-esm/common/closest-attribute-value.mjs +15 -0
  123. data/node_modules/govuk-frontend/govuk-esm/common/index.mjs +159 -0
  124. data/node_modules/govuk-frontend/govuk-esm/common/normalise-dataset.mjs +58 -0
  125. data/node_modules/govuk-frontend/govuk-esm/common.mjs +6 -28
  126. data/node_modules/govuk-frontend/govuk-esm/components/accordion/accordion.mjs +113 -43
  127. data/node_modules/govuk-frontend/govuk-esm/components/button/button.mjs +67 -30
  128. data/node_modules/govuk-frontend/govuk-esm/components/character-count/character-count.mjs +325 -123
  129. data/node_modules/govuk-frontend/govuk-esm/components/checkboxes/checkboxes.mjs +9 -3
  130. data/node_modules/govuk-frontend/govuk-esm/components/details/details.mjs +22 -8
  131. data/node_modules/govuk-frontend/govuk-esm/components/error-summary/error-summary.mjs +48 -6
  132. data/node_modules/govuk-frontend/govuk-esm/components/header/header.mjs +6 -0
  133. data/node_modules/govuk-frontend/govuk-esm/components/notification-banner/notification-banner.mjs +32 -2
  134. data/node_modules/govuk-frontend/govuk-esm/components/radios/radios.mjs +9 -3
  135. data/node_modules/govuk-frontend/govuk-esm/components/skip-link/skip-link.mjs +10 -4
  136. data/node_modules/govuk-frontend/govuk-esm/components/tabs/tabs.mjs +8 -2
  137. data/node_modules/govuk-frontend/govuk-esm/i18n.mjs +380 -0
  138. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Date/now.mjs +13 -0
  139. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/dataset.mjs +68 -0
  140. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/String/prototype/trim.mjs +13 -0
  141. data/node_modules/govuk-frontend/govuk-prototype-kit/init.js +7 -0
  142. data/node_modules/govuk-frontend/govuk-prototype-kit/init.scss +12 -0
  143. data/node_modules/govuk-frontend/govuk-prototype-kit.config.json +138 -7
  144. data/node_modules/govuk-frontend/package.json +1 -1
  145. 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
- function ErrorSummary ($module) {
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 ($module.getAttribute('data-disable-auto-focus') === 'true') {
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')
@@ -1,7 +1,26 @@
1
1
  import '../../vendor/polyfills/Event.mjs' // addEventListener
2
2
 
3
- function NotificationBanner ($module) {
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 ($module.getAttribute('data-disable-auto-focus') === 'true') {
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 || {});