use dynamic translations

This commit is contained in:
john 2025-06-15 20:13:18 +02:00
parent 8d2cc0f47b
commit 5e96ab6955
7 changed files with 88 additions and 49 deletions

View file

@ -1,8 +1,10 @@
import { useUser } from '../../user/user.ts' import { useUser } from '../../user/user.ts'
import NavButton from '../../../components/buttons/NavButton.tsx' import NavButton from '../../../components/buttons/NavButton.tsx'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { useTranslations } from '../../i18n/useTranslations.ts'
export default function AuthNavButtons() { export default function AuthNavButtons() {
const { t } = useTranslations()
const user = useUser() const user = useUser()
const { pathname } = useLocation() const { pathname } = useLocation()
@ -15,15 +17,15 @@ export default function AuthNavButtons() {
if (loggedIn) { if (loggedIn) {
return ( return (
<> <>
<NavButton to="/logout">logout</NavButton> <NavButton to="/logout">{t('nav.logout')}</NavButton>
</> </>
) )
} else { } else {
const search = redirectQuery.toString() const search = redirectQuery.toString()
return ( return (
<> <>
<NavButton to={{ pathname: '/login', search }}>login</NavButton> <NavButton to={{ pathname: '/login', search }}>{t('nav.login')}</NavButton>
<NavButton to={{ pathname: '/signup', search }}>register</NavButton> <NavButton to={{ pathname: '/signup', search }}>{t('nav.register')}</NavButton>
</> </>
) )
} }

View file

@ -8,12 +8,15 @@ import { useUser } from '../../user/user.ts'
import NavBar from '../../../components/NavBar.tsx' import NavBar from '../../../components/NavBar.tsx'
import NavButton from '../../../components/buttons/NavButton.tsx' import NavButton from '../../../components/buttons/NavButton.tsx'
import LinkButton from '../../../components/buttons/LinkButton.tsx' import LinkButton from '../../../components/buttons/LinkButton.tsx'
import { useTranslations } from '../../i18n/useTranslations.ts'
interface LoginPageProps { interface LoginPageProps {
authService: AuthService authService: AuthService
} }
export default function LoginPage({ authService }: LoginPageProps) { export default function LoginPage({ authService }: LoginPageProps) {
const { t } = useTranslations()
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const [username, setUsername] = useState('') const [username, setUsername] = useState('')
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
@ -62,7 +65,7 @@ export default function LoginPage({ authService }: LoginPageProps) {
<SingleColumnLayout <SingleColumnLayout
navbar={ navbar={
<NavBar> <NavBar>
<NavButton to={'/'}>home</NavButton> <NavButton to={'/'}>{t('nav.home')}</NavButton>
</NavBar> </NavBar>
} }
> >
@ -71,7 +74,7 @@ export default function LoginPage({ authService }: LoginPageProps) {
<form className="flex flex-col gap-4 max-w-md" onSubmit={onSubmit}> <form className="flex flex-col gap-4 max-w-md" onSubmit={onSubmit}>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label htmlFor="username" className="text-sm text-gray-600"> <label htmlFor="username" className="text-sm text-gray-600">
username {t('auth.username.label')}
</label> </label>
<TextInput <TextInput
ref={usernameInputRef} ref={usernameInputRef}
@ -84,7 +87,7 @@ export default function LoginPage({ authService }: LoginPageProps) {
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label htmlFor="password" className="text-sm text-gray-600"> <label htmlFor="password" className="text-sm text-gray-600">
password {t('auth.password.label')}
</label> </label>
<TextInput <TextInput
ref={passwordInputRef} ref={passwordInputRef}
@ -105,16 +108,16 @@ export default function LoginPage({ authService }: LoginPageProps) {
className="h-4 w-4" className="h-4 w-4"
/> />
<label htmlFor="rememberMe" className="text-sm text-gray-600"> <label htmlFor="rememberMe" className="text-sm text-gray-600">
DONT log me out &gt;:( {t('auth.remember_me.label')}
</label> </label>
</div> </div>
<Button className="mt-4" disabled={isSubmitting} type="submit"> <Button className="mt-4" disabled={isSubmitting} type="submit">
{isSubmitting ? 'wait...' : 'make login pls'} {isSubmitting ? t('misc.loading') : t('auth.login.cta')}
</Button> </Button>
<LinkButton secondary to={{ pathname: '/signup', search: window.location.search }}> <LinkButton secondary to={{ pathname: '/signup', search: window.location.search }}>
register instead? {t('auth.login.register_instead')}
</LinkButton> </LinkButton>
<span className="text-xs h-3 text-red-500">{error}</span> <span className="text-xs h-3 text-red-500">{error}</span>

View file

@ -3,12 +3,12 @@ import { useEffect, useRef, useState, FormEvent, useCallback, Ref } from 'react'
import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx' import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx'
import TextInput from '../../../components/inputs/TextInput.tsx' import TextInput from '../../../components/inputs/TextInput.tsx'
import Button from '../../../components/buttons/Button.tsx' import Button from '../../../components/buttons/Button.tsx'
import AnchorButton from '../../../components/buttons/AnchorButton.tsx'
import { invalid, valid, Validation } from '../../../utils/validation.ts' import { invalid, valid, Validation } from '../../../utils/validation.ts'
import { AuthService } from '../authService.ts' import { AuthService } from '../authService.ts'
import LinkButton from '../../../components/buttons/LinkButton.tsx' import LinkButton from '../../../components/buttons/LinkButton.tsx'
import NavBar from '../../../components/NavBar.tsx' import NavBar from '../../../components/NavBar.tsx'
import NavButton from '../../../components/buttons/NavButton.tsx' import NavButton from '../../../components/buttons/NavButton.tsx'
import { useTranslations } from '../../i18n/useTranslations.ts'
const SignupCodeKey = 'signupCode' const SignupCodeKey = 'signupCode'
@ -17,6 +17,7 @@ interface SignupPageProps {
} }
export default function SignupPage({ authService }: SignupPageProps) { export default function SignupPage({ authService }: SignupPageProps) {
const { t } = useTranslations()
const { code } = useParams() const { code } = useParams()
const [signupCode, setSignupCode] = useState<string | null>(null) const [signupCode, setSignupCode] = useState<string | null>(null)
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
@ -31,8 +32,6 @@ export default function SignupPage({ authService }: SignupPageProps) {
const userNameInputRef = useRef<HTMLInputElement | null>(null) const userNameInputRef = useRef<HTMLInputElement | null>(null)
const passwordInputRef = useRef<HTMLInputElement | null>(null) const passwordInputRef = useRef<HTMLInputElement | null>(null)
const dialogRef = useRef<HTMLDialogElement | null>(null)
const navigate = useNavigate() const navigate = useNavigate()
useEffect(() => { useEffect(() => {
@ -47,10 +46,6 @@ export default function SignupPage({ authService }: SignupPageProps) {
theSignupCode = localStorage.getItem(SignupCodeKey) theSignupCode = localStorage.getItem(SignupCodeKey)
setSignupCode(theSignupCode) setSignupCode(theSignupCode)
} }
if (!theSignupCode) {
dialogRef.current?.showModal()
}
}, [code, signupCode]) }, [code, signupCode])
useEffect(() => {}, [signupCode]) useEffect(() => {}, [signupCode])
@ -94,7 +89,7 @@ export default function SignupPage({ authService }: SignupPageProps) {
<SingleColumnLayout <SingleColumnLayout
navbar={ navbar={
<NavBar> <NavBar>
<NavButton to={'/'}>home</NavButton> <NavButton to={'/'}>{t('nav.home')}</NavButton>
</NavBar> </NavBar>
} }
> >
@ -103,6 +98,7 @@ export default function SignupPage({ authService }: SignupPageProps) {
<form className="flex flex-col gap-4 max-w-md" onSubmit={onSubmit}> <form className="flex flex-col gap-4 max-w-md" onSubmit={onSubmit}>
<FormInput <FormInput
id="username" id="username"
label={t('auth.username.label')}
value={username} value={username}
onInput={setUsername} onInput={setUsername}
error={usernameError} error={usernameError}
@ -111,13 +107,14 @@ export default function SignupPage({ authService }: SignupPageProps) {
<FormInput <FormInput
id="password" id="password"
label={t('auth.password.label')}
value={password} value={password}
onInput={setPassword} onInput={setPassword}
error={passwordError} error={passwordError}
type="password" type="password"
ref={passwordInputRef} ref={passwordInputRef}
/> />
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center hidden gap-2 mt-2">
<input <input
type="checkbox" type="checkbox"
id="rememberMe" id="rememberMe"
@ -126,7 +123,7 @@ export default function SignupPage({ authService }: SignupPageProps) {
className="h-4 w-4" className="h-4 w-4"
/> />
<label htmlFor="rememberMe" className="text-sm text-gray-600"> <label htmlFor="rememberMe" className="text-sm text-gray-600">
DONT log me out &gt;:( {t('auth.remember_me.label')}
</label> </label>
</div> </div>
<Button <Button
@ -134,44 +131,23 @@ export default function SignupPage({ authService }: SignupPageProps) {
disabled={isSubmitting || !!usernameError || !!passwordError} disabled={isSubmitting || !!usernameError || !!passwordError}
type="submit" type="submit"
> >
{isSubmitting ? 'wait...' : 'give me an account pls'} {isSubmitting ? t('misc.loading') : t('auth.register.cta')}
</Button> </Button>
<LinkButton secondary to={'/login'}> <LinkButton secondary to={'/login'}>
login instead? {t('auth.register.login_instead')}
</LinkButton> </LinkButton>
<span className="text-xs h-3 text-red-500">{error}</span> <span className="text-xs h-3 text-red-500">{error}</span>
</form> </form>
</div> </div>
</main> </main>
<dialog
id="go-away-dialog"
ref={dialogRef}
className="p-6 rounded-lg shadow-lg m-auto outline-none"
>
<div className="text-gray-600 flex flex-col gap-2">
<h1 className={`font-bold text-lg`}>STOP !!!</h1>
<p>You need an invitation to sign up</p>
<p>
I'm surprised you even found your way here without one and honestly I'd prefer it if you
would leave
</p>
<p>
If you <span className="italic">do</span> want to create an account, you should know who
to contact
</p>
<AnchorButton className={`mt-4`} href="https://en.wikipedia.org/wiki/Special:Random">
I'm sorry I'll go somewhere else :(
</AnchorButton>
</div>
</dialog>
</SingleColumnLayout> </SingleColumnLayout>
) )
} }
interface FormInputProps { interface FormInputProps {
id: string id: string
label: string
value: string value: string
onInput: (value: string) => void onInput: (value: string) => void
error: string | null error: string | null
@ -179,11 +155,11 @@ interface FormInputProps {
ref: Ref<HTMLInputElement> ref: Ref<HTMLInputElement>
} }
function FormInput({ id, value, onInput, error, type = 'text', ref }: FormInputProps) { function FormInput({ id, label, value, onInput, error, type = 'text', ref }: FormInputProps) {
return ( return (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label htmlFor={id} className="text-sm text-gray-600"> <label htmlFor={id} className="text-sm text-gray-600">
{id} {label}
</label> </label>
<TextInput ref={ref} type={type} id={id} value={value} onInput={onInput} /> <TextInput ref={ref} type={type} id={id} value={value} onInput={onInput} />
<div className="text-xs h-3 text-red-500">{error}</div> <div className="text-xs h-3 text-red-500">{error}</div>

19
src/app/i18n/en.json Normal file
View file

@ -0,0 +1,19 @@
{
"nav.home": "home",
"nav.login": "login",
"nav.register": "register",
"nav.admin": "admin",
"auth.login.cta": "login",
"auth.login.register_instead": "register instead?",
"auth.register.cta": "signup",
"auth.register.login_instead": "login instead?",
"auth.username.label": "username",
"auth.password.label": "password",
"auth.remember_me.label": "stay logged in",
"misc.loading": "wait...",
"nav.logout": "logout",
"post.add_media.cta": "+ add media",
"post.public.label": "public",
"post.submit.cta": "post",
"post.editor.placeholder": "write something..."
}

View file

@ -0,0 +1,24 @@
export interface Translations {
'auth.login.cta': string
'auth.login.register_instead': string
'auth.password.label': string
'auth.register.cta': string
'auth.register.login_instead': string
'auth.remember_me.label': string
'auth.username.label': string
'misc.loading': string
'nav.admin': string
'nav.home': string
'nav.login': string
'nav.logout': string
'nav.register': string
'post.add_media.cta': string
'post.editor.placeholder': string
'post.public.label': string
'post.submit.cta': string
}
export type TranslationKey = keyof Translations

View file

@ -0,0 +1,13 @@
import { TranslationKey, Translations } from './translationKeys.ts'
import en from './en.json' assert { type: 'json' }
export function useTranslations() {
// TODO somehow handle other languages (reactively)
const texts = en as Translations
function getText<K extends TranslationKey>(key: K): Translations[K] {
return texts[key] ?? key
}
return { t: getText }
}

View file

@ -3,6 +3,7 @@ import FancyTextEditor, { TextInputKeyDownEvent } from './inputs/FancyTextEditor
import Button from './buttons/Button.tsx' import Button from './buttons/Button.tsx'
import { openFileDialog } from '../utils/openFileDialog.ts' import { openFileDialog } from '../utils/openFileDialog.ts'
import makePica from 'pica' import makePica from 'pica'
import { useTranslations } from '../app/i18n/useTranslations.ts'
interface NewPostWidgetProps { interface NewPostWidgetProps {
onSubmit: ( onSubmit: (
@ -22,6 +23,7 @@ interface Attachment {
} }
export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPostWidgetProps) { export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPostWidgetProps) {
const { t } = useTranslations()
const [content, setContent] = useState('') const [content, setContent] = useState('')
const [attachments, setAttachments] = useState<Attachment[]>([]) const [attachments, setAttachments] = useState<Attachment[]>([])
const [isPublic, setIsPublic] = useState(false) const [isPublic, setIsPublic] = useState(false)
@ -72,7 +74,7 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos
onInput={onContentInput} onInput={onContentInput}
onKeyDown={onInputKeyDown} onKeyDown={onInputKeyDown}
className="mb-3" className="mb-3"
placeholder="write something..." placeholder={t('post.editor.placeholder')}
/> />
{attachments.length > 0 && ( {attachments.length > 0 && (
@ -93,7 +95,7 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos
<div className="flex justify-between items-center pt-2"> <div className="flex justify-between items-center pt-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button secondary onClick={onAddMediaClicked}> <Button secondary onClick={onAddMediaClicked}>
+ add media {t('post.add_media.cta')}
</Button> </Button>
<label className="flex items-center gap-1 cursor-pointer"> <label className="flex items-center gap-1 cursor-pointer">
<input <input
@ -103,14 +105,14 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos
disabled={isSubmitting} disabled={isSubmitting}
className="form-checkbox h-4 w-4 text-blue-600" className="form-checkbox h-4 w-4 text-blue-600"
/> />
<span className="text-primary-500">public</span> <span className="text-primary-500">{t('post.public.label')}</span>
</label> </label>
</div> </div>
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
disabled={isSubmitting || (content.trim() === '' && attachments.length === 0)} disabled={isSubmitting || (content.trim() === '' && attachments.length === 0)}
> >
post {t('post.submit.cta')}
</Button> </Button>
</div> </div>
</div> </div>