アイヌ語追加 (#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行の削除
+31 -36
ファイルの表示
@@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from 'react'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { getClientBehaviorSettings, setClientBehaviorSettings } from '@/lib/settings'
import { useUITranslation } from '@/lib/useUITranslation'
import { cn } from '@/lib/utils'
import type { FC, ReactNode } from 'react'
@@ -22,7 +23,7 @@ type Props = {
type SegmentedOption<T extends string> = {
value: T
label: string }
label: ReactNode }
type SegmentedControlProps<T extends string> = {
value: T
@@ -30,27 +31,18 @@ type SegmentedControlProps<T extends string> = {
onChange: (value: T) => void }
type SettingBlockProps = {
title: string
description?: string
title: ReactNode
description?: ReactNode
children: ReactNode }
const animationOptions: SegmentedOption<ClientAnimationMode>[] = [
{ value: 'off', label: 'なし' },
{ value: 'reduced', label: '控えめ' },
{ value: 'normal', label: '通常' }]
const animationOptions: ClientAnimationMode[] = ['off', 'reduced', 'normal']
const embedAutoLoadOptions: SegmentedOption<ClientEmbedAutoLoadMode>[] = [
{ value: 'off', label: '読み込まない' },
{ value: 'manual', label: 'クリック時' },
{ value: 'auto', label: '自動' }]
const embedAutoLoadOptions: ClientEmbedAutoLoadMode[] = ['off', 'manual', 'auto']
const linkPreloadOptions: SegmentedOption<ClientLinkPreloadMode>[] = [
{ value: 'off', label: 'しない' },
{ value: 'intent', label: 'する' }]
const linkPreloadOptions: ClientLinkPreloadMode[] = ['off', 'intent']
const tagRelationDisplayOptions: ClientTagRelationDisplayMode[] = ['flat', 'grouped']
const tagRelationDisplayOptions: SegmentedOption<ClientTagRelationDisplayMode>[] = [
{ value: 'flat', label: 'まとめない' },
{ value: 'grouped', label: 'まとめる' }]
const SegmentedControl = <T extends string,> (
{ value, options, onChange }: SegmentedControlProps<T>,
@@ -92,14 +84,13 @@ const SettingBlock: FC<SettingBlockProps> = (
</div>)
const BehaviourSettingsSection: FC<Props> = (
{ sectionClassName,
onDirtyStateChange },
) => {
const BehaviourSettingsSection: FC<Props> = props => {
const { sectionClassName, onDirtyStateChange } = props
const { label } = useUITranslation ('settings')
const [savedSettings, setSavedSettings] =
useState<ClientBehaviorSettings> (() => getClientBehaviorSettings ())
useState<ClientBehaviorSettings> ({ })
const [draftSettings, setDraftSettings] =
useState<ClientBehaviorSettings> (() => getClientBehaviorSettings ())
useState<ClientBehaviorSettings> ({ })
useEffect (() => {
const current = getClientBehaviorSettings ()
@@ -122,7 +113,7 @@ const BehaviourSettingsSection: FC<Props> = (
const next = setClientBehaviorSettings (draftSettings)
setSavedSettings (next)
setDraftSettings (next)
toast ({ title: '動作設定を保存しました' })
toast ({ title: label ('behavior.saved') })
}
const handleDiscard = () => {
@@ -141,7 +132,7 @@ const BehaviourSettingsSection: FC<Props> = (
<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.behavior')}</h2>
</div>
<div className="flex flex-wrap gap-2">
@@ -150,45 +141,49 @@ const BehaviourSettingsSection: FC<Props> = (
variant="destructive"
disabled={hasUnsavedChanges === false}
onClick={handleDiscard}>
変更を破棄
{label ('actions.discard')}
</Button>
<Button
type="button"
disabled={hasUnsavedChanges === false}
onClick={handleSave}>
保存
{label ('actions.save')}
</Button>
</div>
</div>
<div className="space-y-4">
<SettingBlock title="アニメーション">
<SettingBlock title={label ('behavior.animation')}>
<SegmentedControl
value={draftSettings.animation ?? 'normal'}
options={animationOptions}
options={animationOptions.map (value => ({ value,
label: label (`behavior.animationOptions.${value}`) }))}
onChange={value => updateDraft ('animation', value)}/>
</SettingBlock>
<SettingBlock
title="リンク先の先読み"
description="マウスを置いた時点でリンク先のデータを取得します。">
title={label ('behavior.preload')}
description={label ('behavior.preloadDescription')}>
<SegmentedControl
value={draftSettings.linkPreload ?? 'intent'}
options={linkPreloadOptions}
options={linkPreloadOptions.map (value => ({ value,
label: label (`behavior.preloadOptions.${value}`) }))}
onChange={value => updateDraft ('linkPreload', value)}/>
</SettingBlock>
<SettingBlock title="タグの親子関係表示">
<SettingBlock title={label ('behavior.tagRelations')}>
<SegmentedControl
value={draftSettings.tagRelationDisplay ?? 'grouped'}
options={tagRelationDisplayOptions}
options={tagRelationDisplayOptions.map (value => ({ value,
label: label (`behavior.relationOptions.${value}`) }))}
onChange={value => updateDraft ('tagRelationDisplay', value)}/>
</SettingBlock>
<SettingBlock title="埋め込み自動読込">
<SettingBlock title={label ('behavior.embeds')}>
<SegmentedControl
value={draftSettings.embedAutoLoad ?? 'auto'}
options={embedAutoLoadOptions}
options={embedAutoLoadOptions.map (value => ({ value,
label: label (`behavior.embedOptions.${value}`) }))}
onChange={value => updateDraft ('embedAutoLoad', value)}/>
</SettingBlock>
</div>
+21 -14
ファイルの表示
@@ -1,8 +1,6 @@
import type { FC } from 'react'
import { useState } from 'react'
import { useDialogue } from '@/components/dialogues/DialogueProvider'
import { useDialogue } from '@/lib/dialogues/useDialogue'
import { Button } from '@/components/ui/button'
import { Dialog,
DialogContent,
@@ -12,6 +10,10 @@ import { Dialog,
import { Input } from '@/components/ui/input'
import { toast } from '@/components/ui/use-toast'
import { apiPost } from '@/lib/api'
import { useUITranslation } from '@/lib/useUITranslation'
import { rememberUserCode } from '@/lib/users'
import type { FC } from 'react'
import type { User } from '@/types'
@@ -22,14 +24,17 @@ type Props = { visible: boolean
const InheritDialogue: FC<Props> = ({ visible, onVisibleChange, setUser }) => {
const dialogue = useDialogue ()
const { message, label } = useUITranslation ('settings')
const [inputCode, setInputCode] = useState ('')
const handleTransfer = async () => {
if (!(await dialogue.confirm ({
title: '引継ぎを行ってもよろしいですか?',
description: '現在のアカウントからはログアウトされます.',
confirmText: '引継ぐ',
title: label ('transfer.confirm'),
description: label ('transfer.logoutWarning'),
confirmText: label ('transfer.submit'),
cancelText: label ('actions.cancel'),
closeLabel: label ('actions.close'),
variant: 'danger' })))
return
@@ -39,31 +44,33 @@ const InheritDialogue: FC<Props> = ({ visible, onVisibleChange, setUser }) => {
'/users/verify', { code: inputCode })
if (data.valid)
{
localStorage.setItem ('user_code', inputCode)
rememberUserCode (data.user.inheritanceCode)
setUser (data.user)
toast ({ title: '引継ぎ成功!' })
toast ({ title: label ('transfer.success') })
onVisibleChange (false)
}
else
toast ({ title: '認証失敗', description: 'そのコードは使へません.' })
toast ({ title: label ('transfer.authFailed'), description: label ('transfer.invalidCode') })
}
catch
{
toast ({ title: '通信エラー', description: 'またあとで試してね.' })
toast ({ title: label ('transfer.networkError'), description: label ('transfer.retryLater') })
}
}
return (
<Dialog open={visible} onOpenChange={onVisibleChange}>
<DialogContent className="px-6 pp-6 pt-7">
<DialogContent className="px-6 pp-6 pt-7" closeLabel={label ('actions.close')}>
<DialogHeader className="pl-8">
<DialogTitle>ほかのブラウザから引継ぐ</DialogTitle>
<DialogTitle>{label ('account.inherit')}</DialogTitle>
<DialogDescription asChild>
<div className="flex gap-2">
<Input placeholder="引継ぎコードを入力"
<Input placeholder={message ('transfer.placeholder').text}
aria-label={message ('transfer.placeholder').text}
lang={message ('transfer.placeholder').language}
value={inputCode}
onChange={ev => setInputCode (ev.target.value)}/>
<Button onClick={handleTransfer}>引継ぐ</Button>
<Button onClick={handleTransfer}>{label ('transfer.submit')}</Button>
</div>
</DialogDescription>
</DialogHeader>
+31 -26
ファイルの表示
@@ -8,14 +8,15 @@ import {
isSpaceBinding,
keyBindingFromKeyboardEvent,
SHORTCUT_DEFINITIONS,
SHORTCUT_SCOPE_LABELS,
} from '@/lib/keyboardShortcuts'
import { getEffectiveKeyBindings } from '@/lib/settings'
import { useKeyboardShortcutSettings } from '@/lib/useKeyboardShortcuts'
import { useUITranslation } from '@/lib/useUITranslation'
import type { FC } from 'react'
import type { ShortcutActionId } from '@/lib/keyboardShortcuts'
import type { ClientKeyboardSettings } from '@/lib/settings'
import type { FC } from 'react'
type Props = {
sectionClassName: string
@@ -26,10 +27,9 @@ type Props = {
}
const KeyboardSettingsSection: FC<Props> = (
{ sectionClassName,
onDirtyStateChange },
) => {
const KeyboardSettingsSection: FC<Props> = props => {
const { sectionClassName, onDirtyStateChange } = props
const { label } = useUITranslation ('settings')
const {
keyboardSettings,
saveKeyboardSettings,
@@ -92,7 +92,7 @@ const KeyboardSettingsSection: FC<Props> = (
if (isSpaceBinding (binding))
{
toast ({ title: 'Space は今は割り当てられません.' })
toast ({ title: label ('shortcuts.spaceUnavailable') })
return
}
@@ -108,7 +108,7 @@ const KeyboardSettingsSection: FC<Props> = (
document.addEventListener ('keydown', onKeyDown, true)
return () => document.removeEventListener ('keydown', onKeyDown, true)
}, [capturingActionId])
}, [capturingActionId, label])
const rows = useMemo (() => SHORTCUT_DEFINITIONS.map (definition => ({
...definition,
@@ -126,7 +126,7 @@ const KeyboardSettingsSection: FC<Props> = (
const handleSave = () => {
saveKeyboardSettings (draftKeyboardSettings)
toast ({ title: 'キーボード設定を保存しました' })
toast ({ title: label ('shortcuts.saved') })
}
const handleDiscard = () => {
@@ -146,7 +146,7 @@ const KeyboardSettingsSection: FC<Props> = (
<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.keyboard')}</h2>
</div>
<div className="flex flex-wrap gap-2">
@@ -159,20 +159,20 @@ const KeyboardSettingsSection: FC<Props> = (
bindings: { },
}))
}}>
すべて既定に戻す
{label ('actions.resetAll')}
</Button>
<Button
type="button"
variant="destructive"
disabled={!(hasUnsavedChanges)}
onClick={handleDiscard}>
変更を破棄
{label ('actions.discard')}
</Button>
<Button
type="button"
disabled={!(hasUnsavedChanges) || draftConflictActionIds.size > 0}
onClick={handleSave}>
保存
{label ('actions.save')}
</Button>
</div>
</div>
@@ -187,33 +187,35 @@ const KeyboardSettingsSection: FC<Props> = (
enabled: event.target.checked,
}))
}}/>
<span>キーボード・ショートカットを有効にする</span>
<span>{label ('shortcuts.enable')}</span>
</label>
{capturingActionId != null && (
<p className="text-sm text-muted-foreground">
キー入力待ちです.Escape で取消し,Backspace / Delete で未割当にできます.
{label ('shortcuts.captureHelp')}
</p>)}
<div className="overflow-x-auto">
<table className="w-full min-w-[52rem] table-fixed border-collapse text-sm">
<thead className="border-b border-border">
<tr>
<th className="p-2 text-left">操作</th>
<th className="p-2 text-left">適用範囲</th>
<th className="p-2 text-left">現在のキー</th>
<th className="p-2 text-left">{label ('shortcuts.operation')}</th>
<th className="p-2 text-left">{label ('shortcuts.scope')}</th>
<th className="p-2 text-left">{label ('shortcuts.currentKey')}</th>
<th className="p-2 text-left"></th>
<th className="p-2 text-left"></th>
<th className="p-2 text-left">競合状態</th>
<th className="p-2 text-left">{label ('shortcuts.conflictState')}</th>
</tr>
</thead>
<tbody>
{rows.map (row => (
<tr key={row.id} className="border-b border-border/60 last:border-b-0">
<td className="p-2">{row.label}</td>
<td className="p-2">{SHORTCUT_SCOPE_LABELS[row.scope]}</td>
<td className="p-2">{label (`shortcuts.actions.${row.id}`)}</td>
<td className="p-2">{label (`shortcuts.scopes.${row.scope}`)}</td>
<td className="p-2 font-mono">
{formatKeyBinding (row.currentBinding)}
{row.currentBinding == null
? label ('shortcuts.unassigned')
: <span lang="en">{formatKeyBinding (row.currentBinding)}</span>}
</td>
<td className="p-2">
<Button
@@ -225,7 +227,8 @@ const KeyboardSettingsSection: FC<Props> = (
capturingActionId === row.id ? null : row.id,
)
}}>
{capturingActionId === row.id ? '入力待ち…' : '変更'}
{label (capturingActionId === row.id
? 'shortcuts.capturing' : 'actions.change')}
</Button>
</td>
<td className="p-2">
@@ -244,13 +247,15 @@ const KeyboardSettingsSection: FC<Props> = (
}
})
}}>
既定に戻す
{label ('actions.reset')}
</Button>
</td>
<td className="p-2">
{row.hasConflict
? <span className="text-red-600 dark:text-red-400">競合あり</span>
: 'なし'}
? <span className="text-red-600 dark:text-red-400">
{label ('shortcuts.conflicting')}
</span>
: label ('shortcuts.none')}
</td>
</tr>))}
</tbody>
+25 -32
ファイルの表示
@@ -5,19 +5,13 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
formatKeyBinding,
SHORTCUT_DEFINITIONS,
SHORTCUT_SCOPE_LABELS,
} from '@/lib/keyboardShortcuts'
import { formatKeyBinding, SHORTCUT_DEFINITIONS } from '@/lib/keyboardShortcuts'
import { useUITranslation } from '@/lib/useUITranslation'
import type {
KeyBinding,
ShortcutActionId,
ShortcutScope,
} from '@/lib/keyboardShortcuts'
import type { FC } from 'react'
import type { KeyBinding, ShortcutActionId, ShortcutScope } from '@/lib/keyboardShortcuts'
type Props = {
open: boolean
onOpenChange: (open: boolean) => void
@@ -28,16 +22,10 @@ type Props = {
}
const ShortcutHelpDialog: FC<Props> = (
{
open,
onOpenChange,
activeScope,
effectiveBindings,
conflictActionIds,
availableActionIds,
},
) => {
const ShortcutHelpDialog: FC<Props> = props => {
const { open, onOpenChange, activeScope, effectiveBindings,
conflictActionIds, availableActionIds } = props
const { label } = useUITranslation ('settings')
const rows = SHORTCUT_DEFINITIONS
.filter (definition =>
definition.scope === 'global' || definition.scope === activeScope)
@@ -45,11 +33,12 @@ const ShortcutHelpDialog: FC<Props> = (
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl px-6 pb-6 pt-7">
<DialogContent className="max-w-3xl px-6 pb-6 pt-7"
closeLabel={label ('actions.close')}>
<DialogHeader className="pl-8">
<DialogTitle>キーボード・ショートカット</DialogTitle>
<DialogTitle>{label ('shortcuts.title')}</DialogTitle>
<DialogDescription>
この画面で利用できるショートカットを表示します。
{label ('shortcuts.description')}
</DialogDescription>
</DialogHeader>
@@ -57,24 +46,28 @@ const ShortcutHelpDialog: FC<Props> = (
<table className="w-full min-w-[40rem] table-fixed border-collapse text-sm">
<thead className="border-b border-border">
<tr>
<th className="p-2 text-left">操作</th>
<th className="p-2 text-left">適用範囲</th>
<th className="p-2 text-left">キー</th>
<th className="p-2 text-left">競合</th>
<th className="p-2 text-left">{label ('shortcuts.operation')}</th>
<th className="p-2 text-left">{label ('shortcuts.scope')}</th>
<th className="p-2 text-left">{label ('shortcuts.key')}</th>
<th className="p-2 text-left">{label ('shortcuts.conflict')}</th>
</tr>
</thead>
<tbody>
{rows.map (row => (
<tr key={row.id} className="border-b border-border/60 last:border-b-0">
<td className="p-2">{row.label}</td>
<td className="p-2">{SHORTCUT_SCOPE_LABELS[row.scope]}</td>
<td className="p-2">{label (`shortcuts.actions.${row.id}`)}</td>
<td className="p-2">{label (`shortcuts.scopes.${row.scope}`)}</td>
<td className="p-2 font-mono">
{formatKeyBinding (effectiveBindings[row.id])}
{effectiveBindings[row.id] == null
? label ('shortcuts.unassigned')
: <span lang="en">{formatKeyBinding (effectiveBindings[row.id])}</span>}
</td>
<td className="p-2">
{conflictActionIds.has (row.id)
? <span className="text-red-600 dark:text-red-400">競合あり</span>
: 'なし'}
? <span className="text-red-600 dark:text-red-400">
{label ('shortcuts.conflicting')}
</span>
: label ('shortcuts.none')}
</td>
</tr>))}
</tbody>
+18 -13
ファイルの表示
@@ -1,6 +1,4 @@
import type { FC } from 'react'
import { useDialogue } from '@/components/dialogues/DialogueProvider'
import { useDialogue } from '@/lib/dialogues/useDialogue'
import { Button } from '@/components/ui/button'
import { Dialog,
DialogContent,
@@ -10,6 +8,10 @@ import { Dialog,
DialogTitle } from '@/components/ui/dialog'
import { toast } from '@/components/ui/use-toast'
import { apiPost } from '@/lib/api'
import { useUITranslation } from '@/lib/useUITranslation'
import { rememberUserCode } from '@/lib/users'
import type { FC } from 'react'
import type { User } from '@/types'
@@ -21,18 +23,21 @@ type Props = { visible: boolean
const UserCodeDialogue: FC<Props> = ({ visible, onVisibleChange, user, setUser }) => {
const dialogue = useDialogue ()
const { label } = useUITranslation ('settings')
const handleChange = async () => {
if (!(user))
return
if (!(await dialogue.confirm ({
title: '引継ぎコードを再発行しますか?',
title: label ('userCode.confirm'),
description: (
<div>
<p>再発行するとほかのブラウザからはログアウトされます.</p>
<p>{label ('userCode.logoutWarning')}</p>
</div>),
confirmText: '再発行',
confirmText: label ('userCode.confirmRenew'),
cancelText: label ('actions.cancel'),
closeLabel: label ('actions.close'),
variant: 'danger' })))
return
@@ -40,24 +45,24 @@ const UserCodeDialogue: FC<Props> = ({ visible, onVisibleChange, user, setUser }
{ headers: { 'Content-Type': 'multipart/form-data' } })
if (data.code)
{
localStorage.setItem ('user_code', data.code)
rememberUserCode (data.code)
setUser (user => ({ ...user, inheritanceCode: data.code } as User))
toast ({ title: '再発行しました.' })
toast ({ title: label ('userCode.renewed') })
}
}
return (
<Dialog open={visible} onOpenChange={onVisibleChange}>
<DialogContent className="px-6 pb-6 pt-7">
<DialogContent className="px-6 pb-6 pt-7" closeLabel={label ('actions.close')}>
<DialogHeader className="pl-8">
<DialogTitle>引継ぎコード</DialogTitle>
<DialogTitle>{label ('userCode.title')}</DialogTitle>
<DialogDescription asChild>
<div>
<p>あなたの引継ぎコードはこちらです:</p>
<p>{label ('userCode.description')}</p>
<div className="m-2">{user?.inheritanceCode}</div>
<p className="mt-1 text-sm text-destructive">
このコードはほかの人には教えないでください!
{label ('userCode.secretWarning')}
</p>
</div>
</DialogDescription>
@@ -65,7 +70,7 @@ const UserCodeDialogue: FC<Props> = ({ visible, onVisibleChange, user, setUser }
<DialogFooter>
<Button onClick={handleChange} variant="destructive">
引継ぎコード再発行
{label ('userCode.renew')}
</Button>
</DialogFooter>
</DialogContent>