このコミットが含まれているのは:
2026-11-01 04:22:26 +09:00
コミット 29002d5393
20個のファイルの変更、419行の追加、102行の削除
+1 -1
ファイルの表示
@@ -340,7 +340,7 @@ class CountsCondition
return @definition.alias_source(root) if root.start_with?('aliases_')
return [nil, @definition.name_expression(root)]
end
return [nil, "(#{ @population.latest_sql })"] if root == 'latest'
return [nil, "(#{ @population.current_latest_sql })"] if root == 'latest'
if root == 'user' && parts.any?
expression = parts.first == 'id' ? 'v.created_by_user_id' :
'(SELECT actor.name FROM users actor WHERE actor.id = v.created_by_user_id)'
+1 -1
ファイルの表示
@@ -20,7 +20,7 @@ class CountsDefaults
details = details.transform_values do |value|
value.split(' ').flat_map do |token|
if resource == 'tags' && ['name', 'parents.name'].include?(token)
token == 'name' ? name : ["parents.#{ name }", "parents.#{ aliases }"]
token == 'name' ? name : "parents.#{ name }"
elsif resource == 'tags' && token == 'aliases'
aliases
elsif resource == 'tags' && token.start_with?('aliases.')
+10 -3
ファイルの表示
@@ -41,8 +41,7 @@ class CountsPopulation
raise ArgumentError
end
def latest_sql(row = 'v', cutoff = nil)
cutoff ||= history ? at : timestamp(params['at']) || at
def latest_at_sql(row, cutoff)
keys = definition.keys.map { "newer.#{ _1 } = #{ row }.#{ _1 }" }.join(' AND ')
bound = definition.model.connection.quote(cutoff)
<<~SQL.squish
@@ -52,6 +51,14 @@ class CountsPopulation
SQL
end
def current_latest_sql(row = 'v')
keys = definition.keys.map { "newer.#{ _1 } = #{ row }.#{ _1 }" }.join(' AND ')
<<~SQL.squish
NOT EXISTS (SELECT 1 FROM #{ definition.table } newer
WHERE #{ keys } AND newer.version_no > #{ row }.version_no)
SQL
end
def changed_sql(key)
column = definition.column(key)
"(previous.id IS NOT NULL AND NOT (v.#{ column } <=> previous.#{ column }))"
@@ -125,7 +132,7 @@ class CountsPopulation
raise ArgumentError if point > at
@scope = @scope.where("v.#{ definition.time_column } <= ?", point)
.where(Arel.sql(latest_sql('v', point))).where.not('v.event_type' => 'discard')
.where(Arel.sql(latest_at_sql('v', point))).where.not('v.event_type' => 'discard')
return
end
first = timestamp(params['from'])
+10 -8
ファイルの表示
@@ -1,5 +1,7 @@
# Common entrypoint for six version resources; legacy endpoints keep their contracts.
class CountsTableQuery
SQL_HINTS = ['MAX_EXECUTION_TIME(10000)',
"SET_VAR(optimizer_switch='semijoin=off')"].freeze
MAX_COLUMNS = 100
class TooManyColumns < ArgumentError; end
@@ -127,7 +129,7 @@ class CountsTableQuery
numbered AS (SELECT row_groups.*,
ROW_NUMBER() OVER (ORDER BY #{ order_rows.join(', ') }) AS position FROM row_groups),
group_totals AS (SELECT COUNT(*) AS group_count FROM row_groups)
SELECT /*+ MAX_EXECUTION_TIME(10000) */ cells.*,
SELECT /*+ #{ SQL_HINTS.join(' ') } */ cells.*,
page_rows.position AS counts_position, group_totals.group_count AS counts_group_count
FROM group_totals LEFT JOIN numbered page_rows
ON page_rows.position > #{ (page - 1) * limit }
@@ -175,7 +177,7 @@ class CountsTableQuery
def details
query = population.scope.select(Arel.sql(
"(#{ population.latest_sql }) AS counts_latest"))
"(#{ population.current_latest_sql }) AS counts_latest"))
order = sort_sql { detail_expression(_1) }
query = query.order(Arel.sql((order + ['v.id ASC']).join(', ')))
.limit(limit).offset((page - 1) * limit)
@@ -253,11 +255,11 @@ class CountsTableQuery
if HistoryAccess.admin? || mode == 'summary'
total = population.scope.except(:select)
.optimizer_hints('MAX_EXECUTION_TIME(10000)').count
.optimizer_hints(*SQL_HINTS).count
return @record_counts = { total:, visible: total, denied: 0 }
end
total, denied = unrestricted_population.scope.except(:select)
.optimizer_hints('MAX_EXECUTION_TIME(10000)').pick(Arel.sql('COUNT(*)'),
.optimizer_hints(*SQL_HINTS).pick(Arel.sql('COUNT(*)'),
Arel.sql("COALESCE(SUM(CASE WHEN #{ HistoryAccess.sql(@definition.model, 'v') } " +
'THEN 0 ELSE 1 END), 0)'))
# Related/previous-version predicates may differ under public authority.
@@ -267,7 +269,7 @@ class CountsTableQuery
total.to_i - denied.to_i
else
population.scope.except(:select)
.optimizer_hints('MAX_EXECUTION_TIME(10000)').count
.optimizer_hints(*SQL_HINTS).count
end
@record_counts = { total: total.to_i, denied: denied.to_i, visible: }
end
@@ -309,7 +311,7 @@ class CountsTableQuery
prefix = "WITH cells AS (#{ sql })"
@connection.select_value(<<~SQL).to_i
#{ prefix }
SELECT /*+ MAX_EXECUTION_TIME(10000) */ COUNT(*) FROM (
SELECT /*+ #{ SQL_HINTS.join(' ') } */ COUNT(*) FROM (
SELECT #{ keys.join(', ') } FROM cells GROUP BY #{ keys.join(', ') }
) output_groups
SQL
@@ -335,7 +337,7 @@ class CountsTableQuery
end
values = @connection.select_rows(<<~SQL)
#{ prefix }
SELECT /*+ MAX_EXECUTION_TIME(10000) */ DISTINCT #{ keys.join(', ') } FROM cells
SELECT /*+ #{ SQL_HINTS.join(' ') } */ DISTINCT #{ keys.join(', ') } FROM cells
ORDER BY #{ order } LIMIT #{ MAX_COLUMNS + 1 }
SQL
raise TooManyColumns if values.length > MAX_COLUMNS
@@ -395,7 +397,7 @@ class CountsTableQuery
end
def detail_expression(key)
return "(#{ population.latest_sql })" if key == 'latest'
return "(#{ population.current_latest_sql })" if key == 'latest'
return @definition.name_expression(key) if @definition.resource == 'tags' &&
@definition.name_column(key)
aliases = { 'tags' => 'tags_json', 'parents' => @definition.resource == 'posts' ?
+28
ファイルの表示
@@ -91,6 +91,34 @@ RSpec.describe 'Counts table API', type: :request do
expect(versions(query(predicate.merge('conditions.0.value' => 'true')))).to eq([3])
end
it 'distinguishes the snapshot cutoff from the current latest flag and condition' do
past = { definition: 'latest', at: '2024-01-31T16:00:00Z' }
result = query(past)
expect(versions(result)).to eq([2])
expect(result['versions'].first['latest']).to be(false)
predicate = { conditions: '', 'conditions.0.field' => 'latest',
'conditions.0.operator' => 'eq', 'conditions.0.value' => 'false' }
expect(versions(query(past.merge(predicate)))).to eq([2])
expect(versions(query(past.merge(predicate).merge('conditions.0.value' => 'true'))))
.to eq([])
unchanged = query(definition: 'latest', at: '2024-02-01T00:00:00Z')
expect(versions(unchanged)).to eq([3])
expect(unchanged['versions'].first['latest']).to be(true)
expect(versions(query(sort: 'latest:desc resource_id:asc version_no:asc'))).to eq([3, 1, 2])
end
it 'checks the current latest flag beyond the execution timestamp' do
PostVersion.create!(post: record, version_no: 4, event_type: 'update',
url: record.url, title: 'after execution', tags: '', tags_json: [],
parent_post_ids: '', created_at: Time.utc(2025, 2, 1))
result = query
expect(versions(result)).to eq([1, 2, 3])
expect(result['versions'].map { _1['latest'] }).to eq([false, false, false])
snapshot = query(definition: 'latest')
expect(versions(snapshot)).to eq([3])
expect(snapshot['versions'].first['latest']).to be(false)
end
it 'keeps ANY, ALL and NONE distinct without multiplying parent records' do
values = { conditions: '', 'conditions.0.field' => 'tags',
'conditions.0.values' => '1', condition_values: '',
+3 -3
ファイルの表示
@@ -28,7 +28,7 @@ bundle exec rspec spec/requests/counts_table_spec.rb spec/requests/counts_downlo
frontend ディレクトリで提案する対象(未実行):
```sh
npm run test:run -- src/lib/counts.test.ts src/lib/api.test.ts src/pages/CountsResourcePage.test.tsx src/pages/help/HelpChangesPage.test.tsx src/components/counts/CountsControls.test.tsx src/components/counts/CountsSummaryResults.test.tsx src/components/counts/CountsExportProgress.test.tsx
npm run test:run -- src/lib/counts.test.ts src/lib/api.test.ts src/pages/counts/CountsResourcePage.test.tsx src/pages/help/HelpChangesPage.test.tsx src/components/counts/CountsControls.test.tsx src/components/counts/CountsSummaryResults.test.tsx src/components/counts/CountsExportProgress.test.tsx
```
SSR は frontend/server.ssr.test.mjs が dist/server/entry-server.js を読み込む。
@@ -104,7 +104,7 @@ SQL の速度改善・索引追加は別 Issue とし、今回 migration は追
- frontend/src/components/counts/CountsSummaryResults.tsx:行番号コードの撤去、私的セルの
ドリルダウン抑止、公開列のソート維持。
- frontend/src/components/counts/CountsSortHeader.tsx:禁止座標のソート抑止と伏字の理由表示。
- frontend/src/pages/CountsResourcePage.tsx:専用エラーと、初回失敗後にも再実行できる状態。
- frontend/src/pages/counts/CountsResourcePage.tsx:専用エラーと、初回失敗後にも再実行できる状態。
- frontend/src/locales/ja/counts.json:承認されたエラー文言。
- frontend/src/locales/ain-Kana/counts.json:同じキーと未翻訳・日本語 fallback の管理。
@@ -121,7 +121,7 @@ SQL の速度改善・索引追加は別 Issue とし、今回 migration は追
再描画、開始受付失敗、通信断、独立期限、取消の競合・再試行、unmount、iframe 失敗。
- frontend/src/components/counts/CountsSummaryResults.test.tsx(新規):伏字・理由・コピー・旧行番号。
- frontend/src/components/counts/CountsControls.test.tsx:summary 行番号廃止と表示件数選択。
- frontend/src/pages/CountsResourcePage.test.tsx:初期エラーからの再実行、サイト名付き title。
- frontend/src/pages/counts/CountsResourcePage.test.tsx:初期エラーからの再実行、サイト名付き title。
- frontend/src/lib/api.test.ts:共有 CSRF 待機中の中断、iframe の解放・重複失敗通知。
- frontend/src/lib/counts.test.ts:理由別エラーと任意のサーバー文字列を表示しないこと。
- frontend/src/test/setup.ts:時刻が進む非同期 RAF と Canvas のテスト専用 fallback。
+1 -1
ファイルの表示
@@ -81,7 +81,7 @@ const MaterialListPage = lazyPage (() => import ('@/pages/materials/MaterialList
const MaterialNewPage = lazyPage (() => import ('@/pages/materials/MaterialNewPage'))
const MaterialSyncSuppressionsPage = lazyPage (
() => import ('@/pages/materials/MaterialSyncSuppressionsPage'))
const CountsResourcePage = lazyPage (() => import ('@/pages/CountsResourcePage'))
const CountsResourcePage = lazyPage (() => import ('@/pages/counts/CountsResourcePage'))
const MorePage = lazyPage (() => import ('@/pages/MorePage'))
const GekanatorUnavailablePage = lazyPage (() => import ('@/pages/GekanatorUnavailablePage'))
const NicoTagListPage = lazyPage (() => import ('@/pages/tags/NicoTagListPage'))
+2 -1
ファイルの表示
@@ -120,7 +120,8 @@ export const menuOutline = (
{ ...message ('entertainment'), visible: false, subMenu: [
{ ...message ('theatre'), to: '/theatres/1' },
{ ...message ('gekanator'), to: '/gekanator' }] },
{ ...message ('counts'), visible: false, subMenu: [
{ ...message ('counts'),
name: `${ message ('counts').name } (β)`, visible: false, subMenu: [
{ ...message ('posts'), to: '/counts/posts' },
{ ...message ('tags'), to: '/counts/tags' },
{ ...message ('externalTags'), to: '/counts/nico-tags' }] },
+3 -2
ファイルの表示
@@ -49,8 +49,9 @@ const CountsAxes = ({ params, update }: {
commit (next)
}
return <SortableList groups={groups} name={name} onChange={commit}
renderGroup={(id, content, over) => <fieldset className="mb-2 min-w-0">
<legend>{label (id === 'available' ? 'axisItems' : id)}</legend>
renderGroup={(id, content, over) => <fieldset className="mx-2 mb-2 min-w-0">
<legend className="mb-1 font-semibold">
{label (id === 'available' ? 'axisItems' : id)}</legend>
<div className={cn ('rounded border px-1 dark:border-slate-700',
over && 'ring-1 ring-blue-500 dark:ring-blue-400')}>{content}</div>
</fieldset>} renderItem={(id, group) => {
+1 -1
ファイルの表示
@@ -14,7 +14,7 @@
.ainu-ui-word
{
display: inline-block;
display: inline;
white-space: nowrap !important;
word-break: keep-all !important;
overflow-wrap: normal !important;
+2 -2
ファイルの表示
@@ -1,8 +1,8 @@
{
"_untranslated": ["languageNames.en"],
"languageNames": {
"ja": "シサㇺ イタㇰ",
"ain": "ヤウンモシルン アイヌ イタㇰ"
"ain": "ヤウンモシルン アイヌ イタㇰ",
"en": "イングリㇱ イタㇰ"
},
"loadingLabel": "$t(common:loading)",
"countLabel": "チピㇱキㇷ゚",
+306 -23
ファイルの表示
@@ -1,28 +1,311 @@
{
"_untranslated": [
"*",
"outputRows",
"maskedReason",
"downloadErrors",
"axisLimit",
"privateCondition",
"errors"
],
"outputRows": "行数:{{value}} 件",
"maskedReason": "閲覧権限のない履歴に由来するため、この項目の値は表示できません。集計件数には含まれます。",
"downloadErrors": {
"unavailable": "ダウンロードの認可が失われたか、ファイルの有効期限が切れています。",
"failed": "ダウンロード応答を開始できませんでした。",
"timeout": "ダウンロード応答の開始確認がタイムアウトしました。"
"titles": {
"posts": "ミンタㇻ ウコピㇱキ",
"tags": "シロシ ウコピㇱキ",
"nico_tags": "ソイタ シロシ ウコピㇱキ",
"tag_descriptions": "シロシ オルㇱペ ウコピㇱキ ネワ オルㇱペ",
"tag_groups": "シロシ ウコ イソネカ ウコピㇱキ ネワ オルㇱペ",
"materials": "チカㇻペ ウコピㇱキ ネワ オルㇱペ"
},
"axisLimit": "集計軸は最大{{max}}個です。行・列の項目を減らしてください。",
"privateCondition": "非公開情報を保護するため、この抽出条件では集計できません。",
"target": "ウコピㇱキ イアヌ",
"localeName": "レ({{language}})",
"localeNameWithScript": "レ({{language}}・{{script}})",
"localeAliases": "モㇱマ レ({{language}})",
"localeAliasesWithScript": "モㇱマ レ({{language}}・{{script}})",
"run": "ウコピㇱキ キ",
"columnLimit": {
"title": "コラム チピㇱキㇷ゚ イヨッタ ポロ ネ",
"message": "ウコピㇱキ テーブル コラム {{max}} パㇰノ アエイワンケ エアㇱカイ。コラム イヌㇺケ シンナ カㇻ ネ ヤ、イフナラ クニ ペ ポンノ カㇻ ヤン。"
},
"conditions": "ウコピㇱキ イアヌ",
"output": "アサンケ クニ オルㇱペ",
"filters": "イフナラ イアヌ",
"mode": "アヌカレ",
"exportProgress": {
"title": "エクスポート",
"format": "イソネカ:{{format}}",
"starting": "アオハシ コㇿ アン",
"queued": "アテレ コㇿ アン",
"running": "アカㇻ コㇿ アン",
"aggregating": "ウコピㇱキ コㇿ アン",
"writing": "アサンケ コㇿ アン",
"cancelling": "ソモ キ クス イカㇻ コㇿ アン",
"cancelled": "ソモ キ オケレ",
"ready": "チヌイェㇷ゚ アカㇻ オケレ・ダウンロード アオハシ(アウカオ オケレ ヤ ソモ アエラマン)",
"failed": "ハイタ",
"processed": "{{count}} ペ アカㇻ オケレ",
"measured": "{{processed}} / {{total}} ペ({{percent}}%)"
},
"displaySettings": "アヌカレ イアヌ",
"summary": "ウコピㇱキ",
"details": "オルㇱペ",
"axis": "ウコピㇱキ アㇰシㇱ",
"axes": {
"month": "チュㇷ゚ ネ ウシンナ(UTC)",
"user": "$t(history:actor)",
"tags": "シロシ ネ ウシンナ"
},
"metric": "フㇱコアンペ レコード チピㇱキㇷ゚",
"total": "フㇱコアンペ レコード チピㇱキㇷ゚:{{count}}",
"csv": "CSV",
"csvScope": "タネ アヌカレ ソㇱ ネワ アヌㇺケ コラム CSV ウン アサンケ",
"empty": "ネ フㇱコアンペ イサㇺ。",
"columns": {
"changedAt": "イカㇻ ト ヘㇺ トキ",
"version": "ウェㇽソン",
"name": "レ",
"resourceId": "Id.",
"postTitle": "レ",
"postURL": "URL",
"tags": "シロシ",
"deprecated": "エイワンケ ソモ エアㇱカイ シㇼキ",
"latest": "イヨッタ アシㇼ シロシ",
"thumbnailURL": "ポン ノカ モト URL",
"originalFrom": "ホㇱキ アオ ト ヘㇺ トキ(オㇿ ワノ)",
"originalBefore": "ホㇱキ アオ ト ヘㇺ トキ(エトコ ウン)",
"language": "イタㇰ",
"kind": "ウㇱケ オㇿ・ソイタ",
"sections": "シロシ アオ トキ ウシ",
"names": "イタㇰ ネ ウシンナ レ",
"thumbnailDisplay": "$t(shared:labels.thumbnail)"
},
"fields": {
"from": "アオハシ(エコㇿ マシン トキ・オㇿ ワノ)",
"before": "オケレ(エコㇿ マシン トキ・エトコ ウン)",
"post": "ミンタㇻ Id.",
"tag": "シロシ レ ネ ヤ Id.",
"actor": "イカㇻ クㇽ Id.",
"limit": "アヌカレ ペ チピㇱキㇷ゚"
},
"latestOnly": "イヨッタ アシㇼ ペ パテㇰ",
"truthValues": {
"true": "アニ",
"false": "ソモ"
},
"boolean": {
"true": "ルウェ ウン",
"false": "ソモ ネ"
},
"kinds": {
"internal": "ウㇱケ オㇿ シロシ",
"external": "ソイタ シロシ"
},
"nested": {
"toggle": "テーブル オㇿ テーブル アㇱマカ・アセㇱケ",
"selection": "サパ ペ OFF ネ ヤッカ ポン ペ イヌㇺケ アウカオ コㇿ アン。",
"noColumns": "アサンケ クニ オルㇱペ ソモ アヌㇺケ。",
"asJSON": "オㇱケ テーブル JSON アニ アヌカレ"
},
"sort": {
"action": "クリッㇰ コㇿ シネ ソート。Ctrl ネ ヤ タンネ アオサンケ コㇿ キー アオ・シンナ・アウㇱカ。NULL イヨㇱノ。",
"structured": "サパ テーブル オㇿ ポロンノ アン ペ アピㇱキ ワ、アウカオ ノカ イタㇰ シㇼ ネ アウコシㇼキ。Ctrl ネ ヤ タンネ アオサンケ コㇿ ポロンノ キー ソート エアㇱカイ。ポン テーブル ソート サパ テーブル ウン エラㇺシㇰ ソモ キ。",
"label": "{{column}}:{{direction}}、ホㇱキ ネ イキㇼ {{priority}}",
"none": "ソート イサㇺ",
"ascending": "ポン ペ ホㇱキ",
"descending": "ポロ ペ ホㇱキ",
"unsortedLabel": "{{column}}:ソート イサㇺ",
"singleLabel": "{{column}}:{{direction}}"
},
"notRegistered": "レ ソモ アヌイェ",
"selection": {
"table": "ウコピㇱキ ネワ オルㇱペ テーブル",
"textMode": "イタㇰ イヌㇺケ コㇿ アン",
"hint": "クリッㇰ コㇿ セル アヌㇺケ、ドラッグ ネ ヤ Shift+クリッㇰ コㇿ ウシ アヌㇺケ。タッチ ネ セル タンネ アオサンケ ワ ドラッグ。Ctrl+C/Cmd+C コㇿ コピー。アㇻㇺプヤラ ケイ コㇿ オマン、Shift+アㇻㇺプヤラ ケイ コㇿ ウシ ポロ カㇻ、Escape コㇿ アウㇱカ。",
"copy": "コピー",
"copyHeaders": "サパ レ コㇿ コピー",
"row": "イキㇼ アヌㇺケ",
"column": "コラム アヌㇺケ",
"all": "オピッタ アヌㇺケ"
},
"resize": {
"column": "{{column}} コラム サクマ シンナ。イキㇻ ドラッグ、タッチ ネ タンネ アオサンケ ワ ドラッグ。アㇻㇺプヤラ ケイ アニ カ シンナ エアㇱカイ。"
},
"latestValue": "イヨッタ アシㇼ",
"sections": {
"range": "{{begin}}~{{end}}",
"separator": "、"
},
"reorder": "{{column}}:アサンケ クニ オルㇱペ イキㇼ シンナ",
"state": {
"list": "シロシ ネ ウシンナ ミンタㇻ チピㇱキㇷ゚ イキㇼ",
"trend": "シロシ ネ ウシンナ ミンタㇻ チピㇱキㇷ゚ シンナ シㇼキ",
"details": "ネ トキ コㇿ オルㇱペ",
"history": "フㇱコアンペ ウコピㇱキ",
"historyDetails": "フㇱコアンペ オルㇱペ",
"at": "ウコピㇱキ トキ",
"boundary": "ウコピㇱキ トキ(UTC)",
"period": "トキ(UTC)",
"grain": "トキ イソネカ(UTC)",
"grains": {
"day": "ト ネ ウシンナ",
"month": "チュㇷ゚ ネ ウシンナ",
"year": "パ ネ ウシンナ"
},
"postCount": "ミンタㇻ チピㇱキㇷ゚",
"totals": {
"posts": "ミンタㇻ チピㇱキㇷ゚ オピッタ",
"tags": "ウㇱケ オㇿ シロシ チピㇱキㇷ゚ オピッタ",
"assignments": "シロシ アオ ペ チピㇱキㇷ゚ オピッタ"
}
},
"measure": "アピㇱキ ペ",
"export": "エクスポート",
"exportFormat": "エクスポート イソネカ",
"exportFormats": {
"json": "JSON",
"csv": "CSV"
},
"resources": {
"posts": "ミンタㇻ",
"tags": "シロシ",
"nico_tags": "ソイタ シロシ",
"tag_descriptions": "シロシ オルㇱペ",
"tag_groups": "シロシ ウコ イソネカ",
"materials": "チカㇻペ"
},
"latest": "イヨッタ アシㇼ",
"history": "フㇱコアンペ",
"referenceAt": "アㇱパ ト ヘㇺ トキ",
"referenceHelp": "エヌㇺケ トキ タ イヨッタ アシㇼ アン ペ アウコピㇱキ ワ アヌカレ。ソモ エヌイェ コㇿ タネ イヨッタ アシㇼ ペ アエイワンケ。",
"from": "アオハシ",
"through": "オケレ",
"axisItems": "オルㇱペ イキㇼ",
"rows": "イキㇼ",
"cols": "コラム",
"axisPlace": "{{field}} {{target}} ウン アオ",
"field": "オルㇱペ",
"operator": "オペレーター",
"add": "アオ",
"remove": "アウㇱカ",
"value": "アヌイェ ペ",
"blank": "エンプティ",
"additionalConditions": "モㇱマ イアヌ",
"changedField": "シンナ アカㇻ オルㇱペ",
"eventType": "シンナ イソネカ",
"count": "チピㇱキㇷ゚",
"outputTotal": "チピㇱキㇷ゚:{{value}} ペ",
"excludedRecords": "(アヌカㇻ エアイカㇷ゚ レコード {{count}} ペ アサンケ ソモ キ)",
"recordTotal": "アヌカㇻ エアㇱカイ ペ チピㇱキㇷ゚:{{count}}",
"grain": "トキ イソネカ",
"grains": {
"year": "パ",
"quarter": "パ 4 イキㇼ オㇿ シネ ペ",
"month": "チュㇷ゚",
"week": "シネ ウィーㇰ",
"day": "ト",
"hour": "トキ"
},
"operatorSymbols": {
"in": "IN",
"not_in": "NOT IN",
"any": "ANY",
"all": "ALL",
"none": "NONE",
"eq": "=",
"ne": "≠",
"gt": ">",
"lt": "<",
"ge": "≥",
"le": "≤",
"like": "LIKE",
"range": "~"
},
"operators": {
"in": "IN",
"not_in": "NOT IN",
"any": "アヌㇺケ ペ オㇿ シネ ペ カ アン",
"all": "アヌㇺケ ペ オピッタ アン",
"none": "アヌㇺケ ペ シネ ペ カ イサㇺ",
"eq": "ウコシノㇳ",
"ne": "ウコシノㇳ ソモ キ",
"gt": "イヨッタ ポロ",
"lt": "ポン ワ アン",
"ge": "ポロ ネ ヤ ウコシノㇳ",
"le": "ポン ネ ヤ ウコシノㇳ",
"like": "パターン",
"range": "ウシ"
},
"pattern": {
"input": "ソㇱ アヌカレ イソネカ",
"single": "シネ ペ エヌイェ",
"multiple": "ポロンノ エヌイェ",
"list": "ポロンノ アヌㇺケ イキㇼ",
"pattern": "データ パターン",
"link": "データ ウコテㇷ゚",
"columns": "アヌカレ コラム",
"value": "アエイワンケ ペ",
"selection": "イヌㇺケ"
},
"events": {
"create": "アシㇼ アカㇻ",
"update": "シンナ アカㇻ",
"discard": "アウㇱカ",
"restore": "アホシピレ"
},
"attributes": {
"sourceLanguage": "イタㇰ シンナ カㇻ モト イタㇰ",
"source_language_code": "イタㇰ シンナ カㇻ モト イタㇰ",
"source_version_no": "モト ウェㇽソン チピㇱキㇷ゚",
"event_type": "シンナ イソネカ",
"body": "ヌイェㇷ゚",
"linked_tags": "ウコテㇷ゚ シロシ",
"description": "オルㇱペ",
"members": "ウコオマレ ペ",
"placements": "イアヌ ウシ",
"parent_id": "オナ チカㇻペ",
"tag_id": "シロシ",
"tag_name": "シロシ レ",
"tag_category": "シロシ イソネカ",
"discarded_at": "アウㇱカ ト ヘㇺ トキ",
"updated_at": "フㇱコアンペ イキㇼ アシㇼ アカㇻ ト ヘㇺ トキ",
"updated_by_user_id": "フㇱコアンペ イキㇼ アシㇼ カㇻ クㇽ",
"export_paths_json": "アサンケ パㇱ",
"file_blob_id": "チヌイェㇷ゚ Id.",
"file_filename": "チヌイェㇷ゚ レ",
"file_content_type": "チヌイェㇷ゚ イソネカ",
"file_byte_size": "チヌイェㇷ゚ サイズ",
"file_checksum": "チェックサム",
"file_sha256": "SHA-256",
"source_kind": "モト イソネカ",
"source_uri": "モト URI",
"source_path": "モト パㇱ",
"source_file_id": "モト チヌイェㇷ゚ Id.",
"normalized_source_key": "モト イアヌ キー",
"parent_post_ids": "サパ ミンタㇻ",
"parent_tag_ids": "サパ シロシ",
"video_ms": "チモイェノカ トキ",
"original_created_from": "ホㇱキ アカㇻ ト ヘㇺ トキ(オㇿ ワノ)",
"original_created_before": "ホㇱキ アカㇻ ト ヘㇺ トキ(エトコ ウン)",
"deprecated_at": "エイワンケ ソモ エアㇱカイ アカㇻ ト",
"section_name": "イキㇻ レ",
"children": "ポン ペ",
"profile": "アサンケ イアヌ",
"path": "アサンケ パㇱ"
},
"rowNumberHeading": "No.",
"subtotal": "ポン ウコピㇱキ",
"singleTotal": "ウコピㇱキ オピッタ",
"grandTotal": "オピッタ ウコピㇱキ",
"rowNumbers": "イキㇼ チピㇱキㇷ゚",
"periodSeparator": "〜",
"bot": "$t(history:bot)",
"actorWithId": "{{name}} (#{{id}})",
"axisWithGrain": "{{field}}({{grain}})",
"outputRows": "イキㇼ チピㇱキㇷ゚:{{value}} ペ",
"maskedReason": "エヌカㇻ エアイカㇷ゚ フㇱコアンペ オㇿ ワ アウㇰ ペ クス、タン オルㇱペ アヌカレ エアイカㇷ゚。ウコピㇱキ チピㇱキㇷ゚ オㇿ アン。",
"downloadErrors": {
"unavailable": "ダウンロード エアㇱカイ イレンカ イサㇺ ネ ヤ、チヌイェㇷ゚ エイワンケ エアㇱカイ トキ オケレ。",
"failed": "ダウンロード ホㇱキ アオハシ カ エアイカㇷ゚。",
"timeout": "ダウンロード アオハシ アエヌカㇻ トキ オケレ。"
},
"axisLimit": "ウコピㇱキ アㇰシㇱ {{max}} パㇰノ ネ。イキㇼ ネワ コラム オルㇱペ ポンノ カㇻ ヤン。",
"privateCondition": "ソモ アヌカレ オルㇱペ アシッケウ クス、タン イフナラ イアヌ アニ ウコピㇱキ エアイカㇷ゚。",
"errors": {
"queryTimeout": "集計処理が時間制限を超えました。抽出条件や集計軸を減らして再実行してください。",
"workerUnavailable": "エクスポートワーカーが稼働していません。",
"stopped": "エクスポート処理が停止しました。再実行してください。",
"input": "入力条件が正しくありません。",
"permission": "この操作を実行する権限がありません。",
"authentication": "認証状態を確認して再実行してください。"
"queryTimeout": "ウコピㇱキ イカㇻ トキ オケレ。イフナラ イアヌ ネ ヤ ウコピㇱキ アㇰシㇱ ポンノ カㇻ ワ スイ キ ヤン。",
"workerUnavailable": "エクスポート ワーカー イカㇻ ソモ キ。",
"stopped": "エクスポート イカㇻ オケレ ソモ キ。スイ キ ヤン。",
"input": "エヌイェㇷ゚ シㇼキ ピㇼカ ソモ ネ。",
"permission": "タン イカㇻ キ クス イレンカ エコㇿ ソモ キ。",
"authentication": "エアヌカㇻ シㇼキ エヌカㇻ ワ スイ キ ヤン。"
}
}
+11 -24
ファイルの表示
@@ -1,18 +1,5 @@
{
"_untranslated": [
"exportPath",
"restoreScope",
"nameChanges.title",
"nameChanges.script",
"nameChanges.automatic",
"nameChanges.certified",
"nameChanges.otherLanguages",
"nameChanges.changed",
"nameChanges.currentReference",
"nameChanges.noNameRecord",
"nameChanges.uncertified"
],
"exportPath": "export_path",
"exportPath": "アサンケ パㇱ",
"postTitle": "$t(tags:history)",
"tagTitle": "シロシ イヌイェㇷ゚ フㇱコアンペ",
"materialTitle": "チカㇻペ フㇱコアンペ",
@@ -32,17 +19,17 @@
"confirmTitle": "アホシピレ エヌカㇻ",
"confirmPost": "「{{name}}」 ウェㇽソン {{version}} ウン アホシピレ ヤ?\nピㇼカ ヤ?",
"confirmTag": "シロシ「{{name}}」 ウェㇽソン {{version}} ウン アホシピレ ヤ?\nピㇼカ ヤ?",
"restoreScope": "名称・別名は現在表示中の言語だけを差し戻します。全言語の名称は復元しません。",
"restoreScope": "レ ネワ モㇱマ レ タネ アヌカレ イタㇰ パテㇰ アホシピレ。イタㇰ オピッタ コㇿ レ アホシピレ ソモ キ。",
"nameChanges": {
"currentReference": "現行名・参考",
"noNameRecord": "この版には名称記録なし",
"otherLanguages": "他言語情報",
"changed": "(異動あり)",
"uncertified": "(未公証)",
"title": "言語別の変更",
"script": "文字体系",
"automatic": "自動生成",
"certified": "公証済み"
"currentReference": "タネ レ・アエヌカㇻ クニ ペ",
"noNameRecord": "タン ウェㇽソン タ レ アヌイェ ペ イサㇺ",
"otherLanguages": "モㇱマ イタㇰ オルㇱペ",
"changed": "(シンナ アカㇻ)",
"uncertified": "(ナ ソモ アヌカㇻ オケレ)",
"title": "イタㇰ ネ ウシンナ シンナ アカㇻ ペ",
"script": "ヌイェ イソネカ",
"automatic": "ヤイウㇰ アカㇻ",
"certified": "(アヌカㇻ オケレ)"
},
"revert": "アホシピレ",
"restored": "アホシピレ オケレ!",
+3 -1
ファイルの表示
@@ -24,5 +24,7 @@
"settings": "イアヌ",
"legal": "イレンカ",
"terms": "エイワンケ イレンカ",
"more": "モㇱマ アン ペ »"
"more": "モㇱマ アン ペ »",
"counts": "ウコピㇱキ",
"externalTags": "ソイタ シロシ"
}
+5 -2
ファイルの表示
@@ -1,5 +1,7 @@
{
"head": { "description": "$t(posts:list.tagline)" },
"head": {
"description": "$t(posts:list.tagline)"
},
"dialogue": {
"confirm": "エヌカㇻ ワ ピㇼカ",
"close": "$t(settings:actions.close)"
@@ -56,7 +58,8 @@
"latest": "(イヨッタ アシㇼ)",
"diff": "ウシンナイペ",
"restore": "ホㇱピ",
"show": "ヌカㇻ"
"show": "ヌカㇻ",
"count": "チピㇱキㇷ゚"
},
"messages": {
"loading": "アウㇰ コㇿ アン…",
+1 -1
ファイルの表示
@@ -2,7 +2,7 @@
"languageNames": {
"ja": "日本語",
"ain": "北海道アイヌ語",
"en": "英語"
"en": "イングランド語"
},
"loadingLabel": "Loading",
"countLabel": "件数",
+5 -5
ファイルの表示
@@ -70,11 +70,11 @@
"thumbnailDisplay": "$t(shared:labels.thumbnail)"
},
"fields": {
"from": "開始(端末の時刻帯・以上)",
"before": "終了(端末の時刻帯・未満)",
"post": "投稿 ID",
"tag": "タグ名または ID",
"actor": "操作者 ID",
"from": "開始(以上)",
"before": "終了(未満)",
"post": "投稿 Id.",
"tag": "タグ名または Id.",
"actor": "操作者 Id.",
"limit": "表示件数"
},
"latestOnly": "最新のみ",
-6
ファイルの表示
@@ -1,6 +0,0 @@
import CountsResourcePage from '@/pages/CountsResourcePage'
const CountsPage = () => <CountsResourcePage resource="posts"/>
export default CountsPage
@@ -6,7 +6,7 @@ import { HelmetProvider } from 'react-helmet-async'
import { createMemoryRouter, RouterProvider, useLocation } from 'react-router-dom'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import CountsResourcePage from '@/pages/CountsResourcePage'
import CountsResourcePage from '@/pages/counts/CountsResourcePage'
import DialogueProvider from '@/components/dialogues/DialogueProvider'
import { countsKeys } from '@/lib/queryKeys'
import SortableColumns from '@/components/common/SortableColumns'
@@ -222,7 +222,8 @@ const CountsResourceContent = (
}
catch (failure)
{
if (pendingRun.current !== pending) return
if (pendingRun.current !== pending)
return
pendingRun.current = null
setRunning (false)
setRunFailure (failure)
@@ -252,7 +253,8 @@ const CountsResourceContent = (
if (key === 'measures' || key.startsWith ('measures.')) next.delete (key)
for (const [key, value] of Object.entries (extra)) next.set (key, value)
}
if (key === 'sort') next.delete ('page')
if (key === 'sort')
next.delete ('page')
setParams (next)
}
const download = async (format: 'csv' | 'json') => {
@@ -321,8 +323,11 @@ const CountsResourceContent = (
aria-expanded={expanded.has (target)} aria-controls={`counts-definition-${target}`}
onClick={() => setExpanded (previous => {
const next = new Set (previous)
if (next.has (target)) next.delete (target)
else next.add (target)
if (next.has (target))
next.delete (target)
else
next.add (target)
return next
})}>
<span aria-hidden className="inline-block" style={{
@@ -331,18 +336,22 @@ const CountsResourceContent = (
{label (`resources.${target}`)}
</button>
<AnimatedCollapse open={expanded.has (target)} id={`counts-definition-${target}`}>
<ul className="ml-3">
{(['latest', 'history'] as const).map (definition => {
const next = countsDefinitionParams (definition, params)
const active = target === resource
&& params.get ('definition') === definition
return <li key={definition}><PrefetchLink
to={`/counts/${target}?${next}`}
aria-current={active ? 'page' : undefined}
className={cn ('block py-1', active && 'font-bold underline')}>
{label (definition)}</PrefetchLink></li>
})}
</ul>
<ul className="ml-3">
{(['latest', 'history'] as const).map (definition => {
const next = countsDefinitionParams (definition, params)
const active = target === resource
&& params.get ('definition') === definition
return (
<li key={definition}>
<PrefetchLink
to={`/counts/${target}?${next}`}
aria-current={active ? 'page' : undefined}
className={cn ('block py-1', active && 'font-bold underline')}>
{label (definition)}
</PrefetchLink>
</li>)
})}
</ul>
</AnimatedCollapse>
</li>)}
</ul>