diff --git a/.claude/rules/sim-translations.md b/.claude/rules/sim-translations.md new file mode 100644 index 00000000000..4437690027e --- /dev/null +++ b/.claude/rules/sim-translations.md @@ -0,0 +1,90 @@ +--- +paths: + - "apps/sim/**/*.tsx" + - "apps/sim/**/*.ts" + - "apps/sim/**/*.json" +description: Automatically refactor React/Next.js components to use next-intl without namespaces +--- + +# 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 + +Do not include explanations. + +--- +## Rules +1. No duplicate keys in translation files +2. Don't translate logger info or error messages that are not user-facing. +3. 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. +4. Detect Component Type (Client vs Server) and use the appropriate next-intl functions (useTranslations for Client, getTranslations for Server). +5. No namespaces in keys +6. You MUST replace every user-visible or accessibility string. +7. If a component is rendering rich text like or , you must use the rich text formatting capabilities of next-intl. +eg. +``` +{ + "message": "Please refer to the guidelines." +} +// Returns `<>Please refer to the guidelines.` +t.rich('message', { + guidelines: (chunks) => {chunks} +}); +``` +8. All the translations must be applied in en.json, es.json and pt.json in the translations folder. Find the right place to change the translations file to reuse existing translations and avoid duplicates. If there is no right place, create a new key following the Key Naming Rules. + +## What Must NOT Be Translated + +Do NOT translate: + +- className +- variable names +- function names +- API field names +- route paths (/login, /workspace) +- console logs (unless rendered in UI) +- error codes +- environment variable names +- regex patterns +- non-user-facing constants + +## Key Naming Rules + +All keys must be: + +- snake_case +- descriptive +- stable + +Keys must be grouped semantically: + +- title +- subtitle +- description +- labels.* +- placeholders.* +- buttons.* +- links.* +- aria.* +- errors.* +- helper_text.* +- loading.* + +Examples: + +t('title') +t('labels.email') +t('placeholders.password') +t('buttons.create_account') +t('buttons.loading') +t('aria.show_password') +t('errors.invalid_email') + diff --git a/apps/sim/.gitignore b/apps/sim/.gitignore index e90bb4dc00d..cda9d984023 100644 --- a/apps/sim/.gitignore +++ b/apps/sim/.gitignore @@ -45,4 +45,5 @@ next-env.d.ts # Uploads /uploads -.trigger \ No newline at end of file +.trigger +certificates \ No newline at end of file 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.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)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index c58b102bc54..15fbf3b35e5 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -25,36 +25,11 @@ 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' +import LocaleSelector from '@/components/locale-selector' 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('/')) { @@ -73,20 +48,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({ @@ -98,6 +127,10 @@ export default function LoginPage({ googleAvailable: boolean 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) @@ -142,7 +175,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]) @@ -227,41 +260,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) @@ -275,7 +304,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) } @@ -304,7 +333,7 @@ export default function LoginPage({ if (!forgotPasswordEmail) { setResetStatus({ type: 'error', - message: 'Please enter your email address', + message: resetMessages.enterEmail, }) return } @@ -313,7 +342,7 @@ export default function LoginPage({ if (!emailValidation.isValid) { setResetStatus({ type: 'error', - message: 'Please enter a valid email address', + message: resetMessages.invalidEmail, }) return } @@ -335,20 +364,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) @@ -356,7 +385,7 @@ export default function LoginPage({ setResetStatus({ type: 'success', - message: 'Password reset link sent to your email', + message: resetMessages.success, }) setTimeout(() => { @@ -367,7 +396,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) @@ -386,10 +415,10 @@ export default function LoginPage({ <>

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

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

@@ -417,12 +446,12 @@ export default function LoginPage({
- +
- +
@@ -464,7 +493,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( @@ -478,7 +507,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 ? : } @@ -497,9 +528,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')} )} @@ -511,7 +542,9 @@ export default function LoginPage({
- Or continue with + + {t('sign_in.divider_label')} +
)} @@ -538,12 +571,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')}
)} @@ -551,47 +584,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( @@ -616,9 +652,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/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 bab806f23a2..b4bc6f4727f 100644 --- a/apps/sim/app/(auth)/signup/signup-form.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.tsx @@ -5,73 +5,25 @@ import { createLogger } from '@sim/logger' import { Eye, EyeOff } from 'lucide-react' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' +import { useTranslations } from 'next-intl' +import LocaleSelector from '@/components/locale-selector' import { Input } from '@/components/ui/input' 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' import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons' import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button' +import { useNameValidations } from '@/hooks/sign-up/use-name-validations' +import { usePasswordValidations } from '@/hooks/sign-up/use-password-validations' +import { useValidateEmail } from '@/hooks/sign-up/use-validate-email' import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' 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, @@ -81,6 +33,7 @@ function SignupFormContent({ googleAvailable: boolean isProduction: boolean }) { + const t = useTranslations() const router = useRouter() const searchParams = useSearchParams() const { refetch: refetchSession } = useSession() @@ -98,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) @@ -257,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 @@ -274,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 { @@ -345,10 +300,10 @@ function SignupFormContent({ <>

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

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

@@ -375,16 +330,16 @@ function SignupFormContent({
- +
- +
- +
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 ? : } @@ -479,9 +436,9 @@ function SignupFormContent({ type='submit' disabled={isLoading} loading={isLoading} - loadingText='Creating account' + loadingText={t('sign_up.creating_account')} > - Create account + {t('sign_up.create_account')} )} @@ -501,7 +458,9 @@ function SignupFormContent({
- Or continue with + + {t('sign_up.divider_label')} +
)} @@ -538,36 +497,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} + + ), + })} +
) @@ -582,9 +546,13 @@ export default function SignupPage({ googleAvailable: boolean isProduction: boolean }) { + const t = useTranslations() + return ( Loading...
} + fallback={ +
{t('generic.loading')}...
+ } > - - - - - - {children} - - - - - + + + + + + + {children} + + + + + + ) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx index a213b7431b4..fef802fcbea 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx @@ -1,5 +1,6 @@ 'use client' +import { useTranslations } from 'next-intl' import { useCallback, useState } from 'react' import { useParams, useRouter } from 'next/navigation' import { Badge, DocumentAttachment, Tooltip } from '@/components/emcn' @@ -78,6 +79,7 @@ export function BaseCard({ onUpdate, onDelete, }: BaseCardProps) { + const t = useTranslations('knowledge') const params = useParams() const router = useRouter() const workspaceId = params?.workspaceId as string @@ -184,13 +186,13 @@ export function BaseCard({
- {docCount} {docCount === 1 ? 'doc' : 'docs'} + {docCount} {docCount === 1 ? t('document_count.singular') : t('document_count.plural')} {updatedAt && ( - last updated: {formatRelativeTime(updatedAt)} + {t('labels.last_updated')} {formatRelativeTime(updatedAt)} {formatAbsoluteDate(updatedAt)} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/constants.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/components/constants.ts index aa5b7618fe1..192ac319f8c 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/constants.ts +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/constants.ts @@ -10,11 +10,11 @@ export type SortOption = 'name' | 'createdAt' | 'updatedAt' | 'docCount' export type SortOrder = 'asc' | 'desc' export const SORT_OPTIONS = [ - { value: 'updatedAt-desc', label: 'Last Updated' }, - { value: 'createdAt-desc', label: 'Newest First' }, - { value: 'createdAt-asc', label: 'Oldest First' }, - { value: 'name-asc', label: 'Name (A-Z)' }, - { value: 'name-desc', label: 'Name (Z-A)' }, - { value: 'docCount-desc', label: 'Most Documents' }, - { value: 'docCount-asc', label: 'Least Documents' }, + { value: 'updatedAt-desc', label: 'sort_options.last_updated' }, + { value: 'createdAt-desc', label: 'sort_options.newest_first' }, + { value: 'createdAt-asc', label: 'sort_options.oldest_first' }, + { value: 'name-asc', label: 'sort_options.name_asc' }, + { value: 'name-desc', label: 'sort_options.name_desc' }, + { value: 'docCount-desc', label: 'sort_options.most_docs' }, + { value: 'docCount-asc', label: 'sort_options.least_docs' }, ] as const diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx index 70419c82119..33ebc0c5bfe 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx @@ -1,5 +1,6 @@ 'use client' +import { useTranslations } from 'next-intl' import { useEffect, useRef, useState } from 'react' import { zodResolver } from '@hookform/resolvers/zod' import { createLogger } from '@sim/logger' @@ -79,6 +80,7 @@ interface SubmitStatus { } export function CreateBaseModal({ open, onOpenChange }: CreateBaseModalProps) { + const t = useTranslations('knowledge') const params = useParams() const workspaceId = params.workspaceId as string @@ -304,14 +306,14 @@ export function CreateBaseModal({ open, onOpenChange }: CreateBaseModalProps) { return ( - Create Knowledge Base + {t('modals.create_title')}
- + {/* Hidden decoy fields to prevent browser autofill */}
- +