diff --git a/app/(auth)/sign-in.tsx b/app/(auth)/sign-in.tsx index 9b200d9..0197d84 100644 --- a/app/(auth)/sign-in.tsx +++ b/app/(auth)/sign-in.tsx @@ -9,16 +9,18 @@ import { } from "react-native"; import { Link, useRouter } from "expo-router"; import { SafeAreaView } from "react-native-safe-area-context"; +import { useTranslation } from "react-i18next"; import { Boxes, Eye, EyeOff } from "lucide-react-native"; import { Button } from "~/components/ui/Button"; import { Input } from "~/components/ui/Input"; import { authClient } from "~/lib/auth-client"; import { useServerStore } from "~/stores/server-store"; -const SSO_PROVIDER_LABELS: Record = { - google: "Continue with Google", - apple: "Continue with Apple", - github: "Continue with GitHub", +/** Brand names for SSO providers — not translated; only the surrounding copy is. */ +const SSO_PROVIDER_NAMES: Record = { + google: "Google", + apple: "Apple", + github: "GitHub", }; const PLATFORM_RESTRICTED: Record = { @@ -27,6 +29,7 @@ const PLATFORM_RESTRICTED: Record = { export default function SignInScreen() { const router = useRouter(); + const { t } = useTranslation(); const ssoProviders = useServerStore((s) => s.ssoProviders); const [email, setEmail] = React.useState(""); const [password, setPassword] = React.useState(""); @@ -36,7 +39,7 @@ export default function SignInScreen() { const handleSignIn = async () => { if (!email || !password) { - setErrorMsg("Please enter your email and password."); + setErrorMsg(t("auth.errEmailPassword")); return; } setErrorMsg(null); @@ -47,12 +50,12 @@ export default function SignInScreen() { password, }); if (error) { - setErrorMsg(error.message ?? "Sign in failed. Please try again."); + setErrorMsg(error.message ?? t("auth.errSignInFailed")); } else { router.replace("/(tabs)"); } } catch { - setErrorMsg("Something went wrong. Please try again."); + setErrorMsg(t("auth.errGeneric")); } finally { setLoading(false); } @@ -67,7 +70,7 @@ export default function SignInScreen() { callbackURL: "/(tabs)", }); } catch { - setErrorMsg("Something went wrong. Please try again."); + setErrorMsg(t("auth.errGeneric")); } finally { setLoading(false); } @@ -95,20 +98,20 @@ export default function SignInScreen() { - Welcome back + {t("auth.welcomeBack")} - Sign in to your account to continue. + {t("auth.signInSubtitle")} - Email + {t("auth.email")} - Password + {t("auth.password")} {showPassword ? ( @@ -163,7 +166,7 @@ export default function SignInScreen() { ) : null} @@ -171,7 +174,7 @@ export default function SignInScreen() { <> - or + {t("auth.or")} @@ -183,7 +186,9 @@ export default function SignInScreen() { onPress={() => handleSocialSignIn(id)} disabled={loading} > - {SSO_PROVIDER_LABELS[id] ?? `Continue with ${id}`} + {t("auth.continueWith", { + provider: SSO_PROVIDER_NAMES[id] ?? id, + })} ))} @@ -192,11 +197,11 @@ export default function SignInScreen() { - Don't have an account?{" "} + {t("auth.noAccount")}{" "} - Sign Up + {t("auth.signUp")} diff --git a/app/(auth)/sign-up.tsx b/app/(auth)/sign-up.tsx index ecf8e5d..d2763c2 100644 --- a/app/(auth)/sign-up.tsx +++ b/app/(auth)/sign-up.tsx @@ -9,16 +9,18 @@ import { } from "react-native"; import { Link, useRouter } from "expo-router"; import { SafeAreaView } from "react-native-safe-area-context"; +import { useTranslation } from "react-i18next"; import { Boxes, Eye, EyeOff } from "lucide-react-native"; import { Button } from "~/components/ui/Button"; import { Input } from "~/components/ui/Input"; import { authClient } from "~/lib/auth-client"; import { useServerStore } from "~/stores/server-store"; -const SSO_PROVIDER_LABELS: Record = { - google: "Continue with Google", - apple: "Continue with Apple", - github: "Continue with GitHub", +/** Brand names for SSO providers — not translated; only the surrounding copy is. */ +const SSO_PROVIDER_NAMES: Record = { + google: "Google", + apple: "Apple", + github: "GitHub", }; const PLATFORM_RESTRICTED: Record = { @@ -27,6 +29,7 @@ const PLATFORM_RESTRICTED: Record = { export default function SignUpScreen() { const router = useRouter(); + const { t } = useTranslation(); const ssoProviders = useServerStore((s) => s.ssoProviders); const [name, setName] = React.useState(""); const [email, setEmail] = React.useState(""); @@ -37,11 +40,11 @@ export default function SignUpScreen() { const handleSignUp = async () => { if (!name || !email || !password) { - setErrorMsg("Please fill in all fields."); + setErrorMsg(t("auth.errAllFields")); return; } if (password.length < 8) { - setErrorMsg("Password must be at least 8 characters."); + setErrorMsg(t("auth.errPasswordLength")); return; } setErrorMsg(null); @@ -53,12 +56,12 @@ export default function SignUpScreen() { password, }); if (error) { - setErrorMsg(error.message ?? "Sign up failed. Please try again."); + setErrorMsg(error.message ?? t("auth.errSignUpFailed")); } else { router.replace("/(tabs)"); } } catch { - setErrorMsg("Something went wrong. Please try again."); + setErrorMsg(t("auth.errGeneric")); } finally { setLoading(false); } @@ -73,7 +76,7 @@ export default function SignUpScreen() { callbackURL: "/(tabs)", }); } catch { - setErrorMsg("Something went wrong. Please try again."); + setErrorMsg(t("auth.errGeneric")); } finally { setLoading(false); } @@ -101,20 +104,20 @@ export default function SignUpScreen() { - Create account + {t("auth.createAccountTitle")} - Sign up to get started with ObjectStack. + {t("auth.signUpSubtitle")} - Full Name + {t("auth.fullName")} - Email + {t("auth.email")} - Password + {t("auth.password")} {showPassword ? ( @@ -185,7 +188,7 @@ export default function SignUpScreen() { ) : null} @@ -193,7 +196,7 @@ export default function SignUpScreen() { <> - or + {t("auth.or")} @@ -205,7 +208,9 @@ export default function SignUpScreen() { onPress={() => handleSocialSignIn(id)} disabled={loading} > - {SSO_PROVIDER_LABELS[id] ?? `Continue with ${id}`} + {t("auth.continueWith", { + provider: SSO_PROVIDER_NAMES[id] ?? id, + })} ))} @@ -214,11 +219,11 @@ export default function SignUpScreen() { - Already have an account?{" "} + {t("auth.haveAccount")}{" "} - Sign In + {t("auth.signIn")} diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx index 226e554..f16c80e 100644 --- a/app/(tabs)/profile.tsx +++ b/app/(tabs)/profile.tsx @@ -3,6 +3,7 @@ import { SafeAreaView } from "react-native-safe-area-context"; import { webContentMaxWidth } from "~/lib/responsive"; import { UserCircle } from "lucide-react-native"; import { useRouter } from "expo-router"; +import { useTranslation } from "react-i18next"; import { Button } from "~/components/ui/Button"; import { authClient } from "~/lib/auth-client"; import { useToast } from "~/components/ui/Toast"; @@ -11,6 +12,7 @@ import { useConfirm } from "~/components/ui/ConfirmDialog"; export default function ProfileScreen() { const { data: session } = authClient.useSession(); const router = useRouter(); + const { t } = useTranslation(); const { toastError } = useToast(); const confirm = useConfirm(); @@ -19,15 +21,15 @@ export default function ProfileScreen() { await authClient.signOut(); router.replace("/(auth)/sign-in"); } catch { - toastError("Failed to sign out. Please try again."); + toastError(t("more.signOutFailed")); } }; const handleSignOut = async () => { const ok = await confirm({ - title: "Sign Out", - message: "Are you sure you want to sign out?", - confirmLabel: "Sign Out", + title: t("more.signOutTitle"), + message: t("more.signOutConfirm"), + confirmLabel: t("more.signOutTitle"), destructive: true, }); if (ok) void performSignOut(); @@ -45,7 +47,7 @@ export default function ProfileScreen() { - {session?.user.name ?? "User"} + {session?.user.name ?? t("more.profileFallback")} {session?.user.email ?? ""} @@ -53,11 +55,11 @@ export default function ProfileScreen() { - - - + + + diff --git a/app/account.tsx b/app/account.tsx index a6495ba..91f954f 100644 --- a/app/account.tsx +++ b/app/account.tsx @@ -64,13 +64,13 @@ export default function AccountScreen() { const { toastSuccess, toastError } = useToast(); const notify = (msg: string) => toastSuccess(msg); const fail = (e: unknown) => - toastError(e instanceof Error ? e.message : "Something went wrong"); + toastError(e instanceof Error ? e.message : t("account.genericError")); const onSaveName = async () => { if (!name.trim()) return; try { await updateProfile(name.trim()); - notify("Profile updated."); + notify(t("account.profileUpdated")); } catch (e) { fail(e); } @@ -81,7 +81,7 @@ export default function AccountScreen() { try { await changeEmail(newEmail.trim()); setNewEmail(""); - notify("Verification sent to the new address. The change applies once you confirm it."); + notify(t("account.emailChangeSent")); } catch (e) { fail(e); } @@ -91,7 +91,7 @@ export default function AccountScreen() { if (!user?.email) return; try { await resendVerification(user.email); - notify("Verification email sent."); + notify(t("account.verificationSent")); } catch (e) { fail(e); } @@ -100,11 +100,11 @@ export default function AccountScreen() { const onChangePassword = async () => { if (!currentPassword || !newPassword) return; if (newPassword.length < 8) { - notify("New password must be at least 8 characters."); + notify(t("account.passwordTooShort")); return; } if (newPassword !== confirmPassword) { - notify("New password and confirmation do not match."); + notify(t("account.passwordMismatch")); return; } try { @@ -112,7 +112,7 @@ export default function AccountScreen() { setCurrentPassword(""); setNewPassword(""); setConfirmPassword(""); - notify("Password changed. Other sessions were signed out."); + notify(t("account.passwordChanged")); } catch (e) { fail(e); } @@ -137,7 +137,7 @@ export default function AccountScreen() { setTfCode(""); setTfUri(null); setTfBackupCodes([]); - notify("Two-factor authentication is now enabled."); + notify(t("account.twoFaEnabledToast")); } catch (e) { fail(e); } @@ -148,7 +148,7 @@ export default function AccountScreen() { try { await twoFactor.disable(tfPassword); setTfPassword(""); - notify("Two-factor authentication disabled."); + notify(t("account.twoFaDisabledToast")); } catch (e) { fail(e); } @@ -162,26 +162,27 @@ export default function AccountScreen() { -
+
-
+
- Current: {user?.email ?? "—"} - {user?.emailVerified === false ? " (unverified)" : ""} + {t("account.currentColon")}{" "} + {user?.email ?? "—"} + {user?.emailVerified === false ? t("account.unverifiedSuffix") : ""}
-
+
-
+
{twoFactorEnabled ? ( <> - Two-factor authentication is enabled. + {t("account.twoFaStatusEnabled")} ) : tfUri ? ( <> - Add this secret to your authenticator app, then enter the 6-digit code to confirm. + {t("account.setupInstructions")} {!!totpSecret && ( - Secret + + {t("account.secret")} + {totpSecret} @@ -260,7 +263,7 @@ export default function AccountScreen() { {tfBackupCodes.length > 0 && ( - Backup codes (save these) + {t("account.backupCodes")} {tfBackupCodes.join("\n")} @@ -268,7 +271,7 @@ export default function AccountScreen() { )} ) : ( <> - Protect your account with a time-based one-time code (TOTP). + {t("account.twoFaPromo")} )} diff --git a/locales/ar.json b/locales/ar.json index 1b6832d..4f4fe01 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -35,7 +35,71 @@ "email": "البريد الإلكتروني", "password": "كلمة المرور", "forgotPassword": "هل نسيت كلمة المرور؟", - "sessionExpired": "انتهت صلاحية الجلسة. يرجى تسجيل الدخول مرة أخرى." + "sessionExpired": "انتهت صلاحية الجلسة. يرجى تسجيل الدخول مرة أخرى.", + "welcomeBack": "مرحبًا بعودتك", + "signInSubtitle": "سجّل الدخول إلى حسابك للمتابعة.", + "createAccountTitle": "إنشاء حساب", + "signUpSubtitle": "سجّل لتبدأ استخدام ObjectStack.", + "fullName": "الاسم الكامل", + "namePlaceholder": "محمد أحمد", + "emailPlaceholder": "you@company.com", + "passwordPlaceholder": "أدخل كلمة المرور", + "passwordHint": "8 أحرف على الأقل", + "showPassword": "إظهار كلمة المرور", + "hidePassword": "إخفاء كلمة المرور", + "signingIn": "جارٍ تسجيل الدخول…", + "creatingAccount": "جارٍ إنشاء الحساب…", + "createAccountCta": "إنشاء حساب", + "continueWith": "المتابعة باستخدام {{provider}}", + "or": "أو", + "noAccount": "ليس لديك حساب؟", + "haveAccount": "لديك حساب بالفعل؟", + "errEmailPassword": "يرجى إدخال البريد الإلكتروني وكلمة المرور.", + "errAllFields": "يرجى ملء جميع الحقول.", + "errPasswordLength": "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل.", + "errSignInFailed": "فشل تسجيل الدخول. يرجى المحاولة مرة أخرى.", + "errSignUpFailed": "فشل إنشاء الحساب. يرجى المحاولة مرة أخرى.", + "errGeneric": "حدث خطأ ما. يرجى المحاولة مرة أخرى." + }, + "account": { + "sectionProfile": "الملف الشخصي", + "name": "الاسم", + "namePlaceholder": "اسمك", + "saveProfile": "حفظ الملف", + "profileUpdated": "تم تحديث الملف الشخصي.", + "sectionEmail": "البريد الإلكتروني", + "currentColon": "الحالي:", + "unverifiedSuffix": " (غير مُوثَّق)", + "newEmail": "بريد إلكتروني جديد", + "changeEmail": "تغيير البريد الإلكتروني", + "resendVerification": "إعادة إرسال التحقق", + "emailChangeSent": "تم إرسال التحقق إلى العنوان الجديد. يُطبَّق التغيير بعد تأكيدك له.", + "verificationSent": "تم إرسال بريد التحقق.", + "sectionPassword": "كلمة المرور", + "currentPassword": "كلمة المرور الحالية", + "newPassword": "كلمة المرور الجديدة", + "passwordHint": "8 أحرف على الأقل", + "confirmPassword": "تأكيد كلمة المرور الجديدة", + "confirmPlaceholder": "أعد إدخال كلمة المرور الجديدة", + "changePassword": "تغيير كلمة المرور", + "passwordTooShort": "يجب أن تتكون كلمة المرور الجديدة من 8 أحرف على الأقل.", + "passwordMismatch": "كلمة المرور الجديدة والتأكيد غير متطابقين.", + "passwordChanged": "تم تغيير كلمة المرور. تم تسجيل خروج الجلسات الأخرى.", + "section2fa": "المصادقة الثنائية", + "twoFaStatusEnabled": "المصادقة الثنائية مُفعَّلة.", + "passwordToDisable": "كلمة المرور (للتعطيل)", + "disable2fa": "تعطيل المصادقة الثنائية", + "setupInstructions": "أضف هذا المفتاح إلى تطبيق المصادقة، ثم أدخل الرمز المكوَّن من 6 أرقام للتأكيد.", + "secret": "المفتاح", + "backupCodes": "رموز احتياطية (احفظها)", + "code6": "رمز من 6 أرقام", + "confirmEnable": "تأكيد وتفعيل", + "twoFaPromo": "احمِ حسابك برمز لمرة واحدة يعتمد على الوقت (TOTP).", + "passwordToEnable": "كلمة المرور (للتفعيل)", + "enable2fa": "تفعيل المصادقة الثنائية", + "twoFaEnabledToast": "تم تفعيل المصادقة الثنائية الآن.", + "twoFaDisabledToast": "تم تعطيل المصادقة الثنائية.", + "genericError": "حدث خطأ ما" }, "errors": { "UNAUTHORIZED": "انتهت صلاحية الجلسة. يرجى تسجيل الدخول مرة أخرى.", @@ -201,7 +265,11 @@ "flows": "التدفقات", "language": "اللغة", "signOut": "تسجيل الخروج", - "appearance": "المظهر" + "appearance": "المظهر", + "editProfile": "تعديل الملف الشخصي", + "settings": "الإعدادات", + "helpSupport": "المساعدة والدعم", + "profileFallback": "مستخدم" }, "ai": { "title": "المساعد الذكي", diff --git a/locales/en.json b/locales/en.json index 10d2b0a..431c970 100644 --- a/locales/en.json +++ b/locales/en.json @@ -35,7 +35,71 @@ "email": "Email", "password": "Password", "forgotPassword": "Forgot Password?", - "sessionExpired": "Your session has expired. Please sign in again." + "sessionExpired": "Your session has expired. Please sign in again.", + "welcomeBack": "Welcome back", + "signInSubtitle": "Sign in to your account to continue.", + "createAccountTitle": "Create account", + "signUpSubtitle": "Sign up to get started with ObjectStack.", + "fullName": "Full Name", + "namePlaceholder": "John Doe", + "emailPlaceholder": "you@company.com", + "passwordPlaceholder": "Enter your password", + "passwordHint": "At least 8 characters", + "showPassword": "Show password", + "hidePassword": "Hide password", + "signingIn": "Signing in…", + "creatingAccount": "Creating account…", + "createAccountCta": "Create Account", + "continueWith": "Continue with {{provider}}", + "or": "or", + "noAccount": "Don't have an account?", + "haveAccount": "Already have an account?", + "errEmailPassword": "Please enter your email and password.", + "errAllFields": "Please fill in all fields.", + "errPasswordLength": "Password must be at least 8 characters.", + "errSignInFailed": "Sign in failed. Please try again.", + "errSignUpFailed": "Sign up failed. Please try again.", + "errGeneric": "Something went wrong. Please try again." + }, + "account": { + "sectionProfile": "Profile", + "name": "Name", + "namePlaceholder": "Your name", + "saveProfile": "Save profile", + "profileUpdated": "Profile updated.", + "sectionEmail": "Email", + "currentColon": "Current:", + "unverifiedSuffix": " (unverified)", + "newEmail": "New email", + "changeEmail": "Change email", + "resendVerification": "Resend verification", + "emailChangeSent": "Verification sent to the new address. The change applies once you confirm it.", + "verificationSent": "Verification email sent.", + "sectionPassword": "Password", + "currentPassword": "Current password", + "newPassword": "New password", + "passwordHint": "At least 8 characters", + "confirmPassword": "Confirm new password", + "confirmPlaceholder": "Re-enter new password", + "changePassword": "Change password", + "passwordTooShort": "New password must be at least 8 characters.", + "passwordMismatch": "New password and confirmation do not match.", + "passwordChanged": "Password changed. Other sessions were signed out.", + "section2fa": "Two-Factor Authentication", + "twoFaStatusEnabled": "Two-factor authentication is enabled.", + "passwordToDisable": "Password (to disable)", + "disable2fa": "Disable 2FA", + "setupInstructions": "Add this secret to your authenticator app, then enter the 6-digit code to confirm.", + "secret": "Secret", + "backupCodes": "Backup codes (save these)", + "code6": "6-digit code", + "confirmEnable": "Confirm & enable", + "twoFaPromo": "Protect your account with a time-based one-time code (TOTP).", + "passwordToEnable": "Password (to enable)", + "enable2fa": "Enable 2FA", + "twoFaEnabledToast": "Two-factor authentication is now enabled.", + "twoFaDisabledToast": "Two-factor authentication disabled.", + "genericError": "Something went wrong" }, "errors": { "UNAUTHORIZED": "Your session has expired. Please sign in again.", @@ -198,7 +262,11 @@ "flows": "Flows", "language": "Language", "signOut": "Sign Out", - "appearance": "Appearance" + "appearance": "Appearance", + "editProfile": "Edit Profile", + "settings": "Settings", + "helpSupport": "Help & Support", + "profileFallback": "User" }, "ai": { "title": "AI Assistant", diff --git a/locales/zh.json b/locales/zh.json index a30875f..fd927df 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -35,7 +35,71 @@ "email": "邮箱", "password": "密码", "forgotPassword": "忘记密码?", - "sessionExpired": "会话已过期,请重新登录。" + "sessionExpired": "会话已过期,请重新登录。", + "welcomeBack": "欢迎回来", + "signInSubtitle": "登录你的账号以继续。", + "createAccountTitle": "创建账号", + "signUpSubtitle": "注册以开始使用 ObjectStack。", + "fullName": "姓名", + "namePlaceholder": "张三", + "emailPlaceholder": "you@company.com", + "passwordPlaceholder": "输入密码", + "passwordHint": "至少 8 个字符", + "showPassword": "显示密码", + "hidePassword": "隐藏密码", + "signingIn": "正在登录…", + "creatingAccount": "正在创建账号…", + "createAccountCta": "创建账号", + "continueWith": "使用 {{provider}} 继续", + "or": "或", + "noAccount": "还没有账号?", + "haveAccount": "已有账号?", + "errEmailPassword": "请输入邮箱和密码。", + "errAllFields": "请填写所有字段。", + "errPasswordLength": "密码至少需要 8 个字符。", + "errSignInFailed": "登录失败,请重试。", + "errSignUpFailed": "注册失败,请重试。", + "errGeneric": "出错了,请重试。" + }, + "account": { + "sectionProfile": "个人资料", + "name": "姓名", + "namePlaceholder": "你的姓名", + "saveProfile": "保存资料", + "profileUpdated": "资料已更新。", + "sectionEmail": "邮箱", + "currentColon": "当前:", + "unverifiedSuffix": "(未验证)", + "newEmail": "新邮箱", + "changeEmail": "更改邮箱", + "resendVerification": "重新发送验证邮件", + "emailChangeSent": "验证邮件已发送至新地址,确认后更改才会生效。", + "verificationSent": "验证邮件已发送。", + "sectionPassword": "密码", + "currentPassword": "当前密码", + "newPassword": "新密码", + "passwordHint": "至少 8 个字符", + "confirmPassword": "确认新密码", + "confirmPlaceholder": "再次输入新密码", + "changePassword": "更改密码", + "passwordTooShort": "新密码至少需要 8 个字符。", + "passwordMismatch": "两次输入的新密码不一致。", + "passwordChanged": "密码已更改,其他会话已退出登录。", + "section2fa": "两步验证", + "twoFaStatusEnabled": "两步验证已启用。", + "passwordToDisable": "密码(用于停用)", + "disable2fa": "停用两步验证", + "setupInstructions": "将此密钥添加到你的验证器应用,然后输入 6 位验证码以确认。", + "secret": "密钥", + "backupCodes": "备用码(请妥善保存)", + "code6": "6 位验证码", + "confirmEnable": "确认并启用", + "twoFaPromo": "使用基于时间的一次性验证码(TOTP)保护你的账号。", + "passwordToEnable": "密码(用于启用)", + "enable2fa": "启用两步验证", + "twoFaEnabledToast": "两步验证已启用。", + "twoFaDisabledToast": "两步验证已停用。", + "genericError": "出错了" }, "errors": { "UNAUTHORIZED": "会话已过期,请重新登录。", @@ -196,7 +260,11 @@ "flows": "流程", "language": "语言", "signOut": "退出登录", - "appearance": "外观" + "appearance": "外观", + "editProfile": "编辑资料", + "settings": "设置", + "helpSupport": "帮助与支持", + "profileFallback": "用户" }, "ai": { "title": "AI 助手",