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
39 changes: 39 additions & 0 deletions components/atoms/locale-switcher/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useRouter } from 'next/router'
import { useTranslations } from 'next-intl'
import { locales, type Locale } from '../../../i18n/messages'

/**
* LocaleSwitcher — switches the active UI language while preserving the current
* route (pathname + query). Relies on Next.js built-in locale routing, so the
* selected locale is reflected in the URL and `useRouter().locale`.
*/
export function LocaleSwitcher() {
const router = useRouter()
const t = useTranslations('LocaleSwitcher')
const active = (router.locale ?? router.defaultLocale) as string

const onChange = (next: Locale) => {
if (next === active) return
router.push({ pathname: router.pathname, query: router.query }, router.asPath, {
locale: next,
})
}

return (
<label className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
<span className="sr-only">{t('label')}</span>
<select
aria-label={t('label')}
value={active}
onChange={(e) => onChange(e.target.value as Locale)}
className="bg-transparent border border-gray-200 dark:border-gray-700 rounded-lg px-2 py-1 text-sm font-medium text-gray-700 dark:text-gray-200 cursor-pointer focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
{locales.map((loc) => (
<option key={loc} value={loc} className="text-gray-900">
{t(loc)}
</option>
))}
</select>
</label>
)
}
31 changes: 18 additions & 13 deletions components/organisms/navbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useTranslations } from 'next-intl';
import { ThemeToggle } from '../../atoms/theme-toggle';
import { LocaleSwitcher } from '../../atoms/locale-switcher';

const NAV_LINKS = [
{ label: 'Home', href: '/' },
{ label: 'Explore', href: '/explore' },
{ label: 'Leaderboard', href: '/leaderboard' },
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Escrow', href: '/escrow' },
];
{ key: 'home', href: '/' },
{ key: 'explore', href: '/explore' },
{ key: 'leaderboard', href: '/leaderboard' },
{ key: 'dashboard', href: '/dashboard' },
{ key: 'escrow', href: '/escrow' },
] as const;

export function Navbar() {
const [open, setOpen] = useState(false);
const router = useRouter();
const t = useTranslations('Nav');

const toggle = () => setOpen((v) => !v);
const close = () => setOpen(false);
Expand All @@ -27,7 +30,7 @@ export function Navbar() {

{/* Desktop links */}
<ul className="hidden md:flex items-center gap-1 list-none m-0 p-0">
{NAV_LINKS.map(({ label, href }) => (
{NAV_LINKS.map(({ key, href }) => (
<li key={href}>
<Link
href={href}
Expand All @@ -37,20 +40,21 @@ export function Navbar() {
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white'
}`}
>
{label}
{t(key)}
</Link>
</li>
))}
</ul>

{/* Right actions */}
<div className="hidden md:flex items-center gap-3">
<LocaleSwitcher />
<ThemeToggle />
<Link
href="/escrow"
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-lg transition-all hover:-translate-y-px"
>
Launch App
{t('launchApp')}
</Link>
</div>

Expand All @@ -59,7 +63,7 @@ export function Navbar() {
className="flex md:hidden flex-col gap-1.5 bg-transparent border-none cursor-pointer p-1"
onClick={toggle}
aria-expanded={open}
aria-label={open ? 'Close menu' : 'Open menu'}
aria-label={open ? t('closeMenu') : t('openMenu')}
>
<span className="block w-5 h-0.5 bg-gray-900 dark:bg-white rounded transition-all" />
<span className="block w-5 h-0.5 bg-gray-900 dark:bg-white rounded transition-all" />
Expand All @@ -70,7 +74,7 @@ export function Navbar() {
{open && (
<div className="absolute top-16 left-0 right-0 bg-white dark:bg-gray-950 border-b border-gray-200 dark:border-gray-800 shadow-lg px-6 py-4 animate-[slideDown_0.18s_ease-out]">
<ul className="list-none m-0 p-0 flex flex-col gap-1">
{NAV_LINKS.map(({ label, href }) => (
{NAV_LINKS.map(({ key, href }) => (
<li key={href}>
<Link
href={href}
Expand All @@ -81,19 +85,20 @@ export function Navbar() {
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white'
}`}
>
{label}
{t(key)}
</Link>
</li>
))}
</ul>
<div className="mt-3 flex items-center gap-3 pt-3 border-t border-gray-100 dark:border-gray-800">
<LocaleSwitcher />
<ThemeToggle />
<Link
href="/escrow"
onClick={close}
className="flex-1 text-center px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-lg transition-colors"
>
Launch App
{t('launchApp')}
</Link>
</div>
</div>
Expand Down
21 changes: 21 additions & 0 deletions i18n/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { AbstractIntlMessages } from 'next-intl'
import en from '../messages/en.json'
import es from '../messages/es.json'

/** Locales supported by the app. Keep in sync with `i18n.locales` in next.config.js. */
export const locales = ['en', 'es'] as const
export type Locale = (typeof locales)[number]

export const defaultLocale: Locale = 'en'

const messagesByLocale: Record<Locale, AbstractIntlMessages> = { en, es }

/** Type guard for an incoming (possibly undefined) locale string. */
export function isLocale(value: string | undefined): value is Locale {
return value !== undefined && (locales as readonly string[]).includes(value)
}

/** Resolve the message catalog for a locale, falling back to the default. */
export function getMessages(locale: string | undefined): AbstractIntlMessages {
return messagesByLocale[isLocale(locale) ? locale : defaultLocale]
}
17 changes: 17 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"Nav": {
"home": "Home",
"explore": "Explore",
"leaderboard": "Leaderboard",
"dashboard": "Dashboard",
"escrow": "Escrow",
"launchApp": "Launch App",
"openMenu": "Open menu",
"closeMenu": "Close menu"
},
"LocaleSwitcher": {
"label": "Language",
"en": "English",
"es": "Español"
}
}
17 changes: 17 additions & 0 deletions messages/es.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"Nav": {
"home": "Inicio",
"explore": "Explorar",
"leaderboard": "Clasificación",
"dashboard": "Panel",
"escrow": "Depósito en garantía",
"launchApp": "Abrir app",
"openMenu": "Abrir menú",
"closeMenu": "Cerrar menú"
},
"LocaleSwitcher": {
"label": "Idioma",
"en": "English",
"es": "Español"
}
}
6 changes: 6 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
/** @type {import('next').NextConfig} */
module.exports = {
reactStrictMode: true,
// Built-in locale routing (Pages Router). Prefixes non-default locales, e.g.
// `/es/explore`, and exposes the active locale via `useRouter().locale`.
i18n: {
locales: ['en', 'es'],
defaultLocale: 'en',
},
};
Loading
Loading