Compare commits
3 commits
e1d44586b9
...
9ac43ef4f9
Author | SHA1 | Date | |
---|---|---|---|
9ac43ef4f9 | |||
47cf6171c5 | |||
25bd99abc5 |
94 changed files with 412 additions and 144 deletions
|
@ -46,7 +46,7 @@ homepage
|
|||
|
||||
## Contribution Guide
|
||||
|
||||
See [CONTRIBUTING.md](docs/en-US/CONTRIBUTING.md) in the `docs` directory to learn how to participate in the project.
|
||||
See [CONTRIBUTING.md](docs/en/CONTRIBUTING.md) in the `docs` directory to learn how to participate in the project.
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ homepage
|
|||
|
||||
## 贡献指南
|
||||
|
||||
参见 `docs` 目录中的 [CONTRIBUTING.md](docs/zh-CN/CONTRIBUTING.md) 了解如何参与项目。
|
||||
参见 `docs` 目录中的 [CONTRIBUTING.md](docs/zh-Hans/CONTRIBUTING.md) 了解如何参与项目。
|
||||
|
||||
## 许可
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ homepage
|
|||
|
||||
## 貢獻指南
|
||||
|
||||
參見 `docs` 目錄中的 [CONTRIBUTING.md](docs/zh-TW/CONTRIBUTING.md) 了解如何參與專案。
|
||||
參見 `docs` 目錄中的 [CONTRIBUTING.md](docs/zh-Hant/CONTRIBUTING.md) 了解如何參與專案。
|
||||
|
||||
## 許可
|
||||
|
||||
|
|
|
@ -17,9 +17,7 @@
|
|||
"meta": {
|
||||
"title": "STARSET Mirror",
|
||||
"description": "STARSET Mirror, connecting STARSET and you."
|
||||
},
|
||||
"latestUpdates": "Latest Updates",
|
||||
"featuredProjects": "Featured Projects"
|
||||
}
|
||||
},
|
||||
"projects": {
|
||||
"meta": {
|
||||
|
@ -49,7 +47,6 @@
|
|||
"loading": "Loading...",
|
||||
"back_to_list": "Back to Updates",
|
||||
"filter": {
|
||||
"all": "All Updates",
|
||||
"title": "Filter by Tags",
|
||||
"search_placeholder": "Search tags...",
|
||||
"no_results": "No matching tags found",
|
||||
|
@ -76,7 +73,6 @@
|
|||
"title": "Contributors - STARSET Mirror",
|
||||
"description": "Meet the amazing people behind STARSET Mirror. Our contributors work tirelessly to bring STARSET closer to you."
|
||||
},
|
||||
"title": "Contributors",
|
||||
"tabs": {
|
||||
"contributors": "Contributors",
|
||||
"sponsors": "Sponsors"
|
||||
|
@ -99,16 +95,17 @@
|
|||
},
|
||||
"aria": {
|
||||
"mainContent": "Main content",
|
||||
"breadcrumb": "Page navigation",
|
||||
"navigation": "Site navigation",
|
||||
"menu": "Menu",
|
||||
"navigation": "Navigation",
|
||||
"search": "Search",
|
||||
"darkMode": "Dark mode",
|
||||
"language": "Language selection",
|
||||
"loading": "Loading",
|
||||
"error": "Error",
|
||||
"language": "Change language",
|
||||
"darkMode": "Toggle dark mode",
|
||||
"openMenu": "Open menu",
|
||||
"closeMenu": "Close menu",
|
||||
"openMenu": "Open menu"
|
||||
"error": "Error",
|
||||
"loading": "Loading",
|
||||
"comments": "Comments section",
|
||||
"breadcrumb": "Breadcrumb"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Connecting STARSET and You",
|
||||
|
@ -174,9 +171,7 @@
|
|||
},
|
||||
"common": {
|
||||
"loading": "Loading...",
|
||||
"error": "An error occurred",
|
||||
"retry": "Retry",
|
||||
"close": "Close"
|
||||
"error": "An error occurred"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,22 +17,21 @@
|
|||
"meta": {
|
||||
"title": "STARSET Mirror",
|
||||
"description": "STARSET Mirror,连接星落与你"
|
||||
},
|
||||
"latestUpdates": "最新动态",
|
||||
"featuredProjects": "精选项目"
|
||||
}
|
||||
},
|
||||
"aria": {
|
||||
"mainContent": "主要内容",
|
||||
"breadcrumb": "页面导航",
|
||||
"navigation": "网站导航",
|
||||
"menu": "菜单",
|
||||
"navigation": "导航",
|
||||
"search": "搜索",
|
||||
"darkMode": "深色模式",
|
||||
"language": "语言选择",
|
||||
"loading": "加载中",
|
||||
"error": "错误",
|
||||
"language": "切换语言",
|
||||
"darkMode": "切换深色模式",
|
||||
"openMenu": "打开菜单",
|
||||
"closeMenu": "关闭菜单",
|
||||
"openMenu": "打开菜单"
|
||||
"error": "错误",
|
||||
"loading": "加载中",
|
||||
"comments": "评论区",
|
||||
"breadcrumb": "面包屑导航"
|
||||
},
|
||||
"hero": {
|
||||
"title": "连接星落与你",
|
||||
|
@ -70,7 +69,6 @@
|
|||
"loading": "正在加载...",
|
||||
"back_to_list": "返回动态列表",
|
||||
"filter": {
|
||||
"all": "全部动态",
|
||||
"title": "按标签筛选",
|
||||
"search_placeholder": "搜索标签...",
|
||||
"no_results": "未找到匹配的标签",
|
||||
|
@ -97,7 +95,6 @@
|
|||
"title": "贡献者 - STARSET Mirror",
|
||||
"description": "认识 STARSET Mirror 背后的优秀贡献者们。他们不懈努力,让 STARSET 与你更近。"
|
||||
},
|
||||
"title": "贡献者",
|
||||
"tabs": {
|
||||
"contributors": "贡献者",
|
||||
"sponsors": "赞助人"
|
||||
|
@ -168,9 +165,7 @@
|
|||
},
|
||||
"common": {
|
||||
"loading": "加载中...",
|
||||
"error": "发生错误",
|
||||
"retry": "重试",
|
||||
"close": "关闭"
|
||||
"error": "发生错误"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,22 +17,21 @@
|
|||
"meta": {
|
||||
"title": "STARSET Mirror",
|
||||
"description": "STARSET Mirror,連結星落與你。"
|
||||
},
|
||||
"latestUpdates": "最新動態",
|
||||
"featuredProjects": "查看專案"
|
||||
}
|
||||
},
|
||||
"aria": {
|
||||
"mainContent": "主要內容",
|
||||
"breadcrumb": "頁面導航",
|
||||
"navigation": "網站導航",
|
||||
"menu": "選單",
|
||||
"navigation": "導航",
|
||||
"search": "搜尋",
|
||||
"darkMode": "深色模式",
|
||||
"language": "語言選擇",
|
||||
"loading": "載入中",
|
||||
"error": "錯誤",
|
||||
"language": "切換語言",
|
||||
"darkMode": "切換深色模式",
|
||||
"openMenu": "打開選單",
|
||||
"closeMenu": "關閉選單",
|
||||
"openMenu": "開啟選單"
|
||||
"error": "錯誤",
|
||||
"loading": "載入中",
|
||||
"comments": "評論區",
|
||||
"breadcrumb": "麵包屑導航"
|
||||
},
|
||||
"hero": {
|
||||
"title": "連接星落與你",
|
||||
|
@ -43,6 +42,10 @@
|
|||
}
|
||||
},
|
||||
"projects": {
|
||||
"meta": {
|
||||
"title": "專案 - STARSET Mirror",
|
||||
"description": "探索我們為 STARSET 和社群打造的專案。從翻譯到社群服務,了解我們如何為社群貢獻力量。"
|
||||
},
|
||||
"title": "專案",
|
||||
"tags": {
|
||||
"translation": "翻譯",
|
||||
|
@ -58,11 +61,14 @@
|
|||
}
|
||||
},
|
||||
"updates": {
|
||||
"meta": {
|
||||
"title": "動態 - STARSET Mirror",
|
||||
"description": "了解 STARSET Mirror 的最新動態、更新和活動。跟隨我們支持 STARSET 社群的脚步。"
|
||||
},
|
||||
"title": "專案動態",
|
||||
"loading": "正在載入...",
|
||||
"back_to_list": "返回動態列表",
|
||||
"filter": {
|
||||
"all": "全部動態",
|
||||
"title": "按標籤篩選",
|
||||
"search_placeholder": "搜尋標籤...",
|
||||
"no_results": "未找到匹配的標籤",
|
||||
|
@ -85,7 +91,10 @@
|
|||
}
|
||||
},
|
||||
"contributors": {
|
||||
"title": "貢獻者",
|
||||
"meta": {
|
||||
"title": "貢獻者 - STARSET Mirror",
|
||||
"description": "了解 STARSET Mirror 的核心貢獻者和贊助人。"
|
||||
},
|
||||
"tabs": {
|
||||
"contributors": "貢獻者",
|
||||
"sponsors": "贊助人"
|
||||
|
@ -94,6 +103,10 @@
|
|||
"regular_members": "專案成員 & 社群貢獻者"
|
||||
},
|
||||
"about": {
|
||||
"meta": {
|
||||
"title": "關於 - STARSET Mirror",
|
||||
"description": "了解 STARSET Mirror 的使命、價值觀,以及我们致力于连接 STARSET 与全球粉丝的愿景。"
|
||||
},
|
||||
"title": "關於我們",
|
||||
"contact": {
|
||||
"title": "聯絡方式"
|
||||
|
@ -158,9 +171,7 @@
|
|||
},
|
||||
"common": {
|
||||
"loading": "載入中...",
|
||||
"error": "發生錯誤",
|
||||
"retry": "重試",
|
||||
"close": "關閉"
|
||||
"error": "發生錯誤"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
- [简体中文](./zh-CN/)
|
||||
- [繁體中文](./zh-TW/)
|
||||
- [English](./en-US/)
|
||||
- [简体中文](./zh-Hans/)
|
||||
- [繁體中文](./zh-Hant/)
|
||||
- [English](./en/)
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build && tsx scripts/generate-rss.ts && tsx scripts/generate-sitemap.ts",
|
||||
"build": "tsx scripts/check-translations.ts && vite build && tsx scripts/generate-rss.ts && tsx scripts/generate-sitemap.ts",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"check-translations": "tsx scripts/check-translations.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.6.0",
|
||||
|
@ -36,6 +37,7 @@
|
|||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"chalk": "^5.4.1",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.18",
|
||||
|
|
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
|
@ -81,6 +81,9 @@ importers:
|
|||
autoprefixer:
|
||||
specifier: ^10.4.20
|
||||
version: 10.4.20(postcss@8.5.1)
|
||||
chalk:
|
||||
specifier: ^5.4.1
|
||||
version: 5.4.1
|
||||
eslint:
|
||||
specifier: ^9.19.0
|
||||
version: 9.19.0(jiti@1.21.7)
|
||||
|
@ -989,6 +992,10 @@ packages:
|
|||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chalk@5.4.1:
|
||||
resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
|
||||
chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
@ -2942,6 +2949,8 @@ snapshots:
|
|||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
chalk@5.4.1: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
dependencies:
|
||||
anymatch: 3.1.3
|
||||
|
|
183
scripts/check-translations.ts
Normal file
183
scripts/check-translations.ts
Normal file
|
@ -0,0 +1,183 @@
|
|||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import * as globModule from 'glob';
|
||||
const glob = globModule.glob;
|
||||
import chalk from 'chalk';
|
||||
|
||||
interface TranslationMap {
|
||||
[key: string]: string | TranslationMap;
|
||||
}
|
||||
|
||||
interface TranslationResult {
|
||||
missingKeys: string[];
|
||||
unusedKeys: string[];
|
||||
}
|
||||
|
||||
// 递归获取所有翻译键
|
||||
function getAllTranslationKeys(obj: TranslationMap, prefix = ''): string[] {
|
||||
return Object.entries(obj).reduce<string[]>((acc, [key, value]) => {
|
||||
const currentKey = prefix ? `${prefix}.${key}` : key;
|
||||
if (typeof value === 'object') {
|
||||
return [...acc, ...getAllTranslationKeys(value, currentKey)];
|
||||
}
|
||||
// 如果键以 translation. 开头,移除这个前缀
|
||||
const finalKey = currentKey.startsWith('translation.')
|
||||
? currentKey.substring('translation.'.length)
|
||||
: currentKey;
|
||||
if (!shouldIgnoreKey(finalKey)) {
|
||||
return [...acc, finalKey];
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
// 从源代码中提取翻译键的正则表达式
|
||||
const translationKeyRegexes = [
|
||||
/(?:t|i18n\.t)\(['"]([a-zA-Z][a-zA-Z0-9._-]+)['"](?:\s*,\s*{[^}]*})?\)/gi, // t('key') or t('key', {})
|
||||
/(?:t|i18n\.t)\(\s*(?:[a-zA-Z][a-zA-Z0-9._]+\s*\?\s*)?['"]([a-zA-Z][a-zA-Z0-9._-]+)['"](?:\s*:\s*['"]([a-zA-Z][a-zA-Z0-9._-]+)['"])?(?:\s*,\s*{[^}]*})?\)/gi, // t(isOpen ? 'key1' : 'key2') or t('key')
|
||||
/useTranslation\(['"]([a-zA-Z][a-zA-Z0-9._-]+)['"]\)/gi, // useTranslation('key')
|
||||
/<Trans[^>]*i18nKey=["']([a-zA-Z][a-zA-Z0-9._-]+)["']/gi, // <Trans i18nKey="key" />
|
||||
];
|
||||
|
||||
// 从文件内容中提取翻译键
|
||||
function extractTranslationKeys(content: string): Set<string> {
|
||||
const keys = new Set<string>();
|
||||
for (const regex of translationKeyRegexes) {
|
||||
let match;
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
// 如果有多个捕获组(三元运算符的情况),添加所有捕获的键
|
||||
for (let i = 1; i < match.length; i++) {
|
||||
const key = match[i];
|
||||
if (key && !shouldIgnoreKey(key)) {
|
||||
keys.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
// 忽略的翻译键
|
||||
const ignoredKeys = new Set([
|
||||
'content-type',
|
||||
'page',
|
||||
'tags'
|
||||
]);
|
||||
|
||||
// 忽略的翻译键前缀
|
||||
const ignoredPrefixes = [
|
||||
'projects.status.',
|
||||
'projects.tags.',
|
||||
'social.links',
|
||||
'updates.tags',
|
||||
'data.'
|
||||
];
|
||||
|
||||
// 检查键是否应该被忽略
|
||||
function shouldIgnoreKey(key: string): boolean {
|
||||
if (ignoredKeys.has(key)) return true;
|
||||
return ignoredPrefixes.some(prefix => key.startsWith(prefix));
|
||||
}
|
||||
|
||||
// 比较翻译键
|
||||
function compareTranslations(usedKeys: Set<string>, availableKeys: Set<string>): TranslationResult {
|
||||
const missingKeys = Array.from(usedKeys)
|
||||
.filter(key => !availableKeys.has(key))
|
||||
.sort();
|
||||
|
||||
const unusedKeys = Array.from(availableKeys)
|
||||
.filter(key => !usedKeys.has(key))
|
||||
.sort();
|
||||
|
||||
return { missingKeys, unusedKeys };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(chalk.blue('Checking translations...'));
|
||||
// 获取所有支持的语言
|
||||
const dataDir = path.resolve('data');
|
||||
const langs = fs.readdirSync(dataDir)
|
||||
.filter(file => fs.statSync(path.join(dataDir, file)).isDirectory())
|
||||
.filter(dir => /^[a-z]{2}-[A-Z]{2}$/.test(dir));
|
||||
|
||||
// 扫描源代码文件
|
||||
const srcFiles = await glob('src/**/*.{ts,tsx,js,jsx}', { absolute: true });
|
||||
const usedKeys = new Set<string>();
|
||||
|
||||
// 从所有源代码文件中提取翻译键
|
||||
for (const file of srcFiles) {
|
||||
const content = await fs.readFile(file, 'utf-8');
|
||||
const keys = extractTranslationKeys(content);
|
||||
keys.forEach(key => usedKeys.add(key));
|
||||
}
|
||||
|
||||
// 检查每个语言的翻译
|
||||
let hasWarnings = false;
|
||||
for (const lang of langs) {
|
||||
const indexPath = path.join(dataDir, lang, 'index.json');
|
||||
if (fs.existsSync(indexPath)) {
|
||||
const content = await fs.readJson(indexPath);
|
||||
// 确保我们从 translations 对象开始
|
||||
const translationsObj = content.translations || content;
|
||||
const availableKeys = new Set(getAllTranslationKeys(translationsObj));
|
||||
|
||||
const { missingKeys, unusedKeys } = compareTranslations(usedKeys, availableKeys);
|
||||
|
||||
if (missingKeys.length > 0 || unusedKeys.length > 0) {
|
||||
hasWarnings = true;
|
||||
console.log(chalk.yellow(`\n[${lang}]`));
|
||||
|
||||
if (missingKeys.length > 0) {
|
||||
console.log(chalk.yellow(` Missing translations: (${missingKeys.length} keys)`));
|
||||
// 按命名空间分组输出
|
||||
const groupedKeys = missingKeys.reduce((acc, key) => {
|
||||
const namespace = key.split('.')[0];
|
||||
if (!acc[namespace]) acc[namespace] = [];
|
||||
acc[namespace].push(key);
|
||||
return acc;
|
||||
}, {} as Record<string, string[]>);
|
||||
|
||||
Object.entries(groupedKeys)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.forEach(([namespace, keys]) => {
|
||||
console.log(chalk.cyan(`\n ${namespace}:`));
|
||||
keys.forEach(key => {
|
||||
console.log(chalk.red(` ${key}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (unusedKeys.length > 0) {
|
||||
console.log(chalk.yellow(`\n Unused translations: (${unusedKeys.length} keys)`));
|
||||
// 按命名空间分组输出
|
||||
const groupedKeys = unusedKeys.reduce((acc, key) => {
|
||||
const namespace = key.split('.')[0];
|
||||
if (!acc[namespace]) acc[namespace] = [];
|
||||
acc[namespace].push(key);
|
||||
return acc;
|
||||
}, {} as Record<string, string[]>);
|
||||
|
||||
Object.entries(groupedKeys)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.forEach(([namespace, keys]) => {
|
||||
console.log(chalk.cyan(`\n ${namespace}:`));
|
||||
keys.forEach(key => {
|
||||
console.log(chalk.yellow(` ${key}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasWarnings) {
|
||||
console.log(chalk.green('\n✓ All translations are complete and clean!'));
|
||||
} else {
|
||||
console.log(chalk.yellow('\n⚠️ Please review the translation issues listed above.'));
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(chalk.red('Error:'), error);
|
||||
process.exit(1);
|
||||
});
|
|
@ -14,22 +14,22 @@ interface LanguageConfig {
|
|||
|
||||
const LANGUAGES: LanguageConfig[] = [
|
||||
{
|
||||
code: 'en-US',
|
||||
dataDir: 'en-US',
|
||||
title: 'STARSET Mirror Site Updates',
|
||||
description: 'Latest updates from STARSET Mirror Site'
|
||||
code: 'en',
|
||||
dataDir: 'en',
|
||||
title: 'Starset Mirror - Updates',
|
||||
description: 'Latest updates from Starset Mirror'
|
||||
},
|
||||
{
|
||||
code: 'zh-CN',
|
||||
dataDir: 'zh-CN',
|
||||
title: 'STARSET Mirror 项目动态',
|
||||
description: 'STARSET Mirror 最新动态'
|
||||
code: 'zh-Hans',
|
||||
dataDir: 'zh-Hans',
|
||||
title: 'Starset Mirror - 更新',
|
||||
description: 'Starset Mirror 的最新更新'
|
||||
},
|
||||
{
|
||||
code: 'zh-Hant',
|
||||
dataDir: 'zh-TW',
|
||||
title: 'STARSET Mirror 專案動态',
|
||||
description: 'STARSET Mirror 最新動態'
|
||||
dataDir: 'zh-Hant',
|
||||
title: 'Starset Mirror - 更新',
|
||||
description: 'Starset Mirror 的最新更新'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import xml2js from 'xml2js';
|
|||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const LANGUAGES = ['en-US', 'zh-CN', 'zh-TW'];
|
||||
const LANGUAGES = ['en', 'zh-Hans', 'zh-Hant'];
|
||||
const BASE_URL = 'mirror.starset.fans';
|
||||
|
||||
interface Update {
|
||||
|
|
|
@ -6,7 +6,7 @@ import xml2js from 'xml2js';
|
|||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const LANGUAGES = ['en-US', 'zh-CN', 'zh-TW'];
|
||||
const LANGUAGES = ['en', 'zh-Hans', 'zh-Hant'];
|
||||
const BASE_URL = 'starset.wiki'; // Replace with your actual domain
|
||||
|
||||
async function getYearlyIndices(lang) {
|
||||
|
|
|
@ -4,22 +4,24 @@ import iconMap from '../utils/iconMap';
|
|||
|
||||
const About = () => {
|
||||
const { t } = useTranslation();
|
||||
const aboutData = t('data.about', { returnObjects: true });
|
||||
const socialLinks = t('social.links', { returnObjects: true });
|
||||
const aboutData = t('data.about', { returnObjects: true }) || {};
|
||||
const socialLinks = t('social.links', { returnObjects: true }) || [];
|
||||
|
||||
if (!aboutData) return null;
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-2 md:px-4">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-center mb-8 text-gray-900 dark:text-white">{t('about.title')}</h2>
|
||||
<div className="prose prose-lg mx-auto dark:prose-invert">
|
||||
{aboutData.content.intro.map((paragraph: string, index: number) => (
|
||||
{aboutData.content?.intro?.map((paragraph: string, index: number) => (
|
||||
<p key={index} className="text-gray-600 dark:text-gray-300 mb-6">
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mt-12">
|
||||
{aboutData.stats.map((stat: { value: string; label: string }, index: number) => (
|
||||
{aboutData.stats?.map((stat: { value: string; label: string }, index: number) => (
|
||||
<div key={index} className="text-center">
|
||||
<h3 className="text-4xl font-bold text-blue-600 dark:text-blue-400 mb-2">{stat.value}</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300">{stat.label}</p>
|
||||
|
@ -27,7 +29,7 @@ const About = () => {
|
|||
))}
|
||||
</div>
|
||||
|
||||
{aboutData.content.workScope && (
|
||||
{aboutData.content?.workScope && (
|
||||
<div className="mt-12">
|
||||
<h3 className="text-2xl font-semibold mb-6 text-gray-900 dark:text-white">工作范围</h3>
|
||||
<ol className="space-y-4">
|
||||
|
@ -36,7 +38,7 @@ const About = () => {
|
|||
<span className="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400 font-semibold">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="text-lg">{item}</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
@ -67,14 +69,14 @@ const About = () => {
|
|||
<div className="mt-12">
|
||||
<h3 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-white">{t('about.join.title')}</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
{aboutData.content.contact.description}
|
||||
{aboutData.content?.contact?.description}
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<a
|
||||
href={`mailto:${aboutData.content.contact.email}`}
|
||||
href={`mailto:${aboutData.content?.contact?.email}`}
|
||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 no-underline"
|
||||
>
|
||||
{aboutData.content.contact.email}
|
||||
{aboutData.content?.contact?.email}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useCallback, useRef, useEffect } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import 'artalk/dist/Artalk.css'
|
||||
import Artalk from 'artalk'
|
||||
|
||||
|
@ -9,6 +10,7 @@ interface CommentsProps {
|
|||
|
||||
const Comments = ({ title }: CommentsProps) => {
|
||||
const { pathname } = useLocation()
|
||||
const { t } = useTranslation()
|
||||
const artalkRef = useRef<Artalk>()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
|
@ -59,10 +61,10 @@ const Comments = ({ title }: CommentsProps) => {
|
|||
}, [pathname, title])
|
||||
|
||||
return (
|
||||
<div
|
||||
<section
|
||||
ref={containerRef}
|
||||
className="mt-12 p-4 bg-white dark:bg-gray-800 rounded-lg shadow"
|
||||
aria-label="Comments section"
|
||||
aria-label={t('aria.comments')}
|
||||
role="complementary"
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -23,7 +23,8 @@ interface Contributor {
|
|||
|
||||
const Contributors: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const members = t('data.contributors.members', { returnObjects: true }) as Contributor[];
|
||||
const contributorsData = t('data.contributors', { returnObjects: true }) || {};
|
||||
const members = Array.isArray(contributorsData.members) ? contributorsData.members : [];
|
||||
|
||||
// Fisher-Yates shuffle algorithm
|
||||
const shuffleArray = <T,>(array: T[]): T[] => {
|
||||
|
|
|
@ -3,14 +3,14 @@ import { useTranslation } from 'react-i18next';
|
|||
import { Globe } from 'lucide-react';
|
||||
|
||||
const LanguageSwitcher = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const { i18n, t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const languages = [
|
||||
{ code: 'zh-CN', name: '简体中文' },
|
||||
{ code: 'zh-TW', name: '繁體中文' },
|
||||
{ code: 'en-US', name: 'English' }
|
||||
{ code: 'zh-Hans', name: '简体中文' },
|
||||
{ code: 'zh-Hant', name: '繁體中文' },
|
||||
{ code: 'en', name: 'English' }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -32,9 +32,12 @@ const LanguageSwitcher = () => {
|
|||
return (
|
||||
<div className="relative" ref={menuRef}>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
type="button"
|
||||
className="p-1.5 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
aria-label="Change language"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
aria-label={t('aria.language')}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<Globe className="w-5 h-5" />
|
||||
</button>
|
||||
|
|
|
@ -5,8 +5,12 @@ const LoadingSpinner: React.FC = () => {
|
|||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[200px]" role="status">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent" />
|
||||
<div
|
||||
className="flex items-center justify-center min-h-[200px]"
|
||||
role="status"
|
||||
aria-label={t('aria.loading')}
|
||||
>
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-4 border-primary dark:border-primary border-t-transparent dark:border-t-transparent" />
|
||||
<span className="sr-only">{t('common.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -34,7 +34,11 @@ const Navbar = () => {
|
|||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex items-center">
|
||||
<nav
|
||||
className="hidden md:flex items-center"
|
||||
role="navigation"
|
||||
aria-label={t('aria.navigation')}
|
||||
>
|
||||
<div className="flex items-center space-x-6">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
|
@ -54,7 +58,7 @@ const Navbar = () => {
|
|||
<ThemeSwitcher />
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Mobile Navigation Button */}
|
||||
<div className="md:hidden flex items-center space-x-1.5">
|
||||
|
@ -71,7 +75,11 @@ const Navbar = () => {
|
|||
|
||||
{/* Mobile Navigation Menu */}
|
||||
{isOpen && (
|
||||
<div className="md:hidden">
|
||||
<div
|
||||
className="md:hidden"
|
||||
role="menu"
|
||||
aria-label={t('aria.menu')}
|
||||
>
|
||||
<div className="px-2 pt-2 pb-3 space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
|
|
|
@ -13,18 +13,19 @@ interface Project {
|
|||
|
||||
const Projects = () => {
|
||||
const { t } = useTranslation();
|
||||
const projects = t('data.projects.projects', { returnObjects: true }) as Project[];
|
||||
const projectsData = t('data.projects', { returnObjects: true }) || {};
|
||||
const projects = Array.isArray(projectsData.projects) ? projectsData.projects : [];
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
|
||||
// 获取所有可用的标签
|
||||
const allTags = Array.from(
|
||||
new Set(projects.flatMap(project => project.tags))
|
||||
new Set(projects.flatMap(project => project.tags || []))
|
||||
);
|
||||
|
||||
// 根据选中的标签筛选项目
|
||||
const filteredProjects = selectedTags.length > 0
|
||||
? projects.filter(project =>
|
||||
selectedTags.every(tag => project.tags.includes(tag))
|
||||
selectedTags.every(tag => (project.tags || []).includes(tag))
|
||||
)
|
||||
: projects;
|
||||
|
||||
|
@ -119,7 +120,7 @@ const Projects = () => {
|
|||
{/* 标签区域固定高度 */}
|
||||
<div className="h-8 mb-4 flex items-center overflow-x-auto">
|
||||
<div className="flex gap-2">
|
||||
{project.tags.map((tag) => (
|
||||
{(project.tags || []).map((tag) => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={() => handleTagClick(tag)}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Card, Avatar, Tag } from 'antd';
|
||||
import { GithubOutlined, TwitterOutlined } from '@ant-design/icons';
|
||||
import sponsorsData from '../../data/zh-CN/sponsors.json';
|
||||
import sponsorsData from '../../data/zh-Hans/sponsors.json';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface SponsorType {
|
||||
|
@ -13,26 +13,33 @@ interface SponsorType {
|
|||
github?: string;
|
||||
twitter?: string;
|
||||
};
|
||||
name: string;
|
||||
name_zh_TW?: string;
|
||||
name_en?: string;
|
||||
}
|
||||
|
||||
const Sponsors: React.FC = () => {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const getMessage = () => {
|
||||
const getMessage = (sponsor: SponsorType) => {
|
||||
switch (i18n.language) {
|
||||
case 'zh-Hans':
|
||||
case 'zh-CN':
|
||||
return '赞助人信息仍在同步,将在晚些时候上线。';
|
||||
return sponsor.name;
|
||||
case 'zh-Hant':
|
||||
case 'zh-TW':
|
||||
return '贊助人資訊仍在同步,將在晚些時候上線。';
|
||||
return sponsor.name_zh_TW || sponsor.name;
|
||||
case 'en':
|
||||
case 'en-US':
|
||||
return sponsor.name_en || sponsor.name;
|
||||
default:
|
||||
return 'Sponsor information is still being synchronized and will be available later.';
|
||||
return sponsor.name;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[300px] text-gray-600 dark:text-gray-300 text-lg">
|
||||
{getMessage()}
|
||||
{getMessage(sponsorsData[0])}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@ -51,7 +58,7 @@ const Sponsors: React.FC = () => {
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
<h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-white">{sponsor.nickname}</h3>
|
||||
<h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-white">{getMessage(sponsor)}</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-center gap-3">
|
||||
<Tag color="gold">{sponsor.year}</Tag>
|
||||
|
|
|
@ -23,7 +23,16 @@ const ThemeSwitcher = () => {
|
|||
items={themes}
|
||||
value={theme}
|
||||
onChange={setTheme}
|
||||
/>
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
aria-label={t('aria.darkMode')}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
{currentIcon}
|
||||
</button>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ const Pagination: React.FC<PaginationProps> = ({
|
|||
const Timeline = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const LANGUAGE_CODE_MAP: Record<string, string> = {
|
||||
'zh-CN': 'zh-Hans',
|
||||
'zh-TW': 'zh-Hant'
|
||||
};
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
@ -112,8 +113,12 @@ const Timeline = () => {
|
|||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-red-500">{t('updates.error')}</p>
|
||||
<div
|
||||
className="text-center py-8"
|
||||
role="alert"
|
||||
aria-label={t('aria.error')}
|
||||
>
|
||||
<p className="text-red-500">{t('common.error')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -152,7 +157,7 @@ const Timeline = () => {
|
|||
|
||||
{isLoading ? (
|
||||
<div className="text-center py-20">
|
||||
<p>{t('updates.loading')}</p>
|
||||
<p className="text-gray-900 dark:text-gray-100">{t('updates.loading')}</p>
|
||||
</div>
|
||||
) : updates.length > 0 ? (
|
||||
<>
|
||||
|
@ -211,7 +216,7 @@ const Timeline = () => {
|
|||
</>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
<p>{t('updates.no_results')}</p>
|
||||
<p>{t('updates.notFound.title')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -36,10 +36,33 @@ const TranslationBackground = () => {
|
|||
setIsVisible(true);
|
||||
}, []);
|
||||
|
||||
const getLanguageName = (language: string) => {
|
||||
switch (language) {
|
||||
case 'zh-Hans':
|
||||
return '简体中文';
|
||||
case 'zh-Hant':
|
||||
return '繁體中文';
|
||||
default:
|
||||
return 'English';
|
||||
}
|
||||
};
|
||||
|
||||
const getTranslationName = (language: string) => {
|
||||
switch (language) {
|
||||
case 'zh-Hans':
|
||||
case 'zh-Hant':
|
||||
return '翻译';
|
||||
default:
|
||||
return 'Translation';
|
||||
}
|
||||
};
|
||||
|
||||
const getMainText = (translation: typeof translations.translations[0]) => {
|
||||
switch (i18n.language) {
|
||||
case 'zh-Hans':
|
||||
case 'zh-CN':
|
||||
return translation.zh_CN;
|
||||
case 'zh-Hant':
|
||||
case 'zh-TW':
|
||||
return translation.zh_TW;
|
||||
default:
|
||||
|
@ -49,7 +72,9 @@ const TranslationBackground = () => {
|
|||
|
||||
const getSecondaryText = (translation: typeof translations.translations[0]) => {
|
||||
switch (i18n.language) {
|
||||
case 'zh-Hans':
|
||||
case 'zh-CN':
|
||||
case 'zh-Hant':
|
||||
case 'zh-TW':
|
||||
return translation.en;
|
||||
default:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface DropdownProps {
|
||||
icon: React.ReactNode;
|
||||
|
@ -16,6 +17,7 @@ const Dropdown = ({ icon, items, value, onChange, position = 'right' }: Dropdown
|
|||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
|
@ -31,9 +33,12 @@ const Dropdown = ({ icon, items, value, onChange, position = 'right' }: Dropdown
|
|||
return (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="p-1.5 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
aria-label="Toggle dropdown"
|
||||
aria-label={t(isOpen ? 'aria.closeMenu' : 'aria.openMenu')}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
|
|
|
@ -104,10 +104,11 @@ const TagFilter: React.FC<TagFilterProps> = ({
|
|||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="pl-10 pr-4 py-2 w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder={t('updates.filter.search_placeholder')}
|
||||
className="w-full px-4 py-2.5 pl-10 text-sm rounded-lg bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
aria-label={t('aria.search')}
|
||||
/>
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
|
|
|
@ -3,42 +3,42 @@ import { initReactI18next } from 'react-i18next';
|
|||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
// 导入翻译文件
|
||||
import zhCNTranslation from '../../data/zh-CN/index.json';
|
||||
import zhTWTranslation from '../../data/zh-TW/index.json';
|
||||
import enUSTranslation from '../../data/en-US/index.json';
|
||||
import zhHansTranslation from '../../data/zh-Hans/index.json';
|
||||
import zhHantTranslation from '../../data/zh-Hant/index.json';
|
||||
import enTranslation from '../../data/en/index.json';
|
||||
|
||||
// 导入数据文件
|
||||
import zhCNAbout from '../../data/zh-CN/about.json';
|
||||
import zhCNProjects from '../../data/zh-CN/projects.json';
|
||||
import zhCNContributors from '../../data/zh-CN/contributors.json';
|
||||
import zhCNUpdates from '../../data/zh-CN/updates.json';
|
||||
import zhHansAbout from '../../data/zh-Hans/about.json';
|
||||
import zhHansProjects from '../../data/zh-Hans/projects.json';
|
||||
import zhHansContributors from '../../data/zh-Hans/contributors.json';
|
||||
import zhHansUpdates from '../../data/zh-Hans/updates.json';
|
||||
|
||||
import zhTWAbout from '../../data/zh-TW/about.json';
|
||||
import zhTWProjects from '../../data/zh-TW/projects.json';
|
||||
import zhTWContributors from '../../data/zh-TW/contributors.json';
|
||||
import zhTWUpdates from '../../data/zh-TW/updates.json';
|
||||
import zhHantAbout from '../../data/zh-Hant/about.json';
|
||||
import zhHantProjects from '../../data/zh-Hant/projects.json';
|
||||
import zhHantContributors from '../../data/zh-Hant/contributors.json';
|
||||
import zhHantUpdates from '../../data/zh-Hant/updates.json';
|
||||
|
||||
import enUSAbout from '../../data/en-US/about.json';
|
||||
import enUSProjects from '../../data/en-US/projects.json';
|
||||
import enUSContributors from '../../data/en-US/contributors.json';
|
||||
import enUSUpdates from '../../data/en-US/updates.json';
|
||||
import enAbout from '../../data/en/about.json';
|
||||
import enProjects from '../../data/en/projects.json';
|
||||
import enContributors from '../../data/en/contributors.json';
|
||||
import enUpdates from '../../data/en/updates.json';
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
'zh-CN': {
|
||||
translation: zhCNTranslation.translation
|
||||
'zh-Hans': {
|
||||
translation: zhHansTranslation.translation
|
||||
},
|
||||
'zh-TW': {
|
||||
translation: zhTWTranslation.translation
|
||||
'zh-Hant': {
|
||||
translation: zhHantTranslation.translation
|
||||
},
|
||||
'en-US': {
|
||||
translation: enUSTranslation.translation
|
||||
'en': {
|
||||
translation: enTranslation.translation
|
||||
}
|
||||
},
|
||||
fallbackLng: 'zh-CN',
|
||||
fallbackLng: 'zh-Hans',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
@ -48,30 +48,30 @@ i18n
|
|||
});
|
||||
|
||||
// 添加数据命名空间
|
||||
i18n.addResourceBundle('zh-CN', 'translation', {
|
||||
i18n.addResourceBundle('zh-Hans', 'translation', {
|
||||
data: {
|
||||
about: zhCNAbout,
|
||||
projects: zhCNProjects,
|
||||
contributors: zhCNContributors,
|
||||
updates: zhCNUpdates
|
||||
about: zhHansAbout,
|
||||
projects: zhHansProjects,
|
||||
contributors: zhHansContributors,
|
||||
updates: zhHansUpdates
|
||||
}
|
||||
}, true, true);
|
||||
|
||||
i18n.addResourceBundle('zh-TW', 'translation', {
|
||||
i18n.addResourceBundle('zh-Hant', 'translation', {
|
||||
data: {
|
||||
about: zhTWAbout,
|
||||
projects: zhTWProjects,
|
||||
contributors: zhTWContributors,
|
||||
updates: zhTWUpdates
|
||||
about: zhHantAbout,
|
||||
projects: zhHantProjects,
|
||||
contributors: zhHantContributors,
|
||||
updates: zhHantUpdates
|
||||
}
|
||||
}, true, true);
|
||||
|
||||
i18n.addResourceBundle('en-US', 'translation', {
|
||||
i18n.addResourceBundle('en', 'translation', {
|
||||
data: {
|
||||
about: enUSAbout,
|
||||
projects: enUSProjects,
|
||||
contributors: enUSContributors,
|
||||
updates: enUSUpdates
|
||||
about: enAbout,
|
||||
projects: enProjects,
|
||||
contributors: enContributors,
|
||||
updates: enUpdates
|
||||
}
|
||||
}, true, true);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue