@aiquants/spreadsheet 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -8
- package/README.md +60 -19
- package/dist/index.cjs +13 -13
- package/dist/index.d.cts +113 -119
- package/dist/index.d.ts +113 -119
- package/dist/index.js +3457 -3328
- package/package.json +6 -6
- package/src/Spreadsheet.tsx +68 -13
- package/src/grid/GridBody.tsx +30 -1
- package/src/index.ts +1 -1
- package/src/model/dataValidation.ts +84 -67
- package/src/model/labels.ts +313 -101
- package/src/model/validationMessages.ts +203 -0
package/src/model/labels.ts
CHANGED
|
@@ -1,38 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview UI label vocabulary (v0.8 §1.6 / §4.1.17) — the ONE label bag of the package
|
|
3
|
-
* The v0.7 `sinkAriaLabel` / `rootAriaLabel`
|
|
4
|
-
* props remain), joined by the popover
|
|
2
|
+
* @fileoverview UI label vocabulary (v0.8 §1.6 / §4.1.17) — the ONE label bag of the package, now
|
|
3
|
+
* per UI chrome locale ("en" default, "ja" second). The v0.7 `sinkAriaLabel` / `rootAriaLabel`
|
|
4
|
+
* props are folded into it (clean cut-over: no alias props remain), joined by the popover strings
|
|
5
|
+
* of the rich-cell UI and by the seven engine chrome strings of `@aiquants/virtualscroll`
|
|
6
|
+
* (scroll arrows, scroll-to-edge pills, "No items"), which are REUSED from
|
|
7
|
+
* `VIRTUAL_SCROLL_LABEL_CATALOGS` and never re-typed here. Pure, React-free.
|
|
5
8
|
*
|
|
6
9
|
* Contract (each clause is pinned by labels.spec.ts):
|
|
7
|
-
* - `
|
|
8
|
-
*
|
|
9
|
-
* - `
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* - `SpreadsheetLocale` is the engine's locale set (`VirtualScrollLocale`); it is the UI chrome
|
|
11
|
+
* language and a different axis from `formats.locale` (BCP-47 value formatting).
|
|
12
|
+
* - `SpreadsheetLabels` is the closed 28-key set (7 engine keys + 21 own keys);
|
|
13
|
+
* `SPREADSHEET_LABEL_CATALOGS` supplies a frozen catalog per locale. Count-aware keys are typed
|
|
14
|
+
* `string | ((n: number) => string)`.
|
|
15
|
+
* - `resolveLabels(locale, overrides)` is the single fold point: the locale is resolved by
|
|
16
|
+
* `resolveVirtualScrollLocale` (undefined ⇒ "en", unknown ⇒ RangeError); `undefined` overrides
|
|
17
|
+
* return the catalog by identity; otherwise the overrides must be a plain object (prototype
|
|
18
|
+
* `Object.prototype` or `null`) read by own properties only, unknown keys are a RangeError, the engine subset is
|
|
19
|
+
* resolved by `resolveVirtualScrollLabels` and an own key takes its override when it is not
|
|
20
|
+
* `undefined`, then `assertLabels` runs on the result, which is frozen.
|
|
21
|
+
* - `assertLabels`: every non-count own key is a non-blank string, a count key is a non-blank
|
|
22
|
+
* string or a function, and `sinkAriaLabel` MUST mention "Escape" (WCAG 2.1.2 escape-method
|
|
14
23
|
* advisement, spec 00 §10-4) — otherwise a RangeError, thrown at mount by the root.
|
|
24
|
+
* - `pickEngineLabels(labels)` hands the 7 engine keys to the embedded VirtualGrid.
|
|
15
25
|
* - `resolveLabel(labels, key, n)` is the single read point: a function-valued label receives `n`
|
|
16
26
|
* (omitting `n` for a function value is a RangeError — never a silent 0), a string is returned
|
|
17
27
|
* as-is (`n` ignored, so a consumer may override a count key with a plain string).
|
|
18
28
|
*
|
|
19
|
-
* UI ラベル語彙 (v0.8 §1.6 / §4.1.17) —
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* `
|
|
29
|
+
* UI ラベル語彙 (v0.8 §1.6 / §4.1.17) — パッケージ唯一のラベル集合で、UI クロームロケール別
|
|
30
|
+
* ("en" が既定、"ja" が第 2 ロケール)。v0.7 の `sinkAriaLabel` / `rootAriaLabel` prop を畳み込み
|
|
31
|
+
* (クリーンカットオーバー: 別名 prop は残さない)、リッチセル UI のポップオーバー文言と
|
|
32
|
+
* `@aiquants/virtualscroll` の 7 つのエンジンクローム文言 (スクロール矢印、端スクロールピル、
|
|
33
|
+
* "No items") を加える。エンジン文言は `VIRTUAL_SCROLL_LABEL_CATALOGS` を再利用し、ここでは再記述
|
|
34
|
+
* しない。純関数・React 非依存。
|
|
35
|
+
* 契約: `SpreadsheetLocale` はエンジンのロケール集合 (UI クローム言語で、値書式の `formats.locale`
|
|
36
|
+
* とは別軸)。`SpreadsheetLabels` は閉じた 28 キー (エンジン 7 + 固有 21)、`SPREADSHEET_LABEL_CATALOGS`
|
|
37
|
+
* はロケール別の凍結カタログ、件数キーは `string | ((n: number) => string)`。`resolveLabels` が唯一の
|
|
38
|
+
* 畳込み点 (ロケールは `resolveVirtualScrollLocale` で解決 — undefined は "en"、未知は RangeError。
|
|
39
|
+
* override 未指定はカタログを identity で返却。それ以外は override を素の object (プロトタイプが
|
|
40
|
+
* `Object.prototype` か `null`) に限り自身のプロパティだけを読み、未知キーを RangeError、エンジン部分は
|
|
41
|
+
* `resolveVirtualScrollLabels` で解決、固有キーは undefined でない override を採用し、`assertLabels`
|
|
42
|
+
* 検証の後に凍結)。`assertLabels` は固有キーの非空白文字列性 (件数キーは関数も可) と
|
|
43
|
+
* `sinkAriaLabel` の "Escape" 告知 (spec 00 §10-4) を検証し、違反はマウント時 RangeError。
|
|
44
|
+
* `pickEngineLabels` は埋込み VirtualGrid へ渡すエンジン 7 キーの抽出。`resolveLabel` が唯一の
|
|
45
|
+
* 読み出し点 (関数値は n 必須 — 省略は RangeError、文字列はそのまま)。
|
|
28
46
|
*/
|
|
29
47
|
|
|
48
|
+
import { resolveVirtualScrollLabels, resolveVirtualScrollLocale, VIRTUAL_SCROLL_LABEL_CATALOGS, VIRTUAL_SCROLL_LABEL_KEYS, type VirtualScrollLabelOverrides, type VirtualScrollLabels, type VirtualScrollLocale } from "@aiquants/virtualscroll"
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The UI chrome language of the spreadsheet ("en" | "ja") — the engine's locale set, so the grid
|
|
52
|
+
* body and the embedded scroll chrome always speak one language. A different axis from
|
|
53
|
+
* `formats.locale`, which is the BCP-47 locale of value formatting (numbers / dates); a BCP-47 tag
|
|
54
|
+
* such as "ja-JP" is NOT a SpreadsheetLocale.
|
|
55
|
+
* スプレッドシートの UI クローム言語 ("en" | "ja") — エンジンのロケール集合そのもので、グリッド本体と
|
|
56
|
+
* 埋込みスクロールクロームが常に同一言語。値書式 (数値 / 日付) の BCP-47 ロケールである
|
|
57
|
+
* `formats.locale` とは別軸 ("ja-JP" のような BCP-47 タグは SpreadsheetLocale ではない)。
|
|
58
|
+
*/
|
|
59
|
+
export type SpreadsheetLocale = VirtualScrollLocale
|
|
60
|
+
|
|
30
61
|
/**
|
|
31
|
-
* The closed label vocabulary
|
|
32
|
-
*
|
|
33
|
-
*
|
|
62
|
+
* The closed label vocabulary: the 7 engine chrome keys of `@aiquants/virtualscroll` plus the 21
|
|
63
|
+
* own keys of the spreadsheet. Every key has a value in each SPREADSHEET_LABEL_CATALOGS entry;
|
|
64
|
+
* consumers override any subset through the `labels` prop (SpreadsheetLabelOverrides).
|
|
65
|
+
* 閉じたラベル語彙: `@aiquants/virtualscroll` のエンジンクローム 7 キーとスプレッドシート固有の 21 キー。
|
|
66
|
+
* 全キーが SPREADSHEET_LABEL_CATALOGS の各ロケールに値を持ち、消費者は `labels` prop で任意の部分集合を上書き。
|
|
34
67
|
*/
|
|
35
|
-
export type SpreadsheetLabels = {
|
|
68
|
+
export type SpreadsheetLabels = VirtualScrollLabels & {
|
|
36
69
|
/** Accessible name of the sink (grid input receptor); MUST mention "Escape" (spec 00 §10-4). / シンクのアクセシブルネーム ("Escape" 必須)。 */
|
|
37
70
|
sinkAriaLabel: string
|
|
38
71
|
/** Accessible name of the root role="group" landing point. / root 着地点のアクセシブルネーム。 */
|
|
@@ -73,32 +106,24 @@ export type SpreadsheetLabels = {
|
|
|
73
106
|
listMore: string | ((n: number) => string)
|
|
74
107
|
/** Message popover heading. / メッセージポップオーバーの見出し。 */
|
|
75
108
|
messageTitle: string
|
|
76
|
-
/** Message shown when a strict validation rule rejects the typed value. / 厳格検証で拒否されたときの文言。 */
|
|
77
|
-
validationRejected: string
|
|
78
109
|
/** Close button of every popover. / ポップオーバー共通の閉じるボタン。 */
|
|
79
110
|
popoverClose: string
|
|
80
|
-
/** Accessible label of the note indicator on a cell. / ノートインジケータのラベル。 */
|
|
81
|
-
indicatorNote: string
|
|
82
|
-
/** Accessible label of the comment indicator on a cell. / コメントインジケータのラベル。 */
|
|
83
|
-
indicatorComment: string
|
|
84
|
-
/** Accessible label of the invalid-value indicator on a cell. / 無効値インジケータのラベル。 */
|
|
85
|
-
indicatorInvalid: string
|
|
86
111
|
}
|
|
87
112
|
|
|
88
113
|
/**
|
|
89
114
|
* The `labels` prop shape: any subset of SpreadsheetLabels (an omitted or `undefined` key keeps
|
|
90
|
-
* the
|
|
91
|
-
* `labels` prop の形: SpreadsheetLabels の任意の部分集合 (省略 / undefined
|
|
92
|
-
* SpreadsheetLabels に閉じる。
|
|
115
|
+
* the catalog value of the chosen locale). Keys are closed to SpreadsheetLabels.
|
|
116
|
+
* `labels` prop の形: SpreadsheetLabels の任意の部分集合 (省略 / undefined は選択ロケールのカタログ値の
|
|
117
|
+
* まま)。キーは SpreadsheetLabels に閉じる。
|
|
93
118
|
*/
|
|
94
119
|
export type SpreadsheetLabelOverrides = { readonly [K in keyof SpreadsheetLabels]?: SpreadsheetLabels[K] }
|
|
95
120
|
|
|
96
121
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
122
|
+
* The 21 own (non-engine) keys, in declaration order — the iteration source of the own-key fold and
|
|
123
|
+
* of assertLabels.
|
|
124
|
+
* 固有 (エンジン外) の 21 キー (宣言順) — 固有キー畳込みと assertLabels の走査元。
|
|
100
125
|
*/
|
|
101
|
-
|
|
126
|
+
const OWN_LABEL_KEYS = [
|
|
102
127
|
"sinkAriaLabel",
|
|
103
128
|
"rootAriaLabel",
|
|
104
129
|
"noteTitle",
|
|
@@ -119,93 +144,256 @@ export const LABEL_KEYS = [
|
|
|
119
144
|
"listEmpty",
|
|
120
145
|
"listMore",
|
|
121
146
|
"messageTitle",
|
|
122
|
-
"validationRejected",
|
|
123
147
|
"popoverClose",
|
|
124
|
-
"indicatorNote",
|
|
125
|
-
"indicatorComment",
|
|
126
|
-
"indicatorInvalid",
|
|
127
148
|
] as const satisfies readonly (keyof SpreadsheetLabels)[]
|
|
128
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Every label key: the 7 engine keys (VIRTUAL_SCROLL_LABEL_KEYS, in engine order) followed by the
|
|
152
|
+
* 21 own keys — the closed key set that overrides are validated against, and the census list for
|
|
153
|
+
* docs / tests. Compile-time proofs below that it covers EVERY SpreadsheetLabels key and that the
|
|
154
|
+
* own keys never shadow an engine key.
|
|
155
|
+
* 全ラベルキー: エンジン 7 キー (VIRTUAL_SCROLL_LABEL_KEYS、エンジン順) に固有 21 キーを続けたもの —
|
|
156
|
+
* override 検証の閉じたキー集合で docs / テストの census 表。直下で全キー網羅と、固有キーが
|
|
157
|
+
* エンジンキーを覆い隠さないことを型水準で証明。
|
|
158
|
+
*/
|
|
159
|
+
export const LABEL_KEYS = [...VIRTUAL_SCROLL_LABEL_KEYS, ...OWN_LABEL_KEYS] as const satisfies readonly (keyof SpreadsheetLabels)[]
|
|
160
|
+
|
|
129
161
|
/** Compile-time proof that LABEL_KEYS lists EVERY SpreadsheetLabels key (a key added to the type but not to the list would never be folded). / LABEL_KEYS が SpreadsheetLabels の全キーを網羅することの型水準の証明 (型に増えたキーが表に無いと畳込みから漏れる)。 */
|
|
130
162
|
const labelKeysCoverEveryKey: [Exclude<keyof SpreadsheetLabels, (typeof LABEL_KEYS)[number]>] extends [never] ? true : never = true
|
|
131
163
|
void labelKeysCoverEveryKey
|
|
132
164
|
|
|
165
|
+
/** Compile-time proof that no own key is also an engine key (an overlap would let the own-key fold bypass the engine's validation of that key). / 固有キーがエンジンキーと重ならないことの型水準の証明 (重なると固有キー畳込みがそのキーのエンジン検証を迂回する)。 */
|
|
166
|
+
const ownLabelKeysAreDisjointFromEngine: [Extract<(typeof OWN_LABEL_KEYS)[number], keyof VirtualScrollLabels>] extends [never] ? true : never = true
|
|
167
|
+
void ownLabelKeysAreDisjointFromEngine
|
|
168
|
+
|
|
133
169
|
/**
|
|
134
|
-
*
|
|
135
|
-
* v0.7 prop defaults
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
* Per-locale values of the 21 own keys. `en` is byte-identical to the pre-i18n English defaults
|
|
171
|
+
* (`sinkAriaLabel` / `rootAriaLabel` equal the v0.7 prop defaults, so the fold is DOM-identical);
|
|
172
|
+
* `commentCount` / `listMore` pluralise in `en` and use the counter 件 in `ja`. Frozen.
|
|
173
|
+
* 固有 21 キーのロケール別の値。`en` は i18n 化前の英語既定と byte 同一 (`sinkAriaLabel` /
|
|
174
|
+
* `rootAriaLabel` は v0.7 の prop 既定と同じで畳込みは DOM 恒等)、`commentCount` / `listMore` は
|
|
175
|
+
* `en` で単複を区別し `ja` では助数詞「件」。凍結済み。
|
|
176
|
+
*/
|
|
177
|
+
const OWN_LABEL_CATALOGS: Readonly<Record<SpreadsheetLocale, Omit<SpreadsheetLabels, keyof VirtualScrollLabels>>> = Object.freeze({
|
|
178
|
+
en: Object.freeze({
|
|
179
|
+
sinkAriaLabel: "Spreadsheet grid. Press Escape to move focus out of the grid.",
|
|
180
|
+
rootAriaLabel: "Spreadsheet",
|
|
181
|
+
noteTitle: "Note",
|
|
182
|
+
noteSave: "Save",
|
|
183
|
+
noteEscapeHint: "Escape saves and closes",
|
|
184
|
+
noteRejectedHint: "The note was rejected. Press Ctrl+Enter to retry or Escape to close without saving",
|
|
185
|
+
noteViewerHint: "Escape closes without saving",
|
|
186
|
+
commentTitle: "Comments",
|
|
187
|
+
commentReply: "Reply",
|
|
188
|
+
commentPost: "Post",
|
|
189
|
+
commentCount: (n: number): string => (n === 1 ? "1 comment" : `${n} comments`),
|
|
190
|
+
linkTitle: "Link",
|
|
191
|
+
linkTextField: "Text",
|
|
192
|
+
linkUrlField: "URL",
|
|
193
|
+
linkOpen: "Open",
|
|
194
|
+
linkSave: "Save",
|
|
195
|
+
listTitle: "Choose a value",
|
|
196
|
+
listEmpty: "No matching values",
|
|
197
|
+
listMore: (n: number): string => (n === 1 ? "1 more value — keep typing to narrow" : `${n} more values — keep typing to narrow`),
|
|
198
|
+
messageTitle: "Data validation",
|
|
199
|
+
popoverClose: "Close",
|
|
200
|
+
}),
|
|
201
|
+
ja: Object.freeze({
|
|
202
|
+
sinkAriaLabel: "スプレッドシートのグリッド。Escape キーでグリッドの外へフォーカスを移動します。",
|
|
203
|
+
rootAriaLabel: "スプレッドシート",
|
|
204
|
+
noteTitle: "メモ",
|
|
205
|
+
noteSave: "保存",
|
|
206
|
+
noteEscapeHint: "Escape で保存して閉じます",
|
|
207
|
+
noteRejectedHint: "メモは拒否されました。Ctrl+Enter で再試行、Escape で保存せずに閉じます",
|
|
208
|
+
noteViewerHint: "Escape で保存せずに閉じます",
|
|
209
|
+
commentTitle: "コメント",
|
|
210
|
+
commentReply: "返信",
|
|
211
|
+
commentPost: "投稿",
|
|
212
|
+
commentCount: (n: number): string => `${n} 件のコメント`,
|
|
213
|
+
linkTitle: "リンク",
|
|
214
|
+
linkTextField: "表示テキスト",
|
|
215
|
+
linkUrlField: "URL",
|
|
216
|
+
linkOpen: "開く",
|
|
217
|
+
linkSave: "保存",
|
|
218
|
+
listTitle: "値を選択",
|
|
219
|
+
listEmpty: "一致する値がありません",
|
|
220
|
+
listMore: (n: number): string => `ほかに ${n} 件 — 入力して絞り込んでください`,
|
|
221
|
+
messageTitle: "データの入力規則",
|
|
222
|
+
popoverClose: "閉じる",
|
|
223
|
+
}),
|
|
168
224
|
})
|
|
169
225
|
|
|
170
226
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* (
|
|
176
|
-
*
|
|
227
|
+
* Frozen full catalogs per locale (outer record and every entry frozen): each entry is the engine
|
|
228
|
+
* catalog `VIRTUAL_SCROLL_LABEL_CATALOGS[locale]` (reused by value — single source of truth for
|
|
229
|
+
* the 7 chrome strings) overlaid by the own-key catalog of the same locale.
|
|
230
|
+
* ロケール別の凍結済み完全カタログ (外側のレコードと各エントリを凍結): 各エントリはエンジンカタログ
|
|
231
|
+
* `VIRTUAL_SCROLL_LABEL_CATALOGS[locale]` (値を再利用 — 7 クローム文言の単一の真実源) に同ロケールの
|
|
232
|
+
* 固有キーカタログを重ねたもの。
|
|
233
|
+
*/
|
|
234
|
+
export const SPREADSHEET_LABEL_CATALOGS: Readonly<Record<SpreadsheetLocale, SpreadsheetLabels>> = Object.freeze({
|
|
235
|
+
en: Object.freeze({ ...VIRTUAL_SCROLL_LABEL_CATALOGS.en, ...OWN_LABEL_CATALOGS.en }),
|
|
236
|
+
ja: Object.freeze({ ...VIRTUAL_SCROLL_LABEL_CATALOGS.ja, ...OWN_LABEL_CATALOGS.ja }),
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Describes a rejected value for a RangeError message without ever throwing: a JSON string for
|
|
241
|
+
* strings, "null" for `null`, otherwise the typeof name. It never calls `String()` /
|
|
242
|
+
* `JSON.stringify` on an arbitrary value, so BigInt, Symbol, function and null-prototype inputs
|
|
243
|
+
* still produce the documented RangeError with an accurate description. The ONE describer of every
|
|
244
|
+
* label and data-validation RangeError message of the package (assertLabels, resolveLabels,
|
|
245
|
+
* validateCellText's locale and rule-shape checks).
|
|
246
|
+
* RangeError メッセージ用の拒否値の説明 (例外を投げない): 文字列は JSON 表現、`null` は "null"、それ以外は
|
|
247
|
+
* typeof 名。任意値へ `String()` / `JSON.stringify` を適用しないため、BigInt・Symbol・関数・null プロトタイプの
|
|
248
|
+
* 入力でも文書どおりの RangeError と正確な説明を維持。パッケージのラベル / データ検証の全 RangeError
|
|
249
|
+
* メッセージ (assertLabels、resolveLabels、validateCellText のロケール検査と規則形状検査) の唯一の説明器。
|
|
250
|
+
*
|
|
251
|
+
* @param value - The rejected value / 拒否された値
|
|
252
|
+
* @returns A printable, non-throwing description / 表示用の説明文字列 (例外なし)
|
|
253
|
+
*/
|
|
254
|
+
export const describeLabelValue = (value: unknown): string => {
|
|
255
|
+
if (typeof value === "string") {
|
|
256
|
+
return JSON.stringify(value)
|
|
257
|
+
}
|
|
258
|
+
return value === null ? "null" : typeof value
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Whether a label key is count-aware (`commentCount` / `listMore`), i.e. may hold a function.
|
|
263
|
+
* ラベルキーが件数キー (`commentCount` / `listMore`) — 関数値を許すキー — かどうか。
|
|
264
|
+
*
|
|
265
|
+
* @param key - The own label key / 固有ラベルキー
|
|
266
|
+
* @returns `true` for a count-aware key / 件数キーなら `true`
|
|
267
|
+
*/
|
|
268
|
+
const isCountLabelKey = (key: (typeof OWN_LABEL_KEYS)[number]): key is "commentCount" | "listMore" => key === "commentCount" || key === "listMore"
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Mount-time check of a resolved label bag's own keys (the 7 engine keys are validated by
|
|
272
|
+
* `resolveVirtualScrollLabels`): `sinkAriaLabel` must be a string that mentions "Escape"
|
|
273
|
+
* (case-sensitive — the WCAG 2.1.2 escape-method advisement of spec 00 §10-4); every other
|
|
274
|
+
* non-count own key must be a string with non-whitespace content; `commentCount` / `listMore` must
|
|
275
|
+
* be such a string or a function. Anything else is a consumer configuration error ⇒ RangeError
|
|
276
|
+
* naming the key (fail-fast, never silently repaired).
|
|
277
|
+
* 解決済みラベル集合の固有キーのマウント時検証 (エンジン 7 キーは `resolveVirtualScrollLabels` が検証):
|
|
278
|
+
* `sinkAriaLabel` は "Escape" を含む文字列 (大文字小文字を区別 — spec 00 §10-4 の WCAG 2.1.2 脱出告知)、
|
|
279
|
+
* その他の件数以外の固有キーは空白以外を含む文字列、`commentCount` / `listMore` はその文字列か関数で
|
|
280
|
+
* あること。それ以外は消費者の設定誤りとしてキー名入りの RangeError (fail-fast、黙って修復しない)。
|
|
281
|
+
*
|
|
282
|
+
* @param labels - The resolved label bag / 解決済みラベル集合
|
|
283
|
+
* @returns Nothing; returns normally when every own key is valid / なし (全固有キーが妥当なら正常終了)
|
|
284
|
+
* @throws {RangeError} When an own key violates its rule / 固有キーが規則に違反した場合
|
|
177
285
|
*/
|
|
178
286
|
export const assertLabels = (labels: SpreadsheetLabels): void => {
|
|
179
|
-
const sink = labels.sinkAriaLabel
|
|
180
|
-
// 文字列以外 (JS 呼び手) と Escape 告知の欠落は同じ設定誤り
|
|
287
|
+
const sink: unknown = labels.sinkAriaLabel
|
|
288
|
+
// 文字列以外 (JS 呼び手) と Escape 告知の欠落は同じ設定誤り — 既存メッセージを保つため汎用検査より先に判定
|
|
181
289
|
if (typeof sink !== "string" || !sink.includes("Escape")) {
|
|
182
|
-
throw new RangeError(`[spreadsheet] labels.sinkAriaLabel must be a string mentioning "Escape" (WCAG 2.1.2 escape advisement, spec 00 §10-4); got ${
|
|
290
|
+
throw new RangeError(`[spreadsheet] labels.sinkAriaLabel must be a string mentioning "Escape" (WCAG 2.1.2 escape advisement, spec 00 §10-4); got ${describeLabelValue(sink)}`)
|
|
291
|
+
}
|
|
292
|
+
for (const key of OWN_LABEL_KEYS) {
|
|
293
|
+
const value: unknown = labels[key]
|
|
294
|
+
if (isCountLabelKey(key) && typeof value === "function") {
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
// 空白だけの文字列は描画上は空のアクセシブルネーム / 見出しになるため空文字列と同じ誤り
|
|
298
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
299
|
+
const expected = isCountLabelKey(key) ? "a non-blank string or a function" : "a non-blank string"
|
|
300
|
+
throw new RangeError(`[spreadsheet] labels.${key} must be ${expected}; got ${describeLabelValue(value)}`)
|
|
301
|
+
}
|
|
183
302
|
}
|
|
184
303
|
}
|
|
185
304
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Copies the engine subset of an overrides object (only the OWN keys of VIRTUAL_SCROLL_LABEL_KEYS
|
|
307
|
+
* whose value is not `undefined` — an inherited property is never read); `undefined` when none is
|
|
308
|
+
* present, so the engine returns its catalog by identity.
|
|
309
|
+
* override のエンジン部分 (VIRTUAL_SCROLL_LABEL_KEYS のうち値が undefined でない自身のキーのみ —
|
|
310
|
+
* 継承プロパティは読まない) の複製。1 つも無ければ `undefined` (エンジンがカタログを identity で返すため)。
|
|
311
|
+
*
|
|
312
|
+
* @param overrides - The consumer's overrides / 消費者の上書き
|
|
313
|
+
* @returns The engine overrides, or `undefined` when none is present / エンジン上書き (無ければ `undefined`)
|
|
314
|
+
*/
|
|
315
|
+
const pickEngineOverrides = (overrides: SpreadsheetLabelOverrides): VirtualScrollLabelOverrides | undefined => {
|
|
316
|
+
const picked: { -readonly [K in keyof VirtualScrollLabels]?: VirtualScrollLabels[K] } = {}
|
|
317
|
+
let present = false
|
|
318
|
+
for (const key of VIRTUAL_SCROLL_LABEL_KEYS) {
|
|
319
|
+
// Object.prototype 汚染で継承されたラベルキーを上書きとして拾わない
|
|
320
|
+
const value = Object.hasOwn(overrides, key) ? overrides[key] : undefined
|
|
321
|
+
if (value !== undefined) {
|
|
322
|
+
picked[key] = value
|
|
323
|
+
present = true
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return present ? picked : undefined
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Writes one resolved own label; generic so the union-typed key of the fold loop type-checks
|
|
331
|
+
* against the per-key value type.
|
|
332
|
+
* 解決済みの固有ラベルを 1 件書く (畳込みループの union キーをキー毎の値型と突き合わせるための総称関数)。
|
|
333
|
+
*
|
|
334
|
+
* @param target - The label bag under construction / 構築中のラベル集合
|
|
335
|
+
* @param key - The own key to write / 書き込む固有キー
|
|
336
|
+
* @param value - The value to write / 書き込む値
|
|
337
|
+
* @returns Nothing / なし
|
|
338
|
+
*/
|
|
339
|
+
const putLabel = <K extends (typeof OWN_LABEL_KEYS)[number]>(target: SpreadsheetLabels, key: K, value: SpreadsheetLabels[K]): void => {
|
|
188
340
|
target[key] = value
|
|
189
341
|
}
|
|
190
342
|
|
|
191
343
|
/**
|
|
192
|
-
* The single fold point of the `
|
|
193
|
-
* `
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
344
|
+
* The single fold point of the `locale` + `labels` props. The locale is resolved by
|
|
345
|
+
* `resolveVirtualScrollLocale` (undefined ⇒ "en"). `undefined` overrides return
|
|
346
|
+
* SPREADSHEET_LABEL_CATALOGS[locale] by identity. Otherwise: the overrides must be a plain object
|
|
347
|
+
* (prototype `Object.prototype` or `null`) whose own keys all belong to LABEL_KEYS, and only its
|
|
348
|
+
* own properties are read; the engine subset is resolved by `resolveVirtualScrollLabels` (the
|
|
349
|
+
* engine's own validation); an own key takes its override when it is not `undefined`
|
|
350
|
+
* (destructuring-default semantics of the folded v0.7 props) and the catalog value otherwise; the
|
|
351
|
+
* result is validated by assertLabels and frozen. Inputs are never mutated.
|
|
352
|
+
* `locale` + `labels` prop の唯一の畳込み点。ロケールは `resolveVirtualScrollLocale` で解決
|
|
353
|
+
* (undefined は "en")。override 未指定は SPREADSHEET_LABEL_CATALOGS[locale] を identity で返却。
|
|
354
|
+
* それ以外は、override は自身のキーがすべて LABEL_KEYS に属する素の object (プロトタイプが
|
|
355
|
+
* `Object.prototype` か `null`) で、読むのは自身のプロパティのみ。エンジン部分は
|
|
356
|
+
* `resolveVirtualScrollLabels` (エンジン自身の検証) で解決、固有キーは undefined でない override を採用し
|
|
357
|
+
* それ以外はカタログ値 (畳み込んだ v0.7 prop の分割代入既定の意味論)、結果は assertLabels で検証し凍結。
|
|
358
|
+
* 入力は変更しない。
|
|
359
|
+
*
|
|
360
|
+
* @param locale - UI chrome locale, or `undefined` for the default "en" / UI クロームロケール (`undefined` で既定の "en")
|
|
361
|
+
* @param overrides - Per-key overrides, or `undefined` for none / キー単位の上書き (`undefined` で上書きなし)
|
|
362
|
+
* @returns The resolved, frozen label bag / 解決済みの凍結ラベル集合
|
|
363
|
+
* @throws {RangeError} When the locale is unsupported, the overrides are `null` / not an object,
|
|
364
|
+
* the overrides are not a plain object (an array, a class instance, an object inheriting from
|
|
365
|
+
* another object), an own key is not a label key (including the removed validationRejected /
|
|
366
|
+
* indicator* keys), or a value violates its key's rule / ロケールが非対応、override が `null` /
|
|
367
|
+
* 非 object、override が素の object でない (配列・クラスインスタンス・他 object 継承)、自身のキーが
|
|
368
|
+
* ラベルキー外 (削除済みの validationRejected / indicator* を含む)、または値がキーの規則に違反した場合
|
|
199
369
|
*/
|
|
200
|
-
export const resolveLabels = (overrides: SpreadsheetLabelOverrides | undefined): SpreadsheetLabels => {
|
|
201
|
-
|
|
370
|
+
export const resolveLabels = (locale: SpreadsheetLocale | undefined, overrides: SpreadsheetLabelOverrides | undefined): SpreadsheetLabels => {
|
|
371
|
+
const resolvedLocale = resolveVirtualScrollLocale(locale)
|
|
372
|
+
// override なしはカタログそのもの (identity 安定 — 消費者の useMemo 再計算でも同一 object)
|
|
202
373
|
if (overrides === undefined) {
|
|
203
|
-
return
|
|
374
|
+
return SPREADSHEET_LABEL_CATALOGS[resolvedLocale]
|
|
375
|
+
}
|
|
376
|
+
if (overrides === null || typeof overrides !== "object") {
|
|
377
|
+
throw new RangeError(`[spreadsheet] labels must be an object; got ${describeLabelValue(overrides)}`)
|
|
378
|
+
}
|
|
379
|
+
// 既知キー検査は自身のキーしか見ないため、継承経路を持つ入力 (配列・クラスインスタンス・
|
|
380
|
+
// 他 object 継承) を許すと継承側の綴り誤りが無言で無視される
|
|
381
|
+
const prototype: unknown = Object.getPrototypeOf(overrides)
|
|
382
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
383
|
+
throw new RangeError(`[spreadsheet] labels must be a plain object (prototype Object.prototype or null); got ${Array.isArray(overrides) ? "an array" : "an object with a non-plain prototype"}`)
|
|
204
384
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
385
|
+
// 未知キーを黙って捨てると削除済みキーや綴り誤りの設定が無言で効かなくなる
|
|
386
|
+
for (const key of Object.keys(overrides)) {
|
|
387
|
+
if (!(LABEL_KEYS as readonly string[]).includes(key)) {
|
|
388
|
+
throw new RangeError(`[spreadsheet] labels.${key} is not a known label key; expected one of ${LABEL_KEYS.join(", ")}`)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const engine = resolveVirtualScrollLabels(resolvedLocale, pickEngineOverrides(overrides))
|
|
392
|
+
const resolved: SpreadsheetLabels = { ...engine, ...OWN_LABEL_CATALOGS[resolvedLocale] }
|
|
393
|
+
// 明示 undefined は「未指定」= カタログ値のまま (分割代入既定と同じ)
|
|
394
|
+
for (const key of OWN_LABEL_KEYS) {
|
|
395
|
+
// Object.prototype 汚染で継承されたラベルキーを上書きとして拾わない
|
|
396
|
+
const value = Object.hasOwn(overrides, key) ? overrides[key] : undefined
|
|
209
397
|
if (value !== undefined) {
|
|
210
398
|
putLabel(resolved, key, value)
|
|
211
399
|
}
|
|
@@ -214,12 +402,36 @@ export const resolveLabels = (overrides: SpreadsheetLabelOverrides | undefined):
|
|
|
214
402
|
return Object.freeze(resolved)
|
|
215
403
|
}
|
|
216
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Extracts the 7 engine chrome keys of a resolved label bag as a new frozen VirtualScrollLabels
|
|
407
|
+
* object — the `labels` handed to the embedded VirtualGrid.
|
|
408
|
+
* 解決済みラベル集合からエンジンクローム 7 キーを新しい凍結 VirtualScrollLabels object として抽出 —
|
|
409
|
+
* 埋込み VirtualGrid へ渡す `labels`。
|
|
410
|
+
*
|
|
411
|
+
* @param labels - The resolved label bag / 解決済みラベル集合
|
|
412
|
+
* @returns A new frozen object holding exactly the 7 engine keys / エンジン 7 キーだけを持つ新しい凍結 object
|
|
413
|
+
*/
|
|
414
|
+
export const pickEngineLabels = (labels: SpreadsheetLabels): VirtualScrollLabels => {
|
|
415
|
+
const engine: { -readonly [K in keyof VirtualScrollLabels]?: VirtualScrollLabels[K] } = {}
|
|
416
|
+
for (const key of VIRTUAL_SCROLL_LABEL_KEYS) {
|
|
417
|
+
engine[key] = labels[key]
|
|
418
|
+
}
|
|
419
|
+
// ループは VIRTUAL_SCROLL_LABEL_KEYS 全件を書くため (網羅はエンジン側の型証明が保証) 全キー存在が確定
|
|
420
|
+
return Object.freeze(engine as VirtualScrollLabels)
|
|
421
|
+
}
|
|
422
|
+
|
|
217
423
|
/**
|
|
218
424
|
* The single read point: resolves one label to its display string. A function-valued label is
|
|
219
425
|
* called with `n` (a missing `n` is a caller bug ⇒ RangeError — no silent 0); a string label is
|
|
220
426
|
* returned as-is and ignores `n`.
|
|
221
427
|
* 唯一の読み出し点: ラベル 1 件を表示文字列へ解決。関数値は n で呼ぶ (n 省略は呼び手バグとして
|
|
222
428
|
* RangeError — 黙って 0 にしない)、文字列値はそのまま (n は無視)。
|
|
429
|
+
*
|
|
430
|
+
* @param labels - The resolved label bag / 解決済みラベル集合
|
|
431
|
+
* @param key - The label key to read / 読み出すラベルキー
|
|
432
|
+
* @param n - The count for a count-aware (function-valued) label / 件数キー (関数値) に渡す件数
|
|
433
|
+
* @returns The display string / 表示文字列
|
|
434
|
+
* @throws {RangeError} When the label is a function and `n` is omitted / 関数値で n を省略した場合
|
|
223
435
|
*/
|
|
224
436
|
export const resolveLabel = (labels: SpreadsheetLabels, key: keyof SpreadsheetLabels, n?: number): string => {
|
|
225
437
|
const value = labels[key]
|