このコミットが含まれているのは:
2026-10-25 14:53:35 +09:00
コミット d411b3b40f
10個のファイルの変更、528行の追加、218行の削除
+24 -1
ファイルの表示
@@ -1,9 +1,30 @@
import { describe, expect, it } from 'vitest'
import { columnsForCounts, countsCSV } from '@/lib/counts'
import { columnsForCounts, countColumns, countsCSV, normaliseCountsParams } from '@/lib/counts'
import { createUII18n, translateUIMessage } from '@/lib/i18n'
describe ('counts exports', () => {
it ('resolves every output and CSV header key in both locales', () => {
for (const locale of ['ja', 'ain-Kana'])
{
const instance = createUII18n (locale)
for (const key of Object.values (countColumns))
{
const translated = translateUIMessage (key, 'counts', locale, { }, instance)
expect (translated.text).not.toBe (key)
expect (translated.text).not.toContain ('counts:')
}
const fallback = translateUIMessage ('run', 'counts', locale, { }, instance)
expect (fallback).toEqual ({ text: '集計実行', language: 'ja' })
}
})
it ('makes the monthly default explicit for the existing API', () => {
expect (normaliseCountsParams ('posts', new URLSearchParams ()).get ('axis'))
.toBe ('month')
})
it ('escapes quotes, newlines and spreadsheet formulas in UTF-8 CSV', () => {
expect (countsCSV ([['日本語', 'a"b', 'a\nb', '=1+1']]))
.toBe ('\uFEFF"日本語","a""b","a\nb","\'=1+1"')
@@ -14,5 +35,7 @@ describe ('counts exports', () => {
expect (columnsForCounts ('posts')).not.toContain ('aliases')
expect (columnsForCounts ('tags')).toContain ('aliases')
expect (columnsForCounts ('tags')).not.toContain ('url')
expect (columnsForCounts ('posts')).not.toContain ('event_type')
expect (columnsForCounts ('tags')).not.toContain ('event_type')
})
})
+30 -6
ファイルの表示
@@ -13,22 +13,46 @@ export type CountsResult = {
versions?: (PostVersion | TagVersion)[] }
export const countColumns = {
created_at: '変更日時', resource_id: 'ID', version_no: '版番号',
event_type: '操作種別', user: '操作者', title: '投稿タイトル', url: '投稿 URL',
tags: '記載タグ', added_tags: '追加されたタグ', removed_tags: '削除されたタグ',
name: 'タグ名', category: 'カテゴリ', aliases: '別名', parents: '上位タグ',
deprecated: '廃止状態' }
created_at: 'counts:columns.changedAt', resource_id: 'counts:columns.resourceId',
version_no: 'history:version', user: 'history:actor',
title: 'counts:columns.postTitle', url: 'counts:columns.postURL',
tags: 'counts:columns.tags', added_tags: 'counts:columns.addedTags',
removed_tags: 'counts:columns.removedTags', name: 'history:tagName',
category: 'common:categoryLabel', aliases: 'tags:aliases', parents: 'tags:parents',
deprecated: 'counts:columns.deprecated' }
export type CountColumn = keyof typeof countColumns
export const columnsForCounts = (resource: CountsResource): CountColumn[] => [
'created_at', 'resource_id', 'version_no', 'event_type', 'user',
'created_at', 'resource_id', 'version_no', 'user',
...(resource === 'posts'
? ['title', 'url', 'tags', 'added_tags', 'removed_tags'] as const
: ['name', 'category', 'aliases', 'parents', 'deprecated'] as const)]
export const normaliseCountsParams = (
resource: CountsResource,
source: URLSearchParams,
): URLSearchParams => {
const params = new URLSearchParams (source)
params.delete ('event_type')
if (!['month', 'user'].includes (params.get ('axis') ?? ''))
params.set ('axis', 'month')
if (params.has ('columns'))
{
const available = columnsForCounts (resource)
const columns = params.get ('columns')!.split (',')
.filter (column => available.includes (column as CountColumn))
if (columns.length > 0)
params.set ('columns', [...new Set (columns)].join (','))
else
params.delete ('columns')
}
return params
}
export const fetchCounts = (
resource: CountsResource,
params: URLSearchParams,