Skip to content

Commit 71a0c18

Browse files
committed
♻️ refactor i18n
1 parent 26dfc9c commit 71a0c18

31 files changed

+95
-111
lines changed

Diff for: README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Welcome to My Blog, a simple blog built with Next.js 13 and Markdown for content
66

77
- Next.js 13
88
- React 18
9+
- React Spring
910
- TypeScript
1011
- Tailwind CSS
1112
- I18n
@@ -24,10 +25,10 @@ Welcome to My Blog, a simple blog built with Next.js 13 and Markdown for content
2425
## Project Structure
2526

2627
- `data/`: Contains data-related files.
28+
- `i18n/`: Internationalization and localization files.
2729
- `posts/`: Markdown files for blog posts.
2830
- `giscus.ts`: Configuration for the Giscus comment system.
2931
- `metadata.ts`: Metadata and configuration for the blog.
30-
- `i18n/`: Internationalization and localization files.
3132
- `public/static/`: Public static images and other assets.
3233
- `src/`: The main source code directory.
3334
- `app/`: Next.js pages and routing.
@@ -37,6 +38,7 @@ Welcome to My Blog, a simple blog built with Next.js 13 and Markdown for content
3738
- `plugins/`: Additional plugins or extensions.
3839
- `utils/`: Utility functions and helper modules.
3940
- `middleware.ts`: Next.js middleware.
41+
- `tests/`: Test folder contains the application's test files.
4042
- `types/`: TypeScript type definitions.
4143

4244
<!-- ## License -->

Diff for: data/i18n/index.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const defaultLocale = 'zh';
2+
export const locales = [defaultLocale, 'en'] as const;
3+
export type Locale = (typeof locales)[number];
4+
export type Dictionary = typeof import('./locales/zh.json');
5+
6+
const dictionaries: Record<Locale, () => Promise<Dictionary>> = {
7+
zh: () => import('./locales/zh.json'),
8+
en: () => import('./locales/en.json'),
9+
};
10+
11+
export const getDictionary = (locale: Locale) => dictionaries[locale]();
12+
13+
export const isLocale = (language: string): language is Locale =>
14+
locales.findIndex((locale) => locale === language) > -1;

Diff for: data/i18n/locales/en.json

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"common": {
3+
"home": "Home",
4+
"posts": "Posts",
5+
"about": "About",
6+
"tags": "Tags",
7+
"categories": "Categories",
8+
"latestPosts": "Latest Posts",
9+
"morePosts": "More Posts"
10+
},
11+
"homePage": {
12+
"title": "Mao's Corner",
13+
"description": "Welcome to Mao's Learning Corner, where I document my study notes. Looking forward to exploring the vast realms of knowledge together with you."
14+
},
15+
"postsPage": {
16+
"title": "Mao's Corner",
17+
"description": "Welcome to Mao's Learning Corner, where I document my study notes. Looking forward to exploring the vast realms of knowledge together with you."
18+
},
19+
"notFound": {
20+
"message": "Sorry we couldn't find this page."
21+
}
22+
}

Diff for: data/i18n/locales/zh.json

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"common": {
3+
"home": "首頁",
4+
"posts": "文章",
5+
"about": "關於",
6+
"tags": "標籤",
7+
"categories": "分類",
8+
"latestPosts": "最新文章",
9+
"morePosts": "更多文章"
10+
},
11+
"homePage": {
12+
"title": "Mao's Corner",
13+
"description": "歡迎來到阿毛的學習角落,這裡記錄著我的學習筆記,期待與你一同探索廣闊的知識領域。"
14+
},
15+
"postsPage": {
16+
"title": "Mao's Corner",
17+
"description": "歡迎來到阿毛的學習角落,這裡記錄著我的學習筆記,期待與你一同探索廣闊的知識領域。"
18+
},
19+
"notFound": {
20+
"message": "抱歉!找不到此頁面。"
21+
}
22+
}

Diff for: data/metadata.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Metadata } from 'next';
22
import type { FeedOptions } from 'feed';
3-
import { Locale, defaultLocale, getDictionary, locales } from '~/i18n';
3+
import { Locale, defaultLocale, getDictionary, locales } from '~/data/i18n';
44

55
export const avatarUrl = '/static/mao.jpg';
66
export const name = 'Johnson Mao';
@@ -13,7 +13,7 @@ export async function createMetadata(
1313
locale: Locale = defaultLocale
1414
): Promise<Metadata> {
1515
const {
16-
metadata: { title, description },
16+
homePage: { title, description },
1717
} = await getDictionary(locale);
1818

1919
return {
@@ -50,7 +50,7 @@ export async function createMetadata(
5050

5151
export async function createFeedOptions(locale: Locale): Promise<FeedOptions> {
5252
const {
53-
metadata: { title, description },
53+
homePage: { title, description },
5454
} = await getDictionary(locale);
5555

5656
return {

Diff for: i18n/index.ts

-15
This file was deleted.

Diff for: i18n/locales/en/common.ts

-9
This file was deleted.

Diff for: i18n/locales/en/index.ts

-11
This file was deleted.

Diff for: i18n/locales/en/metadata.ts

-6
This file was deleted.

Diff for: i18n/locales/en/notFound.ts

-5
This file was deleted.

Diff for: i18n/locales/zh/common.ts

-9
This file was deleted.

Diff for: i18n/locales/zh/index.ts

-11
This file was deleted.

Diff for: i18n/locales/zh/metadata.ts

-6
This file was deleted.

Diff for: i18n/locales/zh/notFound.ts

-5
This file was deleted.

Diff for: src/app/[lang]/layout.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Metadata } from 'next';
22
import { avatarUrl, name, copyright, createMetadata } from '~/data/metadata';
3-
import { Locale, getDictionary, locales } from '~/i18n';
3+
import { Locale, getDictionary, locales } from '~/data/i18n';
44
import ThemeSwitcher from '@/components/ThemeSwitcher';
55
import Header, { Avatar } from './Header';
66
import Footer from './Footer';
@@ -19,8 +19,8 @@ export async function generateMetadata({
1919
params: { lang },
2020
}: RootParams): Promise<Metadata> {
2121
const { alternates } = await createMetadata(lang);
22-
const { metadata } = await getDictionary(lang);
23-
const { title } = metadata;
22+
const { homePage } = await getDictionary(lang);
23+
const { title } = homePage;
2424

2525
return {
2626
title: {

Diff for: src/app/[lang]/page.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Metadata } from 'next';
22

3-
import { getDictionary } from '~/i18n';
3+
import { getDictionary } from '~/data/i18n';
44
import Container from '@/components/Container';
55
import { H1 } from '@/components/Heading';
66
import List from '@/components/List';
@@ -21,16 +21,16 @@ export async function generateMetadata({
2121

2222
async function RootPage({ params: { lang } }: RootParams) {
2323
const posts = await getAllDataFrontmatter('posts');
24-
const { metadata } = await getDictionary(lang);
24+
const { homePage, common } = await getDictionary(lang);
2525

2626
return (
2727
<>
2828
<Container className="pb-8">
29-
<H1 className="mb-4 text-3xl font-bold">{metadata.title}</H1>
30-
<p className="text-xl">{metadata.description}</p>
29+
<H1 className="mb-4 text-3xl font-bold">{homePage.title}</H1>
30+
<p className="text-xl">{homePage.description}</p>
3131
</Container>
3232
<Container as="main" className="py-8">
33-
<p className="mb-4 text-lg">近期文章</p>
33+
<p className="mb-4 text-lg">{common.latestPosts}</p>
3434
<List Item={Article} items={posts.slice(0, 4)} />
3535
</Container>
3636
</>

Diff for: src/app/[lang]/posts/InfiniteList.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import Article from '../Article';
99

1010
type InfiniteListProps = {
1111
items: DataFrontmatter[];
12+
morePostsText: string;
1213
};
1314

1415
const MemoArticle = memo(Article);
1516

16-
function InfiniteList({ items }: InfiniteListProps) {
17+
function InfiniteList({ items, morePostsText }: InfiniteListProps) {
1718
const searchParams = useSearchParams();
1819
const total = items.length;
1920
const clampLimit = clamp(1, total);
@@ -24,7 +25,7 @@ function InfiniteList({ items }: InfiniteListProps) {
2425
<List Item={MemoArticle} items={items.slice(0, limit)} />
2526
{limit < total && (
2627
<Link href={`?limit=${clampLimit(limit + 10)}`} replace scroll={false}>
27-
更多文章
28+
{morePostsText}
2829
</Link>
2930
)}
3031
</>

Diff for: src/app/[lang]/posts/page.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Metadata } from 'next';
22

3-
import { getDictionary } from '~/i18n';
3+
import { getDictionary } from '~/data/i18n';
44
import Container from '@/components/Container';
55
import { H1 } from '@/components/Heading';
66
import { getAllDataFrontmatter } from '@/utils/mdx';
@@ -20,16 +20,16 @@ export async function generateMetadata({
2020

2121
async function RootPage({ params: { lang } }: RootParams) {
2222
const posts = await getAllDataFrontmatter('posts');
23-
const { metadata } = await getDictionary(lang);
23+
const { postsPage, common } = await getDictionary(lang);
2424

2525
return (
2626
<>
2727
<Container className="pb-8">
28-
<H1 className="mb-4 text-3xl font-bold">{metadata.title}</H1>
29-
<p className="text-xl">{metadata.description}</p>
28+
<H1 className="mb-4 text-3xl font-bold">{postsPage.title}</H1>
29+
<p className="text-xl">{postsPage.description}</p>
3030
</Container>
3131
<Container as="main" className="py-8">
32-
<InfiniteList items={posts} />
32+
<InfiniteList items={posts} morePostsText={common.morePosts} />
3333
</Container>
3434
</>
3535
);

Diff for: src/app/layout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Metadata } from 'next';
22

3-
import { locales } from '~/i18n';
3+
import { locales } from '~/data/i18n';
44
import { createMetadata, createFeedOptions } from '~/data/metadata';
55
import Container from '@/components/Container';
66
import generateRSS from '@/utils/generateRSS';

Diff for: src/app/not-found.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const metadata: Metadata = {
77

88
function NotFoundPage() {
99
return (
10-
<div className="flex h-screen items-center justify-center">
10+
<div className="flex h-screen flex-col items-center justify-center">
1111
<H2>Page not found</H2>
1212
<p>Sorry, the page you are looking for does not exist.</p>
1313
</div>

Diff for: src/hooks/useI18n.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { usePathname } from 'next/navigation';
2-
import { defaultLocale } from '~/i18n';
2+
import { defaultLocale } from '~/data/i18n';
33
import getLocale from '@/utils/getLocale';
44

55
/**

Diff for: src/middleware.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NextRequest, NextResponse } from 'next/server';
2-
import { defaultLocale, locales } from '~/i18n';
2+
import { defaultLocale, locales } from '~/data/i18n';
33
import getLocale from '@/utils/getLocale';
44

55
export function middleware(request: NextRequest) {

Diff for: src/utils/generateRSS.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import path from 'path';
22
import fs from 'fs';
33
import { Feed, FeedOptions } from 'feed';
4-
import { defaultLocale } from '~/i18n';
4+
import { defaultLocale } from '~/data/i18n';
55

66
export const PUBLIC_FEED_PATH = path.join(process.cwd(), 'public', 'feed');
77

Diff for: src/utils/getLocale.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { Locale, isLocale } from '~/i18n';
1+
import { Locale, isLocale } from '~/data/i18n';
22

33
/**
44
* The function get language locale code from the input.
55
*
6-
* Ensures that the selected locale code matches one of the supported locale codes in the ~/i18n file.
6+
* Ensures that the selected locale code matches one of the supported locale codes in the ~/data/i18n file.
77
*
88
* @example
99
* import getLocale from '@/utils/getLocale';

Diff for: tests/app/[lang]/layout.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { render, screen } from '@testing-library/react';
2-
import { locales } from '~/i18n';
2+
import { locales } from '~/data/i18n';
33
import mockNavigation from '~/tests/navigation';
44
import Layout, { generateMetadata, generateStaticParams } from '@/app/[lang]/layout';
55

Diff for: tests/app/[lang]/page.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { render, screen } from '@testing-library/react';
2-
import en from '~/i18n/locales/en';
2+
import en from '~/data/i18n/locales/en.json';
33
import Page, { generateMetadata } from '@/app/[lang]/page';
44

55
jest.mock('@/utils/mdx', () => ({

Diff for: tests/app/[lang]/posts/infiniteList.test.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ describe('InfiniteList component', () => {
1414
tags: [],
1515
}));
1616
mockNavigation.searchParams.mockReturnValue(new URLSearchParams());
17-
render(<InfiniteList items={generateMockPosts(20)} />);
18-
const link = screen.getByRole('link', { name: '更多文章' });
17+
render(<InfiniteList items={generateMockPosts(20)} morePostsText='MorePost' />);
18+
const link = screen.getByRole('link', { name: 'MorePost' });
1919
const list = screen.getByRole('list');
2020
expect(link).toBeInTheDocument();
2121
expect(list).toBeInTheDocument();

Diff for: tests/app/[lang]/posts/page.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { render, screen } from '@testing-library/react';
2-
import en from '~/i18n/locales/en';
2+
import en from '~/data/i18n/locales/en.json';
33
import mockNavigation from '~/tests/navigation';
44
import Page, { generateMetadata } from '@/app/[lang]/posts/page';
55

Diff for: tests/hooks/useI18n.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { renderHook } from '@testing-library/react';
2-
import { defaultLocale } from '~/i18n';
2+
import { defaultLocale } from '~/data/i18n';
33
import mockNavigation from '~/tests/navigation';
44
import useI18n from '@/hooks/useI18n';
55

Diff for: tests/utils/generateRSS.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import path from 'path';
22
import type { FeedOptions } from 'feed';
3-
import { defaultLocale } from '~/i18n';
3+
import { defaultLocale } from '~/data/i18n';
44
import generateRSS, { PUBLIC_FEED_PATH } from '@/utils/generateRSS';
55

66
const mockWriteFile = jest.fn();

0 commit comments

Comments
 (0)