profile settings keyed (425 -> 352)
ProfileSettings reaches zero: api keys, profile, voice, dock, languages and the model defaults. Language names now come from Intl.DisplayNames rather than the catalog. The runtime already knows "German" in English and "alemao" in Portuguese, so 28 names would have been 56 hand-written entries that Intl gets right for free -- including for locales not shipped yet. Same reasoning D7 gives for dates and numbers. The tag LIST stays hardcoded: which languages you may claim to speak is a product choice. ApiKeys had toLocaleDateString(undefined, ...), which followed the BROWSER locale -- the exact contradiction D7 describes. It uses useFormat().date now. Its revoked-key toggle hand-rolled a plural with a ternary on n === 1; that is a real plural entry now. Scanner: an Authorization header value like "Bearer <token>" is not a sentence. Excluded by auth-scheme prefix; verified the delta is exactly the four such credentials across both remaining areas.
This commit is contained in:
@@ -1,46 +1,67 @@
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useSettings } from 'state/useSettings';
|
||||
import { useTranslation } from '@/lib/i18n';
|
||||
import { SHIPPED_LOCALES } from '@/lib/locales';
|
||||
|
||||
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' },
|
||||
];
|
||||
/**
|
||||
* Language names come from `Intl.DisplayNames`, not from the catalog.
|
||||
*
|
||||
* The runtime already knows every language's name in every locale — "German" in English, "alemão" in
|
||||
* Portuguese — so 28 names would have been 56 hand-written catalog entries that `Intl` gets right for
|
||||
* free, including for locales we have not shipped yet. Same reasoning as D7 for dates and numbers.
|
||||
*
|
||||
* The tags themselves stay a hardcoded list: this is "which languages may I claim to speak", a product
|
||||
* choice, not something to enumerate from the runtime.
|
||||
*/
|
||||
const SPOKEN_LANGUAGE_TAGS = [
|
||||
'en',
|
||||
'pt',
|
||||
'es',
|
||||
'fr',
|
||||
'de',
|
||||
'it',
|
||||
'nl',
|
||||
'ru',
|
||||
'zh',
|
||||
'ja',
|
||||
'ko',
|
||||
'ar',
|
||||
'hi',
|
||||
'tr',
|
||||
'pl',
|
||||
'sv',
|
||||
'da',
|
||||
'no',
|
||||
'fi',
|
||||
'uk',
|
||||
'cs',
|
||||
'ro',
|
||||
'el',
|
||||
'he',
|
||||
'th',
|
||||
'vi',
|
||||
'id',
|
||||
'ms',
|
||||
] as const;
|
||||
|
||||
const getLabel = (code: string) => LANGUAGES.find((l) => l.code === code)?.label ?? code;
|
||||
/** `Intl` yields "alemão" in pt; a picker row reads better capitalized. */
|
||||
const useLanguageName = () => {
|
||||
const { locale } = useTranslation();
|
||||
return useMemo(() => {
|
||||
const names = new Intl.DisplayNames([locale], { type: 'language', fallback: 'code' });
|
||||
return (code: string) => {
|
||||
const name = names.of(code) ?? code;
|
||||
return name.charAt(0).toUpperCase() + name.slice(1);
|
||||
};
|
||||
}, [locale]);
|
||||
};
|
||||
|
||||
export const Languages = () => {
|
||||
const { settings, saveSettings } = useSettings();
|
||||
const { t } = useTranslation();
|
||||
const languageName = useLanguageName();
|
||||
const { spoken, default: defaultLang, translateTo } = settings.languages;
|
||||
const [addingLang, setAddingLang] = useState('');
|
||||
|
||||
@@ -63,7 +84,7 @@ export const Languages = () => {
|
||||
save(updates);
|
||||
};
|
||||
|
||||
const availableToAdd = LANGUAGES.filter((l) => !spoken.includes(l.code));
|
||||
const availableToAdd = SPOKEN_LANGUAGE_TAGS.filter((code) => !spoken.includes(code));
|
||||
|
||||
return (
|
||||
<div className="grid gap-5">
|
||||
@@ -76,7 +97,7 @@ export const Languages = () => {
|
||||
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)}
|
||||
{languageName(code)}
|
||||
{spoken.length > 1 && (
|
||||
<button
|
||||
onClick={() => removeSpoken(code)}
|
||||
@@ -96,9 +117,9 @@ export const Languages = () => {
|
||||
className="text-sm border border-duck-dark/20 rounded-md px-3 py-1.5 bg-background/60 text-duck-dark cursor-pointer"
|
||||
>
|
||||
<option value="">{t('settings.profile.languages.addLanguage')}</option>
|
||||
{availableToAdd.map((l) => (
|
||||
<option key={l.code} value={l.code}>
|
||||
{l.label}
|
||||
{availableToAdd.map((code) => (
|
||||
<option key={code} value={code}>
|
||||
{languageName(code)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -131,9 +152,9 @@ export const Languages = () => {
|
||||
onChange={(ev) => save({ ...settings.languages, translateTo: ev.target.value })}
|
||||
className="text-sm border border-duck-dark/20 rounded-md px-3 py-2 bg-background/60 text-duck-dark cursor-pointer"
|
||||
>
|
||||
{LANGUAGES.map((l) => (
|
||||
<option key={l.code} value={l.code}>
|
||||
{l.label}
|
||||
{SPOKEN_LANGUAGE_TAGS.map((code) => (
|
||||
<option key={code} value={code}>
|
||||
{languageName(code)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
Reference in New Issue
Block a user