Next.js Blog (7): i18n Structure & Routing
Contents
Notice
This text was originally translated using AI tools.
Introduction#
Since the field of development is not limited to one country but is used globally, I thought it would be good to add internationalization features... but I really agonized over this part.
Since I also refer to a lot of English documents, I hope that the content I share is not limited by language. However, when thinking simply, a few problems stood in the way:
- I am not great at writing. My Korean gets tangled sometimes, so I worried that expressing myself in English would be even harder.
- It was clear that management would become cumbersome. I would physically need the same post written in English, which increases the management points accordingly.
However, once I have these thoughts, I usually end up doing it. Thinking of it as a comfortable way to study English while writing, I decided to give it a go.
And if I get too tired later, I'll just quietly turn off the feature.👍
Implementation Plan#
Before starting, let's plan how to implement this.
- User Language Detection: Detect the browser language settings of a user accessing the blog for the first time to set the initial language.
- Settings Control and Storage: Implement a language toggle button at the top like most sites and store the setting
in
localStorageso it persists upon return visits. - URL Structure Design: Keep the Markdown filenames the same for each language but add a prefix like
/en/to the URL to branch the content. - Automatic Redirect: If a setting exists in
localStorage, redirect to that language; otherwise, redirect appropriately based on the browser's default language. - Integrity Validation: To prevent missing English Markdown files, I plan to compare language file pairs at build time and induce a build failure if even one is missing.
One could consider dynamically loading Markdown files based on settings without branching by URL,
but considering SEO, branching directly via URL seems better.
Implementation#
The final planned URL structure is as follows:
- Korean: htbae.gitblog.io/ko/blog/recent
- English: htbae.gitblog.io/en/blog/recent
Let's proceed with this in mind.
1. Project Structure Change
The existing Next.js Pages Router structure needs to be expanded
by one level by adding a [lang] directory to handle language settings.
- Existing Structure
Sections and detailed pages are configured based on a single language.
pages
├── [section]
│ ├── [slug].tsx
│ └── index.tsx
└── index.tsx- New Structure
Add a[lang]dynamic route at the top level to process content branching within the same logic.
pages
├── [lang]
│ ├── [section]
│ │ ├── [slug].tsx
│ │ └── index.tsx
│ └── index.tsx
└── index.tsx2. Modifying Dynamic Route Logic
As the folder structure deepens, changes are needed in the getStaticPaths logic
that generates paths at build time and the getStaticProps logic that fetches data.
Let's take the lowest level page, [slug].tsx, as an example.
// pages/[lang]/[section]/[slug].tsx
const LANGS = ['ko', 'en'];
export const getStaticPaths: GetStaticPaths = async () => {
const sections = getAllSections();
// Create a triple-nested path by combining languages (LANGS), sections, and slugs.
const paths = LANGS.flatMap((lang) =>
sections.flatMap((section) =>
getAllPostIds({ section }).map(({ slug }) => ({
params: { lang, section, slug }, // lang 추가
})),
),
);
return {
paths,
fallback: false, // 404 for paths not generated
};
};Current language information is now required not only for path generation
but also for handling navigation or link movement within the component.
Therefore, I modified getStaticProps to extract the lang value and pass it as a prop.
// pages/[lang]/[section]/[slug].tsx
export const getStaticProps: GetStaticProps = async ({ params }) => {
const lang = params?.lang as string;
const section = params?.section as string;
const slug = params?.slug as string;
if (!lang || !section || !slug) return { notFound: true };
try {
...
return {
props: { lang, section, slug },
};
} catch (err) {
console.error(`[SlugPage] Error loading ${section}/${slug}:`, err);
return { notFound: true };
}
};
3. Root Redirect Handling
The original pages/index.tsx has now been moved to pages/[lang]/index.tsx to serve as the main page for each language.
Now, let's implement the logic to redirect to the appropriate language page based on settings or environment
when accessing the empty root path (/).
// pages/index.tsx
const RootPage = () => {
const router = useRouter();
useEffect(() => {
// Check saved settings
const savedLang = localStorage.getItem('lang') as LangType;
// Detect browser default language (ko if Korean, en otherwise)
const browserLang: LangType = navigator.language.startsWith('ko')
? 'ko'
: 'en';
const targetLang = savedLang || browserLang;
// Move to the determined page
router.replace(`/${targetLang}`);
}, [router]);
return null;
};Conclusion#
The transition to the pages/[lang]/... structure is now complete. Users now have independent URLs for each language,
and preparation is finished to show posts matching each language through dynamic routing.
In the next post, I will implement the 'Language Setting Toggle Button' that allows users to manually switch languages.
I will also finish the internationalization implementation by adding logic to fetch matching Markdown files
based on the setting stored in localStorage.