aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--site/index.html1
-rw-r--r--site/package.json4
-rw-r--r--site/pnpm-lock.yaml121
-rw-r--r--site/public/locales/en/translation.json121
-rw-r--r--site/public/locales/ja/translation.json36
-rw-r--r--site/src/components/GameNotes.tsx142
-rw-r--r--site/src/components/LanguageSwitcher.tsx109
-rw-r--r--site/src/components/NewsFeed.tsx14
-rw-r--r--site/src/components/TitleBar.tsx10
-rw-r--r--site/src/i18n.ts30
-rw-r--r--site/src/main.tsx1
-rw-r--r--site/src/pages/GameSelector.tsx11
-rw-r--r--site/src/pages/Homepage.tsx21
-rw-r--r--site/src/pages/NotFound.tsx10
-rw-r--r--site/src/utils.ts13
16 files changed, 534 insertions, 115 deletions
diff --git a/.gitignore b/.gitignore
index 2dc61e1..0921af8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -176,4 +176,7 @@ wac_result_cache.json
summarization_cache.json
*.bak
*.db
-*.json
+summarization_cache.json
+wac_result_cache.json
+tl_cache.json
+key.json
diff --git a/site/index.html b/site/index.html
index ea2575e..5555eba 100644
--- a/site/index.html
+++ b/site/index.html
@@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/rasis.webp" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta name="language" content="en" />
<title>573 UPDATES</title>
<meta name="description" content="A scraper and aggregator of information/news for various arcade games. Currently supports various KONAMI/BEMANI and Performai ">
</head>
diff --git a/site/package.json b/site/package.json
index 86cc0ef..a980730 100644
--- a/site/package.json
+++ b/site/package.json
@@ -13,8 +13,12 @@
"@tailwindcss/vite": "^4.1.13",
"@vercel/analytics": "^1.5.0",
"firebase": "^12.3.0",
+ "i18next": "^25.5.3",
+ "i18next-browser-languagedetector": "^8.2.0",
+ "i18next-http-backend": "^3.0.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
+ "react-i18next": "^16.0.0",
"react-router-dom": "^7.9.1",
"reactjs-popup": "^2.0.6",
"tailwindcss": "^4.1.13"
diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml
index cb3d7c4..2b43ba7 100644
--- a/site/pnpm-lock.yaml
+++ b/site/pnpm-lock.yaml
@@ -17,12 +17,24 @@ importers:
firebase:
specifier: ^12.3.0
version: 12.3.0
+ i18next:
+ specifier: ^25.5.3
+ version: 25.5.3(typescript@5.9.2)
+ i18next-browser-languagedetector:
+ specifier: ^8.2.0
+ version: 8.2.0
+ i18next-http-backend:
+ specifier: ^3.0.2
+ version: 3.0.2
react:
specifier: ^19.1.1
version: 19.1.1
react-dom:
specifier: ^19.1.1
version: 19.1.1(react@19.1.1)
+ react-i18next:
+ specifier: ^16.0.0
+ version: 16.0.0(i18next@25.5.3(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2)
react-router-dom:
specifier: ^7.9.1
version: 7.9.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@@ -1657,6 +1669,9 @@ packages:
core-js-compat@3.45.1:
resolution: {integrity: sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==}
+ cross-fetch@4.0.0:
+ resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
+
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -1990,9 +2005,26 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
+ html-parse-stringify@3.0.1:
+ resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
+
http-parser-js@0.5.10:
resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==}
+ i18next-browser-languagedetector@8.2.0:
+ resolution: {integrity: sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==}
+
+ i18next-http-backend@3.0.2:
+ resolution: {integrity: sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==}
+
+ i18next@25.5.3:
+ resolution: {integrity: sha512-joFqorDeQ6YpIXni944upwnuHBf5IoPMuqAchGVeQLdWC2JOjxgM9V8UGLhNIIH/Q8QleRxIi0BSRQehSrDLcg==}
+ peerDependencies:
+ typescript: ^5
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
idb@7.1.1:
resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
@@ -2354,6 +2386,15 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+ node-fetch@2.7.0:
+ resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+ engines: {node: 4.x || >=6.0.0}
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+
node-releases@2.0.21:
resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==}
@@ -2457,6 +2498,22 @@ packages:
peerDependencies:
react: ^19.1.1
+ react-i18next@16.0.0:
+ resolution: {integrity: sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q==}
+ peerDependencies:
+ i18next: '>= 25.5.2'
+ react: '>= 16.8.0'
+ react-dom: '*'
+ react-native: '*'
+ typescript: ^5
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ typescript:
+ optional: true
+
react-router-dom@7.9.1:
resolution: {integrity: sha512-U9WBQssBE9B1vmRjo9qTM7YRzfZ3lUxESIZnsf4VjR/lXYz9MHjvOxHzr/aUm4efpktbVOrF09rL/y4VHa8RMw==}
engines: {node: '>=20.0.0'}
@@ -2717,6 +2774,9 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
+ tr46@0.0.3:
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
tr46@1.0.1:
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
@@ -2861,9 +2921,16 @@ packages:
yaml:
optional: true
+ void-elements@3.1.0:
+ resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
+ engines: {node: '>=0.10.0'}
+
web-vitals@4.2.4:
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
+ webidl-conversions@3.0.1:
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+
webidl-conversions@4.0.2:
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
@@ -2875,6 +2942,9 @@ packages:
resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==}
engines: {node: '>=0.8.0'}
+ whatwg-url@5.0.0:
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+
whatwg-url@7.1.0:
resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
@@ -4699,6 +4769,12 @@ snapshots:
dependencies:
browserslist: 4.26.2
+ cross-fetch@4.0.0:
+ dependencies:
+ node-fetch: 2.7.0
+ transitivePeerDependencies:
+ - encoding
+
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -5158,8 +5234,28 @@ snapshots:
dependencies:
function-bind: 1.1.2
+ html-parse-stringify@3.0.1:
+ dependencies:
+ void-elements: 3.1.0
+
http-parser-js@0.5.10: {}
+ i18next-browser-languagedetector@8.2.0:
+ dependencies:
+ '@babel/runtime': 7.28.4
+
+ i18next-http-backend@3.0.2:
+ dependencies:
+ cross-fetch: 4.0.0
+ transitivePeerDependencies:
+ - encoding
+
+ i18next@25.5.3(typescript@5.9.2):
+ dependencies:
+ '@babel/runtime': 7.28.4
+ optionalDependencies:
+ typescript: 5.9.2
+
idb@7.1.1: {}
ignore@5.3.2: {}
@@ -5464,6 +5560,10 @@ snapshots:
natural-compare@1.4.0: {}
+ node-fetch@2.7.0:
+ dependencies:
+ whatwg-url: 5.0.0
+
node-releases@2.0.21: {}
object-inspect@1.13.4: {}
@@ -5566,6 +5666,16 @@ snapshots:
react: 19.1.1
scheduler: 0.26.0
+ react-i18next@16.0.0(i18next@25.5.3(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2):
+ dependencies:
+ '@babel/runtime': 7.28.4
+ html-parse-stringify: 3.0.1
+ i18next: 25.5.3(typescript@5.9.2)
+ react: 19.1.1
+ optionalDependencies:
+ react-dom: 19.1.1(react@19.1.1)
+ typescript: 5.9.2
+
react-router-dom@7.9.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies:
react: 19.1.1
@@ -5891,6 +6001,8 @@ snapshots:
dependencies:
is-number: 7.0.0
+ tr46@0.0.3: {}
+
tr46@1.0.1:
dependencies:
punycode: 2.3.1
@@ -6017,8 +6129,12 @@ snapshots:
lightningcss: 1.30.1
terser: 5.44.0
+ void-elements@3.1.0: {}
+
web-vitals@4.2.4: {}
+ webidl-conversions@3.0.1: {}
+
webidl-conversions@4.0.2: {}
websocket-driver@0.7.4:
@@ -6029,6 +6145,11 @@ snapshots:
websocket-extensions@0.1.4: {}
+ whatwg-url@5.0.0:
+ dependencies:
+ tr46: 0.0.3
+ webidl-conversions: 3.0.1
+
whatwg-url@7.1.0:
dependencies:
lodash.sortby: 4.7.0
diff --git a/site/public/locales/en/translation.json b/site/public/locales/en/translation.json
new file mode 100644
index 0000000..9221da6
--- /dev/null
+++ b/site/public/locales/en/translation.json
@@ -0,0 +1,121 @@
+{
+ "homepage": {
+ "welcome": "Welcome to 573 Updates",
+ "news_aggregation_note": "News and Information for various arcade games is aggregated here!",
+ "rss_feeds": "RSS feeds are available on each game's individual page",
+ "github_info": "Please see the GitHub for API information"
+ },
+ "news_feed": "Main News Feed",
+ "game_selector": "Game Selector",
+ "loading": "Loading...",
+ "news_not_found": "News Not Found",
+ "return_home": "Return Home",
+ "dark_theme_text": "Dark",
+ "light_theme_text": "Light",
+ "view_in_english_text": "View in English",
+ "view_in_original_text": "View Original",
+ "machine_tl_note": "The information above is machine translated and may contain inaccuracies",
+ "ai_summary_note": "The information above is written by AI",
+ "read_more": "READ MORE",
+ "copy_link_to_post": "Copy Link to Post",
+ "copy_link_notif": "Copied Direct Link to Post (Older news are automatically culled after some time)",
+ "subscribed_to_games_count": "Subscribed to",
+ "games": "game(s)",
+ "gameselector": {
+ "title": "Select a Game",
+ "subtitle": "Individual game feeds keep a longer history of news relating to that game than the main feed.",
+ "categories": {
+ "konami": "KONAMI",
+ "sega": "SEGA",
+ "taito": "TAITO",
+ "bandai_namco": "BANDAI NAMCO",
+ "community": "COMMUNITY"
+ },
+ "community_description": "Community-driven projects to continue the legacy of dead/abandoned rhythm games"
+ },
+ "gamenotes": {
+ "common": {
+ "eamuse_maintenance": "e-amusement Maintenance",
+ "nesica_maintenance": "NESiCA Maintenance",
+ "aime_maintenance": "Aime Maintenance",
+ "allnet_warning": "Private Server Warning",
+ "na_service_note": "Official e-amusement service in NA available only at Round1 USA",
+ "private_network_note": "Online Cabinets in non-supported regions (CAN/EU/AUS) are on private networks which run older data",
+ "japan_only_note": "This version of the game is only available in Japan",
+ "international_note": "You may be on the International version if you are outside of Japan"
+ },
+ "sdvx": {
+ "premium_generator": "• [USA] PREMIUM GENERATOR gacha available only ONLINE (No PASELI)",
+ "voltefactory": "• VP/VOLTEFACTORY rewards only available in Japan",
+ "cover_art": "• [USA] Some cover art and/or charts have been removed",
+ "crossregion": "• Official Online play is cross-region (including Japan)"
+ },
+ "iidx": {
+ "features": "• [USA] Certain e-amusement features such as video upload unavailable"
+ },
+ "chunithm_intl": {
+ "updates": "• Updates behind JP version. International and JP are completely seperated",
+ "no_service": "No official service in NA or EU.",
+ "regions_link": "See supported regions here"
+ },
+ "maimaidx_intl": {
+ "updates": "• Updates behind JP version. International and JP are completely seperated",
+ "charts": "• Certain charts are removed from USA region",
+ "service": "Official service in USA/CAN/ASIA",
+ "regions_link": "See supported regions here",
+ "no_eu": "(No official service in EU)"
+ },
+ "ongeki_jp": {
+ "japan_only": "Official service only in Japan. No International Version",
+ "private_network": "You are on a private network if the cabinet is not in Japan"
+ },
+ "idac": {
+ "japan_only": "Official service only in Japan. No International Version",
+ "private_network": "You are on a private network if the cabinet is not in Japan"
+ },
+ "music_diver": {
+ "online_service": "Online service available only at Round1 Japan and Round1 USA locations"
+ },
+ "street_fighter": {
+ "online_service": "Online service in USA only at Round1 locations"
+ },
+ "wacca_plus": {
+ "community": "WACCA PLUS is a community continuation of WACCA REVERSE after online services ended in 2022",
+ "note": "Runs on Mythos networked cabs. Not all cabinets have WACCA PLUS as these updates are opt-in by operators."
+ },
+ "museca_plus": {
+ "community": "MÚSECA PLUS is a fan continuation project for MÚSECA 1+1/2.",
+ "note": "Runs on various e-amusement private networks. Not all cabinets have MÚSECA PLUS as it is opt-in.",
+ "download": "You can also download it as a data_mod"
+ },
+ "rb_deluxe_plus": {
+ "community": "A continuation of the abandoned iOS version of REFLEC BEAT (REFLEC BEAT plus)",
+ "note": "Needs to be sideloaded once you get a hold of the IPA. Network features supported. iOS ONLY",
+ "feed_note": "*Not in main feed as date data is unavailable from this source"
+ },
+ "taiko": {
+ "version_note": "Information below only applies to the latest version of the game (LCD + Banapassport Reader)",
+ "maintenance": "Maintenance time is 1am - 7am JST (i think?)",
+ "usa_note": "Applies to USA cabs as well (9am - 3pm PST)"
+ },
+ "wmmt": {
+ "feed": "Singular news feed for NA, ASIA/OCE, and JPN",
+ "version": "All regions run different versions of the game"
+ },
+ "ddr": {
+ "maintenance_note": "Note that USA GOLD cabinets follow Japanese daily maintenance schedule."
+ },
+ "jubeat": {
+ "online_note": "Online only in Japan and Asia regions. No online service in the US (only old versions running offline-kit)"
+ },
+ "popn_music": {
+ "online_note": "Online only in Japan and Asia regions. Japan and Asia only. No online service in the US (only old versions running offline-kit)"
+ },
+ "nostalgia": {
+ "online_note": "Online only in Japan and Asia regions. Japan and Asia only. No online service in the US"
+ },
+ "polaris_chord": {
+ "online_note": "Official e-amusement service only in Japan."
+ }
+ }
+}
diff --git a/site/public/locales/ja/translation.json b/site/public/locales/ja/translation.json
new file mode 100644
index 0000000..ccf96b0
--- /dev/null
+++ b/site/public/locales/ja/translation.json
@@ -0,0 +1,36 @@
+{
+ "homepage": {
+ "welcome": "573-UPDATES へようこそ",
+ "news_aggregation_note": "様々なアーケードゲームのニュースと情報がここに集約されています!",
+ "rss_feeds": "RSSフィードは各ゲームの個別ページで利用可能です",
+ "github_info": "API情報についてはGitHubをご覧ください"
+ },
+ "news_feed": "メインニュースフィード",
+ "game_selector": "ゲームセレクタ",
+ "loading": "読み込み中...",
+ "news_not_found": "ニュースが見つかりません",
+ "return_home": "ホームに戻る",
+ "dark_theme_text": "ダーク",
+ "light_theme_text": "ライト",
+ "view_in_english_text": "英語で表示",
+ "view_in_original_text": "原文で表示",
+ "machine_tl_note": "上記の情報は機械翻訳されており、不正確な内容が含まれる可能性があります",
+ "ai_summary_note": "上記の情報はAIによって生成されました",
+ "read_more": "続きを読む",
+ "copy_link_to_post": "投稿へのリンクをコピー",
+ "copy_link_notif": "投稿への直接リンクをコピーしました(古いニュースは一定時間後に自動的に削除されます)",
+ "subscribed_to_games_count": "通知購読中",
+ "games": "ゲーム",
+ "gameselector": {
+ "title": "ゲームを選択",
+ "subtitle": "個別のゲームフィードは、メインフィードよりも長い期間のニュース履歴を保持しています。",
+ "categories": {
+ "konami": "KONAMI",
+ "sega": "SEGA",
+ "taito": "TAITO",
+ "bandai_namco": "バンダイナムコ",
+ "community": "コミュニティ"
+ },
+ "community_description": "サービス終了した/放棄されたリズムゲームの遺産を継承するコミュニティ主導のプロジェクト"
+ }
+}
diff --git a/site/src/components/GameNotes.tsx b/site/src/components/GameNotes.tsx
index b9ab8c6..999bf7b 100644
--- a/site/src/components/GameNotes.tsx
+++ b/site/src/components/GameNotes.tsx
@@ -5,17 +5,16 @@ import {
AimeIntlMaintenanceInfo,
AllnetPrivateServerWarning,
} from "./NoteModals";
+import i18next from 'i18next';
export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
sdvx: (
<>
<ul className={`mt-2 ${isMoe ? "text-pink-900" : "text-white"}`}>
- <li>
- • [USA] PREMIUM GENERATOR gacha available only ONLINE (No PASELI)
- </li>
- <li>• VP/VOLTEFACTORY rewards only available in Japan</li>
- <li>• [USA] Some cover art and/or charts have been removed </li>
- <li>• Official Online play is cross-region (including Japan)</li>
+ <li>{i18next.t('gamenotes.sdvx.premium_generator')}</li>
+ <li>{i18next.t('gamenotes.sdvx.voltefactory')}</li>
+ <li>{i18next.t('gamenotes.sdvx.cover_art')}</li>
+ <li>{i18next.t('gamenotes.sdvx.crossregion')}</li>
</ul>
<div className="flex justify-center">
<EamuseMaintenancePopup isMoe={isMoe} />
@@ -23,20 +22,16 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- Official e-amusement service in NA available only at Round1 USA
+ {i18next.t('gamenotes.common.na_service_note')}
<br />
- Online Cabinets in non-supported regions (CAN/EU/AUS) are on private
- networks which run older data
+ {i18next.t('gamenotes.common.private_network_note')}
</p>
</>
),
iidx: (
<>
<ul className={`mt-2 ${isMoe ? "text-pink-900" : "text-white"}`}>
- <li>
- • [USA] Certain e-amusement features such as video upload
- unavailable{" "}
- </li>
+ <li>{i18next.t('gamenotes.iidx.features')}</li>
</ul>
<div className="flex justify-center">
<EamuseMaintenancePopup isMoe={isMoe} />
@@ -44,10 +39,9 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- Official e-amusement service in NA available only at Round1 USA
+ {i18next.t('gamenotes.common.na_service_note')}
<br />
- Online Cabinets in non-supported regions (CAN/EU/AUS) are on private
- networks which run older data
+ {i18next.t('gamenotes.common.private_network_note')}
</p>
</>
),
@@ -59,10 +53,9 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- Official e-amusement service in NA available only at Round1 USA
+ {i18next.t('gamenotes.common.na_service_note')}
<br />
- Online Cabinets in non-supported regions (CAN/EU/AUS) are on private
- networks which run older data
+ {i18next.t('gamenotes.common.private_network_note')}
</p>
</>
),
@@ -74,10 +67,9 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- Official e-amusement service in NA available only at Round1 USA
+ {i18next.t('gamenotes.common.na_service_note')}
<br />
- Online Cabinets in non-supported regions (CAN/EU/AUS) are on private
- networks which run older data
+ {i18next.t('gamenotes.common.private_network_note')}
</p>
</>
),
@@ -89,10 +81,9 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- Official e-amusement service in NA available only at Round1 USA
+ {i18next.t('gamenotes.common.na_service_note')}
<br />
- Online Cabinets in non-supported regions (CAN/EU/AUS) are on private
- networks which run older data
+ {i18next.t('gamenotes.common.private_network_note')}
</p>
</>
),
@@ -104,7 +95,7 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- Official e-amusement service only in Japan.
+ {i18next.t('gamenotes.polaris_chord.online_note')}
</p>
</>
),
@@ -116,12 +107,11 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- Official e-amusement service in NA available only at Round1 USA
+ {i18next.t('gamenotes.common.na_service_note')}
<br />
- Online Cabinets in non-supported regions (CAN/EU/AUS) are on private
- networks which run older data
+ {i18next.t('gamenotes.common.private_network_note')}
<br />
- Note that USA GOLD cabinets follow Japanese daily maintenance schedule.
+ {i18next.t('gamenotes.ddr.maintenance_note')}
</p>
</>
),
@@ -130,14 +120,12 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-center`}
>
- Online only in Japan and Asia regions. No online service in the US (only
- old versions running offline-kit)
+ {i18next.t('gamenotes.jubeat.online_note')}
</p>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- Online Cabinets in non-supported regions (CAN/EU/AUS) are on private
- networks which run older data
+ {i18next.t('gamenotes.common.private_network_note')}
</p>
</>
),
@@ -146,14 +134,12 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-center`}
>
- Online only in Japan and Asia regions. Japan and Asia only. No online
- service in the US (only old versions running offline-kit)
+ {i18next.t('gamenotes.popn_music.online_note')}
</p>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- Online Cabinets in non-supported regions (CAN/EU/AUS) are on private
- networks which run older data
+ {i18next.t('gamenotes.common.private_network_note')}
</p>
</>
),
@@ -162,14 +148,12 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-center`}
>
- Online only in Japan and Asia regions. Japan and Asia only. No online
- service in the US
+ {i18next.t('gamenotes.nostalgia.online_note')}
</p>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- Online Cabinets in non-supported regions (CAN/EU/AUS) are on private
- networks which run older data
+ {i18next.t('gamenotes.common.private_network_note')}
</p>
</>
),
@@ -178,12 +162,12 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-center`}
>
- This version of the game is only available in Japan
+ {i18next.t('gamenotes.common.japan_only_note')}
</p>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-center`}
>
- You may be on the International version if you are outside of Japan
+ {i18next.t('gamenotes.common.international_note')}
</p>
</>
),
@@ -192,12 +176,12 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-center`}
>
- This version of the game is only available in Japan
+ {i18next.t('gamenotes.common.japan_only_note')}
</p>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-center`}
>
- You may be on the International version if you are outside of Japan
+ {i18next.t('gamenotes.common.international_note')}
</p>
</>
),
@@ -206,12 +190,12 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-center`}
>
- Official service only in Japan. No International Version
+ {i18next.t('gamenotes.ongeki_jp.japan_only')}
</p>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-center`}
>
- You are on a private network if the cabinet is not in Japan
+ {i18next.t('gamenotes.ongeki_jp.private_network')}
</p>
<div className="flex justify-center">
<AllnetPrivateServerWarning isMoe={isMoe} />
@@ -223,32 +207,29 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-center`}
>
- Official service only in Japan. No International Version
+ {i18next.t('gamenotes.idac.japan_only')}
</p>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-center`}
>
- You are on a private network if the cabinet is not in Japan
+ {i18next.t('gamenotes.idac.private_network')}
</p>
</>
),
chunithm_intl: (
<>
<ul className={`mt-2 ${isMoe ? "text-pink-900" : "text-white"}`}>
- <li>
- • Updates behind JP version. International and JP are completely
- seperated
- </li>
+ <li>{i18next.t('gamenotes.chunithm_intl.updates')}</li>
</ul>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- No official service in NA or EU.{" "}
+ {i18next.t('gamenotes.chunithm_intl.no_service')}{" "}
<a
className="underline"
href="https://location.am-all.net/alm/location?gm=104&lang=en"
>
- See supported regions here
+ {i18next.t('gamenotes.chunithm_intl.regions_link')}
</a>
</p>
<div className="flex justify-center">
@@ -262,29 +243,24 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<AimeIntlMaintenanceInfo isMoe={isMoe} />
</div>
<ul className={`mt-2 ${isMoe ? "text-pink-900" : "text-white"}`}>
- <li>
- • Updates behind JP version. International and JP are completely
- seperated
- </li>
- <li>
- • Certain charts are removed from USA region
- </li>
+ <li>{i18next.t('gamenotes.maimaidx_intl.updates')}</li>
+ <li>{i18next.t('gamenotes.maimaidx_intl.charts')}</li>
</ul>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- Official service in USA/CAN/ASIA{" "}
+ {i18next.t('gamenotes.maimaidx_intl.service')}{" "}
<a
className="underline"
href="https://location.am-all.net/alm/location?gm=98"
>
- See supported regions here
+ {i18next.t('gamenotes.maimaidx_intl.regions_link')}
</a>
</p>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- (No official service in EU)
+ {i18next.t('gamenotes.maimaidx_intl.no_eu')}
</p>
<div className="flex justify-center">
<AllnetPrivateServerWarning isMoe={isMoe} />
@@ -299,7 +275,7 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-center`}
>
- Online service available only at Round1 Japan and Round1 USA locations
+ {i18next.t('gamenotes.music_diver.online_service')}
</p>
</>
),
@@ -311,7 +287,7 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-center`}
>
- Online service in USA only at Round1 locations
+ {i18next.t('gamenotes.street_fighter.online_service')}
</p>
</>
),
@@ -320,14 +296,12 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-white"} text-center`}
>
- WACCA PLUS is a community continuation of WACCA REVERSE after online
- services ended in 2022
+ {i18next.t('gamenotes.wacca_plus.community')}
</p>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- Runs on Mythos networked cabs. Not all cabinets have WACCA PLUS as these
- updates are opt-in by operators.
+ {i18next.t('gamenotes.wacca_plus.note')}
</p>
</>
),
@@ -336,19 +310,18 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-white"} text-center`}
>
- MÚSECA PLUS is a fan continuation project for MÚSECA 1+1/2.
+ {i18next.t('gamenotes.museca_plus.community')}
</p>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- Runs on various e-amusement private networks. Not all cabinets have
- MÚSECA PLUS as it is opt-in.
+ {i18next.t('gamenotes.museca_plus.note')}
</p>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
<a className="underline" href="https://museca.plus/downloads">
- You can also download it as a data_mod
+ {i18next.t('gamenotes.museca_plus.download')}
</a>
</p>
</>
@@ -358,19 +331,17 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-white"} text-center`}
>
- A continuation of the abandoned iOS version of REFLEC BEAT (REFLEC BEAT
- plus)
+ {i18next.t('gamenotes.rb_deluxe_plus.community')}
</p>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- Needs to be sideloaded once you get a hold of the IPA. Network features
- supported. iOS ONLY
+ {i18next.t('gamenotes.rb_deluxe_plus.note')}
</p>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- *Not in main feed as date data is unavailable from this source
+ {i18next.t('gamenotes.rb_deluxe_plus.feed_note')}
</p>
</>
),
@@ -379,12 +350,13 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-white"} text-center`}
>
- Information below only applies to the latest version of the game (LCD + Banapassport Reader)
+ {i18next.t('gamenotes.taiko.version_note')}
</p>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- Maintenance time is 1am - 7am JST (i think?)<br/>Applies to USA cabs as well (9am - 3pm PST)
+ {i18next.t('gamenotes.taiko.maintenance')}<br/>
+ {i18next.t('gamenotes.taiko.usa_note')}
</p>
</>
),
@@ -393,12 +365,12 @@ export const GameNotes = (isMoe: boolean): Record<string, React.ReactNode> => ({
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-white"} text-center`}
>
- Singular news feed for NA, ASIA/OCE, and JPN
+ {i18next.t('gamenotes.wmmt.feed')}
</p>
<p
className={`mt-3 ${isMoe ? "text-pink-800" : "text-pink-300"} text-right`}
>
- All regions run different versions of the game
+ {i18next.t('gamenotes.wmmt.version')}
</p>
</>
),
diff --git a/site/src/components/LanguageSwitcher.tsx b/site/src/components/LanguageSwitcher.tsx
new file mode 100644
index 0000000..8cceb35
--- /dev/null
+++ b/site/src/components/LanguageSwitcher.tsx
@@ -0,0 +1,109 @@
+import { useTranslation } from 'react-i18next';
+import { useSearchParams } from 'react-router-dom';
+import { useRef, useState, useEffect } from 'react';
+
+const languages = [
+ { code: 'en', name: 'English' },
+ { code: 'ja', name: '日本語' }
+];
+
+interface LanguageSwitcherProps {
+ variant?: 'compact' | 'standard';
+}
+
+function LanguageSwitcher({ variant = 'standard' }: LanguageSwitcherProps) {
+ const { i18n } = useTranslation();
+ const [searchParams] = useSearchParams();
+ const isMoe = searchParams.has("moe");
+ const [isOpen, setIsOpen] = useState(false);
+ const dropdownRef = useRef<HTMLDivElement>(null);
+
+ const currentLanguage = languages.find(lang => lang.code === i18n.language) || languages[0];
+
+ // Close the dropdown when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+ setIsOpen(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ return (
+ <div className="relative inline-block" ref={dropdownRef}>
+ <button
+ onClick={() => setIsOpen(!isOpen)}
+ className={`
+ flex items-center justify-between
+ transition-all duration-200 text-sm rounded
+ ${variant === 'compact' ? 'px-2 py-0.5 min-w-[80px]' : 'px-3 py-1.5 min-w-[100px]'}
+ ${isMoe
+ ? 'bg-pink-200 text-pink-800 hover:bg-pink-300'
+ : 'bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white'
+ }
+ `}
+ aria-haspopup="true"
+ aria-expanded={isOpen}
+ >
+ <span>{currentLanguage.name}</span>
+ <span className="ml-1">
+ <svg
+ className={`h-4 w-4 transition-transform ${isOpen ? 'rotate-180' : ''}`}
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth={2}
+ d="M19 9l-7 7-7-7"
+ />
+ </svg>
+ </span>
+ </button>
+
+ {isOpen && (
+ <div
+ className={`
+ absolute right-0 mt-1 z-10 shadow-lg rounded-md overflow-hidden
+ ${variant === 'compact' ? 'w-24' : 'w-32'}
+ ${isMoe ? 'bg-pink-100 border border-pink-300' : 'bg-gray-800 border border-gray-700'}
+ `}
+ >
+ <div className="py-1">
+ {languages.map((lang) => (
+ <button
+ key={lang.code}
+ onClick={() => {
+ i18n.changeLanguage(lang.code);
+ setIsOpen(false);
+ }}
+ className={`
+ block w-full text-left px-4 py-2 text-sm
+ ${i18n.language === lang.code
+ ? isMoe
+ ? 'bg-pink-300 text-pink-800 font-medium'
+ : 'bg-purple-700 text-white font-medium'
+ : isMoe
+ ? 'text-pink-800 hover:bg-pink-200'
+ : 'text-gray-300 hover:bg-gray-700 hover:text-white'
+ }
+ `}
+ >
+ {lang.name}
+ </button>
+ ))}
+ </div>
+ </div>
+ )}
+ </div>
+ );
+}
+
+export default LanguageSwitcher;
diff --git a/site/src/components/NewsFeed.tsx b/site/src/components/NewsFeed.tsx
index 7cf5a08..2e08b16 100644
--- a/site/src/components/NewsFeed.tsx
+++ b/site/src/components/NewsFeed.tsx
@@ -1,6 +1,7 @@
import { useState, useEffect } from "react";
import { getGameTitle, getShortenedGameName } from "../utils.ts";
import { useSearchParams } from "react-router-dom";
+import { useTranslation } from "react-i18next";
export interface NewsData {
date: string;
@@ -24,6 +25,7 @@ interface NewsFeedProps {
}
export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => {
+ const { t } = useTranslation();
const [showEnglish, setShowEnglish] = useState<Record<string, boolean>>({});
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
const [currentImageIndex, setCurrentImageIndex] = useState<
@@ -168,7 +170,7 @@ export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => {
onClick={() => toggleLanguage(newsId)}
className={`${isMoe ? "bg-pink-200 hover:bg-pink-300" : "bg-gray-800 hover:bg-gray-700"} text-xs py-1 px-2 rounded`}
>
- {isEnglish ? "View Original" : "View in English"}
+ {isEnglish ? t("view_in_original_text") : t("view_in_english_text")}
</button>
)}
</div>
@@ -233,13 +235,13 @@ export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => {
: `${window.location.origin}${pathname === "/news" ? "" : pathname}#${newsId}`;
navigator.clipboard.writeText(url);
alert(
- "Copied Direct Link to Post (Older news are automatically culled after some time)",
+ `${t('copy_link_notif')}`
);
}}
title="Copy permalink"
className="text-xs text-blue-400 hover:underline cursor-pointer"
>
- 🔗 Copy Link to Post
+ 🔗 {`${t('copy_link_to_post')}`}
</a>
</div>
@@ -248,8 +250,7 @@ export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => {
<div
className={`${isMoe ? "bg-pink-200 text-pink-800" : "bg-gray-800 text-white"} px-3 py-2 text-xs text-center`}
>
- The information above is written by AI /
- 上記の情報はAIによって生成されました。
+ {`${t('ai_summary_note')}`}
</div>
)}
@@ -258,8 +259,7 @@ export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => {
<div
className={`${isMoe ? "bg-pink-200 text-pink-800" : "bg-gray-800 text-white"} px-3 py-2 text-xs text-center`}
>
- The information above is machine translated and may contain
- inaccuracies
+ {`${t('machine_tl_note')}`}
</div>
)}
diff --git a/site/src/components/TitleBar.tsx b/site/src/components/TitleBar.tsx
index 2229a45..3822980 100644
--- a/site/src/components/TitleBar.tsx
+++ b/site/src/components/TitleBar.tsx
@@ -5,12 +5,15 @@ import {
useNavigate,
useLocation,
} from "react-router-dom";
+import LanguageSwitcher from "./LanguageSwitcher";
+import { useTranslation } from "react-i18next";
const TitleBar: React.FC = () => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const location = useLocation();
const isMoe = searchParams.has("moe");
+ const { t } = useTranslation();
const toggleTheme = () => {
const params = new URLSearchParams(searchParams);
@@ -56,7 +59,7 @@ const TitleBar: React.FC = () => {
onClick={toggleTheme}
className={`text-sm ${isMoe ? "bg-pink-100 text-pink-800 hover:bg-pink-200 hover:text-pink-600" : "bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white"} font-medium px-3 py-1 rounded`}
>
- {isMoe ? "🌙 Dark" : "🌸 Light"}
+ {isMoe ? "🌙 "+t('dark_theme_text') : "🌸 "+t("light_theme_text")}
</button>
<img
src="/rasis.webp"
@@ -81,14 +84,15 @@ const TitleBar: React.FC = () => {
to={`/${isMoe ? "?moe" : ""}`}
className={`${isMoe ? "text-pink-800 hover:text-pink-600" : "text-gray-300 hover:text-white"} font-medium text-sm sm:text-base`}
>
- Main News Feed
+ {t('news_feed')}
</Link>
<Link
to={`/games${isMoe ? "?moe" : ""}`}
className={`${isMoe ? "text-pink-800 hover:text-pink-600" : "text-gray-300 hover:text-white"} font-medium text-sm sm:text-base`}
>
- Game Selector
+ {t('game_selector')}
</Link>
+ <LanguageSwitcher variant="compact" />
</div>
</div>
</div>
diff --git a/site/src/i18n.ts b/site/src/i18n.ts
new file mode 100644
index 0000000..9bf1aee
--- /dev/null
+++ b/site/src/i18n.ts
@@ -0,0 +1,30 @@
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import LanguageDetector from 'i18next-browser-languagedetector';
+import Backend from 'i18next-http-backend';
+import { updateHtmlLang } from './utils';
+
+i18n
+ .use(Backend)
+ .use(LanguageDetector)
+ .use(initReactI18next)
+ .init({
+ fallbackLng: 'en',
+ backend: {
+ loadPath: '/locales/{{lng}}/{{ns}}.json',
+ },
+
+ interpolation: {
+ escapeValue: false,
+ }
+ });
+
+// Set the HTML lang attribute when language changes
+i18n.on('languageChanged', (lng) => {
+ updateHtmlLang(lng);
+});
+
+// Initialize HTML lang with the current language
+updateHtmlLang(i18n.language);
+
+export default i18n;
diff --git a/site/src/main.tsx b/site/src/main.tsx
index 9a1f365..07f6249 100644
--- a/site/src/main.tsx
+++ b/site/src/main.tsx
@@ -4,6 +4,7 @@ import './index.css'
import { BrowserRouter } from "react-router-dom";
import { Analytics } from "@vercel/analytics/react"
import App from './App.tsx'
+import './i18n.ts'
createRoot(document.getElementById('root')!).render(
<StrictMode>
diff --git a/site/src/pages/GameSelector.tsx b/site/src/pages/GameSelector.tsx
index 41551bc..5586cbf 100644
--- a/site/src/pages/GameSelector.tsx
+++ b/site/src/pages/GameSelector.tsx
@@ -1,5 +1,6 @@
import { Link, useSearchParams } from "react-router-dom";
import TitleBar from "../components/TitleBar";
+import { useTranslation } from "react-i18next";
interface GameCategory {
name: string;
@@ -67,18 +68,19 @@ const gameInfo: GameCategory[] = [
const GameSelector = () => {
const [searchParams] = useSearchParams();
const isMoe = searchParams.has("moe");
+ const { t } = useTranslation();
const renderCategory = (category: GameCategory) => (
<div key={category.name} className="mb-6">
<h2
className={`text-lg font-bold ${isMoe ? "text-pink-700" : "text-gray-200"}`}
>
- {category.name}
+ {t(`gameselector.categories.${category.name.toLowerCase().replace(' ', '_')}`)}
</h2>
<p
className={`text-sm ${isMoe ? "text-pink-600" : "text-gray-400"} mb-2`}
>
- {category.description}
+ {category.name === "COMMUNITY" ? t('gameselector.community_description') : category.description}
</p>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2 mt-2">
{category.games.map((game) => (
@@ -104,13 +106,12 @@ const GameSelector = () => {
<h1
className={`text-2xl font-bold mb-4 ${isMoe ? "text-pink-800" : "text-white"} sm:mb-6`}
>
- Select a Game
+ {t('gameselector.title')}
</h1>
<h2
className={`text-base font-medium ${isMoe ? "text-pink-700" : "text-gray-300"} mb-4`}
>
- Individual game feeds keep a longer history of news relating to that
- game than the main feed.
+ {t('gameselector.subtitle')}
</h2>
{gameInfo.map(renderCategory)}
</div>
diff --git a/site/src/pages/Homepage.tsx b/site/src/pages/Homepage.tsx
index 4e04688..13ec4fd 100644
--- a/site/src/pages/Homepage.tsx
+++ b/site/src/pages/Homepage.tsx
@@ -5,6 +5,7 @@ import { getGameTitle } from "../utils.ts";
import TitleBar from "../components/TitleBar";
import { GameNotes } from "../components/GameNotes";
import NotificationButton from "../components/NotificationButton";
+import { useTranslation } from "react-i18next";
interface ArcadeNewsAPIData {
fetch_time: number;
@@ -12,6 +13,8 @@ interface ArcadeNewsAPIData {
}
export default function Home() {
+ const { t } = useTranslation();
+
const { gameId } = useParams<{ gameId?: string }>();
const [searchParams] = useSearchParams();
const isMoe = searchParams.has("moe");
@@ -116,7 +119,7 @@ export default function Home() {
<h1
className={`${isMoe ? "text-pink-900" : "text-white"} text-3xl font-bold mb-4`}
>
- News Not Found
+ {t('news_not_found')}
</h1>
<p
className={`${isMoe ? "text-pink-700" : "text-gray-400"} text-lg mb-8`}
@@ -142,7 +145,7 @@ export default function Home() {
: "bg-purple-600 text-white hover:bg-purple-700"
}`}
>
- Return Home
+ {t('return_home')}
</a>
</div>
</div>
@@ -218,7 +221,7 @@ export default function Home() {
<div
className={`${isMoe ? "bg-pink-200 text-pink-900" : "bg-gray-800 text-white"} rounded-lg p-6 text-center shadow-lg`}
>
- <h1 className="text-2xl font-bold">Welcome to 573-UPDATES</h1>
+ <h1 className="text-2xl font-bold">{t('homepage.welcome')}</h1>
<h2
className={`text-2xl font-extrabold mb-4 tracking-widest text-center uppercase glow-neon ${
isMoe ? "text-pink-500" : "text-[#00FF00]"
@@ -233,21 +236,20 @@ export default function Home() {
/>
</div>
<p>
- News and Information for various arcade games is aggregated
- here!
+ {t('homepage.news_aggregation_note')}
</p>
<p className="mt-2">
- RSS feeds are available on each game's individual page
+ {t('homepage.rss_feeds')}
</p>
<p className="mt-2">
- Please see the{" "}
+ {t('homepage.github_info').split('GitHub')[0]}{" "}
<a
href="https://github.com/pinapelz/573-updates"
className="text-blue-500 hover:underline"
>
GitHub
</a>{" "}
- for API information
+ {t('homepage.github_info').split('GitHub')[1] || ''}
</p>
<div className="mt-6">
<div className="mt-4">
@@ -267,8 +269,7 @@ export default function Home() {
} flex items-center gap-1 mx-auto transition-colors`}
>
<span>
- Subscribed to {subscribedGames.length} game
- {subscribedGames.length !== 1 ? "s" : ""}
+ {`${t('subscribed_to_games_count')}`} {subscribedGames.length} {`${t('games')}`}
</span>
<svg
className={`w-4 h-4 transition-transform ${showSubscribedDropdown ? "rotate-180" : ""}`}
diff --git a/site/src/pages/NotFound.tsx b/site/src/pages/NotFound.tsx
index 37978af..05173ca 100644
--- a/site/src/pages/NotFound.tsx
+++ b/site/src/pages/NotFound.tsx
@@ -1,7 +1,9 @@
import { useSearchParams } from "react-router-dom";
import TitleBar from "../components/TitleBar";
+import { useTranslation } from "react-i18next";
export default function NotFound() {
+ const { t } = useTranslation();
const [searchParams] = useSearchParams();
const isMoe = searchParams.has("moe");
@@ -16,7 +18,7 @@ export default function NotFound() {
className={`${isMoe ? "bg-pink-200 text-pink-900" : "bg-gray-800 text-white"} rounded-lg p-8 shadow-lg`}
>
<h1 className="text-6xl font-bold mb-4">404</h1>
- <h2 className="text-2xl font-semibold mb-4">Page Not Found</h2>
+ <h2 className="text-2xl font-semibold mb-4">{t('notFound.title')}</h2>
<div className="mb-6">
<img
src="/liris.webp"
@@ -25,7 +27,7 @@ export default function NotFound() {
/>
</div>
<p className="text-lg mb-6">
- The page you're looking for doesn't exist or has been moved.
+ {t('notFound.description')}
</p>
<div className="space-y-3">
<a
@@ -36,7 +38,7 @@ export default function NotFound() {
: "bg-purple-600 text-white hover:bg-purple-700"
}`}
>
- Go to Homepage
+ {t('return_home')}
</a>
<div className="mt-4">
<a
@@ -45,7 +47,7 @@ export default function NotFound() {
isMoe ? "text-pink-600 hover:text-pink-800" : "text-blue-400 hover:text-blue-300"
} underline`}
>
- View All Games
+ {t('notFound.view_all_games')}
</a>
</div>
</div>
diff --git a/site/src/utils.ts b/site/src/utils.ts
index c6d0566..e6d19b6 100644
--- a/site/src/utils.ts
+++ b/site/src/utils.ts
@@ -65,3 +65,16 @@ export const getShortenedGameName = (gameId: string) => {
if(lowerCaseGameId.startsWith("wangan_maxi_asia_oce")) return "wangan_maxi_asia_oce";
return gameId.toUpperCase();
};
+
+export const updateHtmlLang = (language: string): void => {
+ document.documentElement.lang = language;
+ const metaLang = document.querySelector('meta[name="language"]');
+ if (metaLang) {
+ metaLang.setAttribute('content', language);
+ } else {
+ const meta = document.createElement('meta');
+ meta.setAttribute('name', 'language');
+ meta.setAttribute('content', language);
+ document.head.appendChild(meta);
+ }
+};
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage