2 min read
How this blog handles three languages
One folder per post, English as the fallback, and a checkbox that hides untranslated posts. The setup behind the FlakeForge blog.
- next.js
- mdx
- i18n
The FlakeForge site is available in English, Russian, and Uzbek. The interface is fully translated, but blog posts are harder: we do not always have time to translate a post before it goes out. We wanted a setup where a post can ship in one language and pick up translations later, without broken links along the way.
One folder per post
Each post is a folder with one MDX file per language:
content/blog/
three-languages-one-blog/
en.mdx
ru.mdx
uz.mdxThe English file is required and the other two are optional. The folder name is the URL slug, and it is the same in every language, so switching the language on a post keeps you on the same post.
Falling back to English
When a reader opens /uz/blog/some-post and there is no uz.mdx, the page renders en.mdx and says so at the top. The URL still works, and the rest of the page (navigation, dates, labels) stays in Uzbek.
Picking the file is a small function:
export const resolveSourceLocale = (type, slug, locale) => {
if (existsSync(join(CONTENT_ROOT, type, slug, `${locale}.mdx`))) return locale
if (existsSync(join(CONTENT_ROOT, type, slug, 'en.mdx'))) return 'en'
return null
}The "Only Uzbek" checkbox
Fallback keeps links alive, but a reader who picked Uzbek may not want a list full of English posts. The blog index has a checkbox labeled for the active language: "Only English", "Только на русском", or "Faqat oʻzbekcha". When it is checked, posts that would fall back to English are hidden.
The filter runs in the browser over a list that was rendered at build time, so the page itself stays static.
Everything is built ahead of time
Every post page exists for every language at build time. generateStaticParams returns the slugs, and dynamicParams = false turns any other slug into a 404 instead of a server render:
export const generateStaticParams = () => getPostSlugs().map(slug => ({ slug }))
export const dynamicParams = falseFrontmatter goes through a zod schema during the build. A post with a missing title or a malformed date fails the build, so it never reaches the site with an empty heading.