diff --git a/apps/web/app/components/tenant-gate.tsx b/apps/web/app/components/tenant-gate.tsx
index 4f847b6..4577bfe 100644
--- a/apps/web/app/components/tenant-gate.tsx
+++ b/apps/web/app/components/tenant-gate.tsx
@@ -3,15 +3,10 @@
import { useEffect, useRef } from "react";
import { trpc } from "@repo/trpc/client";
import { useAuthClient } from "../lib/auth/auth-client";
+import { useI18n } from "../hooks/useI18n";
-/**
- * After auth, loads the user's tenants via myTenants (auth-only tier — no tenant required).
- * If the user has tenants but none selected, auto-selects the first one before rendering children.
- * If the user has no tenants and is a Super Admin, lets them through (so they can create tenants).
- * If the user has no tenants and is NOT a Super Admin, shows a message.
- * If the user is NOT authenticated, passes through (AuthGate handles that).
- */
export function TenantGate({ children }: { children: React.ReactNode }) {
+ const { LL } = useI18n();
const authClient = useAuthClient();
const sessionResult = authClient?.useSession?.();
const session = sessionResult?.data;
@@ -65,7 +60,6 @@ export function TenantGate({ children }: { children: React.ReactNode }) {
}
}, [tenants, selectedTenantId, switchTenant]);
- // Not authenticated — let AuthGate handle it; don't block on tenants.
if (!isAuthenticated) {
return <>{children}>;
}
@@ -78,7 +72,7 @@ export function TenantGate({ children }: { children: React.ReactNode }) {
className="mx-auto h-10 w-10 animate-spin rounded-full border-2 border-b-blue-400 border-transparent"
aria-hidden
/>
-
Loading your tenants...
+
{LL.Errors.loadingYourTenants()}
);
@@ -108,11 +102,10 @@ export function TenantGate({ children }: { children: React.ReactNode }) {
- No tenants assigned
+ {LL.Errors.noTenantsAssigned()}
- You don't have access to any tenants yet. Please contact an
- administrator to get added to a tenant.
+ {LL.Errors.noTenantsMessage()}
@@ -128,7 +121,7 @@ export function TenantGate({ children }: { children: React.ReactNode }) {
aria-hidden
/>
- Setting up your tenant...
+ {LL.Errors.settingUpTenant()}
- Don't have an account?{" "}
+ {LL.Auth.dontHaveAccount()}{" "}
- Sign up
+ {LL.Auth.signUp()}
diff --git a/apps/web/app/sign-up/page.tsx b/apps/web/app/sign-up/page.tsx
index 028bdc9..7fa86c8 100644
--- a/apps/web/app/sign-up/page.tsx
+++ b/apps/web/app/sign-up/page.tsx
@@ -4,41 +4,47 @@ import Link from "next/link";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useAuthClient } from "../lib/auth/auth-client";
+import { useI18n } from "../hooks/useI18n";
+import type { TranslationFunctions } from "../../i18n/i18n-types";
const NAME_REGEX = /^[a-zA-Z\s'-]+$/;
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
-function validateName(value: string, label: string): string | null {
+function validateName(
+ value: string,
+ label: string,
+ E: TranslationFunctions["Errors"],
+): string | null {
const trimmed = value.trim();
- if (!trimmed) return `${label} is required.`;
- if (trimmed.length < 2) return `${label} must be at least 2 characters.`;
- if (!NAME_REGEX.test(trimmed))
- return `${label} can only contain letters, spaces, hyphens, and apostrophes.`;
+ if (!trimmed) return E.nameRequired({ label });
+ if (trimmed.length < 2) return E.nameMinLength({ label });
+ if (!NAME_REGEX.test(trimmed)) return E.nameInvalidChars({ label });
return null;
}
-function validateEmail(value: string): string | null {
+function validateEmail(value: string, E: TranslationFunctions["Errors"]): string | null {
const trimmed = value.trim();
- if (!trimmed) return "Email is required.";
- if (!EMAIL_REGEX.test(trimmed)) return "Please enter a valid email address.";
+ if (!trimmed) return E.emailRequired();
+ if (!EMAIL_REGEX.test(trimmed)) return E.emailInvalid();
return null;
}
-function validatePassword(value: string): string | null {
- if (!value) return "Password is required.";
- if (value.length < 8) return "Password must be at least 8 characters.";
- if (value.length > 128) return "Password must be at most 128 characters.";
- if (!/[A-Z]/.test(value))
- return "Password must contain at least one uppercase letter.";
- if (!/[a-z]/.test(value))
- return "Password must contain at least one lowercase letter.";
- if (!/[0-9]/.test(value)) return "Password must contain at least one number.";
- if (!/[^A-Za-z0-9]/.test(value))
- return "Password must contain at least one special character.";
+function validatePassword(
+ value: string,
+ E: TranslationFunctions["Errors"],
+): string | null {
+ if (!value) return E.passwordRequired();
+ if (value.length < 8) return E.passwordMinLength();
+ if (value.length > 128) return E.passwordMaxLength();
+ if (!/[A-Z]/.test(value)) return E.passwordUppercase();
+ if (!/[a-z]/.test(value)) return E.passwordLowercase();
+ if (!/[0-9]/.test(value)) return E.passwordNumber();
+ if (!/[^A-Za-z0-9]/.test(value)) return E.passwordSpecialChar();
return null;
}
export default function SignUpPage() {
+ const { LL } = useI18n();
const authClient = useAuthClient();
const router = useRouter();
const sessionResult = authClient?.useSession?.();
@@ -70,17 +76,18 @@ export default function SignUpPage() {
const validate = (): boolean => {
const errors: Record = {};
+ const E = LL.Errors;
- const firstErr = validateName(firstName, "First name");
+ const firstErr = validateName(firstName, LL.Forms.firstName(), E);
if (firstErr) errors.firstName = firstErr;
- const lastErr = validateName(lastName, "Last name");
+ const lastErr = validateName(lastName, LL.Forms.lastName(), E);
if (lastErr) errors.lastName = lastErr;
- const emailErr = validateEmail(email);
+ const emailErr = validateEmail(email, E);
if (emailErr) errors.email = emailErr;
- const pwErr = validatePassword(password);
+ const pwErr = validatePassword(password, E);
if (pwErr) errors.password = pwErr;
setFieldErrors(errors);
@@ -117,11 +124,9 @@ export default function SignUpPage() {
err.message?.toLowerCase().includes("user already exists") ||
err.message?.toLowerCase().includes("already in use")
) {
- setError(
- "An account with this email already exists. Try signing in instead — if you used Google or OTP, you can set a password from your profile.",
- );
+ setError(LL.Errors.accountAlreadyExists());
} else {
- setError(err.message ?? "Sign up failed. Please try again.");
+ setError(err.message ?? LL.Errors.signUpFailed());
}
return;
}
@@ -149,19 +154,16 @@ export default function SignUpPage() {
- Check your inbox
+ {LL.Auth.checkYourInbox()}
- We've sent a verification email to{" "}
- {email}. Please
- check your inbox and click the link to verify your account before
- signing in.
+ {LL.Auth.checkInboxMessage({ email })}
- Go to Sign In
+ {LL.Auth.goToSignIn()}
@@ -174,11 +176,9 @@ export default function SignUpPage() {
{errorParam === "token_expired"
- ? "This verification link has expired. Please try signing in again to receive a new verification email."
- : "This verification link is invalid. Please try signing in again to receive a new verification email."}
+ ? LL.Auth.verificationFailedExpired()
+ : LL.Auth.verificationFailedInvalid()}
- Go to Sign In
+ {LL.Auth.goToSignIn()}
@@ -82,16 +84,16 @@ function VerifyEmailContent() {
- Email Verified!
+ {LL.Auth.emailVerified()}
- Your email has been verified successfully. Redirecting to home...
+ {LL.Auth.emailVerifiedMessage()}
- Go to Home now
+ {LL.Auth.goToHome()}
diff --git a/apps/web/i18n/en/index.ts b/apps/web/i18n/en/index.ts
new file mode 100644
index 0000000..28acac6
--- /dev/null
+++ b/apps/web/i18n/en/index.ts
@@ -0,0 +1,226 @@
+import type { BaseTranslation } from "../i18n-types";
+
+const en = {
+ Common: {
+ loading: "Loading...",
+ backToHome: "Back to Home",
+ back: "Back",
+ cancel: "Cancel",
+ save: "Save",
+ add: "Add",
+ refresh: "Refresh",
+ refreshing: "Refreshing...",
+ adding: "Adding...",
+ saving: "Saving...",
+ sending: "Sending...",
+ or: "Or",
+ roleAdmin: "Admin",
+ roleUser: "User",
+ you: "You",
+ edit: "Edit",
+ delete: "Delete",
+ loadingItems: "Loading items...",
+ noItemsYet: "No items yet. Add one to get started!",
+ itemCount: "{count:number} {{Item|Items}}",
+ itemCountGlobal: "{count:number} {{Item|Items}} (global)",
+ },
+ Navigation: {
+ fullStack: "Full Stack",
+ profileAndSecurity: "Profile & Security",
+ platformTenants: "Platform Tenants",
+ manageMembers: "Manage Members",
+ signOut: "Sign out",
+ switchTenant: "Switch tenant",
+ switching: "Switching...",
+ selectTenant: "Select tenant",
+ },
+ Home: {
+ title: "Full Stack Boilerplate",
+ subtitle: "NextJs (Tailwind CSS), NestJs, Expo, tRPC, Better Auth",
+ crudDemoTitle: "CRUD Demo",
+ crudDemoDescription: "Test Create, Read, Update, Delete operations with tRPC and Prisma integration. Full-stack type safety demonstration.",
+ globalCrudDemoTitle: "Global CRUD Demo",
+ globalCrudDemoDescription: "Same as CRUD but shared across all tenants. Everyone sees and edits the same data.",
+ platformTenantsTitle: "Platform Tenants",
+ platformTenantsDescription: "Create, edit, and remove tenants across the platform. Super-admin only.",
+ manageTenants: "Manage tenants",
+ manageMembersTitle: "Manage Members",
+ manageMembersDescription: "Add or remove members from tenants you administer.",
+ manageMembers: "Manage members",
+ authDemoTitle: "Auth Demo",
+ authDemoDescription: "Test authentication with Better Auth. OAuth integration with Google, session management, and protected routes.",
+ tryItOut: "Try it out",
+ footer: "Built with modern tools for rapid development",
+ },
+ Auth: {
+ signIn: "Sign in",
+ signInTitle: "Sign in",
+ signInSubtitle: "Choose your preferred sign-in method",
+ signInWithGoogle: "Sign in with Google",
+ signInWithEmailOtp: "Sign in with Email OTP",
+ orWithEmailPassword: "Or with Email & Password",
+ signInWithEmailPassword: "Sign in with Email & Password",
+ signingIn: "Signing in...",
+ signUp: "Sign up",
+ signUpTitle: "Create an account",
+ signUpSubtitle: "Sign up with your email and password",
+ signUpWithEmailPassword: "Sign up with Email & Password",
+ creatingAccount: "Creating account...",
+ dontHaveAccount: "Don't have an account?",
+ alreadyHaveAccount: "Already have an account?",
+ forgotPassword: "Forgot Password?",
+ forgotPasswordTitle: "Forgot Password",
+ forgotPasswordSubtitle: "Enter your email to receive a password reset link",
+ checkYourInbox: "Check your inbox",
+ sendResetLink: "Send Reset Link",
+ backToSignIn: "Back to Sign In",
+ resetLinkSentMessage: "If an account exists with this email, you'll receive a link to set your password.",
+ setYourPassword: "Set Your Password",
+ setPasswordSubtitle: "Enter a new password for your account",
+ setPassword: "Set Password",
+ settingPassword: "Setting password...",
+ invalidOrExpiredLink: "Invalid or Expired Link",
+ invalidOrExpiredLinkMessage: "This password reset link is invalid or has expired. Please request a new one.",
+ requestNewLink: "Request New Link",
+ verificationFailed: "Verification Failed",
+ verificationFailedExpired: "This verification link has expired. Please try signing in again to receive a new verification email.",
+ verificationFailedInvalid: "This verification link is invalid. Please try signing in again to receive a new verification email.",
+ emailVerified: "Email Verified!",
+ emailVerifiedMessage: "Your email has been verified successfully. Redirecting to home...",
+ goToHome: "Go to Home now",
+ goToSignIn: "Go to Sign In",
+ sendOtp: "Send OTP",
+ verifyOtp: "Verify OTP",
+ verifying: "Verifying...",
+ changeEmail: "Change Email",
+ otpSentTo: "We sent a verification code to {email:string}",
+ checkInboxMessage: "We've sent a verification email to {email:string}. Please check your inbox and click the link to verify your account before signing in.",
+ passwordSetSuccess: "Password set successfully. You can now sign in.",
+ signInRequired: "Sign in required",
+ signInRequiredMessage: "You need to be authenticated to access this application.",
+ betterAuthDemo: "Better Auth Demo",
+ welcomeBack: "Welcome back!",
+ signInToContinue: "Sign in to continue",
+ youAreSignedIn: "You are signed in",
+ userInformation: "User Information",
+ signOutButton: "Sign Out",
+ orEmailPassword: "Or Email & Password",
+ resendVerificationEmail: "Resend verification email",
+ verificationEmailSent: "Verification email sent! Check your inbox.",
+ passwordMinChars: "Min 8 characters with uppercase, lowercase, number, and special character.",
+ },
+ Dashboard: {
+ platformTenantsTitle: "Platform Tenants",
+ platformTenantsSubtitle: "Create, edit, and remove tenants. Super-admin only.",
+ mustBeLoggedIn: "You must be logged in to access this page.",
+ superAdminRequired: "Super-admin access required. Your email must be in SUPER_ADMIN_EMAILS.",
+ addTenant: "Add tenant",
+ slugLabel: "Slug (a-z, 0-9, hyphens)",
+ slugPlaceholder: "e.g. acme-corp",
+ namePlaceholder: "e.g. Acme Corp",
+ tenantCount: "{count:number} tenant{{s|}}",
+ loadingTenants: "Loading tenants...",
+ noTenantsYet: "No tenants yet. Add one above.",
+ refreshList: "Refresh list",
+ deleteConfirm: "Delete tenant \"{slug:string}\"? This cannot be undone.",
+ dualDatabaseCrudDemo: "Dual Database CRUD Demo",
+ crudDemoSubtitle: "Side-by-side comparison of Mongoose (MongoDB) and Prisma (PostgreSQL)",
+ crudDemoTechStack: "NextJs (TailwindCSS) \u2022 NestJs \u2022 tRPC \u2022 Transactions",
+ tenantDashboardLink: "\u2192 Tenant Dashboard (super-admin)",
+ globalCrudDemoTitle: "Global CRUD Demo",
+ globalCrudDemoSubtitle: "Same as CRUD but shared across all tenants. Everyone sees and edits the same data.",
+ globalCrudDemoTechStack: "Mongoose (MongoDB) & Prisma (PostgreSQL) \u2022 No tenant scope",
+ addTextPlaceholder: "Add text here",
+ },
+ Settings: {
+ profileTitle: "Profile",
+ profileSubtitle: "Manage your account security and linked sign-in methods.",
+ emailNotVerified: "Your email is not verified",
+ verifyEmailPrompt: "Please verify your email to access all features.",
+ accountInfo: "Account Info",
+ linkedAccounts: "Linked Accounts",
+ google: "Google",
+ connected: "Connected",
+ connectGoogle: "Connect Google",
+ emailAndPassword: "Email & Password",
+ passwordSet: "Password set",
+ notSet: "Not set",
+ passwordTitle: "Password",
+ changePassword: "Change Password",
+ currentPassword: "Current Password",
+ newPasswordLabel: "New Password (min 8 characters)",
+ confirmNewPassword: "Confirm New Password",
+ savePassword: "Save Password",
+ addPasswordDescription: "Add a password to sign in without Google or OTP.",
+ checkInboxForPassword: "Check your inbox to set your password.",
+ manageMembersTitle: "Manage Members",
+ manageMembersSubtitle: "Add or remove members from your tenants.",
+ noAdminAccess: "No admin access",
+ noAdminAccessMessage: "You don't have admin access to any tenants.",
+ yourTenants: "Your tenants ({count:number})",
+ membersOf: "Members of {name:string}",
+ addMember: "Add member",
+ memberEmailPlaceholder: "user@example.com",
+ loadingMembers: "Loading members...",
+ noMembersYet: "No members yet. Add one above.",
+ removeMember: "Remove member",
+ selectTenantPrompt: "Select a tenant from the list to manage its members.",
+ },
+ Forms: {
+ email: "Email",
+ emailAddress: "Email Address",
+ emailPlaceholder: "you@example.com",
+ enterYourEmail: "Enter your email",
+ password: "Password",
+ passwordPlaceholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
+ passwordMinChars: "Password (min 8 characters)",
+ confirmPassword: "Confirm Password",
+ firstName: "First Name",
+ firstNamePlaceholder: "John",
+ lastName: "Last Name",
+ lastNamePlaceholder: "Doe",
+ name: "Name",
+ namePlaceholder: "Your name",
+ verificationCode: "Verification Code",
+ verificationCodePlaceholder: "Enter 6-digit code",
+ slug: "Slug",
+ },
+ Errors: {
+ failedSignInGoogle: "Failed to sign in with Google",
+ failedSendOtp: "Failed to send OTP. Please try again.",
+ unableToSendOtp: "Unable to send OTP",
+ invalidOtp: "Invalid OTP. Please try again.",
+ unableToVerifyOtp: "Unable to verify OTP",
+ verifyEmailBeforeSignIn: "Please verify your email address before signing in.",
+ accountCreatedWithSocial: "This account was created with Google or OTP. Use 'Forgot Password' below to set a password.",
+ invalidEmailOrPassword: "Invalid email or password.",
+ passwordsDoNotMatch: "Passwords do not match.",
+ passwordMinLength: "Password must be at least 8 characters.",
+ passwordMaxLength: "Password must be at most 128 characters.",
+ passwordUppercase: "Password must contain at least one uppercase letter.",
+ passwordLowercase: "Password must contain at least one lowercase letter.",
+ passwordNumber: "Password must contain at least one number.",
+ passwordSpecialChar: "Password must contain at least one special character.",
+ failedResetPassword: "Failed to reset password. Please try again.",
+ failedChangePassword: "Failed to change password.",
+ passwordChangedSuccess: "Password changed successfully.",
+ signUpFailed: "Sign up failed. Please try again.",
+ signInFailed: "Sign in failed",
+ accountAlreadyExists: "An account with this email already exists. Try signing in instead \u2014 if you used Google or OTP, you can set a password from your profile.",
+ nameRequired: "{label:string} is required.",
+ nameMinLength: "{label:string} must be at least 2 characters.",
+ nameInvalidChars: "{label:string} can only contain letters, spaces, hyphens, and apostrophes.",
+ emailRequired: "Email is required.",
+ emailInvalid: "Please enter a valid email address.",
+ passwordRequired: "Password is required.",
+ slugRequired: "Slug is required",
+ nameFieldRequired: "Name is required",
+ slugInvalid: "Slug must be lowercase letters, numbers, and hyphens only",
+ noTenantsAssigned: "No tenants assigned",
+ noTenantsMessage: "You don't have access to any tenants yet. Please contact an administrator to get added to a tenant.",
+ loadingYourTenants: "Loading your tenants...",
+ settingUpTenant: "Setting up your tenant...",
+ },
+} satisfies BaseTranslation;
+
+export default en;
diff --git a/apps/web/i18n/formatters.ts b/apps/web/i18n/formatters.ts
new file mode 100644
index 0000000..78734f9
--- /dev/null
+++ b/apps/web/i18n/formatters.ts
@@ -0,0 +1,11 @@
+import type { FormattersInitializer } from 'typesafe-i18n'
+import type { Locales, Formatters } from './i18n-types'
+
+export const initFormatters: FormattersInitializer = (locale: Locales) => {
+
+ const formatters: Formatters = {
+ // add your formatter functions here
+ }
+
+ return formatters
+}
diff --git a/apps/web/i18n/i18n-react.tsx b/apps/web/i18n/i18n-react.tsx
new file mode 100644
index 0000000..f113051
--- /dev/null
+++ b/apps/web/i18n/i18n-react.tsx
@@ -0,0 +1,16 @@
+// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
+/* eslint-disable */
+
+import { useContext } from 'react'
+import { initI18nReact } from 'typesafe-i18n/react'
+import type { I18nContextType } from 'typesafe-i18n/react'
+import type { Formatters, Locales, TranslationFunctions, Translations } from './i18n-types'
+import { loadedFormatters, loadedLocales } from './i18n-util'
+
+const { component: TypesafeI18n, context: I18nContext } = initI18nReact(loadedLocales, loadedFormatters)
+
+const useI18nContext = (): I18nContextType => useContext(I18nContext)
+
+export { I18nContext, useI18nContext }
+
+export default TypesafeI18n
diff --git a/apps/web/i18n/i18n-types.ts b/apps/web/i18n/i18n-types.ts
new file mode 100644
index 0000000..40b418a
--- /dev/null
+++ b/apps/web/i18n/i18n-types.ts
@@ -0,0 +1,1697 @@
+// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
+/* eslint-disable */
+import type { BaseTranslation as BaseTranslationType, LocalizedString, RequiredParams } from 'typesafe-i18n'
+
+export type BaseTranslation = BaseTranslationType
+export type BaseLocale = 'en'
+
+export type Locales =
+ | 'en'
+ | 'nl'
+
+export type Translation = RootTranslation
+
+export type Translations = RootTranslation
+
+type RootTranslation = {
+ Common: {
+ /**
+ * Loading...
+ */
+ loading: string
+ /**
+ * Back to Home
+ */
+ backToHome: string
+ /**
+ * Back
+ */
+ back: string
+ /**
+ * Cancel
+ */
+ cancel: string
+ /**
+ * Save
+ */
+ save: string
+ /**
+ * Add
+ */
+ add: string
+ /**
+ * Refresh
+ */
+ refresh: string
+ /**
+ * Refreshing...
+ */
+ refreshing: string
+ /**
+ * Adding...
+ */
+ adding: string
+ /**
+ * Saving...
+ */
+ saving: string
+ /**
+ * Sending...
+ */
+ sending: string
+ /**
+ * Or
+ */
+ or: string
+ /**
+ * Admin
+ */
+ roleAdmin: string
+ /**
+ * User
+ */
+ roleUser: string
+ /**
+ * You
+ */
+ you: string
+ /**
+ * Edit
+ */
+ edit: string
+ /**
+ * Delete
+ */
+ 'delete': string
+ /**
+ * Loading items...
+ */
+ loadingItems: string
+ /**
+ * No items yet. Add one to get started!
+ */
+ noItemsYet: string
+ /**
+ * {count} {{Item|Items}}
+ * @param {number} count
+ */
+ itemCount: RequiredParams<'count'>
+ /**
+ * {count} {{Item|Items}} (global)
+ * @param {number} count
+ */
+ itemCountGlobal: RequiredParams<'count'>
+ }
+ Navigation: {
+ /**
+ * Full Stack
+ */
+ fullStack: string
+ /**
+ * Profile & Security
+ */
+ profileAndSecurity: string
+ /**
+ * Platform Tenants
+ */
+ platformTenants: string
+ /**
+ * Manage Members
+ */
+ manageMembers: string
+ /**
+ * Sign out
+ */
+ signOut: string
+ /**
+ * Switch tenant
+ */
+ switchTenant: string
+ /**
+ * Switching...
+ */
+ switching: string
+ /**
+ * Select tenant
+ */
+ selectTenant: string
+ }
+ Home: {
+ /**
+ * Full Stack Boilerplate
+ */
+ title: string
+ /**
+ * NextJs (Tailwind CSS), NestJs, Expo, tRPC, Better Auth
+ */
+ subtitle: string
+ /**
+ * CRUD Demo
+ */
+ crudDemoTitle: string
+ /**
+ * Test Create, Read, Update, Delete operations with tRPC and Prisma integration. Full-stack type safety demonstration.
+ */
+ crudDemoDescription: string
+ /**
+ * Global CRUD Demo
+ */
+ globalCrudDemoTitle: string
+ /**
+ * Same as CRUD but shared across all tenants. Everyone sees and edits the same data.
+ */
+ globalCrudDemoDescription: string
+ /**
+ * Platform Tenants
+ */
+ platformTenantsTitle: string
+ /**
+ * Create, edit, and remove tenants across the platform. Super-admin only.
+ */
+ platformTenantsDescription: string
+ /**
+ * Manage tenants
+ */
+ manageTenants: string
+ /**
+ * Manage Members
+ */
+ manageMembersTitle: string
+ /**
+ * Add or remove members from tenants you administer.
+ */
+ manageMembersDescription: string
+ /**
+ * Manage members
+ */
+ manageMembers: string
+ /**
+ * Auth Demo
+ */
+ authDemoTitle: string
+ /**
+ * Test authentication with Better Auth. OAuth integration with Google, session management, and protected routes.
+ */
+ authDemoDescription: string
+ /**
+ * Try it out
+ */
+ tryItOut: string
+ /**
+ * Built with modern tools for rapid development
+ */
+ footer: string
+ }
+ Auth: {
+ /**
+ * Sign in
+ */
+ signIn: string
+ /**
+ * Sign in
+ */
+ signInTitle: string
+ /**
+ * Choose your preferred sign-in method
+ */
+ signInSubtitle: string
+ /**
+ * Sign in with Google
+ */
+ signInWithGoogle: string
+ /**
+ * Sign in with Email OTP
+ */
+ signInWithEmailOtp: string
+ /**
+ * Or with Email & Password
+ */
+ orWithEmailPassword: string
+ /**
+ * Sign in with Email & Password
+ */
+ signInWithEmailPassword: string
+ /**
+ * Signing in...
+ */
+ signingIn: string
+ /**
+ * Sign up
+ */
+ signUp: string
+ /**
+ * Create an account
+ */
+ signUpTitle: string
+ /**
+ * Sign up with your email and password
+ */
+ signUpSubtitle: string
+ /**
+ * Sign up with Email & Password
+ */
+ signUpWithEmailPassword: string
+ /**
+ * Creating account...
+ */
+ creatingAccount: string
+ /**
+ * Don't have an account?
+ */
+ dontHaveAccount: string
+ /**
+ * Already have an account?
+ */
+ alreadyHaveAccount: string
+ /**
+ * Forgot Password?
+ */
+ forgotPassword: string
+ /**
+ * Forgot Password
+ */
+ forgotPasswordTitle: string
+ /**
+ * Enter your email to receive a password reset link
+ */
+ forgotPasswordSubtitle: string
+ /**
+ * Check your inbox
+ */
+ checkYourInbox: string
+ /**
+ * Send Reset Link
+ */
+ sendResetLink: string
+ /**
+ * Back to Sign In
+ */
+ backToSignIn: string
+ /**
+ * If an account exists with this email, you'll receive a link to set your password.
+ */
+ resetLinkSentMessage: string
+ /**
+ * Set Your Password
+ */
+ setYourPassword: string
+ /**
+ * Enter a new password for your account
+ */
+ setPasswordSubtitle: string
+ /**
+ * Set Password
+ */
+ setPassword: string
+ /**
+ * Setting password...
+ */
+ settingPassword: string
+ /**
+ * Invalid or Expired Link
+ */
+ invalidOrExpiredLink: string
+ /**
+ * This password reset link is invalid or has expired. Please request a new one.
+ */
+ invalidOrExpiredLinkMessage: string
+ /**
+ * Request New Link
+ */
+ requestNewLink: string
+ /**
+ * Verification Failed
+ */
+ verificationFailed: string
+ /**
+ * This verification link has expired. Please try signing in again to receive a new verification email.
+ */
+ verificationFailedExpired: string
+ /**
+ * This verification link is invalid. Please try signing in again to receive a new verification email.
+ */
+ verificationFailedInvalid: string
+ /**
+ * Email Verified!
+ */
+ emailVerified: string
+ /**
+ * Your email has been verified successfully. Redirecting to home...
+ */
+ emailVerifiedMessage: string
+ /**
+ * Go to Home now
+ */
+ goToHome: string
+ /**
+ * Go to Sign In
+ */
+ goToSignIn: string
+ /**
+ * Send OTP
+ */
+ sendOtp: string
+ /**
+ * Verify OTP
+ */
+ verifyOtp: string
+ /**
+ * Verifying...
+ */
+ verifying: string
+ /**
+ * Change Email
+ */
+ changeEmail: string
+ /**
+ * We sent a verification code to {email}
+ * @param {string} email
+ */
+ otpSentTo: RequiredParams<'email'>
+ /**
+ * We've sent a verification email to {email}. Please check your inbox and click the link to verify your account before signing in.
+ * @param {string} email
+ */
+ checkInboxMessage: RequiredParams<'email'>
+ /**
+ * Password set successfully. You can now sign in.
+ */
+ passwordSetSuccess: string
+ /**
+ * Sign in required
+ */
+ signInRequired: string
+ /**
+ * You need to be authenticated to access this application.
+ */
+ signInRequiredMessage: string
+ /**
+ * Better Auth Demo
+ */
+ betterAuthDemo: string
+ /**
+ * Welcome back!
+ */
+ welcomeBack: string
+ /**
+ * Sign in to continue
+ */
+ signInToContinue: string
+ /**
+ * You are signed in
+ */
+ youAreSignedIn: string
+ /**
+ * User Information
+ */
+ userInformation: string
+ /**
+ * Sign Out
+ */
+ signOutButton: string
+ /**
+ * Or Email & Password
+ */
+ orEmailPassword: string
+ /**
+ * Resend verification email
+ */
+ resendVerificationEmail: string
+ /**
+ * Verification email sent! Check your inbox.
+ */
+ verificationEmailSent: string
+ /**
+ * Min 8 characters with uppercase, lowercase, number, and special character.
+ */
+ passwordMinChars: string
+ }
+ Dashboard: {
+ /**
+ * Platform Tenants
+ */
+ platformTenantsTitle: string
+ /**
+ * Create, edit, and remove tenants. Super-admin only.
+ */
+ platformTenantsSubtitle: string
+ /**
+ * You must be logged in to access this page.
+ */
+ mustBeLoggedIn: string
+ /**
+ * Super-admin access required. Your email must be in SUPER_ADMIN_EMAILS.
+ */
+ superAdminRequired: string
+ /**
+ * Add tenant
+ */
+ addTenant: string
+ /**
+ * Slug (a-z, 0-9, hyphens)
+ */
+ slugLabel: string
+ /**
+ * e.g. acme-corp
+ */
+ slugPlaceholder: string
+ /**
+ * e.g. Acme Corp
+ */
+ namePlaceholder: string
+ /**
+ * {count} tenant{{s|}}
+ * @param {number} count
+ */
+ tenantCount: RequiredParams<'count'>
+ /**
+ * Loading tenants...
+ */
+ loadingTenants: string
+ /**
+ * No tenants yet. Add one above.
+ */
+ noTenantsYet: string
+ /**
+ * Refresh list
+ */
+ refreshList: string
+ /**
+ * Delete tenant "{slug}"? This cannot be undone.
+ * @param {string} slug
+ */
+ deleteConfirm: RequiredParams<'slug'>
+ /**
+ * Dual Database CRUD Demo
+ */
+ dualDatabaseCrudDemo: string
+ /**
+ * Side-by-side comparison of Mongoose (MongoDB) and Prisma (PostgreSQL)
+ */
+ crudDemoSubtitle: string
+ /**
+ * NextJs (TailwindCSS) • NestJs • tRPC • Transactions
+ */
+ crudDemoTechStack: string
+ /**
+ * → Tenant Dashboard (super-admin)
+ */
+ tenantDashboardLink: string
+ /**
+ * Global CRUD Demo
+ */
+ globalCrudDemoTitle: string
+ /**
+ * Same as CRUD but shared across all tenants. Everyone sees and edits the same data.
+ */
+ globalCrudDemoSubtitle: string
+ /**
+ * Mongoose (MongoDB) & Prisma (PostgreSQL) • No tenant scope
+ */
+ globalCrudDemoTechStack: string
+ /**
+ * Add text here
+ */
+ addTextPlaceholder: string
+ }
+ Settings: {
+ /**
+ * Profile
+ */
+ profileTitle: string
+ /**
+ * Manage your account security and linked sign-in methods.
+ */
+ profileSubtitle: string
+ /**
+ * Your email is not verified
+ */
+ emailNotVerified: string
+ /**
+ * Please verify your email to access all features.
+ */
+ verifyEmailPrompt: string
+ /**
+ * Account Info
+ */
+ accountInfo: string
+ /**
+ * Linked Accounts
+ */
+ linkedAccounts: string
+ /**
+ * Google
+ */
+ google: string
+ /**
+ * Connected
+ */
+ connected: string
+ /**
+ * Connect Google
+ */
+ connectGoogle: string
+ /**
+ * Email & Password
+ */
+ emailAndPassword: string
+ /**
+ * Password set
+ */
+ passwordSet: string
+ /**
+ * Not set
+ */
+ notSet: string
+ /**
+ * Password
+ */
+ passwordTitle: string
+ /**
+ * Change Password
+ */
+ changePassword: string
+ /**
+ * Current Password
+ */
+ currentPassword: string
+ /**
+ * New Password (min 8 characters)
+ */
+ newPasswordLabel: string
+ /**
+ * Confirm New Password
+ */
+ confirmNewPassword: string
+ /**
+ * Save Password
+ */
+ savePassword: string
+ /**
+ * Add a password to sign in without Google or OTP.
+ */
+ addPasswordDescription: string
+ /**
+ * Check your inbox to set your password.
+ */
+ checkInboxForPassword: string
+ /**
+ * Manage Members
+ */
+ manageMembersTitle: string
+ /**
+ * Add or remove members from your tenants.
+ */
+ manageMembersSubtitle: string
+ /**
+ * No admin access
+ */
+ noAdminAccess: string
+ /**
+ * You don't have admin access to any tenants.
+ */
+ noAdminAccessMessage: string
+ /**
+ * Your tenants ({count})
+ * @param {number} count
+ */
+ yourTenants: RequiredParams<'count'>
+ /**
+ * Members of {name}
+ * @param {string} name
+ */
+ membersOf: RequiredParams<'name'>
+ /**
+ * Add member
+ */
+ addMember: string
+ /**
+ * user@example.com
+ */
+ memberEmailPlaceholder: string
+ /**
+ * Loading members...
+ */
+ loadingMembers: string
+ /**
+ * No members yet. Add one above.
+ */
+ noMembersYet: string
+ /**
+ * Remove member
+ */
+ removeMember: string
+ /**
+ * Select a tenant from the list to manage its members.
+ */
+ selectTenantPrompt: string
+ }
+ Forms: {
+ /**
+ * Email
+ */
+ email: string
+ /**
+ * Email Address
+ */
+ emailAddress: string
+ /**
+ * you@example.com
+ */
+ emailPlaceholder: string
+ /**
+ * Enter your email
+ */
+ enterYourEmail: string
+ /**
+ * Password
+ */
+ password: string
+ /**
+ * ••••••••
+ */
+ passwordPlaceholder: string
+ /**
+ * Password (min 8 characters)
+ */
+ passwordMinChars: string
+ /**
+ * Confirm Password
+ */
+ confirmPassword: string
+ /**
+ * First Name
+ */
+ firstName: string
+ /**
+ * John
+ */
+ firstNamePlaceholder: string
+ /**
+ * Last Name
+ */
+ lastName: string
+ /**
+ * Doe
+ */
+ lastNamePlaceholder: string
+ /**
+ * Name
+ */
+ name: string
+ /**
+ * Your name
+ */
+ namePlaceholder: string
+ /**
+ * Verification Code
+ */
+ verificationCode: string
+ /**
+ * Enter 6-digit code
+ */
+ verificationCodePlaceholder: string
+ /**
+ * Slug
+ */
+ slug: string
+ }
+ Errors: {
+ /**
+ * Failed to sign in with Google
+ */
+ failedSignInGoogle: string
+ /**
+ * Failed to send OTP. Please try again.
+ */
+ failedSendOtp: string
+ /**
+ * Unable to send OTP
+ */
+ unableToSendOtp: string
+ /**
+ * Invalid OTP. Please try again.
+ */
+ invalidOtp: string
+ /**
+ * Unable to verify OTP
+ */
+ unableToVerifyOtp: string
+ /**
+ * Please verify your email address before signing in.
+ */
+ verifyEmailBeforeSignIn: string
+ /**
+ * This account was created with Google or OTP. Use 'Forgot Password' below to set a password.
+ */
+ accountCreatedWithSocial: string
+ /**
+ * Invalid email or password.
+ */
+ invalidEmailOrPassword: string
+ /**
+ * Passwords do not match.
+ */
+ passwordsDoNotMatch: string
+ /**
+ * Password must be at least 8 characters.
+ */
+ passwordMinLength: string
+ /**
+ * Password must be at most 128 characters.
+ */
+ passwordMaxLength: string
+ /**
+ * Password must contain at least one uppercase letter.
+ */
+ passwordUppercase: string
+ /**
+ * Password must contain at least one lowercase letter.
+ */
+ passwordLowercase: string
+ /**
+ * Password must contain at least one number.
+ */
+ passwordNumber: string
+ /**
+ * Password must contain at least one special character.
+ */
+ passwordSpecialChar: string
+ /**
+ * Failed to reset password. Please try again.
+ */
+ failedResetPassword: string
+ /**
+ * Failed to change password.
+ */
+ failedChangePassword: string
+ /**
+ * Password changed successfully.
+ */
+ passwordChangedSuccess: string
+ /**
+ * Sign up failed. Please try again.
+ */
+ signUpFailed: string
+ /**
+ * Sign in failed
+ */
+ signInFailed: string
+ /**
+ * An account with this email already exists. Try signing in instead — if you used Google or OTP, you can set a password from your profile.
+ */
+ accountAlreadyExists: string
+ /**
+ * {label} is required.
+ * @param {string} label
+ */
+ nameRequired: RequiredParams<'label'>
+ /**
+ * {label} must be at least 2 characters.
+ * @param {string} label
+ */
+ nameMinLength: RequiredParams<'label'>
+ /**
+ * {label} can only contain letters, spaces, hyphens, and apostrophes.
+ * @param {string} label
+ */
+ nameInvalidChars: RequiredParams<'label'>
+ /**
+ * Email is required.
+ */
+ emailRequired: string
+ /**
+ * Please enter a valid email address.
+ */
+ emailInvalid: string
+ /**
+ * Password is required.
+ */
+ passwordRequired: string
+ /**
+ * Slug is required
+ */
+ slugRequired: string
+ /**
+ * Name is required
+ */
+ nameFieldRequired: string
+ /**
+ * Slug must be lowercase letters, numbers, and hyphens only
+ */
+ slugInvalid: string
+ /**
+ * No tenants assigned
+ */
+ noTenantsAssigned: string
+ /**
+ * You don't have access to any tenants yet. Please contact an administrator to get added to a tenant.
+ */
+ noTenantsMessage: string
+ /**
+ * Loading your tenants...
+ */
+ loadingYourTenants: string
+ /**
+ * Setting up your tenant...
+ */
+ settingUpTenant: string
+ }
+}
+
+export type TranslationFunctions = {
+ Common: {
+ /**
+ * Loading...
+ */
+ loading: () => LocalizedString
+ /**
+ * Back to Home
+ */
+ backToHome: () => LocalizedString
+ /**
+ * Back
+ */
+ back: () => LocalizedString
+ /**
+ * Cancel
+ */
+ cancel: () => LocalizedString
+ /**
+ * Save
+ */
+ save: () => LocalizedString
+ /**
+ * Add
+ */
+ add: () => LocalizedString
+ /**
+ * Refresh
+ */
+ refresh: () => LocalizedString
+ /**
+ * Refreshing...
+ */
+ refreshing: () => LocalizedString
+ /**
+ * Adding...
+ */
+ adding: () => LocalizedString
+ /**
+ * Saving...
+ */
+ saving: () => LocalizedString
+ /**
+ * Sending...
+ */
+ sending: () => LocalizedString
+ /**
+ * Or
+ */
+ or: () => LocalizedString
+ /**
+ * Admin
+ */
+ roleAdmin: () => LocalizedString
+ /**
+ * User
+ */
+ roleUser: () => LocalizedString
+ /**
+ * You
+ */
+ you: () => LocalizedString
+ /**
+ * Edit
+ */
+ edit: () => LocalizedString
+ /**
+ * Delete
+ */
+ 'delete': () => LocalizedString
+ /**
+ * Loading items...
+ */
+ loadingItems: () => LocalizedString
+ /**
+ * No items yet. Add one to get started!
+ */
+ noItemsYet: () => LocalizedString
+ /**
+ * {count} {{Item|Items}}
+ */
+ itemCount: (arg: { count: number }) => LocalizedString
+ /**
+ * {count} {{Item|Items}} (global)
+ */
+ itemCountGlobal: (arg: { count: number }) => LocalizedString
+ }
+ Navigation: {
+ /**
+ * Full Stack
+ */
+ fullStack: () => LocalizedString
+ /**
+ * Profile & Security
+ */
+ profileAndSecurity: () => LocalizedString
+ /**
+ * Platform Tenants
+ */
+ platformTenants: () => LocalizedString
+ /**
+ * Manage Members
+ */
+ manageMembers: () => LocalizedString
+ /**
+ * Sign out
+ */
+ signOut: () => LocalizedString
+ /**
+ * Switch tenant
+ */
+ switchTenant: () => LocalizedString
+ /**
+ * Switching...
+ */
+ switching: () => LocalizedString
+ /**
+ * Select tenant
+ */
+ selectTenant: () => LocalizedString
+ }
+ Home: {
+ /**
+ * Full Stack Boilerplate
+ */
+ title: () => LocalizedString
+ /**
+ * NextJs (Tailwind CSS), NestJs, Expo, tRPC, Better Auth
+ */
+ subtitle: () => LocalizedString
+ /**
+ * CRUD Demo
+ */
+ crudDemoTitle: () => LocalizedString
+ /**
+ * Test Create, Read, Update, Delete operations with tRPC and Prisma integration. Full-stack type safety demonstration.
+ */
+ crudDemoDescription: () => LocalizedString
+ /**
+ * Global CRUD Demo
+ */
+ globalCrudDemoTitle: () => LocalizedString
+ /**
+ * Same as CRUD but shared across all tenants. Everyone sees and edits the same data.
+ */
+ globalCrudDemoDescription: () => LocalizedString
+ /**
+ * Platform Tenants
+ */
+ platformTenantsTitle: () => LocalizedString
+ /**
+ * Create, edit, and remove tenants across the platform. Super-admin only.
+ */
+ platformTenantsDescription: () => LocalizedString
+ /**
+ * Manage tenants
+ */
+ manageTenants: () => LocalizedString
+ /**
+ * Manage Members
+ */
+ manageMembersTitle: () => LocalizedString
+ /**
+ * Add or remove members from tenants you administer.
+ */
+ manageMembersDescription: () => LocalizedString
+ /**
+ * Manage members
+ */
+ manageMembers: () => LocalizedString
+ /**
+ * Auth Demo
+ */
+ authDemoTitle: () => LocalizedString
+ /**
+ * Test authentication with Better Auth. OAuth integration with Google, session management, and protected routes.
+ */
+ authDemoDescription: () => LocalizedString
+ /**
+ * Try it out
+ */
+ tryItOut: () => LocalizedString
+ /**
+ * Built with modern tools for rapid development
+ */
+ footer: () => LocalizedString
+ }
+ Auth: {
+ /**
+ * Sign in
+ */
+ signIn: () => LocalizedString
+ /**
+ * Sign in
+ */
+ signInTitle: () => LocalizedString
+ /**
+ * Choose your preferred sign-in method
+ */
+ signInSubtitle: () => LocalizedString
+ /**
+ * Sign in with Google
+ */
+ signInWithGoogle: () => LocalizedString
+ /**
+ * Sign in with Email OTP
+ */
+ signInWithEmailOtp: () => LocalizedString
+ /**
+ * Or with Email & Password
+ */
+ orWithEmailPassword: () => LocalizedString
+ /**
+ * Sign in with Email & Password
+ */
+ signInWithEmailPassword: () => LocalizedString
+ /**
+ * Signing in...
+ */
+ signingIn: () => LocalizedString
+ /**
+ * Sign up
+ */
+ signUp: () => LocalizedString
+ /**
+ * Create an account
+ */
+ signUpTitle: () => LocalizedString
+ /**
+ * Sign up with your email and password
+ */
+ signUpSubtitle: () => LocalizedString
+ /**
+ * Sign up with Email & Password
+ */
+ signUpWithEmailPassword: () => LocalizedString
+ /**
+ * Creating account...
+ */
+ creatingAccount: () => LocalizedString
+ /**
+ * Don't have an account?
+ */
+ dontHaveAccount: () => LocalizedString
+ /**
+ * Already have an account?
+ */
+ alreadyHaveAccount: () => LocalizedString
+ /**
+ * Forgot Password?
+ */
+ forgotPassword: () => LocalizedString
+ /**
+ * Forgot Password
+ */
+ forgotPasswordTitle: () => LocalizedString
+ /**
+ * Enter your email to receive a password reset link
+ */
+ forgotPasswordSubtitle: () => LocalizedString
+ /**
+ * Check your inbox
+ */
+ checkYourInbox: () => LocalizedString
+ /**
+ * Send Reset Link
+ */
+ sendResetLink: () => LocalizedString
+ /**
+ * Back to Sign In
+ */
+ backToSignIn: () => LocalizedString
+ /**
+ * If an account exists with this email, you'll receive a link to set your password.
+ */
+ resetLinkSentMessage: () => LocalizedString
+ /**
+ * Set Your Password
+ */
+ setYourPassword: () => LocalizedString
+ /**
+ * Enter a new password for your account
+ */
+ setPasswordSubtitle: () => LocalizedString
+ /**
+ * Set Password
+ */
+ setPassword: () => LocalizedString
+ /**
+ * Setting password...
+ */
+ settingPassword: () => LocalizedString
+ /**
+ * Invalid or Expired Link
+ */
+ invalidOrExpiredLink: () => LocalizedString
+ /**
+ * This password reset link is invalid or has expired. Please request a new one.
+ */
+ invalidOrExpiredLinkMessage: () => LocalizedString
+ /**
+ * Request New Link
+ */
+ requestNewLink: () => LocalizedString
+ /**
+ * Verification Failed
+ */
+ verificationFailed: () => LocalizedString
+ /**
+ * This verification link has expired. Please try signing in again to receive a new verification email.
+ */
+ verificationFailedExpired: () => LocalizedString
+ /**
+ * This verification link is invalid. Please try signing in again to receive a new verification email.
+ */
+ verificationFailedInvalid: () => LocalizedString
+ /**
+ * Email Verified!
+ */
+ emailVerified: () => LocalizedString
+ /**
+ * Your email has been verified successfully. Redirecting to home...
+ */
+ emailVerifiedMessage: () => LocalizedString
+ /**
+ * Go to Home now
+ */
+ goToHome: () => LocalizedString
+ /**
+ * Go to Sign In
+ */
+ goToSignIn: () => LocalizedString
+ /**
+ * Send OTP
+ */
+ sendOtp: () => LocalizedString
+ /**
+ * Verify OTP
+ */
+ verifyOtp: () => LocalizedString
+ /**
+ * Verifying...
+ */
+ verifying: () => LocalizedString
+ /**
+ * Change Email
+ */
+ changeEmail: () => LocalizedString
+ /**
+ * We sent a verification code to {email}
+ */
+ otpSentTo: (arg: { email: string }) => LocalizedString
+ /**
+ * We've sent a verification email to {email}. Please check your inbox and click the link to verify your account before signing in.
+ */
+ checkInboxMessage: (arg: { email: string }) => LocalizedString
+ /**
+ * Password set successfully. You can now sign in.
+ */
+ passwordSetSuccess: () => LocalizedString
+ /**
+ * Sign in required
+ */
+ signInRequired: () => LocalizedString
+ /**
+ * You need to be authenticated to access this application.
+ */
+ signInRequiredMessage: () => LocalizedString
+ /**
+ * Better Auth Demo
+ */
+ betterAuthDemo: () => LocalizedString
+ /**
+ * Welcome back!
+ */
+ welcomeBack: () => LocalizedString
+ /**
+ * Sign in to continue
+ */
+ signInToContinue: () => LocalizedString
+ /**
+ * You are signed in
+ */
+ youAreSignedIn: () => LocalizedString
+ /**
+ * User Information
+ */
+ userInformation: () => LocalizedString
+ /**
+ * Sign Out
+ */
+ signOutButton: () => LocalizedString
+ /**
+ * Or Email & Password
+ */
+ orEmailPassword: () => LocalizedString
+ /**
+ * Resend verification email
+ */
+ resendVerificationEmail: () => LocalizedString
+ /**
+ * Verification email sent! Check your inbox.
+ */
+ verificationEmailSent: () => LocalizedString
+ /**
+ * Min 8 characters with uppercase, lowercase, number, and special character.
+ */
+ passwordMinChars: () => LocalizedString
+ }
+ Dashboard: {
+ /**
+ * Platform Tenants
+ */
+ platformTenantsTitle: () => LocalizedString
+ /**
+ * Create, edit, and remove tenants. Super-admin only.
+ */
+ platformTenantsSubtitle: () => LocalizedString
+ /**
+ * You must be logged in to access this page.
+ */
+ mustBeLoggedIn: () => LocalizedString
+ /**
+ * Super-admin access required. Your email must be in SUPER_ADMIN_EMAILS.
+ */
+ superAdminRequired: () => LocalizedString
+ /**
+ * Add tenant
+ */
+ addTenant: () => LocalizedString
+ /**
+ * Slug (a-z, 0-9, hyphens)
+ */
+ slugLabel: () => LocalizedString
+ /**
+ * e.g. acme-corp
+ */
+ slugPlaceholder: () => LocalizedString
+ /**
+ * e.g. Acme Corp
+ */
+ namePlaceholder: () => LocalizedString
+ /**
+ * {count} tenant{{s|}}
+ */
+ tenantCount: (arg: { count: number }) => LocalizedString
+ /**
+ * Loading tenants...
+ */
+ loadingTenants: () => LocalizedString
+ /**
+ * No tenants yet. Add one above.
+ */
+ noTenantsYet: () => LocalizedString
+ /**
+ * Refresh list
+ */
+ refreshList: () => LocalizedString
+ /**
+ * Delete tenant "{slug}"? This cannot be undone.
+ */
+ deleteConfirm: (arg: { slug: string }) => LocalizedString
+ /**
+ * Dual Database CRUD Demo
+ */
+ dualDatabaseCrudDemo: () => LocalizedString
+ /**
+ * Side-by-side comparison of Mongoose (MongoDB) and Prisma (PostgreSQL)
+ */
+ crudDemoSubtitle: () => LocalizedString
+ /**
+ * NextJs (TailwindCSS) • NestJs • tRPC • Transactions
+ */
+ crudDemoTechStack: () => LocalizedString
+ /**
+ * → Tenant Dashboard (super-admin)
+ */
+ tenantDashboardLink: () => LocalizedString
+ /**
+ * Global CRUD Demo
+ */
+ globalCrudDemoTitle: () => LocalizedString
+ /**
+ * Same as CRUD but shared across all tenants. Everyone sees and edits the same data.
+ */
+ globalCrudDemoSubtitle: () => LocalizedString
+ /**
+ * Mongoose (MongoDB) & Prisma (PostgreSQL) • No tenant scope
+ */
+ globalCrudDemoTechStack: () => LocalizedString
+ /**
+ * Add text here
+ */
+ addTextPlaceholder: () => LocalizedString
+ }
+ Settings: {
+ /**
+ * Profile
+ */
+ profileTitle: () => LocalizedString
+ /**
+ * Manage your account security and linked sign-in methods.
+ */
+ profileSubtitle: () => LocalizedString
+ /**
+ * Your email is not verified
+ */
+ emailNotVerified: () => LocalizedString
+ /**
+ * Please verify your email to access all features.
+ */
+ verifyEmailPrompt: () => LocalizedString
+ /**
+ * Account Info
+ */
+ accountInfo: () => LocalizedString
+ /**
+ * Linked Accounts
+ */
+ linkedAccounts: () => LocalizedString
+ /**
+ * Google
+ */
+ google: () => LocalizedString
+ /**
+ * Connected
+ */
+ connected: () => LocalizedString
+ /**
+ * Connect Google
+ */
+ connectGoogle: () => LocalizedString
+ /**
+ * Email & Password
+ */
+ emailAndPassword: () => LocalizedString
+ /**
+ * Password set
+ */
+ passwordSet: () => LocalizedString
+ /**
+ * Not set
+ */
+ notSet: () => LocalizedString
+ /**
+ * Password
+ */
+ passwordTitle: () => LocalizedString
+ /**
+ * Change Password
+ */
+ changePassword: () => LocalizedString
+ /**
+ * Current Password
+ */
+ currentPassword: () => LocalizedString
+ /**
+ * New Password (min 8 characters)
+ */
+ newPasswordLabel: () => LocalizedString
+ /**
+ * Confirm New Password
+ */
+ confirmNewPassword: () => LocalizedString
+ /**
+ * Save Password
+ */
+ savePassword: () => LocalizedString
+ /**
+ * Add a password to sign in without Google or OTP.
+ */
+ addPasswordDescription: () => LocalizedString
+ /**
+ * Check your inbox to set your password.
+ */
+ checkInboxForPassword: () => LocalizedString
+ /**
+ * Manage Members
+ */
+ manageMembersTitle: () => LocalizedString
+ /**
+ * Add or remove members from your tenants.
+ */
+ manageMembersSubtitle: () => LocalizedString
+ /**
+ * No admin access
+ */
+ noAdminAccess: () => LocalizedString
+ /**
+ * You don't have admin access to any tenants.
+ */
+ noAdminAccessMessage: () => LocalizedString
+ /**
+ * Your tenants ({count})
+ */
+ yourTenants: (arg: { count: number }) => LocalizedString
+ /**
+ * Members of {name}
+ */
+ membersOf: (arg: { name: string }) => LocalizedString
+ /**
+ * Add member
+ */
+ addMember: () => LocalizedString
+ /**
+ * user@example.com
+ */
+ memberEmailPlaceholder: () => LocalizedString
+ /**
+ * Loading members...
+ */
+ loadingMembers: () => LocalizedString
+ /**
+ * No members yet. Add one above.
+ */
+ noMembersYet: () => LocalizedString
+ /**
+ * Remove member
+ */
+ removeMember: () => LocalizedString
+ /**
+ * Select a tenant from the list to manage its members.
+ */
+ selectTenantPrompt: () => LocalizedString
+ }
+ Forms: {
+ /**
+ * Email
+ */
+ email: () => LocalizedString
+ /**
+ * Email Address
+ */
+ emailAddress: () => LocalizedString
+ /**
+ * you@example.com
+ */
+ emailPlaceholder: () => LocalizedString
+ /**
+ * Enter your email
+ */
+ enterYourEmail: () => LocalizedString
+ /**
+ * Password
+ */
+ password: () => LocalizedString
+ /**
+ * ••••••••
+ */
+ passwordPlaceholder: () => LocalizedString
+ /**
+ * Password (min 8 characters)
+ */
+ passwordMinChars: () => LocalizedString
+ /**
+ * Confirm Password
+ */
+ confirmPassword: () => LocalizedString
+ /**
+ * First Name
+ */
+ firstName: () => LocalizedString
+ /**
+ * John
+ */
+ firstNamePlaceholder: () => LocalizedString
+ /**
+ * Last Name
+ */
+ lastName: () => LocalizedString
+ /**
+ * Doe
+ */
+ lastNamePlaceholder: () => LocalizedString
+ /**
+ * Name
+ */
+ name: () => LocalizedString
+ /**
+ * Your name
+ */
+ namePlaceholder: () => LocalizedString
+ /**
+ * Verification Code
+ */
+ verificationCode: () => LocalizedString
+ /**
+ * Enter 6-digit code
+ */
+ verificationCodePlaceholder: () => LocalizedString
+ /**
+ * Slug
+ */
+ slug: () => LocalizedString
+ }
+ Errors: {
+ /**
+ * Failed to sign in with Google
+ */
+ failedSignInGoogle: () => LocalizedString
+ /**
+ * Failed to send OTP. Please try again.
+ */
+ failedSendOtp: () => LocalizedString
+ /**
+ * Unable to send OTP
+ */
+ unableToSendOtp: () => LocalizedString
+ /**
+ * Invalid OTP. Please try again.
+ */
+ invalidOtp: () => LocalizedString
+ /**
+ * Unable to verify OTP
+ */
+ unableToVerifyOtp: () => LocalizedString
+ /**
+ * Please verify your email address before signing in.
+ */
+ verifyEmailBeforeSignIn: () => LocalizedString
+ /**
+ * This account was created with Google or OTP. Use 'Forgot Password' below to set a password.
+ */
+ accountCreatedWithSocial: () => LocalizedString
+ /**
+ * Invalid email or password.
+ */
+ invalidEmailOrPassword: () => LocalizedString
+ /**
+ * Passwords do not match.
+ */
+ passwordsDoNotMatch: () => LocalizedString
+ /**
+ * Password must be at least 8 characters.
+ */
+ passwordMinLength: () => LocalizedString
+ /**
+ * Password must be at most 128 characters.
+ */
+ passwordMaxLength: () => LocalizedString
+ /**
+ * Password must contain at least one uppercase letter.
+ */
+ passwordUppercase: () => LocalizedString
+ /**
+ * Password must contain at least one lowercase letter.
+ */
+ passwordLowercase: () => LocalizedString
+ /**
+ * Password must contain at least one number.
+ */
+ passwordNumber: () => LocalizedString
+ /**
+ * Password must contain at least one special character.
+ */
+ passwordSpecialChar: () => LocalizedString
+ /**
+ * Failed to reset password. Please try again.
+ */
+ failedResetPassword: () => LocalizedString
+ /**
+ * Failed to change password.
+ */
+ failedChangePassword: () => LocalizedString
+ /**
+ * Password changed successfully.
+ */
+ passwordChangedSuccess: () => LocalizedString
+ /**
+ * Sign up failed. Please try again.
+ */
+ signUpFailed: () => LocalizedString
+ /**
+ * Sign in failed
+ */
+ signInFailed: () => LocalizedString
+ /**
+ * An account with this email already exists. Try signing in instead — if you used Google or OTP, you can set a password from your profile.
+ */
+ accountAlreadyExists: () => LocalizedString
+ /**
+ * {label} is required.
+ */
+ nameRequired: (arg: { label: string }) => LocalizedString
+ /**
+ * {label} must be at least 2 characters.
+ */
+ nameMinLength: (arg: { label: string }) => LocalizedString
+ /**
+ * {label} can only contain letters, spaces, hyphens, and apostrophes.
+ */
+ nameInvalidChars: (arg: { label: string }) => LocalizedString
+ /**
+ * Email is required.
+ */
+ emailRequired: () => LocalizedString
+ /**
+ * Please enter a valid email address.
+ */
+ emailInvalid: () => LocalizedString
+ /**
+ * Password is required.
+ */
+ passwordRequired: () => LocalizedString
+ /**
+ * Slug is required
+ */
+ slugRequired: () => LocalizedString
+ /**
+ * Name is required
+ */
+ nameFieldRequired: () => LocalizedString
+ /**
+ * Slug must be lowercase letters, numbers, and hyphens only
+ */
+ slugInvalid: () => LocalizedString
+ /**
+ * No tenants assigned
+ */
+ noTenantsAssigned: () => LocalizedString
+ /**
+ * You don't have access to any tenants yet. Please contact an administrator to get added to a tenant.
+ */
+ noTenantsMessage: () => LocalizedString
+ /**
+ * Loading your tenants...
+ */
+ loadingYourTenants: () => LocalizedString
+ /**
+ * Setting up your tenant...
+ */
+ settingUpTenant: () => LocalizedString
+ }
+}
+
+export type Formatters = {}
diff --git a/apps/web/i18n/i18n-util.async.ts b/apps/web/i18n/i18n-util.async.ts
new file mode 100644
index 0000000..435644d
--- /dev/null
+++ b/apps/web/i18n/i18n-util.async.ts
@@ -0,0 +1,27 @@
+// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
+/* eslint-disable */
+
+import { initFormatters } from './formatters'
+import type { Locales, Translations } from './i18n-types'
+import { loadedFormatters, loadedLocales, locales } from './i18n-util'
+
+const localeTranslationLoaders = {
+ en: () => import('./en'),
+ nl: () => import('./nl'),
+}
+
+const updateDictionary = (locale: Locales, dictionary: Partial): Translations =>
+ loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary }
+
+export const importLocaleAsync = async (locale: Locales): Promise =>
+ (await localeTranslationLoaders[locale]()).default as unknown as Translations
+
+export const loadLocaleAsync = async (locale: Locales): Promise => {
+ updateDictionary(locale, await importLocaleAsync(locale))
+ loadFormatters(locale)
+}
+
+export const loadAllLocalesAsync = (): Promise => Promise.all(locales.map(loadLocaleAsync))
+
+export const loadFormatters = (locale: Locales): void =>
+ void (loadedFormatters[locale] = initFormatters(locale))
diff --git a/apps/web/i18n/i18n-util.sync.ts b/apps/web/i18n/i18n-util.sync.ts
new file mode 100644
index 0000000..a46617a
--- /dev/null
+++ b/apps/web/i18n/i18n-util.sync.ts
@@ -0,0 +1,26 @@
+// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
+/* eslint-disable */
+
+import { initFormatters } from './formatters'
+import type { Locales, Translations } from './i18n-types'
+import { loadedFormatters, loadedLocales, locales } from './i18n-util'
+
+import en from './en'
+import nl from './nl'
+
+const localeTranslations = {
+ en,
+ nl,
+}
+
+export const loadLocale = (locale: Locales): void => {
+ if (loadedLocales[locale]) return
+
+ loadedLocales[locale] = localeTranslations[locale] as unknown as Translations
+ loadFormatters(locale)
+}
+
+export const loadAllLocales = (): void => locales.forEach(loadLocale)
+
+export const loadFormatters = (locale: Locales): void =>
+ void (loadedFormatters[locale] = initFormatters(locale))
diff --git a/apps/web/i18n/i18n-util.ts b/apps/web/i18n/i18n-util.ts
new file mode 100644
index 0000000..76699b5
--- /dev/null
+++ b/apps/web/i18n/i18n-util.ts
@@ -0,0 +1,38 @@
+// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
+/* eslint-disable */
+
+import { i18n as initI18n, i18nObject as initI18nObject, i18nString as initI18nString } from 'typesafe-i18n'
+import type { LocaleDetector } from 'typesafe-i18n/detectors'
+import type { LocaleTranslationFunctions, TranslateByString } from 'typesafe-i18n'
+import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors'
+import { initExtendDictionary } from 'typesafe-i18n/utils'
+import type { Formatters, Locales, Translations, TranslationFunctions } from './i18n-types'
+
+export const baseLocale: Locales = 'en'
+
+export const locales: Locales[] = [
+ 'en',
+ 'nl'
+]
+
+export const isLocale = (locale: string): locale is Locales => locales.includes(locale as Locales)
+
+export const loadedLocales: Record = {} as Record
+
+export const loadedFormatters: Record = {} as Record
+
+export const extendDictionary = initExtendDictionary()
+
+export const i18nString = (locale: Locales): TranslateByString => initI18nString(locale, loadedFormatters[locale])
+
+export const i18nObject = (locale: Locales): TranslationFunctions =>
+ initI18nObject(
+ locale,
+ loadedLocales[locale],
+ loadedFormatters[locale]
+ )
+
+export const i18n = (): LocaleTranslationFunctions =>
+ initI18n(loadedLocales, loadedFormatters)
+
+export const detectLocale = (...detectors: LocaleDetector[]): Locales => detectLocaleFn(baseLocale, locales, ...detectors)
diff --git a/apps/web/i18n/nl/index.ts b/apps/web/i18n/nl/index.ts
new file mode 100644
index 0000000..94ebb90
--- /dev/null
+++ b/apps/web/i18n/nl/index.ts
@@ -0,0 +1,226 @@
+import type { Translation } from "../i18n-types";
+
+const nl = {
+ Common: {
+ loading: "Laden...",
+ backToHome: "Terug naar Home",
+ back: "Terug",
+ cancel: "Annuleren",
+ save: "Opslaan",
+ add: "Toevoegen",
+ refresh: "Vernieuwen",
+ refreshing: "Vernieuwen...",
+ adding: "Toevoegen...",
+ saving: "Opslaan...",
+ sending: "Verzenden...",
+ or: "Of",
+ roleAdmin: "Beheerder",
+ roleUser: "Gebruiker",
+ you: "Jij",
+ edit: "Bewerken",
+ delete: "Verwijderen",
+ loadingItems: "Items laden...",
+ noItemsYet: "Nog geen items. Voeg er een toe om te beginnen!",
+ itemCount: "{count} {{Item|Items}}",
+ itemCountGlobal: "{count} {{Item|Items}} (globaal)",
+ },
+ Navigation: {
+ fullStack: "Full Stack",
+ profileAndSecurity: "Profiel & Beveiliging",
+ platformTenants: "Platform Tenants",
+ manageMembers: "Leden beheren",
+ signOut: "Uitloggen",
+ switchTenant: "Wissel van tenant",
+ switching: "Wisselen...",
+ selectTenant: "Selecteer tenant",
+ },
+ Home: {
+ title: "Full Stack Boilerplate",
+ subtitle: "NextJs (Tailwind CSS), NestJs, Expo, tRPC, Better Auth",
+ crudDemoTitle: "CRUD Demo",
+ crudDemoDescription: "Test CRUD-operaties met tRPC en Prisma-integratie. Volledige full-stack typeveiligheid demonstratie.",
+ globalCrudDemoTitle: "Globale CRUD Demo",
+ globalCrudDemoDescription: "Hetzelfde als CRUD maar gedeeld over alle tenants. Iedereen ziet en bewerkt dezelfde gegevens.",
+ platformTenantsTitle: "Platform Tenants",
+ platformTenantsDescription: "Maak, bewerk en verwijder tenants op het platform. Alleen voor super-admin.",
+ manageTenants: "Tenants beheren",
+ manageMembersTitle: "Leden beheren",
+ manageMembersDescription: "Voeg leden toe of verwijder ze uit tenants die je beheert.",
+ manageMembers: "Leden beheren",
+ authDemoTitle: "Auth Demo",
+ authDemoDescription: "Test authenticatie met Better Auth. OAuth-integratie met Google, sessiebeheer en beveiligde routes.",
+ tryItOut: "Probeer het",
+ footer: "Gebouwd met moderne tools voor snelle ontwikkeling",
+ },
+ Auth: {
+ signIn: "Inloggen",
+ signInTitle: "Inloggen",
+ signInSubtitle: "Kies je favoriete inlogmethode",
+ signInWithGoogle: "Inloggen met Google",
+ signInWithEmailOtp: "Inloggen met e-mail OTP",
+ orWithEmailPassword: "Of met e-mail & wachtwoord",
+ signInWithEmailPassword: "Inloggen met e-mail & wachtwoord",
+ signingIn: "Inloggen...",
+ signUp: "Registreren",
+ signUpTitle: "Account aanmaken",
+ signUpSubtitle: "Registreer met je e-mailadres en wachtwoord",
+ signUpWithEmailPassword: "Registreren met e-mail & wachtwoord",
+ creatingAccount: "Account aanmaken...",
+ dontHaveAccount: "Heb je nog geen account?",
+ alreadyHaveAccount: "Heb je al een account?",
+ forgotPassword: "Wachtwoord vergeten?",
+ forgotPasswordTitle: "Wachtwoord vergeten",
+ forgotPasswordSubtitle: "Voer je e-mailadres in om een wachtwoord-resetlink te ontvangen",
+ checkYourInbox: "Controleer je inbox",
+ sendResetLink: "Resetlink verzenden",
+ backToSignIn: "Terug naar inloggen",
+ resetLinkSentMessage: "Als er een account bestaat met dit e-mailadres, ontvang je een link om je wachtwoord in te stellen.",
+ setYourPassword: "Stel je wachtwoord in",
+ setPasswordSubtitle: "Voer een nieuw wachtwoord in voor je account",
+ setPassword: "Wachtwoord instellen",
+ settingPassword: "Wachtwoord instellen...",
+ invalidOrExpiredLink: "Ongeldige of verlopen link",
+ invalidOrExpiredLinkMessage: "Deze wachtwoord-resetlink is ongeldig of verlopen. Vraag een nieuwe aan.",
+ requestNewLink: "Nieuwe link aanvragen",
+ verificationFailed: "Verificatie mislukt",
+ verificationFailedExpired: "Deze verificatielink is verlopen. Probeer opnieuw in te loggen om een nieuwe verificatie-e-mail te ontvangen.",
+ verificationFailedInvalid: "Deze verificatielink is ongeldig. Probeer opnieuw in te loggen om een nieuwe verificatie-e-mail te ontvangen.",
+ emailVerified: "E-mail geverifieerd!",
+ emailVerifiedMessage: "Je e-mailadres is succesvol geverifieerd. Je wordt doorgestuurd naar de homepage...",
+ goToHome: "Ga nu naar Home",
+ goToSignIn: "Ga naar inloggen",
+ sendOtp: "OTP verzenden",
+ verifyOtp: "OTP verifiëren",
+ verifying: "Verifiëren...",
+ changeEmail: "E-mail wijzigen",
+ otpSentTo: "We hebben een verificatiecode gestuurd naar {email}",
+ checkInboxMessage: "We hebben een verificatie-e-mail gestuurd naar {email}. Controleer je inbox en klik op de link om je account te verifiëren voordat je inlogt.",
+ passwordSetSuccess: "Wachtwoord succesvol ingesteld. Je kunt nu inloggen.",
+ signInRequired: "Inloggen vereist",
+ signInRequiredMessage: "Je moet ingelogd zijn om deze applicatie te gebruiken.",
+ betterAuthDemo: "Better Auth Demo",
+ welcomeBack: "Welkom terug!",
+ signInToContinue: "Log in om verder te gaan",
+ youAreSignedIn: "Je bent ingelogd",
+ userInformation: "Gebruikersinformatie",
+ signOutButton: "Uitloggen",
+ orEmailPassword: "Of e-mail & wachtwoord",
+ resendVerificationEmail: "Verificatie-e-mail opnieuw verzenden",
+ verificationEmailSent: "Verificatie-e-mail verzonden! Controleer je inbox.",
+ passwordMinChars: "Minimaal 8 tekens met hoofdletter, kleine letter, cijfer en speciaal teken.",
+ },
+ Dashboard: {
+ platformTenantsTitle: "Platform Tenants",
+ platformTenantsSubtitle: "Maak, bewerk en verwijder tenants. Alleen voor super-admin.",
+ mustBeLoggedIn: "Je moet ingelogd zijn om deze pagina te openen.",
+ superAdminRequired: "Super-admin toegang vereist. Je e-mailadres moet in SUPER_ADMIN_EMAILS staan.",
+ addTenant: "Tenant toevoegen",
+ slugLabel: "Slug (a-z, 0-9, koppeltekens)",
+ slugPlaceholder: "bijv. acme-corp",
+ namePlaceholder: "bijv. Acme Corp",
+ tenantCount: "{count} tenant{{s|}}",
+ loadingTenants: "Tenants laden...",
+ noTenantsYet: "Nog geen tenants. Voeg er hierboven een toe.",
+ refreshList: "Lijst vernieuwen",
+ deleteConfirm: "Tenant \"{slug}\" verwijderen? Dit kan niet ongedaan worden gemaakt.",
+ dualDatabaseCrudDemo: "Dubbele Database CRUD Demo",
+ crudDemoSubtitle: "Vergelijking naast elkaar van Mongoose (MongoDB) en Prisma (PostgreSQL)",
+ crudDemoTechStack: "NextJs (TailwindCSS) \u2022 NestJs \u2022 tRPC \u2022 Transacties",
+ tenantDashboardLink: "\u2192 Tenant Dashboard (super-admin)",
+ globalCrudDemoTitle: "Globale CRUD Demo",
+ globalCrudDemoSubtitle: "Hetzelfde als CRUD maar gedeeld over alle tenants. Iedereen ziet en bewerkt dezelfde gegevens.",
+ globalCrudDemoTechStack: "Mongoose (MongoDB) & Prisma (PostgreSQL) \u2022 Geen tenant scope",
+ addTextPlaceholder: "Voeg hier tekst toe",
+ },
+ Settings: {
+ profileTitle: "Profiel",
+ profileSubtitle: "Beheer je accountbeveiliging en gekoppelde inlogmethoden.",
+ emailNotVerified: "Je e-mail is niet geverifieerd",
+ verifyEmailPrompt: "Verifieer je e-mail om alle functies te gebruiken.",
+ accountInfo: "Accountinformatie",
+ linkedAccounts: "Gekoppelde accounts",
+ google: "Google",
+ connected: "Verbonden",
+ connectGoogle: "Google koppelen",
+ emailAndPassword: "E-mail & wachtwoord",
+ passwordSet: "Wachtwoord ingesteld",
+ notSet: "Niet ingesteld",
+ passwordTitle: "Wachtwoord",
+ changePassword: "Wachtwoord wijzigen",
+ currentPassword: "Huidig wachtwoord",
+ newPasswordLabel: "Nieuw wachtwoord (min. 8 tekens)",
+ confirmNewPassword: "Bevestig nieuw wachtwoord",
+ savePassword: "Wachtwoord opslaan",
+ addPasswordDescription: "Voeg een wachtwoord toe om in te loggen zonder Google of OTP.",
+ checkInboxForPassword: "Controleer je inbox om je wachtwoord in te stellen.",
+ manageMembersTitle: "Leden beheren",
+ manageMembersSubtitle: "Voeg leden toe of verwijder ze uit je tenants.",
+ noAdminAccess: "Geen beheerderstoegang",
+ noAdminAccessMessage: "Je hebt geen beheerderstoegang tot tenants.",
+ yourTenants: "Je tenants ({count})",
+ membersOf: "Leden van {name}",
+ addMember: "Lid toevoegen",
+ memberEmailPlaceholder: "gebruiker@voorbeeld.nl",
+ loadingMembers: "Leden laden...",
+ noMembersYet: "Nog geen leden. Voeg er hierboven een toe.",
+ removeMember: "Lid verwijderen",
+ selectTenantPrompt: "Selecteer een tenant uit de lijst om de leden te beheren.",
+ },
+ Forms: {
+ email: "E-mail",
+ emailAddress: "E-mailadres",
+ emailPlaceholder: "jij@voorbeeld.nl",
+ enterYourEmail: "Voer je e-mailadres in",
+ password: "Wachtwoord",
+ passwordPlaceholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
+ passwordMinChars: "Wachtwoord (min. 8 tekens)",
+ confirmPassword: "Bevestig wachtwoord",
+ firstName: "Voornaam",
+ firstNamePlaceholder: "Jan",
+ lastName: "Achternaam",
+ lastNamePlaceholder: "Jansen",
+ name: "Naam",
+ namePlaceholder: "Je naam",
+ verificationCode: "Verificatiecode",
+ verificationCodePlaceholder: "Voer 6-cijferige code in",
+ slug: "Slug",
+ },
+ Errors: {
+ failedSignInGoogle: "Inloggen met Google mislukt",
+ failedSendOtp: "OTP verzenden mislukt. Probeer het opnieuw.",
+ unableToSendOtp: "Kan OTP niet verzenden",
+ invalidOtp: "Ongeldige OTP. Probeer het opnieuw.",
+ unableToVerifyOtp: "Kan OTP niet verifiëren",
+ verifyEmailBeforeSignIn: "Verifieer je e-mailadres voordat je inlogt.",
+ accountCreatedWithSocial: "Dit account is aangemaakt met Google of OTP. Gebruik 'Wachtwoord vergeten' hieronder om een wachtwoord in te stellen.",
+ invalidEmailOrPassword: "Ongeldig e-mailadres of wachtwoord.",
+ passwordsDoNotMatch: "Wachtwoorden komen niet overeen.",
+ passwordMinLength: "Wachtwoord moet minimaal 8 tekens bevatten.",
+ passwordMaxLength: "Wachtwoord mag maximaal 128 tekens bevatten.",
+ passwordUppercase: "Wachtwoord moet minstens één hoofdletter bevatten.",
+ passwordLowercase: "Wachtwoord moet minstens één kleine letter bevatten.",
+ passwordNumber: "Wachtwoord moet minstens één cijfer bevatten.",
+ passwordSpecialChar: "Wachtwoord moet minstens één speciaal teken bevatten.",
+ failedResetPassword: "Wachtwoord resetten mislukt. Probeer het opnieuw.",
+ failedChangePassword: "Wachtwoord wijzigen mislukt.",
+ passwordChangedSuccess: "Wachtwoord succesvol gewijzigd.",
+ signUpFailed: "Registratie mislukt. Probeer het opnieuw.",
+ signInFailed: "Inloggen mislukt",
+ accountAlreadyExists: "Er bestaat al een account met dit e-mailadres. Probeer in te loggen \u2014 als je Google of OTP hebt gebruikt, kun je een wachtwoord instellen vanuit je profiel.",
+ nameRequired: "{label} is verplicht.",
+ nameMinLength: "{label} moet minimaal 2 tekens bevatten.",
+ nameInvalidChars: "{label} mag alleen letters, spaties, koppeltekens en apostrofs bevatten.",
+ emailRequired: "E-mail is verplicht.",
+ emailInvalid: "Voer een geldig e-mailadres in.",
+ passwordRequired: "Wachtwoord is verplicht.",
+ slugRequired: "Slug is verplicht",
+ nameFieldRequired: "Naam is verplicht",
+ slugInvalid: "Slug mag alleen kleine letters, cijfers en koppeltekens bevatten",
+ noTenantsAssigned: "Geen tenants toegewezen",
+ noTenantsMessage: "Je hebt nog geen toegang tot tenants. Neem contact op met een beheerder om aan een tenant te worden toegevoegd.",
+ loadingYourTenants: "Je tenants laden...",
+ settingUpTenant: "Je tenant instellen...",
+ },
+} satisfies Translation;
+
+export default nl;
diff --git a/apps/web/package.json b/apps/web/package.json
index 0299c75..b1d3e9a 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -9,12 +9,13 @@
"start": "dotenvx run -- next start",
"lint": "eslint . --max-warnings 0",
"lint:fix": "eslint . --fix",
- "check-types": "tsc --noEmit"
+ "check-types": "tsc --noEmit",
+ "i18n:generate": "typesafe-i18n"
},
"dependencies": {
"@repo/trpc": "workspace:*",
- "@repo/utils-core": "workspace:*",
"@repo/ui": "workspace:*",
+ "@repo/utils-core": "workspace:*",
"@tanstack/react-query": "5.76.1",
"@trpc/react-query": "11.8.1",
"next": "^16.1.4",
@@ -33,6 +34,7 @@
"eslint-config-next": "^15.5.6",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.14",
+ "typesafe-i18n": "^5.27.1",
"typescript": "5.9.2"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 28779cc..24af632 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -365,6 +365,9 @@ importers:
tailwindcss:
specifier: ^4.1.14
version: 4.1.18
+ typesafe-i18n:
+ specifier: ^5.27.1
+ version: 5.27.1(typescript@5.9.2)
typescript:
specifier: 5.9.2
version: 5.9.2
@@ -7507,6 +7510,12 @@ packages:
typedarray@0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
+ typesafe-i18n@5.27.1:
+ resolution: {integrity: sha512-749uWo2ZXETT//kWjVYPm8QPYR8xLh8G0wLfoAyCAtAmysX67uCaAyLjAjAWojL6fuJpE5B6yIjwvO9orXzUPg==}
+ hasBin: true
+ peerDependencies:
+ typescript: '>=3.5.1'
+
typescript-eslint@8.50.1:
resolution: {integrity: sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -16416,6 +16425,10 @@ snapshots:
typedarray@0.0.6: {}
+ typesafe-i18n@5.27.1(typescript@5.9.2):
+ dependencies:
+ typescript: 5.9.2
+
typescript-eslint@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2):
dependencies:
'@typescript-eslint/eslint-plugin': 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2)
From eda449623c349cbd09d6207e4c1a978a211f042a Mon Sep 17 00:00:00 2001
From: Anns Shahbaz
Date: Thu, 19 Feb 2026 12:33:51 +0500
Subject: [PATCH 2/5] fixes
---
apps/web/app/components/app-shell.tsx | 12 +-
apps/web/app/components/nav-header.tsx | 270 ++++++++++++----------
apps/web/app/hooks/useI18n.ts | 4 +-
apps/web/app/providers/LocaleProvider.tsx | 34 ++-
4 files changed, 171 insertions(+), 149 deletions(-)
diff --git a/apps/web/app/components/app-shell.tsx b/apps/web/app/components/app-shell.tsx
index 9f87748..fd34ad5 100644
--- a/apps/web/app/components/app-shell.tsx
+++ b/apps/web/app/components/app-shell.tsx
@@ -8,12 +8,12 @@ import { LocaleProvider } from "../providers/LocaleProvider";
export function AppShell({ children }: { children: React.ReactNode }) {
return (
-
-
-
-