Skip to content

Commit 40c28ec

Browse files
committed
add i18n
1 parent 8f0b584 commit 40c28ec

File tree

31 files changed

+701
-105
lines changed

31 files changed

+701
-105
lines changed

app/(default)/layout.tsx

-13
This file was deleted.

app/(default)/page.tsx

-23
This file was deleted.

app/(default)/_components/footer/index.tsx app/[lang]/(default)/_components/footer/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Social from "../social";
22

3-
export default function () {
3+
export default function ({ dict }: { dict: any }) {
44
return (
55
<section>
66
<div className="w-screen flex-col bg-black px-6 py-20 text-white lg:flex lg:px-10 xl:px-24">
@@ -10,7 +10,7 @@ export default function () {
1010
Sora.FM
1111
</a>
1212
<p className="font-inter mt-4 max-w-[350px] text-base font-light text-gray-500">
13-
Sora AI Video Showcases
13+
{dict.brand.title}
1414
</p>
1515
<div className="mb-8 mt-6 flex flex-row">
1616
<Social />

app/(default)/_components/header/index.tsx app/[lang]/(default)/_components/header/index.tsx

+13-9
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
1+
import Langswitch from "../langswitch";
12
import { Nav } from "@/types/nav";
23
import Social from "../social";
34

4-
export default function () {
5+
export default function ({ lang, dict }: { lang: string; dict: any }) {
56
const navigations: Nav[] = [];
67

78
return (
89
<header>
910
<div className="h-auto w-screen">
1011
<nav className="font-inter mx-auto h-auto w-full max-w-7xl lg:relative lg:top-0">
1112
<div className="flex flex-row items-center px-6 py-8 lg:flex-row lg:items-center lg:justify-between lg:px-10 lg:py-8 xl:px-10">
12-
<a href="/" className="text-xl font-medium flex items-center">
13-
{/* <img
14-
src="/logo.png"
15-
className="w-10 h-10 rounded-full mr-3"
16-
alt="logo"
17-
/> */}
18-
<span className="font-bold text-primary text-2xl">Sora.FM</span>
13+
<a
14+
href={`/${lang}/`}
15+
className="text-xl font-medium flex items-center"
16+
>
17+
<span className="font-bold text-primary text-2xl">
18+
{dict.brand.name}
19+
</span>
1920
</a>
2021

2122
<div className="hidden md:flex ml-16">
@@ -33,9 +34,12 @@ export default function () {
3334
<div className="flex-1"></div>
3435

3536
<div className="flex flex-row items-center lg:flex lg:flex-row lg:space-x-3 lg:space-y-0">
36-
<div className=" md:block mr-4 text-white">
37+
<div className="md:block mr-4 text-white">
3738
<Social />
3839
</div>
40+
<div>
41+
<Langswitch />
42+
</div>
3943
</div>
4044
<a href="#" className="absolute right-5 lg:hidden"></a>
4145
</div>

app/(default)/_components/hero/index.tsx app/[lang]/(default)/_components/hero/index.tsx

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
import Input from "../input";
22
import Producthunt from "../producthunt";
33

4-
export default function () {
4+
export default function ({ dict }: { dict: any }) {
55
return (
66
<div className="mx-auto mt-4 max-w-full sm:mt-4 sm:px-0 lg:px-0">
77
<div className="relative isolate overflow-hidden bg-gray-900 px-6 py-12 shadow-2xl sm:rounded-3xl sm:px-24 xl:pt-32">
88
<h1 className="mx-auto max-w-4xl text-center text-3xl font-bold tracking-tight text-primary sm:text-6xl">
9-
Sora AI Video Showcases
9+
{dict.brand.title}
1010
</h1>
1111
<h2 className="mx-auto mt-6 max-w-xl text-center text-xl leading-8 text-gray-300">
12-
Discover awesome AI Videos made with Sora.
12+
{dict.brand.sub_title}
1313
</h2>
1414

15-
<Input />
15+
<Input dict={dict} />
1616

1717
<div className="mx-auto max-w-6xl text-slate-400 text-sm mt-4 text-center">
18-
Sora text-to-video API is not available, we'll get you notified when
19-
it's live.
18+
{dict.subscribe.tip}
2019
</div>
2120

2221
<svg

app/(default)/_components/input/index.tsx app/[lang]/(default)/_components/input/index.tsx

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"use client";
22

33
import { KeyboardEvent, useState } from "react";
4+
import { useParams, useRouter } from "next/navigation";
45

56
import { toast } from "sonner";
6-
import { useRouter } from "next/navigation";
77

8-
export default function () {
8+
export default function ({ dict }: { dict: any }) {
9+
const params = useParams();
10+
911
const router = useRouter();
1012
const [email, setEmail] = useState("");
1113

@@ -56,7 +58,7 @@ export default function () {
5658
type="email"
5759
required
5860
className="min-w-0 flex-auto rounded-md border-0 bg-white/5 px-3.5 py-2 text-white shadow-sm ring-1 ring-inset ring-white/10 focus:ring-2 focus:ring-inset focus:ring-white sm:text-sm sm:leading-6"
59-
placeholder="Enter your email"
61+
placeholder={dict.subscribe.placeholder}
6062
value={email}
6163
onChange={(e) => setEmail(e.target.value)}
6264
onKeyDown={handleInputKeydown}
@@ -66,7 +68,7 @@ export default function () {
6668
className="flex-none rounded-md bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
6769
onClick={handleSubmit}
6870
>
69-
Subscribe
71+
{dict.subscribe.button}
7072
</button>
7173
</div>
7274
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"use client";
2+
3+
import {
4+
Select,
5+
SelectContent,
6+
SelectItem,
7+
SelectTrigger,
8+
SelectValue,
9+
} from "@/components/ui/select";
10+
import { useParams, usePathname, useRouter } from "next/navigation";
11+
12+
import { localeNames } from "@/services/i18n";
13+
14+
export default function () {
15+
const params = useParams();
16+
const lang = params.lang as string;
17+
const router = useRouter();
18+
const pathname = usePathname();
19+
20+
const handleSwitchLanguage = (value: string) => {
21+
if (value !== "lang") {
22+
const newPathName = pathname.replace(lang, value);
23+
router.push(newPathName);
24+
}
25+
};
26+
27+
return (
28+
<Select value={lang} onValueChange={handleSwitchLanguage}>
29+
<SelectTrigger className="w-fit">
30+
<SelectValue placeholder="Language" />
31+
</SelectTrigger>
32+
<SelectContent>
33+
{Object.keys(localeNames).map((key: string) => {
34+
const name = localeNames[key];
35+
return (
36+
<SelectItem className="cursor-pointer" key={key} value={key}>
37+
{name}
38+
</SelectItem>
39+
);
40+
})}
41+
</SelectContent>
42+
</Select>
43+
);
44+
}

app/(default)/_components/videos/index.tsx app/[lang]/(default)/_components/videos/index.tsx

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
"use client";
2-
31
import { Video } from "@/types/video";
42

5-
export default ({ videos }: { videos: Video[] }) => {
3+
export default ({ lang, videos }: { lang: string; videos: Video[] }) => {
64
return (
75
<section className="relative">
86
<div className="mx-auto max-w-7xl px-5 py-4 md:px-10 md:py-4 lg:py-4">
97
<div className="mb-8 gap-5 py-4 [column-count:1] md:mb-12 md:[column-count:2] lg:mb-16 lg:[column-count:3]">
108
{videos.map((video: Video, idx: number) => {
119
return (
1210
<div key={idx}>
13-
<a href={`/video/${video.uuid}`} target="_self">
11+
<a href={`/${lang}/video/${video.uuid}`} target="_self">
1412
<div className="mb-6 gap-6 overflow-hidden rounded-2xl border border-solid border-black shadow-lg bg-gray-900 text-white p-4">
1513
<video
1614
className="video w-full cursor-pointer rounded-md"

app/[lang]/(default)/layout.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Footer from "./_components/footer";
2+
import Header from "./_components/header";
3+
import { ReactNode } from "react";
4+
import { getDictionary } from "@/services/i18n";
5+
6+
export default async function ({
7+
children,
8+
params,
9+
}: {
10+
children: ReactNode;
11+
params: { lang: string };
12+
}) {
13+
const dict = await getDictionary(params.lang);
14+
15+
return (
16+
<div className="bg-gray-900">
17+
<Header lang={params.lang} dict={dict} />
18+
<main>{children}</main>
19+
<Footer dict={dict} />
20+
</div>
21+
);
22+
}

app/[lang]/(default)/page.tsx

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Hero from "./_components/hero";
2+
import { Metadata } from "next";
3+
import Videos from "./_components/videos";
4+
import { getDictionary } from "@/services/i18n";
5+
import { getVideos } from "@/models/video";
6+
7+
export async function generateMetadata({
8+
params,
9+
}: {
10+
params: { lang: string };
11+
}): Promise<Metadata> {
12+
return {
13+
alternates: {
14+
canonical: `${process.env.WEB_BASE_URI}/${params.lang}`,
15+
},
16+
};
17+
}
18+
19+
export default async function ({ params }: { params: { lang: string } }) {
20+
const videos = await getVideos(1, 50);
21+
const dict = await getDictionary(params.lang);
22+
23+
return (
24+
<div>
25+
<Hero dict={dict} />
26+
<Videos lang={params.lang} videos={videos} />
27+
</div>
28+
);
29+
}

app/(default)/video/[uuid]/page.tsx app/[lang]/(default)/video/[uuid]/page.tsx

+16-8
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import { findVideoByUuid, getRandVideos } from "@/models/video";
22

33
import { Metadata } from "next";
44
import Videos from "../../_components/videos";
5+
import { getDictionary } from "@/services/i18n";
56

67
export async function generateMetadata({
78
params,
89
}: {
9-
params: { uuid: string };
10+
params: { lang: string; uuid: string };
1011
}): Promise<Metadata> {
12+
const dict = await getDictionary(params.lang);
13+
1114
let description = "";
1215

1316
if (params.uuid) {
@@ -18,25 +21,30 @@ export async function generateMetadata({
1821
}
1922

2023
return {
21-
description: `${description} Sora AI Video Showcases | Sora.FM`,
24+
description: `${description} ${dict.brand.title} | Sora.FM`,
2225
alternates: {
23-
canonical: `${process.env.WEB_BASE_URI}/video/${params.uuid}`,
26+
canonical: `${process.env.WEB_BASE_URI}/${params.lang}/video/${params.uuid}`,
2427
},
2528
};
2629
}
2730

28-
export default async function ({ params }: { params: { uuid: string } }) {
31+
export default async function ({
32+
params,
33+
}: {
34+
params: { lang: string; uuid: string };
35+
}) {
36+
const dict = await getDictionary(params.lang);
2937
const video = await findVideoByUuid(params.uuid);
3038
const videos = await getRandVideos(1, 50);
3139

3240
return (
3341
<div className="mx-auto mt-4 max-w-full sm:mt-4 sm:px-0 lg:px-0">
3442
<div className="relative isolate overflow-hidden bg-gray-900 px-2 py-12 shadow-2xl sm:rounded-3xl sm:px-24 xl:py-32">
3543
<h1 className="mx-auto max-w-4xl text-center text-3xl font-bold tracking-tight text-primary sm:text-6xl">
36-
Sora AI Video Showcase
44+
{dict.showcase.title}
3745
</h1>
3846
<p className="mx-auto mt-2 px-4 max-w-xl text-center text-xl leading-8 text-gray-300">
39-
This video is made with Sora, by OpenAI's red team.
47+
{dict.showcase.sub_title}
4048
</p>
4149

4250
{video && (
@@ -108,10 +116,10 @@ export default async function ({ params }: { params: { uuid: string } }) {
108116

109117
<div className="relative isolate overflow-hidden bg-gray-900 px-2 py-4 shadow-2xl sm:rounded-3xl sm:px-12 xl:py-12">
110118
<h2 className="mx-auto mt-2 max-w-xl text-center text-xl leading-8 text-gray-300">
111-
More AI Video Showcases
119+
{dict.showcase.more_tip}
112120
</h2>
113121

114-
<Videos videos={videos} />
122+
<Videos lang={params.lang} videos={videos} />
115123
</div>
116124
</div>
117125
);
File renamed without changes.
File renamed without changes.
File renamed without changes.

app/[lang]/layout.tsx

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import "./globals.css";
2+
3+
import { Analytics } from "@vercel/analytics/react";
4+
import { Inter } from "next/font/google";
5+
import type { Metadata } from "next";
6+
import { Toaster } from "sonner";
7+
import { getDictionary } from "@/services/i18n";
8+
9+
const inter = Inter({ subsets: ["latin"] });
10+
11+
export async function generateMetadata({
12+
params,
13+
}: {
14+
params: { lang: string };
15+
}): Promise<Metadata> {
16+
const dict = await getDictionary(params.lang);
17+
18+
return {
19+
title: {
20+
template: `%s, ${dict.brand.title} | Sora.FM`,
21+
default: `${dict.brand.title} | Sora.FM`,
22+
},
23+
description: `${dict.brand.title}, ${dict.brand.sub_title}`,
24+
keywords:
25+
"sora,sora fm,sora ai,openai sora,video ai,ai video,sora video,ai video generator,text to video,sora ai video,sora ai video generator,sora webui,sora showcase,sora ai showcases",
26+
};
27+
}
28+
29+
export default function RootLayout({
30+
children,
31+
params: { lang },
32+
}: Readonly<{
33+
children: React.ReactNode;
34+
params: { lang: string };
35+
}>) {
36+
return (
37+
<html lang={lang}>
38+
<head>
39+
<link rel="icon" href="/favicon.ico" />
40+
</head>
41+
<body className={inter.className}>
42+
<Toaster position="top-center" richColors />
43+
{children}
44+
<Analytics />
45+
</body>
46+
</html>
47+
);
48+
}

0 commit comments

Comments
 (0)