use dynamic translations
This commit is contained in:
parent
8d2cc0f47b
commit
5e96ab6955
7 changed files with 88 additions and 49 deletions
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 >:(
|
{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>
|
||||||
|
|
|
@ -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 >:(
|
{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
19
src/app/i18n/en.json
Normal 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..."
|
||||||
|
}
|
24
src/app/i18n/translationKeys.ts
Normal file
24
src/app/i18n/translationKeys.ts
Normal 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
|
13
src/app/i18n/useTranslations.ts
Normal file
13
src/app/i18n/useTranslations.ts
Normal 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 }
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue