govuk_publishing_components 32.0.0 → 33.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) 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 +1 -1
  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/layout-super-navigation-header.js +13 -4
  13. data/app/assets/javascripts/govuk_publishing_components/components/single-page-notification-button.js +24 -8
  14. data/app/assets/javascripts/govuk_publishing_components/vendor/lux/lux-reporter.js +83 -86
  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/assets/stylesheets/govuk_publishing_components/components/_share-links.scss +0 -6
  21. data/app/views/govuk_publishing_components/components/_accordion.html.erb +14 -1
  22. data/app/views/govuk_publishing_components/components/_error_summary.html.erb +27 -26
  23. data/app/views/govuk_publishing_components/components/_layout_super_navigation_header.html.erb +2 -2
  24. data/app/views/govuk_publishing_components/components/_phase_banner.html.erb +1 -1
  25. data/app/views/govuk_publishing_components/components/_share_links.html.erb +18 -15
  26. data/app/views/govuk_publishing_components/components/_single_page_notification_button.html.erb +1 -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/feedback/_yes_no_banner.html.erb +3 -3
  32. data/config/locales/ar.yml +4 -1
  33. data/config/locales/az.yml +4 -1
  34. data/config/locales/be.yml +4 -1
  35. data/config/locales/bg.yml +4 -1
  36. data/config/locales/bn.yml +4 -1
  37. data/config/locales/cs.yml +4 -1
  38. data/config/locales/cy.yml +4 -1
  39. data/config/locales/da.yml +4 -1
  40. data/config/locales/de.yml +4 -1
  41. data/config/locales/dr.yml +4 -1
  42. data/config/locales/el.yml +4 -1
  43. data/config/locales/en.yml +20 -17
  44. data/config/locales/es-419.yml +4 -1
  45. data/config/locales/es.yml +4 -1
  46. data/config/locales/et.yml +4 -1
  47. data/config/locales/fa.yml +4 -1
  48. data/config/locales/fi.yml +4 -1
  49. data/config/locales/fr.yml +4 -1
  50. data/config/locales/gd.yml +4 -1
  51. data/config/locales/gu.yml +4 -1
  52. data/config/locales/he.yml +4 -1
  53. data/config/locales/hi.yml +4 -1
  54. data/config/locales/hr.yml +4 -1
  55. data/config/locales/hu.yml +4 -1
  56. data/config/locales/hy.yml +4 -1
  57. data/config/locales/id.yml +4 -1
  58. data/config/locales/is.yml +4 -1
  59. data/config/locales/it.yml +4 -1
  60. data/config/locales/ja.yml +4 -1
  61. data/config/locales/ka.yml +4 -1
  62. data/config/locales/kk.yml +4 -1
  63. data/config/locales/ko.yml +4 -1
  64. data/config/locales/lt.yml +4 -1
  65. data/config/locales/lv.yml +4 -1
  66. data/config/locales/ms.yml +4 -1
  67. data/config/locales/mt.yml +4 -1
  68. data/config/locales/nl.yml +4 -1
  69. data/config/locales/no.yml +4 -1
  70. data/config/locales/pa-pk.yml +4 -1
  71. data/config/locales/pa.yml +4 -1
  72. data/config/locales/pl.yml +4 -1
  73. data/config/locales/ps.yml +4 -1
  74. data/config/locales/pt.yml +4 -1
  75. data/config/locales/ro.yml +4 -1
  76. data/config/locales/ru.yml +4 -1
  77. data/config/locales/si.yml +4 -1
  78. data/config/locales/sk.yml +4 -1
  79. data/config/locales/sl.yml +4 -1
  80. data/config/locales/so.yml +4 -1
  81. data/config/locales/sq.yml +4 -1
  82. data/config/locales/sr.yml +4 -1
  83. data/config/locales/sv.yml +4 -1
  84. data/config/locales/sw.yml +4 -1
  85. data/config/locales/ta.yml +4 -1
  86. data/config/locales/th.yml +4 -1
  87. data/config/locales/tk.yml +4 -1
  88. data/config/locales/tr.yml +4 -1
  89. data/config/locales/uk.yml +4 -1
  90. data/config/locales/ur.yml +4 -1
  91. data/config/locales/uz.yml +4 -1
  92. data/config/locales/vi.yml +4 -1
  93. data/config/locales/zh-hk.yml +4 -1
  94. data/config/locales/zh-tw.yml +4 -1
  95. data/config/locales/zh.yml +4 -1
  96. data/lib/govuk_publishing_components/presenters/button_helper.rb +7 -1
  97. data/lib/govuk_publishing_components/presenters/single_page_notification_button_helper.rb +25 -1
  98. data/lib/govuk_publishing_components/version.rb +1 -1
  99. data/node_modules/axe-core/axe.js +4567 -4678
  100. data/node_modules/axe-core/axe.min.js +2 -2
  101. data/node_modules/axe-core/package.json +2 -2
  102. data/node_modules/axe-core/sri-history.json +8 -0
  103. data/node_modules/govuk-frontend/README.md +1 -2
  104. data/node_modules/govuk-frontend/govuk/all.js +1398 -273
  105. data/node_modules/govuk-frontend/govuk/common/closest-attribute-value.js +70 -0
  106. data/node_modules/govuk-frontend/govuk/common/index.js +172 -0
  107. data/node_modules/govuk-frontend/govuk/common/normalise-dataset.js +373 -0
  108. data/node_modules/govuk-frontend/govuk/common.js +138 -3
  109. data/node_modules/govuk-frontend/govuk/components/accordion/accordion.js +753 -25
  110. data/node_modules/govuk-frontend/govuk/components/accordion/fixtures.json +54 -22
  111. data/node_modules/govuk-frontend/govuk/components/accordion/macro-options.json +36 -0
  112. data/node_modules/govuk-frontend/govuk/components/accordion/template.njk +7 -1
  113. data/node_modules/govuk-frontend/govuk/components/back-link/fixtures.json +12 -12
  114. data/node_modules/govuk-frontend/govuk/components/breadcrumbs/fixtures.json +22 -22
  115. data/node_modules/govuk-frontend/govuk/components/button/_index.scss +23 -5
  116. data/node_modules/govuk-frontend/govuk/components/button/button.js +365 -107
  117. data/node_modules/govuk-frontend/govuk/components/button/fixtures.json +85 -66
  118. data/node_modules/govuk-frontend/govuk/components/button/template.njk +1 -1
  119. data/node_modules/govuk-frontend/govuk/components/character-count/_index.scss +9 -0
  120. data/node_modules/govuk-frontend/govuk/components/character-count/character-count.js +1033 -121
  121. data/node_modules/govuk-frontend/govuk/components/character-count/fixtures.json +112 -36
  122. data/node_modules/govuk-frontend/govuk/components/character-count/macro-options.json +42 -0
  123. data/node_modules/govuk-frontend/govuk/components/character-count/template.njk +27 -3
  124. data/node_modules/govuk-frontend/govuk/components/checkboxes/checkboxes.js +30 -2
  125. data/node_modules/govuk-frontend/govuk/components/checkboxes/fixtures.json +96 -93
  126. data/node_modules/govuk-frontend/govuk/components/cookie-banner/fixtures.json +46 -46
  127. data/node_modules/govuk-frontend/govuk/components/date-input/fixtures.json +50 -50
  128. data/node_modules/govuk-frontend/govuk/components/details/details.js +43 -13
  129. data/node_modules/govuk-frontend/govuk/components/details/fixtures.json +20 -20
  130. data/node_modules/govuk-frontend/govuk/components/error-message/fixtures.json +20 -20
  131. data/node_modules/govuk-frontend/govuk/components/error-summary/error-summary.js +268 -6
  132. data/node_modules/govuk-frontend/govuk/components/error-summary/fixtures.json +44 -35
  133. data/node_modules/govuk-frontend/govuk/components/error-summary/template.njk +25 -21
  134. data/node_modules/govuk-frontend/govuk/components/fieldset/fixtures.json +51 -39
  135. data/node_modules/govuk-frontend/govuk/components/file-upload/fixtures.json +26 -26
  136. data/node_modules/govuk-frontend/govuk/components/footer/_index.scss +1 -1
  137. data/node_modules/govuk-frontend/govuk/components/footer/fixtures.json +46 -46
  138. data/node_modules/govuk-frontend/govuk/components/footer/macro-options.json +2 -2
  139. data/node_modules/govuk-frontend/govuk/components/header/fixtures.json +93 -38
  140. data/node_modules/govuk-frontend/govuk/components/header/header.js +6 -0
  141. data/node_modules/govuk-frontend/govuk/components/header/macro-options.json +8 -2
  142. data/node_modules/govuk-frontend/govuk/components/header/template.njk +4 -2
  143. data/node_modules/govuk-frontend/govuk/components/hint/fixtures.json +12 -12
  144. data/node_modules/govuk-frontend/govuk/components/input/fixtures.json +80 -80
  145. data/node_modules/govuk-frontend/govuk/components/inset-text/fixtures.json +12 -12
  146. data/node_modules/govuk-frontend/govuk/components/label/fixtures.json +34 -34
  147. data/node_modules/govuk-frontend/govuk/components/notification-banner/fixtures.json +56 -46
  148. data/node_modules/govuk-frontend/govuk/components/notification-banner/notification-banner.js +252 -2
  149. data/node_modules/govuk-frontend/govuk/components/notification-banner/template.njk +1 -1
  150. data/node_modules/govuk-frontend/govuk/components/pagination/_index.scss +10 -7
  151. data/node_modules/govuk-frontend/govuk/components/pagination/fixtures.json +33 -26
  152. data/node_modules/govuk-frontend/govuk/components/panel/fixtures.json +18 -18
  153. data/node_modules/govuk-frontend/govuk/components/phase-banner/fixtures.json +14 -14
  154. data/node_modules/govuk-frontend/govuk/components/radios/fixtures.json +94 -91
  155. data/node_modules/govuk-frontend/govuk/components/radios/radios.js +30 -2
  156. data/node_modules/govuk-frontend/govuk/components/select/fixtures.json +32 -32
  157. data/node_modules/govuk-frontend/govuk/components/skip-link/fixtures.json +22 -20
  158. data/node_modules/govuk-frontend/govuk/components/skip-link/skip-link.js +10 -4
  159. data/node_modules/govuk-frontend/govuk/components/summary-list/fixtures.json +50 -50
  160. data/node_modules/govuk-frontend/govuk/components/table/_index.scss +1 -1
  161. data/node_modules/govuk-frontend/govuk/components/table/fixtures.json +40 -40
  162. data/node_modules/govuk-frontend/govuk/components/tabs/fixtures.json +29 -29
  163. data/node_modules/govuk-frontend/govuk/components/tabs/tabs.js +28 -0
  164. data/node_modules/govuk-frontend/govuk/components/tag/fixtures.json +28 -28
  165. data/node_modules/govuk-frontend/govuk/components/textarea/fixtures.json +34 -34
  166. data/node_modules/govuk-frontend/govuk/components/warning-text/fixtures.json +14 -14
  167. data/node_modules/govuk-frontend/govuk/core/_section-break.scss +1 -1
  168. data/node_modules/govuk-frontend/govuk/helpers/_colour.scss +2 -2
  169. data/node_modules/govuk-frontend/govuk/helpers/_links.scss +6 -6
  170. data/node_modules/govuk-frontend/govuk/i18n.js +390 -0
  171. data/node_modules/govuk-frontend/govuk/macros/i18n.njk +15 -0
  172. data/node_modules/govuk-frontend/govuk/settings/_all.scss +1 -0
  173. data/node_modules/govuk-frontend/govuk/settings/_colours-palette.scss +12 -0
  174. data/node_modules/govuk-frontend/govuk/settings/_compatibility.scss +26 -0
  175. data/node_modules/govuk-frontend/govuk/settings/_typography-font.scss +23 -0
  176. data/node_modules/govuk-frontend/govuk/settings/_typography-responsive.scss +12 -0
  177. data/node_modules/govuk-frontend/govuk/settings/_warnings.scss +53 -0
  178. data/node_modules/govuk-frontend/govuk/tools/_compatibility.scss +20 -6
  179. data/node_modules/govuk-frontend/govuk/vendor/polyfills/Date/now.js +21 -0
  180. data/node_modules/govuk-frontend/govuk/vendor/polyfills/Element/prototype/dataset.js +300 -0
  181. data/node_modules/govuk-frontend/govuk/vendor/polyfills/String/prototype/trim.js +21 -0
  182. data/node_modules/govuk-frontend/govuk-esm/all.mjs +50 -27
  183. data/node_modules/govuk-frontend/govuk-esm/common/closest-attribute-value.mjs +15 -0
  184. data/node_modules/govuk-frontend/govuk-esm/common/index.mjs +159 -0
  185. data/node_modules/govuk-frontend/govuk-esm/common/normalise-dataset.mjs +58 -0
  186. data/node_modules/govuk-frontend/govuk-esm/common.mjs +6 -28
  187. data/node_modules/govuk-frontend/govuk-esm/components/accordion/accordion.mjs +113 -43
  188. data/node_modules/govuk-frontend/govuk-esm/components/button/button.mjs +67 -30
  189. data/node_modules/govuk-frontend/govuk-esm/components/character-count/character-count.mjs +325 -123
  190. data/node_modules/govuk-frontend/govuk-esm/components/checkboxes/checkboxes.mjs +9 -3
  191. data/node_modules/govuk-frontend/govuk-esm/components/details/details.mjs +22 -8
  192. data/node_modules/govuk-frontend/govuk-esm/components/error-summary/error-summary.mjs +48 -6
  193. data/node_modules/govuk-frontend/govuk-esm/components/header/header.mjs +6 -0
  194. data/node_modules/govuk-frontend/govuk-esm/components/notification-banner/notification-banner.mjs +32 -2
  195. data/node_modules/govuk-frontend/govuk-esm/components/radios/radios.mjs +9 -3
  196. data/node_modules/govuk-frontend/govuk-esm/components/skip-link/skip-link.mjs +10 -4
  197. data/node_modules/govuk-frontend/govuk-esm/components/tabs/tabs.mjs +8 -2
  198. data/node_modules/govuk-frontend/govuk-esm/i18n.mjs +380 -0
  199. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Date/now.mjs +13 -0
  200. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/dataset.mjs +68 -0
  201. data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/String/prototype/trim.mjs +13 -0
  202. data/node_modules/govuk-frontend/govuk-prototype-kit/init.js +7 -0
  203. data/node_modules/govuk-frontend/govuk-prototype-kit/init.scss +12 -0
  204. data/node_modules/govuk-frontend/govuk-prototype-kit.config.json +138 -7
  205. data/node_modules/govuk-frontend/package.json +1 -1
  206. metadata +22 -3
@@ -4,6 +4,20 @@
4
4
  (global.GOVUKFrontend = global.GOVUKFrontend || {}, global.GOVUKFrontend.CharacterCount = factory());
5
5
  }(this, (function () { 'use strict';
6
6
 
7
+ (function(undefined) {
8
+
9
+ // Detection from https://github.com/Financial-Times/polyfill-library/blob/v3.111.0/polyfills/Date/now/detect.js
10
+ var detect = ('Date' in self && 'now' in self.Date && 'getTime' in self.Date.prototype);
11
+
12
+ if (detect) return
13
+
14
+ // Polyfill from https://polyfill.io/v3/polyfill.js?version=3.111.0&features=Date.now&flags=always
15
+ Date.now = function () {
16
+ return new Date().getTime();
17
+ };
18
+
19
+ }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
20
+
7
21
  (function(undefined) {
8
22
 
9
23
  // Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Object/defineProperty/detect.js
@@ -1014,7 +1028,801 @@ if (detect) return
1014
1028
 
1015
1029
  }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
1016
1030
 
1017
- function CharacterCount ($module) {
1031
+ /**
1032
+ * Common helpers which do not require polyfill.
1033
+ *
1034
+ * IMPORTANT: If a helper require a polyfill, please isolate it in its own module
1035
+ * so that the polyfill can be properly tree-shaken and does not burden
1036
+ * the components that do not need that helper
1037
+ *
1038
+ * @module common/index
1039
+ */
1040
+
1041
+ /**
1042
+ * Config flattening function
1043
+ *
1044
+ * Takes any number of objects, flattens them into namespaced key-value pairs,
1045
+ * (e.g. {'i18n.showSection': 'Show section'}) and combines them together, with
1046
+ * greatest priority on the LAST item passed in.
1047
+ *
1048
+ * @returns {object} A flattened object of key-value pairs.
1049
+ */
1050
+ function mergeConfigs (/* configObject1, configObject2, ...configObjects */) {
1051
+ /**
1052
+ * Function to take nested objects and flatten them to a dot-separated keyed
1053
+ * object. Doing this means we don't need to do any deep/recursive merging of
1054
+ * each of our objects, nor transform our dataset from a flat list into a
1055
+ * nested object.
1056
+ *
1057
+ * @param {object} configObject - Deeply nested object
1058
+ * @returns {object} Flattened object with dot-separated keys
1059
+ */
1060
+ var flattenObject = function (configObject) {
1061
+ // Prepare an empty return object
1062
+ var flattenedObject = {};
1063
+
1064
+ // Our flattening function, this is called recursively for each level of
1065
+ // depth in the object. At each level we prepend the previous level names to
1066
+ // the key using `prefix`.
1067
+ var flattenLoop = function (obj, prefix) {
1068
+ // Loop through keys...
1069
+ for (var key in obj) {
1070
+ // Check to see if this is a prototypical key/value,
1071
+ // if it is, skip it.
1072
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) {
1073
+ continue
1074
+ }
1075
+ var value = obj[key];
1076
+ var prefixedKey = prefix ? prefix + '.' + key : key;
1077
+ if (typeof value === 'object') {
1078
+ // If the value is a nested object, recurse over that too
1079
+ flattenLoop(value, prefixedKey);
1080
+ } else {
1081
+ // Otherwise, add this value to our return object
1082
+ flattenedObject[prefixedKey] = value;
1083
+ }
1084
+ }
1085
+ };
1086
+
1087
+ // Kick off the recursive loop
1088
+ flattenLoop(configObject);
1089
+ return flattenedObject
1090
+ };
1091
+
1092
+ // Start with an empty object as our base
1093
+ var formattedConfigObject = {};
1094
+
1095
+ // Loop through each of the remaining passed objects and push their keys
1096
+ // one-by-one into configObject. Any duplicate keys will override the existing
1097
+ // key with the new value.
1098
+ for (var i = 0; i < arguments.length; i++) {
1099
+ var obj = flattenObject(arguments[i]);
1100
+ for (var key in obj) {
1101
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
1102
+ formattedConfigObject[key] = obj[key];
1103
+ }
1104
+ }
1105
+ }
1106
+
1107
+ return formattedConfigObject
1108
+ }
1109
+
1110
+ /**
1111
+ * Extracts keys starting with a particular namespace from a flattened config
1112
+ * object, removing the namespace in the process.
1113
+ *
1114
+ * @param {object} configObject - The object to extract key-value pairs from.
1115
+ * @param {string} namespace - The namespace to filter keys with.
1116
+ * @returns {object} Flattened object with dot-separated key namespace removed
1117
+ */
1118
+ function extractConfigByNamespace (configObject, namespace) {
1119
+ // Check we have what we need
1120
+ if (!configObject || typeof configObject !== 'object') {
1121
+ throw new Error('Provide a `configObject` of type "object".')
1122
+ }
1123
+ if (!namespace || typeof namespace !== 'string') {
1124
+ throw new Error('Provide a `namespace` of type "string" to filter the `configObject` by.')
1125
+ }
1126
+ var newObject = {};
1127
+ for (var key in configObject) {
1128
+ // Split the key into parts, using . as our namespace separator
1129
+ var keyParts = key.split('.');
1130
+ // Check if the first namespace matches the configured namespace
1131
+ if (Object.prototype.hasOwnProperty.call(configObject, key) && keyParts[0] === namespace) {
1132
+ // Remove the first item (the namespace) from the parts array,
1133
+ // but only if there is more than one part (we don't want blank keys!)
1134
+ if (keyParts.length > 1) {
1135
+ keyParts.shift();
1136
+ }
1137
+ // Join the remaining parts back together
1138
+ var newKey = keyParts.join('.');
1139
+ // Add them to our new object
1140
+ newObject[newKey] = configObject[key];
1141
+ }
1142
+ }
1143
+ return newObject
1144
+ }
1145
+
1146
+ /**
1147
+ * @callback nodeListIterator
1148
+ * @param {Element} value - The current node being iterated on
1149
+ * @param {number} index - The current index in the iteration
1150
+ * @param {NodeListOf<Element>} nodes - NodeList from querySelectorAll()
1151
+ * @returns {undefined}
1152
+ */
1153
+
1154
+ /**
1155
+ * Internal support for selecting messages to render, with placeholder
1156
+ * interpolation and locale-aware number formatting and pluralisation
1157
+ *
1158
+ * @class
1159
+ * @private
1160
+ * @param {TranslationsFlattened} translations - Key-value pairs of the translation strings to use.
1161
+ * @param {object} [config] - Configuration options for the function.
1162
+ * @param {string} config.locale - An overriding locale for the PluralRules functionality.
1163
+ */
1164
+ function I18n (translations, config) {
1165
+ // Make list of translations available throughout function
1166
+ this.translations = translations || {};
1167
+
1168
+ // The locale to use for PluralRules and NumberFormat
1169
+ this.locale = (config && config.locale) || document.documentElement.lang || 'en';
1170
+ }
1171
+
1172
+ /**
1173
+ * The most used function - takes the key for a given piece of UI text and
1174
+ * returns the appropriate string.
1175
+ *
1176
+ * @param {string} lookupKey - The lookup key of the string to use.
1177
+ * @param {object} options - Any options passed with the translation string, e.g: for string interpolation.
1178
+ * @returns {string} The appropriate translation string.
1179
+ */
1180
+ I18n.prototype.t = function (lookupKey, options) {
1181
+ if (!lookupKey) {
1182
+ // Print a console error if no lookup key has been provided
1183
+ throw new Error('i18n: lookup key missing')
1184
+ }
1185
+
1186
+ // If the `count` option is set, determine which plural suffix is needed and
1187
+ // change the lookupKey to match. We check to see if it's undefined instead of
1188
+ // falsy, as this could legitimately be 0.
1189
+ if (options && typeof options.count !== 'undefined') {
1190
+ // Get the plural suffix
1191
+ lookupKey = lookupKey + '.' + this.getPluralSuffix(lookupKey, options.count);
1192
+ }
1193
+
1194
+ if (lookupKey in this.translations) {
1195
+ // Fetch the translation string for that lookup key
1196
+ var translationString = this.translations[lookupKey];
1197
+
1198
+ // Check for ${} placeholders in the translation string
1199
+ if (translationString.match(/%{(.\S+)}/)) {
1200
+ if (!options) {
1201
+ throw new Error('i18n: cannot replace placeholders in string if no option data provided')
1202
+ }
1203
+
1204
+ return this.replacePlaceholders(translationString, options)
1205
+ } else {
1206
+ return translationString
1207
+ }
1208
+ } else {
1209
+ // If the key wasn't found in our translations object,
1210
+ // return the lookup key itself as the fallback
1211
+ return lookupKey
1212
+ }
1213
+ };
1214
+
1215
+ /**
1216
+ * Takes a translation string with placeholders, and replaces the placeholders
1217
+ * with the provided data
1218
+ *
1219
+ * @param {string} translationString - The translation string
1220
+ * @param {object} options - Any options passed with the translation string, e.g: for string interpolation.
1221
+ * @returns {string} The translation string to output, with ${} placeholders replaced
1222
+ */
1223
+ I18n.prototype.replacePlaceholders = function (translationString, options) {
1224
+ var formatter;
1225
+
1226
+ if (this.hasIntlNumberFormatSupport()) {
1227
+ formatter = new Intl.NumberFormat(this.locale);
1228
+ }
1229
+
1230
+ return translationString.replace(/%{(.\S+)}/g, function (placeholderWithBraces, placeholderKey) {
1231
+ if (Object.prototype.hasOwnProperty.call(options, placeholderKey)) {
1232
+ var placeholderValue = options[placeholderKey];
1233
+
1234
+ // If a user has passed `false` as the value for the placeholder
1235
+ // treat it as though the value should not be displayed
1236
+ if (placeholderValue === false) {
1237
+ return ''
1238
+ }
1239
+
1240
+ // If the placeholder's value is a number, localise the number formatting
1241
+ if (typeof placeholderValue === 'number' && formatter) {
1242
+ return formatter.format(placeholderValue)
1243
+ }
1244
+
1245
+ return placeholderValue
1246
+ } else {
1247
+ throw new Error('i18n: no data found to replace ' + placeholderWithBraces + ' placeholder in string')
1248
+ }
1249
+ })
1250
+ };
1251
+
1252
+ /**
1253
+ * Check to see if the browser supports Intl and Intl.PluralRules.
1254
+ *
1255
+ * It requires all conditions to be met in order to be supported:
1256
+ * - The browser supports the Intl class (true in IE11)
1257
+ * - The implementation of Intl supports PluralRules (NOT true in IE11)
1258
+ * - The browser/OS has plural rules for the current locale (browser dependent)
1259
+ *
1260
+ * @returns {boolean} Returns true if all conditions are met. Returns false otherwise.
1261
+ */
1262
+ I18n.prototype.hasIntlPluralRulesSupport = function () {
1263
+ return Boolean(window.Intl && ('PluralRules' in window.Intl && Intl.PluralRules.supportedLocalesOf(this.locale).length))
1264
+ };
1265
+
1266
+ /**
1267
+ * Check to see if the browser supports Intl and Intl.NumberFormat.
1268
+ *
1269
+ * It requires all conditions to be met in order to be supported:
1270
+ * - The browser supports the Intl class (true in IE11)
1271
+ * - The implementation of Intl supports NumberFormat (also true in IE11)
1272
+ * - The browser/OS has number formatting rules for the current locale (browser dependent)
1273
+ *
1274
+ * @returns {boolean} Returns true if all conditions are met. Returns false otherwise.
1275
+ */
1276
+ I18n.prototype.hasIntlNumberFormatSupport = function () {
1277
+ return Boolean(window.Intl && ('NumberFormat' in window.Intl && Intl.NumberFormat.supportedLocalesOf(this.locale).length))
1278
+ };
1279
+
1280
+ /**
1281
+ * Get the appropriate suffix for the plural form.
1282
+ *
1283
+ * Uses Intl.PluralRules (or our own fallback implementation) to get the
1284
+ * 'preferred' form to use for the given count.
1285
+ *
1286
+ * Checks that a translation has been provided for that plural form – if it
1287
+ * hasn't, it'll fall back to the 'other' plural form (unless that doesn't exist
1288
+ * either, in which case an error will be thrown)
1289
+ *
1290
+ * @param {string} lookupKey - The lookup key of the string to use.
1291
+ * @param {number} count - Number used to determine which pluralisation to use.
1292
+ * @returns {PluralRule} The suffix associated with the correct pluralisation for this locale.
1293
+ */
1294
+ I18n.prototype.getPluralSuffix = function (lookupKey, count) {
1295
+ // Validate that the number is actually a number.
1296
+ //
1297
+ // Number(count) will turn anything that can't be converted to a Number type
1298
+ // into 'NaN'. isFinite filters out NaN, as it isn't a finite number.
1299
+ count = Number(count);
1300
+ if (!isFinite(count)) { return 'other' }
1301
+
1302
+ var preferredForm;
1303
+
1304
+ // Check to verify that all the requirements for Intl.PluralRules are met.
1305
+ // If so, we can use that instead of our custom implementation. Otherwise,
1306
+ // use the hardcoded fallback.
1307
+ if (this.hasIntlPluralRulesSupport()) {
1308
+ preferredForm = new Intl.PluralRules(this.locale).select(count);
1309
+ } else {
1310
+ preferredForm = this.selectPluralFormUsingFallbackRules(count);
1311
+ }
1312
+
1313
+ // Use the correct plural form if provided
1314
+ if (lookupKey + '.' + preferredForm in this.translations) {
1315
+ return preferredForm
1316
+ // Fall back to `other` if the plural form is missing, but log a warning
1317
+ // to the console
1318
+ } else if (lookupKey + '.other' in this.translations) {
1319
+ if (console && 'warn' in console) {
1320
+ console.warn('i18n: Missing plural form ".' + preferredForm + '" for "' +
1321
+ this.locale + '" locale. Falling back to ".other".');
1322
+ }
1323
+
1324
+ return 'other'
1325
+ // If the required `other` plural form is missing, all we can do is error
1326
+ } else {
1327
+ throw new Error(
1328
+ 'i18n: Plural form ".other" is required for "' + this.locale + '" locale'
1329
+ )
1330
+ }
1331
+ };
1332
+
1333
+ /**
1334
+ * Get the plural form using our fallback implementation
1335
+ *
1336
+ * This is split out into a separate function to make it easier to test the
1337
+ * fallback behaviour in an environment where Intl.PluralRules exists.
1338
+ *
1339
+ * @param {number} count - Number used to determine which pluralisation to use.
1340
+ * @returns {PluralRule} The pluralisation form for count in this locale.
1341
+ */
1342
+ I18n.prototype.selectPluralFormUsingFallbackRules = function (count) {
1343
+ // Currently our custom code can only handle positive integers, so let's
1344
+ // make sure our number is one of those.
1345
+ count = Math.abs(Math.floor(count));
1346
+
1347
+ var ruleset = this.getPluralRulesForLocale();
1348
+
1349
+ if (ruleset) {
1350
+ return I18n.pluralRules[ruleset](count)
1351
+ }
1352
+
1353
+ return 'other'
1354
+ };
1355
+
1356
+ /**
1357
+ * Work out which pluralisation rules to use for the current locale
1358
+ *
1359
+ * The locale may include a regional indicator (such as en-GB), but we don't
1360
+ * usually care about this part, as pluralisation rules are usually the same
1361
+ * regardless of region. There are exceptions, however, (e.g. Portuguese) so
1362
+ * this searches by both the full and shortened locale codes, just to be sure.
1363
+ *
1364
+ * @returns {PluralRuleName | undefined} The name of the pluralisation rule to use (a key for one
1365
+ * of the functions in this.pluralRules)
1366
+ */
1367
+ I18n.prototype.getPluralRulesForLocale = function () {
1368
+ var locale = this.locale;
1369
+ var localeShort = locale.split('-')[0];
1370
+
1371
+ // Look through the plural rules map to find which `pluralRule` is
1372
+ // appropriate for our current `locale`.
1373
+ for (var pluralRule in I18n.pluralRulesMap) {
1374
+ if (Object.prototype.hasOwnProperty.call(I18n.pluralRulesMap, pluralRule)) {
1375
+ var languages = I18n.pluralRulesMap[pluralRule];
1376
+ for (var i = 0; i < languages.length; i++) {
1377
+ if (languages[i] === locale || languages[i] === localeShort) {
1378
+ return pluralRule
1379
+ }
1380
+ }
1381
+ }
1382
+ }
1383
+ };
1384
+
1385
+ /**
1386
+ * Map of plural rules to languages where those rules apply.
1387
+ *
1388
+ * Note: These groups are named for the most dominant or recognisable language
1389
+ * that uses each system. The groupings do not imply that the languages are
1390
+ * related to one another. Many languages have evolved the same systems
1391
+ * independently of one another.
1392
+ *
1393
+ * Code to support more languages can be found in the i18n spike:
1394
+ * {@link https://github.com/alphagov/govuk-frontend/blob/spike-i18n-support/src/govuk/i18n.mjs}
1395
+ *
1396
+ * Languages currently supported:
1397
+ *
1398
+ * Arabic: Arabic (ar)
1399
+ * Chinese: Burmese (my), Chinese (zh), Indonesian (id), Japanese (ja),
1400
+ * Javanese (jv), Korean (ko), Malay (ms), Thai (th), Vietnamese (vi)
1401
+ * French: Armenian (hy), Bangla (bn), French (fr), Gujarati (gu), Hindi (hi),
1402
+ * Persian Farsi (fa), Punjabi (pa), Zulu (zu)
1403
+ * German: Afrikaans (af), Albanian (sq), Azerbaijani (az), Basque (eu),
1404
+ * Bulgarian (bg), Catalan (ca), Danish (da), Dutch (nl), English (en),
1405
+ * Estonian (et), Finnish (fi), Georgian (ka), German (de), Greek (el),
1406
+ * Hungarian (hu), Luxembourgish (lb), Norwegian (no), Somali (so),
1407
+ * Swahili (sw), Swedish (sv), Tamil (ta), Telugu (te), Turkish (tr),
1408
+ * Urdu (ur)
1409
+ * Irish: Irish Gaelic (ga)
1410
+ * Russian: Russian (ru), Ukrainian (uk)
1411
+ * Scottish: Scottish Gaelic (gd)
1412
+ * Spanish: European Portuguese (pt-PT), Italian (it), Spanish (es)
1413
+ * Welsh: Welsh (cy)
1414
+ *
1415
+ * @type {Object<PluralRuleName, string[]>}
1416
+ */
1417
+ I18n.pluralRulesMap = {
1418
+ arabic: ['ar'],
1419
+ chinese: ['my', 'zh', 'id', 'ja', 'jv', 'ko', 'ms', 'th', 'vi'],
1420
+ french: ['hy', 'bn', 'fr', 'gu', 'hi', 'fa', 'pa', 'zu'],
1421
+ german: [
1422
+ 'af', 'sq', 'az', 'eu', 'bg', 'ca', 'da', 'nl', 'en', 'et', 'fi', 'ka',
1423
+ 'de', 'el', 'hu', 'lb', 'no', 'so', 'sw', 'sv', 'ta', 'te', 'tr', 'ur'
1424
+ ],
1425
+ irish: ['ga'],
1426
+ russian: ['ru', 'uk'],
1427
+ scottish: ['gd'],
1428
+ spanish: ['pt-PT', 'it', 'es'],
1429
+ welsh: ['cy']
1430
+ };
1431
+
1432
+ /**
1433
+ * Different pluralisation rule sets
1434
+ *
1435
+ * Returns the appropriate suffix for the plural form associated with `n`.
1436
+ * Possible suffixes: 'zero', 'one', 'two', 'few', 'many', 'other' (the actual
1437
+ * meaning of each differs per locale). 'other' should always exist, even in
1438
+ * languages without plurals, such as Chinese.
1439
+ * {@link https://cldr.unicode.org/index/cldr-spec/plural-rules}
1440
+ *
1441
+ * The count must be a positive integer. Negative numbers and decimals aren't accounted for
1442
+ *
1443
+ * @type {Object<string, function(number): PluralRule>}
1444
+ */
1445
+ I18n.pluralRules = {
1446
+ arabic: function (n) {
1447
+ if (n === 0) { return 'zero' }
1448
+ if (n === 1) { return 'one' }
1449
+ if (n === 2) { return 'two' }
1450
+ if (n % 100 >= 3 && n % 100 <= 10) { return 'few' }
1451
+ if (n % 100 >= 11 && n % 100 <= 99) { return 'many' }
1452
+ return 'other'
1453
+ },
1454
+ chinese: function () {
1455
+ return 'other'
1456
+ },
1457
+ french: function (n) {
1458
+ return n === 0 || n === 1 ? 'one' : 'other'
1459
+ },
1460
+ german: function (n) {
1461
+ return n === 1 ? 'one' : 'other'
1462
+ },
1463
+ irish: function (n) {
1464
+ if (n === 1) { return 'one' }
1465
+ if (n === 2) { return 'two' }
1466
+ if (n >= 3 && n <= 6) { return 'few' }
1467
+ if (n >= 7 && n <= 10) { return 'many' }
1468
+ return 'other'
1469
+ },
1470
+ russian: function (n) {
1471
+ var lastTwo = n % 100;
1472
+ var last = lastTwo % 10;
1473
+ if (last === 1 && lastTwo !== 11) { return 'one' }
1474
+ if (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)) { return 'few' }
1475
+ if (last === 0 || (last >= 5 && last <= 9) || (lastTwo >= 11 && lastTwo <= 14)) { return 'many' }
1476
+ // Note: The 'other' suffix is only used by decimal numbers in Russian.
1477
+ // We don't anticipate it being used, but it's here for consistency.
1478
+ return 'other'
1479
+ },
1480
+ scottish: function (n) {
1481
+ if (n === 1 || n === 11) { return 'one' }
1482
+ if (n === 2 || n === 12) { return 'two' }
1483
+ if ((n >= 3 && n <= 10) || (n >= 13 && n <= 19)) { return 'few' }
1484
+ return 'other'
1485
+ },
1486
+ spanish: function (n) {
1487
+ if (n === 1) { return 'one' }
1488
+ if (n % 1000000 === 0 && n !== 0) { return 'many' }
1489
+ return 'other'
1490
+ },
1491
+ welsh: function (n) {
1492
+ if (n === 0) { return 'zero' }
1493
+ if (n === 1) { return 'one' }
1494
+ if (n === 2) { return 'two' }
1495
+ if (n === 3) { return 'few' }
1496
+ if (n === 6) { return 'many' }
1497
+ return 'other'
1498
+ }
1499
+ };
1500
+
1501
+ /**
1502
+ * Supported languages for plural rules
1503
+ *
1504
+ * @typedef {'arabic' | 'chinese' | 'french' | 'german' | 'irish' | 'russian' | 'scottish' | 'spanish' | 'welsh'} PluralRuleName
1505
+ */
1506
+
1507
+ /**
1508
+ * Plural rule category mnemonic tags
1509
+ *
1510
+ * @typedef {'zero' | 'one' | 'two' | 'few' | 'many' | 'other'} PluralRule
1511
+ */
1512
+
1513
+ /**
1514
+ * Translated message by plural rule they correspond to.
1515
+ *
1516
+ * Allows to group pluralised messages under a single key when passing
1517
+ * translations to a component's constructor
1518
+ *
1519
+ * @typedef {object} TranslationPluralForms
1520
+ * @property {string} [other] - General plural form
1521
+ * @property {string} [zero] - Plural form used with 0
1522
+ * @property {string} [one] - Plural form used with 1
1523
+ * @property {string} [two] - Plural form used with 2
1524
+ * @property {string} [few] - Plural form used for a few
1525
+ * @property {string} [many] - Plural form used for many
1526
+ */
1527
+
1528
+ /**
1529
+ * Translated messages (flattened)
1530
+ *
1531
+ * @private
1532
+ * @typedef {Object<string, string> | {}} TranslationsFlattened
1533
+ */
1534
+
1535
+ (function(undefined) {
1536
+
1537
+ // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-library/13cf7c340974d128d557580b5e2dafcd1b1192d1/polyfills/Element/prototype/dataset/detect.js
1538
+ var detect = (function(){
1539
+ if (!document.documentElement.dataset) {
1540
+ return false;
1541
+ }
1542
+ var el = document.createElement('div');
1543
+ el.setAttribute("data-a-b", "c");
1544
+ return el.dataset && el.dataset.aB == "c";
1545
+ }());
1546
+
1547
+ if (detect) return
1548
+
1549
+ // Polyfill derived from https://raw.githubusercontent.com/Financial-Times/polyfill-library/13cf7c340974d128d557580b5e2dafcd1b1192d1/polyfills/Element/prototype/dataset/polyfill.js
1550
+ Object.defineProperty(Element.prototype, 'dataset', {
1551
+ get: function() {
1552
+ var element = this;
1553
+ var attributes = this.attributes;
1554
+ var map = {};
1555
+
1556
+ for (var i = 0; i < attributes.length; i++) {
1557
+ var attribute = attributes[i];
1558
+
1559
+ // This regex has been edited from the original polyfill, to add
1560
+ // support for period (.) separators in data-* attribute names. These
1561
+ // are allowed in the HTML spec, but were not covered by the original
1562
+ // polyfill's regex. We use periods in our i18n implementation.
1563
+ if (attribute && attribute.name && (/^data-\w[.\w-]*$/).test(attribute.name)) {
1564
+ var name = attribute.name;
1565
+ var value = attribute.value;
1566
+
1567
+ var propName = name.substr(5).replace(/-./g, function (prop) {
1568
+ return prop.charAt(1).toUpperCase();
1569
+ });
1570
+
1571
+ // If this browser supports __defineGetter__ and __defineSetter__,
1572
+ // continue using defineProperty. If not (like IE 8 and below), we use
1573
+ // a hacky fallback which at least gives an object in the right format
1574
+ if ('__defineGetter__' in Object.prototype && '__defineSetter__' in Object.prototype) {
1575
+ Object.defineProperty(map, propName, {
1576
+ enumerable: true,
1577
+ get: function() {
1578
+ return this.value;
1579
+ }.bind({value: value || ''}),
1580
+ set: function setter(name, value) {
1581
+ if (typeof value !== 'undefined') {
1582
+ this.setAttribute(name, value);
1583
+ } else {
1584
+ this.removeAttribute(name);
1585
+ }
1586
+ }.bind(element, name)
1587
+ });
1588
+ } else {
1589
+ map[propName] = value;
1590
+ }
1591
+
1592
+ }
1593
+ }
1594
+
1595
+ return map;
1596
+ }
1597
+ });
1598
+
1599
+ }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
1600
+
1601
+ (function(undefined) {
1602
+
1603
+ // Detection from https://github.com/mdn/content/blob/cf607d68522cd35ee7670782d3ee3a361eaef2e4/files/en-us/web/javascript/reference/global_objects/string/trim/index.md#polyfill
1604
+ var detect = ('trim' in String.prototype);
1605
+
1606
+ if (detect) return
1607
+
1608
+ // Polyfill from https://github.com/mdn/content/blob/cf607d68522cd35ee7670782d3ee3a361eaef2e4/files/en-us/web/javascript/reference/global_objects/string/trim/index.md#polyfill
1609
+ String.prototype.trim = function () {
1610
+ return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
1611
+ };
1612
+
1613
+ }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
1614
+
1615
+ /**
1616
+ * Normalise string
1617
+ *
1618
+ * 'If it looks like a duck, and it quacks like a duck…' 🦆
1619
+ *
1620
+ * If the passed value looks like a boolean or a number, convert it to a boolean
1621
+ * or number.
1622
+ *
1623
+ * Designed to be used to convert config passed via data attributes (which are
1624
+ * always strings) into something sensible.
1625
+ *
1626
+ * @param {string} value - The value to normalise
1627
+ * @returns {string | boolean | number | undefined} Normalised data
1628
+ */
1629
+ function normaliseString (value) {
1630
+ if (typeof value !== 'string') {
1631
+ return value
1632
+ }
1633
+
1634
+ var trimmedValue = value.trim();
1635
+
1636
+ if (trimmedValue === 'true') {
1637
+ return true
1638
+ }
1639
+
1640
+ if (trimmedValue === 'false') {
1641
+ return false
1642
+ }
1643
+
1644
+ // Empty / whitespace-only strings are considered finite so we need to check
1645
+ // the length of the trimmed string as well
1646
+ if (trimmedValue.length > 0 && isFinite(trimmedValue)) {
1647
+ return Number(trimmedValue)
1648
+ }
1649
+
1650
+ return value
1651
+ }
1652
+
1653
+ /**
1654
+ * Normalise dataset
1655
+ *
1656
+ * Loop over an object and normalise each value using normaliseData function
1657
+ *
1658
+ * @param {DOMStringMap} dataset - HTML element dataset
1659
+ * @returns {Object<string, string | boolean | number | undefined>} Normalised dataset
1660
+ */
1661
+ function normaliseDataset (dataset) {
1662
+ var out = {};
1663
+
1664
+ for (var key in dataset) {
1665
+ out[key] = normaliseString(dataset[key]);
1666
+ }
1667
+
1668
+ return out
1669
+ }
1670
+
1671
+ (function(undefined) {
1672
+
1673
+ // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/matches/detect.js
1674
+ var detect = (
1675
+ 'document' in this && "matches" in document.documentElement
1676
+ );
1677
+
1678
+ if (detect) return
1679
+
1680
+ // Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/matches/polyfill.js
1681
+ Element.prototype.matches = Element.prototype.webkitMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.mozMatchesSelector || function matches(selector) {
1682
+ var element = this;
1683
+ var elements = (element.document || element.ownerDocument).querySelectorAll(selector);
1684
+ var index = 0;
1685
+
1686
+ while (elements[index] && elements[index] !== element) {
1687
+ ++index;
1688
+ }
1689
+
1690
+ return !!elements[index];
1691
+ };
1692
+
1693
+ }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
1694
+
1695
+ (function(undefined) {
1696
+
1697
+ // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/closest/detect.js
1698
+ var detect = (
1699
+ 'document' in this && "closest" in document.documentElement
1700
+ );
1701
+
1702
+ if (detect) return
1703
+
1704
+ // Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/closest/polyfill.js
1705
+ Element.prototype.closest = function closest(selector) {
1706
+ var node = this;
1707
+
1708
+ while (node) {
1709
+ if (node.matches(selector)) return node;
1710
+ else node = 'SVGElement' in window && node instanceof SVGElement ? node.parentNode : node.parentElement;
1711
+ }
1712
+
1713
+ return null;
1714
+ };
1715
+
1716
+ }).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
1717
+
1718
+ /**
1719
+ * Returns the value of the given attribute closest to the given element (including itself)
1720
+ *
1721
+ * @param {HTMLElement} $element - The element to start walking the DOM tree up
1722
+ * @param {string} attributeName - The name of the attribute
1723
+ * @returns {string | undefined} Attribute value
1724
+ */
1725
+ function closestAttributeValue ($element, attributeName) {
1726
+ var closestElementWithAttribute = $element.closest('[' + attributeName + ']');
1727
+ if (closestElementWithAttribute) {
1728
+ return closestElementWithAttribute.getAttribute(attributeName)
1729
+ }
1730
+ }
1731
+
1732
+ /**
1733
+ * @constant
1734
+ * @type {CharacterCountTranslations}
1735
+ * @see Default value for {@link CharacterCountConfig.i18n}
1736
+ * @default
1737
+ */
1738
+ var CHARACTER_COUNT_TRANSLATIONS = {
1739
+ // Characters
1740
+ charactersUnderLimit: {
1741
+ one: 'You have %{count} character remaining',
1742
+ other: 'You have %{count} characters remaining'
1743
+ },
1744
+ charactersAtLimit: 'You have 0 characters remaining',
1745
+ charactersOverLimit: {
1746
+ one: 'You have %{count} character too many',
1747
+ other: 'You have %{count} characters too many'
1748
+ },
1749
+ // Words
1750
+ wordsUnderLimit: {
1751
+ one: 'You have %{count} word remaining',
1752
+ other: 'You have %{count} words remaining'
1753
+ },
1754
+ wordsAtLimit: 'You have 0 words remaining',
1755
+ wordsOverLimit: {
1756
+ one: 'You have %{count} word too many',
1757
+ other: 'You have %{count} words too many'
1758
+ },
1759
+ textareaDescription: {
1760
+ other: ''
1761
+ }
1762
+ };
1763
+
1764
+ /**
1765
+ * JavaScript enhancements for the CharacterCount component
1766
+ *
1767
+ * Tracks the number of characters or words in the `.govuk-js-character-count`
1768
+ * `<textarea>` inside the element. Displays a message with the remaining number
1769
+ * of characters/words available, or the number of characters/words in excess.
1770
+ *
1771
+ * You can configure the message to only appear after a certain percentage
1772
+ * of the available characters/words has been entered.
1773
+ *
1774
+ * @class
1775
+ * @param {HTMLElement} $module - The element this component controls
1776
+ * @param {CharacterCountConfig} [config] - Character count config
1777
+ */
1778
+ function CharacterCount ($module, config) {
1779
+ if (!$module) {
1780
+ return this
1781
+ }
1782
+
1783
+ var defaultConfig = {
1784
+ threshold: 0,
1785
+ i18n: CHARACTER_COUNT_TRANSLATIONS
1786
+ };
1787
+
1788
+ // Read config set using dataset ('data-' values)
1789
+ var datasetConfig = normaliseDataset($module.dataset);
1790
+
1791
+ // To ensure data-attributes take complete precedence, even if they change the
1792
+ // type of count, we need to reset the `maxlength` and `maxwords` from the
1793
+ // JavaScript config.
1794
+ //
1795
+ // We can't mutate `config`, though, as it may be shared across multiple
1796
+ // components inside `initAll`.
1797
+ var configOverrides = {};
1798
+ if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
1799
+ configOverrides = {
1800
+ maxlength: false,
1801
+ maxwords: false
1802
+ };
1803
+ }
1804
+
1805
+ this.config = mergeConfigs(
1806
+ defaultConfig,
1807
+ config || {},
1808
+ configOverrides,
1809
+ datasetConfig
1810
+ );
1811
+
1812
+ this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'), {
1813
+ // Read the fallback if necessary rather than have it set in the defaults
1814
+ locale: closestAttributeValue($module, 'lang')
1815
+ });
1816
+
1817
+ // Determine the limit attribute (characters or words)
1818
+ if (this.config.maxwords) {
1819
+ this.maxLength = this.config.maxwords;
1820
+ } else if (this.config.maxlength) {
1821
+ this.maxLength = this.config.maxlength;
1822
+ } else {
1823
+ return
1824
+ }
1825
+
1018
1826
  this.$module = $module;
1019
1827
  this.$textarea = $module.querySelector('.govuk-js-character-count');
1020
1828
  this.$visibleCountMessage = null;
@@ -1022,26 +1830,28 @@ function CharacterCount ($module) {
1022
1830
  this.lastInputTimestamp = null;
1023
1831
  }
1024
1832
 
1025
- CharacterCount.prototype.defaults = {
1026
- characterCountAttribute: 'data-maxlength',
1027
- wordCountAttribute: 'data-maxwords'
1028
- };
1029
-
1030
- // Initialize component
1833
+ /**
1834
+ * Initialise component
1835
+ */
1031
1836
  CharacterCount.prototype.init = function () {
1032
1837
  // Check that required elements are present
1033
1838
  if (!this.$textarea) {
1034
1839
  return
1035
1840
  }
1036
1841
 
1037
- // Check for module
1038
- var $module = this.$module;
1039
1842
  var $textarea = this.$textarea;
1040
- var $fallbackLimitMessage = document.getElementById($textarea.id + '-info');
1843
+ var $textareaDescription = document.getElementById($textarea.id + '-info');
1041
1844
 
1042
- // Move the fallback count message to be immediately after the textarea
1845
+ // Inject a decription for the textarea if none is present already
1846
+ // for when the component was rendered with no maxlength, maxwords
1847
+ // nor custom textareaDescriptionText
1848
+ if ($textareaDescription.innerText.match(/^\s*$/)) {
1849
+ $textareaDescription.innerText = this.i18n.t('textareaDescription', { count: this.maxLength });
1850
+ }
1851
+
1852
+ // Move the textarea description to be immediately after the textarea
1043
1853
  // Kept for backwards compatibility
1044
- $textarea.insertAdjacentElement('afterend', $fallbackLimitMessage);
1854
+ $textarea.insertAdjacentElement('afterend', $textareaDescription);
1045
1855
 
1046
1856
  // Create the *screen reader* specific live-updating counter
1047
1857
  // This doesn't need any styling classes, as it is never visible
@@ -1049,36 +1859,20 @@ CharacterCount.prototype.init = function () {
1049
1859
  $screenReaderCountMessage.className = 'govuk-character-count__sr-status govuk-visually-hidden';
1050
1860
  $screenReaderCountMessage.setAttribute('aria-live', 'polite');
1051
1861
  this.$screenReaderCountMessage = $screenReaderCountMessage;
1052
- $fallbackLimitMessage.insertAdjacentElement('afterend', $screenReaderCountMessage);
1862
+ $textareaDescription.insertAdjacentElement('afterend', $screenReaderCountMessage);
1053
1863
 
1054
1864
  // Create our live-updating counter element, copying the classes from the
1055
- // fallback element for backwards compatibility as these may have been configured
1865
+ // textarea description for backwards compatibility as these may have been
1866
+ // configured
1056
1867
  var $visibleCountMessage = document.createElement('div');
1057
- $visibleCountMessage.className = $fallbackLimitMessage.className;
1868
+ $visibleCountMessage.className = $textareaDescription.className;
1058
1869
  $visibleCountMessage.classList.add('govuk-character-count__status');
1059
1870
  $visibleCountMessage.setAttribute('aria-hidden', 'true');
1060
1871
  this.$visibleCountMessage = $visibleCountMessage;
1061
- $fallbackLimitMessage.insertAdjacentElement('afterend', $visibleCountMessage);
1062
-
1063
- // Hide the fallback limit message
1064
- $fallbackLimitMessage.classList.add('govuk-visually-hidden');
1065
-
1066
- // Read options set using dataset ('data-' values)
1067
- this.options = this.getDataset($module);
1872
+ $textareaDescription.insertAdjacentElement('afterend', $visibleCountMessage);
1068
1873
 
1069
- // Determine the limit attribute (characters or words)
1070
- var countAttribute = this.defaults.characterCountAttribute;
1071
- if (this.options.maxwords) {
1072
- countAttribute = this.defaults.wordCountAttribute;
1073
- }
1074
-
1075
- // Save the element limit
1076
- this.maxLength = $module.getAttribute(countAttribute);
1077
-
1078
- // Check for limit
1079
- if (!this.maxLength) {
1080
- return
1081
- }
1874
+ // Hide the textarea description
1875
+ $textareaDescription.classList.add('govuk-visually-hidden');
1082
1876
 
1083
1877
  // Remove hard limit if set
1084
1878
  $textarea.removeAttribute('maxlength');
@@ -1086,9 +1880,9 @@ CharacterCount.prototype.init = function () {
1086
1880
  this.bindChangeEvents();
1087
1881
 
1088
1882
  // When the page is restored after navigating 'back' in some browsers the
1089
- // state of the character count is not restored until *after* the DOMContentLoaded
1090
- // event is fired, so we need to manually update it after the pageshow event
1091
- // in browsers that support it.
1883
+ // state of the character count is not restored until *after* the
1884
+ // DOMContentLoaded event is fired, so we need to manually update it after the
1885
+ // pageshow event in browsers that support it.
1092
1886
  if ('onpageshow' in window) {
1093
1887
  window.addEventListener('pageshow', this.updateCountMessage.bind(this));
1094
1888
  } else {
@@ -1097,35 +1891,12 @@ CharacterCount.prototype.init = function () {
1097
1891
  this.updateCountMessage();
1098
1892
  };
1099
1893
 
1100
- // Read data attributes
1101
- CharacterCount.prototype.getDataset = function (element) {
1102
- var dataset = {};
1103
- var attributes = element.attributes;
1104
- if (attributes) {
1105
- for (var i = 0; i < attributes.length; i++) {
1106
- var attribute = attributes[i];
1107
- var match = attribute.name.match(/^data-(.+)/);
1108
- if (match) {
1109
- dataset[match[1]] = attribute.value;
1110
- }
1111
- }
1112
- }
1113
- return dataset
1114
- };
1115
-
1116
- // Counts characters or words in text
1117
- CharacterCount.prototype.count = function (text) {
1118
- var length;
1119
- if (this.options.maxwords) {
1120
- var tokens = text.match(/\S+/g) || []; // Matches consecutive non-whitespace chars
1121
- length = tokens.length;
1122
- } else {
1123
- length = text.length;
1124
- }
1125
- return length
1126
- };
1127
-
1128
- // Bind input propertychange to the elements and update based on the change
1894
+ /**
1895
+ * Bind change events
1896
+ *
1897
+ * Set up event listeners on the $textarea so that the count messages update
1898
+ * when the user types.
1899
+ */
1129
1900
  CharacterCount.prototype.bindChangeEvents = function () {
1130
1901
  var $textarea = this.$textarea;
1131
1902
  $textarea.addEventListener('keyup', this.handleKeyUp.bind(this));
@@ -1135,10 +1906,52 @@ CharacterCount.prototype.bindChangeEvents = function () {
1135
1906
  $textarea.addEventListener('blur', this.handleBlur.bind(this));
1136
1907
  };
1137
1908
 
1138
- // Speech recognition software such as Dragon NaturallySpeaking will modify the
1139
- // fields by directly changing its `value`. These changes don't trigger events
1140
- // in JavaScript, so we need to poll to handle when and if they occur.
1141
- CharacterCount.prototype.checkIfValueChanged = function () {
1909
+ /**
1910
+ * Handle key up event
1911
+ *
1912
+ * Update the visible character counter and keep track of when the last update
1913
+ * happened for each keypress
1914
+ */
1915
+ CharacterCount.prototype.handleKeyUp = function () {
1916
+ this.updateVisibleCountMessage();
1917
+ this.lastInputTimestamp = Date.now();
1918
+ };
1919
+
1920
+ /**
1921
+ * Handle focus event
1922
+ *
1923
+ * Speech recognition software such as Dragon NaturallySpeaking will modify the
1924
+ * fields by directly changing its `value`. These changes don't trigger events
1925
+ * in JavaScript, so we need to poll to handle when and if they occur.
1926
+ *
1927
+ * Once the keyup event hasn't been detected for at least 1000 ms (1s), check if
1928
+ * the textarea value has changed and update the count message if it has.
1929
+ *
1930
+ * This is so that the update triggered by the manual comparison doesn't
1931
+ * conflict with debounced KeyboardEvent updates.
1932
+ */
1933
+ CharacterCount.prototype.handleFocus = function () {
1934
+ this.valueChecker = setInterval(function () {
1935
+ if (!this.lastInputTimestamp || (Date.now() - 500) >= this.lastInputTimestamp) {
1936
+ this.updateIfValueChanged();
1937
+ }
1938
+ }.bind(this), 1000);
1939
+ };
1940
+
1941
+ /**
1942
+ * Handle blur event
1943
+ *
1944
+ * Stop checking the textarea value once the textarea no longer has focus
1945
+ */
1946
+ CharacterCount.prototype.handleBlur = function () {
1947
+ // Cancel value checking on blur
1948
+ clearInterval(this.valueChecker);
1949
+ };
1950
+
1951
+ /**
1952
+ * Update count message if textarea value has changed
1953
+ */
1954
+ CharacterCount.prototype.updateIfValueChanged = function () {
1142
1955
  if (!this.$textarea.oldValue) this.$textarea.oldValue = '';
1143
1956
  if (this.$textarea.value !== this.$textarea.oldValue) {
1144
1957
  this.$textarea.oldValue = this.$textarea.value;
@@ -1146,14 +1959,20 @@ CharacterCount.prototype.checkIfValueChanged = function () {
1146
1959
  }
1147
1960
  };
1148
1961
 
1149
- // Helper function to update both the visible and screen reader-specific
1150
- // counters simultaneously (e.g. on init)
1962
+ /**
1963
+ * Update count message
1964
+ *
1965
+ * Helper function to update both the visible and screen reader-specific
1966
+ * counters simultaneously (e.g. on init)
1967
+ */
1151
1968
  CharacterCount.prototype.updateCountMessage = function () {
1152
1969
  this.updateVisibleCountMessage();
1153
1970
  this.updateScreenReaderCountMessage();
1154
1971
  };
1155
1972
 
1156
- // Update visible counter
1973
+ /**
1974
+ * Update visible count message
1975
+ */
1157
1976
  CharacterCount.prototype.updateVisibleCountMessage = function () {
1158
1977
  var $textarea = this.$textarea;
1159
1978
  var $visibleCountMessage = this.$visibleCountMessage;
@@ -1179,10 +1998,12 @@ CharacterCount.prototype.updateVisibleCountMessage = function () {
1179
1998
  }
1180
1999
 
1181
2000
  // Update message
1182
- $visibleCountMessage.innerHTML = this.formattedUpdateMessage();
2001
+ $visibleCountMessage.innerText = this.getCountMessage();
1183
2002
  };
1184
2003
 
1185
- // Update screen reader-specific counter
2004
+ /**
2005
+ * Update screen reader count message
2006
+ */
1186
2007
  CharacterCount.prototype.updateScreenReaderCountMessage = function () {
1187
2008
  var $screenReaderCountMessage = this.$screenReaderCountMessage;
1188
2009
 
@@ -1195,70 +2016,161 @@ CharacterCount.prototype.updateScreenReaderCountMessage = function () {
1195
2016
  }
1196
2017
 
1197
2018
  // Update message
1198
- $screenReaderCountMessage.innerHTML = this.formattedUpdateMessage();
2019
+ $screenReaderCountMessage.innerText = this.getCountMessage();
1199
2020
  };
1200
2021
 
1201
- // Format update message
1202
- CharacterCount.prototype.formattedUpdateMessage = function () {
1203
- var $textarea = this.$textarea;
1204
- var options = this.options;
1205
- var remainingNumber = this.maxLength - this.count($textarea.value);
2022
+ /**
2023
+ * Count the number of characters (or words, if `config.maxwords` is set)
2024
+ * in the given text
2025
+ *
2026
+ * @param {string} text - The text to count the characters of
2027
+ * @returns {number} the number of characters (or words) in the text
2028
+ */
2029
+ CharacterCount.prototype.count = function (text) {
2030
+ if (this.config.maxwords) {
2031
+ var tokens = text.match(/\S+/g) || []; // Matches consecutive non-whitespace chars
2032
+ return tokens.length
2033
+ } else {
2034
+ return text.length
2035
+ }
2036
+ };
2037
+
2038
+ /**
2039
+ * Get count message
2040
+ *
2041
+ * @returns {string} Status message
2042
+ */
2043
+ CharacterCount.prototype.getCountMessage = function () {
2044
+ var remainingNumber = this.maxLength - this.count(this.$textarea.value);
1206
2045
 
1207
- var charVerb = 'remaining';
1208
- var charNoun = 'character';
1209
- var displayNumber = remainingNumber;
1210
- if (options.maxwords) {
1211
- charNoun = 'word';
2046
+ var countType = this.config.maxwords ? 'words' : 'characters';
2047
+ return this.formatCountMessage(remainingNumber, countType)
2048
+ };
2049
+
2050
+ /**
2051
+ * Formats the message shown to users according to what's counted
2052
+ * and how many remain
2053
+ *
2054
+ * @param {number} remainingNumber - The number of words/characaters remaining
2055
+ * @param {string} countType - "words" or "characters"
2056
+ * @returns {string} Status message
2057
+ */
2058
+ CharacterCount.prototype.formatCountMessage = function (remainingNumber, countType) {
2059
+ if (remainingNumber === 0) {
2060
+ return this.i18n.t(countType + 'AtLimit')
1212
2061
  }
1213
- charNoun = charNoun + ((remainingNumber === -1 || remainingNumber === 1) ? '' : 's');
1214
2062
 
1215
- charVerb = (remainingNumber < 0) ? 'too many' : 'remaining';
1216
- displayNumber = Math.abs(remainingNumber);
2063
+ var translationKeySuffix = remainingNumber < 0 ? 'OverLimit' : 'UnderLimit';
1217
2064
 
1218
- return 'You have ' + displayNumber + ' ' + charNoun + ' ' + charVerb
2065
+ return this.i18n.t(countType + translationKeySuffix, { count: Math.abs(remainingNumber) })
1219
2066
  };
1220
2067
 
1221
- // Checks whether the value is over the configured threshold for the input.
1222
- // If there is no configured threshold, it is set to 0 and this function will
1223
- // always return true.
2068
+ /**
2069
+ * Check if count is over threshold
2070
+ *
2071
+ * Checks whether the value is over the configured threshold for the input.
2072
+ * If there is no configured threshold, it is set to 0 and this function will
2073
+ * always return true.
2074
+ *
2075
+ * @returns {boolean} true if the current count is over the config.threshold
2076
+ * (or no threshold is set)
2077
+ */
1224
2078
  CharacterCount.prototype.isOverThreshold = function () {
2079
+ // No threshold means we're always above threshold so save some computation
2080
+ if (!this.config.threshold) {
2081
+ return true
2082
+ }
2083
+
1225
2084
  var $textarea = this.$textarea;
1226
- var options = this.options;
1227
2085
 
1228
2086
  // Determine the remaining number of characters/words
1229
2087
  var currentLength = this.count($textarea.value);
1230
2088
  var maxLength = this.maxLength;
1231
2089
 
1232
- // Set threshold if presented in options
1233
- var thresholdPercent = options.threshold ? options.threshold : 0;
1234
- var thresholdValue = maxLength * thresholdPercent / 100;
2090
+ var thresholdValue = maxLength * this.config.threshold / 100;
1235
2091
 
1236
2092
  return (thresholdValue <= currentLength)
1237
2093
  };
1238
2094
 
1239
- // Update the visible character counter and keep track of when the last update
1240
- // happened for each keypress
1241
- CharacterCount.prototype.handleKeyUp = function () {
1242
- this.updateVisibleCountMessage();
1243
- this.lastInputTimestamp = Date.now();
1244
- };
1245
-
1246
- CharacterCount.prototype.handleFocus = function () {
1247
- // If the field is focused, and a keyup event hasn't been detected for at
1248
- // least 1000 ms (1 second), then run the manual change check.
1249
- // This is so that the update triggered by the manual comparison doesn't
1250
- // conflict with debounced KeyboardEvent updates.
1251
- this.valueChecker = setInterval(function () {
1252
- if (!this.lastInputTimestamp || (Date.now() - 500) >= this.lastInputTimestamp) {
1253
- this.checkIfValueChanged();
1254
- }
1255
- }.bind(this), 1000);
1256
- };
1257
-
1258
- CharacterCount.prototype.handleBlur = function () {
1259
- // Cancel value checking on blur
1260
- clearInterval(this.valueChecker);
1261
- };
2095
+ /**
2096
+ * Character count config
2097
+ *
2098
+ * @typedef {CharacterCountConfigWithMaxLength | CharacterCountConfigWithMaxWords} CharacterCountConfig
2099
+ */
2100
+
2101
+ /**
2102
+ * Character count config (with maximum number of characters)
2103
+ *
2104
+ * @typedef {object} CharacterCountConfigWithMaxLength
2105
+ * @property {number} [maxlength] - The maximum number of characters.
2106
+ * If maxwords is provided, the maxlength option will be ignored.
2107
+ * @property {number} [threshold = 0] - The percentage value of the limit at
2108
+ * which point the count message is displayed. If this attribute is set, the
2109
+ * count message will be hidden by default.
2110
+ * @property {CharacterCountTranslations} [i18n = CHARACTER_COUNT_TRANSLATIONS] - See constant {@link CHARACTER_COUNT_TRANSLATIONS}
2111
+ */
2112
+
2113
+ /**
2114
+ * Character count config (with maximum number of words)
2115
+ *
2116
+ * @typedef {object} CharacterCountConfigWithMaxWords
2117
+ * @property {number} [maxwords] - The maximum number of words. If maxwords is
2118
+ * provided, the maxlength option will be ignored.
2119
+ * @property {number} [threshold = 0] - The percentage value of the limit at
2120
+ * which point the count message is displayed. If this attribute is set, the
2121
+ * count message will be hidden by default.
2122
+ * @property {CharacterCountTranslations} [i18n = CHARACTER_COUNT_TRANSLATIONS] - See constant {@link CHARACTER_COUNT_TRANSLATIONS}
2123
+ */
2124
+
2125
+ /**
2126
+ * Character count translations
2127
+ *
2128
+ * @typedef {object} CharacterCountTranslations
2129
+ *
2130
+ * Messages shown to users as they type. It provides feedback on how many words
2131
+ * or characters they have remaining or if they are over the limit. This also
2132
+ * includes a message used as an accessible description for the textarea.
2133
+ * @property {TranslationPluralForms} [charactersUnderLimit] - Message displayed
2134
+ * when the number of characters is under the configured maximum, `maxlength`.
2135
+ * This message is displayed visually and through assistive technologies. The
2136
+ * component will replace the `%{count}` placeholder with the number of
2137
+ * remaining characters. This is a [pluralised list of
2138
+ * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
2139
+ * @property {string} [charactersAtLimit] - Message displayed when the number of
2140
+ * characters reaches the configured maximum, `maxlength`. This message is
2141
+ * displayed visually and through assistive technologies.
2142
+ * @property {TranslationPluralForms} [charactersOverLimit] - Message displayed
2143
+ * when the number of characters is over the configured maximum, `maxlength`.
2144
+ * This message is displayed visually and through assistive technologies. The
2145
+ * component will replace the `%{count}` placeholder with the number of
2146
+ * remaining characters. This is a [pluralised list of
2147
+ * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
2148
+ * @property {TranslationPluralForms} [wordsUnderLimit] - Message displayed when
2149
+ * the number of words is under the configured maximum, `maxlength`. This
2150
+ * message is displayed visually and through assistive technologies. The
2151
+ * component will replace the `%{count}` placeholder with the number of
2152
+ * remaining words. This is a [pluralised list of
2153
+ * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
2154
+ * @property {string} [wordsAtLimit] - Message displayed when the number of
2155
+ * words reaches the configured maximum, `maxlength`. This message is
2156
+ * displayed visually and through assistive technologies.
2157
+ * @property {TranslationPluralForms} [wordsOverLimit] - Message displayed when
2158
+ * the number of words is over the configured maximum, `maxlength`. This
2159
+ * message is displayed visually and through assistive technologies. The
2160
+ * component will replace the `%{count}` placeholder with the number of
2161
+ * remaining words. This is a [pluralised list of
2162
+ * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
2163
+ * @property {TranslationPluralForms} [textareaDescription] - Message made
2164
+ * available to assistive technologies, if none is already present in the
2165
+ * HTML, to describe that the component accepts only a limited amount of
2166
+ * content. It is visible on the page when JavaScript is unavailable. The
2167
+ * component will replace the `%{count}` placeholder with the value of the
2168
+ * `maxlength` or `maxwords` parameter.
2169
+ */
2170
+
2171
+ /**
2172
+ * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
2173
+ */
1262
2174
 
1263
2175
  return CharacterCount;
1264
2176