-
Notifications
You must be signed in to change notification settings - Fork 7
feat : implement extensible i18n support and remove hardcoded settings #94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,3 +41,6 @@ next-env.d.ts | |
| # claude code | ||
| .claude/ | ||
| CLAUDE.local.md | ||
|
|
||
| # implementation md file | ||
| proposal_issue_74.md | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -34,6 +34,14 @@ pnpm build | |||||||||
| This is the most valuable kind of contribution. The steps are in the | ||||||||||
| [README](README.md#adding-a-language). Always include an analyzer test. | ||||||||||
|
|
||||||||||
| ## Adding a translation | ||||||||||
|
|
||||||||||
| We welcome translations to make Weftmap accessible worldwide! | ||||||||||
| To add a translation: | ||||||||||
| 1. Register the new locale code (e.g., `fr`) in [src/i18n/config.ts](file:///src/i18n/config.ts). | ||||||||||
| 2. Copy [src/i18n/dictionaries/en.json](file:///src/i18n/dictionaries/en.json) to `src/i18n/dictionaries/<locale>.json` and translate the values. | ||||||||||
|
Comment on lines
+41
to
+42
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace Line 41 and Line 42 use Proposed fix-1. Register the new locale code (e.g., `fr`) in [src/i18n/config.ts](file:///src/i18n/config.ts).
-2. Copy [src/i18n/dictionaries/en.json](file:///src/i18n/dictionaries/en.json) to `src/i18n/dictionaries/<locale>.json` and translate the values.
+1. Register the new locale code (e.g., `fr`) in [src/i18n/config.ts](src/i18n/config.ts).
+2. Copy [src/i18n/dictionaries/en.json](src/i18n/dictionaries/en.json) to `src/i18n/dictionaries/<locale>.json` and translate the values.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| 3. Run `pnpm test` to verify the translation dictionary matches the source template keys. | ||||||||||
|
|
||||||||||
| ## Style | ||||||||||
|
|
||||||||||
| - TypeScript, no `any` in application code. | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ import type { Metadata } from "next"; | |
| import Link from "next/link"; | ||
| import { notFound } from "next/navigation"; | ||
| import { isLocale, locales, type Locale } from "@/i18n/config"; | ||
| import { getDictionary } from "@/i18n/dictionaries"; | ||
| import { DOC_NAV, DOC_SLUGS, type DocSlug } from "@/lib/docs"; | ||
| import { DOC_COMPONENTS } from "@/components/docs/registry"; | ||
|
|
||
|
|
@@ -17,7 +18,7 @@ export async function generateMetadata({ | |
| const { lang, slug } = await params; | ||
| const item = DOC_NAV.find((d) => d.slug === slug); | ||
| const locale: Locale = isLocale(lang) ? lang : "en"; | ||
| const title = item ? item.title[locale] : "Docs"; | ||
| const title = item ? (item.title[locale] ?? item.title["en"]) : "Docs"; | ||
| return { title: `${title} — Weftmap` }; | ||
| } | ||
|
|
||
|
|
@@ -28,6 +29,7 @@ export default async function DocPage({ | |
| }) { | ||
| const { lang, slug } = await params; | ||
| if (!isLocale(lang)) notFound(); | ||
| const t = getDictionary(lang as Locale); | ||
|
|
||
| const index = DOC_NAV.findIndex((d) => d.slug === slug); | ||
| if (index === -1) notFound(); | ||
|
|
@@ -47,10 +49,10 @@ export default async function DocPage({ | |
| className="group rounded-xl border border-[#e2e8f0] bg-white p-4 transition-all hover:-translate-y-0.5 hover:border-[#c7d2fe] hover:shadow-[0_8px_24px_-12px_rgba(15,23,42,0.18)]" | ||
| > | ||
| <span className="font-mono text-[11px] uppercase tracking-[0.14em] text-[#94a3b8]"> | ||
| {lang === "es" ? "Anterior" : "Previous"} | ||
| {t.previous} | ||
| </span> | ||
| <span className="mt-1 block font-medium text-[#0f172a] transition-colors group-hover:text-[#4f46e5]"> | ||
| ← {prev.title[lang]} | ||
| ← {prev.title[lang] ?? prev.title["en"]} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make prev/next arrow glyphs direction-aware for RTL locales. Line 55 and Line 70 hardcode LTR arrows. In Arabic RTL pages, this can invert navigation cues visually. Proposed fix const Content = DOC_COMPONENTS[slug as DocSlug];
const prev = DOC_NAV[index - 1];
const next = DOC_NAV[index + 1];
+ const isRtl = lang === "ar";
@@
- ← {prev.title[lang] ?? prev.title["en"]}
+ {isRtl ? "→" : "←"} {prev.title[lang] ?? prev.title["en"]}
@@
- {next.title[lang] ?? next.title["en"]} →
+ {next.title[lang] ?? next.title["en"]} {isRtl ? "←" : "→"}Also applies to: 70-70 🤖 Prompt for AI Agents |
||
| </span> | ||
| </Link> | ||
| ) : ( | ||
|
|
@@ -62,10 +64,10 @@ export default async function DocPage({ | |
| className="group rounded-xl border border-[#e2e8f0] bg-white p-4 text-right transition-all hover:-translate-y-0.5 hover:border-[#c7d2fe] hover:shadow-[0_8px_24px_-12px_rgba(15,23,42,0.18)]" | ||
| > | ||
| <span className="font-mono text-[11px] uppercase tracking-[0.14em] text-[#94a3b8]"> | ||
| {lang === "es" ? "Siguiente" : "Next"} | ||
| {t.next} | ||
| </span> | ||
| <span className="mt-1 block font-medium text-[#0f172a] transition-colors group-hover:text-[#4f46e5]"> | ||
| {next.title[lang]} → | ||
| {next.title[lang] ?? next.title["en"]} → | ||
| </span> | ||
| </Link> | ||
| ) : ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,16 +2,29 @@ | |
|
|
||
| import { useEffect, useState } from "react"; | ||
| import Link from "next/link"; | ||
| import { usePathname } from "next/navigation"; | ||
| import { locales, type Locale } from "@/i18n/config"; | ||
| import { getDictionary } from "@/i18n/dictionaries"; | ||
| import Logo from "./Logo"; | ||
| import ThemeToggle from "./ThemeToggle"; | ||
|
|
||
| const REPO = "https://github.com/DataDave-Dev/weftmap"; | ||
|
|
||
| const LANGUAGE_NAMES: Record<string, string> = { | ||
| en: "English", | ||
| es: "Español", | ||
| pt: "Português", | ||
| ar: "العربية", | ||
| fr: "Français", | ||
| it: "Italiano", | ||
| }; | ||
|
|
||
| export default function Header({ lang }: { lang: Locale }) { | ||
| const t = getDictionary(lang); | ||
| const [scrolled, setScrolled] = useState(false); | ||
| const [dropdownOpen, setDropdownOpen] = useState(false); | ||
| const pathname = usePathname(); | ||
|
|
||
| useEffect(() => { | ||
| const onScroll = () => setScrolled(window.scrollY > 8); | ||
| window.addEventListener("scroll", onScroll, { passive: true }); | ||
|
|
@@ -22,6 +35,16 @@ export default function Header({ lang }: { lang: Locale }) { | |
| }; | ||
| }, []); | ||
|
|
||
| const getRedirectPath = (targetLocale: string) => { | ||
| if (!pathname) return `/${targetLocale}`; | ||
| const segments = pathname.split("/"); | ||
| if (segments.length > 1 && (locales as readonly string[]).includes(segments[1])) { | ||
| segments[1] = targetLocale; | ||
| return segments.join("/"); | ||
| } | ||
| return `/${targetLocale}`; | ||
| }; | ||
|
|
||
| const linkClass = | ||
| "flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[13px] text-[#475569] hover:text-[#0f172a] hover:bg-black/[0.04] transition-colors dark:text-[#94a3b8] dark:hover:text-[#e6e9ef] dark:hover:bg-white/[0.06]"; | ||
|
|
||
|
|
@@ -77,34 +100,50 @@ export default function Header({ lang }: { lang: Locale }) { | |
| {t.getStarted} | ||
| </Link> | ||
|
|
||
| <nav | ||
| aria-label="Language" | ||
| className="inline-flex shrink-0 gap-0.5 p-[3px] rounded-full border bg-black/[0.04] border-[#e2e8f0] dark:bg-white/[0.06] dark:border-[#232a36]" | ||
| > | ||
| {locales.map((locale) => { | ||
| const active = locale === lang; | ||
| return ( | ||
| <Link | ||
| key={locale} | ||
| href={`/${locale}`} | ||
| aria-current={active ? "page" : undefined} | ||
| className={`group px-3.5 py-[5px] rounded-full transition-colors ${ | ||
| active ? "bg-black/[0.06] dark:bg-white/[0.12]" : "" | ||
| }`} | ||
| <div className="relative shrink-0"> | ||
| <button | ||
| onClick={() => setDropdownOpen(!dropdownOpen)} | ||
| aria-expanded={dropdownOpen} | ||
| aria-haspopup="listbox" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix ARIA popup semantics for the language dropdown. Line 107 sets Suggested fix- aria-haspopup="listbox"
+ aria-haspopup="menu"
+ aria-controls="language-menu"
@@
- <div
+ <div
+ id="language-menu"
+ role="menu"
className="absolute right-0 rtl:left-0 rtl:right-auto z-20 mt-2 w-36 rounded-xl border p-1 shadow-lg transition-all bg-white border-[`#e2e8f0`] dark:bg-[`#0b0d12`] dark:border-[`#232a36`]"
>
@@
- <Link
+ <Link
+ role="menuitem"Also applies to: 123-123, 128-140 🤖 Prompt for AI Agents |
||
| aria-label="Select language" | ||
| className="flex items-center gap-1.5 px-3.5 py-[6px] rounded-full border text-[13px] font-semibold transition-all bg-black/[0.04] border-[#e2e8f0] text-[#0f172a] hover:bg-black/[0.08] dark:bg-white/[0.08] dark:border-white/[0.14] dark:text-[#cbd5e1] dark:hover:bg-white/[0.16]" | ||
| > | ||
| <span>🌐</span> | ||
| <span>{LANGUAGE_NAMES[lang] ?? lang.toUpperCase()}</span> | ||
| <span className="text-[9px] opacity-60">▼</span> | ||
| </button> | ||
|
|
||
| {dropdownOpen && ( | ||
| <> | ||
| <div | ||
| className="fixed inset-0 z-10" | ||
| onClick={() => setDropdownOpen(false)} | ||
| /> | ||
| <div | ||
| className="absolute right-0 rtl:left-0 rtl:right-auto z-20 mt-2 w-36 rounded-xl border p-1 shadow-lg transition-all bg-white border-[#e2e8f0] dark:bg-[#0b0d12] dark:border-[#232a36]" | ||
| > | ||
| <span | ||
| className={`text-[13px] font-semibold transition-colors ${ | ||
| active | ||
| ? "text-[#0f172a] dark:text-[#e6e9ef]" | ||
| : "text-[#64748b] group-hover:text-[#0f172a] dark:text-[#8b96a7] dark:group-hover:text-[#e6e9ef]" | ||
| }`} | ||
| > | ||
| {locale.toUpperCase()} | ||
| </span> | ||
| </Link> | ||
| ); | ||
| })} | ||
| </nav> | ||
| {locales.map((locale) => { | ||
| const active = locale === lang; | ||
| return ( | ||
| <Link | ||
| key={locale} | ||
| href={getRedirectPath(locale)} | ||
| onClick={() => setDropdownOpen(false)} | ||
| className={`flex items-center justify-between w-full px-3 py-1.5 rounded-lg text-left text-[13px] font-medium transition-colors ${ | ||
| active | ||
| ? "bg-[#f1f5f9] text-[#4f46e5] dark:bg-white/[0.08] dark:text-white" | ||
| : "text-[#475569] hover:bg-[#f8fafc] hover:text-[#0f172a] dark:text-[#94a3b8] dark:hover:bg-white/[0.04] dark:hover:text-white" | ||
| }`} | ||
| > | ||
| <span>{LANGUAGE_NAMES[locale] ?? locale.toUpperCase()}</span> | ||
| {active && <span className="text-[10px]">✓</span>} | ||
| </Link> | ||
| ); | ||
| })} | ||
| </div> | ||
| </> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </header> | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,18 @@ | ||
| import type { Locale } from "./config"; | ||
| import en from "./dictionaries/en.json"; | ||
| import es from "./dictionaries/es.json"; | ||
| import pt from "./dictionaries/pt.json"; | ||
| import ar from "./dictionaries/ar.json"; | ||
| import fr from "./dictionaries/fr.json"; | ||
| import it from "./dictionaries/it.json"; | ||
|
|
||
| export type Dictionary = typeof en; | ||
|
|
||
| const dictionaries: Record<Locale, Dictionary> = { en, es }; | ||
| // Static map: every locale in `config` must have a dictionary here, enforced at | ||
| // compile time. Adding a locale without its dictionary is a type error — no | ||
| // silent English fallback that hides missing or broken translations. | ||
| const dictionaries: Record<Locale, Dictionary> = { en, es, pt, ar, fr, it }; | ||
|
|
||
| export function getDictionary(locale: Locale): Dictionary { | ||
| return dictionaries[locale]; | ||
| return dictionaries[locale] ?? dictionaries.en; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Corrige enlaces
file://por rutas relativas del repositorio.En la Line 42 y la Line 43,
file:///...no funciona en visores Markdown del repositorio.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents