@gitlab/ui 85.11.0 → 85.12.1

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.
@@ -1,5 +1,11 @@
1
+ import escape from 'lodash/escape';
1
2
  import { i18n } from '../config';
2
3
 
4
+ const defaultPluralHandler = (n, singleValue, pluralValue) => {
5
+ const value = n === 1 ? singleValue : pluralValue;
6
+ return value.replace(/%d/g, n);
7
+ };
8
+
3
9
  /**
4
10
  * Mark a label as translatable.
5
11
  *
@@ -12,4 +18,46 @@ const translate = (key, defaultValue) => {
12
18
  return (_i18n$key = i18n[key]) !== null && _i18n$key !== void 0 ? _i18n$key : defaultValue;
13
19
  };
14
20
 
15
- export { translate };
21
+ /**
22
+ * Marks a label as translatable and pluralized.
23
+ *
24
+ * @param {*} key Translation key to be leveraged by the consumer to provide a generic translation at configuration time.
25
+ * @param {*} singularValue The singular value to be relied on if the consumer doesn't have translation capabilities.
26
+ * @param {*} pluralValue The plural value to be relied on if the consumer doesn't have translation capabilities.
27
+ * @returns {function} A function that takes a number and returns the pluralized translated label.
28
+ */
29
+ const translatePlural = (key, singularValue, pluralValue) => {
30
+ if (i18n[key]) {
31
+ return i18n[key];
32
+ }
33
+ return x => defaultPluralHandler(x, singularValue, pluralValue);
34
+ };
35
+
36
+ /**
37
+ * Very limited implementation of sprintf supporting only named parameters.
38
+ * Copied from the GitLab repo: https://gitlab.com/gitlab-org/gitlab/-/blob/0dff8b02accb3dccbf6cd31236834c37013aad59/app/assets/javascripts/locale/sprintf.js.
39
+ * @param {string} input - (translated) text with parameters (e.g. '%{num_users} users use us')
40
+ * @param {Object.<string, string|number>} [parameters] - object mapping parameter names to values (e.g. { num_users: 5 })
41
+ * @param {boolean} [escapeParameters=true] - whether parameter values should be escaped (see https://lodash.com/docs/4.17.15#escape)
42
+ * @returns {string} the text with parameters replaces (e.g. '5 users use us')
43
+ * @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
44
+ * @see https://gitlab.com/gitlab-org/gitlab-foss/issues/37992
45
+ */
46
+ function sprintf(input, parameters) {
47
+ let escapeParameters = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
48
+ let output = input;
49
+ output = output.replace(/%+/g, '%');
50
+ if (parameters) {
51
+ const mappedParameters = new Map(Object.entries(parameters));
52
+ mappedParameters.forEach((key, parameterName) => {
53
+ const parameterValue = mappedParameters.get(parameterName);
54
+ const escapedParameterValue = escapeParameters ? escape(parameterValue) : parameterValue;
55
+ // Pass the param value as a function to ignore special replacement patterns like $` and $'.
56
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#syntax
57
+ output = output.replace(new RegExp(`%{${parameterName}}`, 'g'), () => escapedParameterValue);
58
+ });
59
+ }
60
+ return output;
61
+ }
62
+
63
+ export { sprintf, translate, translatePlural };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "85.11.0",
3
+ "version": "85.12.1",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -167,8 +167,8 @@
167
167
  "module-alias": "^2.2.2",
168
168
  "npm-run-all": "^4.1.5",
169
169
  "pikaday": "^1.8.0",
170
- "playwright": "^1.44.1",
171
- "playwright-core": "^1.44.1",
170
+ "playwright": "^1.45.0",
171
+ "playwright-core": "^1.45.0",
172
172
  "plop": "^2.5.4",
173
173
  "postcss": "8.4.28",
174
174
  "postcss-loader": "^7.0.2",
@@ -1,5 +1,5 @@
1
1
  $badge-padding-horizontal: 0.75 * $grid-size;
2
- $badge-min-width: 2.5 * $grid-size;
2
+ $badge-min-width: $gl-spacing-scale-3;
3
3
 
4
4
  @mixin gl-badge-variant(
5
5
  $variant,
@@ -74,7 +74,6 @@ $badge-min-width: 2.5 * $grid-size;
74
74
  @include gl-line-height-normal;
75
75
  gap: $gl-spacing-scale-2;
76
76
  padding: $gl-spacing-scale-1 $badge-padding-horizontal;
77
- min-width: $badge-min-width;
78
77
 
79
78
  @media (forced-colors: active) {
80
79
  border: 1px solid;
@@ -86,6 +85,10 @@ $badge-min-width: 2.5 * $grid-size;
86
85
  @include gl-flex-shrink-0;
87
86
  top: auto;
88
87
  }
88
+
89
+ .gl-badge-content {
90
+ min-width: $badge-min-width;
91
+ }
89
92
  }
90
93
 
91
94
  /* Variants */
@@ -83,7 +83,10 @@ export default {
83
83
  :class="{ '-gl-ml-2 gl-ml-n2': isCircularIcon }"
84
84
  :name="icon"
85
85
  />
86
- <!-- @slot The badge content to display. -->
87
- <slot></slot>
86
+ <!-- eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots -->
87
+ <span v-if="$slots.default" class="gl-badge-content">
88
+ <!-- @slot The badge content to display. -->
89
+ <slot></slot>
90
+ </span>
88
91
  </b-badge>
89
92
  </template>
@@ -26,7 +26,7 @@ import GlLoadingIcon from '../../loading_icon/loading_icon.vue';
26
26
  import GlIntersectionObserver from '../../../utilities/intersection_observer/intersection_observer.vue';
27
27
  import GlSearchBoxByType from '../../search_box_by_type/search_box_by_type.vue';
28
28
  import GlBaseDropdown from '../base_dropdown/base_dropdown.vue';
29
- import { translate } from '../../../../utils/i18n';
29
+ import { translatePlural } from '../../../../utils/i18n';
30
30
  import GlListboxItem from './listbox_item.vue';
31
31
  import GlListboxSearchInput from './listbox_search_input.vue';
32
32
  import GlListboxGroup from './listbox_group.vue';
@@ -343,13 +343,11 @@ export default {
343
343
  srOnlyResultsLabel: {
344
344
  type: Function,
345
345
  required: false,
346
- default: (count) => {
347
- const fn = translate('GlCollapsibleListbox.srOnlyResultsLabel', 'Results count');
348
- if (typeof fn === 'function') {
349
- return fn(count);
350
- }
351
- return `${count} result${count > 1 ? 's' : ''}`;
352
- },
346
+ default: translatePlural(
347
+ 'GlCollapsibleListbox.srOnlyResultsLabel',
348
+ '%d result',
349
+ '%d results'
350
+ ),
353
351
  },
354
352
  },
355
353
  data() {
@@ -5,6 +5,7 @@ import isFunction from 'lodash/isFunction';
5
5
  import range from 'lodash/range';
6
6
  import { GlBreakpointInstance, breakpoints } from '../../../utils/breakpoints';
7
7
  import { alignOptions, resizeDebounceTime } from '../../../utils/constants';
8
+ import { sprintf, translate } from '../../../utils/i18n';
8
9
  import GlIcon from '../icon/icon.vue';
9
10
  import GlLink from '../link/link.vue';
10
11
 
@@ -145,9 +146,10 @@ export default {
145
146
  * aria-label getter for numbered page items, defaults to "Go to page <page_number>"
146
147
  */
147
148
  labelPage: {
148
- type: Function,
149
+ // note: `Function` support is for legacy reasons
150
+ type: [Function, String],
149
151
  required: false,
150
- default: (page) => `Go to page ${page}`,
152
+ default: translate('GlPagination.labelPage', 'Go to page %{page}'),
151
153
  },
152
154
  /**
153
155
  * Controls the component\'s horizontal alignment, value should be one of "left", "center", "right" or "fill"
@@ -263,10 +265,14 @@ export default {
263
265
  return this.pageIsDisabled(this.value + 1);
264
266
  },
265
267
  prevPageAriaLabel() {
266
- return this.prevPageIsDisabled ? false : this.labelPrevPage || this.labelPage(this.value - 1);
268
+ return this.prevPageIsDisabled
269
+ ? false
270
+ : this.labelPrevPage || this.labelForPage(this.value - 1);
267
271
  },
268
272
  nextPageAriaLabel() {
269
- return this.nextPageIsDisabled ? false : this.labelNextPage || this.labelPage(this.value + 1);
273
+ return this.nextPageIsDisabled
274
+ ? false
275
+ : this.labelNextPage || this.labelForPage(this.value + 1);
270
276
  },
271
277
  prevPageHref() {
272
278
  if (this.prevPageIsDisabled) return false;
@@ -286,6 +292,13 @@ export default {
286
292
  window.removeEventListener('resize', debounce(this.setBreakpoint, resizeDebounceTime));
287
293
  },
288
294
  methods: {
295
+ labelForPage(page) {
296
+ if (isFunction(this.labelPage)) {
297
+ return this.labelPage(page);
298
+ }
299
+
300
+ return sprintf(this.labelPage, { page });
301
+ },
289
302
  setBreakpoint() {
290
303
  this.breakpoint = GlBreakpointInstance.getBreakpointSize();
291
304
  },
@@ -299,7 +312,7 @@ export default {
299
312
  },
300
313
  getPageItem(page, label = null) {
301
314
  const commonAttrs = {
302
- 'aria-label': label || this.labelPage(page),
315
+ 'aria-label': label || this.labelForPage(page),
303
316
  href: '#',
304
317
  class: [],
305
318
  };
package/src/utils/i18n.js CHANGED
@@ -1,5 +1,12 @@
1
+ import escape from 'lodash/escape';
1
2
  import { i18n } from '../config';
2
3
 
4
+ const defaultPluralHandler = (n, singleValue, pluralValue) => {
5
+ const value = n === 1 ? singleValue : pluralValue;
6
+
7
+ return value.replace(/%d/g, n);
8
+ };
9
+
3
10
  /**
4
11
  * Mark a label as translatable.
5
12
  *
@@ -8,3 +15,48 @@ import { i18n } from '../config';
8
15
  * @returns {string} The translated label.
9
16
  */
10
17
  export const translate = (key, defaultValue) => i18n[key] ?? defaultValue;
18
+
19
+ /**
20
+ * Marks a label as translatable and pluralized.
21
+ *
22
+ * @param {*} key Translation key to be leveraged by the consumer to provide a generic translation at configuration time.
23
+ * @param {*} singularValue The singular value to be relied on if the consumer doesn't have translation capabilities.
24
+ * @param {*} pluralValue The plural value to be relied on if the consumer doesn't have translation capabilities.
25
+ * @returns {function} A function that takes a number and returns the pluralized translated label.
26
+ */
27
+ export const translatePlural = (key, singularValue, pluralValue) => {
28
+ if (i18n[key]) {
29
+ return i18n[key];
30
+ }
31
+ return (x) => defaultPluralHandler(x, singularValue, pluralValue);
32
+ };
33
+
34
+ /**
35
+ * Very limited implementation of sprintf supporting only named parameters.
36
+ * Copied from the GitLab repo: https://gitlab.com/gitlab-org/gitlab/-/blob/0dff8b02accb3dccbf6cd31236834c37013aad59/app/assets/javascripts/locale/sprintf.js.
37
+ * @param {string} input - (translated) text with parameters (e.g. '%{num_users} users use us')
38
+ * @param {Object.<string, string|number>} [parameters] - object mapping parameter names to values (e.g. { num_users: 5 })
39
+ * @param {boolean} [escapeParameters=true] - whether parameter values should be escaped (see https://lodash.com/docs/4.17.15#escape)
40
+ * @returns {string} the text with parameters replaces (e.g. '5 users use us')
41
+ * @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
42
+ * @see https://gitlab.com/gitlab-org/gitlab-foss/issues/37992
43
+ */
44
+ export function sprintf(input, parameters, escapeParameters = true) {
45
+ let output = input;
46
+
47
+ output = output.replace(/%+/g, '%');
48
+
49
+ if (parameters) {
50
+ const mappedParameters = new Map(Object.entries(parameters));
51
+
52
+ mappedParameters.forEach((key, parameterName) => {
53
+ const parameterValue = mappedParameters.get(parameterName);
54
+ const escapedParameterValue = escapeParameters ? escape(parameterValue) : parameterValue;
55
+ // Pass the param value as a function to ignore special replacement patterns like $` and $'.
56
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#syntax
57
+ output = output.replace(new RegExp(`%{${parameterName}}`, 'g'), () => escapedParameterValue);
58
+ });
59
+ }
60
+
61
+ return output;
62
+ }
package/translations.js CHANGED
@@ -2,10 +2,11 @@
2
2
  export default {
3
3
  'ClearIconButton.title': 'Clear',
4
4
  'GlBreadcrumb.showMoreLabel': 'Show more breadcrumbs',
5
- 'GlCollapsibleListbox.srOnlyResultsLabel': 'Results count',
5
+ 'GlCollapsibleListbox.srOnlyResultsLabel': null,
6
6
  'GlKeysetPagination.navigationLabel': 'Pagination',
7
7
  'GlKeysetPagination.nextText': 'Next',
8
8
  'GlKeysetPagination.prevText': 'Previous',
9
+ 'GlPagination.labelPage': 'Go to page %{page}',
9
10
  'GlSearchBoxByType.clearButtonTitle': 'Clear',
10
11
  'GlSearchBoxByType.input.placeholder': 'Search',
11
12
  'GlSorting.sortAscending': 'Sort direction: ascending',