Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions LOCADEX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 🌐 Locadex i18n

This repository is configured to use Locadex for automated internationalisation.

## Configuration:

- **Working Directory**: `./apps/snow-leopard`
- **Branch Prefix**: `locadex/`
- **Configured Locales**: `en-GB`, `es`, `ar`, `zh`
- **Local Translations**: Enabled

## How it works:

- Locadex will automatically analyse your code for translatable content every time you open a PR
- Locadex will modify your build command to automatically generate translations for your content in your configured locales
- Locadex will push its changes to your PR branch, which you can review and merge

## Next Steps:
1. **Get API Keys**: Visit [General Translation Dashboard](https://dash.generaltranslation.com) to generate API Keys
2. **Add API Keys**: Add a Production API Key and Project ID to your project CI workflow to keep your translations up to date
3. In development, using a Development API Key will allow you to hot-reload translations in your app as you make changes

---

Generated by [Locadex](https://generaltranslation.com) • [Documentation](https://generaltranslation.com/docs)
7 changes: 5 additions & 2 deletions apps/snow-leopard/app/(auth)/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Session, User } from '@/lib/auth';
import { db } from '@snow-leopard/db';
import * as schema from '@snow-leopard/db';
import { eq } from 'drizzle-orm';
import { getGT } from 'gt-next/server';

export async function getSession(): Promise<Session | null> {
try {
Expand Down Expand Up @@ -53,7 +54,8 @@ export async function getUserDetails(): Promise<typeof schema.user.$inferSelect
export async function requireAuth(): Promise<User> {
const user = await getUser();
if (!user) {
throw new Error('Authentication required. User not found in session.');
const t = await getGT();
throw new Error(t('Authentication required. User not found in session.'));
}
return user;
}
Expand All @@ -65,6 +67,7 @@ export async function requireAuth(): Promise<User> {
export async function requireUnauth(): Promise<void> {
const user = await getUser();
if (user) {
throw new Error('User is already authenticated.');
const t = await getGT();
throw new Error(t('User is already authenticated.'));
}
}
42 changes: 25 additions & 17 deletions apps/snow-leopard/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { authClient } from '@/lib/auth-client';
import { toast } from '@/components/toast';
import { AuthForm } from '@/components/auth-form';
import { SubmitButton } from '@/components/submit-button';
import { T, useGT } from 'gt-next';

// Client-side check for enabled providers
const googleEnabled = process.env.NEXT_PUBLIC_GOOGLE_ENABLED === 'true';
Expand All @@ -18,6 +19,7 @@ export default function LoginPage() {
const [isSocialLoading, setIsSocialLoading] = useState<string | null>(null);
const [isSuccessful, setIsSuccessful] = useState(false);
const [email, setEmail] = useState('');
const t = useGT();

const handleEmailLogin = async (formData: FormData) => {
const currentEmail = formData.get('email') as string;
Expand All @@ -39,7 +41,7 @@ export default function LoginPage() {
setIsSuccessful(true);
toast({
type: 'success',
description: 'Signed in successfully! Redirecting...'
description: t('Signed in successfully! Redirecting...')
});
router.refresh();
},
Expand All @@ -49,7 +51,7 @@ export default function LoginPage() {
console.error("Email Login Error:", ctx.error);
toast({
type: 'error',
description: ctx.error.message || 'Failed to sign in.',
description: ctx.error.message || t('Failed to sign in.'),
});
},
});
Expand All @@ -72,7 +74,7 @@ export default function LoginPage() {
console.error(`Social Login Error (${provider}):`, ctx.error);
toast({
type: 'error',
description: ctx.error.message || `Failed to sign in with ${provider}.`,
description: ctx.error.message || t('Failed to sign in with {provider}.', { provider }),
});
},
});
Expand All @@ -82,10 +84,14 @@ export default function LoginPage() {
<div className="flex h-dvh w-screen items-start pt-12 md:pt-0 md:items-center justify-center bg-background">
<div className="w-full max-w-md overflow-hidden rounded-2xl flex flex-col gap-12">
<div className="flex flex-col items-center justify-center gap-2 px-8 text-center">
<h3 className="text-xl font-semibold dark:text-zinc-50">Sign In</h3>
<p className="text-sm text-gray-500 dark:text-zinc-400">
Sign in with your email and password
</p>
<T>
<h3 className="text-xl font-semibold dark:text-zinc-50">Sign In</h3>
</T>
<T>
<p className="text-sm text-gray-500 dark:text-zinc-400">
Sign in with your email and password
</p>
</T>
</div>

<div className="px-8">
Expand All @@ -102,20 +108,22 @@ export default function LoginPage() {
<SubmitButton
isSuccessful={isSuccessful}
>
Sign In
<T>Sign In</T>
</SubmitButton>
</AuthForm>
</div>

<p className="text-center text-sm text-gray-600 dark:text-zinc-400">
{"Don't have an account? "}
<Link
href="/register"
className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
>
Sign up
</Link>
</p>
<T>
<p className="text-center text-sm text-gray-600 dark:text-zinc-400">
{"Don't have an account? "}
<Link
href="/register"
className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
>
Sign up
</Link>
</p>
</T>
</div>
</div>
);
Expand Down
46 changes: 27 additions & 19 deletions apps/snow-leopard/app/(auth)/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { authClient } from '@/lib/auth-client';
import { toast } from '@/components/toast';
import { AuthForm } from '@/components/auth-form';
import { SubmitButton } from '@/components/submit-button';
import { T, useGT } from 'gt-next';

const googleEnabled = process.env.NEXT_PUBLIC_GOOGLE_ENABLED === 'true';
const githubEnabled = process.env.NEXT_PUBLIC_GITHUB_ENABLED === 'true';
Expand All @@ -18,6 +19,7 @@ export default function RegisterPage() {
const [isSocialLoading, setIsSocialLoading] = useState<string | null>(null);
const [isSuccessful, setIsSuccessful] = useState(false);
const [email, setEmail] = useState('');
const t = useGT();

const handleEmailSignup = async (formData: FormData) => {
const emailValue = formData.get('email') as string;
Expand All @@ -41,14 +43,14 @@ export default function RegisterPage() {
setIsSuccessful(false);
toast({
type: 'success',
description: 'Account created! Check your email to verify.'
description: t('Account created! Check your email to verify.')
});
router.push('/login');
} else {
setIsSuccessful(true);
toast({
type: 'success',
description: 'Account created! Redirecting...'
description: t('Account created! Redirecting...')
});
router.push('/documents');
}
Expand All @@ -59,7 +61,7 @@ export default function RegisterPage() {
console.error("Email Signup Error:", ctx.error);
toast({
type: 'error',
description: ctx.error.message || 'Failed to create account.',
description: ctx.error.message || t('Failed to create account.'),
});
},
});
Expand All @@ -82,7 +84,7 @@ export default function RegisterPage() {
console.error(`Social Sign Up/In Error (${provider}):`, ctx.error);
toast({
type: 'error',
description: ctx.error.message || `Failed to sign up/in with ${provider}.`,
description: ctx.error.message || t('Failed to sign up/in with {provider}.', { provider }),
});
},
});
Expand All @@ -92,10 +94,14 @@ export default function RegisterPage() {
<div className="flex h-dvh w-screen items-start pt-12 md:pt-0 md:items-center justify-center bg-background">
<div className="w-full max-w-md overflow-hidden rounded-2xl flex flex-col gap-12">
<div className="flex flex-col items-center justify-center gap-2 px-8 text-center">
<h3 className="text-xl font-semibold dark:text-zinc-50">Sign Up</h3>
<p className="text-sm text-gray-500 dark:text-zinc-400">
Create your account with email and password
</p>
<T>
<h3 className="text-xl font-semibold dark:text-zinc-50">Sign Up</h3>
</T>
<T>
<p className="text-sm text-gray-500 dark:text-zinc-400">
Create your account with email and password
</p>
</T>
</div>

<div className="px-8 flex flex-col gap-6">
Expand All @@ -112,22 +118,24 @@ export default function RegisterPage() {
<SubmitButton
isSuccessful={isSuccessful}
>
Sign Up
<T>Sign Up</T>
</SubmitButton>
</AuthForm>
</div>

<div className="text-center">
<p className="text-sm text-gray-600 dark:text-zinc-400">
{'Already have an account? '}
<Link
href="/login"
className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
>
Sign in
</Link>
{' instead.'}
</p>
<T>
<p className="text-sm text-gray-600 dark:text-zinc-400">
{'Already have an account? '}
<Link
href="/login"
className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
>
Sign in
</Link>
{' instead.'}
</p>
</T>
</div>
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion apps/snow-leopard/app/[author]/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import AIChatWidget from '@/components/ai-chat-widget';
import ThemeToggle from '@/components/theme-toggle';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { T } from 'gt-next';

export default async function Page({ params }: any) {
const { author, slug } = await params;
Expand Down Expand Up @@ -36,7 +37,7 @@ export default async function Page({ params }: any) {
<ThemeToggle />
<Link href="/register">
<Button variant="outline" className="fixed top-4 right-4 z-50">
Sign up to Snow Leopard
<T>Sign up to Snow Leopard</T>
</Button>
</Link>
<Blog
Expand Down
10 changes: 6 additions & 4 deletions apps/snow-leopard/app/api/document/actions/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,28 @@ import { NextRequest, NextResponse } from 'next/server';
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { updateDocumentPublishSettings, getActiveSubscriptionByUserId } from "@/lib/db/queries";
import { getGT } from 'gt-next/server';

export async function publishDocument(request: NextRequest, body: any): Promise<NextResponse> {
const readonlyHeaders = await headers();
const requestHeaders = new Headers(readonlyHeaders);
const session = await auth.api.getSession({ headers: requestHeaders });
const t = await getGT();
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
return NextResponse.json({ error: t('Unauthorized') }, { status: 401 });
}
const userId = session.user.id;

if (process.env.STRIPE_ENABLED === 'true') {
const subscription = await getActiveSubscriptionByUserId({ userId });
if (!subscription) {
return NextResponse.json({ error: 'Payment Required: publishing is pro-only' }, { status: 402 });
return NextResponse.json({ error: t('Payment Required: publishing is pro-only') }, { status: 402 });
}
}

const { id: documentId, visibility, author, style, slug } = body;
if (!documentId || !slug) {
return NextResponse.json({ error: 'Invalid parameters' }, { status: 400 });
return NextResponse.json({ error: t('Invalid parameters') }, { status: 400 });
}

try {
Expand All @@ -32,6 +34,6 @@ export async function publishDocument(request: NextRequest, body: any): Promise<
if (typeof error?.message === 'string' && error.message.toLowerCase().includes('already published')) {
return NextResponse.json({ error: error.message }, { status: 409 });
}
return NextResponse.json({ error: error.message || 'Failed to update publish settings' }, { status: 500 });
return NextResponse.json({ error: error.message || t('Failed to update publish settings') }, { status: 500 });
}
}
6 changes: 5 additions & 1 deletion apps/snow-leopard/app/api/document/actions/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getCurrentDocumentByTitle,
getDocumentById
} from '@/lib/db/queries'; // Import Drizzle queries
import { getGT } from 'gt-next/server';

/**
* Gets a file by path - attempts to match ID first, then title.
Expand Down Expand Up @@ -97,11 +98,14 @@ export async function searchDocuments({
query: query,
limit: limit
});

// Get translation function
const t = await getGT();

// Format results for the mention UI (same logic)
const results = documents?.map(doc => ({
id: doc.id,
title: doc.title || 'Untitled Document',
title: doc.title || t('Untitled Document'),
type: 'document' // Assuming a type identifier is needed
})) || [];

Expand Down
6 changes: 4 additions & 2 deletions apps/snow-leopard/app/api/document/publish/route.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { NextRequest, NextResponse } from 'next/server';
import { publishDocument } from '../actions/publish';
import { getGT } from 'gt-next/server';

export async function POST(request: NextRequest) {
let body: any;
const t = await getGT();
try {
body = await request.json();
} catch (error: any) {
console.error('[API /document/publish] Invalid JSON:', error);
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
return NextResponse.json({ error: t('Invalid JSON body') }, { status: 400 });
}
try {
return await publishDocument(request, body);
} catch (error: any) {
console.error('[API /document/publish] Error handling publish:', error);
return NextResponse.json({ error: error.message || 'Error publishing document' }, { status: 500 });
return NextResponse.json({ error: error.message || t('Error publishing document') }, { status: 500 });
}
}
Loading