Skip to content

Commit 7bbcbd8

Browse files
behitekclaude
andcommitted
feat: implement i18n with translated nav/footer and Vietnamese routes
Navigation & Footer Translations: - Updated Navbar to use dynamic translations based on current locale - Updated Footer to use translation strings for all text - Navigation links (Blog, Projects, Contact) now display in correct language - Footer copyright and "Built with" text fully translated Vietnamese Page Routes: - Created /vi/* routes for Vietnamese language versions - Copied main pages to /vi/ directory (index, projects, contact, blog) - Pages automatically detect locale from URL path - Vietnamese routes fully functional How It Works: - English: / (no prefix) - Vietnamese: /vi/* - Language switcher in navbar toggles between EN and VI - Preserves current path when switching (e.g., /projects ↔ /vi/projects) - Nav and Footer automatically display in correct language Current Status: - ✅ i18n infrastructure complete - ✅ Language switcher working in navbar - ✅ Nav and Footer fully translated - ✅ Vietnamese routes functional - 🔄 Page content still needs translation (homepage, projects, contact, blog) Test URLs: - English: http://localhost:4321/ - Vietnamese: http://localhost:4321/vi/ - Click EN/VI button in navbar to switch languages Next: Update page content to use translation strings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a6cbbf0 commit 7bbcbd8

6 files changed

Lines changed: 801 additions & 6 deletions

File tree

new-site/src/components/Footer.astro

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
---
22
import { SITE, SOCIAL_LINKS } from '@utils/constants';
33
import SocialIcon from '@components/SocialIcon.astro';
4+
import { getLangFromUrl, useTranslations } from '@i18n/utils';
45
56
const currentYear = new Date().getFullYear();
7+
const lang = getLangFromUrl(Astro.url);
8+
const t = useTranslations(lang);
69
---
710

811
<footer class="border-t border-[var(--color-border)] bg-[var(--color-bg-secondary)] mt-24">
@@ -41,17 +44,17 @@ const currentYear = new Date().getFullYear();
4144
<ul class="space-y-2">
4245
<li>
4346
<a href="/blog" class="text-[var(--color-text-muted)] hover:text-primary-600 transition-colors text-sm">
44-
Blog
47+
{t.nav.blog}
4548
</a>
4649
</li>
4750
<li>
4851
<a href="/projects" class="text-[var(--color-text-muted)] hover:text-primary-600 transition-colors text-sm">
49-
Projects
52+
{t.nav.projects}
5053
</a>
5154
</li>
5255
<li>
5356
<a href="/contact" class="text-[var(--color-text-muted)] hover:text-primary-600 transition-colors text-sm">
54-
Contact
57+
{t.nav.contact}
5558
</a>
5659
</li>
5760
<li>
@@ -66,10 +69,10 @@ const currentYear = new Date().getFullYear();
6669
<!-- Bottom -->
6770
<div class="border-t border-[var(--color-border)] mt-8 pt-8 flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
6871
<p class="text-[var(--color-text-muted)] text-sm">
69-
© {currentYear} {SITE.title}All rights reserved
72+
© {currentYear} {SITE.title}{t.footer.rights}
7073
</p>
7174
<p class="text-[var(--color-text-muted)] text-sm">
72-
Built with <a href="https://astro.build" class="link" target="_blank" rel="noopener noreferrer">Astro</a> & <a href="https://tailwindcss.com" class="link" target="_blank" rel="noopener noreferrer">TailwindCSS</a>
75+
{t.footer.builtWith} <a href="https://astro.build" class="link" target="_blank" rel="noopener noreferrer">Astro</a> {t.footer.and} <a href="https://tailwindcss.com" class="link" target="_blank" rel="noopener noreferrer">TailwindCSS</a>
7376
</p>
7477
</div>
7578
</div>

new-site/src/components/Navbar.astro

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
---
2-
import { SITE, NAV_LINKS } from '@utils/constants';
2+
import { SITE } from '@utils/constants';
33
import ThemeToggle from './ThemeToggle.astro';
44
import LanguageSwitcher from './LanguageSwitcher.astro';
5+
import { getLangFromUrl, useTranslations } from '@i18n/utils';
56
67
const currentPath = Astro.url.pathname;
8+
const lang = getLangFromUrl(Astro.url);
9+
const t = useTranslations(lang);
10+
11+
const NAV_LINKS = [
12+
{ label: t.nav.blog, href: '/blog' },
13+
{ label: t.nav.projects, href: '/projects' },
14+
{ label: t.nav.contact, href: '/contact' },
15+
];
716
---
817

918
<nav class="fixed top-0 left-0 right-0 z-40 glass border-b border-[var(--color-border)]">
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
---
2+
import { getCollection } from 'astro:content';
3+
import BaseLayout from '@layouts/BaseLayout.astro';
4+
import BlogCard from '@components/BlogCard.astro';
5+
import { readingTime } from '@utils/helpers';
6+
7+
// Get all published blog posts
8+
const allPosts = await getCollection('blog', ({ data }) => {
9+
return data.draft !== true;
10+
});
11+
12+
// Sort by date (newest first)
13+
const sortedPosts = allPosts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
14+
15+
// Get unique categories and languages
16+
const categories = [...new Set(sortedPosts.map((post) => post.data.category))];
17+
const languages = [...new Set(sortedPosts.map((post) => post.data.language))];
18+
19+
// Featured post (most recent)
20+
const featuredPost = sortedPosts[0];
21+
---
22+
23+
<BaseLayout
24+
title="Blog"
25+
description="Learning, building, and sharing insights about AI, ML, NLP, and software engineering"
26+
>
27+
<div class="container py-12">
28+
<!-- Header -->
29+
<div class="text-center mb-12">
30+
<h1 class="text-4xl md:text-5xl font-bold mb-4">✍️ Blog</h1>
31+
<p class="text-xl text-[var(--color-text-muted)] max-w-2xl mx-auto">
32+
Learning, building, and sharing insights about AI/ML, NLP, RAG, and software engineering
33+
</p>
34+
</div>
35+
36+
<!-- Filters -->
37+
<div class="flex flex-wrap items-center justify-center gap-4 mb-12" id="blog-filters">
38+
<button
39+
class="filter-btn active px-4 py-2 rounded-lg font-medium transition-all"
40+
data-filter="all"
41+
>
42+
All Posts ({sortedPosts.length})
43+
</button>
44+
{languages.map((lang) => {
45+
const count = sortedPosts.filter((p) => p.data.language === lang).length;
46+
return (
47+
<button
48+
class="filter-btn px-4 py-2 rounded-lg font-medium transition-all"
49+
data-filter={`lang-${lang}`}
50+
>
51+
{lang === 'en' ? '🇺🇸 English' : '🇻🇳 Vietnamese'} ({count})
52+
</button>
53+
);
54+
})}
55+
{categories.map((cat) => {
56+
const count = sortedPosts.filter((p) => p.data.category === cat).length;
57+
return (
58+
<button
59+
class="filter-btn px-4 py-2 rounded-lg font-medium transition-all"
60+
data-filter={`cat-${cat.toLowerCase().replace(/\//g, '-')}`}
61+
>
62+
{cat} ({count})
63+
</button>
64+
);
65+
})}
66+
</div>
67+
68+
<!-- Featured Post -->
69+
{featuredPost && (
70+
<div class="mb-16 card bg-gradient-to-br from-primary-50 to-accent-cyan/5 dark:from-primary-950 dark:to-slate-900 border-primary-200 dark:border-primary-800">
71+
<div class="flex items-center gap-2 mb-3">
72+
<span class="px-3 py-1 bg-primary-600 text-white text-sm font-bold rounded-full">
73+
📌 FEATURED
74+
</span>
75+
</div>
76+
<h2 class="text-3xl font-bold mb-3">
77+
<a href={`/blog/${featuredPost.slug}`} class="hover:text-primary-600 transition-colors">
78+
{featuredPost.data.title}
79+
</a>
80+
</h2>
81+
<p class="text-lg text-[var(--color-text-muted)] mb-4">
82+
{featuredPost.data.description}
83+
</p>
84+
<div class="flex items-center gap-4 flex-wrap">
85+
<span class={`lang-badge ${featuredPost.data.language === 'vi' ? 'lang-badge-vi' : 'lang-badge-en'}`}>
86+
{featuredPost.data.language === 'vi' ? '🇻🇳 VI' : '🇺🇸 EN'}
87+
</span>
88+
<span class="category-badge category-ai">{featuredPost.data.category}</span>
89+
<span class="text-sm text-[var(--color-text-muted)]">
90+
{new Date(featuredPost.data.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
91+
</span>
92+
<span class="text-sm text-[var(--color-text-muted)]">
93+
{readingTime(featuredPost.body)} min read
94+
</span>
95+
</div>
96+
<a href={`/blog/${featuredPost.slug}`} class="mt-4 btn-primary inline-flex items-center">
97+
Read Article
98+
<svg class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
99+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
100+
</svg>
101+
</a>
102+
</div>
103+
)}
104+
105+
<!-- All Posts Grid -->
106+
<div class="mb-8">
107+
<h2 class="text-2xl font-bold mb-6">All Posts ({sortedPosts.length})</h2>
108+
</div>
109+
110+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" id="posts-grid">
111+
{sortedPosts.map((post) => (
112+
<div
113+
class="blog-post-item"
114+
data-lang={post.data.language}
115+
data-category={post.data.category.toLowerCase().replace(/\//g, '-')}
116+
>
117+
<BlogCard
118+
title={post.data.title}
119+
description={post.data.description}
120+
date={post.data.date}
121+
category={post.data.category}
122+
language={post.data.language}
123+
slug={post.slug}
124+
readingTime={readingTime(post.body)}
125+
/>
126+
</div>
127+
))}
128+
</div>
129+
130+
<!-- Empty State -->
131+
<div id="empty-state" class="hidden text-center py-16">
132+
<p class="text-2xl text-[var(--color-text-muted)] mb-4">No posts found</p>
133+
<button id="reset-filters" class="link text-lg">
134+
Clear filters
135+
</button>
136+
</div>
137+
</div>
138+
</BaseLayout>
139+
140+
<style>
141+
.filter-btn {
142+
@apply bg-slate-100 dark:bg-slate-800 text-[var(--color-text-base)] hover:bg-primary-100 dark:hover:bg-primary-900 hover:text-primary-700 dark:hover:text-primary-300;
143+
}
144+
145+
.filter-btn.active {
146+
@apply bg-primary-600 text-white hover:bg-primary-700;
147+
}
148+
</style>
149+
150+
<script>
151+
// Client-side filtering
152+
const filterButtons = document.querySelectorAll('.filter-btn');
153+
const postItems = document.querySelectorAll('.blog-post-item');
154+
const emptyState = document.getElementById('empty-state');
155+
const postsGrid = document.getElementById('posts-grid');
156+
const resetButton = document.getElementById('reset-filters');
157+
158+
filterButtons.forEach((button) => {
159+
button.addEventListener('click', () => {
160+
const filter = button.getAttribute('data-filter') || 'all';
161+
162+
// Update active state
163+
filterButtons.forEach((btn) => btn.classList.remove('active'));
164+
button.classList.add('active');
165+
166+
// Filter posts
167+
let visibleCount = 0;
168+
169+
postItems.forEach((item) => {
170+
if (filter === 'all') {
171+
item.classList.remove('hidden');
172+
visibleCount++;
173+
} else if (filter.startsWith('lang-')) {
174+
const lang = filter.replace('lang-', '');
175+
if (item.getAttribute('data-lang') === lang) {
176+
item.classList.remove('hidden');
177+
visibleCount++;
178+
} else {
179+
item.classList.add('hidden');
180+
}
181+
} else if (filter.startsWith('cat-')) {
182+
const category = filter.replace('cat-', '');
183+
if (item.getAttribute('data-category') === category) {
184+
item.classList.remove('hidden');
185+
visibleCount++;
186+
} else {
187+
item.classList.add('hidden');
188+
}
189+
}
190+
});
191+
192+
// Show/hide empty state
193+
if (visibleCount === 0) {
194+
postsGrid?.classList.add('hidden');
195+
emptyState?.classList.remove('hidden');
196+
} else {
197+
postsGrid?.classList.remove('hidden');
198+
emptyState?.classList.add('hidden');
199+
}
200+
});
201+
});
202+
203+
// Reset filters
204+
resetButton?.addEventListener('click', () => {
205+
const allButton = document.querySelector('[data-filter="all"]') as HTMLButtonElement;
206+
allButton?.click();
207+
});
208+
</script>

0 commit comments

Comments
 (0)