Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ next-env.d.ts
# claude code
.claude/
CLAUDE.local.md

# implementation md file
proposal_issue_74.md
8 changes: 8 additions & 0 deletions CONTRIBUTING.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ pnpm build
Es el tipo de contribución más valioso. Los pasos están en el
[README](README.es.md#agregar-un-lenguaje). Incluye siempre un test del analyzer.

## Agregar una traducción

¡Damos la bienvenida a las traducciones para que Weftmap sea accesible en todo el mundo!
Para agregar una traducción:
1. Registra el nuevo código de locale (por ejemplo, `fr`) en [src/i18n/config.ts](file:///src/i18n/config.ts).
2. Copia [src/i18n/dictionaries/en.json](file:///src/i18n/dictionaries/en.json) a `src/i18n/dictionaries/<locale>.json` y traduce los valores.
Comment on lines +42 to +43

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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
-1. Registra el nuevo código de locale (por ejemplo, `fr`) en [src/i18n/config.ts](file:///src/i18n/config.ts).
-2. Copia [src/i18n/dictionaries/en.json](file:///src/i18n/dictionaries/en.json) a `src/i18n/dictionaries/<locale>.json` y traduce los valores.
+1. Registra el nuevo código de locale (por ejemplo, `fr`) en [src/i18n/config.ts](src/i18n/config.ts).
+2. Copia [src/i18n/dictionaries/en.json](src/i18n/dictionaries/en.json) a `src/i18n/dictionaries/<locale>.json` y traduce los valores.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
1. Registra el nuevo código de locale (por ejemplo, `fr`) en [src/i18n/config.ts](file:///src/i18n/config.ts).
2. Copia [src/i18n/dictionaries/en.json](file:///src/i18n/dictionaries/en.json) a `src/i18n/dictionaries/<locale>.json` y traduce los valores.
1. Registra el nuevo código de locale (por ejemplo, `fr`) en [src/i18n/config.ts](src/i18n/config.ts).
2. Copia [src/i18n/dictionaries/en.json](src/i18n/dictionaries/en.json) a `src/i18n/dictionaries/<locale>.json` y traduce los valores.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CONTRIBUTING.es.md` around lines 42 - 43, Replace the file:// protocol URLs
in the markdown links with relative repository paths. Change the href values in
the links for src/i18n/config.ts and src/i18n/dictionaries/en.json from
file:///src/i18n/... format to relative paths like src/i18n/... or
./src/i18n/... so that the links work properly when the markdown file is viewed
in repository viewers.

3. Ejecuta `pnpm test` para verificar que el diccionario de traducción coincida con las claves de la plantilla de origen.

## Estilo

- TypeScript, sin `any` en código de aplicación.
Expand Down
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace file:// URLs with repository-relative links.

Line 41 and Line 42 use file:///..., which breaks in GitHub/Markdown viewers.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CONTRIBUTING.md` around lines 41 - 42, Replace the file:// protocol URLs in
the markdown links on lines 41 and 42 with repository-relative paths. In both
instances where you see [src/i18n/config.ts](file:///src/i18n/config.ts) and
[src/i18n/dictionaries/en.json](file:///src/i18n/dictionaries/en.json), remove
the file:/// prefix from the URL portion of the markdown link (keeping just the
relative path like src/i18n/config.ts and src/i18n/dictionaries/en.json
respectively) so the links work properly in GitHub and other markdown viewers.

3. Run `pnpm test` to verify the translation dictionary matches the source template keys.

## Style

- TypeScript, no `any` in application code.
Expand Down
12 changes: 7 additions & 5 deletions src/app/[lang]/docs/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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` };
}

Expand All @@ -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();
Expand All @@ -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"]}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`[lang]/docs/[slug]/page.tsx at line 55, The arrow glyphs on line 55
(← before prev.title) and line 70 (→ after next.title) are hardcoded for LTR
languages and should be direction-aware for RTL locales like Arabic. Determine
if the current language (lang parameter) is RTL, then conditionally render the
appropriate arrow direction: for RTL locales, reverse the arrows so the previous
navigation shows → and next navigation shows ←, while keeping the current LTR
arrows for non-RTL languages. This ensures navigation cues visually align with
the reading direction of the locale.

</span>
</Link>
) : (
Expand All @@ -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>
) : (
Expand Down
4 changes: 3 additions & 1 deletion src/app/[lang]/docs/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ReactNode } from "react";
import { notFound } from "next/navigation";
import { isLocale, locales, type Locale } from "@/i18n/config";
import { getDictionary } from "@/i18n/dictionaries";
import DocsSidebar from "@/components/docs/DocsSidebar";

export function generateStaticParams() {
Expand All @@ -16,13 +17,14 @@ export default async function DocsLayout({
}) {
const { lang } = await params;
if (!isLocale(lang)) notFound();
const t = getDictionary(lang as Locale);

return (
<div className="mx-auto grid max-w-[1320px] gap-10 px-6 py-12 lg:grid-cols-[240px_minmax(0,1fr)] lg:gap-16 lg:py-20">
{/* Mobile docs nav (sidebar is hidden below lg). */}
<details className="group mb-2 rounded-xl border border-[#e2e8f0] bg-white lg:hidden">
<summary className="flex cursor-pointer list-none items-center justify-between px-4 py-3 text-sm font-medium text-[#0f172a] [&::-webkit-details-marker]:hidden">
{lang === "es" ? "Documentación" : "Documentation"}
{t.documentation}
<span aria-hidden="true" className="font-mono text-[#94a3b8] transition-transform group-open:rotate-180">
</span>
Expand Down
4 changes: 2 additions & 2 deletions src/app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ export default async function RootLayout({
}) {
const { lang } = await params;
if (!isLocale(lang)) notFound();
const t = getDictionary(lang);
const t = getDictionary(lang as Locale);

return (
<html lang={lang as Locale} suppressHydrationWarning>
<html lang={lang as Locale} dir={lang === "ar" ? "rtl" : "ltr"} suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
Expand Down
6 changes: 4 additions & 2 deletions src/components/docs/DocsSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import type { Locale } from "@/i18n/config";
import { getDictionary } from "@/i18n/dictionaries";
import { DOC_NAV } from "@/lib/docs";

export default function DocsSidebar({
Expand All @@ -13,12 +14,13 @@ export default function DocsSidebar({
showHeading?: boolean;
}) {
const pathname = usePathname();
const t = getDictionary(lang);

return (
<nav aria-label="Docs">
{showHeading && (
<p className="mb-4 font-mono text-[11px] font-semibold uppercase tracking-[0.18em] text-[#94a3b8]">
{lang === "es" ? "Documentación" : "Documentation"}
{t.documentation}
</p>
)}
<div className="flex flex-col border-l border-[#e2e8f0] dark:border-[#232a36]">
Expand All @@ -36,7 +38,7 @@ export default function DocsSidebar({
: "border-transparent text-[#475569] dark:text-[#9aa6b8] hover:border-[#cbd5e1] hover:text-[#0f172a]"
}`}
>
{d.title[lang]}
{d.title[lang] ?? d.title["en"]}
</Link>
);
})}
Expand Down
93 changes: 66 additions & 27 deletions src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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]";

Expand Down Expand Up @@ -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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix ARIA popup semantics for the language dropdown.

Line 107 sets aria-haspopup="listbox", but the popup renders navigation links (not listbox/options). This mismatch can confuse assistive tech. Use a menu pattern (menu/menuitem) or a true listbox pattern consistently.

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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/layout/Header.tsx` at line 107, The language dropdown
component in Header.tsx is using aria-haspopup="listbox" but the popup contains
navigation links instead of actual listbox options, creating a semantic mismatch
that confuses assistive technology. Change the aria-haspopup value from
"listbox" to "menu" to accurately reflect the menu pattern being rendered with
navigation links. Also apply this same ARIA semantic fix to all related
instances mentioned (around lines 123 and 128-140) to ensure consistent ARIA
patterns throughout the component. Verify that the overall component structure
(button trigger and popup content) follows the menu pattern semantics
consistently.

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>
);
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const locales = ["en", "es"] as const;
export const locales = ["en", "es", "pt", "ar", "fr", "it"] as const;

export type Locale = (typeof locales)[number];

Expand Down
11 changes: 9 additions & 2 deletions src/i18n/dictionaries.ts
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;
}
Loading