diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-10-07 17:25:43 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-10-07 17:25:50 -0700 |
| commit | d7b5b81b5d6ec55d0847b5171c3800a8f7b5c001 (patch) | |
| tree | d646d8339602599eee64910cd252de0df595bcfe | |
| parent | 014443ef502eee0c337a5feb2aa0aeebb8d51557 (diff) | |
feat: add i18n translation (initial JP and EN)
| -rw-r--r-- | .gitignore | 5 | ||||
| -rw-r--r-- | site/index.html | 1 | ||||
| -rw-r--r-- | site/package.json | 4 | ||||
| -rw-r--r-- | site/pnpm-lock.yaml | 121 | ||||
| -rw-r--r-- | site/public/locales/en/translation.json | 121 | ||||
| -rw-r--r-- | site/public/locales/ja/translation.json | 36 | ||||
| -rw-r--r-- | site/src/components/GameNotes.tsx | 142 | ||||
| -rw-r--r-- | site/src/components/LanguageSwitcher.tsx | 109 | ||||
| -rw-r--r-- | site/src/components/NewsFeed.tsx | 14 | ||||
| -rw-r--r-- | site/src/components/TitleBar.tsx | 10 | ||||
| -rw-r--r-- | site/src/i18n.ts | 30 | ||||
| -rw-r--r-- | site/src/main.tsx | 1 | ||||
| -rw-r--r-- | site/src/pages/GameSelector.tsx | 11 | ||||
| -rw-r--r-- | site/src/pages/Homepage.tsx | 21 | ||||
| -rw-r--r-- | site/src/pages/NotFound.tsx | 10 | ||||
| -rw-r--r-- | site/src/utils.ts | 13 |
16 files changed, 534 insertions, 115 deletions
@@ -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); + } +}; |
