@greatapps/common 1.1.673 → 1.1.674

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.
@@ -64,6 +64,12 @@ const messages = {
64
64
 
65
65
  // Conta — modais e seções. `<b>` = trecho em negrito (rich text); `{count}` = interpolação.
66
66
  account: {
67
+ notificationTypes: {
68
+ projectUpdates: 'Atualizações de projetos',
69
+ securityAlerts: 'Alertas de segurança',
70
+ planBilling: 'Mudanças no plano ou faturamento',
71
+ scheduledMaintenance: 'Manutenções programadas',
72
+ },
67
73
  deleteAccount: {
68
74
  title: 'Excluir conta',
69
75
  description:
@@ -384,6 +390,41 @@ const messages = {
384
390
  },
385
391
 
386
392
  // Nomes dos idiomas exibidos no seletor.
393
+ // Planos: features e main features (mapApiPlanToUiPlan).
394
+ plans: {
395
+ features: {
396
+ unlimitedVisits: 'Visitas ilimitadas',
397
+ unlimitedLeads: 'Leads ilimitadas',
398
+ sharePages: 'Compartilhar páginas',
399
+ hosting: 'Hospedagem inclusa',
400
+ freeTemplates: 'Templates gratuitos',
401
+ onboarding: 'Reunião de Onboarding',
402
+ projectManagement: 'Gestão de projetos',
403
+ },
404
+ mainFeatures: {
405
+ pagesUnlimited: 'Páginas ilimitadas',
406
+ pageSingular: '1 página',
407
+ pagesPlural: '{count} páginas',
408
+ domainsUnlimited: 'Domínios ilimitados',
409
+ domainSingular: '1 domínio externo',
410
+ domainsPlural: '{count} domínios externos',
411
+ usersUnlimited: 'Usuários ilimitados',
412
+ },
413
+ },
414
+
415
+ // Periodicidade de cobrança.
416
+ periodicity: {
417
+ monthly: 'Mensal',
418
+ semiannual: 'Semestral',
419
+ annual: 'Anual',
420
+ suffixMonthly: '/mês',
421
+ suffixSemiannual: '/semestre',
422
+ suffixAnnual: '/ano',
423
+ labelMonthly: 'mensal',
424
+ labelSemiannual: 'semestral',
425
+ labelAnnual: 'anual',
426
+ },
427
+
387
428
  languages: {
388
429
  'pt-br': 'Português (Brasil)',
389
430
  'en-us': 'Inglês (EUA)',
@@ -434,6 +475,9 @@ const messages = {
434
475
  tooSmallBodyNoFileWithDimension: 'A imagem selecionada é menor que {dimensionText}. Faça envio de uma imagem com pelo menos {dimensionText} para continuar.',
435
476
  tooSmallBodyNoFileGeneric: 'A imagem selecionada é menor que o tamanho mínimo exigido. Faça envio de uma imagem maior para continuar.',
436
477
  sendAnother: 'Enviar outra',
478
+ errorSvg: 'Formato SVG não é suportado. Use PNG, JPEG, WebP ou GIF.',
479
+ errorFormat: 'Formato não suportado. Use PNG, JPEG, WebP ou GIF.',
480
+ errorTooLarge: 'Arquivo muito grande. Máximo {maxSize}MB.',
437
481
  },
438
482
 
439
483
  // Componentes: NotificationPageContent (Notifications.tsx), NotificationsPopover.tsx.
@@ -3,17 +3,24 @@ import {
3
3
  MAX_FILE_SIZE_MB,
4
4
  } from '../constants/image.constants'
5
5
 
6
+ type TranslateFn = (key: any, values?: Record<string, string | number>) => string;
7
+
6
8
  export interface ImageValidationResult {
7
9
  valid: boolean
8
10
  error?: string
9
11
  }
10
12
 
11
- export function validateImageFormat(file: File): ImageValidationResult {
13
+ export function validateImageFormat(
14
+ file: File,
15
+ translate?: TranslateFn
16
+ ): ImageValidationResult {
12
17
  // Bloqueia SVG explicitamente (segurança + não funciona com crop)
13
18
  if (file.type === 'image/svg+xml' || file.name.toLowerCase().endsWith('.svg')) {
14
19
  return {
15
20
  valid: false,
16
- error: 'Formato SVG não é suportado. Use PNG, JPEG, WebP ou GIF.',
21
+ error: translate
22
+ ? translate('common.imageUpload.errorSvg')
23
+ : 'Formato SVG não é suportado. Use PNG, JPEG, WebP ou GIF.',
17
24
  }
18
25
  }
19
26
 
@@ -24,7 +31,9 @@ export function validateImageFormat(file: File): ImageValidationResult {
24
31
  ) {
25
32
  return {
26
33
  valid: false,
27
- error: 'Formato não suportado. Use PNG, JPEG, WebP ou GIF.',
34
+ error: translate
35
+ ? translate('common.imageUpload.errorFormat')
36
+ : 'Formato não suportado. Use PNG, JPEG, WebP ou GIF.',
28
37
  }
29
38
  }
30
39
  return { valid: true }
@@ -32,22 +41,28 @@ export function validateImageFormat(file: File): ImageValidationResult {
32
41
 
33
42
  export function validateImageSize(
34
43
  file: File,
35
- maxSizeMB: number = MAX_FILE_SIZE_MB
44
+ maxSizeMB: number = MAX_FILE_SIZE_MB,
45
+ translate?: TranslateFn
36
46
  ): ImageValidationResult {
37
47
  if (file.size > maxSizeMB * 1024 * 1024) {
38
48
  return {
39
49
  valid: false,
40
- error: `Arquivo muito grande. Máximo ${maxSizeMB}MB.`,
50
+ error: translate
51
+ ? translate('common.imageUpload.errorTooLarge', { maxSize: maxSizeMB })
52
+ : `Arquivo muito grande. Máximo ${maxSizeMB}MB.`,
41
53
  }
42
54
  }
43
55
  return { valid: true }
44
56
  }
45
57
 
46
- export function validateImage(file: File): ImageValidationResult {
47
- const formatValidation = validateImageFormat(file)
58
+ export function validateImage(
59
+ file: File,
60
+ translate?: TranslateFn
61
+ ): ImageValidationResult {
62
+ const formatValidation = validateImageFormat(file, translate)
48
63
  if (!formatValidation.valid) return formatValidation
49
64
 
50
- const sizeValidation = validateImageSize(file)
65
+ const sizeValidation = validateImageSize(file, MAX_FILE_SIZE_MB, translate)
51
66
  if (!sizeValidation.valid) return sizeValidation
52
67
 
53
68
  return { valid: true }
@@ -13,37 +13,72 @@ import {
13
13
 
14
14
  export type PlanBillingPeriod = "monthly" | "semiannual" | "annual";
15
15
 
16
- const BASE_FEATURES: PlanFeature[] = [
17
- { icon: "check", text: "Visitas ilimitadas" },
18
- { icon: "check", text: "Leads ilimitadas" },
19
- { icon: "check", text: "Compartilhar páginas", tooltip: "sharePages" },
20
- { icon: "check", text: "Hospedagem inclusa" },
21
- { icon: "check", text: "SSL (HTTPS) + CDN" },
22
- { icon: "check", text: "Templates gratuitos" },
23
- ];
24
-
25
- function buildMainFeaturesFromItems(items: Plan["items"]): PlanMainFeatures {
16
+ type TranslateFn = (key: any, values?: Record<string, string | number>) => string;
17
+
18
+ const BASE_FEATURES_KEYS = [
19
+ { icon: "check", key: "plans.features.unlimitedVisits", fallback: "Visitas ilimitadas" },
20
+ { icon: "check", key: "plans.features.unlimitedLeads", fallback: "Leads ilimitadas" },
21
+ { icon: "check", key: "plans.features.sharePages", fallback: "Compartilhar páginas", tooltip: "sharePages" },
22
+ { icon: "check", key: "plans.features.hosting", fallback: "Hospedagem inclusa" },
23
+ // SSL (HTTPS) + CDN — brand/acronym: never translated
24
+ { icon: "check", key: null, fallback: "SSL (HTTPS) + CDN" },
25
+ { icon: "check", key: "plans.features.freeTemplates", fallback: "Templates gratuitos" },
26
+ ] as const;
27
+
28
+ function buildBaseFeatures(translate?: TranslateFn): PlanFeature[] {
29
+ return BASE_FEATURES_KEYS.map(({ icon, key, fallback, ...rest }) => {
30
+ const text = key && translate ? translate(`common.${key}`) : fallback;
31
+ const feature: PlanFeature = { icon, text };
32
+ if ("tooltip" in rest && rest.tooltip) feature.tooltip = rest.tooltip;
33
+ return feature;
34
+ });
35
+ }
36
+
37
+ function buildMainFeaturesFromItems(
38
+ items: Plan["items"],
39
+ translate?: TranslateFn
40
+ ): PlanMainFeatures {
26
41
  const pagesItem = items.find((i) => i.id_addon === ADDON_IDS.PAGES);
27
42
  const domainsItem = items.find((i) => i.id_addon === ADDON_IDS.DOMAINS);
28
43
 
29
44
  const pagesQty = pagesItem?.quantity ?? 0;
30
45
  const domainsQty = domainsItem?.quantity ?? 1;
31
46
 
32
- return {
33
- pages:
34
- pagesQty === 0
35
- ? "Páginas ilimitadas"
36
- : pagesQty === 1
37
- ? "1 página"
38
- : `${pagesQty} páginas`,
39
- domains:
40
- domainsQty >= 999
41
- ? "Domínios ilimitados"
42
- : domainsQty === 1
43
- ? "1 domínio externo"
44
- : `${domainsQty} domínios externos`,
45
- users: "Usuários ilimitados",
46
- };
47
+ let pages: string;
48
+ if (pagesQty === 0) {
49
+ pages = translate
50
+ ? translate("common.plans.mainFeatures.pagesUnlimited")
51
+ : "Páginas ilimitadas";
52
+ } else if (pagesQty === 1) {
53
+ pages = translate
54
+ ? translate("common.plans.mainFeatures.pageSingular")
55
+ : "1 página";
56
+ } else {
57
+ pages = translate
58
+ ? translate("common.plans.mainFeatures.pagesPlural", { count: pagesQty })
59
+ : `${pagesQty} páginas`;
60
+ }
61
+
62
+ let domains: string;
63
+ if (domainsQty >= 999) {
64
+ domains = translate
65
+ ? translate("common.plans.mainFeatures.domainsUnlimited")
66
+ : "Domínios ilimitados";
67
+ } else if (domainsQty === 1) {
68
+ domains = translate
69
+ ? translate("common.plans.mainFeatures.domainSingular")
70
+ : "1 domínio externo";
71
+ } else {
72
+ domains = translate
73
+ ? translate("common.plans.mainFeatures.domainsPlural", { count: domainsQty })
74
+ : `${domainsQty} domínios externos`;
75
+ }
76
+
77
+ const users = translate
78
+ ? translate("common.plans.mainFeatures.usersUnlimited")
79
+ : "Usuários ilimitados";
80
+
81
+ return { pages, domains, users };
47
82
  }
48
83
 
49
84
  function computePricingByPeriod(apiPlan: Plan, currency: CurrencyCode): PlanPricingByPeriod {
@@ -58,7 +93,7 @@ function computePricingByPeriod(apiPlan: Plan, currency: CurrencyCode): PlanPric
58
93
  };
59
94
  }
60
95
 
61
- function buildUiPlan(plan: Plan, currency: CurrencyCode): UiPlan {
96
+ function buildUiPlan(plan: Plan, currency: CurrencyCode, translate?: TranslateFn): UiPlan {
62
97
  const popular = plan.id === 2 || plan.id_plan === 2;
63
98
  const buttonVariant = popular ? "brand" : "default";
64
99
 
@@ -69,23 +104,21 @@ function buildUiPlan(plan: Plan, currency: CurrencyCode): UiPlan {
69
104
  const hasProjectAndOnboarding =
70
105
  lowerName.includes("agência") || lowerName.includes("agencia");
71
106
 
107
+ const onboardingText = translate
108
+ ? translate("common.plans.features.onboarding")
109
+ : "Reunião de Onboarding";
110
+ const projectMgmtText = translate
111
+ ? translate("common.plans.features.projectManagement")
112
+ : "Gestão de projetos";
113
+
72
114
  const features: PlanFeature[] = [
73
- ...BASE_FEATURES,
115
+ ...buildBaseFeatures(translate),
74
116
  hasProjectAndOnboarding
75
- ? { icon: "check", text: "Reunião de Onboarding" }
76
- : { icon: "x", text: "Reunião de Onboarding", disabled: true },
117
+ ? { icon: "check", text: onboardingText }
118
+ : { icon: "x", text: onboardingText, disabled: true },
77
119
  hasProjectAndOnboarding
78
- ? {
79
- icon: "check",
80
- text: "Gestão de projetos",
81
- tooltip: "projectManagement",
82
- }
83
- : {
84
- icon: "x",
85
- text: "Gestão de projetos",
86
- disabled: true,
87
- tooltip: "projectManagement",
88
- },
120
+ ? { icon: "check", text: projectMgmtText, tooltip: "projectManagement" }
121
+ : { icon: "x", text: projectMgmtText, disabled: true, tooltip: "projectManagement" },
89
122
  ];
90
123
 
91
124
  return {
@@ -93,7 +126,7 @@ function buildUiPlan(plan: Plan, currency: CurrencyCode): UiPlan {
93
126
  name: plan.name,
94
127
  isPopular: popular,
95
128
  buttonVariant,
96
- mainFeatures: buildMainFeaturesFromItems(plan.items),
129
+ mainFeatures: buildMainFeaturesFromItems(plan.items, translate),
97
130
  features,
98
131
  originalPrice: formatCurrencyNumber(monthlyFinal, 2, currency),
99
132
  price: formatCurrencyNumber(monthlyFinal, 2, currency),
@@ -105,17 +138,18 @@ function buildUiPlan(plan: Plan, currency: CurrencyCode): UiPlan {
105
138
  /**
106
139
  * Mapeia um plano da API para UiPlan em BRL.
107
140
  * Assinatura de 1 argumento — seguro em `plans.map(mapApiPlanToUiPlan)`.
141
+ * Passa `translate` opcional para i18n; sem ele cai nos literais pt-br (back-compat).
108
142
  */
109
- export function mapApiPlanToUiPlan(plan: Plan): UiPlan {
110
- return buildUiPlan(plan, "brl");
143
+ export function mapApiPlanToUiPlan(plan: Plan, translate?: TranslateFn): UiPlan {
144
+ return buildUiPlan(plan, "brl", translate);
111
145
  }
112
146
 
113
147
  /**
114
148
  * Variante currency-aware: a moeda vem do gateway da conta (getCurrencyForGateway).
115
- * Fábrica para uso ergonômico em `.map`: `plans.map(mapApiPlanToUiPlanForCurrency(currency))`.
149
+ * Fábrica para uso ergonômico em `.map`: `plans.map(mapApiPlanToUiPlanForCurrency(currency, translate))`.
116
150
  * Mantida separada de mapApiPlanToUiPlan para não quebrar callers que passam a fn direto pro `.map`.
117
151
  */
118
152
  export const mapApiPlanToUiPlanForCurrency =
119
- (currency: CurrencyCode) =>
153
+ (currency: CurrencyCode, translate?: TranslateFn) =>
120
154
  (plan: Plan): UiPlan =>
121
- buildUiPlan(plan, currency);
155
+ buildUiPlan(plan, currency, translate);
@@ -1,6 +1,8 @@
1
1
  import type { BillingPeriod } from '../types/billing-period.type';
2
2
  import type { CalculateSubscriptionResponse } from '../types/calculate-subscription.type';
3
3
 
4
+ type TranslateFn = (key: any, values?: Record<string, string | number>) => string;
5
+
4
6
  export function periodicityToBillingPeriod(
5
7
  periodicity: number | string | null | undefined
6
8
  ): BillingPeriod | null {
@@ -16,23 +18,38 @@ export function billingPeriodToNumeric(billingPeriod: BillingPeriod): 30 | 180 |
16
18
  return 30;
17
19
  }
18
20
 
19
- export function getPeriodicityLabel(periodicity: number | string | null | undefined): string {
21
+ export function getPeriodicityLabel(
22
+ periodicity: number | string | null | undefined,
23
+ translate?: TranslateFn
24
+ ): string {
20
25
  const period = periodicityToBillingPeriod(periodicity);
21
- if (period === 'semiannual') return 'Semestral';
22
- if (period === 'annual') return 'Anual';
23
- return 'Mensal';
26
+ if (period === 'semiannual')
27
+ return translate ? translate('common.periodicity.semiannual') : 'Semestral';
28
+ if (period === 'annual')
29
+ return translate ? translate('common.periodicity.annual') : 'Anual';
30
+ return translate ? translate('common.periodicity.monthly') : 'Mensal';
24
31
  }
25
32
 
26
- export function getBillingPeriodSuffix(billingPeriod: BillingPeriod): string {
27
- if (billingPeriod === 'semiannual') return '/semestre';
28
- if (billingPeriod === 'annual') return '/ano';
29
- return '/mês';
33
+ export function getBillingPeriodSuffix(
34
+ billingPeriod: BillingPeriod,
35
+ translate?: TranslateFn
36
+ ): string {
37
+ if (billingPeriod === 'semiannual')
38
+ return translate ? translate('common.periodicity.suffixSemiannual') : '/semestre';
39
+ if (billingPeriod === 'annual')
40
+ return translate ? translate('common.periodicity.suffixAnnual') : '/ano';
41
+ return translate ? translate('common.periodicity.suffixMonthly') : '/mês';
30
42
  }
31
43
 
32
- export function getBillingPeriodLabel(billingPeriod: BillingPeriod): string {
33
- if (billingPeriod === 'semiannual') return 'semestral';
34
- if (billingPeriod === 'annual') return 'anual';
35
- return 'mensal';
44
+ export function getBillingPeriodLabel(
45
+ billingPeriod: BillingPeriod,
46
+ translate?: TranslateFn
47
+ ): string {
48
+ if (billingPeriod === 'semiannual')
49
+ return translate ? translate('common.periodicity.labelSemiannual') : 'semestral';
50
+ if (billingPeriod === 'annual')
51
+ return translate ? translate('common.periodicity.labelAnnual') : 'anual';
52
+ return translate ? translate('common.periodicity.labelMonthly') : 'mensal';
36
53
  }
37
54
 
38
55
  export function getBillingPeriodMonths(billingPeriod: BillingPeriod): number {