@aiquants/contact-map 0.1.1 → 0.3.0

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/README.md CHANGED
@@ -70,7 +70,7 @@ Peer dependencies:
70
70
 
71
71
  | Entry Point | Target | Contents |
72
72
  | --- | --- | --- |
73
- | `@aiquants/contact-map` | Universal | Domain types, Zod schemas, wire protocols, geometry, parsers, and tree builders. |
73
+ | `@aiquants/contact-map` | Universal | Domain types, Zod schemas, wire protocols, geometry, parsers, tree builders, and search query normalization. |
74
74
  | `@aiquants/contact-map/client` | Browser / SSR | React components (`ContactMapSearchPage`, `ContactMapSearchListView`, `ContactMapSearchTreeView`), hooks, and Worker clients. |
75
75
  | `@aiquants/contact-map/server` | Node / Server | DI port interfaces (`ContactMapServerPorts`), streaming handlers, and Drizzle MSSQL service queries. |
76
76
  | `@aiquants/contact-map/styles/contact-map.standalone.css` | CSS | Pre-compiled Tailwind v4 standalone utility styles. |
@@ -123,6 +123,448 @@ export default function ContactMapRoute() {
123
123
  }
124
124
  ```
125
125
 
126
+ ## Search Normalization / 検索語の正規化
127
+
128
+ The search page normalizes the typed query with `normalizeSearchInput` (exported from `@aiquants/contact-map`) before using it in three places: the fuzzy search term, the exact-match ranking term and the highlight query. The contact fields (name, company, department, title, email, phone numbers) are matched as stored. The query is therefore only brought into a form those fields can hold:
129
+
130
+ - Invisible characters (U+200B, U+FEFF) are removed, and surrounding whitespace (U+3000 and U+00A0 included) is trimmed.
131
+ - Width, kana (hiragana → katakana) and case are unified with select-box's `normalizeText` (`ABC商事` → `abc商事`, `さとう` → `サトウ`). Kana stays as written (`つづき` → `ツヅキ`).
132
+ - The query is replaced by its katakana reading only when the whole query is romaji that reads completely. It must hold only Latin letters and whitespace, and select-box's `romajiToKatakana` must leave no letter untransliterated (`tanaka` → `タナカ`, `yamada taro` → `ヤマダ タロ`, `tsuduki` → `ツヅキ`).
133
+ - Mixed text, digits and symbols, and words that do not read completely keep their width / kana / case form (`user@corp.co.jp`, `営業部 tanaka`, `tanaka1`, `ke-su`, `sony`).
134
+
135
+ The highlight marks the spellings that `getSearchQuerySpellings(query)` lists: select-box's `getNormalizedVariants` of each word of the query and of its normalized form. A kana reading is therefore marked only when the whole query reads as romaji. The select-box peer range is ^0.11.0, expanded from `workspace:^` at publish.
136
+
137
+ 検索ページは入力された検索語を `normalizeSearchInput` (`@aiquants/contact-map` から公開) で正規化してから、3 か所で使う: あいまい検索の検索語、完全一致による並べ替えの検索語、ハイライトの検索語。連絡先のフィールド (氏名・会社・部署・役職・メール・電話番号) は保存されたままの値で照合する。そのため検索語は、それらのフィールドが持ちうる形に揃えるだけ:
138
+
139
+ - 不可視文字 (U+200B・U+FEFF) を除き、端の空白 (U+3000・U+00A0 を含む) を除く。
140
+ - select-box の `normalizeText` で全角/半角・かな (ひらがな → カタカナ)・大文字小文字を統一する (`ABC商事` → `abc商事`、`さとう` → `サトウ`)。かなは書いたまま (`つづき` → `ツヅキ`)。
141
+ - 検索語全体が完全に読めるローマ字のときだけ、カタカナの読みに置き換える。ラテン文字と空白だけで、select-box の `romajiToKatakana` が音訳しない文字を残さないことが条件 (`tanaka` → `タナカ`、`yamada taro` → `ヤマダ タロ`、`tsuduki` → `ツヅキ`)。
142
+ - 混在テキスト・数字や記号・完全には読めない語は、全角/半角・かな・大文字小文字の形のまま (`user@corp.co.jp`、`営業部 tanaka`、`tanaka1`、`ke-su`、`sony`)。
143
+
144
+ ハイライトは `getSearchQuerySpellings(query)` が並べる表記に印を付ける。表記は、検索語の各語とその正規化形の各語に対する select-box の `getNormalizedVariants`。そのため、かなの読みに印が付くのは検索語全体がローマ字として読めるときだけ。select-box のピア範囲は ^0.11.0 (公開時に `workspace:^` から展開)。
145
+
146
+ ## Localization / UI 言語
147
+
148
+ Every string the client components render on their own (tabs, buttons, headings, status badges, column headers, empty states, aria-labels, the settings and statistics panels, and the fallback names of tree nodes without a company or a person name) comes from one label catalog per UI chrome locale. The embedded `@aiquants/virtualscroll` list and `@aiquants/directory-tree` tree receive the same locale and their part of the labels, and the tree Web Worker receives the fallback names, so the whole feature speaks one language.
149
+ クライアントコンポーネントが自身で描画する全文言 (タブ、ボタン、見出し、状態バッジ、列見出し、空状態、aria-label、設定 / 統計パネル、会社名や氏名の無いツリーノードの代替名) は UI クロームロケールごとの 1 つのラベルカタログから取得。埋込みの `@aiquants/virtualscroll` 一覧と `@aiquants/directory-tree` ツリーには同じロケールと対応部分のラベルを、ツリー Web Worker には代替名を渡すため、機能全体が 1 つの言語で表示される。
150
+
151
+ - Supported locales: `"en"` (default) and `"ja"`. There is no language negotiation: map `navigator.language` (or anything else) to one of them in the host.
152
+ - 対応ロケールは `"en"` (既定) と `"ja"`。言語ネゴシエーションは行わないため、`navigator.language` 等からの対応付けはホスト側で行う。
153
+
154
+ ### Props / 設定項目
155
+
156
+ | Surface | Prop | Type | Notes |
157
+ | --- | --- | --- | --- |
158
+ | `ContactMapConfigProvider` | `config.locale` | `ContactMapLocale` (`"en" \| "ja"`) | Omitted ⇒ `"en"`. Any other value throws a `RangeError` at mount. |
159
+ | `ContactMapConfigProvider` | `config.labels` | `ContactMapLabelOverrides` | Partial overrides over `CONTACT_MAP_LABEL_CATALOGS[locale]`, validated at mount. |
160
+ | `ContactMapAppView` | `locale` | `ContactMapLocale` | When given it wins over the surrounding provider's `locale`. |
161
+ | `ContactMapAppView` | `labels` | `ContactMapLabelOverrides` | When given the whole object replaces the surrounding provider's `labels` (no key-wise merge). |
162
+ | `ContactMapAppView` | `title` | `string` | Per-instance heading; omitted ⇒ the `appTitle` label. |
163
+ | `SwipeOverlay` | `title` | `string` | Per-instance dialog name; omitted ⇒ the `overlayTitle` label. |
164
+
165
+ Components rendered outside any provider use the `en` catalog. `useContactMapLabels()` returns the resolved labels of the nearest provider, for host chrome that should speak the same language (for example the component a custom `renderHeader` renders).
166
+ プロバイダー外で描画したコンポーネントは `en` カタログを使う。`useContactMapLabels()` は直近プロバイダーの解決済みラベルを返すフックで、同じ言語で表示したいホスト側のクローム (独自 `renderHeader` が描画するコンポーネントなど) 向け。
167
+
168
+ ```tsx
169
+ import { ContactMapAppView } from "@aiquants/contact-map/client"
170
+
171
+ export default function ContactMapRoute() {
172
+ return <ContactMapAppView locale="ja" labels={{ appTitle: "連絡先マップ" }} />
173
+ }
174
+ ```
175
+
176
+ ### Resolver contract / 解決規則
177
+
178
+ `resolveContactMapLabels(locale, overrides)` is the single fold point (the provider calls it once per change):
179
+ `resolveContactMapLabels(locale, overrides)` が唯一の畳込み点 (プロバイダーは変化時にだけ呼ぶ):
180
+
181
+ - `overrides` omitted ⇒ the frozen catalog itself (same identity).
182
+ - `overrides` must be a plain object (prototype `Object.prototype` or `null`); arrays, class instances and objects inheriting from another object throw a `RangeError`. Only own properties are read.
183
+ - An own key outside `CONTACT_MAP_LABEL_KEYS` throws a `RangeError` (`[contact-map] labels.<key> is not a known label key`).
184
+ - A string key must be a string with non-whitespace content; a function key (the keys shown with arguments in the table below) must be a function. An explicit `undefined` keeps the catalog value.
185
+ - A unit key (`recordUnit`, `companyUnit`, `resultUnit`, `timesUnit`, `itemsUnit`, `charactersUnit`) receives the number exactly as it is displayed (`"1"`, `"1,001"`, `"1.0"`), so the English singular / plural follows what the reader sees: only a displayed `"1"` is singular (`"1.0 items"`). `timesUnit` / `itemsUnit` / `charactersUnit` / `throughputUnit` are appended to the number without a separator and carry their own leading space.
186
+ - The 7 engine keys are validated by `@aiquants/virtualscroll` (`[virtualscroll]` messages) and the 2 row kind words by `@aiquants/directory-tree` (`[directory-tree]` messages).
187
+ - The result is frozen. The list VirtualScroll receives exactly the 7 engine keys and the DirectoryTree exactly its 9 keys, both kept value-stable, so an inline `labels` object recreated on every render does not rebuild the embedded scroll chrome.
188
+ - `overrides` 未指定なら凍結カタログそのもの (同一 identity) を返す。
189
+ - `overrides` は素の object (プロトタイプが `Object.prototype` か `null`) に限り、配列・クラスインスタンス・他 object 継承は `RangeError`。読むのは自身のプロパティのみ。
190
+ - `CONTACT_MAP_LABEL_KEYS` 外の自身のキーは `RangeError`。
191
+ - 文字列キーは空白以外を含む文字列、関数キー (下表で引数付きのキー) は関数であることが必須。明示 `undefined` はカタログ値のまま。
192
+ - 単位キー (`recordUnit`、`companyUnit`、`resultUnit`、`timesUnit`、`itemsUnit`、`charactersUnit`) は表示どおりの数値文字列 (`"1"`、`"1,001"`、`"1.0"`) を受け取るため、英語の単数 / 複数は読み手が見る値に従う (単数は表示 `"1"` のときだけで `"1.0 items"`)。`timesUnit` / `itemsUnit` / `charactersUnit` / `throughputUnit` は数値の直後に区切りなしで連結されるため、先頭の空白は単位側が持つ。
193
+ - エンジン 7 キーは `@aiquants/virtualscroll`、行種別語 2 キーは `@aiquants/directory-tree` が検証する。
194
+ - 結果は凍結される。一覧の VirtualScroll にはエンジン 7 キーだけ、DirectoryTree には 9 キーだけを値安定のまま渡すため、毎レンダー作り直すインライン `labels` でも埋込みスクロールのクロームは再構築されない。
195
+
196
+ ### Chrome locale and number formatting / クロームロケールと数値書式
197
+
198
+ `locale` is the UI chrome language (`"en"` | `"ja"`), not a BCP 47 tag. contact-map has no separate formatting-locale prop (`formatLocale`): the chrome locale also fixes digit grouping through a spec-defined mapping, `en` → `"en-US"` and `ja` → `"ja-JP"`. Every count the package renders (annotation, registered / result counts, tree node counts, member counts in the tree, statistics) is grouped with that tag, never with the runtime default locale, so the server render and the browser render agree. Dates keep their ISO-based format.
199
+ `locale` は UI クロームの言語 (`"en"` | `"ja"`) で BCP 47 タグではない。contact-map には書式ロケール専用の prop (`formatLocale`) は無く、クロームロケールが仕様で定めた対応 (`en` → `"en-US"`、`ja` → `"ja-JP"`) で桁区切りも決める。パッケージが描画する件数 (注記、登録件数 / 検索結果件数、ツリーのノード数・所属人数、統計) はすべてこのタグで桁区切りし、実行環境の既定ロケールは使わないため、サーバー描画とブラウザ描画が一致する。日付は ISO ベースの書式のまま。
200
+
201
+ ### Tree fallback names / ツリーの代替名
202
+
203
+ The tree is built in a Web Worker by the isomorphic `IncrementalTreeBuilder`, which cannot receive functions, so the locale reaches it as data:
204
+ ツリーは Web Worker 上で同型の `IncrementalTreeBuilder` が組み立て、関数を受け取れないため、ロケールはデータとして届く:
205
+
206
+ - `useTreeWorker` reads the nearest provider's labels and sends `{ uncategorizedCompany, unnamedPerson: noName }` (`ContactMapTreeNaming`) in the `RESET_TREE` message that opens every worker generation. When the names change (a locale switch or a `labels` override), the session opens a new generation and re-posts every record, so no node keeps a name in the previous language.
207
+ - A contact without a company is placed under the `uncategorizedCompany` node (`"Uncategorized"` / `"未分類"`); a person without a name is called `noName` (`"(No Name)"` / `"名前未設定"`) on every path — the tree row, its accessible name, the list, the detail panel and the popup.
208
+ - Directory entries carry the bare company / department `name` plus `memberCount`; the client composes the display name with the `treeDirectoryName(name, memberCount)` label, so the member count is grouped in the chrome locale and a host can override the format.
209
+ - `IncrementalTreeBuilder` / `buildDirectoryTree` take the names as a required argument and reject blank or non-string names with a `RangeError` (`assertContactMapTreeNaming`).
210
+ - `useTreeWorker` は直近プロバイダーのラベルを読み、各 Worker 世代を開く `RESET_TREE` メッセージで `{ uncategorizedCompany, unnamedPerson: noName }` (`ContactMapTreeNaming`) を送る。代替名が変わると (ロケール切替や `labels` の上書き) セッションは新しい世代を開いて全レコードを再送するため、旧言語の名前を持つノードは残らない。
211
+ - 会社名の無い連絡先は `uncategorizedCompany` のノード (`"Uncategorized"` / `"未分類"`) の下に置き、氏名の無い人物はツリー行・そのアクセシブルネーム・一覧・詳細パネル・ポップアップのすべてで `noName` (`"(No Name)"` / `"名前未設定"`) と呼ぶ。
212
+ - ディレクトリエントリは会社名 / 部署名そのもの (`name`) と `memberCount` を持ち、表示名はクライアントが `treeDirectoryName(name, memberCount)` ラベルで組み立てる (所属人数をクロームロケールで桁区切りし、書式をホストが上書きできる)。
213
+ - `IncrementalTreeBuilder` / `buildDirectoryTree` は代替名を必須引数で受け取り、空白だけ・文字列以外の代替名は `RangeError` (`assertContactMapTreeNaming`)。
214
+
215
+ Not part of the catalog / カタログ対象外:
216
+
217
+ - Raw record field names shown as-is in the detail panel's metadata tab (`person_id`, `is_active`, ...) and the score chip abbreviations `B:` / `F:` are identifiers, not language.
218
+ - 詳細パネルのメタ情報タブに生のまま表示するレコード項目名 (`person_id`、`is_active` など) とスコア表示の略号 `B:` / `F:` は識別子であり言語ではない。
219
+
220
+ ### Catalog / カタログ
221
+
222
+ Function keys are shown with their arguments; the cells show sample output (`count = 2`, `displayed = "2"`, `loaded = 1200`, `name = "<name>"`, `memberCount = 1200`, `fieldLabel = "<label>"`).
223
+ 関数キーは引数付きで示し、セルには出力例 (`count = 2`、`displayed = "2"`、`loaded = 1200`、`name = "<name>"`、`memberCount = 1200`、`fieldLabel = "<label>"`) を載せる。
224
+
225
+ | Key | en | ja |
226
+ | --- | --- | --- |
227
+ | `scrollUp` | `"Scroll up"` | `"上へスクロール"` |
228
+ | `scrollDown` | `"Scroll down"` | `"下へスクロール"` |
229
+ | `scrollLeft` | `"Scroll left"` | `"左へスクロール"` |
230
+ | `scrollRight` | `"Scroll right"` | `"右へスクロール"` |
231
+ | `scrollToTop` | `"Top"` | `"先頭へ"` |
232
+ | `scrollToBottom` | `"Bottom"` | `"末尾へ"` |
233
+ | `noItems` | `"No items"` | `"項目がありません"` |
234
+ | `entryKindDirectory` | `"directory"` | `"ディレクトリ"` |
235
+ | `entryKindFile` | `"file"` | `"ファイル"` |
236
+ | `appTitle` | `"Contact Map"` | `"Contact Map"` |
237
+ | `annotationTotalLabel` | `"Total:"` | `"総件数:"` |
238
+ | `annotationCompaniesLabel` | `"Companies:"` | `"企業数:"` |
239
+ | `annotationStreamingProgress({ loaded, total, percent })` | `"Streaming (1,200 / 4,800 · 25%)"` | `"ストリーミング読み込み中 (1,200 / 4,800 件・25%)"` |
240
+ | `annotationStreamingCount(loaded)` | `"Streaming (1,200 loaded)"` | `"ストリーミング読み込み中 (1,200 件)"` |
241
+ | `annotationLoadFailed(loaded)` | `"Failed to load (1,200 received)"` | `"読み込みに失敗しました (1,200 件受信済)"` |
242
+ | `annotationRetry` | `"Retry"` | `"再試行"` |
243
+ | `recordUnit(displayed)` | `"records"` | `"件"` |
244
+ | `companyUnit(displayed)` | `"companies"` | `"社"` |
245
+ | `searchTab` | `"Fuzzy search"` | `"あいまい検索"` |
246
+ | `searchSettings` | `"Search settings"` | `"検索設定"` |
247
+ | `searchStatistics` | `"Search statistics"` | `"検索統計"` |
248
+ | `operatorAndTitle` | `"AND search (all terms match)"` | `"AND 検索 (すべて一致)"` |
249
+ | `operatorOrTitle` | `"OR search (any term matches)"` | `"OR 検索 (いずれか一致)"` |
250
+ | `operatorAnd` | `"AND"` | `"AND"` |
251
+ | `operatorOr` | `"OR"` | `"OR"` |
252
+ | `operatorAndHint` | `"All terms"` | `"全語一致"` |
253
+ | `operatorOrHint` | `"Any term"` | `"いずれか一致"` |
254
+ | `searchPlaceholder` | `"Fuzzy search..."` | `"あいまい検索..."` |
255
+ | `searchBusyTitle` | `"Searching"` | `"検索処理中"` |
256
+ | `clearSearch` | `"Clear search"` | `"検索をクリア"` |
257
+ | `searching` | `"Searching..."` | `"検索中..."` |
258
+ | `registeredCountLabel` | `"Registered:"` | `"登録件数:"` |
259
+ | `rebuildingIndex` | `"Rebuilding index..."` | `"インデックス再構築中..."` |
260
+ | `errorLabel` | `"Error:"` | `"エラー:"` |
261
+ | `resultCountLabel` | `"Results:"` | `"検索結果:"` |
262
+ | `resultUnit(displayed)` | `"matches"` | `"件"` |
263
+ | `listViewTab` | `"List"` | `"一覧"` |
264
+ | `treeViewTab` | `"Tree"` | `"ツリー"` |
265
+ | `columnNo` | `"No"` | `"No"` |
266
+ | `columnId` | `"ID"` | `"ID"` |
267
+ | `columnScore` | `"Score"` | `"スコア"` |
268
+ | `columnName` | `"Name"` | `"氏名"` |
269
+ | `columnTitle` | `"Title"` | `"役職"` |
270
+ | `columnCompany` | `"Company"` | `"会社名"` |
271
+ | `columnDepartment` | `"Department"` | `"部署"` |
272
+ | `columnPhone` | `"Phone"` | `"電話番号"` |
273
+ | `columnEmail` | `"Email"` | `"メール"` |
274
+ | `columnUpdatedAt` | `"Updated"` | `"更新日時"` |
275
+ | `listRowNumber(rowNumber)` | `"No. 3"` | `"No. 3"` |
276
+ | `listNoSearchResults` | `"No search results found"` | `"検索結果が見つかりません"` |
277
+ | `listNoData` | `"No data"` | `"データがありません"` |
278
+ | `listResizeHandle` | `"Resize the border between the contact list and the detail pane"` | `"連絡先一覧と詳細ペインの境界のサイズ変更ハンドル"` |
279
+ | `detailHeading` | `"Contact details"` | `"連絡先詳細"` |
280
+ | `close` | `"Close"` | `"閉じる"` |
281
+ | `noName` | `"(No Name)"` | `"名前未設定"` |
282
+ | `treeNodeCountTitle` | `"Number of nodes built in the tree view"` | `"ツリービューで構築された全ノード数"` |
283
+ | `treeNodeCountLabel` | `"Nodes:"` | `"ノード:"` |
284
+ | `treeNodeCount(count)` | `"1,200"` | `"1,200 個"` |
285
+ | `uncategorizedCompany` | `"Uncategorized"` | `"未分類"` |
286
+ | `treeDirectoryName(name, memberCount)` | `"<name> (1,200)"` | `"<name> (1,200)"` |
287
+ | `treeStreamInterruptedTitle` | `"Streaming was interrupted. Only the data received so far is shown"` | `"ストリーミングが中断されました。受信済みデータのみ表示しています"` |
288
+ | `treeStreamInterrupted` | `"Interrupted"` | `"中断"` |
289
+ | `treeStreamLoadingTitle` | `"Receiving data streamed from the network"` | `"ネットワークからのデータストリーミングを受信中"` |
290
+ | `treeStreamLoading` | `"Loading..."` | `"読み込み中..."` |
291
+ | `treeBuildingTitle` | `"Building and expanding the directory tree from the received data"` | `"受取データを基にディレクトリツリー構造を最終構築・展開中"` |
292
+ | `treeBuilding` | `"Treeing..."` | `"ツリー構築中..."` |
293
+ | `treeCompleteTitle` | `"Every tree node has been built"` | `"ツリーノードの全件構築が完了しました"` |
294
+ | `treeComplete` | `"Complete"` | `"完了"` |
295
+ | `treeShowEntireCompanyTitle` | `"Show the whole company of every search hit"` | `"検索ヒットした会社全体を表示"` |
296
+ | `treeShowEntireCompany` | `"Whole company"` | `"全体表示"` |
297
+ | `treeExpandAll` | `"Expand all directories"` | `"全ディレクトリを展開"` |
298
+ | `treeExpandAllTitle` | `"Expand all"` | `"すべて展開"` |
299
+ | `treeCollapseAll` | `"Collapse all directories"` | `"全ディレクトリを折りたたみ"` |
300
+ | `treeCollapseAllTitle` | `"Collapse all"` | `"すべて折りたたみ"` |
301
+ | `treeNoMatches` | `"No contacts match the criteria"` | `"条件に一致する連絡先が見つかりません"` |
302
+ | `treeEmpty` | `"No contacts to display"` | `"表示する連絡先がありません"` |
303
+ | `treeResizeHandle` | `"Resize the border between the directory tree and the detail pane"` | `"ディレクトリツリーと詳細ペインの境界のサイズ変更ハンドル"` |
304
+ | `copyField(fieldLabel)` | `"Copy <label>"` | `"「<label>」をコピー"` |
305
+ | `detailEmptyTitle` | `"Select a contact to see its details"` | `"連絡先を選択すると詳細が表示されます"` |
306
+ | `detailEmptyHint` | `"Choose a person from the list"` | `"一覧から対象の人物を選択してください"` |
307
+ | `detailTabSummary` | `"Summary"` | `"サマリー"` |
308
+ | `detailTabBasic` | `"Basic info"` | `"基本情報"` |
309
+ | `detailBasicProfile` | `"Profile"` | `"基本プロフィール"` |
310
+ | `detailTabContact` | `"Contact"` | `"連絡先"` |
311
+ | `detailTabMeta` | `"Metadata"` | `"メタ情報"` |
312
+ | `preferred` | `"Preferred"` | `"優先"` |
313
+ | `summaryEmail` | `"Email"` | `"メール"` |
314
+ | `summaryPhone` | `"Phone"` | `"電話"` |
315
+ | `summaryLastContact` | `"Last contact"` | `"最終接触"` |
316
+ | `sectionEmail` | `"Email"` | `"メール"` |
317
+ | `sectionEmailEmpty` | `"No email addresses registered"` | `"登録されたメールアドレスはありません"` |
318
+ | `sectionPhone` | `"Phone / Fax"` | `"電話・FAX"` |
319
+ | `sectionPhoneEmpty` | `"No phone numbers registered"` | `"登録された電話番号はありません"` |
320
+ | `fieldFullName` | `"Name"` | `"氏名"` |
321
+ | `fieldFullNameReading` | `"Name (reading)"` | `"氏名 (かな)"` |
322
+ | `fieldTitle` | `"Title"` | `"役職"` |
323
+ | `fieldDepartment` | `"Department"` | `"部署"` |
324
+ | `fieldCompanyName` | `"Company"` | `"会社名"` |
325
+ | `fieldCompanyNameReading` | `"Company (reading)"` | `"会社名 (かな)"` |
326
+ | `fieldAddress` | `"Address"` | `"住所"` |
327
+ | `fieldPostalCode` | `"Postal code"` | `"郵便番号"` |
328
+ | `fieldUrl` | `"URL"` | `"URL"` |
329
+ | `fieldCustomEmail` | `"Custom email"` | `"カスタムメール"` |
330
+ | `fieldEmail` | `"Email"` | `"メール"` |
331
+ | `fieldCustomMobilePhone` | `"Custom mobile"` | `"カスタム携帯番号"` |
332
+ | `fieldMobilePhone` | `"Mobile"` | `"携帯番号"` |
333
+ | `fieldCompanyPhone` | `"Company phone"` | `"会社電話番号"` |
334
+ | `fieldCompanyFax` | `"Company fax"` | `"会社 FAX 番号"` |
335
+ | `fieldCreatedBy` | `"Created by"` | `"作成者"` |
336
+ | `fieldCreatedAt` | `"Created at"` | `"作成日時"` |
337
+ | `fieldUpdatedBy` | `"Updated by"` | `"更新者"` |
338
+ | `fieldUpdatedAt` | `"Updated at"` | `"更新日時"` |
339
+ | `popupCustomMobilePhone` | `"Mobile (custom)"` | `"携帯電話 (カスタム)"` |
340
+ | `popupMobilePhone` | `"Mobile"` | `"携帯電話"` |
341
+ | `popupCustomCompanyPhone` | `"Company phone (custom)"` | `"会社電話 (カスタム)"` |
342
+ | `popupCompanyPhone` | `"Company phone"` | `"会社電話"` |
343
+ | `popupCustomEmail` | `"Email (custom)"` | `"メールアドレス (カスタム)"` |
344
+ | `popupEmail` | `"Email"` | `"メールアドレス"` |
345
+ | `popupGmailHint` | `"Double-click or double-tap to open Gmail"` | `"ダブルクリックまたは二度タップで Gmail を開きます"` |
346
+ | `popupLastContactDate` | `"Last contact:"` | `"最終コンタクト日:"` |
347
+ | `popupNoContactMethods` | `"No contact information to display"` | `"表示できる連絡先情報がありません"` |
348
+ | `overlayTitle` | `"Details"` | `"詳細"` |
349
+ | `overlayBackdrop` | `"Tap the background to close"` | `"背景をタップして閉じる"` |
350
+ | `settingsResetAllTitle` | `"Reset all settings"` | `"すべての設定をリセット"` |
351
+ | `settingsResetAll` | `"Reset all"` | `"すべてリセット"` |
352
+ | `settingsTabsLabel` | `"Search option tabs"` | `"検索オプション設定タブ"` |
353
+ | `settingsBasicTab` | `"Basic settings"` | `"基本設定"` |
354
+ | `settingsAdvancedTab` | `"Advanced settings"` | `"詳細設定"` |
355
+ | `indexWorkerName` | `"Index Worker"` | `"Index Worker"` |
356
+ | `levenshteinWorkerName` | `"Levenshtein Worker"` | `"Levenshtein Worker"` |
357
+ | `currentValueLabel` | `"Current:"` | `"現在値:"` |
358
+ | `resetToDefaultsTitle` | `"Reset to defaults"` | `"デフォルト値にリセット"` |
359
+ | `reset` | `"Reset"` | `"リセット"` |
360
+ | `relevanceOnlyNotice` | `"Editable only when sorting by relevance"` | `"Relevance ソート時のみ変更可能"` |
361
+ | `relevanceWeightsHeading` | `"Relevance score weights"` | `"Relevance スコア重み"` |
362
+ | `fieldWeight` | `"Field weight"` | `"フィールド重み"` |
363
+ | `scoreTuningHeading` | `"Score tuning"` | `"スコア調整"` |
364
+ | `maxResults` | `"Maximum results"` | `"最大結果数"` |
365
+ | `basicThresholdHeading` | `"Final similarity threshold"` | `"最終類似度閾値"` |
366
+ | `basicThresholdLabel` | `"Adjust the minimum score"` | `"スコアの下限を調整"` |
367
+ | `basicThresholdRangeRelevance` | `"In relevance mode a score can exceed 1.0 (range: 0.0-1.5)"` | `"Relevance モードではスコアが 1.0 を超える場合があります (範囲: 0.0-1.5)"` |
368
+ | `basicThresholdRange` | `"Range: 0.0-1.0"` | `"範囲: 0.0-1.0"` |
369
+ | `basicDisplayHeading` | `"Display options"` | `"表示オプション"` |
370
+ | `basicCaseSensitive` | `"Case-sensitive"` | `"大文字小文字を区別"` |
371
+ | `basicHighlight` | `"Highlight matches"` | `"ハイライト表示"` |
372
+ | `basicOpenEmailsWithGmail` | `"Open email addresses in Gmail compose"` | `"メールアドレスを Gmail の作成画面で開く"` |
373
+ | `basicMultiTermOperator` | `"Multi-term query operator"` | `"複数語クエリの演算子"` |
374
+ | `basicOperatorOr` | `"OR (any term matches)"` | `"OR (いずれか一致)"` |
375
+ | `basicOperatorAnd` | `"AND (all terms match)"` | `"AND (すべて一致)"` |
376
+ | `basicMultiTermHint` | `"How space-separated terms are combined (default: OR)"` | `"複数語をスペースで入力したときの結合方法 (既定は OR)"` |
377
+ | `basicSortHeading` | `"Sorting"` | `"ソート設定"` |
378
+ | `basicSortBy` | `"Sort by"` | `"ソート基準"` |
379
+ | `basicSortByScore` | `"Score"` | `"スコア"` |
380
+ | `basicSortByRelevance` | `"Relevance"` | `"関連性"` |
381
+ | `basicSortByOriginal` | `"Original order"` | `"元の順序"` |
382
+ | `basicSortOrder` | `"Sort order"` | `"ソート順序"` |
383
+ | `basicSortDescending` | `"Descending"` | `"降順"` |
384
+ | `basicSortAscending` | `"Ascending"` | `"昇順"` |
385
+ | `basicEngineHeading` | `"Search engine"` | `"検索エンジン設定"` |
386
+ | `basicStrategy` | `"Parallel search strategy"` | `"並列検索戦略"` |
387
+ | `basicStrategyBalanced` | `"Balanced"` | `"バランス"` |
388
+ | `basicStrategyIndexFirst` | `"Index first"` | `"Index 優先"` |
389
+ | `basicStrategyLevenshteinFirst` | `"Levenshtein first"` | `"Levenshtein 優先"` |
390
+ | `basicIndexFiltering` | `"Index filtering"` | `"Index フィルタリング"` |
391
+ | `basicLevenshtein` | `"Levenshtein distance"` | `"Levenshtein 距離計算"` |
392
+ | `advancedLearningHeading` | `"Learning weight"` | `"学習重み"` |
393
+ | `advancedLearningLabel` | `"Weight factor"` | `"重み係数"` |
394
+ | `advancedLearningHint` | `"Weight applied when re-learning (higher favors past search results)"` | `"再学習時の重み付け (大きいほど過去の検索結果を優先)"` |
395
+ | `advancedDebounceHeading` | `"Debounce time"` | `"デバウンス時間"` |
396
+ | `advancedDebounceLabel` | `"Input delay (ms)"` | `"入力遅延 (ms)"` |
397
+ | `advancedDebounceHint` | `"How long input is awaited. Shorter is more real-time, longer reduces load"` | `"入力の待ち時間。短いほどリアルタイム、長いほど負荷軽減"` |
398
+ | `indexSettingsHeading` | `"Index Worker settings"` | `"Index Worker 設定"` |
399
+ | `indexBuildHeading` | `"Index build"` | `"インデックス構築"` |
400
+ | `indexNgramSize` | `"N-gram size"` | `"N-gram サイズ"` |
401
+ | `indexNgramSizeHint` | `"How many characters each chunk holds. Smaller is faster but less accurate"` | `"文字列を何文字単位で分割するか。小さいほど高速だが精度低下"` |
402
+ | `indexMinNgramOverlap` | `"Minimum N-gram matches"` | `"最小 N-gram 一致数"` |
403
+ | `indexMinNgramOverlapHint` | `"Candidates with fewer matches are excluded"` | `"一致数がこの値を下回る候補は除外"` |
404
+ | `indexCandidateHeading` | `"Candidate filtering"` | `"候補絞り込み"` |
405
+ | `indexThreshold` | `"Index threshold"` | `"Index 閾値"` |
406
+ | `indexThresholdHint` | `"Lower bound of the Jaro-Winkler similarity (range: 0.0-1.0)"` | `"Jaro-Winkler 類似度の下限 (範囲: 0.0-1.0)"` |
407
+ | `indexNgramOverlapThreshold` | `"N-gram overlap threshold"` | `"N-gram 重複度閾値"` |
408
+ | `indexNgramOverlapThresholdHint` | `"How much N-grams must overlap. Higher narrows candidates more strictly"` | `"N-gram の重なり具合。高いほど厳密に候補を絞る"` |
409
+ | `indexStrategyHeading` | `"Search strategy and candidates"` | `"検索戦略と候補数"` |
410
+ | `indexStrategy` | `"Search strategy"` | `"検索戦略"` |
411
+ | `indexStrategyFast` | `"Fast"` | `"Fast (高速)"` |
412
+ | `indexStrategyAccurate` | `"Accurate"` | `"Accurate (正確)"` |
413
+ | `indexStrategyHybrid` | `"Hybrid (adaptive)"` | `"Hybrid (動的)"` |
414
+ | `indexStrategyHint` | `"Balance processing load against accuracy"` | `"処理負荷と精度のバランスを選択"` |
415
+ | `indexMinCandidatesRatio` | `"Minimum candidate ratio"` | `"最小候補比率"` |
416
+ | `indexMinCandidatesRatioHint` | `"Share of candidates always kept"` | `"最低限確保する候補割合"` |
417
+ | `indexMaxCandidatesRatio` | `"Maximum candidate ratio"` | `"最大候補比率"` |
418
+ | `indexMaxCandidatesRatioHint` | `"Upper bound on candidates; narrowed dynamically when exceeded"` | `"候補数の上限。超えると動的に絞り込み"` |
419
+ | `indexJaroWinklerPrefix` | `"Jaro-Winkler prefix weight"` | `"Jaro-Winkler 接頭辞重み"` |
420
+ | `indexJaroWinklerPrefixHint` | `"Bonus given to a matching prefix"` | `"先頭一致に付与するボーナス値"` |
421
+ | `indexMaxResultsHint` | `"Maximum number of results to fetch. Larger is more thorough but slower"` | `"取得する最大件数。大きいほど包括的だが遅延が増加"` |
422
+ | `indexFieldWeightHint` | `"How much the number of matched fields counts"` | `"マッチフィールド数にどれだけ重み付けするか"` |
423
+ | `indexPerfectMatchBonus` | `"Exact match bonus"` | `"完全一致ボーナス"` |
424
+ | `indexPerfectMatchBonusHint` | `"Bonus added on an exact match"` | `"完全一致時に追加するボーナス値"` |
425
+ | `indexContainmentHeading` | `"Containment score (Japanese / substrings)"` | `"和文・部分文字列包含スコア"` |
426
+ | `indexContainmentBase` | `"Containment base score (containmentScoreBase)"` | `"包含 基礎スコア (containmentScoreBase)"` |
427
+ | `indexContainmentBaseHint` | `"Minimum base score when the query is fully contained in the string (rescues short Japanese queries such as 2 characters)"` | `"クエリが文字列中に完全に含まれる場合の最低基礎スコア (和文 2 文字等の救済)"` |
428
+ | `indexContainmentCoverage` | `"Containment coverage weight (containmentCoverageWeight)"` | `"包含 被覆率重み (containmentCoverageWeight)"` |
429
+ | `indexContainmentCoverageHint` | `"Extra score by the query length / field length ratio (total capped at 1.0)"` | `"クエリ長 / フィールド長の比率に応じた加算スコア (合計 1.0 以下に制限)"` |
430
+ | `levenshteinSettingsHeading` | `"Levenshtein Worker settings"` | `"Levenshtein Worker 詳細設定"` |
431
+ | `levenshteinCandidateHeading` | `"Candidate filter"` | `"候補フィルタ"` |
432
+ | `levenshteinThreshold` | `"Levenshtein threshold"` | `"Levenshtein 閾値"` |
433
+ | `levenshteinThresholdHintRelevance` | `"Keep only results at or above this threshold (range: 0.0-1.5)"` | `"この閾値以上の結果のみ保持 (範囲: 0.0-1.5)"` |
434
+ | `levenshteinThresholdHint` | `"Keep only results at or above this threshold (range: 0.0-1.0)"` | `"この閾値以上の結果のみ保持 (範囲: 0.0-1.0)"` |
435
+ | `levenshteinLengthSimilarity` | `"Length similarity threshold"` | `"長さ類似度閾値"` |
436
+ | `levenshteinLengthSimilarityHint` | `"Exclude candidates whose length differs greatly, for speed"` | `"長さが大きく異なる候補を事前に除外して速度向上"` |
437
+ | `levenshteinLengthPenalty` | `"Length difference penalty"` | `"長さ差ペナルティ"` |
438
+ | `levenshteinLengthPenaltyHint` | `"How much a length difference is penalized. Larger favors equal lengths"` | `"長さ差をどれだけ減点するか。大きいほど長さ一致を重視"` |
439
+ | `levenshteinResultsHeading` | `"Result count"` | `"結果数制御"` |
440
+ | `levenshteinMaxResultsHint` | `"Maximum number of results to return. Larger is more thorough but slower"` | `"返却する最大件数。大きいほど網羅的だが処理時間が増加"` |
441
+ | `levenshteinFieldWeightHint` | `"Weight of the number of matched fields"` | `"一致フィールド数の評価重み"` |
442
+ | `levenshteinPartialBonus` | `"Partial match bonus"` | `"部分一致ボーナス"` |
443
+ | `levenshteinPartialBonusHint` | `"Extra score for a partial match"` | `"部分一致に付与する追加スコア"` |
444
+ | `statsRebuildingTitle` | `"Rebuilding the index"` | `"インデックス再構築中"` |
445
+ | `statsRebuildIndexTabOnlyTitle` | `"Available on the Index tab only"` | `"Index タブでのみ使用可能"` |
446
+ | `statsRebuildTitle` | `"Rebuild the index"` | `"インデックスを再構築"` |
447
+ | `statsRebuilding` | `"Rebuilding..."` | `"再構築中..."` |
448
+ | `statsRebuild` | `"Rebuild Index"` | `"インデックス再構築"` |
449
+ | `statsResetTitle` | `"Reset statistics"` | `"統計をリセット"` |
450
+ | `statsOverviewTab` | `"Overview"` | `"概要"` |
451
+ | `statsIndexTab` | `"Index"` | `"Index"` |
452
+ | `statsLevenshteinTab` | `"Levenshtein"` | `"Levenshtein"` |
453
+ | `statTotalSearches` | `"Total searches"` | `"総検索回数"` |
454
+ | `statAverageSearchTime` | `"Average search time"` | `"平均検索時間"` |
455
+ | `statDataCount` | `"Data items"` | `"データ件数"` |
456
+ | `statResultCount` | `"Search results"` | `"検索結果件数"` |
457
+ | `statDisplayedCount` | `"Displayed items"` | `"表示件数"` |
458
+ | `statsNoData` | `"No statistics yet"` | `"統計データなし"` |
459
+ | `statsBasicHeading` | `"Basic statistics"` | `"基本統計"` |
460
+ | `statTotalProcessingTime` | `"Total processing time"` | `"総処理時間"` |
461
+ | `statLastSearchTime` | `"Last search time"` | `"最後の検索時間"` |
462
+ | `statErrorCount` | `"Errors"` | `"エラー数"` |
463
+ | `statUpdatedAt` | `"Statistics updated at"` | `"統計更新時刻"` |
464
+ | `statsOperationsHeading` | `"Operations"` | `"操作統計"` |
465
+ | `statTotalOperations` | `"Total operations"` | `"総操作数"` |
466
+ | `statsIndexHeading` | `"Index statistics"` | `"インデックス統計"` |
467
+ | `statIndexBuilds` | `"Index builds"` | `"インデックス構築回数"` |
468
+ | `statDataSize` | `"Data size"` | `"データサイズ"` |
469
+ | `statLastBuildTime` | `"Last build time"` | `"最後の構築時間"` |
470
+ | `statsCacheHeading` | `"Cache statistics"` | `"キャッシュ統計"` |
471
+ | `statCacheNgram` | `"N-gram"` | `"N-gram"` |
472
+ | `statCacheWord` | `"Words"` | `"単語"` |
473
+ | `statCachePhonetic` | `"Phonetic"` | `"音韻"` |
474
+ | `statsIndexMetadataHeading` | `"Index metadata"` | `"インデックスメタデータ"` |
475
+ | `statsCalculationsHeading` | `"Calculations"` | `"計算統計"` |
476
+ | `statTotalCalculations` | `"Total calculations"` | `"総計算数"` |
477
+ | `statDistanceCalculations` | `"Distance calculations"` | `"距離計算数"` |
478
+ | `statAverageDistance` | `"Average distance"` | `"平均距離"` |
479
+ | `statMinDistance` | `"Minimum distance"` | `"最小距離"` |
480
+ | `statMaxDistance` | `"Maximum distance"` | `"最大距離"` |
481
+ | `notAvailable` | `"N/A"` | `"N/A"` |
482
+ | `statsMatchesHeading` | `"Matches"` | `"マッチ統計"` |
483
+ | `statPartialMatches` | `"Partial matches"` | `"部分マッチ数"` |
484
+ | `statPerfectMatches` | `"Exact matches"` | `"完全マッチ数"` |
485
+ | `statEarlyTerminations` | `"Early terminations"` | `"早期終了数"` |
486
+ | `statsSimilarityHeading` | `"Similarity distribution"` | `"類似度分布"` |
487
+ | `statAbove80` | `"80% or more"` | `"80% 以上"` |
488
+ | `statAbove60` | `"60% or more"` | `"60% 以上"` |
489
+ | `statAbove40` | `"40% or more"` | `"40% 以上"` |
490
+ | `statBelow40` | `"Below 40%"` | `"40% 未満"` |
491
+ | `statsDistanceHeading` | `"Distance distribution (top 6)"` | `"距離分布 (上位 6)"` |
492
+ | `statDistance(distance)` | `"Distance 2"` | `"距離 2"` |
493
+ | `statsQueryLengthHeading` | `"Query lengths (top 6)"` | `"クエリ長統計 (上位 6)"` |
494
+ | `statQueryLength(length)` | `"Length 4"` | `"長さ 4"` |
495
+ | `statQueryLengthValue({ count, averagePercent })` | `"3 times / average 87.5%"` | `"3 回 / 平均 87.5%"` |
496
+ | `statsFieldPerformanceHeading` | `"Field performance (top 6)"` | `"フィールドパフォーマンス (上位 6)"` |
497
+ | `statAverageScore` | `"Average score"` | `"平均スコア"` |
498
+ | `statMatchCount` | `"Matches"` | `"マッチ数"` |
499
+ | `statBestWorstScore` | `"Best / worst score"` | `"最高 / 最低スコア"` |
500
+ | `statsInsightsHeading` | `"Performance insights"` | `"パフォーマンス洞察"` |
501
+ | `statBestField` | `"Best-performing field"` | `"最高パフォーマンスフィールド"` |
502
+ | `statRecentAverageTime` | `"Recent average processing time"` | `"最近の平均処理時間"` |
503
+ | `statRecentAverageResults` | `"Recent average results"` | `"最近の平均結果数"` |
504
+ | `statThroughput` | `"Throughput"` | `"スループット"` |
505
+ | `statCommonQueryLength` | `"Most common query length"` | `"最頻出クエリ長"` |
506
+ | `statHistoryEntries` | `"History entries"` | `"履歴エントリ数"` |
507
+ | `statHighSimilarity` | `"High similarity"` | `"高類似"` |
508
+ | `statMediumSimilarity` | `"Medium similarity"` | `"中類似"` |
509
+ | `statLowSimilarity` | `"Low similarity"` | `"低類似"` |
510
+ | `statVeryLowSimilarity` | `"Very low similarity"` | `"極低類似"` |
511
+ | `statsRecentHistoryHeading` | `"Recent history (latest 3)"` | `"最近の履歴 (最新 3 件)"` |
512
+ | `statsHistoryEntry({ processingTimeMs, resultCount })` | `"Processing time: 1.50 ms / results: 8"` | `"処理時間: 1.50 ms / 結果: 8 件"` |
513
+ | `timesUnit(displayed)` | `" times"` | `" 回"` |
514
+ | `itemsUnit(displayed)` | `" items"` | `" 件"` |
515
+ | `charactersUnit(displayed)` | `" characters"` | `" 文字"` |
516
+ | `throughputUnit` | `" ops/s"` | `" ops/秒"` |
517
+
518
+ ### Migrating from 0.1.x to 0.2.0 / 0.1.x から 0.2.0 への移行
519
+
520
+ Breaking changes / 破壊的変更:
521
+
522
+ - **Default chrome language is `en`.** Hosts that relied on the former Japanese chrome must opt in with `locale="ja"` on `ContactMapAppView` or `config.locale = "ja"` in `ContactMapConfigProvider`. `locale` is the chrome language only; there is no `formatLocale` prop (digit grouping follows `locale`, see above).
523
+ - **Invalid chrome settings fail fast.** An unsupported `locale` (`"ja-JP"`, `"EN"`, `""`, ...) or an invalid `labels` object throws a `RangeError` when the provider mounts.
524
+ - **`WorkerStatsSection`** takes `kind: "index" | "levenshtein"` instead of `title: string`.
525
+ - **`SwipeOverlay`'s `title`** no longer defaults to `"詳細"` and **`ContactMapAppView`'s `title`** has no hard-coded default; omitted, they use the `overlayTitle` / `appTitle` labels.
526
+ - **Tree builder API.** `new IncrementalTreeBuilder(naming)` and `buildDirectoryTree(records, naming)` require `naming: ContactMapTreeNaming`. A directory entry's `name` is now the bare company / department name and its member count is `memberCount` (the former `"<name> (N)"` is composed by the client with `treeDirectoryName`).
527
+ - **Tree worker API.** `TreeWorkerSession.syncRecords(records, naming)` and `syncTreeWorkerRecords(records, naming)` take the names; the `RESET_TREE` message carries `naming` and must precede a generation's `APPEND_RECORDS` / `FINALIZE_TREE` (the worker throws otherwise). `useTreeWorker(records, isComplete)` keeps its signature and reads the names from the provider.
528
+ - **Fallback names are chrome.** In `en` a contact without a company is under `"Uncategorized"` and a nameless person is `"(No Name)"` everywhere (formerly the fixed Japanese `未分類` / `名前未設定` in the tree). With `locale="ja"` they stay `未分類` / `名前未設定`; `noName` in the detail panel, popup and list is `名前未設定` too.
529
+ - **Counts are grouped with `"en-US"` / `"ja-JP"`** by chrome locale instead of the runtime default; the registered count, the result count and the total search count are now grouped as well (`1,234` instead of `1234`).
530
+ - **Peer floors.** `@aiquants/virtualscroll` ^3.7.0, `@aiquants/directory-tree` ^3.12.0 and `@aiquants/select-box` ^0.10.0 (all expanded from `workspace:^` at publish). contact-map uses only the text normalization helpers of select-box and renders no `SelectBox`, so the select-box floor comes from the `workspace:^` expansion, not from a new API.
531
+
532
+ - **既定のクローム言語は `en`。** 従来の日本語クロームに依存していたホストは `ContactMapAppView` に `locale="ja"`、または `ContactMapConfigProvider` の `config.locale = "ja"` で明示的に選ぶ。`locale` はクローム言語だけで、`formatLocale` prop は無い (桁区切りは `locale` に従う。上記参照)。
533
+ - **不正なクローム設定は即座に失敗。** 非対応の `locale` (`"ja-JP"`、`"EN"`、`""` など) や不正な `labels` はプロバイダーのマウント時に `RangeError`。
534
+ - **`WorkerStatsSection`** は `title: string` の代わりに `kind: "index" | "levenshtein"` を受け取る。
535
+ - **`SwipeOverlay` の `title`** は `"詳細"` を既定にせず、**`ContactMapAppView` の `title`** も固定の既定値を持たない。省略時は `overlayTitle` / `appTitle` ラベル。
536
+ - **ツリービルダー API。** `new IncrementalTreeBuilder(naming)` と `buildDirectoryTree(records, naming)` は `naming: ContactMapTreeNaming` が必須。ディレクトリエントリの `name` は会社名 / 部署名そのもので、所属人数は `memberCount` (従来の `"<名前> (N)"` はクライアントが `treeDirectoryName` で組み立てる)。
537
+ - **ツリー Worker API。** `TreeWorkerSession.syncRecords(records, naming)` と `syncTreeWorkerRecords(records, naming)` は代替名を受け取る。`RESET_TREE` メッセージは `naming` を運び、世代の `APPEND_RECORDS` / `FINALIZE_TREE` より先に送ることが必須 (違反すると Worker が例外)。`useTreeWorker(records, isComplete)` のシグネチャは変わらず、代替名はプロバイダーから読む。
538
+ - **代替名はクローム。** `en` では会社名の無い連絡先は `"Uncategorized"` の下、氏名の無い人物はどこでも `"(No Name)"` (従来ツリーでは固定の日本語 `未分類` / `名前未設定`)。`locale="ja"` では `未分類` / `名前未設定` のままで、詳細パネル・ポップアップ・一覧の `noName` も `名前未設定`。
539
+ - **件数はクロームロケールに応じて `"en-US"` / `"ja-JP"` で桁区切り** (実行環境の既定ではない)。登録件数・検索結果件数・総検索回数も桁区切りされる (`1234` ではなく `1,234`)。
540
+ - **ピア下限。** `@aiquants/virtualscroll` ^3.7.0、`@aiquants/directory-tree` ^3.12.0、`@aiquants/select-box` ^0.10.0 (いずれも公開時に `workspace:^` から展開)。contact-map は select-box のテキスト正規化関数だけを使い `SelectBox` を描画しないため、select-box の下限は新 API ではなく `workspace:^` の展開による。
541
+
542
+ New API / 新しい API:
543
+
544
+ - `locale` and `labels` on `ContactMapConfig` and `ContactMapAppViewProps`; `@aiquants/contact-map/client` exports `CONTACT_MAP_LABEL_KEYS`, `CONTACT_MAP_LABEL_CATALOGS`, `resolveContactMapLabels`, `useContactMapLabels` and the types `ContactMapLocale`, `ContactMapLabels`, `ContactMapOwnLabels`, `ContactMapLabelOverrides`, `ContactMapStreamProgress`, `ContactMapQueryLengthStat`, `ContactMapHistoryEntry`, `WorkerStatsKind`.
545
+ - `@aiquants/contact-map` exports `ContactMapTreeNaming` and `assertContactMapTreeNaming`; `ContactMapDirectoryEntry` gains `memberCount`.
546
+ - `ContactMapConfig` と `ContactMapAppViewProps` の `locale` / `labels`。`@aiquants/contact-map/client` から `CONTACT_MAP_LABEL_KEYS`、`CONTACT_MAP_LABEL_CATALOGS`、`resolveContactMapLabels`、`useContactMapLabels` と型 `ContactMapLocale`、`ContactMapLabels`、`ContactMapOwnLabels`、`ContactMapLabelOverrides`、`ContactMapStreamProgress`、`ContactMapQueryLengthStat`、`ContactMapHistoryEntry`、`WorkerStatsKind` を公開。
547
+ - `@aiquants/contact-map` から `ContactMapTreeNaming` と `assertContactMapTreeNaming` を公開し、`ContactMapDirectoryEntry` に `memberCount` を追加。
548
+
549
+ ```tsx
550
+ // before (0.1.x): Japanese chrome without any prop
551
+ <ContactMapAppView renderHeader={renderHeader} workerUrls={workerUrls} />
552
+
553
+ // after (0.2.0): opt into the Japanese catalog explicitly
554
+ <ContactMapAppView locale="ja" renderHeader={renderHeader} workerUrls={workerUrls} />
555
+ ```
556
+
557
+ ```ts
558
+ // before (0.1.x)
559
+ const { tree } = buildDirectoryTree(records)
560
+ session.syncRecords(records)
561
+
562
+ // after (0.2.0): pass the fallback names of the chrome locale
563
+ const naming = { uncategorizedCompany: labels.uncategorizedCompany, unnamedPerson: labels.noName }
564
+ const { tree } = buildDirectoryTree(records, naming)
565
+ session.syncRecords(records, naming)
566
+ ```
567
+
126
568
  ---
127
569
 
128
570
  ## License / ライセンス