アイヌ語追加 (#428) (#433)

Reviewed-on: #433
Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #433 でマージされました。
このコミットが含まれているのは:
2026-10-20 20:44:14 +09:00
committed by みてるぞ
コミット 084a090cab
294個のファイルの変更、13978行の追加、3461行の削除
+197 -170
ファイルの表示
@@ -1,23 +1,25 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useBeforeUnload, useLocation, useNavigate } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import { useBeforeUnload, useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField'
import Label from '@/components/common/Label'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import TagLink from '@/components/TagLink'
import { useDialogue } from '@/components/dialogues/DialogueProvider'
import { useDialogue } from '@/lib/dialogues/useDialogue'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import BehaviourSettingsSection from '@/components/users/BehaviourSettingsSection'
import InheritDialogue from '@/components/users/InheritDialogue'
import KeyboardSettingsSection from '@/components/users/KeyboardSettingsSection'
import UserCodeDialogue from '@/components/users/UserCodeDialogue'
import { CATEGORY_NAMES, CATEGORIES } from '@/consts'
import { SITE_TITLE } from '@/config'
import { CATEGORIES } from '@/consts'
import PageHead from '@/components/common/PageHead'
import { apiPut } from '@/lib/api'
import { i18n } from '@/lib/i18n'
import {
applyClientAppearance,
applyThemeAppearanceState,
@@ -40,8 +42,11 @@ import { useUnsavedChangesGuard } from '@/lib/useUnsavedChangesGuard'
import { useKeyboardShortcutSettings, useKeyboardShortcuts } from '@/lib/useKeyboardShortcuts'
import { cn, inputClass } from '@/lib/utils'
import { useValidationErrors } from '@/lib/useValidationErrors'
import { useUITranslation } from '@/lib/useUITranslation'
import { useHydrated } from '@/lib/useHydrated'
import { sessionKeys } from '@/lib/queryKeys'
import type { CSSProperties, Dispatch, FC, KeyboardEvent, SetStateAction } from 'react'
import type { CSSProperties, Dispatch, FC, KeyboardEvent, ReactNode, SetStateAction } from 'react'
import type { Category, Tag, User } from '@/types'
import type {
@@ -84,7 +89,6 @@ type ThemeModeItem = {
type ThemeSelectionItem = {
id: UserThemeSlotSelection
label: string
baseTheme: BuiltinThemeId
slotNo: UserThemeSlotNo }
@@ -128,7 +132,7 @@ type ThemeDraftState = {
type ThemeSegmentedOption<T extends string | number> = {
value: T
label: string }
label: ReactNode }
type ThemeSegmentedControlProps<T extends string | number> = {
value: T
@@ -136,15 +140,15 @@ type ThemeSegmentedControlProps<T extends string | number> = {
onChange: (value: T) => void }
const tabs: TabSpec[] = [
{ id: 'account', label: 'アカウント' },
{ id: 'behavior', label: '動作' },
{ id: 'theme', label: 'テーマ' },
{ id: 'keyboard', label: 'キーボード' } ]
{ id: 'account', label: 'tabs.account' },
{ id: 'behavior', label: 'tabs.behavior' },
{ id: 'theme', label: 'tabs.theme' },
{ id: 'keyboard', label: 'tabs.keyboard' }]
const themeModeSelections: ThemeModeItem[] = [
{ id: 'system', label: 'システム設定に従う' },
{ id: 'light', label: 'ライト' },
{ id: 'dark', label: 'ダーク' } ]
{ id: 'system', label: 'theme.modes.system' },
{ id: 'light', label: 'theme.modes.light' },
{ id: 'dark', label: 'theme.modes.dark' }]
const themeSlotNoSelections: ThemeSegmentedOption<UserThemeSlotNo>[] = [
{ value: 1, label: '1' },
@@ -155,20 +159,19 @@ const sectionClassName =
'space-y-4 rounded-xl p-4'
const themeSelections: ThemeSelectionItem[] = [
{ id: 'user:light:1', label: 'ライト 1', baseTheme: 'light', slotNo: 1 },
{ id: 'user:light:2', label: 'ライト 2', baseTheme: 'light', slotNo: 2 },
{ id: 'user:light:3', label: 'ライト 3', baseTheme: 'light', slotNo: 3 },
{ id: 'user:dark:1', label: 'ダーク 1', baseTheme: 'dark', slotNo: 1 },
{ id: 'user:dark:2', label: 'ダーク 2', baseTheme: 'dark', slotNo: 2 },
{ id: 'user:dark:3', label: 'ダーク 3', baseTheme: 'dark', slotNo: 3 },
]
{ id: 'user:light:1', baseTheme: 'light', slotNo: 1 },
{ id: 'user:light:2', baseTheme: 'light', slotNo: 2 },
{ id: 'user:light:3', baseTheme: 'light', slotNo: 3 },
{ id: 'user:dark:1', baseTheme: 'dark', slotNo: 1 },
{ id: 'user:dark:2', baseTheme: 'dark', slotNo: 2 },
{ id: 'user:dark:3', baseTheme: 'dark', slotNo: 3 }]
const themeTokenGroups: ThemeTokenGroupSpec[] = [
{ title: '基本',
{ title: 'theme.basic',
fields: [
{ key: 'background', label: '背景' },
{ key: 'foreground', label: '文字' },
{ key: 'link', label: 'リンク' }] }]
{ key: 'background', label: 'theme.tokens.background' },
{ key: 'foreground', label: 'theme.tokens.foreground' },
{ key: 'link', label: 'theme.tokens.link' }] }]
const ThemeSegmentedControl = <T extends string | number,> (
@@ -219,12 +222,12 @@ const previewTag = (
children: [] })
const previewTags: Record<Category, Tag> = {
deerjikist: previewTag (1, 'にじらー', 'deerjikist', 12, true, { hasDeerjikists: true }),
meme: previewTag (2, 'ミーム', 'meme', 34, true),
character: previewTag (3, 'キャラクター', 'character', 56, false),
general: previewTag (4, '一般', 'general', 78, true),
material: previewTag (5, '素材', 'material', 90, false),
meta: previewTag (6, 'メタ', 'meta', 21, true),
deerjikist: previewTag (1, '', 'deerjikist', 12, true, { hasDeerjikists: true }),
meme: previewTag (2, '', 'meme', 34, true),
character: previewTag (3, '', 'character', 56, false),
general: previewTag (4, '', 'general', 78, true),
material: previewTag (5, '', 'material', 90, false),
meta: previewTag (6, '', 'meta', 21, true),
nico: previewTag (7, 'nico:sm9', 'nico', 43, false) }
const parseTab = (search: string): SettingsTab => {
@@ -243,15 +246,23 @@ const themePreviewStyle = (tagColours: ThemeTagColours): CSSProperties =>
[`--tag-colour-${ category }`, tagColours[category]],
[`--tag-colour-${ category }-hover`, tagColours[category]]])) as CSSProperties
const themeLabelBySelection = (selection: UserThemeSlotSelection): string =>
themeSelections.find (item => item.id === selection)?.label ?? 'ライト 1'
const themeLabelBySelection = (
selection: UserThemeSlotSelection,
locale: string,
): string => {
const item = themeSelections.find (entry => entry.id === selection) ?? themeSelections[0]
return i18n.t ('settings:theme.slot', {
lng: locale,
mode: i18n.t (`settings:theme.modes.${item.baseTheme}`, { lng: locale }),
number: item.slotNo })
}
const ensureCompleteThemeSlots = (
themeSlots: UserThemeSlotMap,
): UserThemeSlotMap =>
USER_THEME_SLOT_SELECTIONS.reduce<UserThemeSlotMap> (
(complete, selection) => {
const [_, baseTheme, slotNo] = selection.split (':')
const [, baseTheme, slotNo] = selection.split (':')
complete[selection] =
themeSlots[selection]
?? getDefaultUserThemeSlot (
@@ -267,7 +278,7 @@ const comparableThemeTokensForSelection = (
themeSlots: UserThemeSlotMap,
selection: UserThemeSlotSelection,
): ThemeTokens => {
const [_, baseTheme, slotNo] = selection.split (':')
const [, baseTheme, slotNo] = selection.split (':')
const fallback =
getDefaultUserThemeSlot (
baseTheme as BuiltinThemeId,
@@ -416,91 +427,79 @@ const hexToThemeToken = (
isTopNavThemeTokenKey (key) ? value : hexToHslToken (value)
const AccountSection: FC<AccountSectionProps> = (
{ name,
onInheritOpen,
onNameChange,
onUserCodeOpen,
onUserSubmit,
user,
userBaseErrors,
userFieldErrors },
) => (
<section className={sectionClassName}>
<h2 className="text-xl font-bold">アカウント</h2>
<FieldError messages={userBaseErrors}/>
const AccountSection: FC<AccountSectionProps> = props => {
const { name, onInheritOpen, onNameChange, onUserCodeOpen,
onUserSubmit, user, userBaseErrors, userFieldErrors } = props
const { message, label } = useUITranslation ('settings')
return (
<section className={sectionClassName}>
<h2 className="text-xl font-bold">{label ('tabs.account')}</h2>
<FieldError messages={userBaseErrors}/>
<FormField label="表示名" messages={userFieldErrors.name}>
{({ describedBy, invalid }) => (
<>
<input
type="text"
aria-describedby={describedBy}
aria-invalid={invalid}
className={inputClass (invalid)}
value={name}
placeholder="名もなきニジラー"
onChange={ev => onNameChange (ev.target.value)}/>
{!(user.name) && (
<p className="mt-1 text-sm text-red-500">
名前が未設定のアカウントは 30 日間アクセスしないと削除されます!!!!
</p>)}
</>)}
</FormField>
<FormField label={label ('account.displayName')} messages={userFieldErrors.name}>
{({ describedBy, invalid }) => (
<>
<input
type="text"
aria-describedby={describedBy}
aria-invalid={invalid}
className={inputClass (invalid)}
value={name}
placeholder={message ('shared:labels.anonymous').text}
lang={message ('shared:labels.anonymous').language}
onChange={ev => onNameChange (ev.target.value)}/>
{!(user.name) && (
<p className="mt-1 text-sm text-red-500">
{label ('account.unnamedWarning')}
</p>)}
</>)}
</FormField>
<div className="flex flex-wrap gap-2">
<Button type="button" onClick={onUserSubmit}>
表示名を更新
</Button>
</div>
<div className="space-y-2">
<Label>引継ぎ</Label>
<div className="flex flex-wrap gap-2">
<Button
type="button"
onClick={onUserCodeOpen}
className="bg-gray-600 text-white"
disabled={!(user)}>
引継ぎコードを表示
</Button>
<Button
type="button"
onClick={onInheritOpen}
className="bg-red-600 text-white"
disabled={!(user)}>
ほかのブラウザから引継ぐ
<Button type="button" onClick={onUserSubmit}>
{label ('account.updateName')}
</Button>
</div>
</div>
</section>)
const ThemeSection: FC<ThemeSectionProps> = (
{ draftThemeMode,
draftActiveLightThemeSlotNo,
draftActiveDarkThemeSlotNo,
currentDisplayedThemeSlot,
selectedTheme,
hasUnsavedChanges,
settingsBaseErrors,
settingsFieldErrors,
onSelectThemeMode,
onSelectLightThemeSlotNo,
onSelectDarkThemeSlotNo,
onThemeTokenChange,
onTagColourChange,
onResetThemeSlot,
onThemeSubmit,
onThemeDiscard },
) => {
<div className="space-y-2">
<Label>{label ('account.transfer')}</Label>
<div className="flex flex-wrap gap-2">
<Button
type="button"
onClick={onUserCodeOpen}
className="bg-gray-600 text-white"
disabled={!(user)}>
{label ('account.showCode')}
</Button>
<Button
type="button"
onClick={onInheritOpen}
className="bg-red-600 text-white"
disabled={!(user)}>
{label ('account.inherit')}
</Button>
</div>
</div>
</section>)
}
const ThemeSection: FC<ThemeSectionProps> = props => {
const { draftThemeMode, draftActiveLightThemeSlotNo, draftActiveDarkThemeSlotNo,
currentDisplayedThemeSlot, selectedTheme, hasUnsavedChanges,
settingsBaseErrors, settingsFieldErrors, onSelectThemeMode,
onSelectLightThemeSlotNo, onSelectDarkThemeSlotNo, onThemeTokenChange,
onTagColourChange, onResetThemeSlot, onThemeSubmit, onThemeDiscard } = props
const { message, label } = useUITranslation ('settings')
const locale = useLocale ()
return (
<section className={sectionClassName}>
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-1">
<h2 className="text-xl font-bold">テーマ</h2>
<h2 className="text-xl font-bold">{label ('tabs.theme')}</h2>
{hasUnsavedChanges && (
<p className="text-sm text-amber-700 dark:text-amber-300">
未保存の変更があります
{label ('unsaved.title')}
</p>)}
</div>
@@ -509,7 +508,7 @@ const ThemeSection: FC<ThemeSectionProps> = (
type="button"
variant="outline"
onClick={onResetThemeSlot}>
既定に戻す
{label ('actions.reset')}
</Button>
<Button
@@ -517,13 +516,14 @@ const ThemeSection: FC<ThemeSectionProps> = (
variant="destructive"
disabled={!(hasUnsavedChanges)}
onClick={onThemeDiscard}>
変更を破棄
{label ('actions.discard')}
</Button>
<Button
type="button"
disabled={!(hasUnsavedChanges)}
onClick={onThemeSubmit}>
{`${ themeLabelBySelection (currentDisplayedThemeSlot) } に保存`}
{label ('theme.saveToSlot', {
slot: themeLabelBySelection (currentDisplayedThemeSlot, locale) })}
</Button>
</div>
</div>
@@ -534,42 +534,48 @@ const ThemeSection: FC<ThemeSectionProps> = (
<div className="space-y-3 rounded-xl p-4">
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
<div className="shrink-0">
<Label>表示モード</Label>
<Label>{label ('theme.displayMode')}</Label>
</div>
<ThemeSegmentedControl
value={draftThemeMode}
options={themeModeSelections.map (selection => ({
value: selection.id,
label: selection.label }))}
label: label (selection.label) }))}
onChange={value => onSelectThemeMode (value as ClientThemeMode)}/>
</div>
<div className="space-y-3">
<div className="grid gap-3 md:grid-cols-2">
<div className="space-y-2">
<Label>ライト時</Label>
<Label>{label ('theme.whenLight')}</Label>
<select
className={cn (inputClass (false), 'w-full')}
value={draftActiveLightThemeSlotNo}
aria-label={message ('theme.whenLight').text}
lang={message ('theme.whenLight').language}
onChange={ev =>
onSelectLightThemeSlotNo (Number (ev.target.value) as UserThemeSlotNo)}>
{themeSlotNoSelections.map (selection => (
<option key={selection.value} value={selection.value}>
{`ライト ${ selection.value }`}
{message ('theme.slot', { mode: message ('theme.modes.light').text,
number: selection.value }).text}
</option>))}
</select>
</div>
<div className="space-y-2">
<Label>ダーク時</Label>
<Label>{label ('theme.whenDark')}</Label>
<select
className={cn (inputClass (false), 'w-full')}
value={draftActiveDarkThemeSlotNo}
aria-label={message ('theme.whenDark').text}
lang={message ('theme.whenDark').language}
onChange={ev =>
onSelectDarkThemeSlotNo (Number (ev.target.value) as UserThemeSlotNo)}>
{themeSlotNoSelections.map (selection => (
<option key={selection.value} value={selection.value}>
{`ダーク ${ selection.value }`}
{message ('theme.slot', { mode: message ('theme.modes.dark').text,
number: selection.value }).text}
</option>))}
</select>
</div>
@@ -582,17 +588,19 @@ const ThemeSection: FC<ThemeSectionProps> = (
<div
key={group.title}
className="space-y-3 rounded-xl p-4">
<h3 className="text-sm font-semibold">{group.title}</h3>
<h3 className="text-sm font-semibold">{label (group.title)}</h3>
<div className="space-y-1">
{group.fields.map (field => (
<div
key={field.key}
className="flex items-center justify-between gap-4 border-b border-border/60 py-2
last:border-b-0">
<p className="text-sm font-medium">{field.label}</p>
<p className="text-sm font-medium">{label (field.label)}</p>
<div className="flex items-center gap-3">
<input
type="color"
aria-label={message (field.label).text}
lang={message (field.label).language}
value={themeTokenToHex (field.key, selectedTheme.tokens[field.key])}
onChange={ev =>
onThemeTokenChange (field.key, hexToThemeToken (field.key, ev.target.value))}
@@ -607,7 +615,7 @@ const ThemeSection: FC<ThemeSectionProps> = (
<div className="space-y-3 rounded-xl bg-background/70 p-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
<h3 className="font-medium">タグ</h3>
<h3 className="font-medium">{label ('navigation:tags')}</h3>
</div>
</div>
@@ -618,10 +626,14 @@ const ThemeSection: FC<ThemeSectionProps> = (
className="flex items-center justify-between gap-4 border-b border-border/60 py-2
last:border-b-0">
<div className="flex min-w-0 items-center gap-3">
<p className="text-sm font-medium">{CATEGORY_NAMES[category]}</p>
<p className="text-sm font-medium">
{label (`common:categories.${category}`)}
</p>
<div style={themePreviewStyle (selectedTheme.tokens.tagColours)}>
<TagLink
tag={previewTags[category]}
lang={message (`theme.previews.${category}`).language}
tag={{ ...previewTags[category],
name: message (`theme.previews.${category}`).text }}
linkFlg={false}
truncateOnMobile
withWiki={false}/>
@@ -631,6 +643,8 @@ const ThemeSection: FC<ThemeSectionProps> = (
<div className="flex items-center gap-3">
<input
type="color"
aria-label={message (`common:categories.${category}`).text}
lang={message (`common:categories.${category}`).language}
value={selectedTheme.tokens.tagColours[category]}
onChange={ev => onTagColourChange (category, ev.target.value)}
className="h-9 w-14 cursor-pointer rounded border border-border
@@ -646,31 +660,42 @@ const ThemeSection: FC<ThemeSectionProps> = (
const SettingPage: FC<Props> = ({ user, setUser }) => {
const hydrated = useHydrated ()
const { data: initialSettings } = useQuery ({
queryKey: sessionKeys.settings (user?.id ?? 0),
queryFn: fetchUserSettings, enabled: user != null })
const { data: initialThemeSlots } = useQuery ({
queryKey: sessionKeys.themeSlots (user?.id ?? 0),
queryFn: fetchUserThemeSlots, enabled: user != null })
const { message, label } = useUITranslation ('settings')
const locale = useLocale ()
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const dialogue = useDialogue ()
const { registerUnsavedChangesSource } = useUnsavedChangesGuard ()
const [inheritVsbl, setInheritVsbl] = useState (false)
const [loading, setLoading] = useState (true)
const [name, setName] = useState ('')
const [loading, setLoading] = useState (initialSettings == null)
const [name, setName] = useState (user?.name ?? '')
const [settingsError, setSettingsError] = useState<string | null> (null)
const [savedThemeMode, setSavedThemeMode] =
useState<ClientThemeMode> (getClientThemeMode ())
useState<ClientThemeMode> (initialSettings?.theme ?? 'system')
const [draftThemeMode, setDraftThemeMode] =
useState<ClientThemeMode> (getClientThemeMode ())
useState<ClientThemeMode> (initialSettings?.theme ?? 'system')
const [savedActiveLightThemeSlotNo, setSavedActiveLightThemeSlotNo] =
useState<UserThemeSlotNo> (getClientActiveLightThemeSlotNo ())
useState<UserThemeSlotNo> (1)
const [draftActiveLightThemeSlotNo, setDraftActiveLightThemeSlotNo] =
useState<UserThemeSlotNo> (getClientActiveLightThemeSlotNo ())
useState<UserThemeSlotNo> (1)
const [savedActiveDarkThemeSlotNo, setSavedActiveDarkThemeSlotNo] =
useState<UserThemeSlotNo> (getClientActiveDarkThemeSlotNo ())
useState<UserThemeSlotNo> (1)
const [draftActiveDarkThemeSlotNo, setDraftActiveDarkThemeSlotNo] =
useState<UserThemeSlotNo> (getClientActiveDarkThemeSlotNo ())
useState<UserThemeSlotNo> (1)
const [savedThemeSlots, setSavedThemeSlots] =
useState<UserThemeSlotMap> (() => ensureCompleteThemeSlots ({ }))
useState<UserThemeSlotMap> (() => ensureCompleteThemeSlots (
normaliseUserThemeSlots (initialThemeSlots ?? [])))
const [draftThemeSlots, setDraftThemeSlots] =
useState<UserThemeSlotMap> (() => ensureCompleteThemeSlots ({ }))
useState<UserThemeSlotMap> (() => ensureCompleteThemeSlots (
normaliseUserThemeSlots (initialThemeSlots ?? [])))
const [dirtyStates, setDirtyStates] =
useState<Partial<Record<SettingsTab, SettingsTabDirtyState>>> ({ })
const [userCodeVsbl, setUserCodeVsbl] = useState (false)
@@ -688,10 +713,10 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
const activeTab = useMemo<SettingsTab> (() => parseTab (location.search), [location.search])
const displayedThemeSelection = useMemo (
() => resolveDisplayedTheme ({
themeMode: draftThemeMode,
themeMode: (!hydrated && draftThemeMode === 'system') ? 'light' : draftThemeMode,
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo }, draftThemeSlots),
[draftActiveDarkThemeSlotNo,
[hydrated, draftActiveDarkThemeSlotNo,
draftActiveLightThemeSlotNo,
draftThemeMode,
draftThemeSlots])
@@ -707,12 +732,12 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
?? getDefaultUserThemeSlot (currentSelection.baseTheme, currentSelection.slotNo)
return {
id: currentSelection.baseTheme,
name: themeLabelBySelection (currentSelection.id),
name: themeLabelBySelection (currentSelection.id, locale),
builtin: false,
baseTheme: currentSelection.baseTheme,
tokens: themeSlot.tokens }
},
[displayedThemeSelection.selection, draftThemeSlots])
[displayedThemeSelection.selection, draftThemeSlots, locale])
const hasUnsavedThemeChanges = useMemo (
() =>
serialiseThemeTokens (draftThemeSlots, displayedThemeSelection.selection)
@@ -752,12 +777,13 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
}
const confirmDiscardChanges = async (
description: string,
cancelText: string,
confirmText: string,
description: ReactNode,
cancelText: ReactNode,
confirmText: ReactNode,
): Promise<boolean> =>
dialogue.confirm ({
title: '未保存の変更があります',
title: label ('unsaved.title'),
closeLabel: label ('actions.close'),
description,
confirmText,
cancelText,
@@ -811,7 +837,7 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
const discardThemeChanges = () => {
const selection = displayedThemeSelection.selection
const [_, baseTheme, slotNo] = selection.split (':')
const [, baseTheme, slotNo] = selection.split (':')
const nextThemeSlots = {
...draftThemeSlots,
[selection]: savedThemeSlots[selection]
@@ -865,9 +891,9 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
}
const confirmed = await confirmDiscardChanges (
'このタブを離れると、保存していない変更は失われます。',
'このタブに残る',
'変更を破棄して移動')
label ('unsaved.leaveTab'),
label ('unsaved.stayTab'),
label ('unsaved.discardAndMove'))
if (!(confirmed))
return
@@ -877,9 +903,9 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
const requestThemeStateChange = async (
buildNextState: (baseState: ThemeDraftState) => ThemeDraftState,
description = 'このまま切り替えると、保存していない変更は失われます。',
cancelText = 'このまま編集する',
confirmText = '変更を破棄して切り替える',
description = label ('unsaved.switchTheme'),
cancelText = label ('unsaved.keepEditing'),
confirmText = label ('unsaved.discardAndSwitch'),
): Promise<void> => {
const baseState = currentThemeDraftState ()
let themeSlots = baseState.themeSlots
@@ -894,7 +920,7 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
return
const selection = displayedThemeSelection.selection
const [_, baseTheme, slotNo] = selection.split (':')
const [, baseTheme, slotNo] = selection.split (':')
themeSlots = {
...baseState.themeSlots,
[selection]: savedThemeSlots[selection]
@@ -1032,12 +1058,12 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
{ headers: { 'Content-Type': 'multipart/form-data' } })
setUser (currentUser => ({ ...currentUser, ...data }))
setName (data.name ?? '')
toast ({ title: '表示名を更新しました.' })
toast ({ title: label ('account.nameSaved') })
}
catch (submitError)
{
applyUserValidationError (submitError)
toast ({ title: '表示名を更新できませんでした.' })
toast ({ title: label ('account.nameFailed') })
}
}
@@ -1068,12 +1094,12 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
setDraftThemeSlots (completeThemeSlots)
setCachedUserThemeSlots (completeThemeSlots)
applyClientAppearance (completeThemeSlots)
toast ({ title: 'テーマ設定を保存しました.' })
toast ({ title: label ('theme.saved') })
}
catch (submitError)
{
applySettingsValidationError (submitError)
toast ({ title: 'テーマ設定を保存できませんでした.' })
toast ({ title: label ('theme.failed') })
}
}
@@ -1132,7 +1158,7 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
if (cancelled)
return
setSettingsError ('設定の読込みに失敗しました.')
setSettingsError ('loadFailed')
}
finally
{
@@ -1222,8 +1248,8 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
draftActiveDarkThemeSlotNo,
currentDisplayedThemeSlot: displayedThemeSelection.selection,
selectedTheme,
displayedThemeLabel: themeLabelBySelection (displayedThemeSelection.selection),
systemThemeLabel: displayedThemeSelection.baseTheme === 'dark' ? 'ダーク' : 'ライト',
displayedThemeLabel: themeLabelBySelection (displayedThemeSelection.selection, locale),
systemThemeLabel: message (`theme.modes.${displayedThemeSelection.baseTheme}`).text,
hasUnsavedChanges: hasUnsavedThemeChanges,
settingsBaseErrors,
settingsFieldErrors,
@@ -1259,17 +1285,17 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
return (
<MainArea>
<Helmet>
<PageHead title={message ('title')}>
<meta name="robots" content="noindex"/>
<title>設定 | {SITE_TITLE}</title>
</Helmet>
</PageHead>
<div className="mx-auto max-w-xl space-y-4 p-4 md:hidden">
<PageTitle>設定</PageTitle>
<FieldError messages={settingsError ? [settingsError] : []}/>
<PageTitle>{label ('title')}</PageTitle>
<FieldError messages={settingsError ? [label (settingsError)] : []}/>
<div
role="tablist"
aria-label="設定区分"
aria-label={message ('tabList').text}
lang={message ('tabList').language}
aria-orientation="horizontal"
className="-mx-4 flex gap-2 overflow-x-auto px-4 pb-1">
{tabs.map (tab => (
@@ -1290,11 +1316,11 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
onClick={() => {
requestActiveTab (tab.id)
}}>
{tab.label}
{label (tab.label)}
{settingsTabErrors[tab.id] ? ' !' : ''}
</button>))}
</div>
{loading ? 'Loading...' : (
{loading ? label ('common:loading') : (
<div
id={tabPanelId (activeTab)}
role="tabpanel"
@@ -1321,7 +1347,8 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
<div className="grid w-full max-w-[58rem] grid-cols-[12rem_minmax(0,1fr)] gap-6">
<div
role="tablist"
aria-label="設定区分"
aria-label={message ('tabList').text}
lang={message ('tabList').language}
aria-orientation="vertical"
className="sticky top-4 flex h-fit flex-col gap-2">
{tabs.map (tab => (
@@ -1345,15 +1372,15 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
requestActiveTab (tab.id)
}}
onKeyDown={handleTabKeyDown}>
{tab.label}
{label (tab.label)}
{settingsTabErrors[tab.id] ? ' ⚠' : ''}
</button>))}
</div>
<div className="w-full max-w-xl justify-self-center space-y-4">
<PageTitle>設定</PageTitle>
<FieldError messages={settingsError ? [settingsError] : []}/>
{loading ? 'Loading...' : (
<PageTitle>{label ('title')}</PageTitle>
<FieldError messages={settingsError ? [label (settingsError)] : []}/>
{loading ? label ('common:loading') : (
<div
id={tabPanelId (activeTab)}
role="tabpanel"