Summary
Add internationalization (i18n) to the documentation site (packages/docs) so
it can be read in multiple languages. The site has no i18n today — routing,
chrome strings, content, SEO, and search are all single-language (English).
- Phase 1 (required, in scope): English (
en) and Turkish (tr),
with en as the default.
- Phase 2 (future, out of scope here): Russian (
ru), Chinese
(zh, "CN"), and Korean (ko, "KR") — added once Phase 1 is in place;
the system must be left open to them with no further architectural changes.
Current state (what i18n must touch)
- No existing i18n —
<html lang="en"> is hard-coded (index.html).
- Routing:
react-router-dom resolves pages from pageBySlug, built from an
eager import.meta.glob('./content/**/*.mdx') keyed by frontmatter slug
(src/pages.ts).
- Content: 73 MDX pages under
src/content/ (getting-started,
foundations/, guides/, components/, reference/, changelog/, index).
- Chrome strings (hard-coded in TSX): "On this page"
(TableOfContents.tsx), "Page not found" (NotFound.tsx), the TopNav brand,
the sidebar/primary nav labels and group names in src/data/navigation.ts,
plus the search dialog and footer.
- SEO:
useDocumentHead + src/seo/vite-plugin-seo.ts prerender per-route
head tags — English-only, no hreflang.
- Search:
src/search/vite-plugin-search.ts builds one MiniSearch index over
English content text.
How to implement
Build a lightweight, bespoke i18n (no heavy dependency) consistent with the
existing hand-rolled Vite/MDX setup.
1. Locale config — single source of truth
- Add
src/i18n/config.ts exporting locales = ['en', 'tr'] as const,
defaultLocale = 'en', and a Locale type. Everything (routing, switcher,
SEO, search, content glob) reads this list, so Phase 2 is "append to the
array + add files", nothing else.
2. Routing — default locale unprefixed, others prefixed
en stays unprefixed at /...; other locales are prefixed: /tr/...
(e.g. /tr/components/button). SEO- and migration-friendly (existing English
URLs don't change).
- Add a
LocaleProvider that derives the active locale from the pathname;
update slugFromPath (src/pages.ts) to strip a leading locale segment before
lookup. Unknown/!translated slug → fall back to the en page (see §5).
3. Locale detection, persistence, <html lang>
- On first visit, pick from
navigator.language (matched against locales),
else defaultLocale; persist the user's explicit choice in localStorage.
- An effect sets
document.documentElement.lang to the active locale on every
navigation/locale change (replacing the hard-coded index.html value).
4. Chrome string catalog
src/i18n/messages/en.ts + tr.ts, each implementing a shared typed
Messages interface (compile error if a key is missing in any locale).
- An
I18nProvider exposes a useT() hook returning the active catalog.
- Replace every hard-coded UI string in
TopNav, Sidebar, Footer,
TableOfContents, SearchDialog, NotFound with t(...). Nav group
labels currently come from GROUP_ORDER/frontmatter, so introduce stable
group keys and translate them through the catalog (not the raw label).
5. Content — per-locale trees with EN fallback
- Reorganize
src/content/ into per-locale trees: content/en/**,
content/tr/**. Update the glob in pages.ts to capture the locale from the
path and build the locale-prefixed slug.
pageBySlug resolves the active locale first, then falls back to the en
page when a translation is missing, rendering a subtle "not yet translated"
banner (a small MDX/shell component). This lets TR ship incrementally.
- Translate the core pages to TR first (landing, Getting Started, Foundations,
Guides); component reference pages may fall back to EN initially and be
translated over time.
6. Language switcher (UI)
- A switcher in
TopNav built with a Manti component (Menu or Select,
not raw HTML) that navigates to the same slug in the target locale and
persists the choice.
7. SEO
useDocumentHead + vite-plugin-seo: per-locale <title>/description,
hreflang alternate links between locale variants of each page, correct
<html lang>, and per-locale entries in the sitemap. The prerender step
iterates locales × pages.
8. Search
- Make
vite-plugin-search emit one MiniSearch index per locale;
SearchProvider selects the index for the active locale so results match the
language being read.
Phase 2 (future — not in this issue)
- Add
ru, zh, ko by appending to locales in src/i18n/config.ts and
dropping in their message catalogs + translated content trees. If Phase 1 is
built config-driven, no further architectural changes are required.
Acceptance criteria (Phase 1)
Summary
Add internationalization (i18n) to the documentation site (
packages/docs) soit can be read in multiple languages. The site has no i18n today — routing,
chrome strings, content, SEO, and search are all single-language (English).
en) and Turkish (tr),with
enas the default.ru), Chinese(
zh, "CN"), and Korean (ko, "KR") — added once Phase 1 is in place;the system must be left open to them with no further architectural changes.
Current state (what i18n must touch)
<html lang="en">is hard-coded (index.html).react-router-domresolves pages frompageBySlug, built from aneager
import.meta.glob('./content/**/*.mdx')keyed by frontmatterslug(
src/pages.ts).src/content/(getting-started,foundations/,guides/,components/,reference/,changelog/,index).(
TableOfContents.tsx), "Page not found" (NotFound.tsx), the TopNav brand,the sidebar/primary nav labels and group names in
src/data/navigation.ts,plus the search dialog and footer.
useDocumentHead+src/seo/vite-plugin-seo.tsprerender per-routehead tags — English-only, no
hreflang.src/search/vite-plugin-search.tsbuilds one MiniSearch index overEnglish content text.
How to implement
Build a lightweight, bespoke i18n (no heavy dependency) consistent with the
existing hand-rolled Vite/MDX setup.
1. Locale config — single source of truth
src/i18n/config.tsexportinglocales = ['en', 'tr'] as const,defaultLocale = 'en', and aLocaletype. Everything (routing, switcher,SEO, search, content glob) reads this list, so Phase 2 is "append to the
array + add files", nothing else.
2. Routing — default locale unprefixed, others prefixed
enstays unprefixed at/...; other locales are prefixed:/tr/...(e.g.
/tr/components/button). SEO- and migration-friendly (existing EnglishURLs don't change).
LocaleProviderthat derives the active locale from the pathname;update
slugFromPath(src/pages.ts) to strip a leading locale segment beforelookup. Unknown/!translated slug → fall back to the
enpage (see §5).3. Locale detection, persistence,
<html lang>navigator.language(matched againstlocales),else
defaultLocale; persist the user's explicit choice inlocalStorage.document.documentElement.langto the active locale on everynavigation/locale change (replacing the hard-coded
index.htmlvalue).4. Chrome string catalog
src/i18n/messages/en.ts+tr.ts, each implementing a shared typedMessagesinterface (compile error if a key is missing in any locale).I18nProviderexposes auseT()hook returning the active catalog.TopNav,Sidebar,Footer,TableOfContents,SearchDialog,NotFoundwitht(...). Nav grouplabels currently come from
GROUP_ORDER/frontmatter, so introduce stablegroup keys and translate them through the catalog (not the raw label).
5. Content — per-locale trees with EN fallback
src/content/into per-locale trees:content/en/**,content/tr/**. Update the glob inpages.tsto capture the locale from thepath and build the locale-prefixed slug.
pageBySlugresolves the active locale first, then falls back to theenpage when a translation is missing, rendering a subtle "not yet translated"
banner (a small MDX/shell component). This lets TR ship incrementally.
Guides); component reference pages may fall back to EN initially and be
translated over time.
6. Language switcher (UI)
TopNavbuilt with a Manti component (MenuorSelect,not raw HTML) that navigates to the same slug in the target locale and
persists the choice.
7. SEO
useDocumentHead+vite-plugin-seo: per-locale<title>/description,hreflangalternate links between locale variants of each page, correct<html lang>, and per-locale entries in the sitemap. The prerender stepiterates
locales × pages.8. Search
vite-plugin-searchemit one MiniSearch index per locale;SearchProviderselects the index for the active locale so results match thelanguage being read.
Phase 2 (future — not in this issue)
ru,zh,koby appending tolocalesinsrc/i18n/config.tsanddropping in their message catalogs + translated content trees. If Phase 1 is
built config-driven, no further architectural changes are required.
Acceptance criteria (Phase 1)
enandtrselectable via a Manti-component language switcher in TopNav.<html lang>updated live./tr/...) with EN fallback + "not yet translated"notice for untranslated pages.
copy left in shell components); missing keys fail typecheck.
hreflang+ per-locale titles/sitemap; search is per-locale.pnpm --filter @manti-ui/docs buildpasses.