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 /site/src/components | |
| parent | 014443ef502eee0c337a5feb2aa0aeebb8d51557 (diff) | |
feat: add i18n translation (initial JP and EN)
Diffstat (limited to 'site/src/components')
| -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 |
4 files changed, 180 insertions, 95 deletions
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> |
