From b07dfc4e48c6dccd8d7a05299387768c22f30638 Mon Sep 17 00:00:00 2001 From: kallebe Date: Thu, 26 Feb 2026 17:34:00 -0300 Subject: [PATCH 01/17] feat: setup next-intl and signUp translations --- apps/sim/app/(auth)/login/login-form.tsx | 6 +- apps/sim/app/(auth)/signup/signup-form.tsx | 70 +++++++++------- apps/sim/app/layout.tsx | 28 ++++--- apps/sim/components/locale-selector.tsx | 52 ++++++++++++ apps/sim/i18n/request.ts | 16 ++++ apps/sim/i18n/types.ts | 9 ++ .../lib/localization/change-locale.server.ts | 13 +++ apps/sim/next.config.ts | 6 +- apps/sim/package.json | 1 + apps/sim/translations/en.json | 26 ++++++ apps/sim/translations/es.json | 25 ++++++ apps/sim/translations/pt.json | 25 ++++++ bun.lock | 83 +++++++++++++++++++ 13 files changed, 315 insertions(+), 45 deletions(-) create mode 100644 apps/sim/components/locale-selector.tsx create mode 100644 apps/sim/i18n/request.ts create mode 100644 apps/sim/i18n/types.ts create mode 100644 apps/sim/lib/localization/change-locale.server.ts create mode 100644 apps/sim/translations/en.json create mode 100644 apps/sim/translations/es.json create mode 100644 apps/sim/translations/pt.json diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index c58b102bc54..df45b58293c 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -25,6 +25,7 @@ import { BrandedButton } from '@/app/(auth)/components/branded-button' import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons' import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button' import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' +import { useTranslations } from 'next-intl' const logger = createLogger('LoginForm') @@ -98,6 +99,7 @@ export default function LoginPage({ googleAvailable: boolean isProduction: boolean }) { + const t = useTranslations() const router = useRouter() const searchParams = useSearchParams() const [isLoading, setIsLoading] = useState(false) @@ -386,10 +388,10 @@ export default function LoginPage({ <>

- Sign in + {t('sign_in.page_title')}

- Enter your details + {t('sign_in.page_sub_title')}

diff --git a/apps/sim/app/(auth)/signup/signup-form.tsx b/apps/sim/app/(auth)/signup/signup-form.tsx index bab806f23a2..2bfccf3bfc2 100644 --- a/apps/sim/app/(auth)/signup/signup-form.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.tsx @@ -17,6 +17,8 @@ import { BrandedButton } from '@/app/(auth)/components/branded-button' import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons' import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button' import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' +import LocaleSelector from '@/components/locale-selector' +import { useTranslations } from 'next-intl' const logger = createLogger('SignupForm') @@ -81,6 +83,7 @@ function SignupFormContent({ googleAvailable: boolean isProduction: boolean }) { + const t = useTranslations() const router = useRouter() const searchParams = useSearchParams() const { refetch: refetchSession } = useSession() @@ -345,10 +348,10 @@ function SignupFormContent({ <>

- Create an account + {t('sign_up.page_title')}

- Create an account or log in + {t('sign_up.page_sub_title')}

@@ -375,12 +378,12 @@ function SignupFormContent({
- +
- +
- +
- Create account + {t('sign_up.create_account')} )} @@ -501,7 +504,9 @@ function SignupFormContent({
- Or continue with + + {t('sign_up.divider_label')} +
)} @@ -538,36 +543,41 @@ function SignupFormContent({ )}
- Already have an account? + {t('sign_up.already_have_account')} - Sign in + {t('sign_up.sign_in_redirect_label')}
- By creating an account, you agree to our{' '} - - Terms of Service - {' '} - and{' '} - - Privacy Policy - + {t.rich('sign_up.create_account_warning', { + terms: (chunks) => ( + + {chunks} + + ), + policy: (chunks) => ( + + {chunks} + + ), + })} +
) diff --git a/apps/sim/app/layout.tsx b/apps/sim/app/layout.tsx index 11d1b3036cc..a71af262c78 100644 --- a/apps/sim/app/layout.tsx +++ b/apps/sim/app/layout.tsx @@ -17,6 +17,8 @@ import { SessionProvider } from '@/app/_shell/providers/session-provider' import { ThemeProvider } from '@/app/_shell/providers/theme-provider' import { TooltipProvider } from '@/app/_shell/providers/tooltip-provider' import { season } from '@/app/_styles/fonts/season/season' +import { NextIntlClientProvider } from 'next-intl' +import { cookies } from 'next/headers' export const viewport: Viewport = { width: 'device-width', @@ -31,7 +33,7 @@ export const viewport: Viewport = { export const metadata: Metadata = generateBrandedMetadata() -export default function RootLayout({ children }: { children: React.ReactNode }) { +export default async function RootLayout({ children }: { children: React.ReactNode }) { const structuredData = generateStructuredData() const themeCSS = generateThemeCSS() @@ -215,17 +217,19 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - - - - - - {children} - - - - - + + + + + + + {children} + + + + + + ) diff --git a/apps/sim/components/locale-selector.tsx b/apps/sim/components/locale-selector.tsx new file mode 100644 index 00000000000..7b45b1e70c1 --- /dev/null +++ b/apps/sim/components/locale-selector.tsx @@ -0,0 +1,52 @@ +'use client' + +import { type Locale, useLocale } from 'next-intl' +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from './ui' +import { useTranslations } from 'next-intl' +import { useMemo } from 'react' +import { useRouter } from 'next/navigation' +import { changeLocale } from '@/lib/localization/change-locale.server' +import { useMutation } from '@tanstack/react-query' + +export default function LocaleSelector() { + const t = useTranslations() + const locale = useLocale() + const { refresh } = useRouter() + + const selectedLocaleLabel = useMemo(() => { + switch (locale) { + case 'pt': + return t('localization.pt') + case 'en': + return t('localization.en') + case 'es': + return t('localization.es') + } + }, [locale]) + + const { mutate: mutateChangeLocale, isPending: isChangingLocale } = useMutation({ + mutationFn: changeLocale, + onSuccess: () => { + refresh() + }, + }) + + const handleLocaleChange = async (locale: Locale) => { + mutateChangeLocale(locale) + } + + return ( + + ) +} diff --git a/apps/sim/i18n/request.ts b/apps/sim/i18n/request.ts new file mode 100644 index 00000000000..e0de5c4f646 --- /dev/null +++ b/apps/sim/i18n/request.ts @@ -0,0 +1,16 @@ +import { cookies } from 'next/headers' +import type { Locale } from 'next-intl' +import { getRequestConfig } from 'next-intl/server' + +export const locales = ['en', 'pt', 'es'] as const +const defaultLocale = 'en' + +export default getRequestConfig(async () => { + const store = await cookies() + const locale = (store.get('locale')?.value as Locale) || defaultLocale + + return { + locale, + messages: (await import(`../translations/${locale}.json`)).default, + } +}) diff --git a/apps/sim/i18n/types.ts b/apps/sim/i18n/types.ts new file mode 100644 index 00000000000..11a1d9420ce --- /dev/null +++ b/apps/sim/i18n/types.ts @@ -0,0 +1,9 @@ +import type messages from '@/translations/en.json' +import type { locales } from './request' + +declare module 'next-intl' { + interface AppConfig { + Locale: (typeof locales)[number] + Messages: typeof messages + } +} diff --git a/apps/sim/lib/localization/change-locale.server.ts b/apps/sim/lib/localization/change-locale.server.ts new file mode 100644 index 00000000000..ebb6b9c65c7 --- /dev/null +++ b/apps/sim/lib/localization/change-locale.server.ts @@ -0,0 +1,13 @@ +'use server' + +import type { Locale } from 'next-intl' +import { cookies } from 'next/headers' + +/** + * change user locale for SIM app + */ +export async function changeLocale(locale: Locale) { + const store = await cookies() + + store.set('locale', locale) +} diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts index ee3652e15b0..a8e89e95aad 100644 --- a/apps/sim/next.config.ts +++ b/apps/sim/next.config.ts @@ -7,6 +7,8 @@ import { getWorkflowExecutionCSPPolicy, } from './lib/core/security/csp' +import createNextIntlPlugin from 'next-intl/plugin' + const nextConfig: NextConfig = { devIndicators: false, images: { @@ -371,4 +373,6 @@ const nextConfig: NextConfig = { }, } -export default nextConfig +const withNextIntl = createNextIntlPlugin() + +export default withNextIntl(nextConfig) diff --git a/apps/sim/package.json b/apps/sim/package.json index 9068e49c3a0..da9c1c24371 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -126,6 +126,7 @@ "nanoid": "^3.3.7", "neo4j-driver": "6.0.1", "next": "16.1.6", + "next-intl": "4.8.3", "next-mdx-remote": "^5.0.0", "next-runtime-env": "3.3.0", "next-themes": "^0.4.6", diff --git a/apps/sim/translations/en.json b/apps/sim/translations/en.json new file mode 100644 index 00000000000..abfa36ed770 --- /dev/null +++ b/apps/sim/translations/en.json @@ -0,0 +1,26 @@ +{ + "localization": { + "pt": "PT - portuguese", + "en": "EN - english", + "es": "ES - Spanish" + }, + "sign_up": { + "page_title": "Create an account", + "page_sub_title": "Create an account or log in", + "full_name_label": "Full name", + "full_name_placeholder": "Enter your name", + "email_label": "Email", + "email_placeholder": "Enter your email", + "password_label": "Password", + "password_placeholder": "Enter your password", + "create_account": "Create account", + "already_have_account": "Already have an account?", + "sign_in_redirect_label": "Sign In", + "create_account_warning": "By creating an account, you agree to our Terms of Service and Privacy Policy", + "divider_label": "Or continue with" + }, + "sign_in": { + "page_title": "Sign in", + "page_sub_title": "Enter your details" + } +} \ No newline at end of file diff --git a/apps/sim/translations/es.json b/apps/sim/translations/es.json new file mode 100644 index 00000000000..b62fcf63a68 --- /dev/null +++ b/apps/sim/translations/es.json @@ -0,0 +1,25 @@ +{ + "localization": { + "pt": "PT - portugués", + "en": "EN - inglés", + "es": "ES - español" + }, + "sign_up": { + "page_title": "Crear una cuenta", + "page_sub_title": "Crea una cuenta o inicia sesión", + "full_name_label": "Nombre completo", + "full_name_placeholder": "Ingresa tu nombre", + "email_label": "Correo electrónico", + "email_placeholder": "Ingresa tu correo electrónico", + "password_label": "Contraseña", + "password_placeholder": "Ingresa tu contraseña", + "create_account": "Crear cuenta", + "already_have_account": "¿Ya tienes una cuenta?", + "sign_in_redirect_label": "Iniciar sesión", + "create_account_warning": "Al crear una cuenta, aceptas nuestros Términos de Servicio y la Política de Privacidad" + }, + "sign_in": { + "page_title": "Iniciar sesión", + "page_sub_title": "Ingresa tus datos" + } +} \ No newline at end of file diff --git a/apps/sim/translations/pt.json b/apps/sim/translations/pt.json new file mode 100644 index 00000000000..b201bafcddf --- /dev/null +++ b/apps/sim/translations/pt.json @@ -0,0 +1,25 @@ +{ + "localization": { + "pt": "PT - português", + "en": "EN - inglês", + "es": "ES - espanhol" + }, + "sign_up": { + "page_title": "Criar uma conta", + "page_sub_title": "Crie uma conta ou faça login", + "full_name_label": "Nome completo", + "full_name_placeholder": "Digite seu nome", + "email_label": "E-mail", + "email_placeholder": "Digite seu e-mail", + "password_label": "Senha", + "password_placeholder": "Digite sua senha", + "create_account": "Criar conta", + "already_have_account": "Já tem uma conta?", + "sign_in_redirect_label": "Entrar", + "create_account_warning": "Ao criar uma conta, você concorda com nossos Termos de Serviço e Política de Privacidade" + }, + "sign_in": { + "page_title": "Entrar", + "page_sub_title": "Insira seus dados" + } +} \ No newline at end of file diff --git a/bun.lock b/bun.lock index d0b2fce86df..7ab010e5085 100644 --- a/bun.lock +++ b/bun.lock @@ -157,6 +157,7 @@ "nanoid": "^3.3.7", "neo4j-driver": "6.0.1", "next": "16.1.6", + "next-intl": "4.8.3", "next-mdx-remote": "^5.0.0", "next-runtime-env": "3.3.0", "next-themes": "^0.4.6", @@ -693,6 +694,14 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + "@formatjs/ecma402-abstract": ["@formatjs/ecma402-abstract@3.1.1", "", { "dependencies": { "@formatjs/fast-memoize": "3.1.0", "@formatjs/intl-localematcher": "0.8.1", "decimal.js": "^10.6.0", "tslib": "^2.8.1" } }, "sha512-jhZbTwda+2tcNrs4kKvxrPLPjx8QsBCLCUgrrJ/S+G9YrGHWLhAyFMMBHJBnBoOwuLHd7L14FgYudviKaxkO2Q=="], + + "@formatjs/fast-memoize": ["@formatjs/fast-memoize@3.1.0", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg=="], + + "@formatjs/icu-messageformat-parser": ["@formatjs/icu-messageformat-parser@3.5.1", "", { "dependencies": { "@formatjs/ecma402-abstract": "3.1.1", "@formatjs/icu-skeleton-parser": "2.1.1", "tslib": "^2.8.1" } }, "sha512-sSDmSvmmoVQ92XqWb499KrIhv/vLisJU8ITFrx7T7NZHUmMY7EL9xgRowAosaljhqnj/5iufG24QrdzB6X3ItA=="], + + "@formatjs/icu-skeleton-parser": ["@formatjs/icu-skeleton-parser@2.1.1", "", { "dependencies": { "@formatjs/ecma402-abstract": "3.1.1", "tslib": "^2.8.1" } }, "sha512-PSFABlcNefjI6yyk8f7nyX1DC7NHmq6WaCHZLySEXBrXuLOB2f935YsnzuPjlz+ibhb9yWTdPeVX1OVcj24w2Q=="], + "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.2", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA=="], "@google-cloud/precise-date": ["@google-cloud/precise-date@4.0.0", "", {}, "sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA=="], @@ -941,6 +950,34 @@ "@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="], + "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], + + "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="], + + "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA=="], + + "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg=="], + + "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng=="], + + "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ=="], + + "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg=="], + + "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA=="], + + "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA=="], + + "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ=="], + + "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg=="], + + "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q=="], + + "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g=="], + + "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="], + "@pdf-lib/standard-fonts": ["@pdf-lib/standard-fonts@1.0.0", "", { "dependencies": { "pako": "^1.0.6" } }, "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA=="], "@pdf-lib/upng": ["@pdf-lib/upng@1.0.1", "", { "dependencies": { "pako": "^1.0.10" } }, "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ=="], @@ -1221,6 +1258,8 @@ "@s2-dev/streamstore": ["@s2-dev/streamstore@0.17.3", "", { "dependencies": { "@protobuf-ts/runtime": "^2.11.1" }, "peerDependencies": { "typescript": "^5.9.3" } }, "sha512-UeXL5+MgZQfNkbhCgEDVm7PrV5B3bxh6Zp4C5pUzQQwaoA+iGh2QiiIptRZynWgayzRv4vh0PYfnKpTzJEXegQ=="], + "@schummar/icu-type-parser": ["@schummar/icu-type-parser@1.21.5", "", {}, "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw=="], + "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], "@shikijs/core": ["@shikijs/core@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA=="], @@ -1365,8 +1404,34 @@ "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + "@swc/core": ["@swc/core@1.15.13", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.13", "@swc/core-darwin-x64": "1.15.13", "@swc/core-linux-arm-gnueabihf": "1.15.13", "@swc/core-linux-arm64-gnu": "1.15.13", "@swc/core-linux-arm64-musl": "1.15.13", "@swc/core-linux-x64-gnu": "1.15.13", "@swc/core-linux-x64-musl": "1.15.13", "@swc/core-win32-arm64-msvc": "1.15.13", "@swc/core-win32-ia32-msvc": "1.15.13", "@swc/core-win32-x64-msvc": "1.15.13" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-0l1gl/72PErwUZuavcRpRAQN9uSst+Nk++niC5IX6lmMWpXoScYx3oq/narT64/sKv/eRiPTaAjBFGDEQiWJIw=="], + + "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ztXusRuC5NV2w+a6pDhX13CGioMLq8CjX5P4XgVJ21ocqz9t19288Do0y8LklplDtwcEhYGTNdMbkmUT7+lDTg=="], + + "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-cVifxQUKhaE7qcO/y9Mq6PEhoyvN9tSLzCnnFZ4EIabFHBuLtDDO6a+vLveOy98hAs5Qu1+bb5Nv0oa1Pihe3Q=="], + + "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.13", "", { "os": "linux", "cpu": "arm" }, "sha512-t+xxEzZ48enl/wGGy7SRYd7kImWQ/+wvVFD7g5JZo234g6/QnIgZ+YdfIyjHB+ZJI3F7a2IQHS7RNjxF29UkWw=="], + + "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-VndeGvKmTXFn6AGwjy0Kg8i7HccOCE7Jt/vmZwRxGtOfNZM1RLYRQ7MfDLo6T0h1Bq6eYzps3L5Ma4zBmjOnOg=="], + + "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-SmZ9m+XqCB35NddHCctvHFLqPZDAs5j8IgD36GoutufDJmeq2VNfgk5rQoqNqKmAK3Y7iFdEmI76QoHIWiCLyw=="], + + "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.13", "", { "os": "linux", "cpu": "x64" }, "sha512-5rij+vB9a29aNkHq72EXI2ihDZPszJb4zlApJY4aCC/q6utgqFA6CkrfTfIb+O8hxtG3zP5KERETz8mfFK6A0A=="], + + "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.13", "", { "os": "linux", "cpu": "x64" }, "sha512-OlSlaOK9JplQ5qn07WiBLibkOw7iml2++ojEXhhR3rbWrNEKCD7sd8+6wSavsInyFdw4PhLA+Hy6YyDBIE23Yw=="], + + "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-zwQii5YVdsfG8Ti9gIKgBKZg8qMkRZxl+OlYWUT5D93Jl4NuNBRausP20tfEkQdAPSRrMCSUZBM6FhW7izAZRg=="], + + "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.13", "", { "os": "win32", "cpu": "ia32" }, "sha512-hYXvyVVntqRlYoAIDwNzkS3tL2ijP3rxyWQMNKaxcCxxkCDto/w3meOK/OB6rbQSkNw0qTUcBfU9k+T0ptYdfQ=="], + + "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.13", "", { "os": "win32", "cpu": "x64" }, "sha512-XTzKs7c/vYCcjmcwawnQvlHHNS1naJEAzcBckMI5OJlnrcgW8UtcX9NHFYvNjGtXuKv0/9KvqL4fuahdvlNGKw=="], + + "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + "@swc/types": ["@swc/types@0.1.25", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g=="], + "@t3-oss/env-core": ["@t3-oss/env-core@0.13.4", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0-beta.0" }, "optionalPeers": ["typescript", "valibot", "zod"] }, "sha512-zVOiYO0+CF7EnBScz8s0O5JnJLPTU0lrUi8qhKXfIxIJXvI/jcppSiXXsEJwfB4A6XZawY/Wg/EQGKANi/aPmQ=="], "@t3-oss/env-nextjs": ["@t3-oss/env-nextjs@0.13.4", "", { "dependencies": { "@t3-oss/env-core": "0.13.4" }, "peerDependencies": { "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0-beta.0" }, "optionalPeers": ["typescript", "valibot", "zod"] }, "sha512-6ecXR7SH7zJKVcBODIkB7wV9QLMU23uV8D9ec6P+ULHJ5Ea/YXEHo+Z/2hSYip5i9ptD/qZh8VuOXyldspvTTg=="], @@ -2349,6 +2414,8 @@ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "icu-minify": ["icu-minify@4.8.3", "", { "dependencies": { "@formatjs/icu-messageformat-parser": "^3.4.0" } }, "sha512-65Av7FLosNk7bPbmQx5z5XG2Y3T2GFppcjiXh4z1idHeVgQxlDpAmkGoYI0eFzAvrOnjpWTL5FmPDhsdfRMPEA=="], + "idb-keyval": ["idb-keyval@6.2.2", "", {}, "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -2373,6 +2440,8 @@ "inquirer": ["inquirer@8.2.7", "", { "dependencies": { "@inquirer/external-editor": "^1.0.0", "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^6.0.1" } }, "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA=="], + "intl-messageformat": ["intl-messageformat@11.1.2", "", { "dependencies": { "@formatjs/ecma402-abstract": "3.1.1", "@formatjs/fast-memoize": "3.1.0", "@formatjs/icu-messageformat-parser": "3.5.1", "tslib": "^2.8.1" } }, "sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg=="], + "ioredis": ["ioredis@5.9.2", "", { "dependencies": { "@ioredis/commands": "1.5.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ=="], "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], @@ -2793,6 +2862,10 @@ "next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="], + "next-intl": ["next-intl@4.8.3", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.8.1", "@parcel/watcher": "^2.4.1", "@swc/core": "^1.15.2", "icu-minify": "^4.8.3", "negotiator": "^1.0.0", "next-intl-swc-plugin-extractor": "^4.8.3", "po-parser": "^2.1.1", "use-intl": "^4.8.3" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0", "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-PvdBDWg+Leh7BR7GJUQbCDVVaBRn37GwDBWc9sv0rVQOJDQ5JU1rVzx9EEGuOGYo0DHAl70++9LQ7HxTawdL7w=="], + + "next-intl-swc-plugin-extractor": ["next-intl-swc-plugin-extractor@4.8.3", "", {}, "sha512-YcaT+R9z69XkGhpDarVFWUprrCMbxgIQYPUaXoE6LGVnLjGdo8hu3gL6bramDVjNKViYY8a/pXPy7Bna0mXORg=="], + "next-mdx-remote": ["next-mdx-remote@5.0.0", "", { "dependencies": { "@babel/code-frame": "^7.23.5", "@mdx-js/mdx": "^3.0.1", "@mdx-js/react": "^3.0.1", "unist-util-remove": "^3.1.0", "vfile": "^6.0.1", "vfile-matter": "^5.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-RNNbqRpK9/dcIFZs/esQhuLA8jANqlH694yqoDBK8hkVdJUndzzGmnPHa2nyi90N4Z9VmzuSWNRpr5ItT3M7xQ=="], "next-runtime-env": ["next-runtime-env@3.3.0", "", { "dependencies": { "next": "^14", "react": "^18" } }, "sha512-JgKVnog9mNbjbjH9csVpMnz2tB2cT5sLF+7O47i6Ze/s/GoiKdV7dHhJHk1gwXpo6h5qPj5PTzryldtSjvrHuQ=="], @@ -2801,6 +2874,8 @@ "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-ensure": ["node-ensure@0.0.0", "", {}, "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw=="], @@ -2971,6 +3046,8 @@ "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], + "po-parser": ["po-parser@2.1.1", "", {}, "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ=="], + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], @@ -3515,6 +3592,8 @@ "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + "use-intl": ["use-intl@4.8.3", "", { "dependencies": { "@formatjs/fast-memoize": "^3.1.0", "@schummar/icu-type-parser": "1.21.5", "icu-minify": "^4.8.3", "intl-messageformat": "^11.1.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" } }, "sha512-nLxlC/RH+le6g3amA508Itnn/00mE+J22ui21QhOWo5V9hCEC43+WtnRAITbJW0ztVZphev5X9gvOf2/Dk9PLA=="], + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], @@ -3753,6 +3832,8 @@ "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "@formatjs/ecma402-abstract/@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.8.1", "", { "dependencies": { "@formatjs/fast-memoize": "3.1.0", "tslib": "^2.8.1" } }, "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA=="], + "@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="], "@langchain/core/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -4093,6 +4174,8 @@ "next/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + "next-intl/@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.8.1", "", { "dependencies": { "@formatjs/fast-memoize": "3.1.0", "tslib": "^2.8.1" } }, "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA=="], + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "nypm/pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], From 01e7a5b7335f98da432312ff7d53cc7af5042770 Mon Sep 17 00:00:00 2001 From: kallebe Date: Mon, 2 Mar 2026 10:44:11 -0300 Subject: [PATCH 02/17] feat: sign_up translations --- .../components/social-login-buttons.tsx | 33 ++++--- .../(auth)/components/sso-login-button.tsx | 4 +- .../support-footer.translations.json | 4 + .../app/(auth)/components/support-footer.tsx | 6 +- apps/sim/app/(auth)/signup/page.tsx | 5 +- apps/sim/app/(auth)/signup/signup-form.tsx | 94 +++++-------------- .../sim/hooks/sign-up/use-name-validations.ts | 24 +++++ .../hooks/sign-up/use-password-validations.ts | 22 +++++ apps/sim/hooks/sign-up/use-validate-email.ts | 24 +++++ apps/sim/translations/en.json | 53 ++++++++++- apps/sim/translations/es.json | 84 +++++++++++++---- apps/sim/translations/pt.json | 84 +++++++++++++---- 12 files changed, 316 insertions(+), 121 deletions(-) create mode 100644 apps/sim/app/(auth)/components/support-footer.translations.json create mode 100644 apps/sim/hooks/sign-up/use-name-validations.ts create mode 100644 apps/sim/hooks/sign-up/use-password-validations.ts create mode 100644 apps/sim/hooks/sign-up/use-validate-email.ts diff --git a/apps/sim/app/(auth)/components/social-login-buttons.tsx b/apps/sim/app/(auth)/components/social-login-buttons.tsx index 541c242a12d..10dfa2476f9 100644 --- a/apps/sim/app/(auth)/components/social-login-buttons.tsx +++ b/apps/sim/app/(auth)/components/social-login-buttons.tsx @@ -5,6 +5,7 @@ import { GithubIcon, GoogleIcon } from '@/components/icons' import { Button } from '@/components/ui/button' import { client } from '@/lib/auth/auth-client' import { inter } from '@/app/_styles/fonts/inter/inter' +import { useTranslations } from 'next-intl' interface SocialLoginButtonsProps { githubAvailable: boolean @@ -21,16 +22,16 @@ export function SocialLoginButtons({ isProduction, children, }: SocialLoginButtonsProps) { + const t = useTranslations('social_login') + const [isGithubLoading, setIsGithubLoading] = useState(false) const [isGoogleLoading, setIsGoogleLoading] = useState(false) const [mounted, setMounted] = useState(false) - // Set mounted state to true on client-side useEffect(() => { setMounted(true) }, []) - // Only render on the client side to avoid hydration errors if (!mounted) return null async function signInWithGithub() { @@ -40,17 +41,19 @@ export function SocialLoginButtons({ try { await client.signIn.social({ provider: 'github', callbackURL }) } catch (err: any) { - let errorMessage = 'Failed to sign in with GitHub' + let errorMessage = t('errors.github.default') if (err.message?.includes('account exists')) { - errorMessage = 'An account with this email already exists. Please sign in instead.' + errorMessage = t('errors.account_exists') } else if (err.message?.includes('cancelled')) { - errorMessage = 'GitHub sign in was cancelled. Please try again.' + errorMessage = t('errors.github.cancelled') } else if (err.message?.includes('network')) { - errorMessage = 'Network error. Please check your connection and try again.' + errorMessage = t('errors.network') } else if (err.message?.includes('rate limit')) { - errorMessage = 'Too many attempts. Please try again later.' + errorMessage = t('errors.rate_limit') } + + console.error(errorMessage) } finally { setIsGithubLoading(false) } @@ -63,17 +66,19 @@ export function SocialLoginButtons({ try { await client.signIn.social({ provider: 'google', callbackURL }) } catch (err: any) { - let errorMessage = 'Failed to sign in with Google' + let errorMessage = t('errors.google.default') if (err.message?.includes('account exists')) { - errorMessage = 'An account with this email already exists. Please sign in instead.' + errorMessage = t('errors.account_exists') } else if (err.message?.includes('cancelled')) { - errorMessage = 'Google sign in was cancelled. Please try again.' + errorMessage = t('errors.google.cancelled') } else if (err.message?.includes('network')) { - errorMessage = 'Network error. Please check your connection and try again.' + errorMessage = t('errors.network') } else if (err.message?.includes('rate limit')) { - errorMessage = 'Too many attempts. Please try again later.' + errorMessage = t('errors.rate_limit') } + + console.error(errorMessage) } finally { setIsGoogleLoading(false) } @@ -87,7 +92,7 @@ export function SocialLoginButtons({ onClick={signInWithGithub} > - {isGithubLoading ? 'Connecting...' : 'GitHub'} + {isGithubLoading ? t('buttons.connecting') : t('buttons.github')} ) @@ -99,7 +104,7 @@ export function SocialLoginButtons({ onClick={signInWithGoogle} > - {isGoogleLoading ? 'Connecting...' : 'Google'} + {isGoogleLoading ? t('buttons.connecting') : t('buttons.google')} ) diff --git a/apps/sim/app/(auth)/components/sso-login-button.tsx b/apps/sim/app/(auth)/components/sso-login-button.tsx index df758576c28..76dc963384e 100644 --- a/apps/sim/app/(auth)/components/sso-login-button.tsx +++ b/apps/sim/app/(auth)/components/sso-login-button.tsx @@ -4,6 +4,7 @@ import { useRouter } from 'next/navigation' import { Button } from '@/components/ui/button' import { getEnv, isTruthy } from '@/lib/core/config/env' import { cn } from '@/lib/core/utils/cn' +import { useTranslations } from 'next-intl' interface SSOLoginButtonProps { callbackURL?: string @@ -23,6 +24,7 @@ export function SSOLoginButton({ primaryClassName, }: SSOLoginButtonProps) { const router = useRouter() + const t = useTranslations() if (!isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))) { return null @@ -47,7 +49,7 @@ export function SSOLoginButton({ variant={variant === 'outline' ? 'outline' : undefined} className={cn(variant === 'outline' ? outlineBtnClasses : primaryBtnClasses, className)} > - Sign in with SSO + {t('sign_in_with_sso')} ) } diff --git a/apps/sim/app/(auth)/components/support-footer.translations.json b/apps/sim/app/(auth)/components/support-footer.translations.json new file mode 100644 index 00000000000..fc8dc977dcc --- /dev/null +++ b/apps/sim/app/(auth)/components/support-footer.translations.json @@ -0,0 +1,4 @@ +{ + "helper_text.need_help": "Need help?", + "links.contact_support": "Contact support" +} \ No newline at end of file diff --git a/apps/sim/app/(auth)/components/support-footer.tsx b/apps/sim/app/(auth)/components/support-footer.tsx index 46614070be8..d0f6502cb49 100644 --- a/apps/sim/app/(auth)/components/support-footer.tsx +++ b/apps/sim/app/(auth)/components/support-footer.tsx @@ -1,5 +1,6 @@ 'use client' +import { useTranslations } from 'next-intl' import { inter } from '@/app/_styles/fonts/inter/inter' import { useBrandConfig } from '@/ee/whitelabeling' @@ -23,17 +24,18 @@ export interface SupportFooterProps { */ export function SupportFooter({ position = 'fixed' }: SupportFooterProps) { const brandConfig = useBrandConfig() + const t = useTranslations() return (
- Need help?{' '} + {t('helper_text.need_help')}{' '} - Contact support + {t('links.contact_support')}
) diff --git a/apps/sim/app/(auth)/signup/page.tsx b/apps/sim/app/(auth)/signup/page.tsx index b267716e35f..4309196b606 100644 --- a/apps/sim/app/(auth)/signup/page.tsx +++ b/apps/sim/app/(auth)/signup/page.tsx @@ -1,12 +1,15 @@ import { isRegistrationDisabled } from '@/lib/core/config/feature-flags' import { getOAuthProviderStatus } from '@/app/(auth)/components/oauth-provider-checker' import SignupForm from '@/app/(auth)/signup/signup-form' +import { getTranslations } from 'next-intl/server' export const dynamic = 'force-dynamic' export default async function SignupPage() { + const t = await getTranslations() + if (isRegistrationDisabled) { - return
Registration is disabled, please contact your admin.
+ return
{t('sign_up.disabled_registration_warning')}
} const { githubAvailable, googleAvailable, isProduction } = await getOAuthProviderStatus() diff --git a/apps/sim/app/(auth)/signup/signup-form.tsx b/apps/sim/app/(auth)/signup/signup-form.tsx index 2bfccf3bfc2..91fb7ccdc64 100644 --- a/apps/sim/app/(auth)/signup/signup-form.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.tsx @@ -10,7 +10,6 @@ import { Label } from '@/components/ui/label' import { client, useSession } from '@/lib/auth/auth-client' import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env' import { cn } from '@/lib/core/utils/cn' -import { quickValidateEmail } from '@/lib/messaging/email/validation' import { inter } from '@/app/_styles/fonts/inter/inter' import { soehne } from '@/app/_styles/fonts/soehne/soehne' import { BrandedButton } from '@/app/(auth)/components/branded-button' @@ -19,61 +18,12 @@ import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button' import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' import LocaleSelector from '@/components/locale-selector' import { useTranslations } from 'next-intl' +import { usePasswordValidations } from '@/hooks/sign-up/use-password-validations' +import { useNameValidations } from '@/hooks/sign-up/use-name-validations' +import { useValidateEmail } from '@/hooks/sign-up/use-validate-email' const logger = createLogger('SignupForm') -const PASSWORD_VALIDATIONS = { - minLength: { regex: /.{8,}/, message: 'Password must be at least 8 characters long.' }, - uppercase: { - regex: /(?=.*?[A-Z])/, - message: 'Password must include at least one uppercase letter.', - }, - lowercase: { - regex: /(?=.*?[a-z])/, - message: 'Password must include at least one lowercase letter.', - }, - number: { regex: /(?=.*?[0-9])/, message: 'Password must include at least one number.' }, - special: { - regex: /(?=.*?[#?!@$%^&*-])/, - message: 'Password must include at least one special character.', - }, -} - -const NAME_VALIDATIONS = { - required: { - test: (value: string) => Boolean(value && typeof value === 'string'), - message: 'Name is required.', - }, - notEmpty: { - test: (value: string) => value.trim().length > 0, - message: 'Name cannot be empty.', - }, - validCharacters: { - regex: /^[\p{L}\s\-']+$/u, - message: 'Name can only contain letters, spaces, hyphens, and apostrophes.', - }, - noConsecutiveSpaces: { - regex: /^(?!.*\s\s).*$/, - message: 'Name cannot contain consecutive spaces.', - }, -} - -const validateEmailField = (emailValue: string): string[] => { - const errors: string[] = [] - - if (!emailValue || !emailValue.trim()) { - errors.push('Email is required.') - return errors - } - - const validation = quickValidateEmail(emailValue.trim().toLowerCase()) - if (!validation.isValid) { - errors.push(validation.reason || 'Please enter a valid email address.') - } - - return errors -} - function SignupFormContent({ githubAvailable, googleAvailable, @@ -101,6 +51,10 @@ function SignupFormContent({ const [isInviteFlow, setIsInviteFlow] = useState(false) const buttonClass = useBrandedButtonClass() + const PASSWORD_VALIDATIONS = usePasswordValidations() + const NAME_VALIDATIONS = useNameValidations() + const validateEmailField = useValidateEmail() + const [name, setName] = useState('') const [nameErrors, setNameErrors] = useState([]) const [showNameValidationError, setShowNameValidationError] = useState(false) @@ -260,7 +214,7 @@ function SignupFormContent({ } if (trimmedName.length > 100) { - setNameErrors(['Name will be truncated to 100 characters. Please shorten your name.']) + setNameErrors([t('sign_up.validations.truncated_name_error')]) setShowNameValidationError(true) setIsLoading(false) return @@ -277,36 +231,34 @@ function SignupFormContent({ { onError: (ctx) => { logger.error('Signup error:', ctx.error) - const errorMessage: string[] = ['Failed to create account'] + const errorMessage: string[] = [t('sign_up.validations.failed_to_create_account')] if (ctx.error.code?.includes('USER_ALREADY_EXISTS')) { - errorMessage.push( - 'An account with this email already exists. Please sign in instead.' - ) + errorMessage.push(t('sign_up.validations.account_with_same_email_already_exists')) setEmailError(errorMessage[0]) } else if ( ctx.error.code?.includes('BAD_REQUEST') || ctx.error.message?.includes('Email and password sign up is not enabled') ) { - errorMessage.push('Email signup is currently disabled.') + errorMessage.push(t('sign_up.validations.email_sign_up_is_not_enabled')) setEmailError(errorMessage[0]) } else if (ctx.error.code?.includes('INVALID_EMAIL')) { - errorMessage.push('Please enter a valid email address.') + errorMessage.push(t('sign_up.validations.email_invalid')) setEmailError(errorMessage[0]) } else if (ctx.error.code?.includes('PASSWORD_TOO_SHORT')) { - errorMessage.push('Password must be at least 8 characters long.') + errorMessage.push(t('sign_up.validations.password_min_8_character')) setPasswordErrors(errorMessage) setShowValidationError(true) } else if (ctx.error.code?.includes('PASSWORD_TOO_LONG')) { - errorMessage.push('Password must be less than 128 characters long.') + errorMessage.push(t('sign_up.validations.password_max_128_characters')) setPasswordErrors(errorMessage) setShowValidationError(true) } else if (ctx.error.code?.includes('network')) { - errorMessage.push('Network error. Please check your connection and try again.') + errorMessage.push(t('network_error')) setPasswordErrors(errorMessage) setShowValidationError(true) } else if (ctx.error.code?.includes('rate limit')) { - errorMessage.push('Too many requests. Please wait a moment before trying again.') + errorMessage.push(t('too_many_requests')) setPasswordErrors(errorMessage) setShowValidationError(true) } else { @@ -387,7 +339,7 @@ function SignupFormContent({ type='text' autoCapitalize='words' autoComplete='name' - title='Name can only contain letters, spaces, hyphens, and apostrophes' + title={t('sign_up.name_title')} value={name} onChange={handleNameChange} className={cn( @@ -463,7 +415,9 @@ function SignupFormContent({ type='button' onClick={() => setShowPassword(!showPassword)} className='-translate-y-1/2 absolute top-1/2 right-3 text-gray-500 transition hover:text-gray-700' - aria-label={showPassword ? 'Hide password' : 'Show password'} + aria-label={ + showPassword ? t('sign_up.hide_password') : t('sign_up.show_password') + } > {showPassword ? : } @@ -482,7 +436,7 @@ function SignupFormContent({ type='submit' disabled={isLoading} loading={isLoading} - loadingText='Creating account' + loadingText={t('sign_up.creating_account')} > {t('sign_up.create_account')} @@ -592,9 +546,13 @@ export default function SignupPage({ googleAvailable: boolean isProduction: boolean }) { + const t = useTranslations() + return ( Loading...
} + fallback={ +
{t('generic.loading')}...
+ } > Boolean(value && typeof value === 'string'), + message: t('sign_up.validations.name_required'), + }, + notEmpty: { + test: (value: string) => value.trim().length > 0, + message: t('sign_up.validations.name_cannot_be_empty'), + }, + validCharacters: { + regex: /^[\p{L}\s\-']+$/u, + message: t('sign_up.validations.name_invalid_format'), + }, + noConsecutiveSpaces: { + regex: /^(?!.*\s\s).*$/, + message: t('sign_up.validations.name_cannot_contain_spaces'), + }, + } +} diff --git a/apps/sim/hooks/sign-up/use-password-validations.ts b/apps/sim/hooks/sign-up/use-password-validations.ts new file mode 100644 index 00000000000..0406c348307 --- /dev/null +++ b/apps/sim/hooks/sign-up/use-password-validations.ts @@ -0,0 +1,22 @@ +import { useTranslations } from 'next-intl' + +export function usePasswordValidations() { + const t = useTranslations() + + return { + minLength: { regex: /.{8,}/, message: t('sign_up.validations.password_min_8_character') }, + uppercase: { + regex: /(?=.*?[A-Z])/, + message: t('sign_up.validations.password_min_1_uppercase_letter'), + }, + lowercase: { + regex: /(?=.*?[a-z])/, + message: t('sign_up.validations.password_min_1_lowercase_letter'), + }, + number: { regex: /(?=.*?[0-9])/, message: t('sign_up.validations.password_min_1_number') }, + special: { + regex: /(?=.*?[#?!@$%^&*-])/, + message: t('sign_up.validations.password_min_1_special_character'), + }, + } +} diff --git a/apps/sim/hooks/sign-up/use-validate-email.ts b/apps/sim/hooks/sign-up/use-validate-email.ts new file mode 100644 index 00000000000..77cd89d8f65 --- /dev/null +++ b/apps/sim/hooks/sign-up/use-validate-email.ts @@ -0,0 +1,24 @@ +import { quickValidateEmail } from '@/lib/messaging/email/validation' +import { useTranslations } from 'next-intl' + +export function useValidateEmail() { + const t = useTranslations() + + const validateEmailField = (emailValue: string): string[] => { + const errors: string[] = [] + + if (!emailValue || !emailValue.trim()) { + errors.push(t('sign_up.validations.email_required')) + return errors + } + + const validation = quickValidateEmail(emailValue.trim().toLowerCase()) + if (!validation.isValid) { + errors.push(validation.reason || t('sign_up.validations.email_invalid')) + } + + return errors + } + + return validateEmailField +} diff --git a/apps/sim/translations/en.json b/apps/sim/translations/en.json index abfa36ed770..85211f869b3 100644 --- a/apps/sim/translations/en.json +++ b/apps/sim/translations/en.json @@ -4,7 +4,36 @@ "en": "EN - english", "es": "ES - Spanish" }, + "helper_text.need_help": "Need help?", + "links.contact_support": "Contact support", + "network_error": "Network error. Please check your connection and try again.", + "too_many_requests": "Too many requests. Please wait a moment before trying again.", + "sign_in_with_sso": "Sign in with SSO", + "social_login": { + "buttons": { + "github": "GitHub", + "google": "Google", + "connecting": "Connecting..." + }, + "errors": { + "account_exists": "An account with this email already exists. Please sign in.", + "network": "Network error. Please check your connection and try again.", + "rate_limit": "Too many attempts. Please try again later.", + "github": { + "default": "Failed to sign in with GitHub", + "cancelled": "GitHub sign in was cancelled. Please try again." + }, + "google": { + "default": "Failed to sign in with Google", + "cancelled": "Google sign in was cancelled. Please try again." + } + } + }, "sign_up": { + "hide_password": "Hide password", + "show_password": "Show password", + "creating_account": "Creating account", + "name_title": "Name can only contain letters, spaces, hyphens, and apostrophes", "page_title": "Create an account", "page_sub_title": "Create an account or log in", "full_name_label": "Full name", @@ -17,10 +46,32 @@ "already_have_account": "Already have an account?", "sign_in_redirect_label": "Sign In", "create_account_warning": "By creating an account, you agree to our Terms of Service and Privacy Policy", - "divider_label": "Or continue with" + "divider_label": "Or continue with", + "disabled_registration_warning": "Registration is disabled, please contact your admin.", + "validations": { + "password_max_128_characters": "Password must be less than 128 characters long.", + "password_min_8_character": "Password must be at least 8 characters long.", + "password_min_1_uppercase_letter": "Password must include at least one uppercase letter.", + "password_min_1_lowercase_letter": "Password must include at least one lowercase letter.", + "password_min_1_number": "Password must include at least one number.", + "password_min_1_special_character": "Password must include at least one special character.", + "name_required": "Name is required.", + "name_cannot_be_empty": "Name cannot be empty.", + "name_invalid_format": "Name can only contain letters, spaces, hyphens, and apostrophes.", + "name_cannot_contain_spaces": "Name cannot contain consecutive spaces.", + "email_required": "Email is required.", + "email_invalid": "Please enter a valid email address.", + "truncated_name_error": "Name will be truncated to 100 characters. Please shorten your name.", + "failed_to_create_account": "Failed to create account", + "account_with_same_email_already_exists": "An account with this email already exists. Please sign in instead.", + "email_sign_up_is_not_enabled":"Email signup is currently disabled." + } }, "sign_in": { "page_title": "Sign in", "page_sub_title": "Enter your details" + }, + "generic": { + "loading": "Loading" } } \ No newline at end of file diff --git a/apps/sim/translations/es.json b/apps/sim/translations/es.json index b62fcf63a68..12995b103d1 100644 --- a/apps/sim/translations/es.json +++ b/apps/sim/translations/es.json @@ -1,25 +1,75 @@ { "localization": { - "pt": "PT - portugués", - "en": "EN - inglés", - "es": "ES - español" + "pt": "PT - portuguese", + "en": "EN - english", + "es": "ES - Spanish" + }, + "network_error": "Network error. Please check your connection and try again.", + "too_many_requests": "Too many requests. Please wait a moment before trying again.", + "sign_in_with_sso": "Sign in with SSO", + "social_login": { + "buttons": { + "github": "GitHub", + "google": "Google", + "connecting": "Connecting..." + }, + "errors": { + "account_exists": "An account with this email already exists. Please sign in.", + "network": "Network error. Please check your connection and try again.", + "rate_limit": "Too many attempts. Please try again later.", + "github": { + "default": "Failed to sign in with GitHub", + "cancelled": "GitHub sign in was cancelled. Please try again." + }, + "google": { + "default": "Failed to sign in with Google", + "cancelled": "Google sign in was cancelled. Please try again." + } + } }, "sign_up": { - "page_title": "Crear una cuenta", - "page_sub_title": "Crea una cuenta o inicia sesión", - "full_name_label": "Nombre completo", - "full_name_placeholder": "Ingresa tu nombre", - "email_label": "Correo electrónico", - "email_placeholder": "Ingresa tu correo electrónico", - "password_label": "Contraseña", - "password_placeholder": "Ingresa tu contraseña", - "create_account": "Crear cuenta", - "already_have_account": "¿Ya tienes una cuenta?", - "sign_in_redirect_label": "Iniciar sesión", - "create_account_warning": "Al crear una cuenta, aceptas nuestros Términos de Servicio y la Política de Privacidad" + "hide_password": "Hide password", + "show_password": "Show password", + "creating_account": "Creating account", + "name_title": "Name can only contain letters, spaces, hyphens, and apostrophes", + "page_title": "Create an account", + "page_sub_title": "Create an account or log in", + "full_name_label": "Full name", + "full_name_placeholder": "Enter your name", + "email_label": "Email", + "email_placeholder": "Enter your email", + "password_label": "Password", + "password_placeholder": "Enter your password", + "create_account": "Create account", + "already_have_account": "Already have an account?", + "sign_in_redirect_label": "Sign In", + "create_account_warning": "By creating an account, you agree to our Terms of Service and Privacy Policy", + "divider_label": "Or continue with", + "disabled_registration_warning": "Registration is disabled, please contact your admin.", + "validations": { + "password_max_128_characters": "Password must be less than 128 characters long.", + "password_min_8_character": "Password must be at least 8 characters long.", + "password_min_1_uppercase_letter": "Password must include at least one uppercase letter.", + "password_min_1_lowercase_letter": "Password must include at least one lowercase letter.", + "password_min_1_number": "Password must include at least one number.", + "password_min_1_special_character": "Password must include at least one special character.", + "name_required": "Name is required.", + "name_cannot_be_empty": "Name cannot be empty.", + "name_invalid_format": "Name can only contain letters, spaces, hyphens, and apostrophes.", + "name_cannot_contain_spaces": "Name cannot contain consecutive spaces.", + "email_required": "Email is required.", + "email_invalid": "Please enter a valid email address.", + "truncated_name_error": "Name will be truncated to 100 characters. Please shorten your name.", + "failed_to_create_account": "Failed to create account", + "account_with_same_email_already_exists": "An account with this email already exists. Please sign in instead.", + "email_sign_up_is_not_enabled":"Email signup is currently disabled." + } }, "sign_in": { - "page_title": "Iniciar sesión", - "page_sub_title": "Ingresa tus datos" + "page_title": "Sign in", + "page_sub_title": "Enter your details" + }, + "generic": { + "loading": "Loading" } } \ No newline at end of file diff --git a/apps/sim/translations/pt.json b/apps/sim/translations/pt.json index b201bafcddf..12995b103d1 100644 --- a/apps/sim/translations/pt.json +++ b/apps/sim/translations/pt.json @@ -1,25 +1,75 @@ { "localization": { - "pt": "PT - português", - "en": "EN - inglês", - "es": "ES - espanhol" + "pt": "PT - portuguese", + "en": "EN - english", + "es": "ES - Spanish" + }, + "network_error": "Network error. Please check your connection and try again.", + "too_many_requests": "Too many requests. Please wait a moment before trying again.", + "sign_in_with_sso": "Sign in with SSO", + "social_login": { + "buttons": { + "github": "GitHub", + "google": "Google", + "connecting": "Connecting..." + }, + "errors": { + "account_exists": "An account with this email already exists. Please sign in.", + "network": "Network error. Please check your connection and try again.", + "rate_limit": "Too many attempts. Please try again later.", + "github": { + "default": "Failed to sign in with GitHub", + "cancelled": "GitHub sign in was cancelled. Please try again." + }, + "google": { + "default": "Failed to sign in with Google", + "cancelled": "Google sign in was cancelled. Please try again." + } + } }, "sign_up": { - "page_title": "Criar uma conta", - "page_sub_title": "Crie uma conta ou faça login", - "full_name_label": "Nome completo", - "full_name_placeholder": "Digite seu nome", - "email_label": "E-mail", - "email_placeholder": "Digite seu e-mail", - "password_label": "Senha", - "password_placeholder": "Digite sua senha", - "create_account": "Criar conta", - "already_have_account": "Já tem uma conta?", - "sign_in_redirect_label": "Entrar", - "create_account_warning": "Ao criar uma conta, você concorda com nossos Termos de Serviço e Política de Privacidade" + "hide_password": "Hide password", + "show_password": "Show password", + "creating_account": "Creating account", + "name_title": "Name can only contain letters, spaces, hyphens, and apostrophes", + "page_title": "Create an account", + "page_sub_title": "Create an account or log in", + "full_name_label": "Full name", + "full_name_placeholder": "Enter your name", + "email_label": "Email", + "email_placeholder": "Enter your email", + "password_label": "Password", + "password_placeholder": "Enter your password", + "create_account": "Create account", + "already_have_account": "Already have an account?", + "sign_in_redirect_label": "Sign In", + "create_account_warning": "By creating an account, you agree to our Terms of Service and Privacy Policy", + "divider_label": "Or continue with", + "disabled_registration_warning": "Registration is disabled, please contact your admin.", + "validations": { + "password_max_128_characters": "Password must be less than 128 characters long.", + "password_min_8_character": "Password must be at least 8 characters long.", + "password_min_1_uppercase_letter": "Password must include at least one uppercase letter.", + "password_min_1_lowercase_letter": "Password must include at least one lowercase letter.", + "password_min_1_number": "Password must include at least one number.", + "password_min_1_special_character": "Password must include at least one special character.", + "name_required": "Name is required.", + "name_cannot_be_empty": "Name cannot be empty.", + "name_invalid_format": "Name can only contain letters, spaces, hyphens, and apostrophes.", + "name_cannot_contain_spaces": "Name cannot contain consecutive spaces.", + "email_required": "Email is required.", + "email_invalid": "Please enter a valid email address.", + "truncated_name_error": "Name will be truncated to 100 characters. Please shorten your name.", + "failed_to_create_account": "Failed to create account", + "account_with_same_email_already_exists": "An account with this email already exists. Please sign in instead.", + "email_sign_up_is_not_enabled":"Email signup is currently disabled." + } }, "sign_in": { - "page_title": "Entrar", - "page_sub_title": "Insira seus dados" + "page_title": "Sign in", + "page_sub_title": "Enter your details" + }, + "generic": { + "loading": "Loading" } } \ No newline at end of file From 80d402304e9c0269ef22e221063442905e237141 Mon Sep 17 00:00:00 2001 From: kallebe Date: Mon, 2 Mar 2026 10:47:00 -0300 Subject: [PATCH 03/17] fix: syncing translations --- apps/sim/translations/es.json | 104 +++++++++++++++++----------------- apps/sim/translations/pt.json | 102 +++++++++++++++++---------------- 2 files changed, 105 insertions(+), 101 deletions(-) diff --git a/apps/sim/translations/es.json b/apps/sim/translations/es.json index 12995b103d1..ce4dce0b6ea 100644 --- a/apps/sim/translations/es.json +++ b/apps/sim/translations/es.json @@ -1,75 +1,77 @@ { "localization": { - "pt": "PT - portuguese", - "en": "EN - english", - "es": "ES - Spanish" + "pt": "PT - portugués", + "en": "EN - inglés", + "es": "ES - español" }, - "network_error": "Network error. Please check your connection and try again.", - "too_many_requests": "Too many requests. Please wait a moment before trying again.", - "sign_in_with_sso": "Sign in with SSO", + "network_error": "Error de red. Verifique su conexión e intente nuevamente.", + "too_many_requests": "Demasiadas solicitudes. Espere un momento antes de intentarlo nuevamente.", + "sign_in_with_sso": "Iniciar sesión con SSO", "social_login": { "buttons": { "github": "GitHub", "google": "Google", - "connecting": "Connecting..." + "connecting": "Conectando..." }, "errors": { - "account_exists": "An account with this email already exists. Please sign in.", - "network": "Network error. Please check your connection and try again.", - "rate_limit": "Too many attempts. Please try again later.", + "account_exists": "Ya existe una cuenta con este correo electrónico. Por favor, inicie sesión.", + "network": "Error de red. Verifique su conexión e intente nuevamente.", + "rate_limit": "Demasiados intentos. Por favor, intente nuevamente más tarde.", "github": { - "default": "Failed to sign in with GitHub", - "cancelled": "GitHub sign in was cancelled. Please try again." + "default": "Error al iniciar sesión con GitHub", + "cancelled": "El inicio de sesión con GitHub fue cancelado. Por favor, intente nuevamente." }, "google": { - "default": "Failed to sign in with Google", - "cancelled": "Google sign in was cancelled. Please try again." + "default": "Error al iniciar sesión con Google", + "cancelled": "El inicio de sesión con Google fue cancelado. Por favor, intente nuevamente." } } }, "sign_up": { - "hide_password": "Hide password", - "show_password": "Show password", - "creating_account": "Creating account", - "name_title": "Name can only contain letters, spaces, hyphens, and apostrophes", - "page_title": "Create an account", - "page_sub_title": "Create an account or log in", - "full_name_label": "Full name", - "full_name_placeholder": "Enter your name", - "email_label": "Email", - "email_placeholder": "Enter your email", - "password_label": "Password", - "password_placeholder": "Enter your password", - "create_account": "Create account", - "already_have_account": "Already have an account?", - "sign_in_redirect_label": "Sign In", - "create_account_warning": "By creating an account, you agree to our Terms of Service and Privacy Policy", - "divider_label": "Or continue with", - "disabled_registration_warning": "Registration is disabled, please contact your admin.", + "hide_password": "Ocultar contraseña", + "show_password": "Mostrar contraseña", + "creating_account": "Creando cuenta", + "name_title": "El nombre solo puede contener letras, espacios, guiones y apóstrofos", + "page_title": "Crear una cuenta", + "page_sub_title": "Cree una cuenta o inicie sesión", + "full_name_label": "Nombre completo", + "full_name_placeholder": "Ingrese su nombre", + "email_label": "Correo electrónico", + "email_placeholder": "Ingrese su correo electrónico", + "password_label": "Contraseña", + "password_placeholder": "Ingrese su contraseña", + "create_account": "Crear cuenta", + "already_have_account": "¿Ya tiene una cuenta?", + "sign_in_redirect_label": "Iniciar sesión", + "create_account_warning": "Al crear una cuenta, acepta nuestros Términos de servicio y Política de privacidad", + "divider_label": "O continúe con", + "disabled_registration_warning": "El registro está deshabilitado, por favor contacte a su administrador.", "validations": { - "password_max_128_characters": "Password must be less than 128 characters long.", - "password_min_8_character": "Password must be at least 8 characters long.", - "password_min_1_uppercase_letter": "Password must include at least one uppercase letter.", - "password_min_1_lowercase_letter": "Password must include at least one lowercase letter.", - "password_min_1_number": "Password must include at least one number.", - "password_min_1_special_character": "Password must include at least one special character.", - "name_required": "Name is required.", - "name_cannot_be_empty": "Name cannot be empty.", - "name_invalid_format": "Name can only contain letters, spaces, hyphens, and apostrophes.", - "name_cannot_contain_spaces": "Name cannot contain consecutive spaces.", - "email_required": "Email is required.", - "email_invalid": "Please enter a valid email address.", - "truncated_name_error": "Name will be truncated to 100 characters. Please shorten your name.", - "failed_to_create_account": "Failed to create account", - "account_with_same_email_already_exists": "An account with this email already exists. Please sign in instead.", - "email_sign_up_is_not_enabled":"Email signup is currently disabled." + "password_max_128_characters": "La contraseña debe tener menos de 128 caracteres.", + "password_min_8_character": "La contraseña debe tener al menos 8 caracteres.", + "password_min_1_uppercase_letter": "La contraseña debe incluir al menos una letra mayúscula.", + "password_min_1_lowercase_letter": "La contraseña debe incluir al menos una letra minúscula.", + "password_min_1_number": "La contraseña debe incluir al menos un número.", + "password_min_1_special_character": "La contraseña debe incluir al menos un carácter especial.", + "name_required": "El nombre es obligatorio.", + "name_cannot_be_empty": "El nombre no puede estar vacío.", + "name_invalid_format": "El nombre solo puede contener letras, espacios, guiones y apóstrofos.", + "name_cannot_contain_spaces": "El nombre no puede contener espacios consecutivos.", + "email_required": "El correo electrónico es obligatorio.", + "email_invalid": "Por favor, ingrese una dirección de correo electrónico válida.", + "truncated_name_error": "El nombre se truncará a 100 caracteres. Por favor, acorte su nombre.", + "failed_to_create_account": "Error al crear la cuenta", + "account_with_same_email_already_exists": "Ya existe una cuenta con este correo electrónico. Por favor, inicie sesión.", + "email_sign_up_is_not_enabled": "El registro por correo electrónico está deshabilitado actualmente." } }, "sign_in": { - "page_title": "Sign in", - "page_sub_title": "Enter your details" + "page_title": "Iniciar sesión", + "page_sub_title": "Ingrese sus datos" }, + "helper_text.need_help": "¿Necesitas ayuda?", + "links.contact_support": "Contactar soporte", "generic": { - "loading": "Loading" + "loading": "Cargando" } } \ No newline at end of file diff --git a/apps/sim/translations/pt.json b/apps/sim/translations/pt.json index 12995b103d1..e1357583bf8 100644 --- a/apps/sim/translations/pt.json +++ b/apps/sim/translations/pt.json @@ -1,75 +1,77 @@ { "localization": { - "pt": "PT - portuguese", - "en": "EN - english", - "es": "ES - Spanish" + "pt": "PT - português", + "en": "EN - inglês", + "es": "ES - espanhol" }, - "network_error": "Network error. Please check your connection and try again.", - "too_many_requests": "Too many requests. Please wait a moment before trying again.", - "sign_in_with_sso": "Sign in with SSO", + "network_error": "Erro de rede. Verifique sua conexão e tente novamente.", + "too_many_requests": "Muitas solicitações. Aguarde um momento antes de tentar novamente.", + "sign_in_with_sso": "Entrar com SSO", "social_login": { "buttons": { "github": "GitHub", "google": "Google", - "connecting": "Connecting..." + "connecting": "Conectando..." }, "errors": { - "account_exists": "An account with this email already exists. Please sign in.", - "network": "Network error. Please check your connection and try again.", - "rate_limit": "Too many attempts. Please try again later.", + "account_exists": "Uma conta com este email já existe. Por favor, faça login.", + "network": "Erro de rede. Verifique sua conexão e tente novamente.", + "rate_limit": "Muitas tentativas. Por favor, tente novamente mais tarde.", "github": { - "default": "Failed to sign in with GitHub", - "cancelled": "GitHub sign in was cancelled. Please try again." + "default": "Falha ao entrar com o GitHub", + "cancelled": "O login com o GitHub foi cancelado. Por favor, tente novamente." }, "google": { - "default": "Failed to sign in with Google", - "cancelled": "Google sign in was cancelled. Please try again." + "default": "Falha ao entrar com o Google", + "cancelled": "O login com o Google foi cancelado. Por favor, tente novamente." } } }, "sign_up": { - "hide_password": "Hide password", - "show_password": "Show password", - "creating_account": "Creating account", - "name_title": "Name can only contain letters, spaces, hyphens, and apostrophes", - "page_title": "Create an account", - "page_sub_title": "Create an account or log in", - "full_name_label": "Full name", - "full_name_placeholder": "Enter your name", + "hide_password": "Ocultar senha", + "show_password": "Mostrar senha", + "creating_account": "Criando conta", + "name_title": "O nome pode conter apenas letras, espaços, hífens e apóstrofos", + "page_title": "Criar uma conta", + "page_sub_title": "Crie uma conta ou faça login", + "full_name_label": "Nome completo", + "full_name_placeholder": "Digite seu nome", "email_label": "Email", - "email_placeholder": "Enter your email", - "password_label": "Password", - "password_placeholder": "Enter your password", - "create_account": "Create account", - "already_have_account": "Already have an account?", - "sign_in_redirect_label": "Sign In", - "create_account_warning": "By creating an account, you agree to our Terms of Service and Privacy Policy", - "divider_label": "Or continue with", - "disabled_registration_warning": "Registration is disabled, please contact your admin.", + "email_placeholder": "Digite seu email", + "password_label": "Senha", + "password_placeholder": "Digite sua senha", + "create_account": "Criar conta", + "already_have_account": "Já tem uma conta?", + "sign_in_redirect_label": "Entrar", + "create_account_warning": "Ao criar uma conta, você concorda com nossos Termos de Serviço e Política de Privacidade", + "divider_label": "Ou continue com", + "disabled_registration_warning": "O registro está desativado, entre em contato com o administrador.", "validations": { - "password_max_128_characters": "Password must be less than 128 characters long.", - "password_min_8_character": "Password must be at least 8 characters long.", - "password_min_1_uppercase_letter": "Password must include at least one uppercase letter.", - "password_min_1_lowercase_letter": "Password must include at least one lowercase letter.", - "password_min_1_number": "Password must include at least one number.", - "password_min_1_special_character": "Password must include at least one special character.", - "name_required": "Name is required.", - "name_cannot_be_empty": "Name cannot be empty.", - "name_invalid_format": "Name can only contain letters, spaces, hyphens, and apostrophes.", - "name_cannot_contain_spaces": "Name cannot contain consecutive spaces.", - "email_required": "Email is required.", - "email_invalid": "Please enter a valid email address.", - "truncated_name_error": "Name will be truncated to 100 characters. Please shorten your name.", - "failed_to_create_account": "Failed to create account", - "account_with_same_email_already_exists": "An account with this email already exists. Please sign in instead.", - "email_sign_up_is_not_enabled":"Email signup is currently disabled." + "password_max_128_characters": "A senha deve ter menos de 128 caracteres.", + "password_min_8_character": "A senha deve ter pelo menos 8 caracteres.", + "password_min_1_uppercase_letter": "A senha deve incluir pelo menos uma letra maiúscula.", + "password_min_1_lowercase_letter": "A senha deve incluir pelo menos uma letra minúscula.", + "password_min_1_number": "A senha deve incluir pelo menos um número.", + "password_min_1_special_character": "A senha deve incluir pelo menos um caractere especial.", + "name_required": "O nome é obrigatório.", + "name_cannot_be_empty": "O nome não pode estar vazio.", + "name_invalid_format": "O nome pode conter apenas letras, espaços, hífens e apóstrofos.", + "name_cannot_contain_spaces": "O nome não pode conter espaços consecutivos.", + "email_required": "O email é obrigatório.", + "email_invalid": "Por favor, insira um endereço de email válido.", + "truncated_name_error": "O nome será truncado para 100 caracteres. Por favor, encurte seu nome.", + "failed_to_create_account": "Falha ao criar conta", + "account_with_same_email_already_exists": "Uma conta com este email já existe. Por favor, faça login.", + "email_sign_up_is_not_enabled": "O cadastro por email está desativado no momento." } }, "sign_in": { - "page_title": "Sign in", - "page_sub_title": "Enter your details" + "page_title": "Entrar", + "page_sub_title": "Insira seus dados" }, + "helper_text.need_help": "Ajuda necessária?", + "links.contact_support": "Entre em contato com o suporte", "generic": { - "loading": "Loading" + "loading": "Carregando" } } \ No newline at end of file From 397058e14425ee8d703a7071678fd7c1d7c9acc2 Mon Sep 17 00:00:00 2001 From: kallebe Date: Mon, 2 Mar 2026 10:47:58 -0300 Subject: [PATCH 04/17] fix: json format --- apps/sim/translations/es.json | 4 ++-- apps/sim/translations/pt.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/sim/translations/es.json b/apps/sim/translations/es.json index ce4dce0b6ea..399c12663d0 100644 --- a/apps/sim/translations/es.json +++ b/apps/sim/translations/es.json @@ -4,6 +4,8 @@ "en": "EN - inglés", "es": "ES - español" }, + "helper_text.need_help": "¿Necesitas ayuda?", + "links.contact_support": "Contactar soporte", "network_error": "Error de red. Verifique su conexión e intente nuevamente.", "too_many_requests": "Demasiadas solicitudes. Espere un momento antes de intentarlo nuevamente.", "sign_in_with_sso": "Iniciar sesión con SSO", @@ -69,8 +71,6 @@ "page_title": "Iniciar sesión", "page_sub_title": "Ingrese sus datos" }, - "helper_text.need_help": "¿Necesitas ayuda?", - "links.contact_support": "Contactar soporte", "generic": { "loading": "Cargando" } diff --git a/apps/sim/translations/pt.json b/apps/sim/translations/pt.json index e1357583bf8..93e0818584b 100644 --- a/apps/sim/translations/pt.json +++ b/apps/sim/translations/pt.json @@ -4,6 +4,8 @@ "en": "EN - inglês", "es": "ES - espanhol" }, + "helper_text.need_help": "Precisa de ajuda?", + "links.contact_support": "Entre em contato com o suporte", "network_error": "Erro de rede. Verifique sua conexão e tente novamente.", "too_many_requests": "Muitas solicitações. Aguarde um momento antes de tentar novamente.", "sign_in_with_sso": "Entrar com SSO", @@ -69,8 +71,6 @@ "page_title": "Entrar", "page_sub_title": "Insira seus dados" }, - "helper_text.need_help": "Ajuda necessária?", - "links.contact_support": "Entre em contato com o suporte", "generic": { "loading": "Carregando" } From 2e964ab71e1605e7d1509f6e0a8be665dec46cb0 Mon Sep 17 00:00:00 2001 From: kallebe Date: Mon, 2 Mar 2026 11:19:31 -0300 Subject: [PATCH 05/17] fix: automated translation --- .claude/rules/sim-translations.md | 253 ++++++++++++++++++ .../support-footer.translations.json | 4 - apps/sim/app/(auth)/login/login-form.tsx | 235 +++++++++------- apps/sim/translations/en.json | 69 ++++- apps/sim/translations/es.json | 69 ++++- apps/sim/translations/pt.json | 69 ++++- 6 files changed, 585 insertions(+), 114 deletions(-) create mode 100644 .claude/rules/sim-translations.md delete mode 100644 apps/sim/app/(auth)/components/support-footer.translations.json diff --git a/.claude/rules/sim-translations.md b/.claude/rules/sim-translations.md new file mode 100644 index 00000000000..0fbae526eb5 --- /dev/null +++ b/.claude/rules/sim-translations.md @@ -0,0 +1,253 @@ +--- +name: next-intl-auto-i18n +description: Automatically refactor React/Next.js components to use next-intl without namespaces and output English JSON. +--- + +# Next-Intl Auto I18n Refactoring + +## Overview + +You will receive a React or Next.js component. + +Your job is to: + +1. Convert the component to use next-intl +2. Replace all user-visible and accessibility text with t() calls +3. Use no namespaces +4. Output the refactored component +5. Output the English JSON containing every key used + +Do not include explanations. + +--- +## 0. Dont translate logger info or error messages that are not user-facing. + +## 0.1 Do not translate error messages or similars as keys to be rendered as translations later. For example, if there is an error message like "Invalid email address", do not create a key like "errors.invalid_email" with the value "Invalid email address". If possible, just create a new hook that calls useTranslation inside it and returns the same object or function with translated texts and keep the code in the same file. For example, if there is an error message like "Invalid email address", create a new hook called useErrorMessages that calls useTranslation and returns an object with the same keys but translated values. Then, replace the error message in the code with the corresponding key from the useErrorMessages hook. This way, you can keep the code organized and avoid creating unnecessary keys in the translation JSON. + + +## 1. Detect Component Type + +### Client Component + +If the file contains: + +'use client' + +Use: + +import { useTranslations } from 'next-intl' +const t = useTranslations() + +--- + +### Server Component + +If the file does NOT contain 'use client' + +Use: + +import { getTranslations } from 'next-intl/server' +const t = await getTranslations() + +Never import both. + +--- + +## 2. No Namespaces + +Do NOT pass a namespace to: + +useTranslations +getTranslations + +Forbidden: + +useTranslations('signup') +getTranslations('signup') + +Always: + +const t = useTranslations() + +Keys must NOT be prefixed with file names or component names. + +--- + +## 3. What Must Be Translated + +You MUST replace every user-visible or accessibility string. + +### Visible text + +- headings +- paragraphs +- button text +- link text +- labels +- helper text +- validation messages +- loading text +- divider text +- empty states + +### Attributes + +- aria-label +- title +- alt +- placeholder +- loadingText +- any prop that renders text + +### JSX logic + +- ternaries returning text +- template strings that render text + +### Text inside + +- +-

+-

+- + +After: + + + +--- + +### Conditional + +Before: + +{isLoading ? 'Saving...' : 'Save'} + +After: + +{isLoading ? t('buttons.saving') : t('buttons.save')} + +--- + +### Attributes + +Before: + + + +After: + + + +--- + +## 7. Validation and Errors + +Inline validation or error messages must be moved to: + +errors.* + +Only keep validation logic in code. + +Example: + +Before: + +{error &&

Invalid email address

} + +After: + +{error &&

{t('errors.invalid_email')}

} + +--- + +## 8. Output Format (STRICT) + +You must output: + +1) The fully transformed component code + - With correct imports + - With t() used everywhere required + - With no remaining hardcoded user-facing strings + +2) Then output the English JSON object containing ALL keys used + +The JSON must: + +- include every key referenced in the component +- use snake_case +- preserve the same semantic grouping used in the keys +- contain only English strings + +Do NOT include explanations. +Do NOT include comments. +Do NOT include markdown. +Only output the code followed by the JSON. diff --git a/apps/sim/app/(auth)/components/support-footer.translations.json b/apps/sim/app/(auth)/components/support-footer.translations.json deleted file mode 100644 index fc8dc977dcc..00000000000 --- a/apps/sim/app/(auth)/components/support-footer.translations.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "helper_text.need_help": "Need help?", - "links.contact_support": "Contact support" -} \ No newline at end of file diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index df45b58293c..72a7fef216c 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -29,33 +29,6 @@ import { useTranslations } from 'next-intl' const logger = createLogger('LoginForm') -const validateEmailField = (emailValue: string): string[] => { - const errors: string[] = [] - - if (!emailValue || !emailValue.trim()) { - errors.push('Email is required.') - return errors - } - - const validation = quickValidateEmail(emailValue.trim().toLowerCase()) - if (!validation.isValid) { - errors.push(validation.reason || 'Please enter a valid email address.') - } - - return errors -} - -const PASSWORD_VALIDATIONS = { - required: { - test: (value: string) => Boolean(value && typeof value === 'string'), - message: 'Password is required.', - }, - notEmpty: { - test: (value: string) => value.trim().length > 0, - message: 'Password cannot be empty.', - }, -} - const validateCallbackUrl = (url: string): boolean => { try { if (url.startsWith('/')) { @@ -74,20 +47,74 @@ const validateCallbackUrl = (url: string): boolean => { } } -const validatePassword = (passwordValue: string): string[] => { - const errors: string[] = [] +function useLoginValidation() { + const t = useTranslations() + + const validateEmailField = (emailValue: string): string[] => { + const errors: string[] = [] + + if (!emailValue || !emailValue.trim()) { + errors.push(t('sign_in.errors.email_required')) + return errors + } + + const validation = quickValidateEmail(emailValue.trim().toLowerCase()) + if (!validation.isValid) { + errors.push(validation.reason || t('sign_in.errors.email_invalid')) + } - if (!PASSWORD_VALIDATIONS.required.test(passwordValue)) { - errors.push(PASSWORD_VALIDATIONS.required.message) return errors } - if (!PASSWORD_VALIDATIONS.notEmpty.test(passwordValue)) { - errors.push(PASSWORD_VALIDATIONS.notEmpty.message) + const validatePassword = (passwordValue: string): string[] => { + const errors: string[] = [] + + if (!Boolean(passwordValue && typeof passwordValue === 'string')) { + errors.push(t('sign_in.errors.password_required')) + return errors + } + + if (!(passwordValue.trim().length > 0)) { + errors.push(t('sign_in.errors.password_not_empty')) + return errors + } + return errors } - return errors + return { validateEmailField, validatePassword } +} + +function useLoginErrorMessages() { + const t = useTranslations() + + return { + invalidCredentials: t('sign_in.errors.invalid_credentials'), + emailSignInDisabled: t('sign_in.errors.email_sign_in_disabled'), + invalidCredentialsRetry: t('sign_in.errors.invalid_credentials_retry'), + noAccountFound: t('sign_in.errors.no_account_found'), + missingCredentials: t('sign_in.errors.missing_credentials'), + emailPasswordDisabled: t('sign_in.errors.email_password_disabled'), + failedToCreateSession: t('sign_in.errors.failed_to_create_session'), + tooManyAttempts: t('sign_in.errors.too_many_attempts'), + accountLocked: t('sign_in.errors.account_locked'), + networkError: t('sign_in.errors.network_error'), + rateLimit: t('sign_in.errors.rate_limit'), + loginFailed: t('sign_in.errors.login_failed'), + resetSuccess: t('sign_in.messages.reset_success'), + } +} + +function useResetPasswordMessages() { + const t = useTranslations() + + return { + enterEmail: t('sign_in.reset_password.errors.enter_email'), + invalidEmail: t('sign_in.reset_password.errors.invalid_email'), + noAccountFound: t('sign_in.reset_password.errors.no_account_found'), + failed: t('sign_in.reset_password.errors.failed'), + success: t('sign_in.reset_password.messages.success'), + } } export default function LoginPage({ @@ -100,6 +127,9 @@ export default function LoginPage({ isProduction: boolean }) { const t = useTranslations() + const { validateEmailField, validatePassword } = useLoginValidation() + const loginErrors = useLoginErrorMessages() + const resetMessages = useResetPasswordMessages() const router = useRouter() const searchParams = useSearchParams() const [isLoading, setIsLoading] = useState(false) @@ -144,7 +174,7 @@ export default function LoginPage({ const resetSuccess = searchParams.get('resetSuccess') === 'true' if (resetSuccess) { - setResetSuccessMessage('Password reset successful. Please sign in with your new password.') + setResetSuccessMessage(loginErrors.resetSuccess) } } }, [searchParams]) @@ -229,41 +259,37 @@ export default function LoginPage({ } errorHandled = true - const errorMessage: string[] = ['Invalid email or password'] + const errorMessage: string[] = [loginErrors.invalidCredentials] if ( ctx.error.code?.includes('BAD_REQUEST') || ctx.error.message?.includes('Email and password sign in is not enabled') ) { - errorMessage.push('Email sign in is currently disabled.') + errorMessage.push(loginErrors.emailSignInDisabled) } else if ( ctx.error.code?.includes('INVALID_CREDENTIALS') || ctx.error.message?.includes('invalid password') ) { - errorMessage.push('Invalid email or password. Please try again.') + errorMessage.push(loginErrors.invalidCredentialsRetry) } else if ( ctx.error.code?.includes('USER_NOT_FOUND') || ctx.error.message?.includes('not found') ) { - errorMessage.push('No account found with this email. Please sign up first.') + errorMessage.push(loginErrors.noAccountFound) } else if (ctx.error.code?.includes('MISSING_CREDENTIALS')) { - errorMessage.push('Please enter both email and password.') + errorMessage.push(loginErrors.missingCredentials) } else if (ctx.error.code?.includes('EMAIL_PASSWORD_DISABLED')) { - errorMessage.push('Email and password login is disabled.') + errorMessage.push(loginErrors.emailPasswordDisabled) } else if (ctx.error.code?.includes('FAILED_TO_CREATE_SESSION')) { - errorMessage.push('Failed to create session. Please try again later.') + errorMessage.push(loginErrors.failedToCreateSession) } else if (ctx.error.code?.includes('too many attempts')) { - errorMessage.push( - 'Too many login attempts. Please try again later or reset your password.' - ) + errorMessage.push(loginErrors.tooManyAttempts) } else if (ctx.error.code?.includes('account locked')) { - errorMessage.push( - 'Your account has been locked for security. Please reset your password.' - ) + errorMessage.push(loginErrors.accountLocked) } else if (ctx.error.code?.includes('network')) { - errorMessage.push('Network error. Please check your connection and try again.') + errorMessage.push(loginErrors.networkError) } else if (ctx.error.message?.includes('rate limit')) { - errorMessage.push('Too many requests. Please wait a moment before trying again.') + errorMessage.push(loginErrors.rateLimit) } setResetSuccessMessage(null) @@ -277,7 +303,7 @@ export default function LoginPage({ // Show error if not already handled by onError callback if (!errorHandled) { setResetSuccessMessage(null) - const errorMessage = result?.error?.message || 'Login failed. Please try again.' + const errorMessage = result?.error?.message || loginErrors.loginFailed setPasswordErrors([errorMessage]) setShowValidationError(true) } @@ -306,7 +332,7 @@ export default function LoginPage({ if (!forgotPasswordEmail) { setResetStatus({ type: 'error', - message: 'Please enter your email address', + message: resetMessages.enterEmail, }) return } @@ -315,7 +341,7 @@ export default function LoginPage({ if (!emailValidation.isValid) { setResetStatus({ type: 'error', - message: 'Please enter a valid email address', + message: resetMessages.invalidEmail, }) return } @@ -337,20 +363,20 @@ export default function LoginPage({ if (!response.ok) { const errorData = await response.json() - let errorMessage = errorData.message || 'Failed to request password reset' + let errorMessage = resetMessages.failed if ( - errorMessage.includes('Invalid body parameters') || - errorMessage.includes('invalid email') + errorData.message?.includes('Invalid body parameters') || + errorData.message?.includes('invalid email') ) { - errorMessage = 'Please enter a valid email address' - } else if (errorMessage.includes('Email is required')) { - errorMessage = 'Please enter your email address' + errorMessage = resetMessages.invalidEmail + } else if (errorData.message?.includes('Email is required')) { + errorMessage = resetMessages.enterEmail } else if ( - errorMessage.includes('user not found') || - errorMessage.includes('User not found') + errorData.message?.includes('user not found') || + errorData.message?.includes('User not found') ) { - errorMessage = 'No account found with this email address' + errorMessage = resetMessages.noAccountFound } throw new Error(errorMessage) @@ -358,7 +384,7 @@ export default function LoginPage({ setResetStatus({ type: 'success', - message: 'Password reset link sent to your email', + message: resetMessages.success, }) setTimeout(() => { @@ -369,7 +395,7 @@ export default function LoginPage({ logger.error('Error requesting password reset:', { error }) setResetStatus({ type: 'error', - message: error instanceof Error ? error.message : 'Failed to request password reset', + message: error instanceof Error ? error.message : resetMessages.failed, }) } finally { setIsSubmittingReset(false) @@ -388,7 +414,7 @@ export default function LoginPage({ <>

- {t('sign_in.page_title')} + {t('sign_in.page_title')}

{t('sign_in.page_sub_title')} @@ -419,12 +445,12 @@ export default function LoginPage({

- +
- +
@@ -466,7 +492,7 @@ export default function LoginPage({ autoCapitalize='none' autoComplete='current-password' autoCorrect='off' - placeholder='Enter your password' + placeholder={t('sign_in.placeholders.password')} value={password} onChange={handlePasswordChange} className={cn( @@ -480,7 +506,9 @@ export default function LoginPage({ type='button' onClick={() => setShowPassword(!showPassword)} className='-translate-y-1/2 absolute top-1/2 right-3 text-gray-500 transition hover:text-gray-700' - aria-label={showPassword ? 'Hide password' : 'Show password'} + aria-label={ + showPassword ? t('sign_in.aria.hide_password') : t('sign_in.aria.show_password') + } > {showPassword ? : } @@ -499,9 +527,9 @@ export default function LoginPage({ type='submit' disabled={isLoading} loading={isLoading} - loadingText='Signing in' + loadingText={t('sign_in.buttons.signing_in')} > - Sign in + {t('sign_in.buttons.sign_in')} )} @@ -513,7 +541,9 @@ export default function LoginPage({
- Or continue with + + {t('sign_in.divider_label')} +
)} @@ -540,12 +570,12 @@ export default function LoginPage({ {/* Only show signup link if email/password signup is enabled */} {!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
- Don't have an account? + {t('sign_in.links.no_account')} - Sign up + {t('sign_in.links.sign_up')}
)} @@ -553,47 +583,50 @@ export default function LoginPage({
- By signing in, you agree to our{' '} - - Terms of Service - {' '} - and{' '} - - Privacy Policy - + {t.rich('sign_in.agreement', { + terms: (chunks) => ( + + {chunks} + + ), + privacy: (chunks) => ( + + {chunks} + + ), + })}
- Reset Password + {t('sign_in.reset_password.title')} - Enter your email address and we'll send you a link to reset your password if your - account exists. + {t('sign_in.reset_password.description')}
- +
setForgotPasswordEmail(e.target.value)} - placeholder='Enter your email' + placeholder={t('sign_in.placeholders.email')} required type='email' className={cn( @@ -618,9 +651,9 @@ export default function LoginPage({ onClick={handleForgotPassword} disabled={isSubmittingReset} loading={isSubmittingReset} - loadingText='Sending' + loadingText={t('sign_in.reset_password.sending')} > - Send Reset Link + {t('sign_in.reset_password.send_reset_link')}
diff --git a/apps/sim/translations/en.json b/apps/sim/translations/en.json index 85211f869b3..d77b78e554a 100644 --- a/apps/sim/translations/en.json +++ b/apps/sim/translations/en.json @@ -4,8 +4,12 @@ "en": "EN - english", "es": "ES - Spanish" }, - "helper_text.need_help": "Need help?", - "links.contact_support": "Contact support", + "helper_text": { + "need_help": "Need help?" + }, + "links": { + "contact_support": "Contact support" + }, "network_error": "Network error. Please check your connection and try again.", "too_many_requests": "Too many requests. Please wait a moment before trying again.", "sign_in_with_sso": "Sign in with SSO", @@ -69,7 +73,66 @@ }, "sign_in": { "page_title": "Sign in", - "page_sub_title": "Enter your details" + "page_sub_title": "Enter your details", + "labels": { + "email": "Email", + "password": "Password" + }, + "placeholders": { + "email": "Enter your email", + "password": "Enter your password" + }, + "buttons": { + "sign_in": "Sign in", + "signing_in": "Signing in" + }, + "links": { + "forgot_password": "Forgot password?", + "no_account": "Don't have an account?", + "sign_up": "Sign up" + }, + "aria": { + "hide_password": "Hide password", + "show_password": "Show password" + }, + "divider_label": "Or continue with", + "agreement": "By signing in, you agree to our Terms of Service and Privacy Policy", + "reset_password": { + "title": "Reset Password", + "description": "Enter your email address and we'll send you a link to reset your password if your account exists.", + "send_reset_link": "Send Reset Link", + "sending": "Sending", + "errors": { + "enter_email": "Please enter your email address", + "invalid_email": "Please enter a valid email address", + "no_account_found": "No account found with this email address", + "failed": "Failed to request password reset" + }, + "messages": { + "success": "Password reset link sent to your email" + } + }, + "errors": { + "email_required": "Email is required.", + "email_invalid": "Please enter a valid email address.", + "password_required": "Password is required.", + "password_not_empty": "Password cannot be empty.", + "invalid_credentials": "Invalid email or password", + "email_sign_in_disabled": "Email sign in is currently disabled.", + "invalid_credentials_retry": "Invalid email or password. Please try again.", + "no_account_found": "No account found with this email. Please sign up first.", + "missing_credentials": "Please enter both email and password.", + "email_password_disabled": "Email and password login is disabled.", + "failed_to_create_session": "Failed to create session. Please try again later.", + "too_many_attempts": "Too many login attempts. Please try again later or reset your password.", + "account_locked": "Your account has been locked for security. Please reset your password.", + "network_error": "Network error. Please check your connection and try again.", + "rate_limit": "Too many requests. Please wait a moment before trying again.", + "login_failed": "Login failed. Please try again." + }, + "messages": { + "reset_success": "Password reset successful. Please sign in with your new password." + } }, "generic": { "loading": "Loading" diff --git a/apps/sim/translations/es.json b/apps/sim/translations/es.json index 399c12663d0..3966536b7fb 100644 --- a/apps/sim/translations/es.json +++ b/apps/sim/translations/es.json @@ -4,8 +4,12 @@ "en": "EN - inglés", "es": "ES - español" }, - "helper_text.need_help": "¿Necesitas ayuda?", - "links.contact_support": "Contactar soporte", + "helper_text": { + "need_help": "¿Necesita ayuda?" + }, + "links": { + "contact_support": "Contactar soporte" + }, "network_error": "Error de red. Verifique su conexión e intente nuevamente.", "too_many_requests": "Demasiadas solicitudes. Espere un momento antes de intentarlo nuevamente.", "sign_in_with_sso": "Iniciar sesión con SSO", @@ -69,7 +73,66 @@ }, "sign_in": { "page_title": "Iniciar sesión", - "page_sub_title": "Ingrese sus datos" + "page_sub_title": "Ingrese sus datos", + "labels": { + "email": "Correo electrónico", + "password": "Contraseña" + }, + "placeholders": { + "email": "Ingrese su correo electrónico", + "password": "Ingrese su contraseña" + }, + "buttons": { + "sign_in": "Iniciar sesión", + "signing_in": "Iniciando sesión" + }, + "links": { + "forgot_password": "¿Olvidó su contraseña?", + "no_account": "¿No tiene una cuenta?", + "sign_up": "Registrarse" + }, + "aria": { + "hide_password": "Ocultar contraseña", + "show_password": "Mostrar contraseña" + }, + "divider_label": "O continúe con", + "agreement": "Al iniciar sesión, acepta nuestros Términos de servicio y Política de privacidad", + "reset_password": { + "title": "Restablecer Contraseña", + "description": "Ingrese su dirección de correo electrónico y le enviaremos un enlace para restablecer su contraseña si su cuenta existe.", + "send_reset_link": "Enviar Enlace de Restablecimiento", + "sending": "Enviando", + "errors": { + "enter_email": "Por favor, ingrese su dirección de correo electrónico", + "invalid_email": "Por favor, ingrese una dirección de correo electrónico válida", + "no_account_found": "No se encontró ninguna cuenta con esta dirección de correo electrónico", + "failed": "Error al solicitar restablecimiento de contraseña" + }, + "messages": { + "success": "Enlace de restablecimiento de contraseña enviado a su correo electrónico" + } + }, + "errors": { + "email_required": "El correo electrónico es obligatorio.", + "email_invalid": "Por favor, ingrese una dirección de correo electrónico válida.", + "password_required": "La contraseña es obligatoria.", + "password_not_empty": "La contraseña no puede estar vacía.", + "invalid_credentials": "Correo electrónico o contraseña inválidos", + "email_sign_in_disabled": "El inicio de sesión por correo electrónico está deshabilitado actualmente.", + "invalid_credentials_retry": "Correo electrónico o contraseña inválidos. Por favor, intente nuevamente.", + "no_account_found": "No se encontró ninguna cuenta con este correo electrónico. Por favor, regístrese primero.", + "missing_credentials": "Por favor, ingrese correo electrónico y contraseña.", + "email_password_disabled": "El inicio de sesión por correo electrónico y contraseña está deshabilitado.", + "failed_to_create_session": "Error al crear la sesión. Por favor, intente nuevamente más tarde.", + "too_many_attempts": "Demasiados intentos de inicio de sesión. Por favor, intente nuevamente más tarde o restablezca su contraseña.", + "account_locked": "Su cuenta ha sido bloqueada por seguridad. Por favor, restablezca su contraseña.", + "network_error": "Error de red. Verifique su conexión e intente nuevamente.", + "rate_limit": "Demasiadas solicitudes. Espere un momento antes de intentarlo nuevamente.", + "login_failed": "Error al iniciar sesión. Por favor, intente nuevamente." + }, + "messages": { + "reset_success": "Contraseña restablecida con éxito. Por favor, inicie sesión con su nueva contraseña." + } }, "generic": { "loading": "Cargando" diff --git a/apps/sim/translations/pt.json b/apps/sim/translations/pt.json index 93e0818584b..db63e2b596b 100644 --- a/apps/sim/translations/pt.json +++ b/apps/sim/translations/pt.json @@ -4,8 +4,12 @@ "en": "EN - inglês", "es": "ES - espanhol" }, - "helper_text.need_help": "Precisa de ajuda?", - "links.contact_support": "Entre em contato com o suporte", + "helper_text": { + "need_help": "Precisa de ajuda?" + }, + "links": { + "contact_support": "Entre em contato com o suporte" + }, "network_error": "Erro de rede. Verifique sua conexão e tente novamente.", "too_many_requests": "Muitas solicitações. Aguarde um momento antes de tentar novamente.", "sign_in_with_sso": "Entrar com SSO", @@ -69,7 +73,66 @@ }, "sign_in": { "page_title": "Entrar", - "page_sub_title": "Insira seus dados" + "page_sub_title": "Insira seus dados", + "labels": { + "email": "Email", + "password": "Senha" + }, + "placeholders": { + "email": "Digite seu email", + "password": "Digite sua senha" + }, + "buttons": { + "sign_in": "Entrar", + "signing_in": "Entrando" + }, + "links": { + "forgot_password": "Esqueceu a senha?", + "no_account": "Não tem uma conta?", + "sign_up": "Cadastre-se" + }, + "aria": { + "hide_password": "Ocultar senha", + "show_password": "Mostrar senha" + }, + "divider_label": "Ou continue com", + "agreement": "Ao entrar, você concorda com nossos Termos de Serviço e Política de Privacidade", + "reset_password": { + "title": "Redefinir Senha", + "description": "Digite seu endereço de email e enviaremos um link para redefinir sua senha, caso sua conta exista.", + "send_reset_link": "Enviar Link de Redefinição", + "sending": "Enviando", + "errors": { + "enter_email": "Por favor, digite seu endereço de email", + "invalid_email": "Por favor, digite um endereço de email válido", + "no_account_found": "Nenhuma conta encontrada com este endereço de email", + "failed": "Falha ao solicitar redefinição de senha" + }, + "messages": { + "success": "Link de redefinição de senha enviado para seu email" + } + }, + "errors": { + "email_required": "O email é obrigatório.", + "email_invalid": "Por favor, insira um endereço de email válido.", + "password_required": "A senha é obrigatória.", + "password_not_empty": "A senha não pode estar vazia.", + "invalid_credentials": "Email ou senha inválidos", + "email_sign_in_disabled": "O login por email está desativado no momento.", + "invalid_credentials_retry": "Email ou senha inválidos. Por favor, tente novamente.", + "no_account_found": "Nenhuma conta encontrada com este email. Por favor, cadastre-se primeiro.", + "missing_credentials": "Por favor, insira email e senha.", + "email_password_disabled": "O login por email e senha está desativado.", + "failed_to_create_session": "Falha ao criar sessão. Por favor, tente novamente mais tarde.", + "too_many_attempts": "Muitas tentativas de login. Por favor, tente novamente mais tarde ou redefina sua senha.", + "account_locked": "Sua conta foi bloqueada por segurança. Por favor, redefina sua senha.", + "network_error": "Erro de rede. Verifique sua conexão e tente novamente.", + "rate_limit": "Muitas solicitações. Aguarde um momento antes de tentar novamente.", + "login_failed": "Falha no login. Por favor, tente novamente." + }, + "messages": { + "reset_success": "Senha redefinida com sucesso. Por favor, entre com sua nova senha." + } }, "generic": { "loading": "Carregando" From 86ec8f3f0621cb80866dd6b9848cd7362a55012d Mon Sep 17 00:00:00 2001 From: kallebe Date: Mon, 2 Mar 2026 15:48:37 -0300 Subject: [PATCH 06/17] feat: settings translations --- .claude/rules/sim-translations.md | 23 +- apps/sim/app/(auth)/login/login-form.tsx | 3 +- .../components/block-menu/block-menu.tsx | 42 +- .../components/canvas-menu/canvas-menu.tsx | 26 +- .../components/command-list/command-list.tsx | 13 +- .../workflow-controls/workflow-controls.tsx | 12 +- .../[workspaceId]/w/[workflowId]/workflow.tsx | 46 +- .../components/api-keys/api-keys.tsx | 51 +- .../create-api-key-modal.tsx | 41 +- .../settings-modal/components/byok/byok.tsx | 55 +- .../components/copilot/copilot.tsx | 77 +- .../components/custom-tools/custom-tools.tsx | 38 +- .../settings-modal/components/debug/debug.tsx | 10 +- .../settings-modal/components/files/files.tsx | 39 +- .../components/general/general.tsx | 77 +- .../mcp/components/form-field/form-field.tsx | 7 +- .../skills/components/skill-modal.tsx | 44 +- .../components/skills/skills.tsx | 36 +- .../cancel-subscription.tsx | 60 +- .../credit-balance/credit-balance.tsx | 35 +- .../components/plan-card/plan-card.tsx | 11 +- .../referral-code/referral-code.tsx | 20 +- .../components/subscription/subscription.tsx | 63 +- .../member-invitation-card.tsx | 51 +- .../no-organization-view.tsx | 55 +- .../remove-member-dialog.tsx | 43 +- .../components/team-members/team-members.tsx | 34 +- .../team-seats-overview.tsx | 23 +- .../components/team-seats/team-seats.tsx | 34 +- .../team-management/team-management.tsx | 58 +- .../components/usage-header/usage-header.tsx | 4 +- .../components/usage-limit/usage-limit.tsx | 6 +- .../settings-modal/settings-modal.tsx | 97 ++- .../components/context-menu/context-menu.tsx | 20 +- .../components/delete-modal/delete-modal.tsx | 179 ++-- apps/sim/translations/en.json | 779 ++++++++++++++++++ apps/sim/translations/es.json | 779 ++++++++++++++++++ apps/sim/translations/pt.json | 779 ++++++++++++++++++ 38 files changed, 3166 insertions(+), 604 deletions(-) diff --git a/.claude/rules/sim-translations.md b/.claude/rules/sim-translations.md index 0fbae526eb5..14f7b74e687 100644 --- a/.claude/rules/sim-translations.md +++ b/.claude/rules/sim-translations.md @@ -1,6 +1,6 @@ --- name: next-intl-auto-i18n -description: Automatically refactor React/Next.js components to use next-intl without namespaces and output English JSON. +description: Automatically refactor React/Next.js components to use next-intl without namespaces --- # Next-Intl Auto I18n Refactoring @@ -14,13 +14,11 @@ Your job is to: 1. Convert the component to use next-intl 2. Replace all user-visible and accessibility text with t() calls 3. Use no namespaces -4. Output the refactored component -5. Output the English JSON containing every key used Do not include explanations. --- -## 0. Dont translate logger info or error messages that are not user-facing. +## 0. Don't translate logger info or error messages that are not user-facing. ## 0.1 Do not translate error messages or similars as keys to be rendered as translations later. For example, if there is an error message like "Invalid email address", do not create a key like "errors.invalid_email" with the value "Invalid email address". If possible, just create a new hook that calls useTranslation inside it and returns the same object or function with translated texts and keep the code in the same file. For example, if there is an error message like "Invalid email address", create a new hook called useErrorMessages that calls useTranslation and returns an object with the same keys but translated values. Then, replace the error message in the code with the corresponding key from the useErrorMessages hook. This way, you can keep the code organized and avoid creating unnecessary keys in the translation JSON. @@ -231,23 +229,14 @@ After: ## 8. Output Format (STRICT) -You must output: - -1) The fully transformed component code - - With correct imports - - With t() used everywhere required - - With no remaining hardcoded user-facing strings - -2) Then output the English JSON object containing ALL keys used - -The JSON must: - +You must: +- translate associated files +- output the refactored component +- if needed, modify translation files (en, es, pt) maintaining the same keys but with translated values - include every key referenced in the component - use snake_case - preserve the same semantic grouping used in the keys -- contain only English strings Do NOT include explanations. Do NOT include comments. Do NOT include markdown. -Only output the code followed by the JSON. diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index 72a7fef216c..15fbf3b35e5 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -26,6 +26,7 @@ import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button' import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' import { useTranslations } from 'next-intl' +import LocaleSelector from '@/components/locale-selector' const logger = createLogger('LoginForm') @@ -605,8 +606,8 @@ export default function LoginPage({ ), })} +
- diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx index 79e8464bf49..f82d32fec17 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx @@ -1,6 +1,7 @@ 'use client' import type { RefObject } from 'react' +import { useTranslations } from 'next-intl' import { Popover, PopoverAnchor, @@ -94,6 +95,7 @@ export function BlockMenu({ onToggleLocked, canAdmin = false, }: BlockMenuProps) { + const t = useTranslations() const isSingleBlock = selectedBlocks.length === 1 const allEnabled = selectedBlocks.every((b) => b.enabled) @@ -122,15 +124,15 @@ export function BlockMenu({ const canRemoveFromSubflow = showRemoveFromSubflow && !hasTriggerBlock const getToggleEnabledLabel = () => { - if (allEnabled) return 'Disable' - if (allDisabled) return 'Enable' - return 'Toggle Enabled' + if (allEnabled) return t('workflows.block_menu.state.disable') + if (allDisabled) return t('workflows.block_menu.state.enable') + return t('workflows.block_menu.state.toggle_enabled') } const getToggleLockedLabel = () => { - if (allLocked) return 'Unlock' - if (allUnlocked) return 'Lock' - return 'Toggle Lock' + if (allLocked) return t('workflows.block_menu.state.unlock') + if (allUnlocked) return t('workflows.block_menu.state.lock') + return t('workflows.block_menu.state.toggle_lock') } return ( @@ -159,7 +161,7 @@ export function BlockMenu({ onClose() }} > - Copy + {t('workflows.block_menu.clipboard.copy')} ⌘C - Paste + {t('workflows.block_menu.clipboard.paste')} ⌘V {!hasSingletonBlock && ( @@ -181,7 +183,7 @@ export function BlockMenu({ onClose() }} > - Duplicate + {t('workflows.block_menu.edit.duplicate')} )} @@ -197,7 +199,9 @@ export function BlockMenu({ } }} > - {hasBlockWithDisabledParent ? 'Parent is disabled' : getToggleEnabledLabel()} + {hasBlockWithDisabledParent + ? t('workflows.block_menu.state.parent_disabled') + : getToggleEnabledLabel()} )} {!allNoteBlocks && !isSubflow && ( @@ -208,7 +212,7 @@ export function BlockMenu({ onClose() }} > - Flip Handles + {t('workflows.block_menu.edit.flip_handles')} )} {canRemoveFromSubflow && ( @@ -219,7 +223,7 @@ export function BlockMenu({ onClose() }} > - Remove from Subflow + {t('workflows.block_menu.subflow.remove_from_subflow')} )} {canAdmin && onToggleLocked && ( @@ -232,7 +236,9 @@ export function BlockMenu({ } }} > - {hasBlockWithLockedParent ? 'Parent is locked' : getToggleLockedLabel()} + {hasBlockWithLockedParent + ? t('workflows.block_menu.state.parent_locked') + : getToggleLockedLabel()} )} @@ -246,7 +252,7 @@ export function BlockMenu({ onClose() }} > - Rename + {t('workflows.block_menu.edit.rename')} )} {isSingleBlock && ( @@ -256,7 +262,7 @@ export function BlockMenu({ onClose() }} > - Open Editor + {t('workflows.block_menu.edit.open_editor')} )} @@ -273,7 +279,7 @@ export function BlockMenu({ } }} > - Run from block + {t('workflows.block_menu.execution.run_from_block')} {/* Hide "Run until" for triggers - they're always at the start */} {!hasTriggerBlock && ( @@ -286,7 +292,7 @@ export function BlockMenu({ } }} > - Run until block + {t('workflows.block_menu.execution.run_until_block')} )} @@ -302,7 +308,7 @@ export function BlockMenu({ onClose() }} > - Delete + {t('workflows.block_menu.destructive.delete')} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/canvas-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/canvas-menu.tsx index e091849c827..6a3473c8f04 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/canvas-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/canvas-menu.tsx @@ -1,6 +1,7 @@ 'use client' import type { RefObject } from 'react' +import { useTranslations } from 'next-intl' import { Popover, PopoverAnchor, @@ -64,6 +65,7 @@ export function CanvasMenu({ canRedo = false, hasLockedBlocks = false, }: CanvasMenuProps) { + const t = useTranslations() return ( - Undo + {t('workflows.canvas_menu.history.undo')} ⌘Z - Redo + {t('workflows.canvas_menu.history.redo')} ⌘⇧Z @@ -116,7 +118,7 @@ export function CanvasMenu({ onClose() }} > - Paste + {t('workflows.canvas_menu.edit.paste')} ⌘V - Add Block + {t('workflows.canvas_menu.edit.add_block')} ⌘K - Auto-layout + {t('workflows.canvas_menu.edit.auto_layout')} ⇧L - Fit to View + {t('workflows.canvas_menu.edit.fit_to_view')} {/* Navigation actions */} @@ -160,7 +162,7 @@ export function CanvasMenu({ onClose() }} > - Open Logs + {t('workflows.canvas_menu.navigation.open_logs')} ⌘L - {isVariablesOpen ? 'Close Variables' : 'Open Variables'} + {isVariablesOpen + ? t('workflows.canvas_menu.navigation.close_variables') + : t('workflows.canvas_menu.navigation.open_variables')} { @@ -177,7 +181,9 @@ export function CanvasMenu({ onClose() }} > - {isChatOpen ? 'Close Chat' : 'Open Chat'} + {isChatOpen + ? t('workflows.canvas_menu.navigation.close_chat') + : t('workflows.canvas_menu.navigation.open_chat')} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx index 3b7cb2bb4be..0cd1d1b6d25 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx @@ -2,6 +2,7 @@ import { useCallback } from 'react' import { createLogger } from '@sim/logger' +import { useTranslations } from 'next-intl' import { Layout, Search } from 'lucide-react' import Image from 'next/image' import { useParams, useRouter } from 'next/navigation' @@ -56,6 +57,7 @@ const commands: CommandItem[] = [ * Centered on the screen for empty workflows */ export function CommandList() { + const t = useTranslations() const params = useParams() const router = useRouter() const { open: openSearchModal } = useSearchModalStore() @@ -63,6 +65,13 @@ export function CommandList() { const workspaceId = params.workspaceId as string | undefined + const commandLabels = { + Templates: t('workflows.command_list.templates'), + 'New Agent': t('workflows.command_list.new_agent'), + Logs: t('workflows.command_list.logs'), + 'Search Blocks': t('workflows.command_list.search_blocks'), + } as const + /** * Handle click on a command row. * @@ -202,6 +211,8 @@ export function CommandList() { {commands.map((command) => { const Icon = command.icon const shortcuts = Array.isArray(command.shortcut) ? command.shortcut : [command.shortcut] + const translatedLabel = + commandLabels[command.label as keyof typeof commandLabels] || command.label return (
- {command.label} + {translatedLabel}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx index e646d61dcf8..ea7d9095410 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx @@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger' import clsx from 'clsx' import { Scan } from 'lucide-react' import { useReactFlow } from 'reactflow' +import { useTranslations } from 'next-intl' import { Button, ChevronDown, @@ -36,6 +37,7 @@ const logger = createLogger('WorkflowControls') * Floating controls for canvas mode, undo/redo, and fit-to-view. */ export const WorkflowControls = memo(function WorkflowControls() { + const t = useTranslations() const reactFlowInstance = useReactFlow() const { fitViewToBounds } = useCanvasViewport(reactFlowInstance) const { mode, setMode } = useCanvasModeStore() @@ -126,7 +128,11 @@ export const WorkflowControls = memo(function WorkflowControls() {
- {mode === 'hand' ? 'Mover' : 'Pointer'} + + {mode === 'hand' + ? t('workflows.canvas.modes.hand') + : t('workflows.canvas.modes.pointer')} + - Mover + {t('workflows.canvas.modes.hand')} { @@ -145,7 +151,7 @@ export const WorkflowControls = memo(function WorkflowControls() { }} > - Pointer + {t('workflows.canvas.modes.pointer')} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index f88b9d9122f..1f081c6b04e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -2,6 +2,7 @@ import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useParams, useRouter } from 'next/navigation' +import { useTranslations } from 'next-intl' import ReactFlow, { applyNodeChanges, ConnectionLineType, @@ -227,6 +228,7 @@ interface BlockData { * Renders the ReactFlow canvas with blocks, edges, and all interactive features. */ const WorkflowContent = React.memo(() => { + const t = useTranslations() const [isCanvasReady, setIsCanvasReady] = useState(false) const [potentialParentId, setPotentialParentId] = useState(null) const [selectedEdges, setSelectedEdges] = useState(new Map()) @@ -280,7 +282,7 @@ const WorkflowContent = React.memo(() => { if (reconnect) { addNotification({ level: 'info', - message: `"${displayName}" reconnected successfully.`, + message: t('workflows.notifications.reconnected_successfully', { displayName }), }) window.dispatchEvent( new CustomEvent('oauth-credentials-updated', { @@ -302,14 +304,14 @@ const WorkflowContent = React.memo(() => { if (oauthCredentials.length > preCount) { addNotification({ level: 'info', - message: `"${displayName}" credential connected successfully.`, + message: t('workflows.notifications.credential_connected', { displayName }), }) } else { const existing = oauthCredentials.find((c) => c.providerId === providerId) const existingName = existing?.displayName || displayName addNotification({ level: 'info', - message: `This account is already connected as "${existingName}".`, + message: t('workflows.notifications.account_already_connected', { existingName }), }) } } catch { @@ -998,7 +1000,7 @@ const WorkflowContent = React.memo(() => { if (hasTrigger) { addNotification({ level: 'error', - message: 'Triggers cannot be placed inside loop or parallel subflows.', + message: t('workflows.errors.triggers_in_loop'), workflowId: activeWorkflowId || undefined, }) return @@ -1009,7 +1011,7 @@ const WorkflowContent = React.memo(() => { if (hasSubflow) { addNotification({ level: 'error', - message: 'Subflows cannot be nested inside other subflows.', + message: t('workflows.errors.nested_subflows'), workflowId: activeWorkflowId || undefined, }) return @@ -1144,14 +1146,16 @@ const WorkflowContent = React.memo(() => { if (allProtected) { addNotification({ level: 'info', - message: 'Cannot delete locked blocks or blocks inside locked containers', + message: t('workflows.errors.locked_blocks_delete'), workflowId: activeWorkflowId || undefined, }) return } addNotification({ level: 'info', - message: `Skipped ${protectedIds.length} protected block(s)`, + message: t('workflows.notifications.protected_blocks_skipped', { + count: protectedIds.length, + }), workflowId: activeWorkflowId || undefined, }) } @@ -1564,8 +1568,8 @@ const WorkflowContent = React.memo(() => { if (triggerIssue) { const message = triggerIssue.issue === 'legacy' - ? 'Cannot add new trigger blocks when a legacy Start block exists. Available in newer workflows.' - : `A workflow can only have one ${triggerIssue.triggerName} trigger block. Please remove the existing one before adding a new one.` + ? t('workflows.errors.legacy_start_block_exists') + : t('workflows.errors.single_trigger_block', { triggerName: triggerIssue.triggerName }) addNotification({ level: 'error', message, @@ -1578,7 +1582,9 @@ const WorkflowContent = React.memo(() => { if (singleInstanceIssue) { addNotification({ level: 'error', - message: `A workflow can only have one ${singleInstanceIssue.blockName} block. Please remove the existing one before adding a new one.`, + message: t('workflows.errors.single_instance_block', { + blockName: singleInstanceIssue.blockName, + }), workflowId: activeWorkflowId || undefined, }) return true @@ -1586,7 +1592,7 @@ const WorkflowContent = React.memo(() => { return false }, - [blocks, addNotification, activeWorkflowId] + [blocks, addNotification, activeWorkflowId, t] ) /** @@ -1660,7 +1666,7 @@ const WorkflowContent = React.memo(() => { if (isTriggerBlock) { addNotification({ level: 'error', - message: 'Triggers cannot be placed inside loop or parallel subflows.', + message: t('workflows.errors.triggers_in_loop'), workflowId: activeWorkflowId || undefined, }) return @@ -1926,10 +1932,10 @@ const WorkflowContent = React.memo(() => { const { type, triggerName } = event.detail const message = type === 'trigger_in_subflow' - ? 'Triggers cannot be placed inside loop or parallel subflows.' + ? t('workflows.errors.triggers_in_loop') : type === 'legacy_incompatibility' - ? 'Cannot add new trigger blocks when a legacy Start block exists. Available in newer workflows.' - : `A workflow can only have one ${triggerName || 'trigger'} trigger block. Please remove the existing one before adding a new one.` + ? t('workflows.errors.legacy_start_block_exists') + : t('workflows.errors.single_trigger_block', { triggerName: triggerName || 'trigger' }) addNotification({ level: 'error', message, @@ -1942,7 +1948,7 @@ const WorkflowContent = React.memo(() => { return () => { window.removeEventListener('show-trigger-warning', handleShowTriggerWarning as EventListener) } - }, [addNotification, activeWorkflowId]) + }, [addNotification, activeWorkflowId, t]) /** Handles drop events on the ReactFlow canvas. */ const onDrop = useCallback( @@ -3041,7 +3047,7 @@ const WorkflowContent = React.memo(() => { if (block && TriggerUtils.isTriggerBlock(block)) { addNotification({ level: 'error', - message: 'Triggers cannot be placed inside loop or parallel subflows.', + message: t('workflows.errors.triggers_in_loop'), workflowId: activeWorkflowId || undefined, }) logger.warn('Prevented trigger block from being placed inside a container', { @@ -3517,14 +3523,16 @@ const WorkflowContent = React.memo(() => { if (allProtected) { addNotification({ level: 'info', - message: 'Cannot delete locked blocks or blocks inside locked containers', + message: t('workflows.errors.locked_blocks_delete'), workflowId: activeWorkflowId || undefined, }) return } addNotification({ level: 'info', - message: `Skipped ${protectedIds.length} protected block(s)`, + message: t('workflows.notifications.protected_blocks_skipped', { + count: protectedIds.length, + }), workflowId: activeWorkflowId || undefined, }) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx index a3c5be94ec7..1a2cc49faa6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx @@ -18,6 +18,7 @@ import { Input, Skeleton } from '@/components/ui' import { useSession } from '@/lib/auth/auth-client' import { formatDate } from '@/lib/core/utils/formatting' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' +import { useTranslations } from 'next-intl' import { type ApiKey, useApiKeys, @@ -34,6 +35,7 @@ interface ApiKeysProps { } export function ApiKeys({ onOpenChange }: ApiKeysProps) { + const t = useTranslations() const { data: session } = useSession() const userId = session?.user?.id const params = useParams() @@ -128,7 +130,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { }, [shouldScrollToBottom]) const formatLastUsed = (dateString?: string) => { - if (!dateString) return 'Never' + if (!dateString) return t('settings.api_keys.never') return formatDate(new Date(dateString)) } @@ -142,7 +144,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { strokeWidth={2} /> setSearchTerm(e.target.value)} className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' @@ -160,7 +162,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { disabled={createButtonDisabled} > - Create + {t('settings.api_keys.buttons.create')}
@@ -182,7 +184,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) {
) : personalKeys.length === 0 && workspaceKeys.length === 0 ? (
- Click "Create" above to get started + {t('settings.api_keys.empty_state')}
) : (
@@ -191,11 +193,11 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { {!searchTerm.trim() ? (
- Workspace + {t('settings.api_keys.sections.workspace')}
{workspaceKeys.length === 0 ? (
- No workspace Sim keys yet + {t('settings.api_keys.no_workspace_keys')}
) : ( workspaceKeys.map((key) => ( @@ -222,7 +224,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { }} disabled={!canManageWorkspaceKeys} > - Delete + {t('settings.api_keys.buttons.delete')}
)) @@ -231,7 +233,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { ) : filteredWorkspaceKeys.length > 0 ? (
- Workspace + {t('settings.api_keys.sections.workspace')}
{filteredWorkspaceKeys.map(({ key }) => (
@@ -257,7 +259,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { }} disabled={!canManageWorkspaceKeys} > - Delete + {t('settings.api_keys.buttons.delete')}
))} @@ -268,7 +270,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { {(!searchTerm.trim() || filteredPersonalKeys.length > 0) && (
- Personal + {t('settings.api_keys.sections.personal')}
{filteredPersonalKeys.map(({ key }) => { const isConflict = conflicts.includes(key.name) @@ -296,13 +298,12 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { setShowDeleteDialog(true) }} > - Delete + {t('settings.api_keys.buttons.delete')}
{isConflict && (
- Workspace Sim key with the same name overrides this. Rename your - personal key to use it. + {t('settings.api_keys.conflict_warning')}
)}
@@ -317,7 +318,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { filteredWorkspaceKeys.length === 0 && (personalKeys.length > 0 || workspaceKeys.length > 0) && (
- No Sim keys found matching "{searchTerm}" + {t('settings.api_keys.no_results', { searchTerm })}
)} @@ -331,7 +332,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) {
- Allow personal Sim keys + {t('settings.api_keys.allow_personal_keys')} @@ -343,7 +344,7 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { - Allow collaborators to create and use their own keys with billing charged to them. + {t('settings.api_keys.allow_personal_keys_tooltip')}
@@ -383,13 +384,15 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { {/* Delete Confirmation Dialog */} - Delete Sim key + {t('settings.api_keys.delete_modal.title')}

- Deleting{' '} - {deleteKey?.name} will - immediately revoke access for any integrations using it.{' '} - This action cannot be undone. + {t.rich('settings.api_keys.delete_modal.description', { + name: () => ( + {deleteKey?.name} + ), + warning: (chunks) => {chunks}, + })}

@@ -401,14 +404,16 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { }} disabled={deleteApiKeyMutation.isPending} > - Cancel + {t('settings.api_keys.delete_modal.cancel')}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx index 12b06921598..35e2e504f41 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx @@ -15,6 +15,7 @@ import { ModalHeader, } from '@/components/emcn' import { type ApiKey, useCreateApiKey } from '@/hooks/queries/api-keys' +import { useTranslations } from 'next-intl' const logger = createLogger('CreateApiKeyModal') @@ -43,6 +44,7 @@ export function CreateApiKeyModal({ defaultKeyType = 'personal', onKeyCreated, }: CreateApiKeyModalProps) { + const t = useTranslations() const [keyName, setKeyName] = useState('') const [keyType, setKeyType] = useState<'personal' | 'workspace'>(defaultKeyType) const [createError, setCreateError] = useState(null) @@ -113,19 +115,19 @@ export function CreateApiKeyModal({ {/* Create API Key Dialog */} - Create new Sim key + {t('settings.create_api_key.title')}

{keyType === 'workspace' - ? "This key will have access to all workflows in this workspace. Make sure to copy it after creation as you won't be able to see it again." - : "This key will have access to your personal workflows. Make sure to copy it after creation as you won't be able to see it again."} + ? t('settings.create_api_key.description_workspace') + : t('settings.create_api_key.description_personal')}

{canManageWorkspaceKeys && (

- Sim Key Type + {t('settings.create_api_key.key_type')}

- Personal + {t('settings.create_api_key.personal')} + + + {t('settings.create_api_key.workspace')} - Workspace
)}

- Enter a name for your Sim key to help you identify it later. + {t('settings.create_api_key.name_label')}

{/* Hidden decoy fields to prevent browser autofill */} @@ -216,13 +222,14 @@ export function CreateApiKeyModal({ }} > - Your Sim key has been created + {t('settings.create_api_key.success_title')}

- This is the only time you will see your Sim key.{' '} - - Copy it now and store it securely. - + {t.rich('settings.create_api_key.success_description', { + strong: (chunks) => ( + {chunks} + ), + })}

{newKey && ( @@ -242,7 +249,9 @@ export function CreateApiKeyModal({ ) : ( )} - Copy to clipboard + + {t('settings.create_api_key.aria.copy_to_clipboard')} +
)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/byok/byok.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/byok/byok.tsx index b8304402b3b..232ac44f649 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/byok/byok.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/byok/byok.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import { createLogger } from '@sim/logger' import { Eye, EyeOff } from 'lucide-react' +import { useTranslations } from 'next-intl' import { useParams } from 'next/navigation' import { Button, @@ -78,6 +79,7 @@ function BYOKKeySkeleton() { } export function BYOK() { + const t = useTranslations() const params = useParams() const workspaceId = (params?.workspaceId as string) || '' @@ -111,7 +113,7 @@ export function BYOK() { setApiKeyInput('') setShowApiKey(false) } catch (err) { - const message = err instanceof Error ? err.message : 'Failed to save API key' + const message = err instanceof Error ? err.message : t('settings.byok.errors.save_failed') setError(message) logger.error('Failed to save BYOK key', { error: err }) } @@ -141,9 +143,7 @@ export function BYOK() { return ( <>
-

- Use your own API keys for hosted model providers. -

+

{t('settings.byok.description')}

{isLoading ? ( @@ -175,13 +175,13 @@ export function BYOK() { {existingKey ? (
) : ( @@ -190,7 +190,7 @@ export function BYOK() { className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90' onClick={() => openEditModal(provider.id)} > - Add Key + {t('settings.byok.buttons.add_key')} )}
@@ -216,20 +216,26 @@ export function BYOK() { {editingProvider && ( <> - {getKeyForProvider(editingProvider) ? 'Update' : 'Add'}{' '} - {PROVIDERS.find((p) => p.id === editingProvider)?.name} API Key + {getKeyForProvider(editingProvider) + ? t('settings.byok.modal.update_title', { + name: PROVIDERS.find((p) => p.id === editingProvider)?.name, + }) + : t('settings.byok.modal.add_title', { + name: PROVIDERS.find((p) => p.id === editingProvider)?.name, + })} )}

- This key will be used for all {PROVIDERS.find((p) => p.id === editingProvider)?.name}{' '} - requests in this workspace. Your key is encrypted and stored securely. + {t('settings.byok.modal.description', { + name: PROVIDERS.find((p) => p.id === editingProvider)?.name, + })}

- Enter your API key + {t('settings.byok.modal.enter_api_key')}

{/* Hidden decoy fields to prevent browser autofill */} - Cancel + {t('settings.byok.buttons.cancel')} @@ -306,22 +314,25 @@ export function BYOK() { setDeleteConfirmProvider(null)}> - Delete API Key + {t('settings.byok.delete_dialog.title')}

- Are you sure you want to delete the{' '} - - {PROVIDERS.find((p) => p.id === deleteConfirmProvider)?.name} - {' '} - API key? This workspace will revert to using platform hosted keys. + {t.rich('settings.byok.delete_dialog.confirm_message', { + name: (chunks) => ( + {chunks} + ), + providerName: PROVIDERS.find((p) => p.id === deleteConfirmProvider)?.name, + })}

diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/copilot/copilot.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/copilot/copilot.tsx index e24ad88f514..a4d495a9777 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/copilot/copilot.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/copilot/copilot.tsx @@ -3,6 +3,7 @@ import { useMemo, useState } from 'react' import { createLogger } from '@sim/logger' import { Check, Copy, Plus, Search } from 'lucide-react' +import { useTranslations } from 'next-intl' import { Button, Input as EmcnInput, @@ -46,6 +47,7 @@ function CopilotKeySkeleton() { * Provides functionality to create, view, and delete copilot API keys. */ export function Copilot() { + const t = useTranslations() const { data: keys = [], isLoading } = useCopilotKeys() const generateKey = useGenerateCopilotKey() const deleteKeyMutation = useDeleteCopilotKey() @@ -75,9 +77,7 @@ export function Copilot() { const trimmedName = newKeyName.trim() const isDuplicate = keys.some((k) => k.name === trimmedName) if (isDuplicate) { - setCreateError( - `A Copilot API key named "${trimmedName}" already exists. Please choose a different name.` - ) + setCreateError(t('settings.copilot.errors.duplicate_name', { name: trimmedName })) return } @@ -93,7 +93,7 @@ export function Copilot() { } } catch (error) { logger.error('Failed to generate copilot API key', { error }) - setCreateError('Failed to create API key. Please check your connection and try again.') + setCreateError(t('settings.copilot.errors.create_failed')) } } @@ -117,7 +117,7 @@ export function Copilot() { } const formatLastUsed = (dateString?: string | null) => { - if (!dateString) return 'Never' + if (!dateString) return t('settings.copilot.last_used_never') return formatDate(new Date(dateString)) } @@ -136,7 +136,7 @@ export function Copilot() { strokeWidth={2} /> setSearchTerm(e.target.value)} className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' @@ -151,7 +151,7 @@ export function Copilot() { disabled={isLoading} > - Create + {t('settings.copilot.buttons.create')}
@@ -165,7 +165,7 @@ export function Copilot() {
) : showEmptyState ? (
- Click "Create" above to get started + {t('settings.copilot.empty_state')}
) : (
@@ -174,10 +174,14 @@ export function Copilot() {
- {key.name || 'Unnamed Key'} + {key.name || t('settings.copilot.unnamed_key')} - (last used: {formatLastUsed(key.lastUsed).toLowerCase()}) + ( + {t('settings.copilot.last_used', { + date: formatLastUsed(key.lastUsed).toLowerCase(), + })} + )

@@ -192,13 +196,13 @@ export function Copilot() { setShowDeleteDialog(true) }} > - Delete + {t('settings.copilot.buttons.delete')}

))} {showNoResults && (
- No API keys found matching "{searchTerm}" + {t('settings.copilot.no_results', { searchTerm })}
)}
@@ -209,16 +213,15 @@ export function Copilot() { {/* Create API Key Dialog */} - Create new API key + {t('settings.copilot.create_dialog.title')}

- This key will allow access to Copilot features. Make sure to copy it after creation as - you won't be able to see it again. + {t('settings.copilot.create_dialog.description')}

- Enter a name for your API key to help you identify it later. + {t('settings.copilot.create_dialog.name_label')}

@@ -245,7 +248,7 @@ export function Copilot() { setCreateError(null) }} > - Cancel + {t('settings.copilot.buttons.cancel')} @@ -271,13 +276,14 @@ export function Copilot() { }} > - Your API key has been created + {t('settings.copilot.new_key_dialog.title')}

- This is the only time you will see your API key.{' '} - - Copy it now and store it securely. - + {t.rich('settings.copilot.new_key_dialog.description', { + bold: (chunks) => ( + {chunks} + ), + })}

{newKey && ( @@ -297,7 +303,7 @@ export function Copilot() { ) : ( )} - Copy to clipboard + {t('settings.copilot.aria.copy_to_clipboard')}
)} @@ -308,15 +314,16 @@ export function Copilot() { {/* Delete Confirmation Dialog */} - Delete API key + {t('settings.copilot.delete_dialog.title')}

- Deleting{' '} - - {deleteKey?.name || 'Unnamed Key'} - {' '} - will immediately revoke access for any integrations using it.{' '} - This action cannot be undone. + {t.rich('settings.copilot.delete_dialog.confirm_message', { + name: (chunks) => ( + {chunks} + ), + warning: (chunks) => {chunks}, + keyName: deleteKey?.name || t('settings.copilot.unnamed_key'), + })}

@@ -328,14 +335,16 @@ export function Copilot() { }} disabled={deleteKeyMutation.isPending} > - Cancel + {t('settings.copilot.buttons.cancel')}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/custom-tools/custom-tools.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/custom-tools/custom-tools.tsx index 42d2f9f4dd8..2ff40d0e4a8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/custom-tools/custom-tools.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/custom-tools/custom-tools.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import { createLogger } from '@sim/logger' import { Plus, Search } from 'lucide-react' +import { useTranslations } from 'next-intl' import { useParams } from 'next/navigation' import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn' import { Input, Skeleton } from '@/components/ui' @@ -27,6 +28,7 @@ function CustomToolSkeleton() { } export function CustomTools() { + const t = useTranslations() const params = useParams() const workspaceId = params.workspaceId as string @@ -108,7 +110,7 @@ export function CustomTools() { strokeWidth={2} /> setSearchTerm(e.target.value)} disabled={isLoading} @@ -117,7 +119,7 @@ export function CustomTools() {
@@ -125,7 +127,9 @@ export function CustomTools() { {error ? (

- {error instanceof Error ? error.message : 'Failed to load tools'} + {error instanceof Error + ? error.message + : t('settings.custom_tools.errors.failed_to_load')}

) : isLoading ? ( @@ -136,7 +140,7 @@ export function CustomTools() {
) : showEmptyState ? (
- Click "Add" above to get started + {t('settings.custom_tools.empty_state')}
) : (
@@ -144,7 +148,7 @@ export function CustomTools() {
- {tool.title || 'Unnamed Tool'} + {tool.title || t('settings.custom_tools.unnamed_tool')} {tool.schema?.function?.description && (

@@ -154,21 +158,23 @@ export function CustomTools() {

))} {showNoResults && (
- No tools found matching "{searchTerm}" + {t('settings.custom_tools.no_results', { searchTerm })}
)}
@@ -201,20 +207,24 @@ export function CustomTools() { - Delete Custom Tool + {t('settings.custom_tools.delete_dialog.title')}

- Are you sure you want to delete{' '} - {toolToDelete?.name}?{' '} - This action cannot be undone. + {t.rich('settings.custom_tools.delete_dialog.confirm_message', { + toolName: toolToDelete?.name, + name: (chunks) => ( + {chunks} + ), + warning: (chunks) => {chunks}, + })}

diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/debug/debug.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/debug/debug.tsx index b36d32a3a46..6b805c9c31c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/debug/debug.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/debug/debug.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import { createLogger } from '@sim/logger' import { useQueryClient } from '@tanstack/react-query' +import { useTranslations } from 'next-intl' import { useParams } from 'next/navigation' import { Button, Input as EmcnInput } from '@/components/emcn' import { workflowKeys } from '@/hooks/queries/workflows' @@ -14,6 +15,7 @@ const logger = createLogger('DebugSettings') * Allows importing workflows by ID for debugging purposes. */ export function Debug() { + const t = useTranslations() const params = useParams() const queryClient = useQueryClient() const workspaceId = params?.workspaceId as string @@ -56,15 +58,13 @@ export function Debug() { return (
-

- Import a workflow by ID along with its associated copilot chats. -

+

{t('settings.debug.description')}

setWorkflowId(e.target.value)} - placeholder='Enter workflow ID' + placeholder={t('settings.debug.placeholders.workflow_id')} disabled={isImporting} />
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/files/files.tsx index 92422d6ee04..06bca76e585 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/files/files.tsx @@ -3,6 +3,7 @@ import { useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { ArrowDown, Loader2, Plus, Search, X } from 'lucide-react' +import { useTranslations } from 'next-intl' import { useParams } from 'next/navigation' import { Button, @@ -75,6 +76,7 @@ const PLAN_NAMES = { } as const export function Files() { + const t = useTranslations() const params = useParams() const workspaceId = params?.workspaceId as string @@ -286,7 +288,7 @@ export function Files() { strokeWidth={2} /> setSearch(e.target.value)} disabled={permissionsLoading} @@ -311,10 +313,13 @@ export function Files() { > {uploading && uploadProgress.total > 0 - ? `${uploadProgress.completed}/${uploadProgress.total}` + ? t('settings.files.buttons.upload_progress', { + completed: uploadProgress.completed, + total: uploadProgress.total, + }) : uploading - ? 'Uploading...' - : 'Upload'} + ? t('settings.files.buttons.uploading') + : t('settings.files.buttons.upload')} )} @@ -326,27 +331,27 @@ export function Files() { renderTableSkeleton() ) : files.length === 0 && failedFiles.length === 0 ? (
- No files uploaded yet + {t('settings.files.empty_state')}
) : filteredFiles.length === 0 && failedFiles.length === 0 ? (
- No files found matching "{search}" + {t('settings.files.no_results', { search })}
) : ( - Name + {t('settings.files.table.name')} - Size + {t('settings.files.table.size')} - Uploaded + {t('settings.files.table.uploaded')} - Actions + {t('settings.files.table.actions')} @@ -380,7 +385,7 @@ export function Files() { variant='ghost' onClick={() => setFailedFiles((prev) => prev.filter((_, i) => i !== index))} className='h-[28px] w-[28px] p-0' - aria-label={`Dismiss ${fileName}`} + aria-label={t('settings.files.aria.dismiss', { fileName })} > @@ -420,7 +425,9 @@ export function Files() { onClick={() => handleDownload(file)} className='h-[28px] w-[28px] p-0' disabled={downloadingFileId === file.id} - aria-label={`Download ${file.name}`} + aria-label={t('settings.files.aria.download', { + fileName: file.name, + })} > {downloadingFileId === file.id ? ( @@ -429,7 +436,7 @@ export function Files() { )} - Download file + {t('settings.files.tooltips.download')} {userPermissions.canEdit && ( @@ -439,12 +446,14 @@ export function Files() { onClick={() => handleDelete(file)} className='h-[28px] w-[28px] p-0' disabled={deleteFile.isPending} - aria-label={`Delete ${file.name}`} + aria-label={t('settings.files.aria.delete', { + fileName: file.name, + })} > - Delete file + {t('settings.files.tooltips.delete')} )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx index 2893557be3e..e3aef80cd44 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx @@ -22,11 +22,13 @@ import { ANONYMOUS_USER_ID } from '@/lib/auth/constants' import { getEnv, isTruthy } from '@/lib/core/config/env' import { isHosted } from '@/lib/core/config/feature-flags' import { getBaseUrl } from '@/lib/core/utils/urls' +import { useTranslations } from 'next-intl' import { useProfilePictureUpload } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/hooks/use-profile-picture-upload' import { useBrandConfig } from '@/ee/whitelabeling' import { useGeneralSettings, useUpdateGeneralSetting } from '@/hooks/queries/general-settings' import { useUpdateUserProfile, useUserProfile } from '@/hooks/queries/user-profile' import { clearUserData } from '@/stores' +import LocaleSelector from '@/components/locale-selector' const logger = createLogger('General') @@ -120,6 +122,7 @@ interface GeneralProps { } export function General({ onOpenChange }: GeneralProps) { + const t = useTranslations() const router = useRouter() const brandConfig = useBrandConfig() const { data: session } = useSession() @@ -193,7 +196,9 @@ export function General({ onOpenChange }: GeneralProps) { }) .catch(() => { setUploadError( - url ? 'Failed to update profile picture' : 'Failed to remove profile picture' + url + ? t('settings.general.errors.failed_update_picture') + : t('settings.general.errors.failed_remove_picture') ) }) }, @@ -289,7 +294,7 @@ export function General({ onOpenChange }: GeneralProps) { }, 1500) } catch (error) { logger.error('Error resetting password:', error) - setResetPasswordError('Failed to send email') + setResetPasswordError(t('settings.general.reset_password.errors.failed_send')) setTimeout(() => { setResetPasswordError(null) @@ -448,7 +453,7 @@ export function General({ onOpenChange }: GeneralProps) { className='h-[12px] w-[12px] flex-shrink-0 p-0' onClick={handleUpdateName} disabled={updateProfile.isPending} - aria-label='Save name' + aria-label={t('settings.general.aria.save_name')} > @@ -460,7 +465,7 @@ export function General({ onOpenChange }: GeneralProps) { variant='ghost' className='h-[10.5px] w-[10.5px] flex-shrink-0 p-0' onClick={() => setIsEditingName(true)} - aria-label='Edit name' + aria-label={t('settings.general.aria.edit_name')} > @@ -473,7 +478,7 @@ export function General({ onOpenChange }: GeneralProps) { {uploadError &&

{uploadError}

}
- +
- +
- +
- +
- +
- +

- We use OpenTelemetry to collect anonymous usage data to improve Sim. You can opt-out at any - time. + {t('settings.general.telemetry_description')}

+
+ + +
+ {isTrainingEnabled && (
- + - + )} @@ -593,7 +606,7 @@ export function General({ onOpenChange }: GeneralProps) { variant='active' className='ml-auto' > - Home Page + {t('settings.general.buttons.home_page')} )}
@@ -601,12 +614,14 @@ export function General({ onOpenChange }: GeneralProps) { {/* Password Reset Confirmation Modal */} - Reset Password + {t('settings.general.reset_password.title')}

- A password reset link will be sent to{' '} - {profile?.email}. - Click the link in the email to create a new password. + {t.rich('settings.general.reset_password.description', { + email: () => ( + {profile?.email} + ), + })}

{resetPasswordError && (

{resetPasswordError}

@@ -617,7 +632,7 @@ export function General({ onOpenChange }: GeneralProps) { onClick={() => setShowResetPasswordModal(false)} disabled={isResettingPassword || resetPasswordSuccess} > - Cancel + {t('settings.general.reset_password.cancel')}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/form-field/form-field.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/form-field/form-field.tsx index 2b3c2a1f68e..0233fd1cdd3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/form-field/form-field.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/form-field/form-field.tsx @@ -1,3 +1,4 @@ +import { useTranslations } from 'next-intl' import { Label } from '@/components/emcn' interface FormFieldProps { @@ -7,12 +8,16 @@ interface FormFieldProps { } export function FormField({ label, children, optional }: FormFieldProps) { + const t = useTranslations() + return (
{children}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/skills/components/skill-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/skills/components/skill-modal.tsx index 190b823203a..e0f41212d61 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/skills/components/skill-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/skills/components/skill-modal.tsx @@ -2,6 +2,7 @@ import type { ChangeEvent } from 'react' import { useMemo, useState } from 'react' +import { useTranslations } from 'next-intl' import { useParams } from 'next/navigation' import { Button, @@ -41,6 +42,7 @@ export function SkillModal({ onDelete, initialValues, }: SkillModalProps) { + const t = useTranslations() const params = useParams() const workspaceId = params.workspaceId as string @@ -77,19 +79,19 @@ export function SkillModal({ const newErrors: FieldErrors = {} if (!name.trim()) { - newErrors.name = 'Name is required' + newErrors.name = t('settings.skill_modal.errors.name_required') } else if (name.length > 64) { - newErrors.name = 'Name must be 64 characters or less' + newErrors.name = t('settings.skill_modal.errors.name_too_long') } else if (!KEBAB_CASE_REGEX.test(name)) { - newErrors.name = 'Name must be kebab-case (e.g. my-skill)' + newErrors.name = t('settings.skill_modal.errors.name_format') } if (!description.trim()) { - newErrors.description = 'Description is required' + newErrors.description = t('settings.skill_modal.errors.description_required') } if (!content.trim()) { - newErrors.content = 'Content is required' + newErrors.content = t('settings.skill_modal.errors.content_required') } if (Object.keys(newErrors).length > 0) { @@ -117,7 +119,7 @@ export function SkillModal({ const message = error instanceof Error && error.message.includes('already exists') ? error.message - : 'Failed to save skill. Please try again.' + : t('settings.skill_modal.errors.save_failed') setErrors({ general: message }) } finally { setSaving(false) @@ -127,16 +129,20 @@ export function SkillModal({ return ( - {initialValues ? 'Edit Skill' : 'Create Skill'} + + {initialValues + ? t('settings.skill_modal.title_edit') + : t('settings.skill_modal.title_create')} +
{ setName(e.target.value) @@ -148,18 +154,18 @@ export function SkillModal({

{errors.name}

) : ( - Lowercase letters, numbers, and hyphens (e.g. my-skill) + {t('settings.skill_modal.hints.name_format')} )}
{ setDescription(e.target.value) @@ -175,11 +181,11 @@ export function SkillModal({