@greatapps/common 1.1.666 → 1.1.668

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 (103) 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/hooks/useChangeEmailForm.mjs +5 -3
  16. package/dist/components/account/hooks/useChangeEmailForm.mjs.map +1 -1
  17. package/dist/components/account/hooks/useChangePasswordForm.mjs +26 -3
  18. package/dist/components/account/hooks/useChangePasswordForm.mjs.map +1 -1
  19. package/dist/components/account/hooks/useChangePhoneForm.mjs +18 -9
  20. package/dist/components/account/hooks/useChangePhoneForm.mjs.map +1 -1
  21. package/dist/components/account/hooks/useDisableTwoFactorAuthForm.mjs +26 -3
  22. package/dist/components/account/hooks/useDisableTwoFactorAuthForm.mjs.map +1 -1
  23. package/dist/components/account/hooks/useOtpVerification.mjs +19 -3
  24. package/dist/components/account/hooks/useOtpVerification.mjs.map +1 -1
  25. package/dist/components/account/hooks/useTwoFactorAuthForm.mjs +27 -4
  26. package/dist/components/account/hooks/useTwoFactorAuthForm.mjs.map +1 -1
  27. package/dist/components/account/sections/ChangeEmailModal.mjs +25 -28
  28. package/dist/components/account/sections/ChangeEmailModal.mjs.map +1 -1
  29. package/dist/components/account/sections/ChangePasswordSection.mjs +8 -6
  30. package/dist/components/account/sections/ChangePasswordSection.mjs.map +1 -1
  31. package/dist/components/account/sections/ChangePhoneModal.mjs +24 -27
  32. package/dist/components/account/sections/ChangePhoneModal.mjs.map +1 -1
  33. package/dist/components/account/sections/MyProfileSection.mjs +37 -22
  34. package/dist/components/account/sections/MyProfileSection.mjs.map +1 -1
  35. package/dist/components/account/sections/PreferencesSection.mjs +48 -24
  36. package/dist/components/account/sections/PreferencesSection.mjs.map +1 -1
  37. package/dist/components/account/sections/SecuritySection.mjs +11 -9
  38. package/dist/components/account/sections/SecuritySection.mjs.map +1 -1
  39. package/dist/components/account/sections/TokenSection.mjs +24 -8
  40. package/dist/components/account/sections/TokenSection.mjs.map +1 -1
  41. package/dist/i18n/config.mjs +13 -0
  42. package/dist/i18n/config.mjs.map +1 -0
  43. package/dist/i18n/country-language.mjs +74 -0
  44. package/dist/i18n/country-language.mjs.map +1 -0
  45. package/dist/i18n/messages/en-us.mjs +219 -0
  46. package/dist/i18n/messages/en-us.mjs.map +1 -0
  47. package/dist/i18n/messages/es-es.mjs +219 -0
  48. package/dist/i18n/messages/es-es.mjs.map +1 -0
  49. package/dist/i18n/messages/index.mjs +16 -0
  50. package/dist/i18n/messages/index.mjs.map +1 -0
  51. package/dist/i18n/messages/pt-br.mjs +224 -0
  52. package/dist/i18n/messages/pt-br.mjs.map +1 -0
  53. package/dist/i18n/normalize.mjs +12 -0
  54. package/dist/i18n/normalize.mjs.map +1 -0
  55. package/dist/i18n/resolve-locale.mjs +42 -0
  56. package/dist/i18n/resolve-locale.mjs.map +1 -0
  57. package/dist/i18n.mjs +14 -0
  58. package/dist/i18n.mjs.map +1 -0
  59. package/dist/providers/auth.provider.mjs +9 -1
  60. package/dist/providers/auth.provider.mjs.map +1 -1
  61. package/dist/server.mjs +2 -0
  62. package/dist/server.mjs.map +1 -1
  63. package/dist/utils/intl/cookie-domain.mjs +17 -0
  64. package/dist/utils/intl/cookie-domain.mjs.map +1 -0
  65. package/dist/utils/intl/locale-cookie.mjs +13 -0
  66. package/dist/utils/intl/locale-cookie.mjs.map +1 -0
  67. package/dist/utils/intl/locales.mjs +3 -2
  68. package/dist/utils/intl/locales.mjs.map +1 -1
  69. package/package.json +7 -1
  70. package/src/components/account/ConfigurationsMyAccountModal.tsx +10 -6
  71. package/src/components/account/ConfirmDeleteAccountModal.tsx +22 -13
  72. package/src/components/account/ConfirmGlobalPreferencesModal.tsx +12 -10
  73. package/src/components/account/DeleteAccountModal.tsx +34 -24
  74. package/src/components/account/DisableTwoFactorAuthModal.tsx +11 -6
  75. package/src/components/account/IsntPossibleDeleteAccountModal.tsx +8 -6
  76. package/src/components/account/TwoFactorAuthModal.tsx +33 -29
  77. package/src/components/account/hooks/useChangeEmailForm.tsx +5 -3
  78. package/src/components/account/hooks/useChangePasswordForm.tsx +20 -6
  79. package/src/components/account/hooks/useChangePhoneForm.tsx +16 -10
  80. package/src/components/account/hooks/useDisableTwoFactorAuthForm.tsx +20 -6
  81. package/src/components/account/hooks/useOtpVerification.tsx +16 -4
  82. package/src/components/account/hooks/useTwoFactorAuthForm.tsx +21 -7
  83. package/src/components/account/sections/ChangeEmailModal.tsx +35 -21
  84. package/src/components/account/sections/ChangePasswordSection.tsx +10 -6
  85. package/src/components/account/sections/ChangePhoneModal.tsx +50 -22
  86. package/src/components/account/sections/MyProfileSection.tsx +37 -23
  87. package/src/components/account/sections/PreferencesSection.tsx +52 -25
  88. package/src/components/account/sections/SecuritySection.tsx +19 -10
  89. package/src/components/account/sections/TokenSection.tsx +22 -10
  90. package/src/i18n/config.ts +17 -0
  91. package/src/i18n/country-language.ts +60 -0
  92. package/src/i18n/messages/en-us.ts +234 -0
  93. package/src/i18n/messages/es-es.ts +235 -0
  94. package/src/i18n/messages/index.ts +24 -0
  95. package/src/i18n/messages/pt-br.ts +244 -0
  96. package/src/i18n/normalize.ts +15 -0
  97. package/src/i18n/resolve-locale.ts +77 -0
  98. package/src/i18n.ts +10 -0
  99. package/src/providers/auth.provider.tsx +13 -1
  100. package/src/server.ts +2 -0
  101. package/src/utils/intl/cookie-domain.ts +29 -0
  102. package/src/utils/intl/locale-cookie.ts +18 -0
  103. package/src/utils/intl/locales.ts +3 -2
@@ -2,9 +2,13 @@
2
2
 
3
3
  import { useState, useEffect } from 'react';
4
4
  import { useForm } from 'react-hook-form';
5
+ import { useTranslations, useLocale } from 'next-intl';
6
+ import { useRouter } from 'next/navigation';
5
7
  import { toast } from 'sonner';
6
8
  import { IconLock } from '@tabler/icons-react';
7
9
  import { cn } from '../../../infra/utils/clsx';
10
+ import type { Locale } from '../../../i18n/config';
11
+ import { setLocaleCookie } from '../../../utils/intl/locale-cookie';
8
12
  import { Button } from '../../ui/buttons/Button';
9
13
  import { Separator } from '../../ui/data-display/Separator';
10
14
  import { Toast } from '../../ui/feedback/Toast';
@@ -13,6 +17,7 @@ import { ComboboxField } from '../../ui/form/ComboboxField';
13
17
  import { SelectField } from '../../ui/form/SelectField';
14
18
  import ConfirmGlobalPreferencesModal from '../ConfirmGlobalPreferencesModal';
15
19
  import { useAuth } from '../../../providers/auth.provider';
20
+ import { useWhitelabel } from '../../../providers/whitelabel.provider';
16
21
  import { UserProfile } from '../../../modules/users/schema';
17
22
  import type { UpdateAccountRequest } from '../../../modules/accounts/types';
18
23
  import {
@@ -41,9 +46,27 @@ interface PreferencesSectionProps {
41
46
  }
42
47
 
43
48
  export function PreferencesSection({ onClose }: PreferencesSectionProps) {
49
+ // Padrão raiz: um único `translate` chamado pela chave inteira (`common.<modulo>.<chave>`).
50
+ const translate = useTranslations();
51
+ // Exceção: chave dinâmica (option.value) numa lista → namespace escopado.
52
+ const translateLanguage = useTranslations('common.languages');
53
+ const currentLocale = useLocale();
54
+ const router = useRouter();
44
55
  const { data: timezones = [] } = useTimezones();
45
56
  const { data: currencies = [] } = useCurrencies();
46
57
  const { user, account } = useAuth();
58
+ const { whitelabel } = useWhitelabel();
59
+ const languageOptions = LANGUAGE_OPTIONS.map((option) => ({
60
+ ...option,
61
+ label: translateLanguage(option.value as Locale),
62
+ displayValue: translateLanguage(option.value as Locale),
63
+ }));
64
+ const timeFormatOptions = TIME_FORMAT_OPTIONS.map((option) => ({
65
+ ...option,
66
+ label: translate(
67
+ `common.preferences.timeFormatOptions.${option.value === '24h' ? 'h24' : 'h12'}`,
68
+ ),
69
+ }));
47
70
  const isOwner = user?.profile === UserProfile.owner;
48
71
  const canEditPreferences = isOwner || user?.profile === UserProfile.admin;
49
72
  const updateAccountUser = useUpdateAccountUser();
@@ -74,7 +97,7 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
74
97
  companyName: account.name ?? '',
75
98
  language: LANGUAGE_OPTIONS.find(
76
99
  (o) => o.apiValue === user.language?.toLowerCase()
77
- )?.value ?? 'pt-BR',
100
+ )?.value ?? 'pt-br',
78
101
  currency: account.currency,
79
102
  timezone: account.timezone,
80
103
  timeFormat: '24h',
@@ -98,16 +121,20 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
98
121
  }
99
122
 
100
123
  reset(data);
101
- toast.custom((t) => (
102
- <Toast variant="success" message="Preferências salvas com sucesso" toastId={t} />
124
+ toast.custom((toastId) => (
125
+ <Toast variant="success" message={translate('common.preferences.savedSuccess')} toastId={toastId} />
103
126
  ));
104
127
  onClose();
128
+ if (data.language !== currentLocale) {
129
+ setLocaleCookie(data.language, whitelabel?.domain);
130
+ router.refresh();
131
+ }
105
132
  } catch (error) {
106
- toast.custom((t) => (
133
+ toast.custom((toastId) => (
107
134
  <Toast
108
135
  variant="error"
109
- message={error instanceof Error ? error.message : 'Erro ao salvar preferências'}
110
- toastId={t}
136
+ message={error instanceof Error ? error.message : translate('common.preferences.saveError')}
137
+ toastId={toastId}
111
138
  />
112
139
  ));
113
140
  } finally {
@@ -148,12 +175,12 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
148
175
  <>
149
176
  <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col h-full">
150
177
  <div className="overflow-y-auto overscroll-contain px-4 pb-4 lg:p-5 flex flex-col gap-5 lg:gap-6 flex-1">
151
- <span className="paragraph-medium-semibold text-gray-950">Preferências</span>
178
+ <span className="paragraph-medium-semibold text-gray-950">{translate('common.preferences.title')}</span>
152
179
  <div className="flex flex-col gap-8">
153
180
  <div className="flex flex-col lg:flex-row items-center gap-3">
154
181
  <FormField
155
- label="Nome da empresa"
156
- placeholder="Digite o nome da empresa"
182
+ label={translate('common.preferences.companyName')}
183
+ placeholder={translate('common.preferences.companyNamePlaceholder')}
157
184
  classnameContainer={cn('w-full', !isOwner && 'cursor-not-allowed opacity-50')}
158
185
  value={watch('companyName')}
159
186
  onChange={(e) => setValue('companyName', e.target.value, { shouldDirty: true })}
@@ -163,18 +190,18 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
163
190
 
164
191
  <div className="flex flex-col lg:flex-row items-center gap-3">
165
192
  <ComboboxField
166
- label="Idioma"
167
- placeholder="Selecione o idioma"
168
- searchPlaceholder="Buscar idioma..."
193
+ label={translate('common.preferences.language')}
194
+ placeholder={translate('common.preferences.languagePlaceholder')}
195
+ searchPlaceholder={translate('common.preferences.languageSearch')}
169
196
  containerClassName="w-full"
170
- options={LANGUAGE_OPTIONS}
197
+ options={languageOptions}
171
198
  value={watch('language')}
172
199
  onChange={(value) => setValue('language', value, { shouldDirty: true })}
173
200
  />
174
201
  <ComboboxField
175
- label="Moeda"
176
- placeholder="Selecione a moeda"
177
- searchPlaceholder="Buscar moeda..."
202
+ label={translate('common.preferences.currency')}
203
+ placeholder={translate('common.preferences.currencyPlaceholder')}
204
+ searchPlaceholder={translate('common.preferences.currencySearch')}
178
205
  containerClassName="w-full"
179
206
  options={currencies}
180
207
  value={watch('currency')}
@@ -187,12 +214,12 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
187
214
  <Separator />
188
215
 
189
216
  <div className="flex flex-col gap-4">
190
- <span className="paragraph-medium-semibold text-gray-950">Exibição hora</span>
217
+ <span className="paragraph-medium-semibold text-gray-950">{translate('common.preferences.timeDisplay')}</span>
191
218
  <div className="flex flex-col lg:flex-row items-center gap-3">
192
219
  <ComboboxField
193
- label="Fuso horário"
194
- placeholder="Selecione o fuso horário"
195
- searchPlaceholder="Buscar fuso horário..."
220
+ label={translate('common.preferences.timezone')}
221
+ placeholder={translate('common.preferences.timezonePlaceholder')}
222
+ searchPlaceholder={translate('common.preferences.timezoneSearch')}
196
223
  containerClassName="w-full"
197
224
  options={timezones}
198
225
  value={watch('timezone')}
@@ -201,11 +228,11 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
201
228
  disabled={!canEditPreferences}
202
229
  />
203
230
  <SelectField
204
- label="Formato hora"
205
- placeholder="Selecione o formato"
231
+ label={translate('common.preferences.timeFormat')}
232
+ placeholder={translate('common.preferences.timeFormatPlaceholder')}
206
233
  className="h-10!"
207
234
  containerClassName="w-full"
208
- options={TIME_FORMAT_OPTIONS}
235
+ options={timeFormatOptions}
209
236
  value={watch('timeFormat')}
210
237
  onChange={(value) =>
211
238
  setValue('timeFormat', value as string, { shouldDirty: true })
@@ -230,10 +257,10 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
230
257
  onClose();
231
258
  }}
232
259
  >
233
- Cancelar
260
+ {translate('common.actions.cancel')}
234
261
  </Button>
235
262
  <Button className="h-10!" type="submit" disabled={isSaving}>
236
- {isSaving ? 'Salvando...' : 'Salvar alterações'}
263
+ {isSaving ? translate('common.actions.saving') : translate('common.actions.saveChanges')}
237
264
  </Button>
238
265
  </div>
239
266
  </form>
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { IconAlertTriangle, IconBrandGoogleFilled } from '@tabler/icons-react';
4
+ import { useTranslations } from 'next-intl';
4
5
  import { toast } from 'sonner';
5
6
  import { useQueryClient } from '@tanstack/react-query';
6
7
  import { useAuth } from '../../../providers/auth.provider';
@@ -18,6 +19,7 @@ interface SecuritySectionProps {
18
19
  export function SecuritySection({
19
20
  onDeleteAccount,
20
21
  }: SecuritySectionProps) {
22
+ const translate = useTranslations();
21
23
  const { user } = useAuth();
22
24
  const queryClient = useQueryClient();
23
25
  const { mutateAsync: unlinkGoogle, isPending: isUnlinking } = useUnlinkGoogle();
@@ -32,14 +34,16 @@ export function SecuritySection({
32
34
  queryClient.invalidateQueries({ queryKey: USER_QUERY_KEY });
33
35
  } catch (error) {
34
36
  const message =
35
- error instanceof Error ? error.message : 'Erro ao desvincular conta Google.';
36
- toast.custom((t) => <Toast variant="error" message={message} toastId={t} />);
37
+ error instanceof Error ? error.message : translate('common.account.security.unlinkError');
38
+ toast.custom((toastId) => <Toast variant="error" message={message} toastId={toastId} />);
37
39
  }
38
40
  };
39
41
 
40
42
  return (
41
43
  <div className="h-full overflow-y-auto overscroll-contain px-4 pb-20 lg:p-5 lg:pt-5 flex flex-col gap-5 lg:gap-6 lg:pb-31 lg:overscroll-auto">
42
- <span className="paragraph-medium-semibold text-gray-950 hidden lg:block">Segurança</span>
44
+ <span className="paragraph-medium-semibold text-gray-950 hidden lg:block">
45
+ {translate('common.account.security.title')}
46
+ </span>
43
47
 
44
48
  {hasGoogleLinked && (
45
49
  <div className="flex flex-col items-center lg:items-start gap-4">
@@ -48,10 +52,11 @@ export function SecuritySection({
48
52
  </div>
49
53
 
50
54
  <div className="flex flex-col gap-2">
51
- <span className="paragraph-medium-semibold text-gray-950">Conta Google vinculada</span>
55
+ <span className="paragraph-medium-semibold text-gray-950">
56
+ {translate('common.account.security.googleLinkedTitle')}
57
+ </span>
52
58
  <span className="paragraph-small-regular text-gray-600">
53
- Sua conta está vinculada ao Google. Se desvincular, use &quot;Esqueci minha senha&quot; para
54
- redefinir uma senha de acesso por e-mail.
59
+ {translate('common.account.security.googleLinkedDescription')}
55
60
  </span>
56
61
  </div>
57
62
 
@@ -61,7 +66,9 @@ export function SecuritySection({
61
66
  className="paragraph-small-semibold text-zinc-700 bg-zinc-100 rounded-lg px-3 py-2 cursor-pointer hover:bg-zinc-200 transition-colors w-fit disabled:opacity-50 disabled:cursor-not-allowed"
62
67
  onClick={handleUnlinkGoogle}
63
68
  >
64
- {isUnlinking ? 'Desvinculando...' : 'Desvincular Google'}
69
+ {isUnlinking
70
+ ? translate('common.account.security.unlinking')
71
+ : translate('common.account.security.unlinkGoogle')}
65
72
  </button>
66
73
  </div>
67
74
  )}
@@ -72,9 +79,11 @@ export function SecuritySection({
72
79
  </div>
73
80
 
74
81
  <div className="flex flex-col gap-2">
75
- <span className="paragraph-medium-semibold text-gray-950">Excluir conta</span>
82
+ <span className="paragraph-medium-semibold text-gray-950">
83
+ {translate('common.account.deleteAccount.title')}
84
+ </span>
76
85
  <span className="paragraph-small-regular text-gray-600">
77
- Esta ação é permanente e não pode ser desfeita. Todos os seus dados, projetos e configurações serão removidos definitivamente.
86
+ {translate('common.account.security.deleteAccountDescription')}
78
87
  </span>
79
88
  </div>
80
89
 
@@ -84,7 +93,7 @@ export function SecuritySection({
84
93
  className="paragraph-small-semibold text-red-600 bg-red-50 rounded-lg px-3 py-2 cursor-pointer hover:bg-red-100 transition-colors w-fit"
85
94
  onClick={onDeleteAccount}
86
95
  >
87
- Excluir minha conta
96
+ {translate('common.account.security.deleteMyAccount')}
88
97
  </button>
89
98
  )}
90
99
  </div>
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { IconCheck, IconCopy, IconInfoCircle } from '@tabler/icons-react';
4
+ import { useTranslations } from 'next-intl';
4
5
  import { toast } from 'sonner';
5
6
  import { Toast } from '../../ui/feedback/Toast';
6
7
  import { Button } from '../../ui/buttons/Button';
@@ -10,31 +11,42 @@ import { useAccountToken } from '../../../modules/accounts/hooks/use-account-tok
10
11
  import { useIsDefaultWhitelabel } from '../../../providers/whitelabel.provider';
11
12
 
12
13
  export function TokenSection() {
14
+ const translate = useTranslations();
13
15
  const { data: token = '', isLoading } = useAccountToken();
14
16
  const isDefaultWl = useIsDefaultWhitelabel();
15
17
 
16
18
  const { isCopied, copy } = useCopyToClipboard({
17
19
  onSuccess: () => {
18
- toast.custom((t) => (
19
- <Toast variant="success" message="Token copiado com sucesso" toastId={t} />
20
+ toast.custom((toastId) => (
21
+ <Toast
22
+ variant="success"
23
+ message={translate('common.account.token.copySuccess')}
24
+ toastId={toastId}
25
+ />
20
26
  ));
21
27
  },
22
28
  onError: () => {
23
- toast.custom((t) => (
24
- <Toast variant="error" message="Não foi possível copiar o token" toastId={t} />
29
+ toast.custom((toastId) => (
30
+ <Toast
31
+ variant="error"
32
+ message={translate('common.account.token.copyError')}
33
+ toastId={toastId}
34
+ />
25
35
  ));
26
36
  },
27
37
  });
28
38
 
29
39
  return (
30
40
  <div className="h-full overflow-y-auto overscroll-contain px-4 pb-20 lg:p-5 lg:pt-5 flex flex-col gap-5 lg:gap-6 lg:pb-31 lg:overscroll-auto">
31
- <span className="paragraph-medium-semibold text-gray-950">Token da conta</span>
41
+ <span className="paragraph-medium-semibold text-gray-950">
42
+ {translate('common.account.token.title')}
43
+ </span>
32
44
 
33
45
  <div className="flex flex-col gap-3">
34
46
  <div className="flex items-end gap-3">
35
47
  <FormField
36
- label="Token"
37
- value={isLoading ? 'Carregando...' : token}
48
+ label={translate('common.account.token.label')}
49
+ value={isLoading ? translate('common.actions.loading') : token}
38
50
  disabled
39
51
  readOnly
40
52
  classnameContainer="bg-gray-50 cursor-default opacity-100!"
@@ -48,14 +60,14 @@ export function TokenSection() {
48
60
  disabled={isLoading || !token}
49
61
  >
50
62
  {isCopied ? <IconCheck size={16} /> : <IconCopy size={16} />}
51
- {isCopied ? 'Copiado' : 'Copiar'}
63
+ {isCopied ? translate('common.actions.copied') : translate('common.actions.copy')}
52
64
  </Button>
53
65
  </div>
54
66
 
55
67
  <div className={`flex items-center gap-3 p-4 rounded-lg ${isDefaultWl ? 'bg-cyan-50' : 'bg-zinc-50'}`}>
56
68
  <IconInfoCircle className="size-4 text-zinc-950 shrink-0" />
57
69
  <span className="paragraph-small-regular text-zinc-950 max-w-[420px]">
58
- Este token é exclusivo da sua conta e permite acesso à API. Não compartilhe com terceiros.
70
+ {translate('common.account.token.info')}
59
71
  </span>
60
72
  </div>
61
73
 
@@ -65,7 +77,7 @@ export function TokenSection() {
65
77
  rel="noopener noreferrer"
66
78
  className="paragraph-small-medium text-gray-500 underline hover:text-gray-700 transition-colors w-fit"
67
79
  >
68
- Acessar a documentação
80
+ {translate('common.account.token.docsLink')}
69
81
  </a>
70
82
  </div>
71
83
  </div>
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Locales suportados pela plataforma GreatApps.
3
+ * Identificadores no padrão `idioma-REGIÃO` em minúsculas: `pt-br`, `en-us`, `es-es`.
4
+ */
5
+ export const LOCALES = ['pt-br', 'en-us', 'es-es'] as const;
6
+
7
+ export type Locale = (typeof LOCALES)[number];
8
+
9
+ /** Idioma padrão — fallback final (o produto nasce em pt-BR). */
10
+ export const DEFAULT_LOCALE: Locale = 'pt-br';
11
+
12
+ /** Nome do cookie onde o locale resolvido é persistido (convenção next-intl). */
13
+ export const LOCALE_COOKIE = 'NEXT_LOCALE';
14
+
15
+ export function isLocale(value: unknown): value is Locale {
16
+ return typeof value === 'string' && (LOCALES as readonly string[]).includes(value);
17
+ }
@@ -0,0 +1,60 @@
1
+ import { Locale } from './config';
2
+
3
+ /**
4
+ * Países de língua portuguesa (ISO 3166-1 alpha-2).
5
+ */
6
+ const PORTUGUESE_COUNTRIES = new Set<string>([
7
+ 'BR', // Brasil
8
+ 'PT', // Portugal
9
+ 'AO', // Angola
10
+ 'MZ', // Moçambique
11
+ 'CV', // Cabo Verde
12
+ 'GW', // Guiné-Bissau
13
+ 'ST', // São Tomé e Príncipe
14
+ 'TL', // Timor-Leste
15
+ ]);
16
+
17
+ /**
18
+ * Países de língua espanhola (ISO 3166-1 alpha-2).
19
+ */
20
+ const SPANISH_COUNTRIES = new Set<string>([
21
+ 'ES', // Espanha
22
+ 'MX', // México
23
+ 'AR', // Argentina
24
+ 'CO', // Colômbia
25
+ 'PE', // Peru
26
+ 'VE', // Venezuela
27
+ 'CL', // Chile
28
+ 'EC', // Equador
29
+ 'GT', // Guatemala
30
+ 'CU', // Cuba
31
+ 'BO', // Bolívia
32
+ 'DO', // República Dominicana
33
+ 'HN', // Honduras
34
+ 'PY', // Paraguai
35
+ 'SV', // El Salvador
36
+ 'NI', // Nicarágua
37
+ 'CR', // Costa Rica
38
+ 'PA', // Panamá
39
+ 'UY', // Uruguai
40
+ 'PR', // Porto Rico
41
+ 'GQ', // Guiné Equatorial
42
+ ]);
43
+
44
+ /**
45
+ * Infere o idioma a partir do código de país (ex.: header `CF-IPCountry` da Cloudflare
46
+ * ou `session.location.country` do JWT):
47
+ * - país lusófono → `pt`
48
+ * - país hispanófono → `es`
49
+ * - demais países → `en`
50
+ *
51
+ * Retorna `null` quando o país é desconhecido/ausente (deixa o fallback de locale decidir).
52
+ */
53
+ export function localeFromCountry(country?: string | null): Locale | null {
54
+ if (!country) return null;
55
+ const code = country.trim().toUpperCase();
56
+ if (code === 'XX' || code === 'T1') return null; // Cloudflare: desconhecido / Tor
57
+ if (PORTUGUESE_COUNTRIES.has(code)) return 'pt-br';
58
+ if (SPANISH_COUNTRIES.has(code)) return 'es-es';
59
+ return 'en-us';
60
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * en-us translation catalog for @greatapps/common components.
3
+ * Consumer apps merge this object under the `common` namespace.
4
+ * Mirror the module structure of `pt-br.ts` (the source of truth).
5
+ */
6
+ const messages = {
7
+ actions: {
8
+ save: 'Save',
9
+ saveChanges: 'Save changes',
10
+ cancel: 'Cancel',
11
+ confirm: 'Confirm',
12
+ delete: 'Delete',
13
+ edit: 'Edit',
14
+ close: 'Close',
15
+ back: 'Back',
16
+ continue: 'Continue',
17
+ add: 'Add',
18
+ remove: 'Remove',
19
+ search: 'Search',
20
+ loading: 'Loading...',
21
+ saving: 'Saving...',
22
+ sending: 'Sending...',
23
+ copy: 'Copy',
24
+ copied: 'Copied',
25
+ },
26
+
27
+ validations: {
28
+ required: 'Required field',
29
+ invalidEmail: 'Invalid email',
30
+ invalidPhone: 'Invalid phone number',
31
+ },
32
+
33
+ preferences: {
34
+ title: 'Preferences',
35
+ companyName: 'Company name',
36
+ companyNamePlaceholder: 'Enter the company name',
37
+ language: 'Language',
38
+ languagePlaceholder: 'Select the language',
39
+ languageSearch: 'Search language...',
40
+ currency: 'Currency',
41
+ currencyPlaceholder: 'Select the currency',
42
+ currencySearch: 'Search currency...',
43
+ timeDisplay: 'Time display',
44
+ timezone: 'Time zone',
45
+ timezonePlaceholder: 'Select the time zone',
46
+ timezoneSearch: 'Search time zone...',
47
+ timeFormat: 'Time format',
48
+ timeFormatPlaceholder: 'Select the format',
49
+ timeFormatOptions: {
50
+ h24: '24 hours',
51
+ h12: '12 hours (AM/PM)',
52
+ },
53
+ savedSuccess: 'Preferences saved successfully',
54
+ saveError: 'Error saving preferences',
55
+ },
56
+
57
+ account: {
58
+ deleteAccount: {
59
+ title: 'Delete account',
60
+ description:
61
+ 'This action is <b>irreversible</b>. By confirming, your account and all your data will be deleted.',
62
+ reasonLabel: 'Reason for cancellation',
63
+ reasonPlaceholder: 'Select',
64
+ descriptionLabel: 'Describe what led you to make this decision',
65
+ descriptionPlaceholder: 'Explain your decision...',
66
+ charactersCount: '{count}/400 characters',
67
+ passwordLabel: 'Password',
68
+ passwordPlaceholder: 'Enter your password',
69
+ submit: 'Delete account',
70
+ requiredFields: 'Fill in all required fields',
71
+ reasons: {
72
+ noFeature: 'The tool lacks a feature I need',
73
+ technical: 'I encountered technical issues or errors in the system',
74
+ support: 'I had issues with support or customer service',
75
+ updates: 'Updates take longer than expected',
76
+ temporary: 'The cancellation is temporary',
77
+ other: 'Other reason',
78
+ },
79
+ },
80
+ confirmDelete: {
81
+ title: 'Sorry to see you go!',
82
+ warning:
83
+ 'All your <b>{count} pages</b> will be permanently deleted. This action is <b>irreversible!</b>',
84
+ sessionExpired: 'Session expired. Fill in the fields again.',
85
+ success: 'Account deleted successfully',
86
+ error: 'Error deleting account. Please try again.',
87
+ keepAccount: 'Keep my account',
88
+ deleteAnyway: 'Delete anyway',
89
+ deleting: 'Deleting...',
90
+ },
91
+ cantDelete: {
92
+ title: 'Unable to delete account',
93
+ description:
94
+ 'You have an active subscription. To delete your account, <b>first cancel your subscription</b> on the Subscription page and then try again.',
95
+ confirm: 'Ok, I understand',
96
+ goToSubscription: 'Go to subscription',
97
+ },
98
+ twoFactor: {
99
+ title: 'Two-factor authentication',
100
+ instructions:
101
+ 'Install an authenticator app on your mobile device (e.g., <link>Google Authenticator</link>), scan the QR Code on the side or copy the key in the app, and then enter the 6-digit code generated to continue.',
102
+ qrAlt: 'QR Code for two-factor authentication',
103
+ retry: 'Try again',
104
+ pasteDigits: 'Paste the 6 digits below',
105
+ activating: 'Activating...',
106
+ activate: 'Activate authentication',
107
+ recoveryTitle: 'Security codes',
108
+ recoveryHeading: 'Save these emergency recovery codes',
109
+ recoveryText1:
110
+ 'If you lose access to your phone, you will not be able to log into your account without a two-factor code.',
111
+ recoveryText2: 'Print, copy, or write the codes below in a safe location.',
112
+ copyCodes: 'Copy codes',
113
+ finish: 'Finish',
114
+ codesCopied: 'Codes copied successfully',
115
+ generateError: 'Error generating QR Code. Try again.',
116
+ confirmError: 'Error confirming code. Try again.',
117
+ disableTitle: 'Disable 2FA authentication',
118
+ disableDescription:
119
+ 'Enter the 6-digit code from your authenticator app to confirm disabling.',
120
+ authenticatorCode: 'Authenticator code',
121
+ disabling: 'Disabling...',
122
+ disable: 'Disable 2FA',
123
+ disabledSuccess: 'Two-factor authentication disabled',
124
+ disableError: 'Error disabling 2FA. Try again.',
125
+ invalidCode: 'Invalid code',
126
+ codeResent: 'Code resent',
127
+ resendError: 'Error resending code. Try again.',
128
+ },
129
+ profile: {
130
+ title: 'My profile',
131
+ avatarAlt: 'User photo',
132
+ changePhoto: 'Change photo',
133
+ name: 'First name',
134
+ namePlaceholder: 'Enter your first name',
135
+ lastName: 'Last name',
136
+ lastNamePlaceholder: 'Enter your last name',
137
+ gender: 'Gender',
138
+ genderPlaceholder: 'Select your gender',
139
+ genderOptions: {
140
+ male: 'Male',
141
+ female: 'Female',
142
+ },
143
+ phone: 'Phone',
144
+ change: 'Change',
145
+ accessData: 'Access data',
146
+ email: 'Email',
147
+ savedSuccess: 'Profile updated successfully',
148
+ saveError: 'Error updating profile',
149
+ },
150
+ otp: {
151
+ validationCode: 'Validation code',
152
+ incorrectCode: 'Incorrect code',
153
+ resend: 'Resend code',
154
+ resendIn: 'Resend in <b>{time}</b>',
155
+ validating: 'Validating...',
156
+ },
157
+ changeEmail: {
158
+ title: 'Change email',
159
+ confirm: 'Change email',
160
+ sentCodeTo: 'We sent a validation code to email <b>{email}.</b>',
161
+ updateTitle: 'Update contact email',
162
+ updateDescription: 'Enter your new email to keep your contact information updated.',
163
+ newEmail: 'New email',
164
+ newEmailPlaceholder: 'Enter your new email',
165
+ emailChanged: 'Email changed successfully',
166
+ emailExistsError: 'Email already exists or an error occurred. Please try again later.',
167
+ },
168
+ changePhone: {
169
+ titleEdit: 'Change phone',
170
+ titleAdd: 'Add phone',
171
+ sentCodeTo: 'We sent a validation code to your current number <b>{phone}.</b>',
172
+ confirm: 'Change phone',
173
+ updateTitle: 'Update contact phone',
174
+ addTitle: 'Add contact phone',
175
+ updateDescription: 'Enter the new number to keep your contact information updated.',
176
+ addDescription: 'Enter your phone number for contact.',
177
+ newNumber: 'New number',
178
+ numberLabel: 'Phone number',
179
+ phoneChanged: 'Phone changed',
180
+ phoneAdded: 'Phone added',
181
+ invalidNumber: 'Invalid phone number',
182
+ saveError: 'An error occurred while saving the phone number. Please try again later.',
183
+ numberExistsError: 'Number already exists or an error occurred. Please try again later.',
184
+ },
185
+ security: {
186
+ title: 'Security',
187
+ googleLinkedTitle: 'Google account linked',
188
+ googleLinkedDescription:
189
+ 'Your account is linked to Google. If you unlink it, use "Forgot my password" to reset an email access password.',
190
+ unlinking: 'Unlinking...',
191
+ unlinkGoogle: 'Unlink Google',
192
+ unlinkError: 'Error unlinking Google account.',
193
+ deleteAccountDescription:
194
+ 'This action is permanent and cannot be undone. All your data, projects, and settings will be permanently removed.',
195
+ deleteMyAccount: 'Delete my account',
196
+ },
197
+ token: {
198
+ title: 'Account token',
199
+ label: 'Token',
200
+ copySuccess: 'Token copied successfully',
201
+ copyError: 'Failed to copy token',
202
+ info: 'This token is unique to your account and allows API access. Do not share with third parties.',
203
+ docsLink: 'Access documentation',
204
+ },
205
+ changePassword: {
206
+ title: 'Change password',
207
+ currentPassword: 'Current password',
208
+ currentPasswordPlaceholder: 'Enter your current password',
209
+ newPassword: 'New password',
210
+ newPasswordPlaceholder: 'Enter your new password',
211
+ submit: 'Save new password',
212
+ changeError: 'Error changing password',
213
+ changedSuccess: 'Password changed successfully',
214
+ genericError: 'Error changing password. Try again.',
215
+ },
216
+ globalPreferences: {
217
+ title: 'Change global preferences',
218
+ srDescription: 'Confirmation of global account preferences change',
219
+ warning:
220
+ 'You are changing settings that impact <b>all account users</b>. Do you want to confirm?',
221
+ },
222
+ configurations: {
223
+ title: 'Settings',
224
+ },
225
+ },
226
+
227
+ languages: {
228
+ 'pt-br': 'Portuguese (Brazil)',
229
+ 'en-us': 'English (US)',
230
+ 'es-es': 'Spanish',
231
+ },
232
+ } as const;
233
+
234
+ export default messages;