diff --git a/.gitignore b/.gitignore index 5dc5d9b28..60ecd7ed8 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ i18nRepo .env .envrc .direnv + +.vs diff --git a/.storybook/preview.ts b/.storybook/preview.ts index b30cd1d81..a95232671 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -4,6 +4,24 @@ import { type CSSResult, html } from 'lit'; import { configureTheme } from '../src/theming/config'; import type { Decorator, Preview } from '@storybook/web-components-vite'; import { withActions } from 'storybook/actions/decorator'; +import { registerI18n } from 'igniteui-i18n-core'; +import { + ResourceStringsBG, + ResourceStringsDE, + ResourceStringsES, + ResourceStringsFR, + ResourceStringsJA, +} from 'igniteui-i18n-resources'; + +const LocalizationResources = new Map( + Object.entries({ + de: ResourceStringsDE, + fr: ResourceStringsFR, + es: ResourceStringsES, + ja: ResourceStringsJA, + bg: ResourceStringsBG, + }) +); type ThemeImport = { styles: CSSResult }; @@ -62,6 +80,12 @@ const themeProvider: Decorator = (story, context) => { `; }; +const localeProvider: Decorator = (story, context) => { + const { localization } = context.globals; + document.documentElement.lang = localization ?? 'en'; + return story(); +}; + export default { globalTypes: { theme: { @@ -102,6 +126,21 @@ export default { ], }, }, + localization: { + name: 'Localization', + description: 'Component localization', + toolbar: { + icon: 'globe', + items: [ + { value: 'en', title: 'English' }, + { value: 'de', title: 'German' }, + { value: 'fr', title: 'French' }, + { value: 'es', title: 'Spanish' }, + { value: 'ja', title: 'Japanese' }, + { value: 'bg', title: 'Bulgarian' }, + ], + }, + }, size: { name: 'Size', description: 'Component size', @@ -120,6 +159,7 @@ export default { theme: 'bootstrap', variant: 'light', direction: 'ltr', + localization: 'en', size: 'default', }, parameters: { @@ -127,9 +167,13 @@ export default { disable: true, }, }, - decorators: [themeProvider, withActions], + decorators: [themeProvider, withActions, localeProvider], loaders: [ async (context) => { + for (const [locale, resources] of LocalizationResources.entries()) { + registerI18n(resources, locale); + } + const { theme, variant } = context.globals; return { theme: getTheme({ theme, variant }) }; }, diff --git a/package-lock.json b/package-lock.json index 27fe9dc34..42b7061bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@floating-ui/dom": "^1.7.4", "@lit-labs/virtualizer": "^2.1.1", "@lit/context": "^1.1.6", + "igniteui-i18n-core": "0.6.0-alpha.4", "lit": "^3.3.1" }, "devDependencies": { @@ -36,6 +37,7 @@ "globby": "^15.0.0", "husky": "^9.1.7", "ig-typedoc-theme": "^6.2.3", + "igniteui-i18n-resources": "0.6.0-alpha.4", "igniteui-theming": "^20.0.0", "keep-a-changelog": "^2.7.1", "lint-staged": "^16.2.4", @@ -6490,6 +6492,16 @@ "dev": true, "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7522,6 +7534,33 @@ "typedoc-plugin-localization": "^3.0.6" } }, + "node_modules/igniteui-i18n-core": { + "version": "0.6.0-alpha.4", + "resolved": "https://registry.npmjs.org/igniteui-i18n-core/-/igniteui-i18n-core-0.6.0-alpha.4.tgz", + "integrity": "sha512-N0LQ2AmzfY2Qu7Av2wDosqnoM68W8bHNvxCSkn5WxPr8yzrL1Fn8D1a03wMG29qDGVUSKaKa7mfuhc9+CVbBlw==", + "license": "SEE LICENSE IN LICENSE", + "optionalDependencies": { + "events": "^3.3.0" + }, + "peerDependencies": { + "igniteui-i18n-resources": "0.6.0-alpha.4" + }, + "peerDependenciesMeta": { + "igniteui-i18n-resources": { + "optional": true + } + } + }, + "node_modules/igniteui-i18n-resources": { + "version": "0.6.0-alpha.4", + "resolved": "https://registry.npmjs.org/igniteui-i18n-resources/-/igniteui-i18n-resources-0.6.0-alpha.4.tgz", + "integrity": "sha512-QAF7pH/TDD7jkbDdNYEsQ/s5m9AED9l8Wtp2lkOLJCgGP6VbGULWeyRuAplK1IlIVWS0oUbkoi57NAnDc3nz8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "igniteui-i18n-core": "0.6.0-alpha.4" + } + }, "node_modules/igniteui-theming": { "version": "20.0.0", "resolved": "https://registry.npmjs.org/igniteui-theming/-/igniteui-theming-20.0.0.tgz", diff --git a/package.json b/package.json index 850e033b5..ba6b532fc 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,8 @@ "@floating-ui/dom": "^1.7.4", "@lit-labs/virtualizer": "^2.1.1", "@lit/context": "^1.1.6", - "lit": "^3.3.1" + "lit": "^3.3.1", + "igniteui-i18n-core": "0.6.0-alpha.4" }, "devDependencies": { "@biomejs/biome": "~2.2.6", @@ -80,6 +81,7 @@ "globby": "^15.0.0", "husky": "^9.1.7", "ig-typedoc-theme": "^6.2.3", + "igniteui-i18n-resources": "0.6.0-alpha.4", "igniteui-theming": "^20.0.0", "keep-a-changelog": "^2.7.1", "lint-staged": "^16.2.4", diff --git a/src/components/calendar/base.ts b/src/components/calendar/base.ts index bc4c12346..b573009e5 100644 --- a/src/components/calendar/base.ts +++ b/src/components/calendar/base.ts @@ -4,6 +4,11 @@ import { property, state } from 'lit/decorators.js'; import { blazorDeepImport } from '../common/decorators/blazorDeepImport.js'; import { blazorIndirectRender } from '../common/decorators/blazorIndirectRender.js'; import { watch } from '../common/decorators/watch.js'; +import { + IgcCalendarResourceStringEN, + type IgcCalendarResourceStrings, +} from '../common/i18n/EN/calendar.resources.js'; +import { addI18nController } from '../common/i18n/i18n-controller.js'; import { first } from '../common/util.js'; import { convertToDate, convertToDates, getWeekDayNumber } from './helpers.js'; import { CalendarDay } from './model.js'; @@ -16,6 +21,10 @@ import type { @blazorIndirectRender @blazorDeepImport export class IgcCalendarBaseComponent extends LitElement { + protected readonly _i18nController = + addI18nController(this, { + defaultEN: IgcCalendarResourceStringEN, + }); private _initialActiveDateSet = false; protected get _hasValues() { @@ -130,7 +139,25 @@ export class IgcCalendarBaseComponent extends LitElement { * @attr locale */ @property() - public locale = 'en'; + public set locale(value: string) { + this._i18nController.locale = value; + } + + public get locale() { + return this._i18nController.locale; + } + + /** + * The resource strings for localization. + */ + @property({ attribute: false }) + public set resourceStrings(value: IgcCalendarResourceStrings) { + this._i18nController.resourceStrings = value; + } + + public get resourceStrings(): IgcCalendarResourceStrings { + return this._i18nController.resourceStrings; + } /** Gets/Sets the special dates for the component. */ @property({ attribute: false }) diff --git a/src/components/calendar/calendar-rendering.spec.ts b/src/components/calendar/calendar-rendering.spec.ts index a37ac91ee..ef7ee1ca3 100644 --- a/src/components/calendar/calendar-rendering.spec.ts +++ b/src/components/calendar/calendar-rendering.spec.ts @@ -62,7 +62,7 @@ describe('Calendar Rendering', () => { `
- Select date + Select Date

diff --git a/src/components/calendar/calendar.ts b/src/components/calendar/calendar.ts index f4d7393b9..d35c9247b 100644 --- a/src/components/calendar/calendar.ts +++ b/src/components/calendar/calendar.ts @@ -17,7 +17,6 @@ import { } from '../common/controllers/key-bindings.js'; import { watch } from '../common/decorators/watch.js'; import { registerComponent } from '../common/definitions/register.js'; -import { IgcCalendarResourceStringEN } from '../common/i18n/calendar.resources.js'; import { createDateTimeFormatters } from '../common/localization/intl-formatters.js'; import type { Constructor } from '../common/mixins/constructor.js'; import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; @@ -220,12 +219,6 @@ export default class IgcCalendarComponent extends EventEmitterMixin< public formatOptions: Pick = { month: 'long', weekday: 'narrow' }; - /** - * The resource strings for localization. - */ - @property({ attribute: false }) - public resourceStrings = IgcCalendarResourceStringEN; - private _intl = createDateTimeFormatters(this.locale, { month: { month: this.formatOptions.month, diff --git a/src/components/calendar/days-view/days-view.ts b/src/components/calendar/days-view/days-view.ts index 785c67e9b..05c8b1ee9 100644 --- a/src/components/calendar/days-view/days-view.ts +++ b/src/components/calendar/days-view/days-view.ts @@ -7,7 +7,6 @@ import { blazorIndirectRender } from '../../common/decorators/blazorIndirectRend import { blazorSuppressComponent } from '../../common/decorators/blazorSuppressComponent.js'; import { watch } from '../../common/decorators/watch.js'; import { registerComponent } from '../../common/definitions/register.js'; -import { IgcCalendarResourceStringEN } from '../../common/i18n/calendar.resources.js'; import { createDateTimeFormatters } from '../../common/localization/intl-formatters.js'; import type { Constructor } from '../../common/mixins/constructor.js'; import { EventEmitterMixin } from '../../common/mixins/event-emitter.js'; @@ -101,10 +100,6 @@ export default class IgcDaysViewComponent extends EventEmitterMixin< return this._rangePreviewDate?.native; } - /** The resource strings. */ - @property({ attribute: false }) - public resourceStrings = IgcCalendarResourceStringEN; - /** * The format of the days. Defaults to narrow. * @attr week-day-format diff --git a/src/components/carousel/carousel.spec.ts b/src/components/carousel/carousel.spec.ts index f4d6f1b5d..0eb28c214 100644 --- a/src/components/carousel/carousel.spec.ts +++ b/src/components/carousel/carousel.spec.ts @@ -133,12 +133,12 @@ describe('Carousel', () => { )?.id; expect(carousel).shadowDom.to.equal( `
- + - + diff --git a/src/components/carousel/carousel.ts b/src/components/carousel/carousel.ts index 098149dff..c5f2c3ac0 100644 --- a/src/components/carousel/carousel.ts +++ b/src/components/carousel/carousel.ts @@ -1,4 +1,8 @@ import { ContextProvider } from '@lit/context'; +import { + CarouselResourceStringsEN, + type ICarouselResourceStrings, +} from 'igniteui-i18n-core'; import { html, LitElement, nothing } from 'lit'; import { property, queryAll, state } from 'lit/decorators.js'; import { cache } from 'lit/directives/cache.js'; @@ -31,6 +35,7 @@ import { } from '../common/controllers/slot.js'; import { watch } from '../common/decorators/watch.js'; import { registerComponent } from '../common/definitions/register.js'; +import { addI18nController } from '../common/i18n/i18n-controller.js'; import type { Constructor } from '../common/mixins/constructor.js'; import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; import { partMap } from '../common/part-map.js'; @@ -130,6 +135,11 @@ export default class IgcCarouselComponent extends EventEmitterMixin< initial: true, }); + private readonly _i18nController = + addI18nController(this, { + defaultEN: CarouselResourceStringsEN, + }); + private readonly _context = new ContextProvider(this, { context: carouselContext, initialValue: this, @@ -216,7 +226,17 @@ export default class IgcCarouselComponent extends EventEmitterMixin< * @attr indicators-label-format */ @property({ attribute: 'indicators-label-format' }) - public indicatorsLabelFormat = 'Slide {0}'; + public set indicatorsLabelFormat(value: string) { + this._indicatorsLabelFormat = value; + } + + public get indicatorsLabelFormat() { + return ( + this._indicatorsLabelFormat ?? + `${this.resourceStrings.carousel_slide} {0}` + ); + } + private _indicatorsLabelFormat: string | undefined; /** * The format used to set the aria-label on the carousel slides and the text displayed @@ -227,7 +247,17 @@ export default class IgcCarouselComponent extends EventEmitterMixin< * @attr slides-label-format */ @property({ attribute: 'slides-label-format' }) - public slidesLabelFormat = '{0} of {1}'; + public set slidesLabelFormat(value: string) { + this._slidesLabelFormat = value; + } + + public get slidesLabelFormat() { + return ( + this._slidesLabelFormat ?? `{0} ${this.resourceStrings.carousel_of} {1}` + ); + } + + private _slidesLabelFormat: string | undefined; /** * The duration in milliseconds between changing the active slide. @@ -250,6 +280,31 @@ export default class IgcCarouselComponent extends EventEmitterMixin< @property({ attribute: 'animation-type' }) public animationType: HorizontalTransitionAnimation = 'slide'; + /** + * Gets/Sets the locale used for formatting and displaying the dates in the component. + * @attr locale + */ + @property() + public set locale(value: string) { + this._i18nController.locale = value; + } + + public get locale() { + return this._i18nController.locale; + } + + /** + * The resource strings for localization. + */ + @property({ attribute: false }) + public set resourceStrings(value: ICarouselResourceStrings) { + this._i18nController.resourceStrings = value; + } + + public get resourceStrings(): ICarouselResourceStrings { + return this._i18nController.resourceStrings; + } + /* blazorSuppress */ /** * The slides of the carousel. @@ -722,7 +777,8 @@ export default class IgcCarouselComponent extends EventEmitterMixin< ${ref(this._prevButtonRef)} type="button" part="navigation previous" - aria-label="Previous slide" + aria-label=${this.resourceStrings.carousel_previous_slide ?? + 'previous slide'} aria-controls=${this._carouselId} ?disabled=${this.disableLoop && this.current === 0} @click=${this._handleNavigationInteractionPrevious} @@ -740,7 +796,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin< ${ref(this._nextButtonRef)} type="button" part="navigation next" - aria-label="Next slide" + aria-label=${this.resourceStrings.carousel_next_slide ?? 'next slide'} aria-controls=${this._carouselId} ?disabled=${this.disableLoop && this.current === this.total - 1} @click=${this._handleNavigationInteractionNext} diff --git a/src/components/chat/chat-state.ts b/src/components/chat/chat-state.ts index f91e62ed4..87cf751b2 100644 --- a/src/components/chat/chat-state.ts +++ b/src/components/chat/chat-state.ts @@ -1,4 +1,4 @@ -import { IgcChatResourceStringEN } from '../common/i18n/chat.resources.js'; +import { IgcChatResourceStringEN } from '../common/i18n/EN/chat.resources.js'; import { isEmpty, nanoid } from '../common/util.js'; import IgcToastComponent from '../toast/toast.js'; import IgcTooltipComponent from '../tooltip/tooltip.js'; diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts index c298da872..1dc227fc8 100644 --- a/src/components/chat/chat.ts +++ b/src/components/chat/chat.ts @@ -8,7 +8,7 @@ import IgcButtonComponent from '../button/button.js'; import { chatContext, chatUserInputContext } from '../common/context.js'; import { addSlotController, setSlots } from '../common/controllers/slot.js'; import { registerComponent } from '../common/definitions/register.js'; -import { IgcChatResourceStringEN } from '../common/i18n/chat.resources.js'; +import { IgcChatResourceStringEN } from '../common/i18n/EN/chat.resources.js'; import type { Constructor } from '../common/mixins/constructor.js'; import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; import { isEmpty } from '../common/util.js'; diff --git a/src/components/chip/chip.ts b/src/components/chip/chip.ts index 6cff4fbb8..e23c33b7a 100644 --- a/src/components/chip/chip.ts +++ b/src/components/chip/chip.ts @@ -1,3 +1,7 @@ +import { + ChipResourceStringsEN, + type IChipResourceStrings, +} from 'igniteui-i18n-core'; import { html, LitElement, nothing } from 'lit'; import { property } from 'lit/decorators.js'; import { createRef, ref } from 'lit/directives/ref.js'; @@ -5,6 +9,7 @@ import { addThemingController } from '../../theming/theming-controller.js'; import { addKeybindings } from '../common/controllers/key-bindings.js'; import { addSlotController, setSlots } from '../common/controllers/slot.js'; import { registerComponent } from '../common/definitions/register.js'; +import { addI18nController } from '../common/i18n/i18n-controller.js'; import type { Constructor } from '../common/mixins/constructor.js'; import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; import IgcIconComponent from '../icon/icon.js'; @@ -90,6 +95,38 @@ export default class IgcChipComponent extends EventEmitterMixin< @property({ reflect: true }) public variant!: StyleVariant; + /** + * Gets/Sets the locale used for formatting and displaying the dates in the component. + * @attr locale + */ + @property() + public set locale(value: string) { + this._i18nController.locale = value; + } + + public get locale() { + return this._i18nController.locale; + } + + /** + * The resource strings for localization. + */ + @property({ attribute: false }) + public set resourceStrings(value: IChipResourceStrings) { + this._i18nController.resourceStrings = value; + } + + public get resourceStrings(): IChipResourceStrings { + return this._i18nController.resourceStrings; + } + + private readonly _i18nController = addI18nController( + this, + { + defaultEN: ChipResourceStringsEN, + } + ); + constructor() { super(); @@ -122,7 +159,11 @@ export default class IgcChipComponent extends EventEmitterMixin< this.selectable && this.selected ? html` - + ` : nothing; @@ -154,7 +195,7 @@ export default class IgcChipComponent extends EventEmitterMixin< collection="default" tabindex="0" role="button" - aria-label="Remove" + aria-label=${this.resourceStrings.chip_remove ?? 'remove chip'} > ` diff --git a/src/components/combo/combo.spec.ts b/src/components/combo/combo.spec.ts index 0a12bc7da..7a81f7085 100644 --- a/src/components/combo/combo.spec.ts +++ b/src/components/combo/combo.spec.ts @@ -209,7 +209,7 @@ describe('Combo', () => { expect(combo.autofocusList).to.be.false; expect(combo.label).to.be.undefined; expect(combo.placeholder).to.be.undefined; - expect(combo.placeholderSearch).to.equal('Search'); + expect(combo.placeholderSearch).to.equal('Enter a Search Term'); expect(combo.outlined).to.be.false; expect(combo.valueKey).to.equal('id'); expect(combo.displayKey).to.equal('name'); diff --git a/src/components/combo/combo.ts b/src/components/combo/combo.ts index 87518e363..4fd1eb80f 100644 --- a/src/components/combo/combo.ts +++ b/src/components/combo/combo.ts @@ -1,3 +1,7 @@ +import { + ComboResourceStringsEN, + type IComboResourceStrings, +} from 'igniteui-i18n-core'; import { html, LitElement, nothing, type TemplateResult } from 'lit'; import { property, @@ -13,6 +17,7 @@ import { blazorAdditionalDependencies } from '../common/decorators/blazorAdditio import { blazorIndirectRender } from '../common/decorators/blazorIndirectRender.js'; import { watch } from '../common/decorators/watch.js'; import { registerComponent } from '../common/definitions/register.js'; +import { addI18nController } from '../common/i18n/i18n-controller.js'; import type { Constructor } from '../common/mixins/constructor.js'; import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; import { FormAssociatedRequiredMixin } from '../common/mixins/forms/associated-required.js'; @@ -144,6 +149,13 @@ export default class IgcComboComponent< }, }); + private readonly _i18nController = addI18nController( + this, + { + defaultEN: ComboResourceStringsEN, + } + ); + protected override readonly _formValue = createFormValueState< ComboValue[] >(this, { @@ -257,6 +269,19 @@ export default class IgcComboComponent< @property({ type: Boolean, attribute: 'autofocus-list' }) public autofocusList = false; + /** + * Gets/Sets the locale used for formatting and displaying the dates in the component. + * @attr locale + */ + @property() + public set locale(value: string) { + this._i18nController.locale = value; + } + + public get locale() { + return this._i18nController.locale; + } + /** * The label attribute of the control. * @attr label @@ -276,7 +301,19 @@ export default class IgcComboComponent< * @attr placeholder-search */ @property({ attribute: 'placeholder-search' }) - public placeholderSearch = 'Search'; + public set placeholderSearch(value: string) { + this._placeholderSearch = value; + } + + public get placeholderSearch() { + return ( + this._placeholderSearch ?? + this.resourceStrings.combo_filter_search_placeholder ?? + 'Search' + ); + } + + private _placeholderSearch: string | undefined; /** * Sets the open state of the component. @@ -285,6 +322,18 @@ export default class IgcComboComponent< @property({ type: Boolean, reflect: true }) public open = false; + /** + * The resource strings for localization. + */ + @property({ attribute: false }) + public set resourceStrings(value: IComboResourceStrings) { + this._i18nController.resourceStrings = value; + } + + public get resourceStrings(): IComboResourceStrings { + return this._i18nController.resourceStrings; + } + /** * The key in the data source used when selecting items. * @attr value-key @@ -967,7 +1016,7 @@ export default class IgcComboComponent< private renderEmptyTemplate() { return html`
- The list is empty + ${this.resourceStrings.combo_empty_message}
`; } diff --git a/src/components/combo/validators.ts b/src/components/combo/validators.ts index 7a1c3f863..1e4f065be 100644 --- a/src/components/combo/validators.ts +++ b/src/components/combo/validators.ts @@ -1,11 +1,11 @@ -import messages from '../common/localization/validation-en.js'; +import { validationResourcesKeys } from '../common/i18n/utils.js'; import type { Validator } from '../common/validators.js'; import type IgcComboComponent from './combo.js'; export const comboValidators: Validator[] = [ { key: 'valueMissing', - message: messages.required, + messageResourceKey: validationResourcesKeys.required, isValid: ({ required, value }) => required ? Array.isArray(value) && value.length > 0 : true, }, diff --git a/src/components/common/i18n/calendar.resources.ts b/src/components/common/i18n/EN/calendar.resources.ts similarity index 51% rename from src/components/common/i18n/calendar.resources.ts rename to src/components/common/i18n/EN/calendar.resources.ts index 42b234033..37553a0e4 100644 --- a/src/components/common/i18n/calendar.resources.ts +++ b/src/components/common/i18n/EN/calendar.resources.ts @@ -1,3 +1,6 @@ +import { CalendarResourceStringsEN } from 'igniteui-i18n-core'; +import { convertToIgcResource } from '../utils.js'; + /* blazorSuppress */ export interface IgcCalendarResourceStrings { selectMonth: string; @@ -16,19 +19,5 @@ export interface IgcCalendarResourceStrings { weekLabel: string; } -export const IgcCalendarResourceStringEN: IgcCalendarResourceStrings = { - selectMonth: 'Select month', - selectYear: 'Select year', - selectDate: 'Select date', - selectRange: 'Select range', - selectedDate: 'Selected date', - startDate: 'Start', - endDate: 'End', - previousMonth: 'Previous month', - nextMonth: 'Next month', - previousYear: 'Previous year', - nextYear: 'Next year', - previousYears: 'Previous {0} years', - nextYears: 'Next {0} years', - weekLabel: 'Wk', -}; +export const IgcCalendarResourceStringEN: IgcCalendarResourceStrings = + convertToIgcResource(CalendarResourceStringsEN); diff --git a/src/components/common/i18n/chat.resources.ts b/src/components/common/i18n/EN/chat.resources.ts similarity index 100% rename from src/components/common/i18n/chat.resources.ts rename to src/components/common/i18n/EN/chat.resources.ts diff --git a/src/components/common/i18n/date-range-picker.resources.ts b/src/components/common/i18n/EN/date-range-picker.resources.ts similarity index 72% rename from src/components/common/i18n/date-range-picker.resources.ts rename to src/components/common/i18n/EN/date-range-picker.resources.ts index 01781e21a..e94d28326 100644 --- a/src/components/common/i18n/date-range-picker.resources.ts +++ b/src/components/common/i18n/EN/date-range-picker.resources.ts @@ -1,3 +1,5 @@ +import { DateRangePickerResourceStringsEN } from 'igniteui-i18n-core'; +import { convertToIgcResource } from '../utils.js'; import { IgcCalendarResourceStringEN, type IgcCalendarResourceStrings, @@ -17,12 +19,6 @@ export interface IgcDateRangePickerResourceStrings export const IgcDateRangePickerResourceStringsEN: IgcDateRangePickerResourceStrings = { - separator: 'to', - done: 'Done', - cancel: 'Cancel', - last7Days: 'Last 7 days', - last30Days: 'Last 30 days', - currentMonth: 'Current month', - yearToDate: 'Year to date', + ...convertToIgcResource(DateRangePickerResourceStringsEN), ...IgcCalendarResourceStringEN, }; diff --git a/src/components/common/i18n/i18n-controller.ts b/src/components/common/i18n/i18n-controller.ts new file mode 100644 index 000000000..89bc39882 --- /dev/null +++ b/src/components/common/i18n/i18n-controller.ts @@ -0,0 +1,214 @@ +import { + getCurrentI18n, + getDisplayNamesFormatter, + getI18nManager, + type IResourceChangeEventArgs, + type IResourceStrings, +} from 'igniteui-i18n-core'; +import type { ReactiveController, ReactiveControllerHost } from 'lit'; +import { + calendarResourcesMap, + convertToCoreResource, + dateRangePickerResourcesMap, +} from './utils.js'; + +/** + * Defines the structure for the host element that will use this controller. + * The host must be a Lit element (ReactiveControllerHost) and an HTMLElement. + */ +interface I18nControllerHost extends ReactiveControllerHost, HTMLElement { + // Properties the host is expected to have/use, though they are managed by the controller. + resourceStrings?: unknown; + locale?: string; +} + +type ResourceChangeCallback = ( + event: CustomEvent +) => unknown; + +/** Configuration object for the I18nController. */ +type I18nControllerConfig = { + /** The full default English resource strings object for the component. */ + defaultEN: T; + /** An optional callback to execute when the global locale changes. */ + onResourceChange?: ResourceChangeCallback; +}; + +/** + * Manages localization (i18n) for a Lit web component. + * It handles the current locale, component-specific resource overrides, + * and updates when the global localization state changes. + */ +class I18nController implements ReactiveController { + //#region Internal properties and state + + private readonly _host: I18nControllerHost; + private readonly _defaultEN: T; + + private _resourceChangeCallback?: ResourceChangeCallback; + private _defaultResourceStrings: T; + private _locale?: string; + private _resourceStrings?: T; + + //#endregion + + //#region Public properties + + /** + * Sets a custom locale that overrides the global one for this host component instance. + * Setting a new locale triggers an update of the resource strings. + */ + public set locale(value: string | undefined) { + if (this._locale !== value) { + this._locale = value; + this._defaultResourceStrings = this._getCurrentResourceStrings(); + this._host.requestUpdate(); + } + } + + /** + * Gets the resolved locale for the host component. + * This is the component's custom locale if set, otherwise it falls back to the + * global locale. + */ + public get locale(): string { + return this._locale ?? getCurrentI18n(); + } + + /** + * Sets custom resource string for component with this controller. + * Gets the resolved resource string for component. + */ + public set resourceStrings(value: T | undefined) { + if (this._resourceStrings !== value) { + this._resourceStrings = value; + this._host.requestUpdate(); + } + } + + /** Get resolved resource strings for component */ + public get resourceStrings(): T { + return this._resourceStrings ?? this._defaultResourceStrings; + } + + //#endregion + + //#region Life-cycle hooks and event listener + + constructor(host: I18nControllerHost, config: I18nControllerConfig) { + this._host = host; + this._defaultEN = config.defaultEN; + this._resourceChangeCallback = config.onResourceChange; + + this._defaultResourceStrings = this._getCurrentResourceStrings(); + this._registerResources(this._defaultEN); + + this._host.addController(this); + } + + /** @internal */ + public hostConnected(): void { + getI18nManager().addEventListener('onResourceChange', this); + } + + /** @internal */ + public hostDisconnected(): void { + getI18nManager().removeEventListener('onResourceChange', this); + } + + /** @internal */ + public handleEvent(event: CustomEvent): void { + this._defaultResourceStrings = this._getCurrentResourceStrings(); + this._resourceChangeCallback?.(event); + this._host.requestUpdate(); + } + + //#endregion + + //#region Internal API + + /** Registers the default English resources with the global i18n manager. */ + private _registerResources(resource: T): void { + const convertedResource = convertToCoreResource(resource); + const manager = getI18nManager(); + + manager.registerI18n(convertedResource, manager.defaultLocale); + } + + /** + * Helper to find the correct resource map based on the component's default resources (`#defaultEN`). + * This relies on structural checking (the component's key names). + */ + private _getResourceMapForComponent(): + | Map + | undefined { + const keys = Object.keys(this._defaultEN); + + if (keys.includes('last7Days')) { + return dateRangePickerResourcesMap; + } + + if (keys.includes('selectMonth')) { + return calendarResourcesMap; + } + + return undefined; + } + + /** + * Gets the current, locale-specific resource strings for the component. + * The logic maps component keys (from defaultEN) to core library keys + * and retrieves the localized string from the i18n manager. + * + * Result is truncated, containing only relevant locale strings. + */ + private _getCurrentResourceStrings(): T { + const coreResourceStrings = getI18nManager().getCurrentResourceStrings( + this.locale + ); + const resourceMap = this._getResourceMapForComponent(); + + if (!resourceMap) { + return coreResourceStrings as T; + } + + const normalizedResourceStrings: T = {} as T; + const defaultComponentKeys = Object.keys(this._defaultEN) as (keyof T)[]; + + for (const igcKey of defaultComponentKeys) { + const coreKey = resourceMap.get(igcKey as string); + let resolvedValue: T[keyof T] = this._defaultEN[igcKey]; + + if (coreKey) { + if (coreKey.includes('getWeekLabel')) { + resolvedValue = getDisplayNamesFormatter().getWeekLabel(this.locale, { + style: 'short', + }) as T[keyof T]; + } else { + resolvedValue = + coreKey in coreResourceStrings + ? (coreResourceStrings[ + coreKey as keyof IResourceStrings + ] as T[keyof T]) + : this._defaultEN[igcKey]; + } + } + + normalizedResourceStrings[igcKey] = resolvedValue; + } + + return normalizedResourceStrings; + } + + //#endregion +} + +/** Factory function to create and attach the I18nController to a host. */ +export function addI18nController( + host: I18nControllerHost, + config: I18nControllerConfig +): I18nController { + return new I18nController(host, config); +} + +export type { I18nController }; diff --git a/src/components/common/i18n/i18n.spec.ts b/src/components/common/i18n/i18n.spec.ts new file mode 100644 index 000000000..727f3109c --- /dev/null +++ b/src/components/common/i18n/i18n.spec.ts @@ -0,0 +1,207 @@ +import { + defineCE, + elementUpdated, + expect, + fixture, + html, + unsafeStatic, +} from '@open-wc/testing'; +import { + ComboResourceStringsEN, + getI18nManager, + type IComboResourceStrings, + type IResourceStrings, + registerI18n, + setCurrentI18n, +} from 'igniteui-i18n-core'; +import { ResourceStringsBG } from 'igniteui-i18n-resources'; +import { LitElement } from 'lit'; +import { + type IgcDateRangePickerResourceStrings, + IgcDateRangePickerResourceStringsEN, +} from './EN/date-range-picker.resources.js'; +import { addI18nController, type I18nController } from './i18n-controller.js'; + +class TestLocalizedClass extends LitElement { + public set locale(value: string) { + this.i18nController.locale = value; + } + public get locale() { + return this.i18nController.locale; + } + + public set resourceStrings(value: T) { + this.i18nController.resourceStrings = value; + } + + public get resourceStrings(): T { + return this.i18nController.resourceStrings; + } + + public readonly i18nController = addI18nController(this, { + defaultEN: this.defaultEN, + }); + + public get defaultEN(): T { + return {} as T; + } +} + +describe('Localization', () => { + let tagOld: string; + let tagNew: string; + let instance: LitElement & { + locale: string; + resourceStrings: object; + i18nController: I18nController; + }; + + before(() => { + tagOld = defineCE( + class extends TestLocalizedClass { + public override get defaultEN() { + return IgcDateRangePickerResourceStringsEN; + } + + protected override render() { + return html` +
+ ${this.resourceStrings.selectDate} +
+ + `; + } + } + ); + + tagNew = defineCE( + class extends TestLocalizedClass { + public override get defaultEN() { + return ComboResourceStringsEN; + } + + protected override render() { + return html` +
+ ${this.resourceStrings.combo_empty_message} +
+ `; + } + } + ); + }); + + describe('Old resource strings format compatibility', () => { + beforeEach(async () => { + const tagName = unsafeStatic(tagOld); + instance = await fixture(html`<${tagName}>([[getI18nManager().defaultLang, {}]]); + }); + + it('should initialize correct resource strings', () => { + expect(instance.shadowRoot?.getElementById('select')?.innerText).to.equal( + 'Select Date' + ); + expect( + instance.shadowRoot?.getElementById('previous')?.innerText + ).to.equal('Previous {0} Years'); + }); + + it('should update the resource string when they are explicitly set', async () => { + instance.resourceStrings = { + selectDate: 'Избор на дата', + previousYears: 'Предходни {0} години', + }; + instance.requestUpdate(); + await elementUpdated(instance); + + expect(instance.shadowRoot?.getElementById('select')?.innerText).to.equal( + 'Избор на дата' + ); + expect( + instance.shadowRoot?.getElementById('previous')?.innerText + ).to.equal('Предходни {0} години'); + }); + + it('should set custom locale and stay that even when locale is changed globally', async () => { + setCurrentI18n('de'); + + instance.locale = 'bg'; + instance.requestUpdate(); + await elementUpdated(instance); + + expect(instance.locale).to.equal('bg'); + }); + + it('should convert to old resource names when resource strings are set globally from new API', async () => { + registerI18n(ResourceStringsBG, 'bg'); + + instance.locale = 'bg'; + instance.requestUpdate(); + await elementUpdated(instance); + + expect(instance.shadowRoot?.getElementById('select')?.innerText).to.equal( + 'Избор на дата' + ); + expect( + instance.shadowRoot?.getElementById('previous')?.innerText + ).to.equal('Предходни {0} години'); + }); + }); + + describe('New resource strings format', () => { + beforeEach(async () => { + const tagName = unsafeStatic(tagNew); + instance = await fixture(html`<${tagName}>([[getI18nManager().defaultLang, {}]]); + }); + + it('should initialize correct resource strings', () => { + expect(instance.shadowRoot?.getElementById('start')?.innerText).to.equal( + 'The list is empty' + ); + }); + + it('should update the resource string when they are explicitly set', async () => { + instance.resourceStrings = { + combo_empty_message: 'Списъкът e празен', + }; + instance.requestUpdate(); + await elementUpdated(instance); + + expect(instance.shadowRoot?.getElementById('start')?.innerText).to.equal( + 'Списъкът e празен' + ); + }); + + it('should set custom locale and stay that even when locale is changed globally', async () => { + setCurrentI18n('de'); + + instance.locale = 'bg'; + instance.requestUpdate(); + await elementUpdated(instance); + + expect(instance.locale).to.equal('bg'); + }); + + it('should update resource strings when are set globally', async () => { + registerI18n(ResourceStringsBG, 'bg'); + + instance.locale = 'bg'; + instance.requestUpdate(); + await elementUpdated(instance); + + expect(instance.shadowRoot?.getElementById('start')?.innerText).to.equal( + 'Списъкът е празен' + ); + }); + }); +}); diff --git a/src/components/common/i18n/utils.ts b/src/components/common/i18n/utils.ts new file mode 100644 index 000000000..55f916c52 --- /dev/null +++ b/src/components/common/i18n/utils.ts @@ -0,0 +1,143 @@ +import type { IResourceStrings } from 'igniteui-i18n-core'; +import { isObject, isString } from '../util.js'; +import type { IgcCalendarResourceStrings } from './EN/calendar.resources.js'; +import type { IgcDateRangePickerResourceStrings } from './EN/date-range-picker.resources.js'; + +export const validationResourcesKeys = { + minLength: 'min_length_validation_error', + maxLength: 'max_length_validation_error', + required: 'required_validation_error', + pattern: 'pattern_validation_error', + mask: 'mask_validation_error', + min: 'min_validation_error', + max: 'max_validation_error', + email: 'email_validation_error', + url: 'url_validation_error', + disabledDate: 'disabled_date_validation_error', +} as const; + +export const calendarResourcesMap = new Map< + keyof IgcCalendarResourceStrings, + string | undefined +>([ + ['selectMonth', 'calendar_select_month'], + ['selectYear', 'calendar_select_year'], + ['selectDate', 'calendar_select_date'], + ['selectRange', 'calendar_range_placeholder'], + ['selectedDate', undefined], // This one seems not to be in use anyway + ['startDate', 'calendar_range_label_start'], + ['endDate', 'calendar_range_label_end'], + ['previousMonth', 'calendar_previous_month'], + ['nextMonth', 'calendar_next_month'], + ['previousYear', 'calendar_previous_year'], + ['nextYear', 'calendar_next_year'], + ['previousYears', 'calendar_previous_years'], + ['nextYears', 'calendar_next_years'], + ['weekLabel', 'i18n/getWeekLabel'], +]); + +export const dateRangePickerResourcesMap = new Map< + keyof IgcDateRangePickerResourceStrings, + string | undefined +>([ + ['separator', 'date_range_picker_date_separator'], + ['done', 'date_range_picker_done_button'], + ['cancel', 'date_range_picker_cancel_button'], + ['last7Days', 'date_range_picker_last7Days'], + ['last30Days', 'date_range_picker_last30Days'], + ['currentMonth', 'date_range_picker_currentMonth'], + ['yearToDate', 'date_range_picker_yearToDate'], + ...( + calendarResourcesMap as Map< + keyof IgcDateRangePickerResourceStrings, + string | undefined + > + ).entries(), +]); + +function isCalendarResource( + resource: unknown +): resource is IgcCalendarResourceStrings { + return ( + isObject(resource) && + 'selectMonth' in resource && + !isDateRangePickerResource(resource) + ); +} + +function isDateRangePickerResource( + resource: unknown +): resource is IgcDateRangePickerResourceStrings { + return isObject(resource) && 'last7Days' in resource; +} + +function getResourceMap( + resource: T +): Map | undefined { + if (isCalendarResource(resource)) { + return calendarResourcesMap; + } + + if (isDateRangePickerResource(resource)) { + return dateRangePickerResourcesMap; + } + + return undefined; +} + +function getResourceMapForCore( + resource: T +): Map | undefined { + if ('date_range_picker_last7Days' in resource) { + return dateRangePickerResourcesMap; + } + + if ('calendar_select_month' in resource) { + return calendarResourcesMap; + } + + return undefined; +} + +export function convertToIgcResource( + resource: IResourceStrings +): T { + const result = {} as T; + const resourceMap = getResourceMapForCore(resource); + + if (!resourceMap) { + return resource as T; + } + + for (const [componentKey, coreKey] of resourceMap) { + if (coreKey && coreKey in resource) { + const coreValue = resource[coreKey as keyof IResourceStrings]; + + if (isString(coreValue)) { + result[componentKey as keyof T] = coreValue as T[keyof T]; + } + } + } + + return result; +} + +export function convertToCoreResource(resource: T): IResourceStrings { + const result: IResourceStrings = {}; + const resourceMap = getResourceMap(resource); + + if (resourceMap) { + for (const [key, coreKey] of resourceMap) { + if (coreKey) { + const value = resource[key as keyof T]; + if (isString(value)) { + result[coreKey as keyof IResourceStrings] = value; + } + } + } + } else { + return resource as IResourceStrings; + } + + return result; +} diff --git a/src/components/common/localization/validation-en.ts b/src/components/common/localization/validation-en.ts deleted file mode 100644 index d14e47352..000000000 --- a/src/components/common/localization/validation-en.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default { - minLength: 'Entry should be at least {0} character(s) long', - maxLength: 'Entry should be no more than {0} character(s) long', - required: 'This field is required', - pattern: 'Entry does not match the required pattern', - mask: 'All required positions should be filled', - min: 'A value of at least {0} should be entered', - max: 'A value no more than {0} should be entered', - email: 'A valid email address should be entered', - url: 'A valid url address should be entered', - disabledDate: 'The entered value {0} is within the disabled dates range', -} as const; diff --git a/src/components/common/mixins/form-associated.spec.ts b/src/components/common/mixins/form-associated.spec.ts index 55b4ba333..fded52c85 100644 --- a/src/components/common/mixins/form-associated.spec.ts +++ b/src/components/common/mixins/form-associated.spec.ts @@ -181,7 +181,7 @@ describe('Form associated mixin tests', () => { expect(instance.checkValidity()).to.be.false; expect(hasValidityFlags(instance, 'valueMissing')).to.be.true; - expect(instance.validationMessage).to.equal(requiredValidator.message); + expect(instance.validationMessage).to.equal('This field is required'); }); }); diff --git a/src/components/common/mixins/forms/associated.ts b/src/components/common/mixins/forms/associated.ts index 4abbf1429..ad76bddcf 100644 --- a/src/components/common/mixins/forms/associated.ts +++ b/src/components/common/mixins/forms/associated.ts @@ -1,6 +1,11 @@ +import { + type IValidationResourceStrings, + ValidationResourceStringsEN, +} from 'igniteui-i18n-core'; import type { LitElement } from 'lit'; import { property } from 'lit/decorators.js'; import { addInternalsController } from '../../controllers/internals.js'; +import { addI18nController } from '../../i18n/i18n-controller.js'; import { addSafeEventListener, isFunction, isString } from '../../util.js'; import type { Validator } from '../../validators.js'; import type { Constructor } from '../constructor.js'; @@ -36,6 +41,10 @@ function BaseFormAssociated>(base: T) { //#region Internal state and properties private readonly __internals = addInternalsController(this); + private readonly __i18nController = + addI18nController(this, { + defaultEN: ValidationResourceStringsEN, + }); protected readonly _formValue!: FormValue; private _isFormSubmit = false; @@ -132,6 +141,17 @@ function BaseFormAssociated>(base: T) { return this.__internals.willValidate; } + /** + * The resource strings for localization. + */ + @property({ attribute: false }) + public set resourceStrings(value: IValidationResourceStrings) { + this.__i18nController.resourceStrings = value; + } + + public get resourceStrings(): IValidationResourceStrings { + return this.__i18nController.resourceStrings; + } //#endregion //#region Life-cycle hooks @@ -189,9 +209,18 @@ function BaseFormAssociated>(base: T) { if (!isValid) { validationFailed = true; - message = isFunction(validator.message) - ? validator.message(this) - : validator.message; + const resourceKey = ( + isFunction(validator.messageResourceKey) + ? validator.messageResourceKey(this) + : validator.messageResourceKey + ) as keyof IValidationResourceStrings; + + message = + this.resourceStrings[resourceKey] ?? + "Couldn't retrieve validation resource string!"; + if (isFunction(validator.messageFormat)) { + message = validator.messageFormat(message, this); + } } } diff --git a/src/components/common/validators.ts b/src/components/common/validators.ts index beaf2ca86..122966aae 100644 --- a/src/components/common/validators.ts +++ b/src/components/common/validators.ts @@ -1,5 +1,5 @@ import { CalendarDay } from '../calendar/model.js'; -import validatorMessages from './localization/validation-en.js'; +import { validationResourcesKeys } from './i18n/utils.js'; import { asNumber, formatString, @@ -9,12 +9,14 @@ import { } from './util.js'; type ValidatorHandler = (host: T) => boolean; -type ValidatorMessageFormat = (host: T) => string; +type ValidatorMessageKeyFormat = (host: T) => string; +type ValidatorMessageFormat = (message: string, host: T) => string; /** @ignore */ export interface Validator { key: keyof ValidityStateFlags; - message: string | ValidatorMessageFormat; + messageResourceKey: string | ValidatorMessageKeyFormat; + messageFormat?: ValidatorMessageFormat; isValid: ValidatorHandler; } @@ -26,7 +28,7 @@ export const requiredValidator: Validator<{ value?: unknown; }> = { key: 'valueMissing', - message: validatorMessages.required, + messageResourceKey: validationResourcesKeys.required, isValid: ({ required, value }) => (required ? !!value : true), }; @@ -35,7 +37,7 @@ export const requiredBooleanValidator: Validator<{ checked: boolean; }> = { key: 'valueMissing', - message: validatorMessages.required, + messageResourceKey: validationResourcesKeys.required, isValid: ({ required, checked }) => (required ? checked : true), }; @@ -44,8 +46,8 @@ export const minLengthValidator: Validator<{ value: string; }> = { key: 'tooShort', - message: ({ minLength }) => - formatString(validatorMessages.minLength, minLength), + messageResourceKey: validationResourcesKeys.minLength, + messageFormat: (message, { minLength }) => formatString(message, minLength), isValid: ({ minLength, value }) => minLength && value ? value.length >= asNumber(minLength) : true, }; @@ -55,8 +57,8 @@ export const maxLengthValidator: Validator<{ value: string; }> = { key: 'tooLong', - message: ({ maxLength }) => - formatString(validatorMessages.maxLength, maxLength), + messageResourceKey: validationResourcesKeys.maxLength, + messageFormat: (message, { maxLength }) => formatString(message, maxLength), isValid: ({ maxLength, value }) => maxLength && value ? value.length <= asNumber(maxLength) : true, }; @@ -64,7 +66,7 @@ export const maxLengthValidator: Validator<{ export const patternValidator: Validator<{ pattern?: string; value: string }> = { key: 'patternMismatch', - message: validatorMessages.pattern, + messageResourceKey: validationResourcesKeys.pattern, isValid: ({ pattern, value }) => pattern && value ? new RegExp(pattern, 'u').test(value) : true, }; @@ -74,7 +76,8 @@ export const minValidator: Validator<{ value: number | string; }> = { key: 'rangeUnderflow', - message: ({ min }) => formatString(validatorMessages.min, min), + messageResourceKey: validationResourcesKeys.min, + messageFormat: (message, { min }) => formatString(message, min), isValid: ({ min, value }) => isDefined(value) && value !== '' && isDefined(min) ? asNumber(value) >= asNumber(min) @@ -86,7 +89,8 @@ export const maxValidator: Validator<{ value: number | string; }> = { key: 'rangeOverflow', - message: ({ max }) => formatString(validatorMessages.max, max), + messageResourceKey: validationResourcesKeys.max, + messageFormat: (message, { max }) => formatString(message, max), isValid: ({ max, value }) => isDefined(value) && value !== '' && isDefined(max) ? asNumber(value) <= asNumber(max) @@ -99,7 +103,7 @@ export const stepValidator: Validator<{ value: number | string; }> = { key: 'stepMismatch', - message: 'Value does not conform to step constraint', + messageResourceKey: 'Value does not conform to step constraint', isValid: ({ min, step, value }) => { if (isDefined(value) && value !== '' && isDefined(step)) { const _value = asNumber(value) - asNumber(min); @@ -118,13 +122,13 @@ export const stepValidator: Validator<{ export const emailValidator: Validator<{ value: string }> = { key: 'typeMismatch', - message: validatorMessages.email, + messageResourceKey: validationResourcesKeys.email, isValid: ({ value }) => (value ? emailRegex.test(value) : true), }; export const urlValidator: Validator<{ value: string }> = { key: 'typeMismatch', - message: validatorMessages.url, + messageResourceKey: validationResourcesKeys.url, isValid: ({ value }) => (value ? URL.canParse(value) : true), }; @@ -133,7 +137,8 @@ export const minDateValidator: Validator<{ min?: Date | null; }> = { key: 'rangeUnderflow', - message: ({ min }) => formatString(validatorMessages.min, min), + messageResourceKey: validationResourcesKeys.min, + messageFormat: (message, { min }) => formatString(message, min), isValid: ({ value, min }) => value && min ? CalendarDay.compare(value, min) >= 0 : true, }; @@ -143,7 +148,8 @@ export const maxDateValidator: Validator<{ max?: Date | null; }> = { key: 'rangeOverflow', - message: ({ max }) => formatString(validatorMessages.max, max), + messageResourceKey: validationResourcesKeys.max, + messageFormat: (message, { max }) => formatString(message, max), isValid: ({ value, max }) => value && max ? CalendarDay.compare(value, max) <= 0 : true, }; diff --git a/src/components/date-picker/date-picker.spec.ts b/src/components/date-picker/date-picker.spec.ts index 595bce81c..fd5954a19 100644 --- a/src/components/date-picker/date-picker.spec.ts +++ b/src/components/date-picker/date-picker.spec.ts @@ -569,7 +569,7 @@ describe('Date picker', () => { it('should default inputFormat to whatever Intl.DateTimeFormat returns for the current locale', async () => { const defaultFormat = 'MM/dd/yyyy'; - expect(picker.locale).to.equal('en'); + expect(picker.locale).to.equal('en-US'); expect(picker.inputFormat).to.equal(defaultFormat); picker.locale = 'fr'; @@ -579,7 +579,7 @@ describe('Date picker', () => { }); it('should use the value of inputFormat for displayFormat, if it is not defined', async () => { - expect(picker.locale).to.equal('en'); + expect(picker.locale).to.equal('en-US'); expect(picker.getAttribute('display-format')).to.be.null; expect(picker.displayFormat).to.equal(picker.inputFormat); diff --git a/src/components/date-picker/date-picker.ts b/src/components/date-picker/date-picker.ts index 55c27ce6b..ca63242ee 100644 --- a/src/components/date-picker/date-picker.ts +++ b/src/components/date-picker/date-picker.ts @@ -25,7 +25,8 @@ import { registerComponent } from '../common/definitions/register.js'; import { IgcCalendarResourceStringEN, type IgcCalendarResourceStrings, -} from '../common/i18n/calendar.resources.js'; +} from '../common/i18n/EN/calendar.resources.js'; +import { addI18nController } from '../common/i18n/i18n-controller.js'; import { IgcBaseComboBoxLikeComponent } from '../common/mixins/combo-box.js'; import type { AbstractConstructor } from '../common/mixins/constructor.js'; import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; @@ -200,6 +201,11 @@ export default class IgcDatePickerComponent extends FormAssociatedRequiredMixin( private readonly _slots = addSlotController(this, { slots: Slots }); private _oldValue: Date | null = null; + private readonly _i18nController = + addI18nController(this, { + defaultEN: IgcCalendarResourceStringEN, + }); + private _activeDate: Date | null = null; private _min: Date | null = null; private _max: Date | null = null; @@ -430,23 +436,36 @@ export default class IgcDatePickerComponent extends FormAssociatedRequiredMixin( return this._inputFormat ?? this._input?.inputFormat; } - /** - * The locale settings used to display the value. - * @attr - */ - @property() - public locale = 'en'; - /** The prompt symbol to use for unfilled parts of the mask. * @attr */ @property() public prompt = '_'; - /** The resource strings of the calendar. */ + /** + * Gets/Sets the locale used for formatting the display value. + * @attr locale + */ + @property() + public set locale(value: string) { + this._i18nController.locale = value; + } + + public get locale() { + return this._i18nController.locale; + } + + /** + * The resource strings for localization. + */ @property({ attribute: false }) - public resourceStrings: IgcCalendarResourceStrings = - IgcCalendarResourceStringEN; + public set resourceStrings(value: IgcCalendarResourceStrings) { + this._i18nController.resourceStrings = value; + } + + public get resourceStrings(): IgcCalendarResourceStrings { + return this._i18nController.resourceStrings; + } /** Sets the start day of the week for the calendar. */ @property({ attribute: 'week-start' }) diff --git a/src/components/date-picker/validators.ts b/src/components/date-picker/validators.ts index ffbf67f1b..f2bad0021 100644 --- a/src/components/date-picker/validators.ts +++ b/src/components/date-picker/validators.ts @@ -1,5 +1,5 @@ import { isDateInRanges } from '../calendar/helpers.js'; -import messages from '../common/localization/validation-en.js'; +import { validationResourcesKeys } from '../common/i18n/utils.js'; import { formatString } from '../common/util.js'; import { maxDateValidator, @@ -15,7 +15,8 @@ export const datePickerValidators: Validator[] = [ maxDateValidator, { key: 'badInput', - message: ({ value }) => formatString(messages.disabledDate, value), + messageResourceKey: validationResourcesKeys.disabledDate, + messageFormat: (message, { value }) => formatString(message, value), isValid: ({ value, disabledDates }) => value && disabledDates ? !isDateInRanges(value, disabledDates) : true, }, diff --git a/src/components/date-range-picker/date-range-picker.common.spec.ts b/src/components/date-range-picker/date-range-picker.common.spec.ts index adf8ad348..e40360bdb 100644 --- a/src/components/date-range-picker/date-range-picker.common.spec.ts +++ b/src/components/date-range-picker/date-range-picker.common.spec.ts @@ -13,7 +13,7 @@ import { escapeKey, } from '../common/controllers/key-bindings.js'; import { defineComponents } from '../common/definitions/defineComponents.js'; -import type { IgcDateRangePickerResourceStrings } from '../common/i18n/date-range-picker.resources.js'; +import type { IgcDateRangePickerResourceStrings } from '../common/i18n/EN/date-range-picker.resources.js'; import { checkDatesEqual, simulateClick, @@ -307,7 +307,7 @@ describe('Date range picker - common tests for single and two inputs mode', () = it('should default inputFormat to whatever Intl.DateTimeFormat returns for the current locale', async () => { const defaultFormat = 'MM/dd/yyyy'; - expect(picker.locale).to.equal('en'); + expect(picker.locale).to.equal('en-US'); expect(picker.inputFormat).to.equal(defaultFormat); picker.locale = 'fr'; @@ -317,7 +317,7 @@ describe('Date range picker - common tests for single and two inputs mode', () = }); it('should use the value of inputFormat for displayFormat, if it is not defined', async () => { - expect(picker.locale).to.equal('en'); + expect(picker.locale).to.equal('en-US'); expect(picker.getAttribute('display-format')).to.be.null; expect(picker.displayFormat).to.equal(picker.inputFormat); diff --git a/src/components/date-range-picker/date-range-picker.ts b/src/components/date-range-picker/date-range-picker.ts index 33ba01e97..2c7a1042b 100644 --- a/src/components/date-range-picker/date-range-picker.ts +++ b/src/components/date-range-picker/date-range-picker.ts @@ -30,7 +30,11 @@ import { blazorAdditionalDependencies } from '../common/decorators/blazorAdditio import { shadowOptions } from '../common/decorators/shadow-options.js'; import { watch } from '../common/decorators/watch.js'; import { registerComponent } from '../common/definitions/register.js'; -import { IgcDateRangePickerResourceStringsEN } from '../common/i18n/date-range-picker.resources.js'; +import { + type IgcDateRangePickerResourceStrings, + IgcDateRangePickerResourceStringsEN, +} from '../common/i18n/EN/date-range-picker.resources.js'; +import { addI18nController } from '../common/i18n/i18n-controller.js'; import { IgcBaseComboBoxLikeComponent } from '../common/mixins/combo-box.js'; import type { AbstractConstructor } from '../common/mixins/constructor.js'; import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; @@ -224,6 +228,11 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM } ); + private readonly _i18nController = + addI18nController(this, { + defaultEN: IgcDateRangePickerResourceStringsEN, + }); + private _activeDate: Date | null = null; private _min: Date | null = null; private _max: Date | null = null; @@ -334,11 +343,23 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM * @attr */ @property() - public locale = 'en'; + public set locale(value: string) { + this._i18nController.locale = value; + } + + public get locale() { + return this._i18nController.locale; + } /** The resource strings of the date range picker. */ @property({ attribute: false }) - public resourceStrings = IgcDateRangePickerResourceStringsEN; + public set resourceStrings(value: IgcDateRangePickerResourceStrings) { + this._i18nController.resourceStrings = value; + } + + public get resourceStrings(): IgcDateRangePickerResourceStrings { + return this._i18nController.resourceStrings; + } // #endregion diff --git a/src/components/date-range-picker/predefined-ranges-area.ts b/src/components/date-range-picker/predefined-ranges-area.ts index 8305f3fd5..f68803635 100644 --- a/src/components/date-range-picker/predefined-ranges-area.ts +++ b/src/components/date-range-picker/predefined-ranges-area.ts @@ -8,7 +8,8 @@ import { registerComponent } from '../common/definitions/register.js'; import { type IgcDateRangePickerResourceStrings, IgcDateRangePickerResourceStringsEN, -} from '../common/i18n/date-range-picker.resources.js'; +} from '../common/i18n/EN/date-range-picker.resources.js'; +import { addI18nController } from '../common/i18n/i18n-controller.js'; import type { CustomDateRange, DateRangeValue } from './date-range-picker.js'; import { styles } from './predefined-ranges-area.base.css.js'; import { all } from './themes/ranges-themes.js'; @@ -26,6 +27,11 @@ export default class IgcPredefinedRangesAreaComponent extends LitElement { public static readonly tagName = 'igc-predefined-ranges-area'; public static override styles = [styles, shared]; + private readonly _i18nController = + addI18nController(this, { + defaultEN: IgcDateRangePickerResourceStringsEN, + }); + /* blazorSuppress */ public static register(): void { registerComponent(IgcPredefinedRangesAreaComponent, IgcChipComponent); @@ -53,8 +59,13 @@ export default class IgcPredefinedRangesAreaComponent extends LitElement { /** The resource strings of the date range area component. */ @property({ attribute: false }) - public resourceStrings: IgcDateRangePickerResourceStrings = - IgcDateRangePickerResourceStringsEN; + public set resourceStrings(value: IgcDateRangePickerResourceStrings) { + this._i18nController.resourceStrings = value; + } + + public get resourceStrings(): IgcDateRangePickerResourceStrings { + return this._i18nController.resourceStrings; + } constructor() { super(); diff --git a/src/components/date-range-picker/validators.ts b/src/components/date-range-picker/validators.ts index 4e8dcb83d..b48e507ab 100644 --- a/src/components/date-range-picker/validators.ts +++ b/src/components/date-range-picker/validators.ts @@ -1,7 +1,7 @@ import { calendarRange, isDateInRanges } from '../calendar/helpers.js'; import { CalendarDay } from '../calendar/model.js'; import type { DateRangeDescriptor } from '../calendar/types.js'; -import messages from '../common/localization/validation-en.js'; +import { validationResourcesKeys } from '../common/i18n/utils.js'; import { formatString, isEmpty } from '../common/util.js'; import type { Validator } from '../common/validators.js'; import type IgcDateRangePickerComponent from './date-range-picker.js'; @@ -12,7 +12,8 @@ export const minDateRangeValidator: Validator<{ min?: Date | null; }> = { key: 'rangeUnderflow', - message: ({ min }) => formatString(messages.min, min), + messageResourceKey: validationResourcesKeys.min, + messageFormat: (message, { min }) => formatString(message, min), isValid: ({ value, min }) => { if (!min) { return true; @@ -31,7 +32,8 @@ export const maxDateRangeValidator: Validator<{ max?: Date | null; }> = { key: 'rangeOverflow', - message: ({ max }) => formatString(messages.max, max), + messageResourceKey: validationResourcesKeys.max, + messageFormat: (message, { max }) => formatString(message, max), isValid: ({ value, max }) => { if (!max) { return true; @@ -50,7 +52,7 @@ export const requiredDateRangeValidator: Validator<{ value: DateRangeValue | null; }> = { key: 'valueMissing', - message: messages.required, + messageResourceKey: validationResourcesKeys.required, isValid: ({ required, value }) => { return required ? isCompleteDateRange(value) : true; }, @@ -62,7 +64,8 @@ export const badInputDateRangeValidator: Validator<{ disabledDates?: DateRangeDescriptor[]; }> = { key: 'badInput', - message: ({ value }) => formatString(messages.disabledDate, value), + messageResourceKey: validationResourcesKeys.disabledDate, + messageFormat: (message, { value }) => formatString(message, value), isValid: ({ value, disabledDates }) => { if ( !isCompleteDateRange(value) || diff --git a/src/components/date-time-input/date-time-input.ts b/src/components/date-time-input/date-time-input.ts index 24c73383d..bf56c53ec 100644 --- a/src/components/date-time-input/date-time-input.ts +++ b/src/components/date-time-input/date-time-input.ts @@ -14,6 +14,7 @@ import { } from '../common/controllers/key-bindings.js'; import { watch } from '../common/decorators/watch.js'; import { registerComponent } from '../common/definitions/register.js'; +import { addI18nController } from '../common/i18n/i18n-controller.js'; import type { AbstractConstructor } from '../common/mixins/constructor.js'; import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; import { FormValueDateTimeTransformers } from '../common/mixins/forms/form-transformers.js'; @@ -87,6 +88,10 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< transformers: FormValueDateTimeTransformers, }); + private readonly _i18nController = addI18nController(this, { + defaultEN: {}, + }); + protected _defaultMask!: string; private _oldValue: Date | null = null; private _min: Date | null = null; @@ -188,11 +193,17 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< public spinLoop = true; /** - * The locale settings used to display the value. - * @attr + * Gets/Sets the locale used for formatting the display value. + * @attr locale */ @property() - public locale = 'en'; + public set locale(value: string) { + this._i18nController.locale = value; + } + + public get locale() { + return this._i18nController.locale; + } @watch('locale', { waitUntilFirstUpdate: true }) protected setDefaultMask(): void { diff --git a/src/components/input/validators.ts b/src/components/input/validators.ts index 84101e12b..8f163c273 100644 --- a/src/components/input/validators.ts +++ b/src/components/input/validators.ts @@ -29,10 +29,10 @@ export const stringValidators: Validator[] = [ return true; } }, - message: (host) => + messageResourceKey: (host) => (host.type === 'email' - ? emailValidator.message - : urlValidator.message) as string, + ? emailValidator.messageResourceKey + : urlValidator.messageResourceKey) as string, }, ]; diff --git a/src/components/mask-input/validators.ts b/src/components/mask-input/validators.ts index 599af3b13..dd3bffd0b 100644 --- a/src/components/mask-input/validators.ts +++ b/src/components/mask-input/validators.ts @@ -1,4 +1,4 @@ -import messages from '../common/localization/validation-en.js'; +import { validationResourcesKeys } from '../common/i18n/utils.js'; import { requiredValidator, type Validator } from '../common/validators.js'; import type IgcMaskInputComponent from './mask-input.js'; @@ -6,7 +6,7 @@ export const maskValidators: Validator[] = [ requiredValidator, { key: 'badInput', - message: messages.mask, + messageResourceKey: validationResourcesKeys.mask, // @ts-expect-error - protected access isValid: ({ _parser, _maskedValue }) => _parser.isValidString(_maskedValue), }, diff --git a/src/components/radio/validators.ts b/src/components/radio/validators.ts index a99744bb6..06414f3e6 100644 --- a/src/components/radio/validators.ts +++ b/src/components/radio/validators.ts @@ -1,4 +1,4 @@ -import messages from '../common/localization/validation-en.js'; +import { validationResourcesKeys } from '../common/i18n/utils.js'; import type { Validator } from '../common/validators.js'; import type IgcRadioComponent from './radio.js'; import { getGroup } from './utils.js'; @@ -6,7 +6,7 @@ import { getGroup } from './utils.js'; export const radioValidators: Validator[] = [ { key: 'valueMissing', - message: messages.required, + messageResourceKey: validationResourcesKeys.required, isValid: (host) => { const { radios, checked } = getGroup(host); return radios.some((radio) => radio.required) ? checked.length > 0 : true; diff --git a/src/index.ts b/src/index.ts index 01af701f4..324c4660f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -88,11 +88,29 @@ export { configureTheme } from './theming/config.js'; export { IgcCalendarResourceStringEN, type IgcCalendarResourceStrings, -} from './components/common/i18n/calendar.resources.js'; +} from './components/common/i18n/EN/calendar.resources.js'; export { IgcChatResourceStringEN, type IgcChatResourceStrings, -} from './components/common/i18n/chat.resources.js'; +} from './components/common/i18n/EN/chat.resources.js'; +export { + type ICalendarResourceStrings, + type ICarouselResourceStrings, + type IComboResourceStrings, + type IChipResourceStrings, + type IDateRangePickerResourceStrings, + type IValidationResourceStrings, + CalendarResourceStringsEN, + CarouselResourceStringsEN, + ComboResourceStringsEN, + ChipResourceStringsEN, + DateRangePickerResourceStringsEN, + ValidationResourceStringsEN, + registerI18n, + setCurrentI18n, + getCurrentI18n, + getCurrentResourceStrings, +} from 'igniteui-i18n-core'; // Event maps export type { IgcBannerComponentEventMap } from './components/banner/banner.js'; diff --git a/stories/calendar.stories.ts b/stories/calendar.stories.ts index f97a373db..01eff6a2c 100644 --- a/stories/calendar.stories.ts +++ b/stories/calendar.stories.ts @@ -112,7 +112,6 @@ const metadata: Meta = { description: 'Gets/Sets the locale used for formatting and displaying the dates in the component.', control: 'text', - table: { defaultValue: { summary: 'en' } }, }, }, args: { @@ -125,7 +124,6 @@ const metadata: Meta = { selection: 'single', showWeekNumbers: false, weekStart: 'sunday', - locale: 'en', }, }; diff --git a/stories/carousel.stories.ts b/stories/carousel.stories.ts index c8982e2f1..0003658a5 100644 --- a/stories/carousel.stories.ts +++ b/stories/carousel.stories.ts @@ -79,14 +79,12 @@ const metadata: Meta = { description: "The format used to set the aria-label on the carousel indicators.\nInstances of '{0}' will be replaced with the index of the corresponding slide.", control: 'text', - table: { defaultValue: { summary: 'Slide {0}' } }, }, slidesLabelFormat: { type: 'string', description: "The format used to set the aria-label on the carousel slides and the text displayed\nwhen the number of indicators is greater than tha maximum indicator count.\nInstances of '{0}' will be replaced with the index of the corresponding slide.\nInstances of '{1}' will be replaced with the total amount of slides.", control: 'text', - table: { defaultValue: { summary: '{0} of {1}' } }, }, interval: { type: 'number', @@ -108,6 +106,12 @@ const metadata: Meta = { control: { type: 'inline-radio' }, table: { defaultValue: { summary: 'slide' } }, }, + locale: { + type: 'string', + description: + 'Gets/Sets the locale used for formatting and displaying the dates in the component.', + control: 'text', + }, }, args: { disableLoop: false, @@ -116,8 +120,6 @@ const metadata: Meta = { hideIndicators: false, vertical: false, indicatorsOrientation: 'end', - indicatorsLabelFormat: 'Slide {0}', - slidesLabelFormat: '{0} of {1}', maximumIndicatorsCount: 10, animationType: 'slide', }, @@ -156,6 +158,8 @@ interface IgcCarouselArgs { maximumIndicatorsCount: number; /** The animation type. */ animationType: 'slide' | 'fade' | 'none'; + /** Gets/Sets the locale used for formatting and displaying the dates in the component. */ + locale: string; } type Story = StoryObj; diff --git a/stories/chip.stories.ts b/stories/chip.stories.ts index b5ad0d073..189c031de 100644 --- a/stories/chip.stories.ts +++ b/stories/chip.stories.ts @@ -51,6 +51,12 @@ const metadata: Meta = { options: ['primary', 'info', 'success', 'warning', 'danger'], control: { type: 'select' }, }, + locale: { + type: 'string', + description: + 'Gets/Sets the locale used for formatting and displaying the dates in the component.', + control: 'text', + }, }, args: { disabled: false, @@ -73,6 +79,8 @@ interface IgcChipArgs { selected: boolean; /** A property that sets the color variant of the chip component. */ variant: 'primary' | 'info' | 'success' | 'warning' | 'danger'; + /** Gets/Sets the locale used for formatting and displaying the dates in the component. */ + locale: string; } type Story = StoryObj; diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts index 918f472e6..890600a05 100644 --- a/stories/combo.stories.ts +++ b/stories/combo.stories.ts @@ -69,6 +69,12 @@ const metadata: Meta = { control: 'boolean', table: { defaultValue: { summary: 'false' } }, }, + locale: { + type: 'string', + description: + 'Gets/Sets the locale used for formatting and displaying the dates in the component.', + control: 'text', + }, label: { type: 'string', description: 'The label attribute of the control.', @@ -83,7 +89,6 @@ const metadata: Meta = { type: 'string', description: 'The placeholder attribute of the search input.', control: 'text', - table: { defaultValue: { summary: 'Search' } }, }, open: { type: 'boolean', @@ -159,7 +164,6 @@ const metadata: Meta = { singleSelect: false, autofocus: false, autofocusList: false, - placeholderSearch: 'Search', open: false, groupSorting: 'asc', caseSensitiveIcon: false, @@ -181,6 +185,8 @@ interface IgcComboArgs { autofocus: boolean; /** Focuses the list of options when the menu opens. */ autofocusList: boolean; + /** Gets/Sets the locale used for formatting and displaying the dates in the component. */ + locale: string; /** The label attribute of the control. */ label: string; /** The placeholder attribute of the control. */ diff --git a/stories/date-range-picker.stories.ts b/stories/date-range-picker.stories.ts index adc89be89..01c8f642a 100644 --- a/stories/date-range-picker.stories.ts +++ b/stories/date-range-picker.stories.ts @@ -71,7 +71,6 @@ const metadata: Meta = { type: 'string', description: 'The locale settings used to display the value.', control: 'text', - table: { defaultValue: { summary: 'en' } }, }, readOnly: { type: 'boolean', @@ -264,7 +263,6 @@ const metadata: Meta = { mode: 'dropdown', useTwoInputs: false, usePredefinedRanges: false, - locale: 'en', readOnly: false, nonEditable: false, outlined: false, diff --git a/stories/date-time-input.stories.ts b/stories/date-time-input.stories.ts index 67a5bd049..e2fc0e254 100644 --- a/stories/date-time-input.stories.ts +++ b/stories/date-time-input.stories.ts @@ -62,14 +62,22 @@ const metadata: Meta = { }, locale: { type: 'string', - description: 'The locale settings used to display the value.', + description: + 'Gets/Sets the locale used for formatting the display value.', control: 'text', - table: { defaultValue: { summary: 'en' } }, + }, + mask: { + type: 'string', + description: 'The masked pattern of the component.', + control: 'text', + table: { defaultValue: { summary: 'CCCCCCCCCC' } }, }, prompt: { type: 'string', - description: 'The prompt symbol to use for unfilled parts of the mask.', + description: + 'The prompt symbol to use for unfilled parts of the mask pattern.', control: 'text', + table: { defaultValue: { summary: '_' } }, }, required: { type: 'boolean', @@ -120,7 +128,8 @@ const metadata: Meta = { }, args: { spinLoop: true, - locale: 'en', + mask: 'CCCCCCCCCC', + prompt: '_', required: false, disabled: false, invalid: false, @@ -147,9 +156,11 @@ interface IgcDateTimeInputArgs { displayFormat: string; /** Sets whether to loop over the currently spun segment. */ spinLoop: boolean; - /** The locale settings used to display the value. */ + /** Gets/Sets the locale used for formatting the display value. */ locale: string; - /** The prompt symbol to use for unfilled parts of the mask. */ + /** The masked pattern of the component. */ + mask: string; + /** The prompt symbol to use for unfilled parts of the mask pattern. */ prompt: string; /** When set, makes the component a required field for validation. */ required: boolean; diff --git a/stories/mask-input.stories.ts b/stories/mask-input.stories.ts index 9687bf60f..c9d69a6b5 100644 --- a/stories/mask-input.stories.ts +++ b/stories/mask-input.stories.ts @@ -49,13 +49,16 @@ const metadata: Meta = { }, mask: { type: 'string', - description: 'The mask pattern to apply on the input.', + description: 'The masked pattern of the component.', control: 'text', + table: { defaultValue: { summary: 'CCCCCCCCCC' } }, }, prompt: { type: 'string', - description: 'The prompt symbol to use for unfilled parts of the mask.', + description: + 'The prompt symbol to use for unfilled parts of the mask pattern.', control: 'text', + table: { defaultValue: { summary: '_' } }, }, required: { type: 'boolean', @@ -106,6 +109,8 @@ const metadata: Meta = { }, args: { valueMode: 'raw', + mask: 'CCCCCCCCCC', + prompt: '_', required: false, disabled: false, invalid: false, @@ -130,9 +135,9 @@ interface IgcMaskInputArgs { * Regardless of the currently set `value-mode`, an empty value will return an empty string. */ value: string | Date; - /** The mask pattern to apply on the input. */ + /** The masked pattern of the component. */ mask: string; - /** The prompt symbol to use for unfilled parts of the mask. */ + /** The prompt symbol to use for unfilled parts of the mask pattern. */ prompt: string; /** When set, makes the component a required field for validation. */ required: boolean;