@gitlab/duo-ui 8.6.0 → 8.7.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # [8.7.0](https://gitlab.com/gitlab-org/duo-ui/compare/v8.6.0...v8.7.0) (2025-03-11)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * adapt default to BCP 47 ([7786272](https://gitlab.com/gitlab-org/duo-ui/commit/7786272f35b6447bda958ac01fc6922d0fb4c102))
7
+
8
+
9
+ ### Features
10
+
11
+ * improve date formatting with localization support ([24c2548](https://gitlab.com/gitlab-org/duo-ui/commit/24c2548b2576fd70e5e93511f7107d965dd6361d))
12
+
1
13
  # [8.6.0](https://gitlab.com/gitlab-org/duo-ui/compare/v8.5.0...v8.6.0) (2025-03-11)
2
14
 
3
15
 
@@ -1,6 +1,6 @@
1
1
  import { GlButton, GlIcon } from '@gitlab/ui';
2
2
  import { translate } from '../../../../utils/i18n';
3
- import { formatDate } from '../../../../utils/date';
3
+ import { formatLocalizedDate } from '../../../../utils/date';
4
4
  import DuoChatThreadsEmpty from './duo_chat_threads_empty';
5
5
  import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
6
6
 
@@ -19,11 +19,15 @@ var script = {
19
19
  threads: {
20
20
  type: Array,
21
21
  required: true
22
+ },
23
+ preferredLocale: {
24
+ type: Array,
25
+ required: true
22
26
  }
23
27
  },
24
28
  computed: {
25
- formatDate() {
26
- return formatDate;
29
+ formattedLocalDate() {
30
+ return date => formatLocalizedDate(date, this.preferredLocale);
27
31
  },
28
32
  groupedThreads() {
29
33
  return this.threads.reduce((threadsGroupedByDate, thread) => {
@@ -61,7 +65,7 @@ var script = {
61
65
  const __vue_script__ = script;
62
66
 
63
67
  /* template */
64
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:"gl-h-full gl-p-5"},[_c('div',{staticClass:"gl-bg-gray-50 gl-text-gray-500 gl-p-4 gl-mb-5 gl-rounded-base",attrs:{"data-testid":"chat-threads-info-banner"}},[_c('p',{staticClass:"gl-m-0 gl-flex"},[_c('gl-icon',{staticClass:"gl-mr-4",attrs:{"name":"bulb"}}),_vm._v(_vm._s(_vm.$options.i18n.CHAT_HISTORY_INFO)+"\n ")],1)]),_vm._v(" "),(_vm.hasThreads)?_vm._l((_vm.groupedThreads),function(threadsForDate,date){return _c('div',{key:date},[_c('div',{staticClass:"gl-font-bold gl-neutral-900 gl-mb-4",attrs:{"data-testid":"chat-threads-date-header"}},[_vm._v("\n "+_vm._s(_vm.formatDate(date))+"\n ")]),_vm._v(" "),_c('div',_vm._l((threadsForDate),function(thread){return _c('div',{key:thread.id,staticClass:"gl-flex gl-align-center gl-mb-4"},[_c('div',{staticClass:"thread-box hover:gl-bg-gray-50 focus:gl-bg-gray-50 gl-text-ellipsis gl-overflow-hidden gl-rounded-base gl-cursor-pointer gl-rounded-base gl-p-4 gl-w-full gl-whitespace-nowrap",attrs:{"tabindex":"0","data-testid":"chat-threads-thread-box"},on:{"click":function($event){return _vm.onSelectThread(thread)}}},[_vm._v("\n "+_vm._s(thread.title || 'Untitled Chat')+"\n ")]),_vm._v(" "),_c('gl-button',{staticClass:"gl-neutral-900 !gl-p-4",attrs:{"data-testid":"chat-threads-delete-thread-button","icon":"remove","category":"tertiary","size":"small","aria-label":_vm.$options.i18n.THREAD_DELETE_LABEL},on:{"click":function($event){return _vm.$emit('delete-thread', thread.id)}}})],1)}),0)])}):_c('duo-chat-threads-empty')],2)};
68
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:"gl-h-full gl-p-5"},[_c('div',{staticClass:"gl-bg-gray-50 gl-text-gray-500 gl-p-4 gl-mb-5 gl-rounded-base",attrs:{"data-testid":"chat-threads-info-banner"}},[_c('p',{staticClass:"gl-m-0 gl-flex"},[_c('gl-icon',{staticClass:"gl-mr-4",attrs:{"name":"bulb"}}),_vm._v(_vm._s(_vm.$options.i18n.CHAT_HISTORY_INFO)+"\n ")],1)]),_vm._v(" "),(_vm.hasThreads)?_vm._l((_vm.groupedThreads),function(threadsForDate,date){return _c('div',{key:date},[_c('div',{staticClass:"gl-font-bold gl-neutral-900 gl-mb-4",attrs:{"data-testid":"chat-threads-date-header"}},[_vm._v("\n "+_vm._s(_vm.formattedLocalDate(date))+"\n ")]),_vm._v(" "),_c('div',_vm._l((threadsForDate),function(thread){return _c('div',{key:thread.id,staticClass:"gl-flex gl-align-center gl-mb-4"},[_c('div',{staticClass:"thread-box hover:gl-bg-gray-50 focus:gl-bg-gray-50 gl-text-ellipsis gl-overflow-hidden gl-rounded-base gl-cursor-pointer gl-rounded-base gl-p-4 gl-w-full gl-whitespace-nowrap",attrs:{"tabindex":"0","data-testid":"chat-threads-thread-box"},on:{"click":function($event){return _vm.onSelectThread(thread)}}},[_vm._v("\n "+_vm._s(thread.title || 'Untitled Chat')+"\n ")]),_vm._v(" "),_c('gl-button',{staticClass:"gl-neutral-900 !gl-p-4",attrs:{"data-testid":"chat-threads-delete-thread-button","icon":"remove","category":"tertiary","size":"small","aria-label":_vm.$options.i18n.THREAD_DELETE_LABEL},on:{"click":function($event){return _vm.$emit('delete-thread', thread.id)}}})],1)}),0)])}):_c('duo-chat-threads-empty')],2)};
65
69
  var __vue_staticRenderFns__ = [];
66
70
 
67
71
  /* style */
@@ -33,6 +33,14 @@ const isThread = thread => typeof thread === 'object' && typeof thread.id === 's
33
33
 
34
34
  // eslint-disable-next-line unicorn/no-array-callback-reference
35
35
  const threadListValidator = threads => threads.every(isThread);
36
+ const localeValidator = value => {
37
+ try {
38
+ Intl.getCanonicalLocales(value);
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ };
36
44
  var script = {
37
45
  name: 'DuoChat',
38
46
  components: {
@@ -232,6 +240,16 @@ var script = {
232
240
  type: Boolean,
233
241
  required: false,
234
242
  default: false
243
+ },
244
+ /**
245
+ * The preferred locale for the chat interface.
246
+ * Follows BCP 47 language tag format (e.g., 'en-US', 'fr-FR', 'es-ES').
247
+ */
248
+ preferredLocale: {
249
+ type: Array,
250
+ required: false,
251
+ default: () => ['en-US', 'en'],
252
+ validator: localeValidator
235
253
  }
236
254
  },
237
255
  data() {
@@ -574,7 +592,7 @@ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=
574
592
  },attrs:{"width":_vm.shouldRenderResizable ? _vm.dimensions.width : null,"height":_vm.shouldRenderResizable ? _vm.dimensions.height : null,"max-width":_vm.shouldRenderResizable ? _vm.dimensions.maxWidth : null,"max-height":_vm.shouldRenderResizable ? _vm.dimensions.maxHeight : null,"min-width":_vm.shouldRenderResizable ? _vm.dimensions.minWidth : null,"left":_vm.shouldRenderResizable ? _vm.dimensions.left : null,"top":_vm.shouldRenderResizable ? _vm.dimensions.top : null,"fit-parent":true,"min-height":_vm.shouldRenderResizable ? _vm.dimensions.minHeight : null,"active":_vm.shouldRenderResizable ? ['l', 't', 'lt'] : null},on:{"resize:end":_vm.updateSize}},[(!_vm.isHidden)?_c('aside',{staticClass:"markdown-code-block duo-chat gl-bottom-0 gl-max-h-full",class:{
575
593
  'resizable-content': _vm.shouldRenderResizable,
576
594
  'duo-chat-drawer': !_vm.shouldRenderResizable,
577
- },attrs:{"id":"chat-component","role":"complementary","data-testid":"chat-component"}},[(_vm.showHeader)?_c('duo-chat-header',{attrs:{"active-thread-id":_vm.activeThreadId,"title":_vm.isMultithreaded && _vm.currentView === 'list' ? _vm.$options.i18n.CHAT_HISTORY_TITLE : _vm.title,"error":_vm.error,"is-multithreaded":_vm.isMultithreaded,"current-view":_vm.currentView,"should-render-resizable":_vm.shouldRenderResizable,"badge-type":_vm.isMultithreaded ? null : _vm.badgeType},on:{"go-back":_vm.onGoBack,"new-chat":_vm.onNewChat,"close":_vm.hideChat},scopedSlots:_vm._u([{key:"subheader",fn:function(){return [_vm._t("subheader")]},proxy:true}],null,true)}):_vm._e(),_vm._v(" "),(_vm.shouldShowThreadList)?_c('div',{staticClass:"gl-h-full"},[_c('duo-chat-threads',{attrs:{"threads":_vm.threadList},on:{"new-chat":_vm.onNewChat,"select-thread":_vm.onSelectThread,"delete-thread":_vm.onDeleteThread,"close":_vm.hideChat}})],1):_c('span',{staticClass:"gl-h-full gl-flex gl-flex-col gl-justify-end"},[_c('div',{staticClass:"duo-chat-drawer-body gl-bg-default",attrs:{"data-testid":"chat-history"},on:{"scroll":_vm.handleScrollingTrottled}},[_c('transition-group',{staticClass:"duo-chat-history gl-flex gl-flex-col gl-justify-end",class:[
595
+ },attrs:{"id":"chat-component","role":"complementary","data-testid":"chat-component"}},[(_vm.showHeader)?_c('duo-chat-header',{attrs:{"active-thread-id":_vm.activeThreadId,"title":_vm.isMultithreaded && _vm.currentView === 'list' ? _vm.$options.i18n.CHAT_HISTORY_TITLE : _vm.title,"error":_vm.error,"is-multithreaded":_vm.isMultithreaded,"current-view":_vm.currentView,"should-render-resizable":_vm.shouldRenderResizable,"badge-type":_vm.isMultithreaded ? null : _vm.badgeType},on:{"go-back":_vm.onGoBack,"new-chat":_vm.onNewChat,"close":_vm.hideChat},scopedSlots:_vm._u([{key:"subheader",fn:function(){return [_vm._t("subheader")]},proxy:true}],null,true)}):_vm._e(),_vm._v(" "),(_vm.shouldShowThreadList)?_c('div',{staticClass:"gl-h-full"},[_c('duo-chat-threads',{attrs:{"threads":_vm.threadList,"preferred-locale":_vm.preferredLocale},on:{"new-chat":_vm.onNewChat,"select-thread":_vm.onSelectThread,"delete-thread":_vm.onDeleteThread,"close":_vm.hideChat}})],1):_c('span',{staticClass:"gl-h-full gl-flex gl-flex-col gl-justify-end"},[_c('div',{staticClass:"duo-chat-drawer-body gl-bg-default",attrs:{"data-testid":"chat-history"},on:{"scroll":_vm.handleScrollingTrottled}},[_c('transition-group',{staticClass:"duo-chat-history gl-flex gl-flex-col gl-justify-end",class:[
578
596
  {
579
597
  'gl-h-full': !_vm.hasMessages,
580
598
  'force-scroll-bar': _vm.hasMessages,
@@ -1,3 +1,4 @@
1
+ const DEFAULT_LOCALE = ['en-US'];
1
2
  function getOrdinalSuffix(day) {
2
3
  if (day > 3 && day < 21) return 'th';
3
4
  switch (day % 10) {
@@ -11,17 +12,39 @@ function getOrdinalSuffix(day) {
11
12
  return 'th';
12
13
  }
13
14
  }
14
- function formatDate(dateStr) {
15
+ const isValidDateString = dateStr => {
16
+ if (!dateStr) return false;
15
17
  const date = new Date(dateStr);
18
+ return date.toString() !== 'Invalid Date';
19
+ };
20
+ const getValidLocale = preferredLocale => {
21
+ const locale = preferredLocale !== null && preferredLocale !== void 0 && preferredLocale.length ? preferredLocale : DEFAULT_LOCALE;
22
+ const supportedLocales = Intl.DateTimeFormat.supportedLocalesOf(locale);
23
+ return supportedLocales.length ? supportedLocales : DEFAULT_LOCALE;
24
+ };
25
+ const createDateFormatter = locale => {
26
+ return new Intl.DateTimeFormat(locale, {
27
+ month: 'long',
28
+ day: 'numeric',
29
+ year: 'numeric'
30
+ });
31
+ };
32
+ const formatEnglishDate = (date, formatter) => {
16
33
  const day = date.getDate();
17
34
  const suffix = getOrdinalSuffix(day);
18
- const month = new Intl.DateTimeFormat('en-US', {
19
- month: 'long'
20
- }).format(date);
21
- const year = new Intl.DateTimeFormat('en-US', {
22
- year: 'numeric'
23
- }).format(date);
24
- return `${month} ${day}${suffix}, ${year}`;
35
+ const parts = formatter.formatToParts(date);
36
+ const formattedParts = parts.map(part => part.type === 'day' ? `${part.value}${suffix}` : part.value);
37
+ return formattedParts.join('');
38
+ };
39
+ function formatLocalizedDate(dateStr, preferredLocale) {
40
+ var _validLocale$;
41
+ if (!isValidDateString(dateStr)) {
42
+ return 'Invalid Date';
43
+ }
44
+ const date = new Date(dateStr);
45
+ const validLocale = getValidLocale(preferredLocale);
46
+ const formatter = createDateFormatter(validLocale);
47
+ return (_validLocale$ = validLocale[0]) !== null && _validLocale$ !== void 0 && _validLocale$.startsWith('en') ? formatEnglishDate(date, formatter) : formatter.format(date);
25
48
  }
26
49
 
27
- export { formatDate, getOrdinalSuffix };
50
+ export { formatLocalizedDate, getOrdinalSuffix };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/duo-ui",
3
- "version": "8.6.0",
3
+ "version": "8.7.0",
4
4
  "description": "Duo UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -1,7 +1,7 @@
1
1
  <script>
2
2
  import { GlButton, GlIcon } from '@gitlab/ui';
3
3
  import { translate } from '../../../../utils/i18n';
4
- import { formatDate } from '../../../../utils/date';
4
+ import { formatLocalizedDate } from '../../../../utils/date';
5
5
  import DuoChatThreadsEmpty from './duo_chat_threads_empty.vue';
6
6
 
7
7
  const i18n = {
@@ -26,11 +26,15 @@ export default {
26
26
  type: Array,
27
27
  required: true,
28
28
  },
29
+ preferredLocale: {
30
+ type: Array,
31
+ required: true,
32
+ },
29
33
  },
30
34
 
31
35
  computed: {
32
- formatDate() {
33
- return formatDate;
36
+ formattedLocalDate() {
37
+ return (date) => formatLocalizedDate(date, this.preferredLocale);
34
38
  },
35
39
 
36
40
  groupedThreads() {
@@ -86,7 +90,7 @@ export default {
86
90
  <template v-if="hasThreads">
87
91
  <div v-for="(threadsForDate, date) in groupedThreads" :key="date">
88
92
  <div data-testid="chat-threads-date-header" class="gl-font-bold gl-neutral-900 gl-mb-4">
89
- {{ formatDate(date) }}
93
+ {{ formattedLocalDate(date) }}
90
94
  </div>
91
95
 
92
96
  <div>
@@ -86,6 +86,15 @@ const isThread = (thread) =>
86
86
  // eslint-disable-next-line unicorn/no-array-callback-reference
87
87
  const threadListValidator = (threads) => threads.every(isThread);
88
88
 
89
+ const localeValidator = (value) => {
90
+ try {
91
+ Intl.getCanonicalLocales(value);
92
+ return true;
93
+ } catch {
94
+ return false;
95
+ }
96
+ };
97
+
89
98
  export default {
90
99
  name: 'DuoChat',
91
100
  components: {
@@ -287,6 +296,16 @@ export default {
287
296
  required: false,
288
297
  default: false,
289
298
  },
299
+ /**
300
+ * The preferred locale for the chat interface.
301
+ * Follows BCP 47 language tag format (e.g., 'en-US', 'fr-FR', 'es-ES').
302
+ */
303
+ preferredLocale: {
304
+ type: Array,
305
+ required: false,
306
+ default: () => ['en-US', 'en'],
307
+ validator: localeValidator,
308
+ },
290
309
  },
291
310
  data() {
292
311
  return {
@@ -686,6 +705,7 @@ export default {
686
705
  <div v-if="shouldShowThreadList" class="gl-h-full">
687
706
  <duo-chat-threads
688
707
  :threads="threadList"
708
+ :preferred-locale="preferredLocale"
689
709
  @new-chat="onNewChat"
690
710
  @select-thread="onSelectThread"
691
711
  @delete-thread="onDeleteThread"
package/src/utils/date.js CHANGED
@@ -1,3 +1,5 @@
1
+ const DEFAULT_LOCALE = ['en-US'];
2
+
1
3
  export function getOrdinalSuffix(day) {
2
4
  if (day > 3 && day < 21) return 'th';
3
5
  switch (day % 10) {
@@ -12,13 +14,48 @@ export function getOrdinalSuffix(day) {
12
14
  }
13
15
  }
14
16
 
15
- export function formatDate(dateStr) {
17
+ const isValidDateString = (dateStr) => {
18
+ if (!dateStr) return false;
16
19
  const date = new Date(dateStr);
20
+ return date.toString() !== 'Invalid Date';
21
+ };
22
+
23
+ const getValidLocale = (preferredLocale) => {
24
+ const locale = preferredLocale?.length ? preferredLocale : DEFAULT_LOCALE;
25
+ const supportedLocales = Intl.DateTimeFormat.supportedLocalesOf(locale);
26
+ return supportedLocales.length ? supportedLocales : DEFAULT_LOCALE;
27
+ };
28
+
29
+ const createDateFormatter = (locale) => {
30
+ return new Intl.DateTimeFormat(locale, {
31
+ month: 'long',
32
+ day: 'numeric',
33
+ year: 'numeric',
34
+ });
35
+ };
36
+
37
+ const formatEnglishDate = (date, formatter) => {
17
38
  const day = date.getDate();
18
39
  const suffix = getOrdinalSuffix(day);
19
40
 
20
- const month = new Intl.DateTimeFormat('en-US', { month: 'long' }).format(date);
21
- const year = new Intl.DateTimeFormat('en-US', { year: 'numeric' }).format(date);
41
+ const parts = formatter.formatToParts(date);
42
+ const formattedParts = parts.map((part) =>
43
+ part.type === 'day' ? `${part.value}${suffix}` : part.value
44
+ );
45
+
46
+ return formattedParts.join('');
47
+ };
48
+
49
+ export function formatLocalizedDate(dateStr, preferredLocale) {
50
+ if (!isValidDateString(dateStr)) {
51
+ return 'Invalid Date';
52
+ }
53
+
54
+ const date = new Date(dateStr);
55
+ const validLocale = getValidLocale(preferredLocale);
56
+ const formatter = createDateFormatter(validLocale);
22
57
 
23
- return `${month} ${day}${suffix}, ${year}`;
58
+ return validLocale[0]?.startsWith('en')
59
+ ? formatEnglishDate(date, formatter)
60
+ : formatter.format(date);
24
61
  }