@voyantjs/promotions-ui 0.35.0 → 0.37.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.
@@ -0,0 +1,3 @@
1
+ import type { PromotionsUiMessages } from "./messages.js";
2
+ export declare const promotionsUiEn: PromotionsUiMessages;
3
+ //# sourceMappingURL=en.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAEzD,eAAO,MAAM,cAAc,EAAE,oBAgJ5B,CAAA"}
@@ -0,0 +1,144 @@
1
+ export const promotionsUiEn = {
2
+ common: {
3
+ cancel: "Cancel",
4
+ create: "Create",
5
+ saveChanges: "Save changes",
6
+ saving: "Saving...",
7
+ active: "Active",
8
+ discountTypeLabels: {
9
+ percentage: "Percentage",
10
+ fixed_amount: "Fixed amount",
11
+ },
12
+ applicationModeLabels: {
13
+ auto: "Auto-applied",
14
+ code: "Code-redeemed",
15
+ },
16
+ statusLabels: {
17
+ active: "Active",
18
+ scheduled: "Scheduled",
19
+ expired: "Expired",
20
+ archived: "Archived",
21
+ },
22
+ scopeKindLabels: {
23
+ global: "Global",
24
+ products: "Products",
25
+ categories: "Categories",
26
+ destinations: "Destinations",
27
+ markets: "Markets",
28
+ audiences: "Audiences",
29
+ },
30
+ audienceLabels: {
31
+ staff: "Staff",
32
+ customer: "Customer",
33
+ partner: "Partner",
34
+ supplier: "Supplier",
35
+ },
36
+ },
37
+ promotionsPage: {
38
+ title: "Promotions",
39
+ description: "Auto-applied catalog discounts and code-redeemed offers.",
40
+ newPromotion: "New promotion",
41
+ searchLabel: "Search promotions",
42
+ searchPlaceholder: "Search name, slug, description, or code",
43
+ modePlaceholder: "Mode",
44
+ allModes: "All modes",
45
+ statusPlaceholder: "Status",
46
+ allStatuses: "All statuses",
47
+ scopePlaceholder: "Scope",
48
+ allScopes: "All scopes",
49
+ validityRangePlaceholder: "Validity range",
50
+ clearFilters: "Clear",
51
+ loadFailed: "Failed to load promotions.",
52
+ loadFailedPrefix: "Failed to load: {message}",
53
+ empty: "No promotions match the current filters.",
54
+ columns: {
55
+ name: "Name",
56
+ mode: "Mode",
57
+ scope: "Scope",
58
+ discount: "Discount",
59
+ validity: "Validity",
60
+ code: "Code",
61
+ status: "Status",
62
+ },
63
+ badges: {
64
+ auto: "Auto",
65
+ code: "Code",
66
+ stackable: "Stackable",
67
+ },
68
+ pagination: {
69
+ showing: "Showing {shown} of {total}",
70
+ previous: "Previous",
71
+ next: "Next",
72
+ page: "Page {page} of {pageCount}",
73
+ },
74
+ summaries: {
75
+ globalScope: "Global",
76
+ productsScope: "{count} {noun}",
77
+ categoriesScope: "{count} {noun}",
78
+ destinationsScope: "{count} {noun}",
79
+ marketsScope: "Markets: {markets}",
80
+ audiencesScope: "Audiences: {audiences}",
81
+ productNouns: { singular: "product", plural: "products" },
82
+ categoryNouns: { singular: "category", plural: "categories" },
83
+ destinationNouns: { singular: "destination", plural: "destinations" },
84
+ unknownPercentage: "?",
85
+ anytime: "Anytime",
86
+ until: "Until {date}",
87
+ from: "From {date}",
88
+ range: "{from} - {until}",
89
+ noCode: "-",
90
+ },
91
+ },
92
+ promotionDialog: {
93
+ titles: {
94
+ create: "New promotion",
95
+ edit: "Edit promotion",
96
+ },
97
+ description: "Set discount, scope, and validity. Code-gated offers require a non-empty code; leave it blank for auto-applied offers.",
98
+ fields: {
99
+ name: "Name",
100
+ slug: "Slug",
101
+ description: "Description",
102
+ type: "Type",
103
+ percent: "Percent",
104
+ amount: "Amount",
105
+ currency: "Currency",
106
+ scope: "Scope",
107
+ scopeIds: "{scope}",
108
+ audiences: "Audiences",
109
+ validFrom: "Valid from",
110
+ validUntil: "Valid until",
111
+ code: "Code (optional)",
112
+ minPax: "Min pax (optional)",
113
+ stackable: "Stackable with other offers",
114
+ active: "Active",
115
+ },
116
+ placeholders: {
117
+ name: "Spring Sale 2026",
118
+ slug: "spring-sale-2026",
119
+ description: "Internal note: what this offer is for",
120
+ percent: "20",
121
+ amount: "5.00",
122
+ currency: "USD",
123
+ code: "EARLYBIRD2026",
124
+ minPax: "4",
125
+ productIds: "Add product references",
126
+ categoryIds: "Add category references",
127
+ destinationIds: "Add destination references",
128
+ marketIds: "Add market references",
129
+ },
130
+ hints: {
131
+ globalScope: "Applies to every product.",
132
+ commaSeparatedIds: "comma-separated references",
133
+ },
134
+ validation: {
135
+ nameRequired: "Name is required",
136
+ slugRequired: "Slug is required",
137
+ discountPercentRequired: "Discount percent is required for percentage offers",
138
+ discountAmountRequired: "Discount amount is required for fixed-amount offers",
139
+ currencyRequired: "Currency is required for fixed-amount offers",
140
+ scopeInvalid: "invalid",
141
+ scopeInvalidPrefix: "Scope: {message}",
142
+ },
143
+ },
144
+ };
@@ -0,0 +1,5 @@
1
+ export { promotionsUiEn } from "./en.js";
2
+ export type { PromotionsUiMessages } from "./messages.js";
3
+ export { getPromotionsUiI18n, type PromotionsUiMessageOverrides, PromotionsUiMessagesProvider, promotionsUiMessageDefinitions, resolvePromotionsUiMessages, usePromotionsUiI18n, usePromotionsUiI18nOrDefault, usePromotionsUiMessages, usePromotionsUiMessagesOrDefault, } from "./provider.js";
4
+ export { promotionsUiRo } from "./ro.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/i18n/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACxC,YAAY,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AACzD,OAAO,EACL,mBAAmB,EACnB,KAAK,4BAA4B,EACjC,4BAA4B,EAC5B,8BAA8B,EAC9B,2BAA2B,EAC3B,mBAAmB,EACnB,4BAA4B,EAC5B,uBAAuB,EACvB,gCAAgC,GACjC,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA"}
@@ -0,0 +1,3 @@
1
+ export { promotionsUiEn } from "./en.js";
2
+ export { getPromotionsUiI18n, PromotionsUiMessagesProvider, promotionsUiMessageDefinitions, resolvePromotionsUiMessages, usePromotionsUiI18n, usePromotionsUiI18nOrDefault, usePromotionsUiMessages, usePromotionsUiMessagesOrDefault, } from "./provider.js";
3
+ export { promotionsUiRo } from "./ro.js";
@@ -0,0 +1,132 @@
1
+ import type { PromotionalOfferApplicationMode, PromotionalOfferListStatus, PromotionalOfferScopeKind } from "@voyantjs/promotions-react";
2
+ export type PromotionsUiMessages = {
3
+ common: {
4
+ cancel: string;
5
+ create: string;
6
+ saveChanges: string;
7
+ saving: string;
8
+ active: string;
9
+ discountTypeLabels: Record<"percentage" | "fixed_amount", string>;
10
+ applicationModeLabels: Record<PromotionalOfferApplicationMode, string>;
11
+ statusLabels: Record<PromotionalOfferListStatus, string>;
12
+ scopeKindLabels: Record<PromotionalOfferScopeKind, string>;
13
+ audienceLabels: Record<"staff" | "customer" | "partner" | "supplier", string>;
14
+ };
15
+ promotionsPage: {
16
+ title: string;
17
+ description: string;
18
+ newPromotion: string;
19
+ searchLabel: string;
20
+ searchPlaceholder: string;
21
+ modePlaceholder: string;
22
+ allModes: string;
23
+ statusPlaceholder: string;
24
+ allStatuses: string;
25
+ scopePlaceholder: string;
26
+ allScopes: string;
27
+ validityRangePlaceholder: string;
28
+ clearFilters: string;
29
+ loadFailed: string;
30
+ loadFailedPrefix: string;
31
+ empty: string;
32
+ columns: {
33
+ name: string;
34
+ mode: string;
35
+ scope: string;
36
+ discount: string;
37
+ validity: string;
38
+ code: string;
39
+ status: string;
40
+ };
41
+ badges: {
42
+ auto: string;
43
+ code: string;
44
+ stackable: string;
45
+ };
46
+ pagination: {
47
+ showing: string;
48
+ previous: string;
49
+ next: string;
50
+ page: string;
51
+ };
52
+ summaries: {
53
+ globalScope: string;
54
+ productsScope: string;
55
+ categoriesScope: string;
56
+ destinationsScope: string;
57
+ marketsScope: string;
58
+ audiencesScope: string;
59
+ productNouns: {
60
+ singular: string;
61
+ plural: string;
62
+ };
63
+ categoryNouns: {
64
+ singular: string;
65
+ plural: string;
66
+ };
67
+ destinationNouns: {
68
+ singular: string;
69
+ plural: string;
70
+ };
71
+ unknownPercentage: string;
72
+ anytime: string;
73
+ until: string;
74
+ from: string;
75
+ range: string;
76
+ noCode: string;
77
+ };
78
+ };
79
+ promotionDialog: {
80
+ titles: {
81
+ create: string;
82
+ edit: string;
83
+ };
84
+ description: string;
85
+ fields: {
86
+ name: string;
87
+ slug: string;
88
+ description: string;
89
+ type: string;
90
+ percent: string;
91
+ amount: string;
92
+ currency: string;
93
+ scope: string;
94
+ scopeIds: string;
95
+ audiences: string;
96
+ validFrom: string;
97
+ validUntil: string;
98
+ code: string;
99
+ minPax: string;
100
+ stackable: string;
101
+ active: string;
102
+ };
103
+ placeholders: {
104
+ name: string;
105
+ slug: string;
106
+ description: string;
107
+ percent: string;
108
+ amount: string;
109
+ currency: string;
110
+ code: string;
111
+ minPax: string;
112
+ productIds: string;
113
+ categoryIds: string;
114
+ destinationIds: string;
115
+ marketIds: string;
116
+ };
117
+ hints: {
118
+ globalScope: string;
119
+ commaSeparatedIds: string;
120
+ };
121
+ validation: {
122
+ nameRequired: string;
123
+ slugRequired: string;
124
+ discountPercentRequired: string;
125
+ discountAmountRequired: string;
126
+ currencyRequired: string;
127
+ scopeInvalid: string;
128
+ scopeInvalidPrefix: string;
129
+ };
130
+ };
131
+ };
132
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/i18n/messages.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import { type LocaleMessageOverrides, type PackageI18nValue } from "@voyantjs/i18n";
2
+ import type { ReactNode } from "react";
3
+ import type { PromotionsUiMessages } from "./messages.js";
4
+ export declare const promotionsUiMessageDefinitions: {
5
+ en: PromotionsUiMessages;
6
+ ro: PromotionsUiMessages;
7
+ };
8
+ export type PromotionsUiMessageOverrides = LocaleMessageOverrides<PromotionsUiMessages>;
9
+ export declare function resolvePromotionsUiMessages({ locale, overrides, }: {
10
+ locale: string | null | undefined;
11
+ overrides?: PromotionsUiMessageOverrides | null;
12
+ }): PromotionsUiMessages;
13
+ export declare function getPromotionsUiI18n({ locale, overrides, }: {
14
+ locale?: string | null | undefined;
15
+ overrides?: PromotionsUiMessageOverrides | null;
16
+ }): PackageI18nValue<PromotionsUiMessages>;
17
+ export declare function PromotionsUiMessagesProvider({ children, locale, overrides, }: {
18
+ children: ReactNode;
19
+ locale: string | null | undefined;
20
+ overrides?: PromotionsUiMessageOverrides | null;
21
+ }): import("react/jsx-runtime").JSX.Element;
22
+ export declare const usePromotionsUiI18n: () => PackageI18nValue<PromotionsUiMessages>;
23
+ export declare const usePromotionsUiMessages: () => PromotionsUiMessages;
24
+ export declare function usePromotionsUiI18nOrDefault(): PackageI18nValue<PromotionsUiMessages>;
25
+ export declare function usePromotionsUiMessagesOrDefault(): PromotionsUiMessages;
26
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/i18n/provider.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,EAEtB,MAAM,gBAAgB,CAAA;AACvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAKzD,eAAO,MAAM,8BAA8B;;;CAGe,CAAA;AAE1D,MAAM,MAAM,4BAA4B,GAAG,sBAAsB,CAAC,oBAAoB,CAAC,CAAA;AAUvF,wBAAgB,2BAA2B,CAAC,EAC1C,MAAM,EACN,SAAS,GACV,EAAE;IACD,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IACjC,SAAS,CAAC,EAAE,4BAA4B,GAAG,IAAI,CAAA;CAChD,wBAOA;AAED,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,SAAS,GACV,EAAE;IACD,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAClC,SAAS,CAAC,EAAE,4BAA4B,GAAG,IAAI,CAAA;CAChD,GAAG,gBAAgB,CAAC,oBAAoB,CAAC,CASzC;AAED,wBAAgB,4BAA4B,CAAC,EAC3C,QAAQ,EACR,MAAM,EACN,SAAS,GACV,EAAE;IACD,QAAQ,EAAE,SAAS,CAAA;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IACjC,SAAS,CAAC,EAAE,4BAA4B,GAAG,IAAI,CAAA;CAChD,2CAWA;AAED,eAAO,MAAM,mBAAmB,8CAA8B,CAAA;AAC9D,eAAO,MAAM,uBAAuB,4BAAkC,CAAA;AAEtE,wBAAgB,4BAA4B,2CAE3C;AAED,wBAAgB,gCAAgC,yBAE/C"}
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createLocaleFormatters, createPackageMessagesContext, resolvePackageMessages, } from "@voyantjs/i18n";
4
+ import { promotionsUiEn } from "./en.js";
5
+ import { promotionsUiRo } from "./ro.js";
6
+ const fallbackLocale = "en";
7
+ export const promotionsUiMessageDefinitions = {
8
+ en: promotionsUiEn,
9
+ ro: promotionsUiRo,
10
+ };
11
+ const promotionsUiContext = createPackageMessagesContext("PromotionsUiMessages");
12
+ const defaultPromotionsUiI18n = {
13
+ messages: promotionsUiEn,
14
+ ...createLocaleFormatters(fallbackLocale),
15
+ };
16
+ export function resolvePromotionsUiMessages({ locale, overrides, }) {
17
+ return resolvePackageMessages({
18
+ definitions: promotionsUiMessageDefinitions,
19
+ fallbackLocale,
20
+ locale,
21
+ overrides,
22
+ });
23
+ }
24
+ export function getPromotionsUiI18n({ locale, overrides, }) {
25
+ const resolvedLocale = locale ?? fallbackLocale;
26
+ return {
27
+ messages: resolvePromotionsUiMessages({
28
+ locale: resolvedLocale,
29
+ overrides,
30
+ }),
31
+ ...createLocaleFormatters(resolvedLocale),
32
+ };
33
+ }
34
+ export function PromotionsUiMessagesProvider({ children, locale, overrides, }) {
35
+ return (_jsx(promotionsUiContext.ResolvedMessagesProvider, { definitions: promotionsUiMessageDefinitions, fallbackLocale: fallbackLocale, locale: locale, overrides: overrides, children: children }));
36
+ }
37
+ export const usePromotionsUiI18n = promotionsUiContext.useI18n;
38
+ export const usePromotionsUiMessages = promotionsUiContext.useMessages;
39
+ export function usePromotionsUiI18nOrDefault() {
40
+ return promotionsUiContext.useOptionalI18n() ?? defaultPromotionsUiI18n;
41
+ }
42
+ export function usePromotionsUiMessagesOrDefault() {
43
+ return usePromotionsUiI18nOrDefault().messages;
44
+ }
@@ -0,0 +1,3 @@
1
+ import type { PromotionsUiMessages } from "./messages.js";
2
+ export declare const promotionsUiRo: PromotionsUiMessages;
3
+ //# sourceMappingURL=ro.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ro.d.ts","sourceRoot":"","sources":["../../src/i18n/ro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAEzD,eAAO,MAAM,cAAc,EAAE,oBAgJ5B,CAAA"}
@@ -0,0 +1,144 @@
1
+ export const promotionsUiRo = {
2
+ common: {
3
+ cancel: "Anuleaza",
4
+ create: "Creeaza",
5
+ saveChanges: "Salveaza modificarile",
6
+ saving: "Se salveaza...",
7
+ active: "Activa",
8
+ discountTypeLabels: {
9
+ percentage: "Procent",
10
+ fixed_amount: "Suma fixa",
11
+ },
12
+ applicationModeLabels: {
13
+ auto: "Aplicata automat",
14
+ code: "Cu cod",
15
+ },
16
+ statusLabels: {
17
+ active: "Activa",
18
+ scheduled: "Programata",
19
+ expired: "Expirata",
20
+ archived: "Arhivata",
21
+ },
22
+ scopeKindLabels: {
23
+ global: "Global",
24
+ products: "Produse",
25
+ categories: "Categorii",
26
+ destinations: "Destinatii",
27
+ markets: "Piete",
28
+ audiences: "Audiente",
29
+ },
30
+ audienceLabels: {
31
+ staff: "Echipa",
32
+ customer: "Client",
33
+ partner: "Partener",
34
+ supplier: "Furnizor",
35
+ },
36
+ },
37
+ promotionsPage: {
38
+ title: "Promotii",
39
+ description: "Discounturi aplicate automat in catalog si oferte valorificate cu cod.",
40
+ newPromotion: "Promotie noua",
41
+ searchLabel: "Cauta promotii",
42
+ searchPlaceholder: "Cauta dupa nume, slug, descriere sau cod",
43
+ modePlaceholder: "Mod",
44
+ allModes: "Toate modurile",
45
+ statusPlaceholder: "Status",
46
+ allStatuses: "Toate statusurile",
47
+ scopePlaceholder: "Domeniu",
48
+ allScopes: "Toate domeniile",
49
+ validityRangePlaceholder: "Interval valabilitate",
50
+ clearFilters: "Sterge",
51
+ loadFailed: "Promotiile nu au putut fi incarcate.",
52
+ loadFailedPrefix: "Incarcarea a esuat: {message}",
53
+ empty: "Nicio promotie nu se potriveste filtrelor curente.",
54
+ columns: {
55
+ name: "Nume",
56
+ mode: "Mod",
57
+ scope: "Domeniu",
58
+ discount: "Discount",
59
+ validity: "Valabilitate",
60
+ code: "Cod",
61
+ status: "Status",
62
+ },
63
+ badges: {
64
+ auto: "Automat",
65
+ code: "Cod",
66
+ stackable: "Cumulabila",
67
+ },
68
+ pagination: {
69
+ showing: "Afisate {shown} din {total}",
70
+ previous: "Anterior",
71
+ next: "Urmator",
72
+ page: "Pagina {page} din {pageCount}",
73
+ },
74
+ summaries: {
75
+ globalScope: "Global",
76
+ productsScope: "{count} {noun}",
77
+ categoriesScope: "{count} {noun}",
78
+ destinationsScope: "{count} {noun}",
79
+ marketsScope: "Piete: {markets}",
80
+ audiencesScope: "Audiente: {audiences}",
81
+ productNouns: { singular: "produs", plural: "produse" },
82
+ categoryNouns: { singular: "categorie", plural: "categorii" },
83
+ destinationNouns: { singular: "destinatie", plural: "destinatii" },
84
+ unknownPercentage: "?",
85
+ anytime: "Oricand",
86
+ until: "Pana la {date}",
87
+ from: "Din {date}",
88
+ range: "{from} - {until}",
89
+ noCode: "-",
90
+ },
91
+ },
92
+ promotionDialog: {
93
+ titles: {
94
+ create: "Promotie noua",
95
+ edit: "Editeaza promotia",
96
+ },
97
+ description: "Seteaza discountul, domeniul si valabilitatea. Ofertele pe baza de cod necesita un cod completat; lasa campul gol pentru oferte aplicate automat.",
98
+ fields: {
99
+ name: "Nume",
100
+ slug: "Slug",
101
+ description: "Descriere",
102
+ type: "Tip",
103
+ percent: "Procent",
104
+ amount: "Suma",
105
+ currency: "Moneda",
106
+ scope: "Domeniu",
107
+ scopeIds: "{scope}",
108
+ audiences: "Audiente",
109
+ validFrom: "Valabila din",
110
+ validUntil: "Valabila pana la",
111
+ code: "Cod (optional)",
112
+ minPax: "Pax minim (optional)",
113
+ stackable: "Cumulabila cu alte oferte",
114
+ active: "Activa",
115
+ },
116
+ placeholders: {
117
+ name: "Reducere primavara 2026",
118
+ slug: "reducere-primavara-2026",
119
+ description: "Nota interna: scopul acestei oferte",
120
+ percent: "20",
121
+ amount: "5.00",
122
+ currency: "USD",
123
+ code: "EARLYBIRD2026",
124
+ minPax: "4",
125
+ productIds: "Adauga referinte de produse",
126
+ categoryIds: "Adauga referinte de categorii",
127
+ destinationIds: "Adauga referinte de destinatii",
128
+ marketIds: "Adauga referinte de piete",
129
+ },
130
+ hints: {
131
+ globalScope: "Se aplica tuturor produselor.",
132
+ commaSeparatedIds: "Referinte separate prin virgula",
133
+ },
134
+ validation: {
135
+ nameRequired: "Numele este obligatoriu",
136
+ slugRequired: "Slug-ul este obligatoriu",
137
+ discountPercentRequired: "Procentul de discount este obligatoriu pentru ofertele procentuale",
138
+ discountAmountRequired: "Suma discountului este obligatorie pentru ofertele cu suma fixa",
139
+ currencyRequired: "Moneda este obligatorie pentru ofertele cu suma fixa",
140
+ scopeInvalid: "invalid",
141
+ scopeInvalidPrefix: "Domeniu: {message}",
142
+ },
143
+ },
144
+ };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export type { PromotionsUiMessages } from "./i18n/index.js";
2
+ export { getPromotionsUiI18n, type PromotionsUiMessageOverrides, PromotionsUiMessagesProvider, promotionsUiMessageDefinitions, resolvePromotionsUiMessages, usePromotionsUiI18n, usePromotionsUiI18nOrDefault, usePromotionsUiMessages, usePromotionsUiMessagesOrDefault, } from "./i18n/index.js";
1
3
  export { PromotionDialog, type PromotionDialogProps } from "./promotion-dialog.js";
2
4
  export { loadPromotionsPage, type PromotionDialogRenderProps, PromotionsPage, type PromotionsPageProps, } from "./promotions-page.js";
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAClF,OAAO,EACL,kBAAkB,EAClB,KAAK,0BAA0B,EAC/B,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAC3D,OAAO,EACL,mBAAmB,EACnB,KAAK,4BAA4B,EACjC,4BAA4B,EAC5B,8BAA8B,EAC9B,2BAA2B,EAC3B,mBAAmB,EACnB,4BAA4B,EAC5B,uBAAuB,EACvB,gCAAgC,GACjC,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAClF,OAAO,EACL,kBAAkB,EAClB,KAAK,0BAA0B,EAC/B,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,sBAAsB,CAAA"}
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
+ export { getPromotionsUiI18n, PromotionsUiMessagesProvider, promotionsUiMessageDefinitions, resolvePromotionsUiMessages, usePromotionsUiI18n, usePromotionsUiI18nOrDefault, usePromotionsUiMessages, usePromotionsUiMessagesOrDefault, } from "./i18n/index.js";
1
2
  export { PromotionDialog } from "./promotion-dialog.js";
2
3
  export { loadPromotionsPage, PromotionsPage, } from "./promotions-page.js";
@@ -1,12 +1,3 @@
1
- /**
2
- * Create / edit dialog for a promotional offer.
3
- *
4
- * The discriminated-union scope picker is the trickiest piece here:
5
- * a `kind` dropdown plus per-kind sub-fields rendered conditionally.
6
- * Comma-separated text input for ID lists (productIds, marketIds, etc.)
7
- * keeps the v1 UX simple — a future PR can swap in proper combobox-with-
8
- * search components per scope kind.
9
- */
10
1
  import { type PromotionalOfferRecord } from "@voyantjs/promotions-react";
11
2
  export interface PromotionDialogProps {
12
3
  open: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"promotion-dialog.d.ts","sourceRoot":"","sources":["../src/promotion-dialog.tsx"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,OAAO,EACL,KAAK,sBAAsB,EAM5B,MAAM,4BAA4B,CAAA;AAuBnC,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,oDAAoD;IACpD,KAAK,CAAC,EAAE,sBAAsB,CAAA;CAC/B;AAqKD,wBAAgB,eAAe,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE,oBAAoB,2CAgSlF"}
1
+ {"version":3,"file":"promotion-dialog.d.ts","sourceRoot":"","sources":["../src/promotion-dialog.tsx"],"names":[],"mappings":"AAaA,OAAO,EACL,KAAK,sBAAsB,EAO5B,MAAM,4BAA4B,CAAA;AA0BnC,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,oDAAoD;IACpD,KAAK,CAAC,EAAE,sBAAsB,CAAA;CAC/B;AA4KD,wBAAgB,eAAe,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE,oBAAoB,2CA2SlF"}
@@ -9,11 +9,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
9
9
  * keeps the v1 UX simple — a future PR can swap in proper combobox-with-
10
10
  * search components per scope kind.
11
11
  */
12
+ import { formatMessage } from "@voyantjs/i18n";
12
13
  import { promotionalOfferScopeSchema, useCreatePromotion, useUpdatePromotion, } from "@voyantjs/promotions-react";
13
14
  import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Textarea, } from "@voyantjs/ui/components";
14
15
  import { CurrencyInput } from "@voyantjs/ui/components/currency-input";
15
16
  import { DateTimePicker } from "@voyantjs/ui/components/date-time-picker";
16
17
  import { useEffect, useState } from "react";
18
+ import { usePromotionsUiMessagesOrDefault } from "./i18n/provider.js";
17
19
  const SCOPE_KINDS = [
18
20
  "global",
19
21
  "products",
@@ -108,27 +110,31 @@ function parseIds(raw) {
108
110
  .map((s) => s.trim())
109
111
  .filter((s) => s.length > 0);
110
112
  }
111
- function buildPayload(state) {
113
+ function buildPayload(state, messages) {
112
114
  if (!state.name.trim())
113
- return { error: "Name is required" };
115
+ return { error: messages.validation.nameRequired };
114
116
  if (!state.slug.trim())
115
- return { error: "Slug is required" };
117
+ return { error: messages.validation.slugRequired };
116
118
  if (state.discountType === "percentage" && !state.discountPercent) {
117
- return { error: "Discount percent is required for percentage offers" };
119
+ return { error: messages.validation.discountPercentRequired };
118
120
  }
119
121
  if (state.discountType === "fixed_amount") {
120
122
  if (state.discountAmountCents == null || state.discountAmountCents <= 0) {
121
- return { error: "Discount amount is required for fixed-amount offers" };
123
+ return { error: messages.validation.discountAmountRequired };
122
124
  }
123
125
  if (!state.currency.trim())
124
- return { error: "Currency is required for fixed-amount offers" };
126
+ return { error: messages.validation.currencyRequired };
125
127
  }
126
128
  // Validate scope shape via Zod so the user gets clear errors when (e.g.)
127
129
  // a products scope has an empty ID list.
128
130
  const scope = buildScope(state);
129
131
  const scopeResult = promotionalOfferScopeSchema.safeParse(scope);
130
132
  if (!scopeResult.success) {
131
- return { error: `Scope: ${scopeResult.error.issues[0]?.message ?? "invalid"}` };
133
+ return {
134
+ error: formatMessage(messages.validation.scopeInvalidPrefix, {
135
+ message: scopeResult.error.issues[0]?.message ?? messages.validation.scopeInvalid,
136
+ }),
137
+ };
132
138
  }
133
139
  const payload = {
134
140
  name: state.name.trim(),
@@ -149,6 +155,8 @@ function buildPayload(state) {
149
155
  return payload;
150
156
  }
151
157
  export function PromotionDialog({ open, onOpenChange, offer }) {
158
+ const messages = usePromotionsUiMessagesOrDefault();
159
+ const dialogMessages = messages.promotionDialog;
152
160
  const [state, setState] = useState(emptyForm());
153
161
  const [error, setError] = useState(null);
154
162
  const createMutation = useCreatePromotion();
@@ -167,7 +175,7 @@ export function PromotionDialog({ open, onOpenChange, offer }) {
167
175
  }
168
176
  async function handleSave() {
169
177
  setError(null);
170
- const result = buildPayload(state);
178
+ const result = buildPayload(state, dialogMessages);
171
179
  if ("error" in result) {
172
180
  setError(result.error);
173
181
  return;
@@ -185,47 +193,44 @@ export function PromotionDialog({ open, onOpenChange, offer }) {
185
193
  setError(err instanceof Error ? err.message : String(err));
186
194
  }
187
195
  }
188
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-h-[90vh] max-w-2xl overflow-y-auto", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: isEdit ? "Edit promotion" : "New promotion" }), _jsx(DialogDescription, { children: "Set discount, scope, and validity. Code-gated offers require a non-empty code; leave it blank for auto-applied offers." })] }), _jsxs("div", { className: "grid gap-4 py-2", children: [_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-name", children: "Name" }), _jsx(Input, { id: "promotion-name", value: state.name, onChange: (e) => setField("name", e.target.value), placeholder: "Spring Sale 2026" })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-slug", children: "Slug" }), _jsx(Input, { id: "promotion-slug", value: state.slug, onChange: (e) => setField("slug", e.target.value), placeholder: "spring-sale-2026" })] })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-description", children: "Description" }), _jsx(Textarea, { id: "promotion-description", value: state.description, onChange: (e) => setField("description", e.target.value), rows: 2, placeholder: "Internal note \u2014 what this offer is for" })] }), _jsxs("div", { className: "grid grid-cols-3 gap-3", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { children: "Type" }), _jsxs(Select, { value: state.discountType, onValueChange: (v) => {
196
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-h-[90vh] max-w-2xl overflow-y-auto", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: isEdit ? dialogMessages.titles.edit : dialogMessages.titles.create }), _jsx(DialogDescription, { children: dialogMessages.description })] }), _jsxs("div", { className: "grid gap-4 py-2", children: [_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-name", children: dialogMessages.fields.name }), _jsx(Input, { id: "promotion-name", value: state.name, onChange: (e) => setField("name", e.target.value), placeholder: dialogMessages.placeholders.name })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-slug", children: dialogMessages.fields.slug }), _jsx(Input, { id: "promotion-slug", value: state.slug, onChange: (e) => setField("slug", e.target.value), placeholder: dialogMessages.placeholders.slug })] })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-description", children: dialogMessages.fields.description }), _jsx(Textarea, { id: "promotion-description", value: state.description, onChange: (e) => setField("description", e.target.value), rows: 2, placeholder: dialogMessages.placeholders.description })] }), _jsxs("div", { className: "grid grid-cols-3 gap-3", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { children: dialogMessages.fields.type }), _jsxs(Select, { value: state.discountType, onValueChange: (v) => {
189
197
  if (v === "percentage" || v === "fixed_amount")
190
198
  setField("discountType", v);
191
- }, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "percentage", children: "Percentage" }), _jsx(SelectItem, { value: "fixed_amount", children: "Fixed amount" })] })] })] }), state.discountType === "percentage" ? (_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-percent", children: "Percent" }), _jsx(Input, { id: "promotion-percent", type: "number", step: "0.01", min: "0", max: "100", value: state.discountPercent, onChange: (e) => setField("discountPercent", e.target.value), placeholder: "20" })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-amount", children: "Amount" }), _jsx(CurrencyInput, { id: "promotion-amount", value: state.discountAmountCents, onChange: (value) => setField("discountAmountCents", value), currency: state.currency, placeholder: "5.00" })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-currency", children: "Currency" }), _jsx(Input, { id: "promotion-currency", value: state.currency, onChange: (e) => setField("currency", e.target.value), placeholder: "USD", maxLength: 3 })] })] }))] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { children: "Scope" }), _jsxs(Select, { value: state.scopeKind, onValueChange: (v) => {
199
+ }, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "percentage", children: messages.common.discountTypeLabels.percentage }), _jsx(SelectItem, { value: "fixed_amount", children: messages.common.discountTypeLabels.fixed_amount })] })] })] }), state.discountType === "percentage" ? (_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-percent", children: dialogMessages.fields.percent }), _jsx(Input, { id: "promotion-percent", type: "number", step: "0.01", min: "0", max: "100", value: state.discountPercent, onChange: (e) => setField("discountPercent", e.target.value), placeholder: dialogMessages.placeholders.percent })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-amount", children: dialogMessages.fields.amount }), _jsx(CurrencyInput, { id: "promotion-amount", value: state.discountAmountCents, onChange: (value) => setField("discountAmountCents", value), currency: state.currency, placeholder: dialogMessages.placeholders.amount })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-currency", children: dialogMessages.fields.currency }), _jsx(Input, { id: "promotion-currency", value: state.currency, onChange: (e) => setField("currency", e.target.value), placeholder: dialogMessages.placeholders.currency, maxLength: 3 })] })] }))] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { children: dialogMessages.fields.scope }), _jsxs(Select, { value: state.scopeKind, onValueChange: (v) => {
192
200
  if (v != null && SCOPE_KINDS.includes(v)) {
193
201
  setField("scopeKind", v);
194
202
  }
195
- }, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: SCOPE_KINDS.map((kind) => (_jsx(SelectItem, { value: kind, children: kind }, kind))) })] }), state.scopeKind === "global" ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Applies to every product." })) : null, (state.scopeKind === "products" ||
203
+ }, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: SCOPE_KINDS.map((kind) => (_jsx(SelectItem, { value: kind, children: messages.common.scopeKindLabels[kind] }, kind))) })] }), state.scopeKind === "global" ? (_jsx("p", { className: "text-sm text-muted-foreground", children: dialogMessages.hints.globalScope })) : null, (state.scopeKind === "products" ||
196
204
  state.scopeKind === "categories" ||
197
205
  state.scopeKind === "destinations" ||
198
- state.scopeKind === "markets") && (_jsxs("div", { className: "grid gap-1.5", children: [_jsxs(Label, { htmlFor: "promotion-scope-ids", children: [scopeIdsLabel(state.scopeKind), " (comma-separated IDs)"] }), _jsx(Input, { id: "promotion-scope-ids", value: state.scopeIds, onChange: (e) => setField("scopeIds", e.target.value), placeholder: scopeIdsPlaceholder(state.scopeKind) })] })), state.scopeKind === "audiences" && (_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: "Audiences" }), _jsx("div", { className: "flex flex-wrap gap-3", children: AUDIENCE_OPTIONS.map((audience) => {
206
+ state.scopeKind === "markets") && (_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-scope-ids", children: formatMessage(dialogMessages.fields.scopeIds, {
207
+ scope: scopeIdsLabel(state.scopeKind, messages.common.scopeKindLabels),
208
+ }) }), _jsx(Input, { id: "promotion-scope-ids", value: state.scopeIds, onChange: (e) => setField("scopeIds", e.target.value), placeholder: scopeIdsPlaceholder(state.scopeKind, dialogMessages.placeholders) })] })), state.scopeKind === "audiences" && (_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: dialogMessages.fields.audiences }), _jsx("div", { className: "flex flex-wrap gap-3", children: AUDIENCE_OPTIONS.map((audience) => {
199
209
  const selected = state.scopeAudiences.includes(audience);
200
210
  return (_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: selected, onChange: (e) => {
201
211
  const next = e.target.checked
202
212
  ? [...state.scopeAudiences, audience]
203
213
  : state.scopeAudiences.filter((a) => a !== audience);
204
214
  setField("scopeAudiences", next);
205
- } }), audience] }, audience));
206
- }) })] }))] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-valid-from", children: "Valid from" }), _jsx(DateTimePicker, { value: state.validFrom, onChange: (nextValue) => setField("validFrom", nextValue ?? "") })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-valid-until", children: "Valid until" }), _jsx(DateTimePicker, { value: state.validUntil, onChange: (nextValue) => setField("validUntil", nextValue ?? "") })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-code", children: "Code (optional)" }), _jsx(Input, { id: "promotion-code", value: state.code, onChange: (e) => setField("code", e.target.value), placeholder: "EARLYBIRD2026" })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-min-pax", children: "Min pax (optional)" }), _jsx(Input, { id: "promotion-min-pax", type: "number", min: "1", step: "1", value: state.minPax, onChange: (e) => setField("minPax", e.target.value), placeholder: "4" })] })] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "promotion-stackable", checked: state.stackable, onCheckedChange: (v) => setField("stackable", Boolean(v)) }), _jsx(Label, { htmlFor: "promotion-stackable", children: "Stackable with other offers" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "promotion-active", checked: state.active, onCheckedChange: (v) => setField("active", Boolean(v)) }), _jsx(Label, { htmlFor: "promotion-active", children: "Active" })] })] }), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: isPending, children: "Cancel" }), _jsx(Button, { onClick: handleSave, disabled: isPending, children: isPending ? "Saving…" : isEdit ? "Save changes" : "Create" })] })] }) }));
215
+ } }), messages.common.audienceLabels[audience]] }, audience));
216
+ }) })] }))] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-valid-from", children: dialogMessages.fields.validFrom }), _jsx(DateTimePicker, { value: state.validFrom, onChange: (nextValue) => setField("validFrom", nextValue ?? "") })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-valid-until", children: dialogMessages.fields.validUntil }), _jsx(DateTimePicker, { value: state.validUntil, onChange: (nextValue) => setField("validUntil", nextValue ?? "") })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-code", children: dialogMessages.fields.code }), _jsx(Input, { id: "promotion-code", value: state.code, onChange: (e) => setField("code", e.target.value), placeholder: dialogMessages.placeholders.code })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: "promotion-min-pax", children: dialogMessages.fields.minPax }), _jsx(Input, { id: "promotion-min-pax", type: "number", min: "1", step: "1", value: state.minPax, onChange: (e) => setField("minPax", e.target.value), placeholder: dialogMessages.placeholders.minPax })] })] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "promotion-stackable", checked: state.stackable, onCheckedChange: (v) => setField("stackable", Boolean(v)) }), _jsx(Label, { htmlFor: "promotion-stackable", children: dialogMessages.fields.stackable })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "promotion-active", checked: state.active, onCheckedChange: (v) => setField("active", Boolean(v)) }), _jsx(Label, { htmlFor: "promotion-active", children: dialogMessages.fields.active })] })] }), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: isPending, children: messages.common.cancel }), _jsx(Button, { onClick: handleSave, disabled: isPending, children: isPending
217
+ ? messages.common.saving
218
+ : isEdit
219
+ ? messages.common.saveChanges
220
+ : messages.common.create })] })] }) }));
207
221
  }
208
- function scopeIdsLabel(kind) {
209
- switch (kind) {
210
- case "products":
211
- return "Product IDs";
212
- case "categories":
213
- return "Category IDs";
214
- case "destinations":
215
- return "Destination IDs";
216
- case "markets":
217
- return "Market IDs";
218
- }
222
+ function scopeIdsLabel(kind, labels) {
223
+ return labels[kind];
219
224
  }
220
- function scopeIdsPlaceholder(kind) {
225
+ function scopeIdsPlaceholder(kind, placeholders) {
221
226
  switch (kind) {
222
227
  case "products":
223
- return "prod_xxx, prod_yyy";
228
+ return placeholders.productIds;
224
229
  case "categories":
225
- return "cat_xxx";
230
+ return placeholders.categoryIds;
226
231
  case "destinations":
227
- return "dest_xxx";
232
+ return placeholders.destinationIds;
228
233
  case "markets":
229
- return "mkt_xxx";
234
+ return placeholders.marketIds;
230
235
  }
231
236
  }
@@ -1 +1 @@
1
- {"version":3,"file":"promotions-page.d.ts","sourceRoot":"","sources":["../src/promotions-page.tsx"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAKL,KAAK,sBAAsB,EAG3B,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EAEzB,MAAM,4BAA4B,CAAA;AAsBnC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AA0BtC,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,KAAK,CAAC,EAAE,sBAAsB,CAAA;IAC9B,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,eAAe,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,sBAAsB,KAAK,IAAI,CAAA;IAClF,qBAAqB,CAAC,EAAE,CAAC,KAAK,EAAE,0BAA0B,KAAK,SAAS,CAAA;CACzE;AAED,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,WAAW,EACxB,MAAM,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,EACzC,KAAK,GAAE,mBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAQhC;AAED,wBAAgB,cAAc,CAAC,EAC7B,SAAS,EACT,QAA4B,EAC5B,eAAe,EACf,qBAAqB,GACtB,GAAE,mBAAwB,2CAyR1B"}
1
+ {"version":3,"file":"promotions-page.d.ts","sourceRoot":"","sources":["../src/promotions-page.tsx"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,EAKL,KAAK,sBAAsB,EAG3B,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EAEzB,MAAM,4BAA4B,CAAA;AAsBnC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AA4BtC,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,KAAK,CAAC,EAAE,sBAAsB,CAAA;IAC9B,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,eAAe,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,sBAAsB,KAAK,IAAI,CAAA;IAClF,qBAAqB,CAAC,EAAE,CAAC,KAAK,EAAE,0BAA0B,KAAK,SAAS,CAAA;CACzE;AAED,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,WAAW,EACxB,MAAM,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,EACzC,KAAK,GAAE,mBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAQhC;AAED,wBAAgB,cAAc,CAAC,EAC7B,SAAS,EACT,QAA4B,EAC5B,eAAe,EACf,qBAAqB,GACtB,GAAE,mBAAwB,2CAgS1B"}
@@ -1,10 +1,12 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { formatMessage } from "@voyantjs/i18n";
3
4
  import { createPromotionsClientOptions, getPromotionsListQueryOptions, usePromotionsList, } from "@voyantjs/promotions-react";
4
5
  import { Badge, Button, DateRangePicker, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/ui/components";
5
6
  import { cn } from "@voyantjs/ui/lib/utils";
6
7
  import { Plus, Search, X } from "lucide-react";
7
8
  import { useState } from "react";
9
+ import { usePromotionsUiMessagesOrDefault } from "./i18n/provider.js";
8
10
  import { PromotionDialog } from "./promotion-dialog.js";
9
11
  const DEFAULT_PAGE_SIZE = 25;
10
12
  const ALL = "__all__";
@@ -23,6 +25,8 @@ export function loadPromotionsPage(queryClient, client, query = {}) {
23
25
  return queryClient.ensureQueryData(getPromotionsListQueryOptions({ limit: DEFAULT_PAGE_SIZE, offset: 0, ...query }, createPromotionsClientOptions(client)));
24
26
  }
25
27
  export function PromotionsPage({ className, pageSize = DEFAULT_PAGE_SIZE, onOpenPromotion, renderPromotionDialog, } = {}) {
28
+ const messages = usePromotionsUiMessagesOrDefault();
29
+ const pageMessages = messages.promotionsPage;
26
30
  const [search, setSearch] = useState("");
27
31
  const [applicationMode, setApplicationMode] = useState(ALL);
28
32
  const [status, setStatus] = useState(ALL);
@@ -86,62 +90,101 @@ export function PromotionsPage({ className, pageSize = DEFAULT_PAGE_SIZE, onOpen
86
90
  void refetch();
87
91
  },
88
92
  })) : (_jsx(PromotionDialog, { open: dialogOpen, onOpenChange: setDialogOpen, offer: editingOffer }));
89
- return (_jsxs("div", { className: cn("flex flex-col gap-6 p-6", className), children: [_jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: "Promotions" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Auto-applied catalog discounts and code-redeemed offers." })] }), _jsxs(Button, { onClick: openCreate, children: [_jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }), "New promotion"] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsxs("div", { className: "relative min-w-[14rem] max-w-sm flex-1", children: [_jsx(Label, { htmlFor: "promotions-search", className: "sr-only", children: "Search promotions" }), _jsx(Search, { className: "absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground", "aria-hidden": "true" }), _jsx(Input, { id: "promotions-search", placeholder: "Search name, slug, description, or code", value: search, onChange: (event) => {
93
+ return (_jsxs("div", { className: cn("flex flex-col gap-6 p-6", className), children: [_jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: pageMessages.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: pageMessages.description })] }), _jsxs(Button, { onClick: openCreate, children: [_jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }), pageMessages.newPromotion] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsxs("div", { className: "relative min-w-[14rem] max-w-sm flex-1", children: [_jsx(Label, { htmlFor: "promotions-search", className: "sr-only", children: pageMessages.searchLabel }), _jsx(Search, { className: "absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground", "aria-hidden": "true" }), _jsx(Input, { id: "promotions-search", placeholder: pageMessages.searchPlaceholder, value: search, onChange: (event) => {
90
94
  setSearch(event.target.value);
91
95
  resetPage();
92
96
  }, className: "pl-9" })] }), _jsxs(Select, { value: applicationMode, onValueChange: (value) => {
93
97
  setApplicationMode(value ?? ALL);
94
98
  resetPage();
95
- }, children: [_jsx(SelectTrigger, { className: "w-[10.5rem]", children: _jsx(SelectValue, { placeholder: "Mode" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: ALL, children: "All modes" }), applicationModes.map((mode) => (_jsx(SelectItem, { value: mode, children: applicationModeLabel(mode) }, mode)))] })] }), _jsxs(Select, { value: status, onValueChange: (value) => {
99
+ }, children: [_jsx(SelectTrigger, { className: "w-[10.5rem]", children: _jsx(SelectValue, { placeholder: pageMessages.modePlaceholder }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: ALL, children: pageMessages.allModes }), applicationModes.map((mode) => (_jsx(SelectItem, { value: mode, children: messages.common.applicationModeLabels[mode] }, mode)))] })] }), _jsxs(Select, { value: status, onValueChange: (value) => {
96
100
  setStatus(value ?? ALL);
97
101
  resetPage();
98
- }, children: [_jsx(SelectTrigger, { className: "w-[10.5rem]", children: _jsx(SelectValue, { placeholder: "Status" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: ALL, children: "All statuses" }), statusFilters.map((value) => (_jsx(SelectItem, { value: value, children: statusLabel(value) }, value)))] })] }), _jsxs(Select, { value: scopeKind, onValueChange: (value) => {
102
+ }, children: [_jsx(SelectTrigger, { className: "w-[10.5rem]", children: _jsx(SelectValue, { placeholder: pageMessages.statusPlaceholder }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: ALL, children: pageMessages.allStatuses }), statusFilters.map((value) => (_jsx(SelectItem, { value: value, children: messages.common.statusLabels[value] }, value)))] })] }), _jsxs(Select, { value: scopeKind, onValueChange: (value) => {
99
103
  setScopeKind(value ?? ALL);
100
104
  resetPage();
101
- }, children: [_jsx(SelectTrigger, { className: "w-[11rem]", children: _jsx(SelectValue, { placeholder: "Scope" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: ALL, children: "All scopes" }), scopeKinds.map((value) => (_jsx(SelectItem, { value: value, children: scopeKindLabel(value) }, value)))] })] }), _jsx(DateRangePicker, { value: validityRange, onChange: (value) => {
105
+ }, children: [_jsx(SelectTrigger, { className: "w-[11rem]", children: _jsx(SelectValue, { placeholder: pageMessages.scopePlaceholder }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: ALL, children: pageMessages.allScopes }), scopeKinds.map((value) => (_jsx(SelectItem, { value: value, children: messages.common.scopeKindLabels[value] }, value)))] })] }), _jsx(DateRangePicker, { value: validityRange, onChange: (value) => {
102
106
  setValidityRange(value);
103
107
  resetPage();
104
- }, placeholder: "Validity range", className: "w-[15rem]" }), hasActiveFilters ? (_jsxs(Button, { variant: "ghost", onClick: clearFilters, children: [_jsx(X, { className: "mr-2 size-4", "aria-hidden": "true" }), "Clear"] })) : null] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Name" }), _jsx(TableHead, { children: "Mode" }), _jsx(TableHead, { children: "Scope" }), _jsx(TableHead, { children: "Discount" }), _jsx(TableHead, { children: "Validity" }), _jsx(TableHead, { children: "Code" }), _jsx(TableHead, { children: "Status" })] }) }), _jsx(TableBody, { children: showSkeleton ? (_jsx(PromotionRowSkeleton, { rows: 6 })) : isError ? (_jsx(TableRow, { children: _jsxs(TableCell, { colSpan: TABLE_COLUMN_COUNT, className: "h-24 text-center text-sm text-destructive", children: ["Failed to load: ", error instanceof Error ? error.message : String(error)] }) })) : offers.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: TABLE_COLUMN_COUNT, className: "h-24 text-center text-sm text-muted-foreground", children: "No promotions match the current filters." }) })) : (offers.map((offer) => (_jsxs(TableRow, { className: "cursor-pointer", onClick: () => openOffer(offer), children: [_jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: offer.name }), _jsx("div", { className: "font-mono text-xs text-muted-foreground", children: offer.slug })] }), _jsx(TableCell, { children: _jsx(Badge, { variant: offer.code == null ? "secondary" : "outline", children: offer.code == null ? "Auto" : "Code" }) }), _jsx(TableCell, { className: "text-muted-foreground", children: summarizeScope(offer.scope) }), _jsx(TableCell, { children: summarizeDiscount(offer) }), _jsx(TableCell, { className: "text-muted-foreground", children: summarizeValidity(offer.validFrom, offer.validUntil) }), _jsx(TableCell, { className: "font-mono text-xs", children: offer.code ?? "-" }), _jsx(TableCell, { children: _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Badge, { variant: statusBadgeVariant(getOfferStatus(offer)), children: statusLabel(getOfferStatus(offer)) }), offer.stackable ? _jsx(Badge, { variant: "secondary", children: "Stackable" }) : null] }) })] }, offer.id)))) })] }) }), _jsx(PaginationBar, { shown: offers.length, total: total, page: page, pageCount: pageCount, onPrevious: () => setPageIndex((prev) => Math.max(0, prev - 1)), onNext: () => setPageIndex((prev) => prev + 1), canGoBack: pageIndex > 0, canGoForward: (pageIndex + 1) * pageSize < total }), dialog] }));
108
+ }, placeholder: pageMessages.validityRangePlaceholder, className: "w-[15rem]" }), hasActiveFilters ? (_jsxs(Button, { variant: "ghost", onClick: clearFilters, children: [_jsx(X, { className: "mr-2 size-4", "aria-hidden": "true" }), pageMessages.clearFilters] })) : null] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: pageMessages.columns.name }), _jsx(TableHead, { children: pageMessages.columns.mode }), _jsx(TableHead, { children: pageMessages.columns.scope }), _jsx(TableHead, { children: pageMessages.columns.discount }), _jsx(TableHead, { children: pageMessages.columns.validity }), _jsx(TableHead, { children: pageMessages.columns.code }), _jsx(TableHead, { children: pageMessages.columns.status })] }) }), _jsx(TableBody, { children: showSkeleton ? (_jsx(PromotionRowSkeleton, { rows: 6 })) : isError ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: TABLE_COLUMN_COUNT, className: "h-24 text-center text-sm text-destructive", children: formatMessage(pageMessages.loadFailedPrefix, {
109
+ message: error instanceof Error ? error.message : String(error),
110
+ }) }) })) : offers.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: TABLE_COLUMN_COUNT, className: "h-24 text-center text-sm text-muted-foreground", children: pageMessages.empty }) })) : (offers.map((offer) => (_jsxs(TableRow, { className: "cursor-pointer", onClick: () => openOffer(offer), children: [_jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: offer.name }), _jsx("div", { className: "font-mono text-xs text-muted-foreground", children: offer.slug })] }), _jsx(TableCell, { children: _jsx(Badge, { variant: offer.code == null ? "secondary" : "outline", children: offer.code == null ? pageMessages.badges.auto : pageMessages.badges.code }) }), _jsx(TableCell, { className: "text-muted-foreground", children: summarizeScope(offer.scope, messages) }), _jsx(TableCell, { children: summarizeDiscount(offer, pageMessages) }), _jsx(TableCell, { className: "text-muted-foreground", children: summarizeValidity(offer.validFrom, offer.validUntil, pageMessages) }), _jsx(TableCell, { className: "font-mono text-xs", children: offer.code ?? pageMessages.summaries.noCode }), _jsx(TableCell, { children: _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Badge, { variant: statusBadgeVariant(getOfferStatus(offer)), children: messages.common.statusLabels[getOfferStatus(offer)] }), offer.stackable ? (_jsx(Badge, { variant: "secondary", children: pageMessages.badges.stackable })) : null] }) })] }, offer.id)))) })] }) }), _jsx(PaginationBar, { shown: offers.length, total: total, page: page, pageCount: pageCount, onPrevious: () => setPageIndex((prev) => Math.max(0, prev - 1)), onNext: () => setPageIndex((prev) => prev + 1), canGoBack: pageIndex > 0, canGoForward: (pageIndex + 1) * pageSize < total, messages: pageMessages.pagination }), dialog] }));
105
111
  }
106
- function PaginationBar({ shown, total, page, pageCount, onPrevious, onNext, canGoBack, canGoForward, }) {
107
- return (_jsxs("div", { className: "flex items-center justify-between text-sm text-muted-foreground", children: [_jsxs("span", { children: ["Showing ", shown, " of ", total] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", disabled: !canGoBack, onClick: onPrevious, children: "Previous" }), _jsxs("span", { children: ["Page ", page, " of ", pageCount] }), _jsx(Button, { variant: "outline", size: "sm", disabled: !canGoForward, onClick: onNext, children: "Next" })] })] }));
112
+ function PaginationBar({ shown, total, page, pageCount, onPrevious, onNext, canGoBack, canGoForward, messages, }) {
113
+ return (_jsxs("div", { className: "flex items-center justify-between text-sm text-muted-foreground", children: [_jsx("span", { children: formatMessage(messages.showing, {
114
+ shown,
115
+ total,
116
+ }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", disabled: !canGoBack, onClick: onPrevious, children: messages.previous }), _jsx("span", { children: formatMessage(messages.page, {
117
+ page,
118
+ pageCount,
119
+ }) }), _jsx(Button, { variant: "outline", size: "sm", disabled: !canGoForward, onClick: onNext, children: messages.next })] })] }));
108
120
  }
109
121
  function PromotionRowSkeleton({ rows }) {
110
122
  return (_jsx(_Fragment, { children: Array.from({ length: rows }).map((_, index) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-40" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-5 w-16 rounded-full" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-28" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-20" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-32" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-20" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-5 w-20 rounded-full" }) })] }, `promotion-skeleton-${index}`))) }));
111
123
  }
112
- function summarizeScope(scope) {
124
+ function summarizeScope(scope, messages) {
125
+ const summary = messages.promotionsPage.summaries;
113
126
  switch (scope.kind) {
114
127
  case "global":
115
- return "Global";
128
+ return summary.globalScope;
116
129
  case "products":
117
- return `${scope.productIds.length} product${scope.productIds.length === 1 ? "" : "s"}`;
130
+ return formatMessage(summary.productsScope, {
131
+ count: scope.productIds.length,
132
+ noun: scope.productIds.length === 1
133
+ ? summary.productNouns.singular
134
+ : summary.productNouns.plural,
135
+ });
118
136
  case "categories":
119
- return `${scope.categoryIds.length} categor${scope.categoryIds.length === 1 ? "y" : "ies"}`;
137
+ return formatMessage(summary.categoriesScope, {
138
+ count: scope.categoryIds.length,
139
+ noun: scope.categoryIds.length === 1
140
+ ? summary.categoryNouns.singular
141
+ : summary.categoryNouns.plural,
142
+ });
120
143
  case "destinations":
121
- return `${scope.destinationIds.length} destination${scope.destinationIds.length === 1 ? "" : "s"}`;
144
+ return formatMessage(summary.destinationsScope, {
145
+ count: scope.destinationIds.length,
146
+ noun: scope.destinationIds.length === 1
147
+ ? summary.destinationNouns.singular
148
+ : summary.destinationNouns.plural,
149
+ });
122
150
  case "markets":
123
- return `Markets: ${scope.marketIds.join(", ")}`;
151
+ return formatMessage(summary.marketsScope, {
152
+ markets: scope.marketIds.join(", "),
153
+ });
124
154
  case "audiences":
125
- return `Audiences: ${scope.audiences.join(", ")}`;
155
+ return formatMessage(summary.audiencesScope, {
156
+ audiences: scope.audiences
157
+ .map((audience) => messages.common.audienceLabels[audience])
158
+ .join(", "),
159
+ });
126
160
  }
127
161
  }
128
- function summarizeDiscount(offer) {
162
+ function summarizeDiscount(offer, messages) {
129
163
  if (offer.discountType === "percentage") {
130
- return `${offer.discountPercent ?? "?"}%`;
164
+ return `${offer.discountPercent ?? messages.summaries.unknownPercentage}%`;
131
165
  }
132
166
  const cents = offer.discountAmountCents ?? 0;
133
167
  const currency = offer.currency ?? "";
134
168
  return `${(cents / 100).toFixed(2)} ${currency}`.trim();
135
169
  }
136
- function summarizeValidity(from, until) {
170
+ function summarizeValidity(from, until, messages) {
137
171
  if (from == null && until == null)
138
- return "Anytime";
172
+ return messages.summaries.anytime;
139
173
  const fmt = (iso) => iso.slice(0, 10);
140
- if (from == null)
141
- return `Until ${fmt(until ?? "")}`;
142
- if (until == null)
143
- return `From ${fmt(from)}`;
144
- return `${fmt(from)} - ${fmt(until)}`;
174
+ if (from == null) {
175
+ return formatMessage(messages.summaries.until, {
176
+ date: fmt(until ?? ""),
177
+ });
178
+ }
179
+ if (until == null) {
180
+ return formatMessage(messages.summaries.from, {
181
+ date: fmt(from),
182
+ });
183
+ }
184
+ return formatMessage(messages.summaries.range, {
185
+ from: fmt(from),
186
+ until: fmt(until),
187
+ });
145
188
  }
146
189
  function getOfferStatus(offer) {
147
190
  if (!offer.active)
@@ -165,34 +208,3 @@ function statusBadgeVariant(status) {
165
208
  return "outline";
166
209
  }
167
210
  }
168
- function applicationModeLabel(mode) {
169
- return mode === "auto" ? "Auto-applied" : "Code-redeemed";
170
- }
171
- function statusLabel(status) {
172
- switch (status) {
173
- case "active":
174
- return "Active";
175
- case "scheduled":
176
- return "Scheduled";
177
- case "expired":
178
- return "Expired";
179
- case "archived":
180
- return "Archived";
181
- }
182
- }
183
- function scopeKindLabel(scopeKind) {
184
- switch (scopeKind) {
185
- case "global":
186
- return "Global";
187
- case "products":
188
- return "Products";
189
- case "categories":
190
- return "Categories";
191
- case "destinations":
192
- return "Destinations";
193
- case "markets":
194
- return "Markets";
195
- case "audiences":
196
- return "Audiences";
197
- }
198
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/promotions-ui",
3
- "version": "0.35.0",
3
+ "version": "0.37.1",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,6 +15,21 @@
15
15
  "import": "./dist/index.js",
16
16
  "default": "./dist/index.js"
17
17
  },
18
+ "./i18n": {
19
+ "types": "./dist/i18n/index.d.ts",
20
+ "import": "./dist/i18n/index.js",
21
+ "default": "./dist/i18n/index.js"
22
+ },
23
+ "./i18n/en": {
24
+ "types": "./dist/i18n/en.d.ts",
25
+ "import": "./dist/i18n/en.js",
26
+ "default": "./dist/i18n/en.js"
27
+ },
28
+ "./i18n/ro": {
29
+ "types": "./dist/i18n/ro.d.ts",
30
+ "import": "./dist/i18n/ro.js",
31
+ "default": "./dist/i18n/ro.js"
32
+ },
18
33
  "./components/*": {
19
34
  "types": "./dist/*.d.ts",
20
35
  "import": "./dist/*.js",
@@ -26,8 +41,11 @@
26
41
  "react": "^19.0.0",
27
42
  "react-dom": "^19.0.0",
28
43
  "zod": "^4.3.6",
29
- "@voyantjs/promotions-react": "0.35.0",
30
- "@voyantjs/ui": "0.35.0"
44
+ "@voyantjs/promotions-react": "0.37.1",
45
+ "@voyantjs/ui": "0.37.1"
46
+ },
47
+ "dependencies": {
48
+ "@voyantjs/i18n": "0.37.1"
31
49
  },
32
50
  "devDependencies": {
33
51
  "@tanstack/react-query": "^5.96.2",
@@ -39,8 +57,9 @@
39
57
  "typescript": "^6.0.2",
40
58
  "vitest": "^4.1.2",
41
59
  "zod": "^4.3.6",
42
- "@voyantjs/promotions-react": "0.35.0",
43
- "@voyantjs/ui": "0.35.0",
60
+ "@voyantjs/i18n": "0.37.1",
61
+ "@voyantjs/promotions-react": "0.37.1",
62
+ "@voyantjs/ui": "0.37.1",
44
63
  "@voyantjs/voyant-typescript-config": "0.1.0"
45
64
  },
46
65
  "files": [