このコミットが含まれているのは:
2026-10-18 19:03:02 +09:00
コミット 22f4a0af40
4個のファイルの変更、152行の追加、33行の削除
+11 -1
ファイルの表示
@@ -5,7 +5,10 @@
"nameChanges.title",
"nameChanges.script",
"nameChanges.automatic",
"nameChanges.certified"
"nameChanges.certified",
"nameChanges.otherLanguages",
"nameChanges.changed",
"nameChanges.uncertified"
],
"exportPath": "export_path",
"postTitle": "$t(tags:history)",
@@ -29,11 +32,18 @@
"confirmTag": "シロシ「{{name}}」 ウェㇽソン {{version}} ウン アホシピレ ヤ?\nピㇼカ ヤ?",
"restoreScope": "名称・別名は現在表示中の言語だけを差し戻します。全言語の名称は復元しません。",
"nameChanges": {
"otherLanguages": "他言語情報",
"changed": "(異動あり)",
"uncertified": "(未公証)",
"title": "言語別の変更",
"script": "文字体系",
"automatic": "自動生成",
"certified": "公証済み"
},
"languageNames": {
"ja": "日本語",
"ain": "ヤウンモシルン アイヌ イタㇰ"
},
"revert": "アホシピレ",
"restored": "アホシピレ オケレ!",
"restoreFailed": "アホシピレ カ エアイカㇷ゚……",
+7
ファイルの表示
@@ -21,11 +21,18 @@
"confirmTag": "タグ『{{name}}』を版 {{version}} に差戻します.\nよろしいですか?",
"restoreScope": "名称・別名は現在表示中の言語だけを差し戻します。全言語の名称は復元しません。",
"nameChanges": {
"otherLanguages": "他言語情報",
"changed": "(異動あり)",
"uncertified": "(未公証)",
"title": "言語別の変更",
"script": "文字体系",
"automatic": "自動生成",
"certified": "公証済み"
},
"languageNames": {
"ja": "日本語",
"ain": "ヤウンモシルン アイヌ イタㇰ"
},
"revert": "差戻",
"restored": "差戻しました.",
"restoreFailed": "差戻に失敗……",
+133 -32
ファイルの表示
@@ -1,9 +1,11 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect } from 'react'
import { useEffect, useId, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import LocalisedText from '@/components/LocalisedText'
import PageTitle from '@/components/common/PageTitle'
import Pagination from '@/components/common/Pagination'
import MainArea from '@/components/layout/MainArea'
@@ -12,6 +14,7 @@ import PageHead from '@/components/common/PageHead'
import { apiPut } from '@/lib/api'
import { useDialogue } from '@/lib/dialogues/useDialogue'
import { clientScrollBehaviour } from '@/lib/clientAnimation'
import { localeLanguage } from '@/lib/locales'
import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
import { fetchTagChanges } from '@/lib/tags'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
@@ -20,7 +23,60 @@ import { cn, localisedDateString } from '@/lib/utils'
import type { FC, ReactNode } from 'react'
import type { TagNameSnapshot } from '@/types'
import type { TagNameSnapshot, TagVersion } from '@/types'
const normaliseNameSnapshot = (value: unknown): TagNameSnapshot | null => {
if (value == null || typeof value !== 'object' || Array.isArray (value))
return null
const entry = value as Record<string, unknown>
const automatic = entry.autoGenerated ?? entry.auto_generated
const script = entry.scriptCode ?? entry.script_code
const aliases = Array.isArray (entry.aliases) ? entry.aliases : []
return {
name: typeof entry.name === 'string' ? entry.name : null,
scriptCode: typeof script === 'string' ? script : null,
autoGenerated: typeof automatic === 'boolean' ? automatic : null,
aliases: aliases.flatMap<TagNameSnapshot['aliases'][number]> (alias => {
if (typeof alias === 'string')
return [{ name: alias, scriptCode: null }]
if (alias == null || typeof alias !== 'object' || typeof alias.name !== 'string')
return []
const aliasScript = alias.scriptCode ?? alias.script_code
return [{ name: alias.name,
scriptCode: typeof aliasScript === 'string' ? aliasScript : null }]
}) }
}
const snapshotNames = (names: TagVersion['names']): Record<string, TagNameSnapshot> => {
try
{
const entries = typeof names === 'string' ? JSON.parse (names) : names
if (entries == null || typeof entries !== 'object' || Array.isArray (entries))
return { }
return Object.fromEntries (Object.entries (entries).flatMap (([language, value]) => {
const snapshot = normaliseNameSnapshot (value)
return snapshot == null ? [] : [[language, snapshot]]
}))
}
catch
{
return { }
}
}
const renderPrimaryName = (
snapshot: TagNameSnapshot,
language: string,
label: (key: string) => ReactNode,
) => (
<>
<span lang={language} className="inline-block whitespace-nowrap">{snapshot.name}</span>
{snapshot.name != null && snapshot.autoGenerated === true
&& label ('nameChanges.uncertified')}
</>)
const renderNameSnapshot = (
@@ -31,36 +87,30 @@ const renderNameSnapshot = (
<div className="space-y-1">
<div>
{label ('name')}:
<span lang={language} className="inline-block whitespace-nowrap">{snapshot.name}</span>
{renderPrimaryName (snapshot, language, label)}
</div>
{snapshot.scriptCode != null && (
<div>{label ('nameChanges.script')}:<code>{snapshot.scriptCode}</code></div>)}
{snapshot.aliases.length > 0 && (
<div>
{label ('tags:aliases')}:
{snapshot.aliases.map (alias => (
<span key={`${alias.name}:${alias.scriptCode}`} className="mr-2">
<span lang={language} className="inline-block whitespace-nowrap">{alias.name}</span>
{alias.scriptCode != null && <code> ({alias.scriptCode})</code>}
</span>))}
</div>)}
{snapshot.autoGenerated != null && (
<div>
{label (snapshot.autoGenerated ? 'nameChanges.automatic' : 'nameChanges.certified')}
</div>)}
</div>)
const renderDiff = (
diff: { current: string | null; prev: string | null },
format: (value: string) => ReactNode = value => value,
const renderDiff = <T,> (
diff: { current: T | null; prev: T | null },
format: (value: T) => ReactNode,
equal: (prev: T | null, current: T | null) => boolean = Object.is,
) => (
<>
{diff.prev !== diff.current
{!(equal (diff.prev, diff.current))
? (
<>
<del className="text-red-600 dark:text-red-400">
{diff.prev && <>{format (diff.prev)}<br/></>}
{diff.prev != null && diff.prev !== '' && <>{format (diff.prev)}<br/></>}
</del>
<ins className="text-green-600 dark:text-green-400">
{diff.current != null && format (diff.current)}
@@ -70,6 +120,73 @@ const renderDiff = (
</>)
const TagVersionNames = ({ change }: { change: TagVersion }) => {
const locale = useLocale ()
const language = localeLanguage (locale)
const { message, label } = useUITranslation ('history')
const { i18n } = useTranslation ('history')
const [expanded, setExpanded] = useState (false)
const panelId = useId ()
const names = snapshotNames (change.names)
const nameChanges = (change.nameChanges ?? []).map (entry => ({
...entry,
current: normaliseNameSnapshot (entry.current),
prev: normaliseNameSnapshot (entry.prev) }))
const selected = nameChanges.find (entry => entry.languageCode === language)
const legacyName = (name: string | null): TagNameSnapshot | null =>
name == null ? null : {
name, autoGenerated: null, scriptCode: null, aliases: [] }
const current =
selected != null
? selected.current
: names[language] ?? legacyName (change.name.current)
const previous =
selected != null
? selected.prev
: ((change.nameChanges != null && change.name.prev === change.name.current)
? current : legacyName (change.name.prev))
const otherLanguages = [...new Set ([...Object.keys (names),
...nameChanges.map (entry => entry.languageCode)])]
.filter (code => code !== language).sort ()
const otherChanges = nameChanges.filter (entry => entry.languageCode !== language)
return (
<>
{renderDiff ({ current, prev: previous },
snapshot => renderPrimaryName (snapshot, language, label),
(prev, next) => prev?.name === next?.name
&& prev?.autoGenerated === next?.autoGenerated)}
{otherLanguages.length > 0 && (
<div className="mt-2 text-sm">
<button type="button" className="text-left underline"
aria-expanded={expanded} aria-controls={panelId}
onClick={() => setExpanded (value => !value)}>
{label ('nameChanges.otherLanguages')}
{otherChanges.length > 0 && label ('nameChanges.changed')}
</button>
<div id={panelId} hidden={!expanded} className="mt-2 space-y-2">
{otherLanguages.map (code => {
const entry = nameChanges.find (value => value.languageCode === code)
const key = `languageNames.${code}`
const known = i18n.exists (key, { ns: 'history', lng: locale })
const snapshot = names[code]
return (
<div key={code}>
<LocalisedText language={known ? code : 'und'} className="font-semibold">
{known ? message (key).text : code}
</LocalisedText>
{entry != null
? renderDiff (entry,
value => renderNameSnapshot (value, code, label))
: snapshot != null && renderNameSnapshot (snapshot, code, label)}
</div>)
})}
</div>
</div>)}
</>)
}
const TagHistoryPage: FC = () => {
const { message, label } = useUITranslation ('history')
const dialogue = useDialogue ()
@@ -150,23 +267,7 @@ const TagHistoryPage: FC = () => {
className={cn ('even:bg-gray-100 dark:even:bg-gray-700')}>
<td className="p-2">{change.tagId}.{change.versionNo}</td>
<td className="p-2 break-all">
{renderDiff (change.name)}
{(change.nameChanges?.length ?? 0) > 0 && (
<div className="mt-2 space-y-2 text-sm">
<div className="font-semibold">{label ('nameChanges.title')}</div>
{change.nameChanges?.map (entry => (
<div key={entry.languageCode}>
<code>{entry.languageCode}</code>
{entry.prev != null && (
<div className="text-red-600 line-through dark:text-red-400">
{renderNameSnapshot (entry.prev, entry.languageCode, label)}
</div>)}
{entry.current != null && (
<div className="text-green-600 underline dark:text-green-400">
{renderNameSnapshot (entry.current, entry.languageCode, label)}
</div>)}
</div>))}
</div>)}
<TagVersionNames change={change}/>
</td>
<td className="p-2 break-all">
{renderDiff (change.category,
+1
ファイルの表示
@@ -382,6 +382,7 @@ export type TagVersion = {
tagId: number
versionNo: number
eventType: 'create' | 'update' | 'discard' | 'restore'
names?: Record<string, TagNameSnapshot> | string | null
nameChanges?: TagNameChange[]
name: { current: string | null; prev: string | null }
category: { current: Category; prev: Category | null }