From 765ef6f9fca4ee41734bba00e9d8a30e9e2649cc Mon Sep 17 00:00:00 2001 From: cialloo Date: Fri, 3 Oct 2025 11:49:51 +0800 Subject: [PATCH] Add internationalization support with i18next and language selector component --- package-lock.json | 100 +++++++++++++++++++++++++- package.json | 5 +- src/App.tsx | 107 ++++++++++++++-------------- src/components/LanguageSelector.css | 35 +++++++++ src/components/LanguageSelector.tsx | 33 +++++++++ src/i18n.ts | 36 ++++++++++ src/locales/en.json | 74 +++++++++++++++++++ src/locales/zh.json | 74 +++++++++++++++++++ src/main.tsx | 1 + 9 files changed, 410 insertions(+), 55 deletions(-) create mode 100644 src/components/LanguageSelector.css create mode 100644 src/components/LanguageSelector.tsx create mode 100644 src/i18n.ts create mode 100644 src/locales/en.json create mode 100644 src/locales/zh.json diff --git a/package-lock.json b/package-lock.json index 509081e..d042ea2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index f5339cc..d77d072 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 7859c35..211b117 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { @@ -40,16 +44,15 @@ function App() {

- Welcome to the Ultimate
- Counter-Strike Community + {t('hero.title')}
+ {t('hero.titleHighlight')} {t('hero.titleEnd')}

- 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')}

- - + +
@@ -71,22 +74,22 @@ function App() {
👥
{stats.onlinePlayers.toLocaleString()}
-
Online Players
+
{t('stats.onlinePlayers')}
🖥️
{stats.totalServers}
-
Active Servers
+
{t('stats.activeServers')}
⏱️
{stats.totalPlayTime}
-
Hours Played
+
{t('stats.hoursPlayed')}
🎯
{stats.activeGames}
-
Live Matches
+
{t('stats.liveMatches')}
@@ -94,31 +97,31 @@ function App() { {/* Features Section */}
-

Community Features

+

{t('features.title')}

🖥️
-

Server Browser

-

Find and join the best Counter-Strike servers. Browse by game mode, region, and player count.

- +

{t('features.serverBrowser.title')}

+

{t('features.serverBrowser.description')}

+
📝
-

Community Blog

-

Stay updated with the latest news, tournament results, and community announcements.

- +

{t('features.blog.title')}

+

{t('features.blog.description')}

+
📦
-

Open Source

-

Contribute to our open-source projects. Custom maps, configs, and community tools.

- +

{t('features.git.title')}

+

{t('features.git.description')}

+
💬
-

Discussion Forum

-

Join discussions about strategies, share your experiences, and connect with fellow gamers.

- +

{t('features.forum.title')}

+

{t('features.forum.description')}

+
@@ -128,7 +131,7 @@ function App() {
-

Recent Activity

+

{t('activity.title')}

{recentChats.map((chat, index) => (
@@ -141,7 +144,7 @@ function App() {
-

🏆 Top Players

+

{t('activity.topPlayers')}

1. ProGamer99 - 2,450 pts
2. SniperElite - 2,180 pts
@@ -149,11 +152,11 @@ function App() {
-

🎯 Quick Stats

+

{t('activity.quickStats')}

-
Avg Match Time: 12m 34s
-
Most Popular Map: Dust2
-
Peak Hours: 8-11 PM
+
{t('activity.avgMatchTime')}
+
{t('activity.mostPopularMap')}
+
{t('activity.peakHours')}
@@ -164,32 +167,32 @@ function App() {
diff --git a/src/components/LanguageSelector.css b/src/components/LanguageSelector.css new file mode 100644 index 0000000..997717c --- /dev/null +++ b/src/components/LanguageSelector.css @@ -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; + } +} \ No newline at end of file diff --git a/src/components/LanguageSelector.tsx b/src/components/LanguageSelector.tsx new file mode 100644 index 0000000..12842bd --- /dev/null +++ b/src/components/LanguageSelector.tsx @@ -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 ( +
+ +
+ ) +} + +export default LanguageSelector \ No newline at end of file diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..2714b36 --- /dev/null +++ b/src/i18n.ts @@ -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 \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000..d9bdfc3 --- /dev/null +++ b/src/locales/en.json @@ -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": "中文" + } +} \ No newline at end of file diff --git a/src/locales/zh.json b/src/locales/zh.json new file mode 100644 index 0000000..e0969ce --- /dev/null +++ b/src/locales/zh.json @@ -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": "中文" + } +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index bef5202..cd81cee 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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(