@greatapps/common 1.1.675 → 1.1.677

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.
Files changed (240) hide show
  1. package/dist/components/account/ConfigurationsMyAccountModal.mjs +8 -6
  2. package/dist/components/account/ConfigurationsMyAccountModal.mjs.map +1 -1
  3. package/dist/components/account/ConfirmDeleteAccountModal.mjs +20 -19
  4. package/dist/components/account/ConfirmDeleteAccountModal.mjs.map +1 -1
  5. package/dist/components/account/ConfirmGlobalPreferencesModal.mjs +10 -11
  6. package/dist/components/account/ConfirmGlobalPreferencesModal.mjs.map +1 -1
  7. package/dist/components/account/DeleteAccountModal.mjs +34 -28
  8. package/dist/components/account/DeleteAccountModal.mjs.map +1 -1
  9. package/dist/components/account/DisableTwoFactorAuthModal.mjs +7 -5
  10. package/dist/components/account/DisableTwoFactorAuthModal.mjs.map +1 -1
  11. package/dist/components/account/IsntPossibleDeleteAccountModal.mjs +8 -9
  12. package/dist/components/account/IsntPossibleDeleteAccountModal.mjs.map +1 -1
  13. package/dist/components/account/TwoFactorAuthModal.mjs +22 -23
  14. package/dist/components/account/TwoFactorAuthModal.mjs.map +1 -1
  15. package/dist/components/account/constants.mjs +4 -4
  16. package/dist/components/account/constants.mjs.map +1 -1
  17. package/dist/components/account/hooks/useChangeEmailForm.mjs +5 -3
  18. package/dist/components/account/hooks/useChangeEmailForm.mjs.map +1 -1
  19. package/dist/components/account/hooks/useChangePasswordForm.mjs +26 -3
  20. package/dist/components/account/hooks/useChangePasswordForm.mjs.map +1 -1
  21. package/dist/components/account/hooks/useChangePhoneForm.mjs +18 -9
  22. package/dist/components/account/hooks/useChangePhoneForm.mjs.map +1 -1
  23. package/dist/components/account/hooks/useDisableTwoFactorAuthForm.mjs +26 -3
  24. package/dist/components/account/hooks/useDisableTwoFactorAuthForm.mjs.map +1 -1
  25. package/dist/components/account/hooks/useOtpVerification.mjs +19 -3
  26. package/dist/components/account/hooks/useOtpVerification.mjs.map +1 -1
  27. package/dist/components/account/hooks/useTwoFactorAuthForm.mjs +27 -4
  28. package/dist/components/account/hooks/useTwoFactorAuthForm.mjs.map +1 -1
  29. package/dist/components/account/sections/ChangeEmailModal.mjs +25 -28
  30. package/dist/components/account/sections/ChangeEmailModal.mjs.map +1 -1
  31. package/dist/components/account/sections/ChangePasswordSection.mjs +8 -6
  32. package/dist/components/account/sections/ChangePasswordSection.mjs.map +1 -1
  33. package/dist/components/account/sections/ChangePhoneModal.mjs +24 -27
  34. package/dist/components/account/sections/ChangePhoneModal.mjs.map +1 -1
  35. package/dist/components/account/sections/MyProfileSection.mjs +37 -22
  36. package/dist/components/account/sections/MyProfileSection.mjs.map +1 -1
  37. package/dist/components/account/sections/PreferencesSection.mjs +48 -24
  38. package/dist/components/account/sections/PreferencesSection.mjs.map +1 -1
  39. package/dist/components/account/sections/SecuritySection.mjs +11 -9
  40. package/dist/components/account/sections/SecuritySection.mjs.map +1 -1
  41. package/dist/components/account/sections/TokenSection.mjs +24 -8
  42. package/dist/components/account/sections/TokenSection.mjs.map +1 -1
  43. package/dist/components/layouts/AppMobileNavBar.mjs +2 -148
  44. package/dist/components/layouts/AppMobileNavBar.mjs.map +1 -1
  45. package/dist/components/layouts/NavBar.mjs +5 -3
  46. package/dist/components/layouts/NavBar.mjs.map +1 -1
  47. package/dist/components/layouts/NotificationsPopover.mjs +5 -3
  48. package/dist/components/layouts/NotificationsPopover.mjs.map +1 -1
  49. package/dist/components/layouts/ProfilePopover.mjs +6 -4
  50. package/dist/components/layouts/ProfilePopover.mjs.map +1 -1
  51. package/dist/components/layouts/UsersSelectorPopover.mjs +9 -7
  52. package/dist/components/layouts/UsersSelectorPopover.mjs.map +1 -1
  53. package/dist/components/modals/BuyCreditsModal.mjs +17 -18
  54. package/dist/components/modals/BuyCreditsModal.mjs.map +1 -1
  55. package/dist/components/modals/CreditsDisabledModal.mjs +6 -4
  56. package/dist/components/modals/CreditsDisabledModal.mjs.map +1 -1
  57. package/dist/components/modals/PaidPlanRequiredModal.mjs +5 -3
  58. package/dist/components/modals/PaidPlanRequiredModal.mjs.map +1 -1
  59. package/dist/components/modals/cards/AddCardModal.mjs +64 -46
  60. package/dist/components/modals/cards/AddCardModal.mjs.map +1 -1
  61. package/dist/components/modals/cards/CannotDeleteCardModal.mjs +9 -7
  62. package/dist/components/modals/cards/CannotDeleteCardModal.mjs.map +1 -1
  63. package/dist/components/modals/cards/CardFormFields.mjs +65 -51
  64. package/dist/components/modals/cards/CardFormFields.mjs.map +1 -1
  65. package/dist/components/modals/cards/DeleteCardModal.mjs +10 -8
  66. package/dist/components/modals/cards/DeleteCardModal.mjs.map +1 -1
  67. package/dist/components/navigation/CancelledSubscriptionBanner.mjs +4 -2
  68. package/dist/components/navigation/CancelledSubscriptionBanner.mjs.map +1 -1
  69. package/dist/components/navigation/OverdueInvoiceBanner.mjs +4 -2
  70. package/dist/components/navigation/OverdueInvoiceBanner.mjs.map +1 -1
  71. package/dist/components/navigation/PendingCancellationBanner.mjs +4 -6
  72. package/dist/components/navigation/PendingCancellationBanner.mjs.map +1 -1
  73. package/dist/components/navigation/ProjectSelectorPopover.mjs +8 -6
  74. package/dist/components/navigation/ProjectSelectorPopover.mjs.map +1 -1
  75. package/dist/components/navigation/UpcomingInvoiceBanner.mjs +4 -6
  76. package/dist/components/navigation/UpcomingInvoiceBanner.mjs.map +1 -1
  77. package/dist/components/navigation/subcomponents/BottomLinks.mjs +5 -3
  78. package/dist/components/navigation/subcomponents/BottomLinks.mjs.map +1 -1
  79. package/dist/components/navigation/subcomponents/CreditsCard.mjs +3 -1
  80. package/dist/components/navigation/subcomponents/CreditsCard.mjs.map +1 -1
  81. package/dist/components/navigation/subcomponents/PlanCard.mjs +3 -1
  82. package/dist/components/navigation/subcomponents/PlanCard.mjs.map +1 -1
  83. package/dist/components/navigation/subcomponents/ProjectSelector.mjs +5 -3
  84. package/dist/components/navigation/subcomponents/ProjectSelector.mjs.map +1 -1
  85. package/dist/components/pages/NotFoundPage.mjs +5 -3
  86. package/dist/components/pages/NotFoundPage.mjs.map +1 -1
  87. package/dist/components/pages/notifications/Notifications.mjs +13 -10
  88. package/dist/components/pages/notifications/Notifications.mjs.map +1 -1
  89. package/dist/components/ui/buttons/CopyButton.mjs +9 -5
  90. package/dist/components/ui/buttons/CopyButton.mjs.map +1 -1
  91. package/dist/components/ui/data-display/CardItem.mjs +7 -5
  92. package/dist/components/ui/data-display/CardItem.mjs.map +1 -1
  93. package/dist/components/ui/data-display/PaymentInfoCard.mjs +10 -8
  94. package/dist/components/ui/data-display/PaymentInfoCard.mjs.map +1 -1
  95. package/dist/components/ui/feedback/CircularProgress.mjs.map +1 -1
  96. package/dist/components/ui/feedback/DefaultCircularProgress.mjs +3 -1
  97. package/dist/components/ui/feedback/DefaultCircularProgress.mjs.map +1 -1
  98. package/dist/components/ui/form/ComboboxField.mjs +11 -6
  99. package/dist/components/ui/form/ComboboxField.mjs.map +1 -1
  100. package/dist/components/ui/form/DatePicker.mjs +5 -2
  101. package/dist/components/ui/form/DatePicker.mjs.map +1 -1
  102. package/dist/components/ui/form/DateRangePicker.mjs +5 -2
  103. package/dist/components/ui/form/DateRangePicker.mjs.map +1 -1
  104. package/dist/components/ui/form/PhoneInput.mjs +5 -3
  105. package/dist/components/ui/form/PhoneInput.mjs.map +1 -1
  106. package/dist/components/ui/form/SelectField.mjs +6 -3
  107. package/dist/components/ui/form/SelectField.mjs.map +1 -1
  108. package/dist/components/widgets/ImageUpload/ImageCropModal.mjs +11 -18
  109. package/dist/components/widgets/ImageUpload/ImageCropModal.mjs.map +1 -1
  110. package/dist/components/widgets/ImageUpload/ImageTooSmallModal.mjs +6 -15
  111. package/dist/components/widgets/ImageUpload/ImageTooSmallModal.mjs.map +1 -1
  112. package/dist/i18n/config.mjs +13 -0
  113. package/dist/i18n/config.mjs.map +1 -0
  114. package/dist/i18n/country-language.mjs +74 -0
  115. package/dist/i18n/country-language.mjs.map +1 -0
  116. package/dist/i18n/messages/en-us.mjs +494 -0
  117. package/dist/i18n/messages/en-us.mjs.map +1 -0
  118. package/dist/i18n/messages/es-es.mjs +494 -0
  119. package/dist/i18n/messages/es-es.mjs.map +1 -0
  120. package/dist/i18n/messages/index.mjs +16 -0
  121. package/dist/i18n/messages/index.mjs.map +1 -0
  122. package/dist/i18n/messages/pt-br.mjs +507 -0
  123. package/dist/i18n/messages/pt-br.mjs.map +1 -0
  124. package/dist/i18n/normalize.mjs +12 -0
  125. package/dist/i18n/normalize.mjs.map +1 -0
  126. package/dist/i18n/resolve-locale.mjs +42 -0
  127. package/dist/i18n/resolve-locale.mjs.map +1 -0
  128. package/dist/i18n.mjs +14 -0
  129. package/dist/i18n.mjs.map +1 -0
  130. package/dist/index.mjs +19 -3
  131. package/dist/index.mjs.map +1 -1
  132. package/dist/modules/accounts/hooks/use-currency-formatter.hook.mjs +20 -0
  133. package/dist/modules/accounts/hooks/use-currency-formatter.hook.mjs.map +1 -0
  134. package/dist/modules/cards/hooks/handle-delete-card.hook.mjs +2 -1
  135. package/dist/modules/cards/hooks/handle-delete-card.hook.mjs.map +1 -1
  136. package/dist/modules/images/utils/validate-image.mjs +8 -8
  137. package/dist/modules/images/utils/validate-image.mjs.map +1 -1
  138. package/dist/modules/plans/hooks/list-plans.hook.mjs +2 -2
  139. package/dist/modules/plans/hooks/list-plans.hook.mjs.map +1 -1
  140. package/dist/modules/plans/types/plan.type.mjs +5 -1
  141. package/dist/modules/plans/types/plan.type.mjs.map +1 -1
  142. package/dist/modules/plans/utils/map-api-plan-to-ui.mjs +61 -35
  143. package/dist/modules/plans/utils/map-api-plan-to-ui.mjs.map +1 -1
  144. package/dist/modules/projects/services/projects.service.mjs +9 -0
  145. package/dist/modules/projects/services/projects.service.mjs.map +1 -1
  146. package/dist/modules/subscriptions/utils/periodicity.mjs +20 -14
  147. package/dist/modules/subscriptions/utils/periodicity.mjs.map +1 -1
  148. package/dist/providers/auth.provider.mjs +9 -1
  149. package/dist/providers/auth.provider.mjs.map +1 -1
  150. package/dist/server.mjs +2 -0
  151. package/dist/server.mjs.map +1 -1
  152. package/dist/utils/format/currency.mjs +32 -4
  153. package/dist/utils/format/currency.mjs.map +1 -1
  154. package/dist/utils/intl/cookie-domain.mjs +17 -0
  155. package/dist/utils/intl/cookie-domain.mjs.map +1 -0
  156. package/dist/utils/intl/locale-cookie.mjs +13 -0
  157. package/dist/utils/intl/locale-cookie.mjs.map +1 -0
  158. package/dist/utils/intl/locales.mjs +3 -2
  159. package/dist/utils/intl/locales.mjs.map +1 -1
  160. package/package.json +7 -1
  161. package/src/components/account/ConfigurationsMyAccountModal.tsx +10 -6
  162. package/src/components/account/ConfirmDeleteAccountModal.tsx +22 -13
  163. package/src/components/account/ConfirmGlobalPreferencesModal.tsx +12 -10
  164. package/src/components/account/DeleteAccountModal.tsx +34 -24
  165. package/src/components/account/DisableTwoFactorAuthModal.tsx +11 -6
  166. package/src/components/account/IsntPossibleDeleteAccountModal.tsx +8 -6
  167. package/src/components/account/TwoFactorAuthModal.tsx +33 -29
  168. package/src/components/account/constants.ts +8 -5
  169. package/src/components/account/hooks/useChangeEmailForm.tsx +5 -3
  170. package/src/components/account/hooks/useChangePasswordForm.tsx +20 -6
  171. package/src/components/account/hooks/useChangePhoneForm.tsx +16 -10
  172. package/src/components/account/hooks/useDisableTwoFactorAuthForm.tsx +20 -6
  173. package/src/components/account/hooks/useOtpVerification.tsx +16 -4
  174. package/src/components/account/hooks/useTwoFactorAuthForm.tsx +21 -7
  175. package/src/components/account/sections/ChangeEmailModal.tsx +35 -21
  176. package/src/components/account/sections/ChangePasswordSection.tsx +10 -6
  177. package/src/components/account/sections/ChangePhoneModal.tsx +50 -22
  178. package/src/components/account/sections/MyProfileSection.tsx +37 -23
  179. package/src/components/account/sections/PreferencesSection.tsx +52 -25
  180. package/src/components/account/sections/SecuritySection.tsx +19 -10
  181. package/src/components/account/sections/TokenSection.tsx +22 -10
  182. package/src/components/layouts/AppMobileNavBar.tsx +1 -154
  183. package/src/components/layouts/NavBar.tsx +5 -3
  184. package/src/components/layouts/NotificationsPopover.tsx +5 -3
  185. package/src/components/layouts/ProfilePopover.tsx +6 -4
  186. package/src/components/layouts/UsersSelectorPopover.tsx +9 -7
  187. package/src/components/modals/BuyCreditsModal.tsx +18 -16
  188. package/src/components/modals/CreditsDisabledModal.tsx +6 -6
  189. package/src/components/modals/PaidPlanRequiredModal.tsx +5 -3
  190. package/src/components/modals/cards/AddCardModal.tsx +89 -62
  191. package/src/components/modals/cards/CannotDeleteCardModal.tsx +9 -8
  192. package/src/components/modals/cards/CardFormFields.tsx +93 -75
  193. package/src/components/modals/cards/DeleteCardModal.tsx +10 -8
  194. package/src/components/navigation/CancelledSubscriptionBanner.tsx +4 -2
  195. package/src/components/navigation/OverdueInvoiceBanner.tsx +4 -2
  196. package/src/components/navigation/PendingCancellationBanner.tsx +4 -2
  197. package/src/components/navigation/ProjectSelectorPopover.tsx +8 -6
  198. package/src/components/navigation/UpcomingInvoiceBanner.tsx +4 -2
  199. package/src/components/navigation/subcomponents/BottomLinks.tsx +5 -3
  200. package/src/components/navigation/subcomponents/CreditsCard.tsx +3 -1
  201. package/src/components/navigation/subcomponents/PlanCard.tsx +3 -1
  202. package/src/components/navigation/subcomponents/ProjectSelector.tsx +5 -3
  203. package/src/components/pages/NotFoundPage.tsx +5 -3
  204. package/src/components/pages/notifications/Notifications.tsx +13 -10
  205. package/src/components/ui/buttons/CopyButton.tsx +10 -5
  206. package/src/components/ui/data-display/CardItem.tsx +7 -5
  207. package/src/components/ui/data-display/PaymentInfoCard.tsx +10 -8
  208. package/src/components/ui/feedback/CircularProgress.tsx +4 -0
  209. package/src/components/ui/feedback/DefaultCircularProgress.tsx +3 -1
  210. package/src/components/ui/form/ComboboxField.tsx +11 -6
  211. package/src/components/ui/form/DatePicker.tsx +5 -2
  212. package/src/components/ui/form/DateRangePicker.tsx +5 -2
  213. package/src/components/ui/form/PhoneInput.tsx +5 -3
  214. package/src/components/ui/form/SelectField.tsx +6 -3
  215. package/src/components/widgets/ImageUpload/ImageCropModal.tsx +19 -11
  216. package/src/components/widgets/ImageUpload/ImageTooSmallModal.tsx +6 -5
  217. package/src/i18n/config.ts +17 -0
  218. package/src/i18n/country-language.ts +60 -0
  219. package/src/i18n/messages/en-us.ts +514 -0
  220. package/src/i18n/messages/es-es.ts +515 -0
  221. package/src/i18n/messages/index.ts +24 -0
  222. package/src/i18n/messages/pt-br.ts +538 -0
  223. package/src/i18n/normalize.ts +15 -0
  224. package/src/i18n/resolve-locale.ts +77 -0
  225. package/src/i18n.ts +10 -0
  226. package/src/index.ts +13 -2
  227. package/src/modules/accounts/hooks/use-currency-formatter.hook.tsx +32 -0
  228. package/src/modules/cards/hooks/handle-delete-card.hook.ts +8 -3
  229. package/src/modules/images/utils/validate-image.ts +23 -8
  230. package/src/modules/plans/hooks/list-plans.hook.ts +2 -2
  231. package/src/modules/plans/types/plan.type.ts +4 -0
  232. package/src/modules/plans/utils/map-api-plan-to-ui.ts +109 -47
  233. package/src/modules/projects/services/projects.service.ts +12 -0
  234. package/src/modules/subscriptions/utils/periodicity.ts +29 -12
  235. package/src/providers/auth.provider.tsx +13 -1
  236. package/src/server.ts +2 -0
  237. package/src/utils/format/currency.ts +62 -7
  238. package/src/utils/intl/cookie-domain.ts +29 -0
  239. package/src/utils/intl/locale-cookie.ts +18 -0
  240. package/src/utils/intl/locales.ts +3 -2
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import { useCurrentAccount } from './current-account.hook';
4
+ import {
5
+ formatCurrency as formatCurrencyBase,
6
+ formatCurrencyNumber as formatCurrencyNumberBase,
7
+ getCurrencyForGateway,
8
+ type CurrencyCode,
9
+ } from '../../../utils/format/currency';
10
+
11
+ /**
12
+ * Formatadores de moeda já amarrados à moeda da conta atual (derivada de account.gateway).
13
+ *
14
+ * Use em componentes client para exibir preços sem passar a moeda manualmente — a moeda é
15
+ * determinada pelo backend (roteamento de gateway) e é read-only para o usuário.
16
+ *
17
+ * Conta stripe_usd → USD ('$1,234.56'); demais → BRL ('R$ 1.234,56').
18
+ */
19
+ export function useCurrencyFormatter(): {
20
+ currency: CurrencyCode;
21
+ formatCurrency: (value: string | number) => string;
22
+ formatCurrencyNumber: (value?: number | null, decimals?: number) => string;
23
+ } {
24
+ const { data: account } = useCurrentAccount();
25
+ const currency = getCurrencyForGateway(account?.gateway);
26
+
27
+ return {
28
+ currency,
29
+ formatCurrency: (value) => formatCurrencyBase(value, currency),
30
+ formatCurrencyNumber: (value, decimals = 2) => formatCurrencyNumberBase(value, decimals, currency),
31
+ };
32
+ }
@@ -3,6 +3,7 @@
3
3
  import { useCallback } from 'react';
4
4
  import { useModalManager } from '../../../store/useModalManager';
5
5
  import { useActiveSubscription } from '../../subscriptions/hooks/find-active-subscription.hook';
6
+ import { hasSubscriptionExpired } from '../../subscriptions/utils/has-subscription-expired';
6
7
  import type { Card } from '../types';
7
8
 
8
9
  type UseHandleDeleteCardOptions = {
@@ -18,11 +19,15 @@ export function useHandleDeleteCard({
18
19
 
19
20
  return useCallback(
20
21
  (card: Card) => {
22
+ // Só bloqueia a exclusão quando o cartão é o método de pagamento de uma
23
+ // assinatura REALMENTE ativa (mesma definição de ConfigurationsMyAccountModal).
24
+ // Se a assinatura foi cancelada ou já expirou, ela não está mais ativa —
25
+ // pula a verificação e segue direto pra exclusão do cartão.
21
26
  const isCardLinkedToSubscription =
22
27
  !!activeSubscription &&
23
- !!activeSubscription.date_cancellation &&
24
- (!activeSubscription.date_due ||
25
- activeSubscription.date_due < new Date()) &&
28
+ activeSubscription.active === true &&
29
+ !activeSubscription.date_cancellation &&
30
+ !hasSubscriptionExpired(activeSubscription.date_due) &&
26
31
  card.id.toString() === activeSubscription.id_card?.toString();
27
32
 
28
33
  if (isCardLinkedToSubscription) {
@@ -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,11 +13,11 @@ export function usePlans() {
13
13
  return useQuery<Plan[]>({
14
14
  queryKey,
15
15
  queryFn: ({ signal }) => apiFetch<Plan[]>('/api/plans', { signal }),
16
- staleTime: Infinity,
16
+ staleTime: 60 * 1000,
17
17
  gcTime: Infinity,
18
18
  refetchOnWindowFocus: false,
19
19
  refetchOnReconnect: false,
20
- refetchOnMount: false,
20
+ refetchOnMount: true,
21
21
  retry: 1,
22
22
  });
23
23
  }
@@ -5,6 +5,7 @@ export const PlanItemSchema = z.object({
5
5
  id_addon: z.number(),
6
6
  quantity: z.number(),
7
7
  value: z.number(),
8
+ value_usd: z.coerce.number().nullish(),
8
9
  });
9
10
 
10
11
  export type PlanItem = z.infer<typeof PlanItemSchema>;
@@ -14,6 +15,9 @@ export const PlanSchema = z.object({
14
15
  id_plan: z.number().optional(),
15
16
  name: z.string(),
16
17
  value: z.number().default(0),
18
+ // Preço em USD (conta stripe_usd). Operador cadastra no backend; ausente em planos
19
+ // sem suporte internacional. Espelha a coluna value_usd de gapps-r3-api.
20
+ value_usd: z.coerce.number().nullish(),
17
21
  type: z.string().optional(),
18
22
  discount_semester: z.number().default(0),
19
23
  discount_annual: z.number().default(0),
@@ -6,45 +6,85 @@ import type {
6
6
  UiPlan,
7
7
  } from "../types/plan.type";
8
8
  import { ADDON_IDS } from "../../subscriptions/constants/addons.constants";
9
- import { formatCurrencyNumber } from "../../../utils/format/currency";
9
+ import {
10
+ formatCurrencyNumber,
11
+ type CurrencyCode,
12
+ } from "../../../utils/format/currency";
10
13
 
11
14
  export type PlanBillingPeriod = "monthly" | "semiannual" | "annual";
12
15
 
13
- const BASE_FEATURES: PlanFeature[] = [
14
- { icon: "check", text: "Visitas ilimitadas" },
15
- { icon: "check", text: "Leads ilimitadas" },
16
- { icon: "check", text: "Compartilhar páginas", tooltip: "sharePages" },
17
- { icon: "check", text: "Hospedagem inclusa" },
18
- { icon: "check", text: "SSL (HTTPS) + CDN" },
19
- { icon: "check", text: "Templates gratuitos" },
20
- ];
16
+ type TranslateFn = (key: any, values?: Record<string, string | number>) => string;
21
17
 
22
- function buildMainFeaturesFromItems(items: Plan["items"]): PlanMainFeatures {
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 {
23
41
  const pagesItem = items.find((i) => i.id_addon === ADDON_IDS.PAGES);
24
42
  const domainsItem = items.find((i) => i.id_addon === ADDON_IDS.DOMAINS);
25
43
 
26
44
  const pagesQty = pagesItem?.quantity ?? 0;
27
45
  const domainsQty = domainsItem?.quantity ?? 1;
28
46
 
29
- return {
30
- pages:
31
- pagesQty === 0
32
- ? "Páginas ilimitadas"
33
- : pagesQty === 1
34
- ? "1 página"
35
- : `${pagesQty} páginas`,
36
- domains:
37
- domainsQty >= 999
38
- ? "Domínios ilimitados"
39
- : domainsQty === 1
40
- ? "1 domínio externo"
41
- : `${domainsQty} domínios externos`,
42
- users: "Usuários ilimitados",
43
- };
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 };
44
82
  }
45
83
 
46
- function computePricingByPeriod(apiPlan: Plan): PlanPricingByPeriod {
47
- const monthly = apiPlan.value;
84
+ function computePricingByPeriod(apiPlan: Plan, currency: CurrencyCode): PlanPricingByPeriod {
85
+ // Conta USD usa value_usd; fallback pro value (BRL) evita NaN quando o operador ainda
86
+ // não cadastrou o preço internacional. Os descontos por período são percentuais — aplicam igual.
87
+ const monthly = currency === "usd" ? (apiPlan.value_usd ?? apiPlan.value) : apiPlan.value;
48
88
  const round2 = (n: number) => Math.round(n * 100) / 100;
49
89
  return {
50
90
  monthly,
@@ -53,46 +93,68 @@ function computePricingByPeriod(apiPlan: Plan): PlanPricingByPeriod {
53
93
  };
54
94
  }
55
95
 
56
- export function mapApiPlanToUiPlan(plan: Plan): UiPlan {
96
+ function buildUiPlan(plan: Plan, currency: CurrencyCode, translate?: TranslateFn): UiPlan {
57
97
  const popular = plan.id === 2 || plan.id_plan === 2;
58
98
  const buttonVariant = popular ? "brand" : "default";
59
99
 
60
- const pricingByPeriod = computePricingByPeriod(plan);
100
+ const pricingByPeriod = computePricingByPeriod(plan, currency);
61
101
  const monthlyFinal = pricingByPeriod.monthly;
62
102
 
63
103
  const lowerName = plan.name.toLowerCase();
64
104
  const hasProjectAndOnboarding =
65
105
  lowerName.includes("agência") || lowerName.includes("agencia");
66
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
+
67
114
  const features: PlanFeature[] = [
68
- ...BASE_FEATURES,
115
+ ...buildBaseFeatures(translate),
69
116
  hasProjectAndOnboarding
70
- ? { icon: "check", text: "Reunião de Onboarding" }
71
- : { icon: "x", text: "Reunião de Onboarding", disabled: true },
117
+ ? { icon: "check", text: onboardingText }
118
+ : { icon: "x", text: onboardingText, disabled: true },
72
119
  hasProjectAndOnboarding
73
- ? {
74
- icon: "check",
75
- text: "Gestão de projetos",
76
- tooltip: "projectManagement",
77
- }
78
- : {
79
- icon: "x",
80
- text: "Gestão de projetos",
81
- disabled: true,
82
- tooltip: "projectManagement",
83
- },
120
+ ? { icon: "check", text: projectMgmtText, tooltip: "projectManagement" }
121
+ : { icon: "x", text: projectMgmtText, disabled: true, tooltip: "projectManagement" },
84
122
  ];
85
123
 
124
+ const currencyAwareItems = plan.items.map((item) => ({
125
+ ...item,
126
+ value: currency === "usd" ? (item.value_usd ?? item.value) : item.value,
127
+ }));
128
+
86
129
  return {
87
130
  planId: plan.id,
88
131
  name: plan.name,
89
132
  isPopular: popular,
90
133
  buttonVariant,
91
- mainFeatures: buildMainFeaturesFromItems(plan.items),
134
+ mainFeatures: buildMainFeaturesFromItems(plan.items, translate),
92
135
  features,
93
- originalPrice: formatCurrencyNumber(monthlyFinal),
94
- price: formatCurrencyNumber(monthlyFinal),
136
+ originalPrice: formatCurrencyNumber(monthlyFinal, 2, currency),
137
+ price: formatCurrencyNumber(monthlyFinal, 2, currency),
95
138
  pricingByPeriod,
96
- items: plan.items,
139
+ items: currencyAwareItems,
97
140
  };
98
141
  }
142
+
143
+ /**
144
+ * Mapeia um plano da API para UiPlan em BRL.
145
+ * Assinatura de 1 argumento — seguro em `plans.map(mapApiPlanToUiPlan)`.
146
+ * Passa `translate` opcional para i18n; sem ele cai nos literais pt-br (back-compat).
147
+ */
148
+ export function mapApiPlanToUiPlan(plan: Plan, translate?: TranslateFn): UiPlan {
149
+ return buildUiPlan(plan, "brl", translate);
150
+ }
151
+
152
+ /**
153
+ * Variante currency-aware: a moeda vem do gateway da conta (getCurrencyForGateway).
154
+ * Fábrica para uso ergonômico em `.map`: `plans.map(mapApiPlanToUiPlanForCurrency(currency, translate))`.
155
+ * Mantida separada de mapApiPlanToUiPlan para não quebrar callers que passam a fn direto pro `.map`.
156
+ */
157
+ export const mapApiPlanToUiPlanForCurrency =
158
+ (currency: CurrencyCode, translate?: TranslateFn) =>
159
+ (plan: Plan): UiPlan =>
160
+ buildUiPlan(plan, currency, translate);
@@ -131,6 +131,18 @@ class ProjectsService {
131
131
  body: JSON.stringify({ id_project_to_move: moveToProjectId }),
132
132
  });
133
133
 
134
+ // Alguns backends respondem `null` no delete quando a exclusão foi bem-sucedida.
135
+ // Nesse caso não há payload para validar e o fluxo deve seguir sem erro.
136
+ if (response == null) {
137
+ console.warn('[ProjectsService.delete] null response treated as success', {
138
+ id_account,
139
+ id_user,
140
+ projectId,
141
+ moveToProjectId,
142
+ });
143
+ return;
144
+ }
145
+
134
146
  if (response.status === 0) {
135
147
  throw new ApiError(
136
148
  response.message || 'Erro ao deletar projeto',
@@ -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 {
@@ -5,6 +5,7 @@ import {
5
5
  ReactNode,
6
6
  useCallback,
7
7
  useContext,
8
+ useEffect,
8
9
  useMemo,
9
10
  } from "react";
10
11
  import { useQueryClient } from "@tanstack/react-query";
@@ -41,8 +42,10 @@ import {
41
42
  useCurrentAccount,
42
43
  ACCOUNT_QUERY_KEY,
43
44
  } from "../modules/accounts/hooks/current-account.hook";
44
- import { useWhitelabelUrls } from "./whitelabel.provider";
45
+ import { useWhitelabelUrls, useWhitelabel } from "./whitelabel.provider";
45
46
  import { useWlQueryKey } from "../hooks/useAuthQueryKey";
47
+ import { normalizeLocale } from "../i18n/normalize";
48
+ import { setLocaleCookie } from "../utils/intl/locale-cookie";
46
49
 
47
50
  interface AuthContextProps extends AuthState {
48
51
  login: (credentials: LoginRequest) => Promise<LoginResult>;
@@ -68,8 +71,17 @@ export function AuthProvider({ children }: AuthProviderProps) {
68
71
  const queryClient = useQueryClient();
69
72
  const setUserData = useSetUserData();
70
73
  const { accountsUrl, pagesUrl } = useWhitelabelUrls();
74
+ const { whitelabel } = useWhitelabel();
71
75
  const accountKey = useWlQueryKey([...ACCOUNT_QUERY_KEY]);
72
76
 
77
+ // Mantém o cookie NEXT_LOCALE alinhado à preferência da conta (login, sessão existente,
78
+ // atualização). Compartilhado entre subdomínios via domínio do whitelabel. A próxima
79
+ // navegação/requisição passa a resolver o locale pela preferência do usuário.
80
+ useEffect(() => {
81
+ const locale = normalizeLocale(user?.language);
82
+ if (locale) setLocaleCookie(locale, whitelabel?.domain);
83
+ }, [user?.language, whitelabel?.domain]);
84
+
73
85
  const { mutateAsync: loginMutate, isPending: isLogging } = useLogin();
74
86
  const { mutateAsync: registerMutate, isPending: isRegistering } =
75
87
  useRegister();
package/src/server.ts CHANGED
@@ -74,6 +74,8 @@ export {
74
74
  // Server Utils
75
75
  export { getClientInfoFromRequest } from './infra/utils/client-info';
76
76
  export { getUserContext } from './modules/auth/utils/get-user-context';
77
+ export { resolveLocale } from './i18n/resolve-locale';
78
+ export type { ResolveLocaleOptions } from './i18n/resolve-locale';
77
79
  export { requireValidSession } from './modules/auth/utils/require-valid-session';
78
80
  export { requireTokenNotExpired } from './modules/auth/utils/require-token-not-expired';
79
81
  export { buildWlOverride, buildWlOverrideFromJwt } from './modules/auth/utils/build-wl-override';
@@ -1,14 +1,69 @@
1
1
  /**
2
- * Formata um número (opcional) para moeda BRL
3
- * @param value - Número a ser formatado (pode ser undefined ou null)
4
- * @param decimals - Casas decimais (padrão: 2)
5
- * @returns String formatada como "R$ X.XXX,XX"
2
+ * Moeda de exibição. Espelha getCurrencyForGateway do backend (gapps-r3-api):
3
+ * 'stripe_usd' USD; demais gateways BRL.
6
4
  */
7
- export const formatCurrencyNumber = (value?: number | null, decimals = 2): string => {
5
+ export type CurrencyCode = 'brl' | 'usd';
6
+
7
+ const CURRENCY_FORMAT: Record<CurrencyCode, { locale: string; currency: string }> = {
8
+ brl: { locale: 'pt-BR', currency: 'BRL' },
9
+ usd: { locale: 'en-US', currency: 'USD' },
10
+ };
11
+
12
+ /**
13
+ * Deriva a moeda de exibição a partir do gateway da conta.
14
+ * A moeda é definida pelo backend no roteamento de gateway — read-only para o usuário.
15
+ * @param gateway - account.gateway ('stripe_usd' | 'stripe_brl' | 'vindi' | ...)
16
+ */
17
+ export const getCurrencyForGateway = (gateway?: string | null): CurrencyCode =>
18
+ gateway === 'stripe_usd' ? 'usd' : 'brl';
19
+
20
+ /**
21
+ * Formata um valor (string ou número) como moeda.
22
+ * String: assume centavos (remove não-dígitos e divide por 100) — comportamento legado.
23
+ * @param value - valor a formatar
24
+ * @param currency - moeda de exibição (padrão 'brl' por retrocompatibilidade)
25
+ * @returns String formatada (ex: "R$ 1.234,56" ou "$1,234.56")
26
+ */
27
+ export const formatCurrency = (value: string | number, currency: CurrencyCode = 'brl'): string => {
28
+ const fmt = CURRENCY_FORMAT[currency];
29
+ if (typeof value === 'number') {
30
+ return value.toLocaleString(fmt.locale, {
31
+ style: 'currency',
32
+ currency: fmt.currency,
33
+ });
34
+ }
35
+
36
+ const numericValue = value.replace(/\D/g, '');
37
+ const numberValue = parseFloat(numericValue) / 100;
38
+ return numberValue.toLocaleString(fmt.locale, {
39
+ style: 'currency',
40
+ currency: fmt.currency,
41
+ });
42
+ };
43
+
44
+ /**
45
+ * Converte uma string de moeda para número (centavos / 100). Independente de moeda.
46
+ * @param value - String com valor de moeda
47
+ * @returns Número decimal
48
+ */
49
+ export const parseCurrencyToNumber = (value: string): number => {
50
+ const numericValue = value.replace(/\D/g, '');
51
+ return parseFloat(numericValue) / 100;
52
+ };
53
+
54
+ /**
55
+ * Formata um número (opcional) como moeda.
56
+ * @param value - Número a formatar (pode ser undefined/null → 0)
57
+ * @param decimals - Casas decimais (padrão 2)
58
+ * @param currency - Moeda de exibição (padrão 'brl' por retrocompatibilidade)
59
+ * @returns String formatada (ex: "R$ 1.234,56" ou "$1,234.56")
60
+ */
61
+ export const formatCurrencyNumber = (value?: number | null, decimals = 2, currency: CurrencyCode = 'brl'): string => {
8
62
  const safeValue = value ?? 0;
9
- return safeValue.toLocaleString('pt-BR', {
63
+ const fmt = CURRENCY_FORMAT[currency];
64
+ return safeValue.toLocaleString(fmt.locale, {
10
65
  style: 'currency',
11
- currency: 'BRL',
66
+ currency: fmt.currency,
12
67
  minimumFractionDigits: decimals,
13
68
  maximumFractionDigits: decimals,
14
69
  });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Deriva o domínio de cookie compartilhável entre subdomínios a partir do domínio do
3
+ * whitelabel (ex.: `https://app.greatpages.com.br` → `.app.greatpages.com.br`).
4
+ *
5
+ * Mesma normalização usada pelo cookie de auth `greatapps` (auth.service.normalizeCookieDomain),
6
+ * para que o cookie de locale acompanhe o mesmo escopo. Pura (sem APIs de servidor) → client+server.
7
+ * Retorna `undefined` para localhost/host inválido (cookie fica host-scoped).
8
+ */
9
+ export function cookieDomainFromDomain(domain?: string | null): string | undefined {
10
+ if (!domain) return undefined;
11
+
12
+ const normalized = domain.trim();
13
+ if (!normalized) return undefined;
14
+
15
+ let rawHost: string;
16
+ try {
17
+ rawHost = new URL(normalized).hostname;
18
+ } catch {
19
+ rawHost = normalized
20
+ .replace(/^https?:\/\//i, '')
21
+ .replace(/^www\./i, '')
22
+ .split('/')[0]
23
+ .split(':')[0];
24
+ }
25
+
26
+ if (!rawHost || rawHost === 'localhost') return undefined;
27
+
28
+ return rawHost.startsWith('.') ? rawHost : `.${rawHost}`;
29
+ }
@@ -0,0 +1,18 @@
1
+ import { LOCALE_COOKIE } from '../../i18n/config';
2
+ import { cookieDomainFromDomain } from './cookie-domain';
3
+
4
+ const ONE_YEAR = 60 * 60 * 24 * 365;
5
+
6
+ /**
7
+ * Persiste o locale escolhido no cookie `NEXT_LOCALE` (lido por `resolveLocale` no servidor).
8
+ *
9
+ * Passe `whitelabelDomain` (de `useWhitelabel().whitelabel?.domain`) para o cookie ser
10
+ * compartilhado entre subdomínios (accounts. → pages. → apps.), no mesmo escopo do cookie
11
+ * de auth. Sem ele, o cookie fica host-scoped.
12
+ */
13
+ export function setLocaleCookie(locale: string, whitelabelDomain?: string | null): void {
14
+ if (typeof document === 'undefined') return;
15
+ const domain = cookieDomainFromDomain(whitelabelDomain);
16
+ const domainAttr = domain ? `; domain=${domain}` : '';
17
+ document.cookie = `${LOCALE_COOKIE}=${locale}; path=/; max-age=${ONE_YEAR}; samesite=lax${domainAttr}`;
18
+ }
@@ -6,8 +6,9 @@ export type LocaleOption = {
6
6
  };
7
7
 
8
8
  export const LANGUAGE_OPTIONS: LocaleOption[] = [
9
- { value: 'pt-BR', label: 'Português (Brasil)', displayValue: 'Português (Brasil)', apiValue: 'pt-br' },
10
- { value: 'en-US', label: 'Inglês (EUA)', displayValue: 'Inglês (EUA)', apiValue: 'en' },
9
+ { value: 'pt-br', label: 'Português (Brasil)', displayValue: 'Português (Brasil)', apiValue: 'pt-br' },
10
+ { value: 'en-us', label: 'Inglês (EUA)', displayValue: 'Inglês (EUA)', apiValue: 'en' },
11
+ { value: 'es-es', label: 'Espanhol', displayValue: 'Espanhol', apiValue: 'es' },
11
12
  ];
12
13
 
13
14
  export function buildLocaleOptions(): LocaleOption[] {