141 lines
4.9 KiB
TypeScript
141 lines
4.9 KiB
TypeScript
import { useState } from 'react';
|
|
import { X } from 'lucide-react';
|
|
import { Label } from '@/components/ui/label';
|
|
import { useSettings } from '@/state/useSettings';
|
|
|
|
const LANGUAGES = [
|
|
{ code: 'en', label: 'English' },
|
|
{ code: 'pt', label: 'Portuguese' },
|
|
{ code: 'es', label: 'Spanish' },
|
|
{ code: 'fr', label: 'French' },
|
|
{ code: 'de', label: 'German' },
|
|
{ code: 'it', label: 'Italian' },
|
|
{ code: 'nl', label: 'Dutch' },
|
|
{ code: 'ru', label: 'Russian' },
|
|
{ code: 'zh', label: 'Chinese' },
|
|
{ code: 'ja', label: 'Japanese' },
|
|
{ code: 'ko', label: 'Korean' },
|
|
{ code: 'ar', label: 'Arabic' },
|
|
{ code: 'hi', label: 'Hindi' },
|
|
{ code: 'tr', label: 'Turkish' },
|
|
{ code: 'pl', label: 'Polish' },
|
|
{ code: 'sv', label: 'Swedish' },
|
|
{ code: 'da', label: 'Danish' },
|
|
{ code: 'no', label: 'Norwegian' },
|
|
{ code: 'fi', label: 'Finnish' },
|
|
{ code: 'uk', label: 'Ukrainian' },
|
|
{ code: 'cs', label: 'Czech' },
|
|
{ code: 'ro', label: 'Romanian' },
|
|
{ code: 'el', label: 'Greek' },
|
|
{ code: 'he', label: 'Hebrew' },
|
|
{ code: 'th', label: 'Thai' },
|
|
{ code: 'vi', label: 'Vietnamese' },
|
|
{ code: 'id', label: 'Indonesian' },
|
|
{ code: 'ms', label: 'Malay' },
|
|
];
|
|
|
|
const getLabel = (code: string) => LANGUAGES.find((l) => l.code === code)?.label ?? code;
|
|
|
|
export const Languages = () => {
|
|
const { settings, saveSettings } = useSettings();
|
|
const { spoken, default: defaultLang, translateTo } = settings.languages;
|
|
const [addingLang, setAddingLang] = useState('');
|
|
|
|
const save = (languages: typeof settings.languages) => {
|
|
saveSettings({ ...settings, languages });
|
|
};
|
|
|
|
const addSpoken = (code: string) => {
|
|
if (!code || spoken.includes(code)) return;
|
|
save({ ...settings.languages, spoken: [...spoken, code] });
|
|
setAddingLang('');
|
|
};
|
|
|
|
const removeSpoken = (code: string) => {
|
|
const next = spoken.filter((s) => s !== code);
|
|
const updates = { ...settings.languages, spoken: next };
|
|
if (defaultLang === code) updates.default = next[0] ?? 'en';
|
|
if (translateTo === code) updates.translateTo = next[0] ?? 'en';
|
|
save(updates);
|
|
};
|
|
|
|
const availableToAdd = LANGUAGES.filter((l) => !spoken.includes(l.code));
|
|
|
|
return (
|
|
<div className="grid gap-5">
|
|
{/* Spoken languages */}
|
|
<div className="grid gap-2">
|
|
<span className="text-sm font-medium text-duck-dark/70">Languages you speak</span>
|
|
<div className="flex flex-wrap gap-2">
|
|
{spoken.map((code) => (
|
|
<span
|
|
key={code}
|
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-duck-teal/10 text-duck-teal text-sm font-medium"
|
|
>
|
|
{getLabel(code)}
|
|
{spoken.length > 1 && (
|
|
<button
|
|
onClick={() => removeSpoken(code)}
|
|
className="hover:text-red-500 cursor-pointer transition-colors"
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
</button>
|
|
)}
|
|
</span>
|
|
))}
|
|
</div>
|
|
{availableToAdd.length > 0 && (
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<select
|
|
value={addingLang}
|
|
onChange={(ev) => addSpoken(ev.target.value)}
|
|
className="text-sm border border-duck-dark/20 rounded-md px-3 py-1.5 bg-white/60 text-duck-dark cursor-pointer"
|
|
>
|
|
<option value="">Add a language...</option>
|
|
{availableToAdd.map((l) => (
|
|
<option key={l.code} value={l.code}>
|
|
{l.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Default language */}
|
|
<Label className="grid gap-2">
|
|
<span className="text-duck-dark/70">Default language</span>
|
|
<select
|
|
value={defaultLang}
|
|
onChange={(ev) => save({ ...settings.languages, default: ev.target.value })}
|
|
className="text-sm border border-duck-dark/20 rounded-md px-3 py-2 bg-white/60 text-duck-dark cursor-pointer"
|
|
>
|
|
{spoken.map((code) => (
|
|
<option key={code} value={code}>
|
|
{getLabel(code)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<span className="text-xs text-duck-dark/40">Used for future UI localization.</span>
|
|
</Label>
|
|
|
|
{/* Translate from */}
|
|
<Label className="grid gap-2">
|
|
<span className="text-duck-dark/70">Translate to</span>
|
|
<select
|
|
value={translateTo}
|
|
onChange={(ev) => save({ ...settings.languages, translateTo: ev.target.value })}
|
|
className="text-sm border border-duck-dark/20 rounded-md px-3 py-2 bg-white/60 text-duck-dark cursor-pointer"
|
|
>
|
|
{LANGUAGES.map((l) => (
|
|
<option key={l.code} value={l.code}>
|
|
{l.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<span className="text-xs text-duck-dark/40">Target language when translating content you don't speak.</span>
|
|
</Label>
|
|
</div>
|
|
);
|
|
};
|