Add internationalization support with i18next and language selector component

This commit is contained in:
2025-10-03 11:49:51 +08:00
parent a4a3cfafd2
commit 765ef6f9fc
9 changed files with 410 additions and 55 deletions

100
package-lock.json generated
View File

@@ -8,8 +8,11 @@
"name": "www.cialloo.com",
"version": "0.0.0",
"dependencies": {
"i18next": "^25.5.3",
"i18next-browser-languagedetector": "^8.2.0",
"react": "^19.1.1",
"react-dom": "^19.1.1"
"react-dom": "^19.1.1",
"react-i18next": "^16.0.0"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
@@ -259,6 +262,15 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@@ -1983,6 +1995,55 @@
"node": ">=8"
}
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"license": "MIT",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/i18next": {
"version": "25.5.3",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.5.3.tgz",
"integrity": "sha512-joFqorDeQ6YpIXni944upwnuHBf5IoPMuqAchGVeQLdWC2JOjxgM9V8UGLhNIIH/Q8QleRxIi0BSRQehSrDLcg==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
"integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2716,6 +2777,32 @@
"react": "^19.1.1"
}
},
"node_modules/react-i18next": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.0.0.tgz",
"integrity": "sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 25.5.2",
"react": ">= 16.8.0",
"typescript": "^5"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@@ -2986,7 +3073,7 @@
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -3169,6 +3256,15 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -10,8 +10,11 @@
"preview": "vite preview"
},
"dependencies": {
"i18next": "^25.5.3",
"i18next-browser-languagedetector": "^8.2.0",
"react": "^19.1.1",
"react-dom": "^19.1.1"
"react-dom": "^19.1.1",
"react-i18next": "^16.0.0"
},
"devDependencies": {
"@eslint/js": "^9.36.0",

View File

@@ -1,6 +1,9 @@
import { useTranslation } from 'react-i18next'
import LanguageSelector from './components/LanguageSelector'
import './App.css'
function App() {
const { t } = useTranslation()
// Mock data - in real app, this would come from API
const stats = {
@@ -24,14 +27,15 @@ function App() {
<nav className="navbar">
<div className="nav-container">
<div className="nav-logo">
<h2>🎯 CS Community</h2>
<h2>🎯 {t('nav.logo')}</h2>
</div>
<div className="nav-links">
<a href="#servers">Servers</a>
<a href="#blog">Blog</a>
<a href="#git">Git</a>
<a href="#forum">Forum</a>
<button className="join-btn">Join Now</button>
<a href="#servers">{t('nav.servers')}</a>
<a href="#blog">{t('nav.blog')}</a>
<a href="#git">{t('nav.git')}</a>
<a href="#forum">{t('nav.forum')}</a>
<LanguageSelector />
<button className="join-btn">{t('nav.joinNow')}</button>
</div>
</div>
</nav>
@@ -40,16 +44,15 @@ function App() {
<section className="hero">
<div className="hero-content">
<h1 className="hero-title">
Welcome to the Ultimate<br />
<span className="highlight">Counter-Strike</span> Community
{t('hero.title')}<br />
<span className="highlight">{t('hero.titleHighlight')}</span> {t('hero.titleEnd')}
</h1>
<p className="hero-subtitle">
Join thousands of players in competitive matches, casual games, and community events.
Experience the best gaming community with dedicated servers and passionate players.
{t('hero.subtitle')}
</p>
<div className="hero-buttons">
<button className="btn-primary">🎮 Start Playing</button>
<button className="btn-secondary">📊 View Stats</button>
<button className="btn-primary">{t('hero.startPlaying')}</button>
<button className="btn-secondary">{t('hero.viewStats')}</button>
</div>
</div>
<div className="hero-visual">
@@ -71,22 +74,22 @@ function App() {
<div className="stat-card">
<div className="stat-icon">👥</div>
<div className="stat-number">{stats.onlinePlayers.toLocaleString()}</div>
<div className="stat-label">Online Players</div>
<div className="stat-label">{t('stats.onlinePlayers')}</div>
</div>
<div className="stat-card">
<div className="stat-icon">🖥</div>
<div className="stat-number">{stats.totalServers}</div>
<div className="stat-label">Active Servers</div>
<div className="stat-label">{t('stats.activeServers')}</div>
</div>
<div className="stat-card">
<div className="stat-icon"></div>
<div className="stat-number">{stats.totalPlayTime}</div>
<div className="stat-label">Hours Played</div>
<div className="stat-label">{t('stats.hoursPlayed')}</div>
</div>
<div className="stat-card">
<div className="stat-icon">🎯</div>
<div className="stat-number">{stats.activeGames}</div>
<div className="stat-label">Live Matches</div>
<div className="stat-label">{t('stats.liveMatches')}</div>
</div>
</div>
</section>
@@ -94,31 +97,31 @@ function App() {
{/* Features Section */}
<section className="features-section">
<div className="features-container">
<h2 className="section-title">Community Features</h2>
<h2 className="section-title">{t('features.title')}</h2>
<div className="features-grid">
<div className="feature-card" id="servers">
<div className="feature-icon">🖥</div>
<h3>Server Browser</h3>
<p>Find and join the best Counter-Strike servers. Browse by game mode, region, and player count.</p>
<button className="feature-btn">Browse Servers</button>
<h3>{t('features.serverBrowser.title')}</h3>
<p>{t('features.serverBrowser.description')}</p>
<button className="feature-btn">{t('features.browseServers')}</button>
</div>
<div className="feature-card" id="blog">
<div className="feature-icon">📝</div>
<h3>Community Blog</h3>
<p>Stay updated with the latest news, tournament results, and community announcements.</p>
<button className="feature-btn">Read Blog</button>
<h3>{t('features.blog.title')}</h3>
<p>{t('features.blog.description')}</p>
<button className="feature-btn">{t('features.readBlog')}</button>
</div>
<div className="feature-card" id="git">
<div className="feature-icon">📦</div>
<h3>Open Source</h3>
<p>Contribute to our open-source projects. Custom maps, configs, and community tools.</p>
<button className="feature-btn">View GitHub</button>
<h3>{t('features.git.title')}</h3>
<p>{t('features.git.description')}</p>
<button className="feature-btn">{t('features.viewGitHub')}</button>
</div>
<div className="feature-card" id="forum">
<div className="feature-icon">💬</div>
<h3>Discussion Forum</h3>
<p>Join discussions about strategies, share your experiences, and connect with fellow gamers.</p>
<button className="feature-btn">Join Forum</button>
<h3>{t('features.forum.title')}</h3>
<p>{t('features.forum.description')}</p>
<button className="feature-btn">{t('features.joinForum')}</button>
</div>
</div>
</div>
@@ -128,7 +131,7 @@ function App() {
<section className="activity-section">
<div className="activity-container">
<div className="activity-main">
<h2 className="section-title">Recent Activity</h2>
<h2 className="section-title">{t('activity.title')}</h2>
<div className="chat-feed">
{recentChats.map((chat, index) => (
<div key={index} className="chat-message">
@@ -141,7 +144,7 @@ function App() {
</div>
<div className="activity-sidebar">
<div className="sidebar-card">
<h3>🏆 Top Players</h3>
<h3>{t('activity.topPlayers')}</h3>
<div className="leaderboard">
<div className="leader-item">1. ProGamer99 - 2,450 pts</div>
<div className="leader-item">2. SniperElite - 2,180 pts</div>
@@ -149,11 +152,11 @@ function App() {
</div>
</div>
<div className="sidebar-card">
<h3>🎯 Quick Stats</h3>
<h3>{t('activity.quickStats')}</h3>
<div className="quick-stats">
<div>Avg Match Time: 12m 34s</div>
<div>Most Popular Map: Dust2</div>
<div>Peak Hours: 8-11 PM</div>
<div>{t('activity.avgMatchTime')}</div>
<div>{t('activity.mostPopularMap')}</div>
<div>{t('activity.peakHours')}</div>
</div>
</div>
</div>
@@ -164,32 +167,32 @@ function App() {
<footer className="footer">
<div className="footer-container">
<div className="footer-section">
<h4>🎯 CS Community</h4>
<p>The ultimate destination for Counter-Strike enthusiasts worldwide.</p>
<h4>{t('footer.community')}</h4>
<p>{t('footer.communityDesc')}</p>
</div>
<div className="footer-section">
<h4>Quick Links</h4>
<a href="#servers">Servers</a>
<a href="#blog">Blog</a>
<a href="#forum">Forum</a>
<a href="#git">GitHub</a>
<h4>{t('footer.quickLinks')}</h4>
<a href="#servers">{t('nav.servers')}</a>
<a href="#blog">{t('nav.blog')}</a>
<a href="#forum">{t('nav.forum')}</a>
<a href="#git">{t('nav.git')}</a>
</div>
<div className="footer-section">
<h4>Community</h4>
<a href="#">Discord</a>
<a href="#">Steam Group</a>
<a href="#">Tournaments</a>
<a href="#">Support</a>
<h4>{t('footer.communityLinks')}</h4>
<a href="#">{t('footer.discord')}</a>
<a href="#">{t('footer.steamGroup')}</a>
<a href="#">{t('footer.tournaments')}</a>
<a href="#">{t('footer.support')}</a>
</div>
<div className="footer-section">
<h4>Legal</h4>
<a href="#">Privacy Policy</a>
<a href="#">Terms of Service</a>
<a href="#">Contact</a>
<h4>{t('footer.legal')}</h4>
<a href="#">{t('footer.privacyPolicy')}</a>
<a href="#">{t('footer.termsOfService')}</a>
<a href="#">{t('footer.contact')}</a>
</div>
</div>
<div className="footer-bottom">
<p>&copy; 2025 CS Community. All rights reserved. | Made with for gamers</p>
<p>{t('footer.copyright')}</p>
</div>
</footer>
</div>

View File

@@ -0,0 +1,35 @@
.language-selector {
position: relative;
}
.language-dropdown {
background: rgba(0, 0, 0, 0.8);
border: 1px solid var(--primary-red);
color: var(--text-primary);
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.9rem;
cursor: pointer;
min-width: 100px;
transition: all 0.3s ease;
}
.language-dropdown:focus {
outline: none;
border-color: var(--primary-red-hover);
box-shadow: 0 0 0 2px rgba(255, 70, 85, 0.2);
}
.language-dropdown option {
background: var(--dark-bg);
color: var(--text-primary);
}
/* Mobile responsive */
@media (max-width: 768px) {
.language-dropdown {
font-size: 0.8rem;
padding: 0.4rem 0.8rem;
min-width: 80px;
}
}

View File

@@ -0,0 +1,33 @@
import { useTranslation } from 'react-i18next'
import './LanguageSelector.css'
function LanguageSelector() {
const { i18n, t } = useTranslation()
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng)
}
const languages = [
{ code: 'en', name: t('languages.english') },
{ code: 'zh', name: t('languages.chinese') }
]
return (
<div className="language-selector">
<select
value={i18n.language}
onChange={(e) => changeLanguage(e.target.value)}
className="language-dropdown"
>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.name}
</option>
))}
</select>
</div>
)
}
export default LanguageSelector

36
src/i18n.ts Normal file
View File

@@ -0,0 +1,36 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import en from './locales/en.json'
import zh from './locales/zh.json'
const resources = {
en: {
translation: en
},
zh: {
translation: zh
}
}
i18n
.use(LanguageDetector) // Detect user language
.use(initReactI18next) // Pass i18n down to react-i18next
.init({
resources,
fallbackLng: 'en', // Fallback to English if detection fails
debug: false,
interpolation: {
escapeValue: false // React already does escaping
},
detection: {
order: ['localStorage', 'navigator', 'htmlTag'],
lookupLocalStorage: 'language',
caches: ['localStorage']
}
})
export default i18n

74
src/locales/en.json Normal file
View File

@@ -0,0 +1,74 @@
{
"nav": {
"logo": "CS Community",
"servers": "Servers",
"blog": "Blog",
"git": "Git",
"forum": "Forum",
"joinNow": "Join Now"
},
"hero": {
"title": "Welcome to the Ultimate",
"titleHighlight": "Counter-Strike",
"titleEnd": "Community",
"subtitle": "Join thousands of players in competitive matches, casual games, and community events. Experience the best gaming community with dedicated servers and passionate players.",
"startPlaying": "🎮 Start Playing",
"viewStats": "📊 View Stats"
},
"stats": {
"onlinePlayers": "Online Players",
"activeServers": "Active Servers",
"hoursPlayed": "Hours Played",
"liveMatches": "Live Matches"
},
"features": {
"title": "Community Features",
"serverBrowser": {
"title": "Server Browser",
"description": "Find and join the best Counter-Strike servers. Browse by game mode, region, and player count."
},
"blog": {
"title": "Community Blog",
"description": "Stay updated with the latest news, tournament results, and community announcements."
},
"git": {
"title": "Open Source",
"description": "Contribute to our open-source projects. Custom maps, configs, and community tools."
},
"forum": {
"title": "Discussion Forum",
"description": "Join discussions about strategies, share your experiences, and connect with fellow gamers."
},
"browseServers": "Browse Servers",
"readBlog": "Read Blog",
"viewGitHub": "View GitHub",
"joinForum": "Join Forum"
},
"activity": {
"title": "Recent Activity",
"topPlayers": "🏆 Top Players",
"quickStats": "🎯 Quick Stats",
"avgMatchTime": "Avg Match Time: 12m 34s",
"mostPopularMap": "Most Popular Map: Dust2",
"peakHours": "Peak Hours: 8-11 PM"
},
"footer": {
"community": "🎯 CS Community",
"communityDesc": "The ultimate destination for Counter-Strike enthusiasts worldwide.",
"quickLinks": "Quick Links",
"communityLinks": "Community",
"legal": "Legal",
"discord": "Discord",
"steamGroup": "Steam Group",
"tournaments": "Tournaments",
"support": "Support",
"privacyPolicy": "Privacy Policy",
"termsOfService": "Terms of Service",
"contact": "Contact",
"copyright": "© 2025 CS Community. All rights reserved. | Made with ❤️ for gamers"
},
"languages": {
"english": "English",
"chinese": "中文"
}
}

74
src/locales/zh.json Normal file
View File

@@ -0,0 +1,74 @@
{
"nav": {
"logo": "CS 社区",
"servers": "服务器",
"blog": "博客",
"git": "开源",
"forum": "论坛",
"joinNow": "立即加入"
},
"hero": {
"title": "欢迎来到终极",
"titleHighlight": "反恐精英",
"titleEnd": "社区",
"subtitle": "加入数千名玩家参与竞技比赛、休闲游戏和社区活动。在专用服务器和热情玩家中体验最佳游戏社区。",
"startPlaying": "🎮 开始游戏",
"viewStats": "📊 查看统计"
},
"stats": {
"onlinePlayers": "在线玩家",
"activeServers": "活跃服务器",
"hoursPlayed": "游戏时长",
"liveMatches": "实时比赛"
},
"features": {
"title": "社区功能",
"serverBrowser": {
"title": "服务器浏览器",
"description": "查找并加入最佳反恐精英服务器。按游戏模式、地区和玩家数量浏览。"
},
"blog": {
"title": "社区博客",
"description": "及时了解最新新闻、锦标赛结果和社区公告。"
},
"git": {
"title": "开源项目",
"description": "为我们的开源项目做出贡献。自定义地图、配置和社区工具。"
},
"forum": {
"title": "讨论论坛",
"description": "加入关于策略的讨论,分享您的经验,与其他玩家交流。"
},
"browseServers": "浏览服务器",
"readBlog": "阅读博客",
"viewGitHub": "查看 GitHub",
"joinForum": "加入论坛"
},
"activity": {
"title": "最近活动",
"topPlayers": "🏆 顶级玩家",
"quickStats": "🎯 快速统计",
"avgMatchTime": "平均比赛时长12分34秒",
"mostPopularMap": "最受欢迎地图Dust2",
"peakHours": "高峰时段晚上8-11点"
},
"footer": {
"community": "🎯 CS 社区",
"communityDesc": "反恐精英爱好者的终极目的地。",
"quickLinks": "快速链接",
"communityLinks": "社区",
"legal": "法律",
"discord": "Discord",
"steamGroup": "Steam 群组",
"tournaments": "锦标赛",
"support": "支持",
"privacyPolicy": "隐私政策",
"termsOfService": "服务条款",
"contact": "联系我们",
"copyright": "© 2025 CS 社区。保留所有权利。| 用 ❤️ 为玩家打造"
},
"languages": {
"english": "English",
"chinese": "中文"
}
}

View File

@@ -1,6 +1,7 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import './i18n'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(