このコミットが含まれているのは:
@@ -0,0 +1,190 @@
|
||||
import { fieldsForCounts, selectedCountChildren, sortedCountChildren } from '@/lib/counts'
|
||||
import { translateUIMessage } from '@/lib/i18n'
|
||||
import { localisedDateString, msToTime, originalCreatedAtString } from '@/lib/utils'
|
||||
|
||||
import type { CountColumn, CountField, CountRecord,
|
||||
CountsResource, CountsVersion } from '@/lib/counts'
|
||||
import type { LocaleCode } from '@/lib/locales'
|
||||
|
||||
|
||||
export const countsCellText = (
|
||||
row: CountsVersion,
|
||||
column: CountColumn,
|
||||
locale: LocaleCode,
|
||||
): string => {
|
||||
switch (column)
|
||||
{
|
||||
case 'resource_id': return String (row.resourceId)
|
||||
case 'version_no': return String (row.versionNo)
|
||||
case 'is_latest':
|
||||
return translateUIMessage (`boolean.${row.isLatest}`, 'counts', locale).text
|
||||
case 'title': return row.title ?? ''
|
||||
case 'url': return row.url ?? ''
|
||||
case 'thumbnail':
|
||||
case 'thumbnail_base': return row.thumbnailBase ?? ''
|
||||
case 'name': return row.name ?? ''
|
||||
case 'category':
|
||||
return translateUIMessage (`common:categories.${row.category}`, 'counts', locale).text
|
||||
case 'video_ms': return row.videoMs == null ? '' : msToTime (row.videoMs)
|
||||
case 'original_created_at':
|
||||
return originalCreatedAtString (
|
||||
row.originalCreatedFrom ?? null, row.originalCreatedBefore ?? null, locale)
|
||||
case 'original_created_from':
|
||||
case 'original_created_before':
|
||||
{
|
||||
const value = column === 'original_created_from'
|
||||
? row.originalCreatedFrom : row.originalCreatedBefore
|
||||
return value == null ? '' : localisedDateString (value, locale)
|
||||
}
|
||||
case 'deprecated':
|
||||
return row.deprecated == null ? '' : localisedDateString (row.deprecated, locale)
|
||||
case 'user':
|
||||
if (row.createdByUser == null)
|
||||
return translateUIMessage ('history:bot', 'counts', locale).text
|
||||
return row.createdByUser.name
|
||||
? `${row.createdByUser.name} (#${row.createdByUser.id})`
|
||||
: translateUIMessage ('history:anonymous', 'counts', locale, {
|
||||
id: row.createdByUser.id }).text
|
||||
case 'created_at': return localisedDateString (row.createdAt, locale)
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const countsCSVCell = (
|
||||
row: CountsVersion,
|
||||
column: CountColumn,
|
||||
resource: CountsResource,
|
||||
params: URLSearchParams,
|
||||
locale: LocaleCode,
|
||||
): string => {
|
||||
const field = fieldsForCounts (resource).find (field => field.key === column)!
|
||||
if (field.children != null)
|
||||
{
|
||||
const rows = row[column as keyof CountsVersion] as CountRecord[] ?? []
|
||||
return JSON.stringify (countsNestedExport (rows, field.children, column, params, locale))
|
||||
}
|
||||
return countsCellText (row, column, locale)
|
||||
}
|
||||
|
||||
|
||||
const displayNames = new Map<string, Intl.DisplayNames> ()
|
||||
|
||||
|
||||
export const countsLogicalName = (
|
||||
row: CountRecord,
|
||||
field: string,
|
||||
locale: LocaleCode,
|
||||
): { text: string; language?: string } => {
|
||||
const value = row[field]
|
||||
if (value == null) return { text: '' }
|
||||
const code = String (value)
|
||||
if (field === 'languageCode' || field === 'scriptCode')
|
||||
{
|
||||
if (field === 'languageCode' && ['ja', 'ain'].includes (code))
|
||||
return translateUIMessage (`history:languageNames.${code}`, 'counts', locale)
|
||||
const master = row[field === 'languageCode' ? 'languageName' : 'scriptName']
|
||||
if (typeof master === 'string' && master.trim () !== ''
|
||||
&& master.toLowerCase () !== code.toLowerCase ())
|
||||
return { text: master }
|
||||
try
|
||||
{
|
||||
const type = field === 'languageCode' ? 'language' : 'script'
|
||||
if (!displayNames.has (type))
|
||||
displayNames.set (type, new Intl.DisplayNames (['ja'], { type, fallback: 'none' }))
|
||||
const text = displayNames.get (type)!.of (code)
|
||||
if (text != null && text !== code) return { text, language: 'ja' }
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Invalid or unregistered codes have no logical label; never expose the code as a name.
|
||||
}
|
||||
return translateUIMessage ('notRegistered', 'counts', locale)
|
||||
}
|
||||
if (field === 'kind' || field === 'category')
|
||||
return translateUIMessage (field === 'kind' ? `kinds.${code}`
|
||||
: `common:categories.${code}`, 'counts', locale)
|
||||
if (typeof value === 'boolean')
|
||||
return translateUIMessage (`boolean.${value}`, 'counts', locale)
|
||||
if (field === 'beginMs' || field === 'endMs') return { text: msToTime (Number (value)) }
|
||||
return { text: String (value) }
|
||||
}
|
||||
|
||||
|
||||
export const countsNestedOrder = (
|
||||
rows: CountRecord[],
|
||||
path: string,
|
||||
params: URLSearchParams,
|
||||
locale: LocaleCode,
|
||||
): CountRecord[] => sortedCountChildren (rows, path, params, (row, key) =>
|
||||
['languageCode', 'scriptCode'].includes (key)
|
||||
? countsLogicalName (row, key, locale).text : row[key])
|
||||
|
||||
|
||||
export const countsNestedExport = (
|
||||
rows: CountRecord[],
|
||||
fields: CountField[],
|
||||
path: string,
|
||||
params: URLSearchParams,
|
||||
locale: LocaleCode,
|
||||
): unknown => {
|
||||
const selected = selectedCountChildren (params)
|
||||
const columns = fields.filter (field => selected.has (`${path}.${field.key}`))
|
||||
if (columns.length === 0)
|
||||
return { [translateUIMessage ('common:countLabel', 'counts', locale).text]: rows.length }
|
||||
return countsNestedOrder (rows, path, params, locale).map (row =>
|
||||
Object.fromEntries (columns.map (field => [
|
||||
translateUIMessage (field.label, 'counts', locale).text,
|
||||
field.children
|
||||
? countsNestedExport (row[field.key] as CountRecord[] ?? [], field.children,
|
||||
`${path}.${field.key}`, params, locale)
|
||||
: countsLogicalName (row, field.key, locale).text])))
|
||||
}
|
||||
|
||||
|
||||
export const countsNestedText = (
|
||||
rows: CountRecord[],
|
||||
fields: CountField[],
|
||||
path: string,
|
||||
params: URLSearchParams,
|
||||
locale: LocaleCode,
|
||||
): string => {
|
||||
const selected = selectedCountChildren (params)
|
||||
const columns = fields.filter (field => selected.has (`${path}.${field.key}`))
|
||||
if (columns.length === 0)
|
||||
return `${translateUIMessage ('common:countLabel', 'counts', locale).text}: ${rows.length}`
|
||||
return countsNestedOrder (rows, path, params, locale).map (row => columns.map (field => {
|
||||
const value = field.children
|
||||
? countsNestedText (row[field.key] as CountRecord[] ?? [], field.children,
|
||||
`${path}.${field.key}`, params, locale)
|
||||
: countsLogicalName (row, field.key, locale).text
|
||||
return `${translateUIMessage (field.label, 'counts', locale).text}: ${value}`
|
||||
}).join (' | ')).join ('\n')
|
||||
}
|
||||
|
||||
|
||||
export const countsCellCopy = (
|
||||
row: CountsVersion,
|
||||
field: CountField,
|
||||
params: URLSearchParams,
|
||||
locale: LocaleCode,
|
||||
): string => field.children
|
||||
? countsNestedText (row[field.key as keyof CountsVersion] as CountRecord[] ?? [],
|
||||
field.children, field.key, params, locale)
|
||||
: countsCellText (row, field.key as CountColumn, locale)
|
||||
|
||||
|
||||
// Historical URLs are displayed without fetching current attachments or metadata.
|
||||
export const countsImageURL = (value: string | null | undefined): string | null => {
|
||||
if (!value) return null
|
||||
try
|
||||
{
|
||||
const url = new URL (value)
|
||||
return (['https:', 'http:'].includes (url.protocol) && !url.username && !url.password)
|
||||
? url.href : null
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null
|
||||
}
|
||||
}
|
||||
新しいイシューから参照
ユーザーをブロックする