@greatapps/common 1.1.779 → 1.1.781

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 (43) hide show
  1. package/dist/components/account/sections/PreferencesSection.mjs +3 -1
  2. package/dist/components/account/sections/PreferencesSection.mjs.map +1 -1
  3. package/dist/components/layouts/AppMobileNavBar.mjs +64 -5
  4. package/dist/components/layouts/AppMobileNavBar.mjs.map +1 -1
  5. package/dist/components/layouts/AppNavBar.mjs +2 -0
  6. package/dist/components/layouts/AppNavBar.mjs.map +1 -1
  7. package/dist/components/layouts/GreatDomainIcon.mjs +40 -0
  8. package/dist/components/layouts/GreatDomainIcon.mjs.map +1 -0
  9. package/dist/components/layouts/NavBar.mjs +109 -52
  10. package/dist/components/layouts/NavBar.mjs.map +1 -1
  11. package/dist/hooks/useDomMutationGuardHit.mjs +36 -0
  12. package/dist/hooks/useDomMutationGuardHit.mjs.map +1 -0
  13. package/dist/i18n/messages/en-us.mjs +3 -1
  14. package/dist/i18n/messages/en-us.mjs.map +1 -1
  15. package/dist/i18n/messages/es-es.mjs +3 -1
  16. package/dist/i18n/messages/es-es.mjs.map +1 -1
  17. package/dist/i18n/messages/pt-br.mjs +3 -1
  18. package/dist/i18n/messages/pt-br.mjs.map +1 -1
  19. package/dist/i18n/resolve-locale.mjs +10 -0
  20. package/dist/i18n/resolve-locale.mjs.map +1 -1
  21. package/dist/index.mjs +4 -0
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/modules/auth/services/auth.service.mjs +12 -6
  24. package/dist/modules/auth/services/auth.service.mjs.map +1 -1
  25. package/dist/utils/dom/dom-mutation-guard.mjs +20 -0
  26. package/dist/utils/dom/dom-mutation-guard.mjs.map +1 -0
  27. package/dist/utils/intl/locales.mjs +6 -1
  28. package/dist/utils/intl/locales.mjs.map +1 -1
  29. package/package.json +7 -8
  30. package/src/components/account/sections/PreferencesSection.tsx +3 -2
  31. package/src/components/layouts/AppMobileNavBar.tsx +90 -6
  32. package/src/components/layouts/AppNavBar.tsx +5 -1
  33. package/src/components/layouts/GreatDomainIcon.tsx +43 -0
  34. package/src/components/layouts/NavBar.tsx +158 -66
  35. package/src/hooks/useDomMutationGuardHit.ts +55 -0
  36. package/src/i18n/messages/en-us.ts +2 -0
  37. package/src/i18n/messages/es-es.ts +2 -0
  38. package/src/i18n/messages/pt-br.ts +2 -0
  39. package/src/i18n/resolve-locale.ts +19 -4
  40. package/src/index.ts +3 -0
  41. package/src/modules/auth/services/auth.service.ts +14 -6
  42. package/src/utils/dom/dom-mutation-guard.ts +46 -0
  43. package/src/utils/intl/locales.ts +7 -0
@@ -6,6 +6,7 @@ import Link from "next/link";
6
6
  import { useTranslations } from "next-intl";
7
7
  import { IconChevronRight } from "@tabler/icons-react";
8
8
  import { Separator } from "../ui/data-display/Separator";
9
+ import { GreatDomainIcon } from "./GreatDomainIcon";
9
10
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/overlay/Tooltip";
10
11
  import {
11
12
  useDesktopSidebarStore,
@@ -20,7 +21,9 @@ import { cn } from "../../infra/utils/clsx";
20
21
  type NavBarProps = {
21
22
  logoHref: string;
22
23
  appIconHref: string;
23
- activeApp?: "gapps" | "gpages";
24
+ /** Sem href o ícone do GreatDomain não é renderizado: é a flag do app. */
25
+ domainIconHref?: string;
26
+ activeApp?: "gapps" | "gpages" | "gdomain";
24
27
  showExpandButton?: boolean;
25
28
  onExpandClick?: () => void;
26
29
  children?: React.ReactNode;
@@ -30,6 +33,7 @@ type NavBarProps = {
30
33
  function NavBar({
31
34
  logoHref,
32
35
  appIconHref,
36
+ domainIconHref,
33
37
  activeApp,
34
38
  showExpandButton = false,
35
39
  onExpandClick,
@@ -130,7 +134,9 @@ function NavBar({
130
134
  <IconChevronRight size={16} className="text-gray-500" />
131
135
  </button>
132
136
  </TooltipTrigger>
133
- <TooltipContent side="right">{translate('common.layout.navbar.expandMenu')}</TooltipContent>
137
+ <TooltipContent side="right">
138
+ {translate("common.layout.navbar.expandMenu")}
139
+ </TooltipContent>
134
140
  </Tooltip>
135
141
  )}
136
142
  {/* Desktop expand button */}
@@ -144,78 +150,164 @@ function NavBar({
144
150
  <IconChevronRight size={16} className="text-gray-500" />
145
151
  </button>
146
152
  </TooltipTrigger>
147
- <TooltipContent side="right">{translate('common.layout.navbar.expandMenu')}</TooltipContent>
153
+ <TooltipContent side="right">
154
+ {translate("common.layout.navbar.expandMenu")}
155
+ </TooltipContent>
148
156
  </Tooltip>
149
157
  )}
150
158
  </div>
151
159
 
152
- {/* GreatPages icon */}
153
- <div className="relative">
154
- <Tooltip>
155
- <TooltipTrigger asChild>
156
- <Link
157
- href={appIconHref}
160
+ {/* Ícones de app — 8px entre eles (Figma 5929:2676) */}
161
+ <div className="flex flex-col gap-2 items-center">
162
+ {/* GreatPages icon */}
163
+ <div className="relative">
164
+ <Tooltip>
165
+ <TooltipTrigger asChild>
166
+ <Link
167
+ href={appIconHref}
168
+ className={cn(
169
+ "size-10 rounded-lg cursor-pointer flex items-center justify-center border-2 ring-2 transition-colors duration-300",
170
+ activeApp === "gpages"
171
+ ? isDefaultWl
172
+ ? "bg-cyan-100 border-white ring-cyan-300"
173
+ : "border-white ring-transparent"
174
+ : isDefaultWl
175
+ ? "hover:bg-gray-100 border-transparent ring-transparent"
176
+ : "border-transparent ring-transparent",
177
+ )}
178
+ style={
179
+ !isDefaultWl
180
+ ? {
181
+ backgroundColor: primaryColor,
182
+ boxShadow:
183
+ activeApp === "gpages"
184
+ ? `0 0 0 2px ${primaryColor}40`
185
+ : undefined,
186
+ opacity: activeApp !== "gpages" ? 0.2 : undefined,
187
+ }
188
+ : undefined
189
+ }
190
+ >
191
+ {isDefaultWl ? (
192
+ <Image
193
+ src="/icons/icon-navbar.svg"
194
+ alt="GreatPages"
195
+ width={24}
196
+ height={24}
197
+ />
198
+ ) : (
199
+ <svg
200
+ xmlns="http://www.w3.org/2000/svg"
201
+ width="24"
202
+ height="24"
203
+ viewBox="0 0 24 24"
204
+ fill="none"
205
+ stroke="white"
206
+ strokeWidth="2"
207
+ strokeLinecap="round"
208
+ strokeLinejoin="round"
209
+ >
210
+ <path d="M4 8h16" />
211
+ <path d="M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12" />
212
+ <path d="M8 4v4" />
213
+ </svg>
214
+ )}
215
+ </Link>
216
+ </TooltipTrigger>
217
+ <TooltipContent side="right">
218
+ {isDefaultWl
219
+ ? translate("common.layout.navbar.greatPages")
220
+ : translate("common.layout.navbar.pages")}
221
+ </TooltipContent>
222
+ </Tooltip>
223
+ <div
224
+ className={cn(
225
+ "absolute -left-4 top-1/2 -translate-y-1/2 z-10 w-[3px] h-5 rounded-r-xl transition-opacity duration-300",
226
+ isDefaultWl ? "bg-cyan-300" : "",
227
+ activeApp === "gpages" ? "opacity-100" : "opacity-0",
228
+ )}
229
+ style={
230
+ !isDefaultWl ? { backgroundColor: primaryColor } : undefined
231
+ }
232
+ />
233
+ </div>
234
+
235
+ {/* GreatDomain icon */}
236
+ {domainIconHref ? (
237
+ <div className="relative">
238
+ <Tooltip>
239
+ <TooltipTrigger asChild>
240
+ <Link
241
+ href={domainIconHref}
242
+ aria-label={
243
+ isDefaultWl
244
+ ? translate("common.layout.navbar.greatDomain")
245
+ : translate("common.layout.navbar.domains")
246
+ }
247
+ className={cn(
248
+ "size-10 rounded-[10px] cursor-pointer flex items-center justify-center border-2 ring-2 transition-colors duration-300",
249
+ activeApp === "gdomain"
250
+ ? isDefaultWl
251
+ ? "bg-[#EDE9FE] border-white ring-[#C092FF]"
252
+ : "border-white ring-transparent"
253
+ : isDefaultWl
254
+ ? "hover:bg-gray-100 border-transparent ring-transparent"
255
+ : "border-transparent ring-transparent",
256
+ )}
257
+ style={
258
+ !isDefaultWl
259
+ ? {
260
+ backgroundColor: primaryColor,
261
+ boxShadow:
262
+ activeApp === "gdomain"
263
+ ? `0 0 0 2px ${primaryColor}40`
264
+ : undefined,
265
+ opacity: activeApp !== "gdomain" ? 0.2 : undefined,
266
+ }
267
+ : undefined
268
+ }
269
+ >
270
+ {isDefaultWl ? (
271
+ <GreatDomainIcon active={activeApp === "gdomain"} />
272
+ ) : (
273
+ <svg
274
+ xmlns="http://www.w3.org/2000/svg"
275
+ width="24"
276
+ height="24"
277
+ viewBox="0 0 24 24"
278
+ fill="none"
279
+ stroke="white"
280
+ strokeWidth="2"
281
+ strokeLinecap="round"
282
+ strokeLinejoin="round"
283
+ >
284
+ <path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
285
+ <path d="M3.6 9h16.8" />
286
+ <path d="M3.6 15h16.8" />
287
+ <path d="M11.5 3a17 17 0 0 0 0 18" />
288
+ <path d="M12.5 3a17 17 0 0 1 0 18" />
289
+ </svg>
290
+ )}
291
+ </Link>
292
+ </TooltipTrigger>
293
+ <TooltipContent side="right">
294
+ {isDefaultWl
295
+ ? translate("common.layout.navbar.greatDomain")
296
+ : translate("common.layout.navbar.domains")}
297
+ </TooltipContent>
298
+ </Tooltip>
299
+ <div
158
300
  className={cn(
159
- "size-10 rounded-lg cursor-pointer flex items-center justify-center border-2 ring-2 transition-colors duration-300",
160
- activeApp === "gpages"
161
- ? isDefaultWl
162
- ? "bg-cyan-100 border-white ring-cyan-300"
163
- : "border-white ring-transparent"
164
- : isDefaultWl
165
- ? "hover:bg-gray-100 border-transparent ring-transparent"
166
- : "border-transparent ring-transparent",
301
+ "absolute -left-4 top-1/2 -translate-y-1/2 z-10 w-[3px] h-5 rounded-r-xl transition-opacity duration-300",
302
+ isDefaultWl ? "bg-[#984DFF]" : "",
303
+ activeApp === "gdomain" ? "opacity-100" : "opacity-0",
167
304
  )}
168
305
  style={
169
- !isDefaultWl
170
- ? {
171
- backgroundColor: primaryColor,
172
- boxShadow:
173
- activeApp === "gpages"
174
- ? `0 0 0 2px ${primaryColor}40`
175
- : undefined,
176
- opacity: activeApp !== "gpages" ? 0.2 : undefined,
177
- }
178
- : undefined
306
+ !isDefaultWl ? { backgroundColor: primaryColor } : undefined
179
307
  }
180
- >
181
- {isDefaultWl ? (
182
- <Image
183
- src="/icons/icon-navbar.svg"
184
- alt="GreatPages"
185
- width={24}
186
- height={24}
187
- />
188
- ) : (
189
- <svg
190
- xmlns="http://www.w3.org/2000/svg"
191
- width="24"
192
- height="24"
193
- viewBox="0 0 24 24"
194
- fill="none"
195
- stroke="white"
196
- strokeWidth="2"
197
- strokeLinecap="round"
198
- strokeLinejoin="round"
199
- >
200
- <path d="M4 8h16" />
201
- <path d="M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12" />
202
- <path d="M8 4v4" />
203
- </svg>
204
- )}
205
- </Link>
206
- </TooltipTrigger>
207
- <TooltipContent side="right">
208
- {isDefaultWl ? translate('common.layout.navbar.greatPages') : translate('common.layout.navbar.pages')}
209
- </TooltipContent>
210
- </Tooltip>
211
- <div
212
- className={cn(
213
- "absolute -left-4 top-1/2 -translate-y-1/2 z-10 w-[3px] h-5 rounded-r-xl transition-opacity duration-300",
214
- isDefaultWl ? "bg-cyan-300" : "",
215
- activeApp === "gpages" ? "opacity-100" : "opacity-0",
216
- )}
217
- style={!isDefaultWl ? { backgroundColor: primaryColor } : undefined}
218
- />
308
+ />
309
+ </div>
310
+ ) : null}
219
311
  </div>
220
312
  </div>
221
313
 
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef } from 'react';
4
+
5
+ export type DomMutationGuardHit = {
6
+ kind: 'removeChild' | 'insertBefore';
7
+ language: string;
8
+ languages: string;
9
+ documentLang: string;
10
+ translatedNodes: number;
11
+ path: string;
12
+ }
13
+
14
+ /**
15
+ * Reporta quando a guarda de mutação de DOM (`DOM_MUTATION_GUARD_SCRIPT`) entra em ação.
16
+ *
17
+ * Sem isso a correção fica cega: o app para de cair, mas ninguém descobre quantos clientes estão
18
+ * navegando com o tradutor do navegador ligado. Reporta UMA vez por sessão — o objetivo é medir
19
+ * quantas sessões são afetadas, não quantas operações de DOM foram salvas.
20
+ */
21
+ export function useDomMutationGuardHit(onHit: (hit: DomMutationGuardHit) => void): void {
22
+ const onHitRef = useRef(onHit);
23
+ onHitRef.current = onHit;
24
+
25
+ useEffect(() => {
26
+ let reported = false;
27
+
28
+ const report = (kind: 'removeChild' | 'insertBefore') => {
29
+ if (reported) return;
30
+ reported = true;
31
+ window.__gpDomGuardOnHit = null;
32
+ onHitRef.current({
33
+ kind,
34
+ language: navigator.language,
35
+ languages: navigator.languages?.join(',') ?? '',
36
+ documentLang: document.documentElement.lang,
37
+ // <font> sem classe é a assinatura do Google Tradutor
38
+ translatedNodes: document.querySelectorAll('font[style]').length,
39
+ path: window.location.pathname,
40
+ });
41
+ };
42
+
43
+ // Disparos que aconteceram antes deste componente montar.
44
+ const already = window.__gpDomGuard;
45
+ if (already && (already.removeChild > 0 || already.insertBefore > 0)) {
46
+ report(already.insertBefore > 0 ? 'insertBefore' : 'removeChild');
47
+ return;
48
+ }
49
+
50
+ window.__gpDomGuardOnHit = report;
51
+ return () => {
52
+ window.__gpDomGuardOnHit = null;
53
+ };
54
+ }, []);
55
+ }
@@ -540,6 +540,8 @@ const messages = {
540
540
  expandMenu: 'Expand menu',
541
541
  greatPages: 'GreatPages',
542
542
  pages: 'Pages',
543
+ greatDomain: 'GreatDomain',
544
+ domains: 'Domains',
543
545
  },
544
546
  profile: {
545
547
  usedCredits: 'Credits used',
@@ -541,6 +541,8 @@ const messages = {
541
541
  expandMenu: 'Expandir menú',
542
542
  greatPages: 'GreatPages',
543
543
  pages: 'Páginas',
544
+ greatDomain: 'GreatDomain',
545
+ domains: 'Dominios',
544
546
  },
545
547
  profile: {
546
548
  usedCredits: 'Créditos utilizados',
@@ -620,6 +620,8 @@ const messages = {
620
620
  expandMenu: 'Expandir menu',
621
621
  greatPages: 'GreatPages',
622
622
  pages: 'Páginas',
623
+ greatDomain: 'GreatDomain',
624
+ domains: 'Domínios',
623
625
  },
624
626
  profile: {
625
627
  usedCredits: 'Créditos usados',
@@ -39,14 +39,24 @@ export interface ResolveLocaleOptions {
39
39
  userLanguage?: string | null;
40
40
  }
41
41
 
42
+ function localeFromAcceptLanguage(header: string | null): Locale | null {
43
+ if (!header) return null;
44
+ for (const entry of header.split(',')) {
45
+ const locale = normalizeLocale(entry.split(';')[0].trim());
46
+ if (locale) return locale;
47
+ }
48
+ return null;
49
+ }
50
+
42
51
  /**
43
52
  * Resolve o locale da requisição (server-side). Ordem de prioridade:
44
53
  *
45
54
  * 1. **Whitelabel não-padrão** (`id_wl != 1`) → `pt` (por enquanto, whitelabels não são localizadas).
46
55
  * 2. **Preferência da conta** (`userLanguage`), quando fornecida.
47
56
  * 3. **Cookie `NEXT_LOCALE`** — cache da preferência ou escolha manual do visitante.
48
- * 4. **Inferência por país** — JWT (`session.location.country`) ou header `CF-IPCountry`.
49
- * 5. **Fallback** → {@link DEFAULT_LOCALE}.
57
+ * 4. **Header `Accept-Language`** do navegador.
58
+ * 5. **Inferência por país** — JWT (`session.location.country`) ou header `CF-IPCountry`.
59
+ * 6. **Fallback** → {@link DEFAULT_LOCALE}.
50
60
  */
51
61
  export async function resolveLocale(options: ResolveLocaleOptions = {}): Promise<Locale> {
52
62
  const cookieStore = await cookies();
@@ -67,12 +77,17 @@ export async function resolveLocale(options: ResolveLocaleOptions = {}): Promise
67
77
  const fromCookie = normalizeLocale(cookieStore.get(LOCALE_COOKIE)?.value);
68
78
  if (fromCookie) return fromCookie;
69
79
 
70
- // 4. Inferência por país (geo)
71
80
  const headersList = await headers();
81
+
82
+ // 4. Header Accept-Language do navegador
83
+ const fromAcceptLanguage = localeFromAcceptLanguage(headersList.get('accept-language'));
84
+ if (fromAcceptLanguage) return fromAcceptLanguage;
85
+
86
+ // 5. Inferência por país (geo)
72
87
  const country = jwt?.country ?? headersList.get(GEO_HEADER);
73
88
  const fromGeo = localeFromCountry(country);
74
89
  if (fromGeo) return fromGeo;
75
90
 
76
- // 5. Fallback
91
+ // 6. Fallback
77
92
  return DEFAULT_LOCALE;
78
93
  }
package/src/index.ts CHANGED
@@ -515,6 +515,9 @@ export { default as usePasswordVisibility } from "./hooks/usePasswordVisibility"
515
515
  export { default as useCountdownTimer } from "./hooks/useCountdownTimer";
516
516
  export { default as useIsMobile } from "./hooks/useIsMobile";
517
517
  export { useDebounce } from "./hooks/useDebounce";
518
+ export { useDomMutationGuardHit } from "./hooks/useDomMutationGuardHit";
519
+ export type { DomMutationGuardHit } from "./hooks/useDomMutationGuardHit";
520
+ export { DOM_MUTATION_GUARD_SCRIPT } from "./utils/dom/dom-mutation-guard";
518
521
  export { useDebouncedEffect } from "./hooks/useDebouncedEffect";
519
522
  export { useDebounceState } from "./hooks/useDebounceState";
520
523
 
@@ -41,6 +41,8 @@ import {
41
41
  VerifyEmailResponse,
42
42
  } from "../schema";
43
43
  import { findWhitelabel } from "../../../server";
44
+ import { resolveLocale } from "../../../i18n/resolve-locale";
45
+ import { localeToApiLanguage } from "../../../utils/intl/locales";
44
46
 
45
47
  const AUTH_COOKIE_NAME = "greatapps";
46
48
  const COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
@@ -295,12 +297,15 @@ class AuthService {
295
297
 
296
298
  const idPlan = await this.resolvePlanId(data.idPlan);
297
299
 
300
+ const whitelabel = await findWhitelabel().catch(() => null);
301
+ const language = localeToApiLanguage(await resolveLocale({ whitelabelId: whitelabel?.id ?? null }));
302
+
298
303
  const payload = {
299
304
  onboarding_token: data.onboardingToken,
300
305
  name: data.user.name,
301
306
  bussiness_type: data.businessType ?? 0,
302
- language: "pt-br",
303
- timezone: "America/Sao_Paulo",
307
+ language,
308
+ timezone: clientInfo.timezone,
304
309
  currency: "BRL",
305
310
  location: clientInfo.location,
306
311
  ip: clientInfo.ip,
@@ -317,7 +322,7 @@ class AuthService {
317
322
  ddi: data.user.ddi || "55",
318
323
  phone: data.user.phone || "",
319
324
  profile: "owner",
320
- language: "pt-br",
325
+ language,
321
326
  },
322
327
  subscription:
323
328
  isFreePlan(idPlan)
@@ -411,11 +416,14 @@ class AuthService {
411
416
 
412
417
  const idPlan = await this.resolvePlanId(data.idPlan);
413
418
 
419
+ const whitelabel = await findWhitelabel().catch(() => null);
420
+ const language = localeToApiLanguage(await resolveLocale({ whitelabelId: whitelabel?.id ?? null }));
421
+
414
422
  const payload: RegisterApiRequest = {
415
423
  name: data.accountName,
416
424
  bussiness_type: data.businessType ?? 0,
417
- language: "pt-br",
418
- timezone: "America/Sao_Paulo",
425
+ language,
426
+ timezone: clientInfo.timezone,
419
427
  currency: "BRL",
420
428
  location: clientInfo.location,
421
429
  ip: clientInfo.ip,
@@ -433,7 +441,7 @@ class AuthService {
433
441
  phone: data.phone,
434
442
  password: data.password,
435
443
  profile: "owner",
436
- language: "pt-br",
444
+ language,
437
445
  },
438
446
  subscription:
439
447
  isFreePlan(idPlan)
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Guarda contra mutação externa do DOM (tradutor do navegador, extensões).
3
+ *
4
+ * PROBLEMA: o Google Tradutor do Chrome substitui cada text node por `<font><font>…</font></font>`.
5
+ * O React continua guardando a referência do text node ORIGINAL no fiber. No commit seguinte ele
6
+ * chama `parent.insertBefore(novo, textNodeAntigo)` ou `parent.removeChild(textNodeAntigo)` — e o nó
7
+ * já não é mais filho daquele pai. O DOM levanta `NotFoundError`, o erro sobe até o root e o app
8
+ * inteiro cai no `global-error.tsx` (evento `render.root_crashed` no Sentry).
9
+ *
10
+ * Isso derrubou clientes reais em CO/PE/MX/AR/FR (zero casos no BR, onde a página já está no idioma
11
+ * do usuário e o Chrome não oferece tradução), inclusive em cima da tela de trocar cartão — o cliente
12
+ * ficava sem conseguir pagar.
13
+ *
14
+ * SOLUÇÃO: transformar essas duas operações em no-op quando o nó de referência já não pertence mais
15
+ * ao pai. O React segue adiante, a página não morre, e o trecho traduzido apenas deixa de receber
16
+ * aquela atualização pontual (o próximo re-render completo normaliza).
17
+ *
18
+ * Precisa rodar ANTES da hidratação, por isso é injetado como `<script>` inline no `<head>` do root
19
+ * layout em vez de virar um `useEffect`.
20
+ */
21
+
22
+ declare global {
23
+ interface Window {
24
+ /** Contadores de quantas vezes a guarda entrou em ação nesta sessão. */
25
+ __gpDomGuard?: { removeChild: number; insertBefore: number };
26
+ /** Hook opcional preenchido pelo reporter para avisar o Sentry no primeiro disparo. */
27
+ __gpDomGuardOnHit?: ((kind: 'removeChild' | 'insertBefore') => void) | null;
28
+ }
29
+ }
30
+
31
+ export const DOM_MUTATION_GUARD_SCRIPT = `(function(){
32
+ if(typeof Node==="undefined"||!Node.prototype)return;
33
+ if(window.__gpDomGuard)return;
34
+ var s={removeChild:0,insertBefore:0};
35
+ window.__gpDomGuard=s;
36
+ window.__gpDomGuardOnHit=window.__gpDomGuardOnHit||null;
37
+ function hit(k){s[k]++;try{if(window.__gpDomGuardOnHit)window.__gpDomGuardOnHit(k)}catch(e){}}
38
+ var removeChild=Node.prototype.removeChild;
39
+ Node.prototype.removeChild=function(child){
40
+ if(child&&child.parentNode!==this){hit("removeChild");return child}
41
+ return removeChild.apply(this,arguments)};
42
+ var insertBefore=Node.prototype.insertBefore;
43
+ Node.prototype.insertBefore=function(newNode,referenceNode){
44
+ if(referenceNode&&referenceNode.parentNode!==this){hit("insertBefore");return newNode}
45
+ return insertBefore.apply(this,arguments)};
46
+ })();`;
@@ -1,3 +1,5 @@
1
+ import { Locale } from '../../i18n/config';
2
+
1
3
  export type LocaleOption = {
2
4
  value: string;
3
5
  label: string;
@@ -14,3 +16,8 @@ export const LANGUAGE_OPTIONS: LocaleOption[] = [
14
16
  export function buildLocaleOptions(): LocaleOption[] {
15
17
  return LANGUAGE_OPTIONS;
16
18
  }
19
+
20
+ export function localeToApiLanguage(locale: Locale): string {
21
+ const option = LANGUAGE_OPTIONS.find((o) => o.value === locale);
22
+ return option?.apiValue ?? locale;
23
+ }