From 92662e1624a3c23fc3315c7345d355c860dfae02 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 31 May 2025 19:22:43 +0700 Subject: [PATCH] feat: before translation --- apps/frontend/next.config.js | 1 - apps/frontend/src/app/(app)/layout.tsx | 4 + .../launches/launches.component.tsx | 4 +- .../src/components/launches/menu/menu.tsx | 3 + .../components/layout/language.component.tsx | 151 +++++++++++++++ .../src/components/layout/layout.settings.tsx | 2 + apps/frontend/src/middleware.ts | 28 ++- .../src/helpers/variable.context.tsx | 2 + .../get.transation.service.client.ts | 39 ++++ .../get.translation.service.backend.ts | 18 ++ .../src/translation/i18n.config.ts | 6 + .../src/translation/i18next.ts | 33 ++++ .../translation/locales/en/translation.json | 3 + package.json | 9 + pnpm-lock.yaml | 180 +++++++++++++++++- 15 files changed, 475 insertions(+), 8 deletions(-) create mode 100644 apps/frontend/src/components/layout/language.component.tsx create mode 100644 libraries/react-shared-libraries/src/translation/get.transation.service.client.ts create mode 100644 libraries/react-shared-libraries/src/translation/get.translation.service.backend.ts create mode 100644 libraries/react-shared-libraries/src/translation/i18n.config.ts create mode 100644 libraries/react-shared-libraries/src/translation/i18next.ts create mode 100644 libraries/react-shared-libraries/src/translation/locales/en/translation.json diff --git a/apps/frontend/next.config.js b/apps/frontend/next.config.js index 8244048e..9f459813 100644 --- a/apps/frontend/next.config.js +++ b/apps/frontend/next.config.js @@ -1,5 +1,4 @@ // @ts-check - /** @type {import('next').NextConfig} */ const nextConfig = { experimental: { diff --git a/apps/frontend/src/app/(app)/layout.tsx b/apps/frontend/src/app/(app)/layout.tsx index e40404b0..d610ff69 100644 --- a/apps/frontend/src/app/(app)/layout.tsx +++ b/apps/frontend/src/app/(app)/layout.tsx @@ -15,10 +15,13 @@ import { PHProvider } from '@gitroom/react/helpers/posthog'; import UtmSaver from '@gitroom/helpers/utils/utm.saver'; import { ToltScript } from '@gitroom/frontend/components/layout/tolt.script'; import { FacebookComponent } from '@gitroom/frontend/components/layout/facebook.component'; +import { headers } from 'next/headers'; +import { headerName } from '@gitroom/react/translation/i18n.config'; const chakra = Chakra_Petch({ weight: '400', subsets: ['latin'] }); export default async function AppLayout({ children }: { children: ReactNode }) { + const allHeaders = headers(); const Plausible = !!process.env.STRIPE_PUBLISHABLE_KEY ? PlausibleProvider : Fragment; @@ -49,6 +52,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) { neynarClientId={process.env.NEYNAR_CLIENT_ID!} isSecured={!process.env.NOT_SECURED} disableImageCompression={!!process.env.DISABLE_IMAGE_COMPRESSION} + language={allHeaders.get(headerName)} > diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx index daa73061..d9831979 100644 --- a/apps/frontend/src/components/launches/launches.component.tsx +++ b/apps/frontend/src/components/launches/launches.component.tsx @@ -23,6 +23,7 @@ import { DNDProvider } from '@gitroom/frontend/components/launches/helpers/dnd.p import { GeneratorComponent } from './generator/generator'; import { useVariables } from '@gitroom/react/helpers/variable.context'; import { NewPost } from '@gitroom/frontend/components/launches/new.post'; +import { useT } from '@gitroom/react/translation/get.transation.service.client'; interface MenuComponentInterface { refreshChannel: ( @@ -280,6 +281,7 @@ export const LaunchesComponent = () => { const search = useSearchParams(); const toast = useToaster(); const fireEvents = useFireEvents(); + const t = useT(); const [reload, setReload] = useState(false); const load = useCallback(async (path: string) => { @@ -418,7 +420,7 @@ export const LaunchesComponent = () => {
-

Channels

+

{t('channels')}

{sortedIntegrations.length === 0 && (
No channels
diff --git a/apps/frontend/src/components/launches/menu/menu.tsx b/apps/frontend/src/components/launches/menu/menu.tsx index 6398e10d..1b36c4a5 100644 --- a/apps/frontend/src/components/launches/menu/menu.tsx +++ b/apps/frontend/src/components/launches/menu/menu.tsx @@ -1,3 +1,5 @@ +'use client'; + import React, { FC, MouseEventHandler, @@ -19,6 +21,7 @@ import { Integration } from '@prisma/client'; import { SettingsModal } from '@gitroom/frontend/components/launches/settings.modal'; import { CustomVariables } from '@gitroom/frontend/components/launches/add.provider.component'; import { useRouter } from 'next/navigation'; +import { useT } from '@gitroom/react/translation/get.transation.service.client'; export const Menu: FC<{ canEnable: boolean; diff --git a/apps/frontend/src/components/layout/language.component.tsx b/apps/frontend/src/components/layout/language.component.tsx new file mode 100644 index 00000000..7938485e --- /dev/null +++ b/apps/frontend/src/components/layout/language.component.tsx @@ -0,0 +1,151 @@ +'use client'; + +import { useModals } from '@mantine/modals'; +import { + cookieName, + fallbackLng, + languages, +} from '@gitroom/react/translation/i18n.config'; +import i18next from 'i18next'; +import useCookie from 'react-use-cookie'; +import ReactCountryFlag from 'react-country-flag'; +import { List, Box, Group, Text } from '@mantine/core'; +import { useCallback } from 'react'; +import countries from 'i18n-iso-countries'; + +// Register required locales +import countriesEn from 'i18n-iso-countries/langs/en.json'; +countries.registerLocale(countriesEn); + +export const ChangeLanguageComponent = () => { + const currentLanguage = i18next.resolvedLanguage || fallbackLng; + const availableLanguages = languages; + const [_, setCookie] = useCookie(cookieName, currentLanguage || fallbackLng); + + const handleLanguageChange = (language: string) => { + setCookie(language); + window.location.reload(); + }; + + // Function to get language name in its native script + const getLanguageName = useCallback((code: string) => { + try { + // Use browser's Intl API to get language name in native script + const displayNames = new Intl.DisplayNames([code], { type: 'language' }); + return displayNames.of(code); + } catch (error) { + // Fallback to language code if the API isn't supported or language is not found + return code; + } + }, []); + + // Get appropriate country code for the flag based on language code + const getCountryCodeForFlag = useCallback((languageCode: string) => { + // For multi-region languages, here are some common defaults + if (languageCode === 'en') return 'GB'; + if (languageCode === 'es') return 'ES'; + if (languageCode === 'ar') return 'SA'; + if (languageCode === 'zh') return 'CN'; + + // Check if language code itself is a valid country code + try { + const countryName = countries.getName(languageCode.toUpperCase(), 'en'); + if (countryName) { + return languageCode.toUpperCase(); + } + } catch (e) { + // Not a valid country code, continue to next approach + } + + // Try to extract region code if language code has a region component (e.g., en-US) + const parts = languageCode.split('-'); + if (parts.length > 1) { + const regionCode = parts[1].toUpperCase(); + try { + const countryName = countries.getName(regionCode, 'en'); + if (countryName) { + return regionCode; + } + } catch (e) { + // Not a valid country code, continue to next approach + } + } + + // For most language codes that match their primary country + // Examples: fr->FR, it->IT, de->DE, etc. + return languageCode.toUpperCase(); + }, []); + + return ( + + {availableLanguages.map((language) => ( + + ({ + padding: '8px 12px', + borderRadius: theme.radius.sm, + cursor: 'pointer', + backgroundColor: + language === currentLanguage + ? theme.colorScheme === 'dark' + ? theme.colors.dark[5] + : theme.colors.gray[1] + : 'transparent', + '&:hover': { + backgroundColor: + theme.colorScheme === 'dark' + ? theme.colors.dark[6] + : theme.colors.gray[0], + }, + })} + onClick={() => handleLanguageChange(language)} + > + + + + {getLanguageName(language)} + + + + + ))} + + ); +}; + +export const LanguageComponent = () => { + const modal = useModals(); + const openModal = () => { + modal.openModal({ + title: 'Change Language', + children: , + size: 'md', + centered: true, + }); + }; + + return ( + + + + ); +}; diff --git a/apps/frontend/src/components/layout/layout.settings.tsx b/apps/frontend/src/components/layout/layout.settings.tsx index 61dff892..77a396ec 100644 --- a/apps/frontend/src/components/layout/layout.settings.tsx +++ b/apps/frontend/src/components/layout/layout.settings.tsx @@ -40,6 +40,7 @@ import { extend } from 'dayjs'; import { useSearchParams } from 'next/navigation'; import { CheckPayment } from '@gitroom/frontend/components/layout/check.payment'; import { ChromeExtensionComponent } from '@gitroom/frontend/components/layout/chrome.extension.component'; +import { LanguageComponent } from '@gitroom/frontend/components/layout/language.component'; extend(utc); extend(weekOfYear); @@ -142,6 +143,7 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => { id="systray-buttons" className="flex items-center justify-self-end gap-[8px] order-2 md:order-3" > + diff --git a/apps/frontend/src/middleware.ts b/apps/frontend/src/middleware.ts index 1797e737..514ea3d6 100644 --- a/apps/frontend/src/middleware.ts +++ b/apps/frontend/src/middleware.ts @@ -2,18 +2,38 @@ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management'; import { internalFetch } from '@gitroom/helpers/utils/internal.fetch'; +import acceptLanguage from 'accept-language'; +import { + cookieName, + fallbackLng, + headerName, + languages, +} from '@gitroom/react/translation/i18n.config'; +acceptLanguage.languages(languages); // This function can be marked `async` if using `await` inside export async function middleware(request: NextRequest) { const nextUrl = request.nextUrl; - const authCookie = request.cookies.get('auth') || request.headers.get('auth') || nextUrl.searchParams.get('loggedAuth'); + const authCookie = + request.cookies.get('auth') || + request.headers.get('auth') || + nextUrl.searchParams.get('loggedAuth'); + + const lng = + (request.cookies.has(cookieName) + ? acceptLanguage.get(request.cookies.get(cookieName).value) + : acceptLanguage.get(request.headers.get('Accept-Language'))) || + fallbackLng; + + const headers = new Headers(request.headers); + headers.set(headerName, lng); if ( nextUrl.pathname.startsWith('/uploads/') || nextUrl.pathname.startsWith('/p/') || nextUrl.pathname.startsWith('/icons/') ) { - return NextResponse.next(); + return NextResponse.next({ headers }); } // If the URL is logout, delete the cookie and redirect to login if (nextUrl.href.indexOf('/auth/logout') > -1) { @@ -77,7 +97,7 @@ export async function middleware(request: NextRequest) { }); return redirect; } - return NextResponse.next(); + return NextResponse.next({ headers }); } try { @@ -121,7 +141,7 @@ export async function middleware(request: NextRequest) { ); } - const next = NextResponse.next(); + const next = NextResponse.next({ headers }); if ( nextUrl.pathname === '/marketplace/seller' || diff --git a/libraries/react-shared-libraries/src/helpers/variable.context.tsx b/libraries/react-shared-libraries/src/helpers/variable.context.tsx index 7edfdcc2..e66d8f1f 100644 --- a/libraries/react-shared-libraries/src/helpers/variable.context.tsx +++ b/libraries/react-shared-libraries/src/helpers/variable.context.tsx @@ -19,6 +19,7 @@ interface VariableContextInterface { neynarClientId: string; isSecured: boolean; disableImageCompression: boolean; + language: string; tolt: string; } const VariableContext = createContext({ @@ -38,6 +39,7 @@ const VariableContext = createContext({ facebookPixel: '', neynarClientId: '', disableImageCompression: false, + language: '', tolt: '', } as VariableContextInterface); diff --git a/libraries/react-shared-libraries/src/translation/get.transation.service.client.ts b/libraries/react-shared-libraries/src/translation/get.transation.service.client.ts new file mode 100644 index 00000000..01c493e4 --- /dev/null +++ b/libraries/react-shared-libraries/src/translation/get.transation.service.client.ts @@ -0,0 +1,39 @@ +'use client'; + +import i18next from './i18next'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { UseTranslationOptions } from 'react-i18next/index'; +import useCookie from 'react-use-cookie'; +import { cookieName, fallbackLng } from '@gitroom/react/translation/i18n.config'; +import { useVariables } from '@gitroom/react/helpers/variable.context'; + +const runsOnServerSide = typeof window === 'undefined'; + +export function useT(ns?: string, options?: UseTranslationOptions) { + const {language} = useVariables(); + const [lng] = useCookie(cookieName, language || fallbackLng); + + if (typeof lng !== 'string') { + throw new Error('useT is only available inside /app/[lng]'); + } + + const [activeLng, setActiveLng] = useState(i18next.resolvedLanguage); + const {t} = useTranslation(ns, options); + + useEffect(() => { + if (activeLng === i18next.resolvedLanguage) return; + setActiveLng(i18next.resolvedLanguage); + }, [activeLng]); + + useEffect(() => { + if (!lng || i18next.resolvedLanguage === lng) return; + i18next.changeLanguage(lng); + }, [lng]); + + if (runsOnServerSide && i18next.resolvedLanguage !== lng) { + i18next.changeLanguage(lng); + } + + return t; +} \ No newline at end of file diff --git a/libraries/react-shared-libraries/src/translation/get.translation.service.backend.ts b/libraries/react-shared-libraries/src/translation/get.translation.service.backend.ts new file mode 100644 index 00000000..23926923 --- /dev/null +++ b/libraries/react-shared-libraries/src/translation/get.translation.service.backend.ts @@ -0,0 +1,18 @@ +import i18next from './i18next' +import { headerName } from './i18n.config' +import { headers } from 'next/headers' + +export async function getT(ns: string, options: any) { + const headerList = await headers() + const lng = headerList.get(headerName) + if (lng && i18next.resolvedLanguage !== lng) { + await i18next.changeLanguage(lng) + } + if (ns && !i18next.hasLoadedNamespace(ns)) { + await i18next.loadNamespaces(ns) + } + return { + t: i18next.getFixedT(lng ?? i18next.resolvedLanguage, Array.isArray(ns) ? ns[0] : ns, options?.keyPrefix), + i18n: i18next + } +} \ No newline at end of file diff --git a/libraries/react-shared-libraries/src/translation/i18n.config.ts b/libraries/react-shared-libraries/src/translation/i18n.config.ts new file mode 100644 index 00000000..efc1abd1 --- /dev/null +++ b/libraries/react-shared-libraries/src/translation/i18n.config.ts @@ -0,0 +1,6 @@ +export const fallbackLng = 'en'; +export const languages = [fallbackLng]; +export const defaultNS = 'translation'; +export const cookieName = 'i18next'; +export const headerName = 'x-i18next-current-language'; + diff --git a/libraries/react-shared-libraries/src/translation/i18next.ts b/libraries/react-shared-libraries/src/translation/i18next.ts new file mode 100644 index 00000000..6c80eef7 --- /dev/null +++ b/libraries/react-shared-libraries/src/translation/i18next.ts @@ -0,0 +1,33 @@ +import i18next from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import resourcesToBackend from 'i18next-resources-to-backend'; +import { initReactI18next } from 'react-i18next/initReactI18next'; +import { fallbackLng, languages, defaultNS } from './i18n.config'; + +const runsOnServerSide = typeof window === 'undefined'; + +i18next + .use(initReactI18next) + .use(LanguageDetector) + .use( + resourcesToBackend( + (language: any, namespace: any) => { + console.log(namespace, language); + return import(`./locales/${language}/${namespace}.json`); + } + ) + ) + .init({ + // debug: true, + supportedLngs: languages, + fallbackLng, + lng: undefined, // let detect the language on client side + fallbackNS: defaultNS, + defaultNS, + detection: { + order: ['path', 'htmlTag', 'cookie', 'navigator'], + }, + preload: runsOnServerSide ? languages : [], + }); + +export default i18next; diff --git a/libraries/react-shared-libraries/src/translation/locales/en/translation.json b/libraries/react-shared-libraries/src/translation/locales/en/translation.json new file mode 100644 index 00000000..8f01b528 --- /dev/null +++ b/libraries/react-shared-libraries/src/translation/locales/en/translation.json @@ -0,0 +1,3 @@ +{ + "channels": "Channels" +} \ No newline at end of file diff --git a/package.json b/package.json index 25d64a10..f53a69f3 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "@uppy/react": "^4.0.2", "@uppy/status-bar": "^4.0.3", "@uppy/xhr-upload": "^4.1.0", + "accept-language": "^3.0.20", "ai": "^4.0.22", "algoliasearch": "^5.18.0", "array-move": "^4.0.0", @@ -144,6 +145,10 @@ "google-auth-library": "^9.11.0", "googleapis": "^137.1.0", "hot-reload-extension-vite": "^1.0.13", + "i18n-iso-countries": "^7.14.0", + "i18next": "^25.2.1", + "i18next-browser-languagedetector": "^8.1.0", + "i18next-resources-to-backend": "^1.2.1", "ioredis": "^5.3.2", "json-to-graphql-query": "^2.2.5", "jsonwebtoken": "^9.0.2", @@ -155,6 +160,7 @@ "nestjs-command": "^3.1.4", "nestjs-real-ip": "^3.0.1", "next": "^14.2.14", + "next-i18next": "^15.4.2", "next-plausible": "^3.12.0", "node-telegram-bot-api": "^0.66.0", "nodemailer": "^6.9.15", @@ -165,14 +171,17 @@ "posthog-js": "^1.178.0", "react": "18.3.1", "react-colorful": "^5.6.1", + "react-country-flag": "^3.1.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "18.3.1", "react-dropzone": "^14.3.5", "react-hook-form": "^7.50.1", + "react-i18next": "^15.5.2", "react-loading": "^2.0.3", "react-tag-autocomplete": "^7.2.0", "react-tooltip": "^5.26.2", + "react-use-cookie": "^1.6.1", "react-use-keypress": "^1.3.1", "redis": "^4.6.12", "reflect-metadata": "^0.1.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6484f6b7..15048b2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -231,6 +231,9 @@ importers: '@uppy/xhr-upload': specifier: ^4.1.0 version: 4.3.3(@uppy/core@4.4.4) + accept-language: + specifier: ^3.0.20 + version: 3.0.20 ai: specifier: ^4.0.22 version: 4.3.13(react@18.3.1)(zod@3.24.4) @@ -309,6 +312,18 @@ importers: hot-reload-extension-vite: specifier: ^1.0.13 version: 1.0.13(bufferutil@4.0.9)(utf-8-validate@5.0.10) + i18n-iso-countries: + specifier: ^7.14.0 + version: 7.14.0 + i18next: + specifier: ^25.2.1 + version: 25.2.1(typescript@5.5.4) + i18next-browser-languagedetector: + specifier: ^8.1.0 + version: 8.1.0 + i18next-resources-to-backend: + specifier: ^1.2.1 + version: 1.2.1 ioredis: specifier: ^5.3.2 version: 5.6.1 @@ -342,6 +357,9 @@ importers: next: specifier: ^14.2.14 version: 14.2.28(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.62.1) + next-i18next: + specifier: ^15.4.2 + version: 15.4.2(i18next@25.2.1(typescript@5.5.4))(next@14.2.28(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.62.1))(react-i18next@15.5.2(i18next@25.2.1(typescript@5.5.4))(react-dom@18.3.1(react@18.3.1))(react-native@0.79.2(@babel/core@7.27.1)(@types/react@18.3.1)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.5.4))(react@18.3.1) next-plausible: specifier: ^3.12.0 version: 3.12.4(next@14.2.28(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.62.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -372,9 +390,12 @@ importers: react-colorful: specifier: ^5.6.1 version: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-country-flag: + specifier: ^3.1.0 + version: 3.1.0(react@18.3.1) react-dnd: specifier: ^16.0.1 - version: 16.0.1(@types/node@18.16.9)(@types/react@18.3.1)(react@18.3.1) + version: 16.0.1(@types/hoist-non-react-statics@3.3.6)(@types/node@18.16.9)(@types/react@18.3.1)(react@18.3.1) react-dnd-html5-backend: specifier: ^16.0.1 version: 16.0.1 @@ -387,6 +408,9 @@ importers: react-hook-form: specifier: ^7.50.1 version: 7.56.2(react@18.3.1) + react-i18next: + specifier: ^15.5.2 + version: 15.5.2(i18next@25.2.1(typescript@5.5.4))(react-dom@18.3.1(react@18.3.1))(react-native@0.79.2(@babel/core@7.27.1)(@types/react@18.3.1)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.5.4) react-loading: specifier: ^2.0.3 version: 2.0.3(prop-types@15.8.1)(react@18.3.1) @@ -396,6 +420,9 @@ importers: react-tooltip: specifier: ^5.26.2 version: 5.28.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-use-cookie: + specifier: ^1.6.1 + version: 1.6.1(react@18.3.1) react-use-keypress: specifier: ^1.3.1 version: 1.3.1(react@18.3.1) @@ -5936,6 +5963,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hoist-non-react-statics@3.3.6': + resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==} + '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} @@ -6723,6 +6753,9 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + accept-language@3.0.20: + resolution: {integrity: sha512-xklPzRma4aoDEPk0ZfMjeuxB2FP4JBYlAR25OFUqCoOYDjYo6wGwAs49SnTN/MoB5VpnNX9tENfZ+vEIFmHQMQ==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -7167,6 +7200,10 @@ packages: bcp-47-match@2.0.3: resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} + bcp47@1.1.2: + resolution: {integrity: sha512-JnkkL4GUpOvvanH9AZPX38CxhiLsXMBicBY2IAtqiVN8YulGDQybUydWA4W6yAMtw6iShtw+8HEF6cfrTHU+UQ==} + engines: {node: '>=0.10'} + bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} @@ -8278,6 +8315,9 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diacritics@1.3.0: + resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -9627,6 +9667,9 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + html-to-text@9.0.5: resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} engines: {node: '>=14'} @@ -9733,6 +9776,27 @@ packages: resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} engines: {node: '>=10.18'} + i18n-iso-countries@7.14.0: + resolution: {integrity: sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==} + engines: {node: '>= 12'} + + i18next-browser-languagedetector@8.1.0: + resolution: {integrity: sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q==} + + i18next-fs-backend@2.6.0: + resolution: {integrity: sha512-3ZlhNoF9yxnM8pa8bWp5120/Ob6t4lVl1l/tbLmkml/ei3ud8IWySCHt2lrY5xWRlSU5D9IV2sm5bEbGuTqwTw==} + + i18next-resources-to-backend@1.2.1: + resolution: {integrity: sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==} + + i18next@25.2.1: + resolution: {integrity: sha512-+UoXK5wh+VlE1Zy5p6MjcvctHXAhRwQKCxiJD8noKZzIXmnAX8gdHX5fLPA3MEVxEN4vbZkQFy8N0LyD9tUqPw==} + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + ibm-cloud-sdk-core@5.3.2: resolution: {integrity: sha512-YhtS+7hGNO61h/4jNShHxbbuJ1TnDqiFKQzfEaqePnonOvv8NnxWxOk92FlKKCCzZNOT34Gnd7WCLVJTntwEFQ==} engines: {node: '>=18'} @@ -11744,6 +11808,15 @@ packages: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} + next-i18next@15.4.2: + resolution: {integrity: sha512-zgRxWf7kdXtM686ecGIBQL+Bq0+DqAhRlasRZ3vVF0TmrNTWkVhs52n//oU3Fj5O7r/xOKkECDUwfOuXVwTK/g==} + engines: {node: '>=14'} + peerDependencies: + i18next: '>= 23.7.13' + next: '>= 12.0.0' + react: '>= 17.0.2' + react-i18next: '>= 13.5.0' + next-plausible@3.12.4: resolution: {integrity: sha512-cD3+ixJxf8yBYvsideTxqli3fvrB7R4BXcvsNJz8Sm2X1QN039WfiXjCyNWkub4h5++rRs6fHhchUMnOuJokcg==} peerDependencies: @@ -12900,6 +12973,12 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + react-country-flag@3.1.0: + resolution: {integrity: sha512-JWQFw1efdv9sTC+TGQvTKXQg1NKbDU2mBiAiRWcKM9F1sK+/zjhP2yGmm8YDddWyZdXVkR8Md47rPMJmo4YO5g==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16' + react-devtools-core@6.1.1: resolution: {integrity: sha512-TFo1MEnkqE6hzAbaztnyR5uLTMoz6wnEWwWBsCUzNt+sVXJycuRJdDqvL078M4/h65BI/YO5XWTaxZDWVsW0fw==} @@ -12946,6 +13025,22 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-i18next@15.5.2: + resolution: {integrity: sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A==} + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -13136,6 +13231,12 @@ packages: '@types/react': optional: true + react-use-cookie@1.6.1: + resolution: {integrity: sha512-77bENFpc+pi1roLJIrRvekprB4IjwG1Mowir0QdbwA0pDjgMtp3Ya9E1H9dJAisCxBvTZih6k5+2SSVo9BR2qw==} + engines: {node: '>=8', npm: '>=5'} + peerDependencies: + react: '>=16.8' + react-use-keypress@1.3.1: resolution: {integrity: sha512-fo+LQrxviMcZt7efCFPc6CX9/oNEPD+MJ/qSs4nK3/lyRNtquhG9f1J8GQq2VFfIYUVDUdPKz8fGIwErO1Pcuw==} peerDependencies: @@ -15182,6 +15283,10 @@ packages: vlq@1.0.1: resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==} + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + w3c-xmlserializer@4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} @@ -22821,6 +22926,11 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/hoist-non-react-statics@3.3.6': + dependencies: + '@types/react': 18.3.1 + hoist-non-react-statics: 3.3.2 + '@types/http-cache-semantics@4.0.4': {} '@types/http-errors@2.0.4': {} @@ -24185,6 +24295,10 @@ snapshots: dependencies: event-target-shim: 5.0.1 + accept-language@3.0.20: + dependencies: + bcp47: 1.1.2 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -24686,6 +24800,8 @@ snapshots: bcp-47-match@2.0.3: {} + bcp47@1.1.2: {} + bcrypt-pbkdf@1.0.2: dependencies: tweetnacl: 0.14.5 @@ -25924,6 +26040,8 @@ snapshots: dependencies: dequal: 2.0.3 + diacritics@1.3.0: {} + didyoumean@1.2.2: {} diff-match-patch@1.0.5: {} @@ -27840,6 +27958,10 @@ snapshots: html-escaper@2.0.2: {} + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + html-to-text@9.0.5: dependencies: '@selderee/plugin-htmlparser2': 0.11.0 @@ -28006,6 +28128,26 @@ snapshots: hyperdyperid@1.2.0: {} + i18n-iso-countries@7.14.0: + dependencies: + diacritics: 1.3.0 + + i18next-browser-languagedetector@8.1.0: + dependencies: + '@babel/runtime': 7.27.1 + + i18next-fs-backend@2.6.0: {} + + i18next-resources-to-backend@1.2.1: + dependencies: + '@babel/runtime': 7.27.1 + + i18next@25.2.1(typescript@5.5.4): + dependencies: + '@babel/runtime': 7.27.1 + optionalDependencies: + typescript: 5.5.4 + ibm-cloud-sdk-core@5.3.2: dependencies: '@types/debug': 4.1.12 @@ -30698,6 +30840,18 @@ snapshots: netmask@2.0.2: {} + next-i18next@15.4.2(i18next@25.2.1(typescript@5.5.4))(next@14.2.28(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.62.1))(react-i18next@15.5.2(i18next@25.2.1(typescript@5.5.4))(react-dom@18.3.1(react@18.3.1))(react-native@0.79.2(@babel/core@7.27.1)(@types/react@18.3.1)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.5.4))(react@18.3.1): + dependencies: + '@babel/runtime': 7.27.1 + '@types/hoist-non-react-statics': 3.3.6 + core-js: 3.42.0 + hoist-non-react-statics: 3.3.2 + i18next: 25.2.1(typescript@5.5.4) + i18next-fs-backend: 2.6.0 + next: 14.2.28(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.62.1) + react: 18.3.1 + react-i18next: 15.5.2(i18next@25.2.1(typescript@5.5.4))(react-dom@18.3.1(react@18.3.1))(react-native@0.79.2(@babel/core@7.27.1)(@types/react@18.3.1)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.5.4) + next-plausible@3.12.4(next@14.2.28(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.62.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: next: 14.2.28(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.62.1) @@ -32030,6 +32184,10 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-country-flag@3.1.0(react@18.3.1): + dependencies: + react: 18.3.1 + react-devtools-core@6.1.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: shell-quote: 1.8.2 @@ -32042,7 +32200,7 @@ snapshots: dependencies: dnd-core: 16.0.1 - react-dnd@16.0.1(@types/node@18.16.9)(@types/react@18.3.1)(react@18.3.1): + react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.6)(@types/node@18.16.9)(@types/react@18.3.1)(react@18.3.1): dependencies: '@react-dnd/invariant': 4.0.2 '@react-dnd/shallowequal': 4.0.2 @@ -32051,6 +32209,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 18.3.1 optionalDependencies: + '@types/hoist-non-react-statics': 3.3.6 '@types/node': 18.16.9 '@types/react': 18.3.1 @@ -32078,6 +32237,17 @@ snapshots: dependencies: react: 18.3.1 + react-i18next@15.5.2(i18next@25.2.1(typescript@5.5.4))(react-dom@18.3.1(react@18.3.1))(react-native@0.79.2(@babel/core@7.27.1)(@types/react@18.3.1)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.5.4): + dependencies: + '@babel/runtime': 7.27.1 + html-parse-stringify: 3.0.1 + i18next: 25.2.1(typescript@5.5.4) + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + react-native: 0.79.2(@babel/core@7.27.1)(@types/react@18.3.1)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10) + typescript: 5.5.4 + react-is@16.13.1: {} react-is@17.0.2: {} @@ -32336,6 +32506,10 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 + react-use-cookie@1.6.1(react@18.3.1): + dependencies: + react: 18.3.1 + react-use-keypress@1.3.1(react@18.3.1): dependencies: react: 18.3.1 @@ -34671,6 +34845,8 @@ snapshots: vlq@1.0.1: {} + void-elements@3.1.0: {} + w3c-xmlserializer@4.0.0: dependencies: xml-name-validator: 4.0.0