diff --git a/public/UITexts.example b/public/UITexts.example index bb8779b..c7480c7 100644 --- a/public/UITexts.example +++ b/public/UITexts.example @@ -109,5 +109,65 @@ "groupforum.message.hide": "Hide message", "group.forum.enable.caption": "Enable / Disable group forum", "group.forum.enable.help": "If you disable the group forum, all posts will also be deleted!", - "groupforum.view.no_threads": "There are currently no active threads" -} \ No newline at end of file + "groupforum.view.no_threads": "There are currently no active threads", + "login.username": "Name of your Habbo", + "login.forgot_password": "Forgotten your password?", + + "nitro.login.firsttime.title": "First time here?", + "nitro.login.firsttime.text": "Don't have a Habbo yet?", + "nitro.login.firsttime.link": "You can create one here", + "nitro.login.card.title": "What's your Habbo called?", + + "nitro.login.server.offline.short": "The gameserver isn't running right now. Please try again in a moment.", + "nitro.login.server.offline.long": "The gameserver isn't running right now, so new accounts can't be created. Please try again in a moment.", + "nitro.login.server.checking": "Checking…", + "nitro.login.server.retry": "Retry", + + "nitro.login.register.title": "Habbo Details", + "nitro.login.register.next": "Next", + "nitro.login.register.finish": "Finish", + "nitro.login.register.creating": "Creating…", + + "nitro.login.register.intro.credentials": "Let's create your account. Enter your email and pick a password — we'll check that email isn't already in use.", + "nitro.login.register.intro.avatar": "Now it's time to make your own Habbo character! To make your own Habbo, please start by choosing your Habbo Name.", + "nitro.login.register.intro.room": "Last step — pick a starter room, or skip and create your own later.", + + "nitro.login.register.confirm.label": "Confirm password", + "nitro.login.register.username.placeholder": "HabboName", + + "nitro.login.register.hotlooks.count": "%count% looks available", + "nitro.login.register.hotlooks.none": "No hot looks loaded", + + "nitro.login.register.room.skip.title": "I'm okay — I'll create my own rooms", + "nitro.login.register.room.skip.description": "Skip for now and start with an empty hotel inventory.", + "nitro.login.register.room.loading": "Loading rooms…", + "nitro.login.register.room.error": "Could not load room options. You can still skip this step.", + + "nitro.login.register.success": "Welcome aboard, %username%! Your account is ready — log in below with the password you just chose.", + + "nitro.login.forgot.title": "Reset password", + "nitro.login.forgot.email.label": "Email address", + "nitro.login.forgot.send": "Send email", + "nitro.login.forgot.success": "Email sent! If an account matches that address you'll find a reset link in your inbox shortly (check spam if it doesn't show up within a minute).", + + "nitro.login.error.missing_credentials": "Please enter both your Habbo name and password.", + "nitro.login.error.invalid_credentials": "Invalid Habbo name or password.", + "nitro.login.error.too_many_attempts": "Too many attempts. Try again in %seconds%s.", + "nitro.login.error.turnstile": "Please complete the security check.", + "nitro.login.error.server_offline": "The gameserver is not running. Please try again later.", + "nitro.login.error.login_unreachable": "Unable to reach the login service. Please try again.", + "nitro.login.error.register_failed": "Unable to create your account.", + "nitro.login.error.register_unreachable": "Unable to reach the registration service.", + "nitro.login.error.forgot_failed": "Unable to send a reset email right now.", + "nitro.login.error.forgot_unreachable": "Unable to reach the password reset service.", + "nitro.login.error.missing_fields": "Please fill in every field.", + "nitro.login.error.invalid_email": "Please enter a valid email address.", + "nitro.login.error.password_too_short": "Your password must be at least 8 characters.", + "nitro.login.error.password_mismatch": "Passwords do not match.", + "nitro.login.error.email_taken": "This email is already in use.", + "nitro.login.error.missing_username": "Please choose a Habbo name.", + "nitro.login.error.username_length": "Habbo name must be 3–16 characters.", + "nitro.login.error.username_taken": "This Habbo name is already taken.", + "nitro.login.error.missing_email": "Please enter your email address." +} +} diff --git a/src/App.tsx b/src/App.tsx index 94128b3..032ac0c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -35,7 +35,6 @@ export const App: FC<{}> = props => setPrepareTrigger(prev => prev + 1); }, []); - // Listen for socket closed events (code 1000 "Bye" - server rejected SSO) useNitroEvent(NitroEventType.SOCKET_CLOSED, showSessionExpired); useMessageEvent(LoadGameUrlEvent, event => @@ -61,8 +60,6 @@ export const App: FC<{}> = props => if(!ssoTicket || ssoTicket === '') { - // Configuration is loaded lazily — fetch it up-front so the login - // screen toggle and Turnstile keys are available before we decide. let configInitError: unknown = null; try { await GetConfiguration().init(); } catch(e) { configInitError = e; } @@ -77,6 +74,9 @@ export const App: FC<{}> = props => if(loginScreenEnabled) { + try { await GetLocalizationManager().init(); } + catch(localizationErr) { NitroLogger.error('[LoginScreen] Localization init failed', localizationErr); } + setIsReady(false); setShowLogin(true); return; @@ -110,7 +110,7 @@ export const App: FC<{}> = props => eventMode: 'none', failIfMajorPerformanceCaveat: false, roundPixels: true, - useBackBuffer // Keep disabled by default unless explicitly enabled in NitroConfig + useBackBuffer }); await GetConfiguration().init(); diff --git a/src/components/login/LoginView.tsx b/src/components/login/LoginView.tsx index 024b56e..38aca42 100644 --- a/src/components/login/LoginView.tsx +++ b/src/components/login/LoginView.tsx @@ -1,8 +1,26 @@ import { GetConfiguration } from '@nitrots/nitro-renderer'; import { FC, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { GetConfigurationValue } from '../../api'; +import { GetConfigurationValue, LocalizeText } from '../../api'; import { TurnstileWidget } from './TurnstileWidget'; +const t = (key: string, fallback: string, params?: string[], replacements?: string[]): string => +{ + try + { + const value = LocalizeText(key, params ?? null, replacements ?? null); + if(value && value !== key) return value; + } + catch {} + + if(!params || !replacements) return fallback; + let out = fallback; + for(let i = 0; i < params.length; i++) + { + if(replacements[i] !== undefined) out = out.replace('%' + params[i] + '%', replacements[i]); + } + return out; +}; + type DialogMode = 'login' | 'register' | 'forgot'; const interpolate = (value: string | null | undefined): string => @@ -207,19 +225,19 @@ export const LoginView: FC = ({ onAuthenticated }) => if(state.lockedUntil > nowTs) { const remaining = Math.ceil((state.lockedUntil - nowTs) / 1000); - setError(`Too many attempts. Try again in ${ remaining }s.`); + setError(t('nitro.login.error.too_many_attempts', 'Too many attempts. Try again in %seconds%s.', [ 'seconds' ], [ String(remaining) ])); return; } if(!username.trim() || !password) { - setError('Please enter both your Habbo name and password.'); + setError(t('nitro.login.error.missing_credentials', 'Please enter both your Habbo name and password.')); return; } if(turnstileEnabled && !loginTurnstileToken) { - setError('Please complete the security check.'); + setError(t('nitro.login.error.turnstile', 'Please complete the security check.')); return; } @@ -231,7 +249,7 @@ export const LoginView: FC = ({ onAuthenticated }) => const serverOk = await pingLoginServer(); if(!serverOk) { - setError('The gameserver is not running. Please try again later.'); + setError(t('nitro.login.error.server_offline', 'The gameserver is not running. Please try again later.')); return; } const { ok, payload } = await postJson(loginUrl, { @@ -250,14 +268,14 @@ export const LoginView: FC = ({ onAuthenticated }) => } recordFailure(); - const message = typeof payload.error === 'string' ? payload.error : 'Invalid Habbo name or password.'; + const message = typeof payload.error === 'string' ? payload.error : t('nitro.login.error.invalid_credentials', 'Invalid Habbo name or password.'); setError(message); resetLoginTurnstile(); } catch(err) { recordFailure(); - setError('Unable to reach the login service. Please try again.'); + setError(t('nitro.login.error.login_unreachable', 'Unable to reach the login service. Please try again.')); resetLoginTurnstile(); } finally @@ -294,7 +312,7 @@ export const LoginView: FC = ({ onAuthenticated }) => const { ok, status, payload } = await postJson(checkEmailUrl, { email }); const result = interpretAvailability(ok, status, payload); if(result.available) return { available: true }; - return { available: false, error: result.error || 'This email is already in use.' }; + return { available: false, error: result.error || t('nitro.login.error.email_taken', 'This email is already in use.') }; } catch { @@ -309,7 +327,7 @@ export const LoginView: FC = ({ onAuthenticated }) => const { ok, status, payload } = await postJson(checkUsernameUrl, { username }); const result = interpretAvailability(ok, status, payload); if(result.available) return { available: true }; - return { available: false, error: result.error || 'This Habbo name is already taken.' }; + return { available: false, error: result.error || t('nitro.login.error.username_taken', 'This Habbo name is already taken.') }; } catch { @@ -321,7 +339,7 @@ export const LoginView: FC = ({ onAuthenticated }) => { if(turnstileEnabled && !body.turnstileToken) { - setError('Please complete the security check.'); + setError(t('nitro.login.error.turnstile', 'Please complete the security check.')); return; } @@ -343,7 +361,7 @@ export const LoginView: FC = ({ onAuthenticated }) => if(ok) { - const friendly = `Welcome aboard, ${ body.username }! Your account is ready — log in below with the password you just chose.`; + const friendly = t('nitro.login.register.success', 'Welcome aboard, %username%! Your account is ready — log in below with the password you just chose.', [ 'username' ], [ body.username ]); setInfo(typeof payload.message === 'string' ? payload.message : friendly); setMode('login'); setUsername(body.username); @@ -351,12 +369,12 @@ export const LoginView: FC = ({ onAuthenticated }) => return; } - setError(typeof payload.error === 'string' ? payload.error : 'Unable to create your account.'); + setError(typeof payload.error === 'string' ? payload.error : t('nitro.login.error.register_failed', 'Unable to create your account.')); onDialogReset(); } catch { - setError('Unable to reach the registration service.'); + setError(t('nitro.login.error.register_unreachable', 'Unable to reach the registration service.')); onDialogReset(); } finally @@ -369,7 +387,7 @@ export const LoginView: FC = ({ onAuthenticated }) => { if(turnstileEnabled && !body.turnstileToken) { - setError('Please complete the security check.'); + setError(t('nitro.login.error.turnstile', 'Please complete the security check.')); return; } @@ -386,18 +404,18 @@ export const LoginView: FC = ({ onAuthenticated }) => if(ok) { - const friendly = 'Email sent! If an account matches that address you\'ll find a reset link in your inbox shortly (check spam if it doesn\'t show up within a minute).'; + const friendly = t('nitro.login.forgot.success', 'Email sent! If an account matches that address you\'ll find a reset link in your inbox shortly (check spam if it doesn\'t show up within a minute).'); setInfo(typeof payload.message === 'string' ? payload.message : friendly); setMode('login'); return; } - setError(typeof payload.error === 'string' ? payload.error : 'Unable to send a reset email right now.'); + setError(typeof payload.error === 'string' ? payload.error : t('nitro.login.error.forgot_failed', 'Unable to send a reset email right now.')); onDialogReset(); } catch { - setError('Unable to reach the password reset service.'); + setError(t('nitro.login.error.forgot_unreachable', 'Unable to reach the password reset service.')); onDialogReset(); } finally @@ -420,18 +438,18 @@ export const LoginView: FC = ({ onAuthenticated }) =>
-
First time here?
+
{ t('nitro.login.firsttime.title', 'First time here?') }
- Don't have a Habbo yet? - setMode('register') }>You can create one here + { t('nitro.login.firsttime.text', 'Don\'t have a Habbo yet?') } + setMode('register') }>{ t('nitro.login.firsttime.link', 'You can create one here') }
-
What's your Habbo called?
+
{ t('nitro.login.card.title', 'What\'s your Habbo called?') }
- + = ({ onAuthenticated }) => />
- + = ({ onAuthenticated }) => /> } { loginServerReachable === false &&
- The gameserver isn't running right now. Please try again in a moment. + { t('nitro.login.server.offline.short', 'The gameserver isn\'t running right now. Please try again in a moment.') }
} @@ -478,9 +496,9 @@ export const LoginView: FC = ({ onAuthenticated }) => type="submit" className="ok-button" disabled={ submitting || isLocked || loginServerReachable === false || loginPingingServer } - >{ loginPingingServer ? 'Checking…' : 'OK' } + >{ loginPingingServer ? t('nitro.login.server.checking', 'Checking…') : t('login.title', 'Log in') }
- setMode('forgot') }>Forgotten your password? + setMode('forgot') }>{ t('login.forgot_password', 'Forgotten your password?') }
@@ -742,7 +760,7 @@ const RegisterDialog: FC = props => .catch(() => { if(cancelled) return; setRoomTemplates([]); - setRoomTemplatesError('Could not load room options. You can still skip this step.'); + setRoomTemplatesError(t('nitro.login.register.room.error', 'Could not load room options. You can still skip this step.')); }); return () => { cancelled = true; }; }, [ step, roomTemplates, roomTemplatesUrl ]); @@ -863,22 +881,22 @@ const RegisterDialog: FC = props => if(!email.trim() || !password || !confirm) { - setLocalError('Please fill in every field.'); + setLocalError(t('nitro.login.error.missing_fields', 'Please fill in every field.')); return; } if(!EMAIL_REGEX.test(email.trim())) { - setLocalError('Please enter a valid email address.'); + setLocalError(t('nitro.login.error.invalid_email', 'Please enter a valid email address.')); return; } if(password.length < 8) { - setLocalError('Your password must be at least 8 characters.'); + setLocalError(t('nitro.login.error.password_too_short', 'Your password must be at least 8 characters.')); return; } if(password !== confirm) { - setLocalError('Passwords do not match.'); + setLocalError(t('nitro.login.error.password_mismatch', 'Passwords do not match.')); return; } @@ -888,13 +906,13 @@ const RegisterDialog: FC = props => const serverOk = await pingServer(); if(!serverOk) { - setLocalError('The gameserver is not running. Please try again later.'); + setLocalError(t('nitro.login.error.server_offline', 'The gameserver is not running. Please try again later.')); return; } const result = await onCheckEmail(email.trim()); if(!result.available) { - setLocalError(result.error || 'This email is already in use.'); + setLocalError(result.error || t('nitro.login.error.email_taken', 'This email is already in use.')); return; } setStep('avatar'); @@ -973,18 +991,18 @@ const RegisterDialog: FC = props => const trimmed = username.trim(); if(!trimmed) { - setLocalError('Please choose a Habbo name.'); + setLocalError(t('nitro.login.error.missing_username', 'Please choose a Habbo name.')); return; } if(trimmed.length < 3 || trimmed.length > 16) { - setLocalError('Habbo name must be 3–16 characters.'); + setLocalError(t('nitro.login.error.username_length', 'Habbo name must be 3–16 characters.')); return; } if(turnstileEnabled && !turnstileToken) { - setLocalError('Please complete the security check.'); + setLocalError(t('nitro.login.error.turnstile', 'Please complete the security check.')); return; } @@ -994,13 +1012,13 @@ const RegisterDialog: FC = props => const serverOk = await pingServer(); if(!serverOk) { - setLocalError('The gameserver is not running. Please try again later.'); + setLocalError(t('nitro.login.error.server_offline', 'The gameserver is not running. Please try again later.')); return; } const result = await onCheckUsername(trimmed); if(!result.available) { - setLocalError(result.error || 'This Habbo name is already taken.'); + setLocalError(result.error || t('nitro.login.error.username_taken', 'This Habbo name is already taken.')); return; } } @@ -1040,35 +1058,35 @@ const RegisterDialog: FC = props =>
- Habbo Details - + { t('nitro.login.register.title', 'Habbo Details') } +
{ step === 'credentials' &&
- Let's create your account. Enter your email and pick a password — we'll check that email isn't already in use. + { t('nitro.login.register.intro.credentials', 'Let\'s create your account. Enter your email and pick a password — we\'ll check that email isn\'t already in use.') }
{ serverOffline &&
- The gameserver isn't running right now, so new accounts can't be created. Please try again in a moment. + { t('nitro.login.server.offline.long', 'The gameserver isn\'t running right now, so new accounts can\'t be created. Please try again in a moment.') }
}
- + setEmail(e.target.value) } />
- + setPassword(e.target.value) } />
- + setConfirm(e.target.value) } />
@@ -1077,7 +1095,7 @@ const RegisterDialog: FC = props =>
1/3
@@ -1086,29 +1104,29 @@ const RegisterDialog: FC = props => { step === 'avatar' &&
- Now it's time to make your own Habbo character! To make your own Habbo, please start by choosing your Habbo Name. + { t('nitro.login.register.intro.avatar', 'Now it\'s time to make your own Habbo character! To make your own Habbo, please start by choosing your Habbo Name.') }
{ serverOffline &&
- The gameserver isn't running right now, so new accounts can't be created. Please try again in a moment. + { t('nitro.login.server.offline.long', 'The gameserver isn\'t running right now, so new accounts can\'t be created. Please try again in a moment.') }
}
- setUsername(e.target.value) } />
@@ -1156,8 +1174,10 @@ const RegisterDialog: FC = props =>
@@ -1174,10 +1194,10 @@ const RegisterDialog: FC = props => { info &&
{ info }
}
- + 2/3
@@ -1186,13 +1206,13 @@ const RegisterDialog: FC = props => { step === 'room' &&
- Last step — pick a starter room, or skip and create your own later. + { t('nitro.login.register.intro.room', 'Last step — pick a starter room, or skip and create your own later.') }
{ serverOffline &&
- The gameserver isn't running right now, so new accounts can't be created. Please try again in a moment. + { t('nitro.login.server.offline.long', 'The gameserver isn\'t running right now, so new accounts can\'t be created. Please try again in a moment.') }
} @@ -1202,12 +1222,12 @@ const RegisterDialog: FC = props => setSelectedTemplateId(null) } />
-
I'm okay — I'll create my own rooms
-
Skip for now and start with an empty hotel inventory.
+
{ t('nitro.login.register.room.skip.title', 'I\'m okay — I\'ll create my own rooms') }
+
{ t('nitro.login.register.room.skip.description', 'Skip for now and start with an empty hotel inventory.') }
- { roomTemplates === null &&
Loading rooms…
} + { roomTemplates === null &&
{ t('nitro.login.register.room.loading', 'Loading rooms…') }
} { roomTemplates !== null && roomTemplates.map(template => (