feat(i18n): localize static elements, navbar switcher, and search palette

This commit is contained in:
Poyraz
2026-06-27 14:42:45 +03:00
parent d3c56713fb
commit 0d112c6568
7 changed files with 96 additions and 68 deletions
+5 -2
View File
@@ -1,6 +1,7 @@
import Link from "next/link";
import { Link } from "@/i18n/routing";
import { Icon } from "@iconify/react";
import { Card, Typography } from "poyraz-ui/atoms";
import { useTranslations } from "next-intl";
export const CONTACT_LINKS = [
{
@@ -27,6 +28,8 @@ export const CONTACT_LINKS = [
] as const;
export function ContactContent() {
const t = useTranslations("Contact");
return (
<section className="grid gap-3 md:grid-cols-3">
{CONTACT_LINKS.map((item) => (
@@ -42,7 +45,7 @@ export function ContactContent() {
<Icon icon={item.icon} width={18} height={18} />
</div>
<Typography variant="large" className="mt-3 text-base leading-tight">
{item.label}
{t(item.id)}
</Typography>
<Typography variant="small" className="mt-1 text-muted-foreground">
{item.value}
+12 -9
View File
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { Button, Card, Typography } from "poyraz-ui/atoms";
import { Modal, ModalContent, ModalTitle } from "poyraz-ui/molecules";
import { YoutubeLiteEmbed } from "@/components/youtube-lite-embed";
import { useTranslations } from "next-intl";
type ContentContentProps = {
youtubeLinks: readonly string[];
@@ -11,6 +12,7 @@ type ContentContentProps = {
};
function PdfFirstPagePreview({ src, title }: { src: string; title: string }) {
const t = useTranslations("Content");
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [failed, setFailed] = useState(false);
@@ -86,7 +88,7 @@ function PdfFirstPagePreview({ src, title }: { src: string; title: string }) {
return (
<div className="flex h-full w-full items-center justify-center bg-muted/20 p-2">
<Typography variant="small" className="text-muted-foreground">
Önizleme yüklenemedi
{t("pdfPreviewError")}
</Typography>
</div>
);
@@ -107,6 +109,7 @@ export function ContentContent({
youtubeLinks,
pdfFiles,
}: ContentContentProps) {
const t = useTranslations("Content");
const [pdfModalOpen, setPdfModalOpen] = useState(false);
const [activePdfIndex, setActivePdfIndex] = useState(0);
@@ -128,7 +131,7 @@ export function ContentContent({
<section className="flex h-full flex-col gap-3 overflow-y-auto">
<section className="space-y-2">
<Typography variant="large" className="text-base">
Son YouTube Videoları
{t("youtubeTitle")}
</Typography>
<div className="grid gap-2 md:grid-cols-3">
{embeddedVideos.map((link) => (
@@ -136,7 +139,7 @@ export function ContentContent({
key={link}
className="overflow-hidden rounded-sm border-border p-0"
>
<YoutubeLiteEmbed link={link} title="YouTube video oynatici" />
<YoutubeLiteEmbed link={link} title={t("youtubeEmbedTitle")} />
</Card>
))}
</div>
@@ -144,7 +147,7 @@ export function ContentContent({
<section className="space-y-2">
<Typography variant="large" className="text-base">
LinkedIn PDF Notları
{t("pdfTitle")}
</Typography>
<div className="grid grid-cols-2 gap-2 lg:grid-cols-3">
{pdfFiles.map((pdf, index) => (
@@ -157,7 +160,7 @@ export function ContentContent({
<Card className="overflow-hidden rounded-sm border-border p-0 transition-colors hover:border-zinc-700">
<PdfFirstPagePreview
src={`/pdf/${pdf}`}
title={`${pdf} önizleme`}
title={t("pdfPreviewTitle", { name: pdf })}
/>
</Card>
</button>
@@ -168,7 +171,7 @@ export function ContentContent({
<Modal open={pdfModalOpen} onOpenChange={setPdfModalOpen}>
<ModalContent size="xl" className="rounded-sm p-4">
<ModalTitle>
{activePdf ? activePdf.replace(/\.pdf$/i, "") : "PDF Notu"}
{activePdf ? activePdf.replace(/\.pdf$/i, "") : t("pdfModalDefaultTitle")}
</ModalTitle>
<div className="mt-3 flex flex-wrap items-center justify-between gap-2">
@@ -187,7 +190,7 @@ export function ContentContent({
setActivePdfIndex((prev) => Math.max(0, prev - 1))
}
>
Önceki
{t("prev")}
</Button>
<Button
type="button"
@@ -200,7 +203,7 @@ export function ContentContent({
)
}
>
Sonraki
{t("next")}
</Button>
</div>
</div>
@@ -216,7 +219,7 @@ export function ContentContent({
) : (
<Card className="mt-3 rounded-sm border-border p-3">
<Typography variant="small" className="text-muted-foreground">
`/public/pdf` içinde PDF bulunamadı.
{t("pdfNotFound")}
</Typography>
</Card>
)}
+6 -4
View File
@@ -5,12 +5,14 @@ import { useCallback, useEffect, useState } from "react";
import { Icon } from "@iconify/react";
import { Typography } from "poyraz-ui/atoms";
import { StaggerContainer, StaggerItem } from "@/components/motion-wrapper";
import { useTranslations } from "next-intl";
type GalleryContentProps = {
images: string[];
};
export function GalleryContent({ images }: GalleryContentProps) {
const t = useTranslations("Gallery");
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
// Keyboard navigation for lightbox
@@ -53,7 +55,7 @@ export function GalleryContent({ images }: GalleryContentProps) {
<section className="flex h-full items-center justify-center">
<div className="rounded-xl border border-dashed border-border py-20 px-10 text-center">
<Typography variant="muted">
Henüz galeriye görsel eklenmemiş.
{t("empty")}
</Typography>
</div>
</section>
@@ -95,7 +97,7 @@ export function GalleryContent({ images }: GalleryContentProps) {
type="button"
className="absolute top-4 right-4 z-10 flex h-12 w-12 cursor-pointer items-center justify-center rounded-full bg-zinc-100 text-zinc-900 transition-colors hover:bg-zinc-200 shadow-sm border border-zinc-200"
onClick={closeLightbox}
aria-label="Kapat"
aria-label={t("close")}
>
<Icon icon="mdi:close" width={24} height={24} />
</button>
@@ -109,7 +111,7 @@ export function GalleryContent({ images }: GalleryContentProps) {
e.stopPropagation();
setLightboxIndex(lightboxIndex - 1);
}}
aria-label="Önceki"
aria-label={t("prev")}
>
<Icon icon="mdi:chevron-left" width={32} height={32} />
</button>
@@ -124,7 +126,7 @@ export function GalleryContent({ images }: GalleryContentProps) {
e.stopPropagation();
setLightboxIndex(lightboxIndex + 1);
}}
aria-label="Sonraki"
aria-label={t("next")}
>
<Icon icon="mdi:chevron-right" width={32} height={32} />
</button>
+3 -2
View File
@@ -4,6 +4,7 @@ import Image from "next/image";
import { useRef, useState } from "react";
import { Card, Typography } from "poyraz-ui/atoms";
import { NewsCard } from "poyraz-ui/molecules";
import { useTranslations } from "next-intl";
type HomeHeroProps = {
news: {
@@ -19,6 +20,7 @@ type HomeHeroProps = {
export function HomeHero({ news }: HomeHeroProps) {
const [frame, setFrame] = useState<1 | 2>(1);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const t = useTranslations("Home");
const handleMouseEnter = () => {
if (intervalRef.current) return;
@@ -62,8 +64,7 @@ export function HomeHero({ news }: HomeHeroProps) {
</Typography>
</div>
<Typography variant="small" className="text-muted-foreground">
Teknolojiyi merak eden bir genç. Yazılım geliştirme, yapay zeka ve
teknoloji dünyasındaki gelişmeleri takip ediyor.
{t("role")}
</Typography>
</div>
+32 -26
View File
@@ -1,8 +1,8 @@
"use client";
"use client";
import Image from "next/image";
import { Icon } from "@iconify/react";
import Link from "next/link";
import { Link } from "@/i18n/routing";
import { useTranslations } from "next-intl";
import { useMemo, useState } from "react";
import {
Avatar,
@@ -23,15 +23,6 @@ import {
type CategoryFilter = "all" | LinkDirectoryCategory;
const FILTER_ITEMS: ReadonlyArray<{ id: CategoryFilter; label: string }> = [
{ id: "all", label: "Tüm kategoriler" },
...LINK_DIRECTORY_CATEGORIES,
];
const CATEGORY_LABELS = Object.fromEntries(
LINK_DIRECTORY_CATEGORIES.map((item) => [item.id, item.label]),
) as Record<(typeof LINK_DIRECTORY_CATEGORIES)[number]["id"], string>;
const CATEGORY_ORDER = {
resources: 0,
navigation: 1,
@@ -67,9 +58,20 @@ function formatHref(href: string) {
}
export function LinksContent() {
const t = useTranslations("Links");
const tNav = useTranslations("Nav");
const [activeCategory, setActiveCategory] = useState<CategoryFilter>("all");
const [query, setQuery] = useState("");
const filterItems = useMemo(() => [
{ id: "all" as const, label: t("allCategories") },
...LINK_DIRECTORY_CATEGORIES.map((item) => ({
id: item.id,
label: t(`categories.${item.id}`),
})),
], [t]);
const filteredItems = useMemo(() => {
const normalizedQuery = normalize(query.trim());
const queryTokens = normalizedQuery.split(/\s+/).filter(Boolean);
@@ -84,12 +86,17 @@ export function LinksContent() {
}
const haystack = normalize(
[item.label, item.href, CATEGORY_LABELS[item.category], ...item.keywords].join(" "),
[
tNav.has(item.id) ? tNav(item.id) : item.label,
item.href,
t(`categories.${item.category}`),
...item.keywords,
].join(" "),
);
return queryTokens.every((token) => haystack.includes(token));
});
}, [activeCategory, query]);
}, [activeCategory, query, t, tNav]);
return (
<section className="relative isolate min-h-dvh overflow-hidden px-4 py-8 sm:px-6 sm:py-10">
@@ -128,8 +135,7 @@ export function LinksContent() {
</div>
<Typography variant="small" className="max-w-lg text-sm leading-6 text-muted-foreground">
Burada sosyal hesaplarım, portfolyo sayfalarım ve hızlı erişim linklerimin
tamamı tek yerde duruyor. Direkt açıp paylaşabilirsin.
{t("desc")}
</Typography>
</div>
@@ -165,7 +171,7 @@ export function LinksContent() {
</span>
<div className="min-w-0">
<Typography variant="large" className="truncate text-base">
{item.label}
{tNav.has(item.id) ? tNav(item.id) : item.label}
</Typography>
<Typography
variant="small"
@@ -183,10 +189,10 @@ export function LinksContent() {
<div className="space-y-3 pt-1">
<div>
<Typography variant="large" className="text-base">
Tüm Linkler
{t("allLinks")}
</Typography>
<Typography variant="small" className="text-muted-foreground">
Sayfalar, kaynaklar ve sosyal profiller
{t("allLinksDesc")}
</Typography>
</div>
@@ -196,10 +202,10 @@ export function LinksContent() {
onValueChange={(value) => setActiveCategory(value as CategoryFilter)}
>
<SelectTrigger className="h-11 rounded-2xl border-border bg-background px-4 text-sm">
<SelectValue placeholder="Kategori seç" />
<SelectValue placeholder={t("selectCategory")} />
</SelectTrigger>
<SelectContent>
{FILTER_ITEMS.map((item) => (
{filterItems.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.label}
</SelectItem>
@@ -210,8 +216,8 @@ export function LinksContent() {
<Input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Link ara... github, medium, /blog"
aria-label="Linklerde arama"
placeholder={t("searchPlaceholder")}
aria-label={t("allLinks")}
className="h-11 rounded-2xl"
/>
</div>
@@ -234,10 +240,10 @@ export function LinksContent() {
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<Typography variant="large" className="text-base leading-tight">
{item.label}
{tNav.has(item.id) ? tNav(item.id) : item.label}
</Typography>
<span className="inline-flex rounded-full border border-border px-2.5 py-0.5 text-[11px] text-muted-foreground">
{CATEGORY_LABELS[item.category]}
{t(`categories.${item.category}`)}
</span>
</div>
@@ -264,7 +270,7 @@ export function LinksContent() {
{filteredItems.length === 0 ? (
<Card className="rounded-2xl border-border px-4 py-5">
<Typography variant="small" className="text-muted-foreground">
Seçili filtreye uygun link bulunamadı.
{t("empty")}
</Typography>
</Card>
) : null}
+18 -8
View File
@@ -1,8 +1,9 @@
"use client";
import { Icon } from "@iconify/react";
import { useRouter } from "next/navigation";
import { useRouter } from "@/i18n/routing";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useLocale, useTranslations } from "next-intl";
import {
CommandPalette,
CommandPaletteContent,
@@ -15,7 +16,7 @@ import {
CommandPaletteSeparator,
} from "poyraz-ui/molecules";
import {
COMMAND_PALETTE_GROUPS,
getCommandPaletteGroups,
type CommandPaletteItem as PaletteItem,
} from "@/lib/command-palette-links";
import { useKeyboardShortcutLabel } from "@/lib/use-keyboard-shortcut-label";
@@ -27,8 +28,17 @@ type SearchCommandProps = {
export function SearchCommand({ open, onOpenChange }: SearchCommandProps) {
const router = useRouter();
const locale = useLocale();
const tLinks = useTranslations("Links");
const tNav = useTranslations("Nav");
const t = useTranslations("SearchCommand");
const shortcut = useKeyboardShortcutLabel();
const [query, setQuery] = useState("");
const groups = useMemo(() => {
return getCommandPaletteGroups(locale, tLinks, tNav);
}, [locale, tLinks, tNav]);
const handleOpenChange = useCallback((nextOpen: boolean) => {
if (!nextOpen) {
setQuery("");
@@ -51,11 +61,11 @@ export function SearchCommand({ open, onOpenChange }: SearchCommandProps) {
const filteredGroups = useMemo(() => {
const value = query.trim().toLowerCase();
if (!value) return COMMAND_PALETTE_GROUPS;
if (!value) return groups;
const tokens = value.split(/\s+/).filter(Boolean);
return COMMAND_PALETTE_GROUPS.map((group) => ({
return groups.map((group) => ({
...group,
items: group.items.filter((item) => {
const haystack = [
@@ -70,7 +80,7 @@ export function SearchCommand({ open, onOpenChange }: SearchCommandProps) {
return tokens.every((token) => haystack.includes(token));
}),
})).filter((group) => group.items.length > 0);
}, [query]);
}, [query, groups]);
const handleCommandSelect = (item: PaletteItem) => {
handleOpenChange(false);
@@ -87,13 +97,13 @@ export function SearchCommand({ open, onOpenChange }: SearchCommandProps) {
<CommandPalette open={open} onOpenChange={handleOpenChange}>
<CommandPaletteContent className=" rounded-none p-0 pt-3 w-72 sm:w-full sm:max-w-xl sm:rounded-sm sm:p-0 sm:pt-2">
<CommandPaletteInput
placeholder="Sayfa ve bağlantılarda ara..."
placeholder={t("placeholder")}
className="mt-2 pr-10"
onValueChange={setQuery}
/>
<CommandPaletteList>
{filteredGroups.length === 0 ? (
<CommandPaletteEmpty>Sonuç bulunamadı.</CommandPaletteEmpty>
<CommandPaletteEmpty>{t("empty")}</CommandPaletteEmpty>
) : (
filteredGroups.map((group, index) => (
<div key={group.id}>
@@ -115,7 +125,7 @@ export function SearchCommand({ open, onOpenChange }: SearchCommandProps) {
)}
</CommandPaletteList>
<CommandPaletteFooter className="text-xs text-muted-foreground">
Hızlıca açmak için {shortcut} kullan
{t("footer", { shortcut })}
</CommandPaletteFooter>
</CommandPaletteContent>
</CommandPalette>
+17 -14
View File
@@ -2,8 +2,9 @@
import { Icon } from "@iconify/react";
import dynamic from "next/dynamic";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Link, usePathname } from "@/i18n/routing";
import { useTranslations } from "next-intl";
import { LanguageSwitcher } from "@/components/language-switcher";
import { useState } from "react";
import {
Avatar,
@@ -45,6 +46,7 @@ export function SiteNavbar() {
const pathname = usePathname();
const [searchOpen, setSearchOpen] = useState(false);
const shortcut = useKeyboardShortcutLabel();
const t = useTranslations("Nav");
const isActiveLink = (href: string) => {
if (href === "/") return pathname === "/";
@@ -60,13 +62,14 @@ export function SiteNavbar() {
href={item.href}
target={item.external ? "_blank" : undefined}
rel={item.external ? "noreferrer" : undefined}
aria-label={item.label}
title={item.label}
aria-label={t.has(item.id) ? t(item.id) : item.label}
title={t.has(item.id) ? t(item.id) : item.label}
className="inline-flex h-8 w-8 items-center justify-center rounded-sm border border-border text-muted-foreground transition-colors hover:text-foreground"
>
<Icon icon={item.icon} width={16} height={16} />
</Link>
))}
<LanguageSwitcher />
</div>
<header className="flex items-center justify-between gap-3 border-b border-border pb-4">
@@ -99,7 +102,7 @@ export function SiteNavbar() {
href={item.href}
className={getNavLinkClass(isActiveLink(item.href))}
>
{item.label}
{t(item.id)}
</Link>
</li>
))}
@@ -115,11 +118,11 @@ export function SiteNavbar() {
type="button"
onClick={() => setSearchOpen(true)}
className="inline-flex h-8 w-44 cursor-pointer items-center justify-between rounded-sm border border-border px-2.5 text-sm text-muted-foreground transition-colors hover:text-foreground sm:w-52"
aria-label="Komut paletini aç"
aria-label={t("search")}
>
<span className="inline-flex items-center gap-2">
<Icon icon="mdi:magnify" width={16} height={16} />
<span>Ara</span>
<span>{t("search")}</span>
</span>
<span className="text-xs text-muted-foreground">{shortcut}</span>
</button>
@@ -127,10 +130,10 @@ export function SiteNavbar() {
<DropdownMenu>
<DropdownMenuTrigger className="inline-flex h-8 cursor-pointer items-center gap-2 rounded-sm border border-border px-2.5 text-sm text-muted-foreground transition-colors hover:text-foreground">
<Icon icon="mdi:share-variant" width={16} height={16} />
<span>Sosyal</span>
<span>{t("social")}</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52 rounded-sm">
<DropdownMenuLabel>Sosyal Bağlantılar</DropdownMenuLabel>
<DropdownMenuLabel>{t("socialLinks")}</DropdownMenuLabel>
<DropdownMenuSeparator />
{SOCIAL_LINKS.map((item) => (
<DropdownMenuItem key={item.id} asChild>
@@ -153,21 +156,21 @@ export function SiteNavbar() {
<Sheet>
<SheetTrigger className="inline-flex h-8 cursor-pointer items-center gap-2 rounded-sm border border-border px-2.5 text-sm text-muted-foreground transition-colors hover:text-foreground">
<Icon icon="mdi:menu" width={18} height={18} />
<span>Menü</span>
<span>{t("menu")}</span>
</SheetTrigger>
<SheetContent side="right" className="w-72 p-4">
<SheetTitle className="sr-only">Mobil Menü</SheetTitle>
<SheetTitle className="sr-only">{t("mobileMenu")}</SheetTitle>
<div className="flex flex-col gap-4">
<SheetClose asChild>
<button
type="button"
onClick={() => setSearchOpen(true)}
className="inline-flex h-9 w-full cursor-pointer items-center justify-between rounded-sm border border-border px-3 text-sm text-muted-foreground transition-colors hover:text-foreground"
aria-label="Komut paletini aç"
aria-label={t("search")}
>
<span className="inline-flex items-center gap-2">
<Icon icon="mdi:magnify" width={16} height={16} />
<span>Ara</span>
<span>{t("search")}</span>
</span>
<span className="text-xs text-muted-foreground/80">
{shortcut}
@@ -186,7 +189,7 @@ export function SiteNavbar() {
href={item.href}
className={getNavLinkClass(isActiveLink(item.href))}
>
{item.label}
{t(item.id)}
</Link>
</SheetClose>
</li>