@kubohiroya/turbowarp-extension-security-dialog 0.1.0 → 0.2.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/NOTICE.md CHANGED
@@ -30,3 +30,5 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30
30
  ## Public Editor screenshots
31
31
 
32
32
  `assets/screenshots/required-modes-{en,ja}-20261025.png` are unmodified browser captures of the public Editor on 25 October 2026, showing Camera Source 0.19.3, Asset Cache 0.3.1 and Bubble 0.13.4. Their origin, Editor revisions and hashes are recorded in `assets/screenshots/capture-20261025.json`. The captured UI, icons and banners retain their respective rights holders and applicable licenses; this package does not relicense those elements.
33
+
34
+ `assets/screenshots/localized-descriptions-{en,ja}-20261025.png` are unmodified captures of the updated public Editor on 25 October 2026, showing localized descriptions for Camera Source 0.19.4, Asset Cache 0.3.2 and Bubble 0.13.5. Their origin, revisions and hashes are recorded in `assets/screenshots/localized-descriptions-20261025.json`. The captured UI, icons and banners retain their respective rights holders and applicable licenses.
package/README.ja.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # turbowarp-extension-security-dialog
2
2
 
3
- [English](./README.md) | **日本語**
3
+ [English](README.md) | 日本語
4
4
 
5
5
  TurboWarpの複数の機能拡張を、1つの一覧で確認するための共通パッケージです。
6
6
  関連タスクと受け入れ基準は [scratch-gui #33](https://github.com/kubohiroya/scratch-gui/issues/33) に記録しています。
7
7
 
8
8
  ## 公開Editorのスクリーンショット
9
9
 
10
- [英語版](./README.md)と日本語版は、同じダイアログの表示言語を切り替えています。
10
+ [英語版](./README.md)と日本語版は、同じダイアログの表示言語を切り替え、ラベルと拡張の説明文を各言語で表示しています。
11
11
  2026年10月25日の[公開Editor](https://kubohiroya.github.io/turbowarp-extension-data-editor/editor.html?lang=ja)で、
12
- Camera Source 0.19.3・Asset Cache 0.3.1・Bubble 0.13.4を開いた実画面です。
12
+ Camera Source 0.19.4・Asset Cache 0.3.2・Bubble 0.13.5を開いた実画面です。
13
13
 
14
- ![3拡張の日本語確認画面。サンドボックスありは非対応で無効、サンドボックスなしは動作に必要。サンドボックス内では動作しない理由を表示](https://raw.githubusercontent.com/kubohiroya/turbowarp-extension-security-dialog/main/assets/screenshots/required-modes-ja-20261025.png)
14
+ ![Camera Source・Asset Cache・Bubbleの説明が日本語で表示された確認画面。サンドボックスありは非対応で無効、サンドボックスなしは動作に必要。サンドボックス内では動作しない理由を表示](https://raw.githubusercontent.com/kubohiroya/turbowarp-extension-security-dialog/main/assets/screenshots/localized-descriptions-ja-20261025.png)
15
15
 
16
16
  3拡張とも`Unsandboxed: required`を宣言しています。「サンドボックスあり」は無効化され、
17
17
  「この拡張はサンドボックス内では動作しません。」と理由が表示されます。
@@ -25,7 +25,7 @@ Camera Source 0.19.3・Asset Cache 0.3.1・Bubble 0.13.4を開いた実画面で
25
25
  | Bubble | ホストの実行環境と描画機能へ直接アクセスして、ステージ上に重ねた吹き出しや立ち絵を描画・更新します。 |
26
26
 
27
27
  画像はダイアログ部分をブラウザーで直接撮影したもので、加工していません。
28
- 取得元の版・Editor build・画像hashは[撮影台帳](./assets/screenshots/capture-20261025.json)に記録しています。
28
+ 取得元の版・Editor build・画像hashは[撮影台帳](./assets/screenshots/localized-descriptions-20261025.json)に記録しています。
29
29
 
30
30
  ## 連動するパッケージ
31
31
 
@@ -186,6 +186,22 @@ HTTP/HTTPSの出所URLは完全なURLを表示し、別タブで開きます。
186
186
  プレビュー枠内をスクロールして確認できます。ポインターを外すかEscapeで閉じ、選択や詳細状態を保持します。
187
187
  メタデータの非同期取得中も実行方法を選べます。ソースの一覧内表示は引き続き「詳細」を押したときだけです。
188
188
 
189
+ ### 機能拡張の説明文の言語
190
+
191
+ 拡張コードを実行せず、任意の言語別ヘッダーコメントを読み取ります。
192
+
193
+ ```js
194
+ // Description: Share one camera stream between extensions.
195
+ // Description[ja]: 1つのカメラ映像を複数の機能拡張で共有します。
196
+ // Description[ja-JP]: 日本語の説明(日本向け)。
197
+ ```
198
+
199
+ 言語タグの大文字・小文字は区別しません。Editorの言語の完全一致、基底言語、従来の`Description`の順で説明文を選びます。表示中に言語を変えると、ソースを再取得せず説明が切り替わり、許可の選択状態は保たれます。翻訳がない既存の拡張も従来どおり表示できます。
200
+
201
+ 同梱メンバーのnoticeにある説明は親バンドルへ混ぜません。HTMLに見える説明も文字列として表示します。独自の`loadInfo`を使う場合は、任意の`ExtensionMetadata.descriptions`へ小文字の言語タグをキーとした説明を渡せます。`getExtensionDescription(metadata, locale)`でも同じ選択処理を利用できます。
202
+
203
+ 上の画像は、日本語説明へ対応した公開Editorと公開済みの3拡張を使用しています。英語版では各拡張の従来の`Description`、日本語版では`Description[ja]`を表示します。対応する翻訳がない拡張は元の説明を表示します。説明の言語が変わっても、必要な実行モードや許可の選択は変わりません。パッケージを更新しても、既存SB3に埋め込まれたコードは自動で差し替わりません。
204
+
189
205
  ## 実行モードの選択
190
206
 
191
207
  個別選択・一括選択・一覧の見出しは、禁止アイコンの「拒否」、盾の「サンドボックスあり」、
@@ -238,8 +254,8 @@ scratch-guiは既定OFFの起動時フラグで導入し、ホスト独自のセ
238
254
  ## 開発と検証
239
255
 
240
256
  ```sh
241
- npm install
242
- npm test
257
+ pnpm install
258
+ pnpm test
243
259
  ```
244
260
 
245
261
  GUIとの統合では、実VMでの拒否後の表示・保存、エディター初期画面への遷移、
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # turbowarp-extension-security-dialog
2
2
 
3
- **English** | [日本語](./README.ja.md)
3
+ English | [日本語](README.ja.md)
4
4
 
5
5
  A shared package that reviews every custom extension a TurboWarp project is about to load
6
6
  in **one single list**, instead of one prompt per extension.
@@ -9,12 +9,12 @@ Related tasks and acceptance criteria are tracked in
9
9
 
10
10
  ## Screenshots from the public Editor
11
11
 
12
- [English](./README.md) and [Japanese](./README.ja.md) use the same dialog with localized labels.
13
- This capture shows Camera Source 0.19.3, Asset Cache 0.3.1 and Bubble 0.13.4 in the
12
+ [English](./README.md) and [Japanese](./README.ja.md) use the same dialog with localized labels and extension descriptions.
13
+ This capture shows Camera Source 0.19.4, Asset Cache 0.3.2 and Bubble 0.13.5 in the
14
14
  [public Editor](https://kubohiroya.github.io/turbowarp-extension-data-editor/editor.html?lang=en)
15
15
  on 25 October 2026.
16
16
 
17
- ![English extension review: With sandbox is disabled as Unsupported for all three extensions; Without sandbox is marked Required to run, with the reason This extension cannot run inside the sandbox.](https://raw.githubusercontent.com/kubohiroya/turbowarp-extension-security-dialog/main/assets/screenshots/required-modes-en-20261025.png)
17
+ ![English descriptions for Camera Source, Asset Cache and Bubble: With sandbox is disabled as Unsupported for all three extensions; Without sandbox is marked Required to run, with the reason This extension cannot run inside the sandbox.](https://raw.githubusercontent.com/kubohiroya/turbowarp-extension-security-dialog/main/assets/screenshots/localized-descriptions-en-20261025.png)
18
18
 
19
19
  All three extensions declare `Unsandboxed: required`. **With sandbox** is disabled, with
20
20
  **This extension cannot run inside the sandbox.** shown as the reason. **Without sandbox**
@@ -28,7 +28,7 @@ choice and OK. These labels describe execution requirements, not a safety certif
28
28
  | Bubble | Accesses the host runtime and renderer directly to draw and update layered speech bubbles and portraits on the stage. |
29
29
 
30
30
  The PNG is an unmodified, modal-only browser capture. Source versions, Editor build and image
31
- hashes are recorded in [the capture manifest](./assets/screenshots/capture-20261025.json).
31
+ hashes are recorded in [the capture manifest](./assets/screenshots/localized-descriptions-20261025.json).
32
32
 
33
33
  ## How it fits together
34
34
 
@@ -193,6 +193,29 @@ or a file timestamp.
193
193
 
194
194
  Run modes remain selectable while metadata is still loading.
195
195
 
196
+ ### Localized extension descriptions
197
+
198
+ The dialog reads optional language-tagged header comments, without evaluating extension code:
199
+
200
+ ```js
201
+ // Description: Share one camera stream between extensions.
202
+ // Description[ja]: 1つのカメラ映像を複数の機能拡張で共有します。
203
+ // Description[ja-JP]: 日本語の説明(日本向け)。
204
+ ```
205
+
206
+ Tags are case-insensitive. Display selection uses the exact Editor locale, then its base
207
+ language, then the original `Description`. A locale change updates the text without refetching
208
+ source or changing permission choices. Missing translations keep existing extensions compatible.
209
+ Only the enclosing header is used; bundled member notices cannot supply its translation.
210
+ HTML-looking text is rendered as text. The optional `ExtensionMetadata.descriptions` record uses
211
+ lowercase keys, and `getExtensionDescription(metadata, locale)` exposes the same lookup.
212
+
213
+ The public Editor and the three published bundles shown above support localized descriptions.
214
+ The English capture uses each original `Description`; the Japanese capture uses `Description[ja]`.
215
+ Extensions without a matching translation retain their original description. The selected language
216
+ does not change required modes or permission choices. Updating a package does not replace code
217
+ already embedded in an existing SB3.
218
+
196
219
  ## Choosing a run mode
197
220
 
198
221
  Per-row choices, the bulk choices and the list heading are ordered: **Deny** (prohibition
@@ -252,8 +275,8 @@ own security policy take precedence.
252
275
  ## Development and verification
253
276
 
254
277
  ```sh
255
- npm install
256
- npm test
278
+ pnpm install
279
+ pnpm test
257
280
  ```
258
281
 
259
282
  GUI integration additionally verifies display and storage after a real-VM denial, the
package/dist/index.cjs CHANGED
@@ -47,6 +47,7 @@ __export(index_exports, {
47
47
  encodeChoices: () => encodeChoices,
48
48
  fingerprint: () => fingerprint,
49
49
  formatMessage: () => formatMessage,
50
+ getExtensionDescription: () => getExtensionDescription,
50
51
  getGitHubRepository: () => getGitHubRepository,
51
52
  getPreviewCandidates: () => getPreviewCandidates,
52
53
  loadExtensionInfo: () => loadExtensionInfo,
@@ -125,6 +126,19 @@ var parseExtensionMetadata = (source) => {
125
126
  };
126
127
  const header = source.slice(0, 16384).match(/^\s*(?:(?:\/\/[^\n]*(?:\n|$))|(?:\/\*[\s\S]*?\*\/)\s*)*/)[0];
127
128
  for (const line of header.split("\n")) {
129
+ if (/^\s*(?:\/\/|\/\*|\*)\s*Bundled extensions:\s*$/i.test(line)) break;
130
+ const localized = /^\s*(?:\/\/|\/\*|\*) ?Description\[([a-z]{2,8}(?:-[a-z0-9]{1,8})*)\]:\s*(.*?)\s*(?:\*\/)?$/i.exec(line);
131
+ if (localized) {
132
+ const locale = localized[1].toLowerCase();
133
+ const description = localized[2].slice(0, 1e3);
134
+ if (description.trim() && !/[\r\n\u2028\u2029]/.test(description)) {
135
+ if (!metadata.descriptions) metadata.descriptions = {};
136
+ if (!Object.prototype.hasOwnProperty.call(metadata.descriptions, locale)) {
137
+ metadata.descriptions[locale] = description;
138
+ }
139
+ }
140
+ continue;
141
+ }
128
142
  const match = /^\s*(?:\/\/|\/\*|\*)\s*([\w -]+):\s*(.*?)\s*(?:\*\/)?$/.exec(line);
129
143
  const field = match && Object.prototype.hasOwnProperty.call(fields, match[1].toLowerCase()) && fields[match[1].toLowerCase()];
130
144
  if (field && !Object.prototype.hasOwnProperty.call(metadata, field)) {
@@ -133,6 +147,18 @@ var parseExtensionMetadata = (source) => {
133
147
  }
134
148
  return metadata;
135
149
  };
150
+ var getExtensionDescription = (metadata, locale) => {
151
+ const normalized = typeof locale === "string" ? locale.trim().toLowerCase() : "";
152
+ const descriptions = metadata.descriptions || {};
153
+ if (/^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/.test(normalized)) {
154
+ for (const candidate of [normalized, normalized.split("-")[0]]) {
155
+ if (Object.prototype.hasOwnProperty.call(descriptions, candidate) && typeof descriptions[candidate] === "string" && descriptions[candidate].trim()) {
156
+ return descriptions[candidate];
157
+ }
158
+ }
159
+ }
160
+ return metadata.description || "";
161
+ };
136
162
  var decodeExtensionDataURL = (url) => {
137
163
  const comma = url.indexOf(",");
138
164
  if (comma < 0) throw new Error("Invalid extension data URL");
package/dist/index.d.ts CHANGED
@@ -3,9 +3,11 @@ export interface ExtensionEntry { url: string; ids: string[]; banner?: string; }
3
3
  export interface ExtensionMetadata {
4
4
  name?: string; id?: string; description?: string; author?: string; version?: string; updatedAt?: string;
5
5
  license?: string; repository?: string; banner?: string; unsandboxed?: string;
6
+ descriptions?: Record<string, string>;
6
7
  }
7
8
  export interface ExtensionInfo { source: string; metadata: ExtensionMetadata; }
8
9
  export function parseExtensionMetadata(source: string): ExtensionMetadata;
10
+ export function getExtensionDescription(metadata: ExtensionMetadata, locale?: string): string;
9
11
  export function decodeExtensionDataURL(url: string): string;
10
12
  export function loadExtensionInfo(url: string, signal: AbortSignal): Promise<ExtensionInfo>;
11
13
  export function getGitHubRepository(value?: string): string | null;
package/dist/index.mjs CHANGED
@@ -80,6 +80,19 @@ var parseExtensionMetadata = (source) => {
80
80
  };
81
81
  const header = source.slice(0, 16384).match(/^\s*(?:(?:\/\/[^\n]*(?:\n|$))|(?:\/\*[\s\S]*?\*\/)\s*)*/)[0];
82
82
  for (const line of header.split("\n")) {
83
+ if (/^\s*(?:\/\/|\/\*|\*)\s*Bundled extensions:\s*$/i.test(line)) break;
84
+ const localized = /^\s*(?:\/\/|\/\*|\*) ?Description\[([a-z]{2,8}(?:-[a-z0-9]{1,8})*)\]:\s*(.*?)\s*(?:\*\/)?$/i.exec(line);
85
+ if (localized) {
86
+ const locale = localized[1].toLowerCase();
87
+ const description = localized[2].slice(0, 1e3);
88
+ if (description.trim() && !/[\r\n\u2028\u2029]/.test(description)) {
89
+ if (!metadata.descriptions) metadata.descriptions = {};
90
+ if (!Object.prototype.hasOwnProperty.call(metadata.descriptions, locale)) {
91
+ metadata.descriptions[locale] = description;
92
+ }
93
+ }
94
+ continue;
95
+ }
83
96
  const match = /^\s*(?:\/\/|\/\*|\*)\s*([\w -]+):\s*(.*?)\s*(?:\*\/)?$/.exec(line);
84
97
  const field = match && Object.prototype.hasOwnProperty.call(fields, match[1].toLowerCase()) && fields[match[1].toLowerCase()];
85
98
  if (field && !Object.prototype.hasOwnProperty.call(metadata, field)) {
@@ -88,6 +101,18 @@ var parseExtensionMetadata = (source) => {
88
101
  }
89
102
  return metadata;
90
103
  };
104
+ var getExtensionDescription = (metadata, locale) => {
105
+ const normalized = typeof locale === "string" ? locale.trim().toLowerCase() : "";
106
+ const descriptions = metadata.descriptions || {};
107
+ if (/^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/.test(normalized)) {
108
+ for (const candidate of [normalized, normalized.split("-")[0]]) {
109
+ if (Object.prototype.hasOwnProperty.call(descriptions, candidate) && typeof descriptions[candidate] === "string" && descriptions[candidate].trim()) {
110
+ return descriptions[candidate];
111
+ }
112
+ }
113
+ }
114
+ return metadata.description || "";
115
+ };
91
116
  var decodeExtensionDataURL = (url) => {
92
117
  const comma = url.indexOf(",");
93
118
  if (comma < 0) throw new Error("Invalid extension data URL");
@@ -347,6 +372,7 @@ export {
347
372
  encodeChoices,
348
373
  fingerprint,
349
374
  formatMessage,
375
+ getExtensionDescription,
350
376
  getGitHubRepository,
351
377
  getPreviewCandidates,
352
378
  loadExtensionInfo,
package/dist/react.cjs CHANGED
@@ -286,6 +286,19 @@ var parseExtensionMetadata = (source) => {
286
286
  };
287
287
  const header = source.slice(0, 16384).match(/^\s*(?:(?:\/\/[^\n]*(?:\n|$))|(?:\/\*[\s\S]*?\*\/)\s*)*/)[0];
288
288
  for (const line of header.split("\n")) {
289
+ if (/^\s*(?:\/\/|\/\*|\*)\s*Bundled extensions:\s*$/i.test(line)) break;
290
+ const localized = /^\s*(?:\/\/|\/\*|\*) ?Description\[([a-z]{2,8}(?:-[a-z0-9]{1,8})*)\]:\s*(.*?)\s*(?:\*\/)?$/i.exec(line);
291
+ if (localized) {
292
+ const locale = localized[1].toLowerCase();
293
+ const description = localized[2].slice(0, 1e3);
294
+ if (description.trim() && !/[\r\n\u2028\u2029]/.test(description)) {
295
+ if (!metadata.descriptions) metadata.descriptions = {};
296
+ if (!Object.prototype.hasOwnProperty.call(metadata.descriptions, locale)) {
297
+ metadata.descriptions[locale] = description;
298
+ }
299
+ }
300
+ continue;
301
+ }
289
302
  const match = /^\s*(?:\/\/|\/\*|\*)\s*([\w -]+):\s*(.*?)\s*(?:\*\/)?$/.exec(line);
290
303
  const field = match && Object.prototype.hasOwnProperty.call(fields, match[1].toLowerCase()) && fields[match[1].toLowerCase()];
291
304
  if (field && !Object.prototype.hasOwnProperty.call(metadata, field)) {
@@ -294,6 +307,18 @@ var parseExtensionMetadata = (source) => {
294
307
  }
295
308
  return metadata;
296
309
  };
310
+ var getExtensionDescription = (metadata, locale) => {
311
+ const normalized = typeof locale === "string" ? locale.trim().toLowerCase() : "";
312
+ const descriptions = metadata.descriptions || {};
313
+ if (/^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/.test(normalized)) {
314
+ for (const candidate of [normalized, normalized.split("-")[0]]) {
315
+ if (Object.prototype.hasOwnProperty.call(descriptions, candidate) && typeof descriptions[candidate] === "string" && descriptions[candidate].trim()) {
316
+ return descriptions[candidate];
317
+ }
318
+ }
319
+ }
320
+ return metadata.description || "";
321
+ };
297
322
  var decodeExtensionDataURL = (url) => {
298
323
  const comma = url.indexOf(",");
299
324
  if (comma < 0) throw new Error("Invalid extension data URL");
@@ -637,6 +662,7 @@ var ExtensionSecurityDialog = class extends import_react2.default.Component {
637
662
  renderRow(entry, index) {
638
663
  const row = this.state.rows[entry.url] || {};
639
664
  const metadata = row.metadata || {};
665
+ const description = getExtensionDescription(metadata, this.props.locale);
640
666
  const required = requiresUnsandboxed(metadata);
641
667
  const reasonId = `extension-mode-reason-${index}`;
642
668
  const requiredId = `extension-mode-required-${index}`;
@@ -679,7 +705,7 @@ var ExtensionSecurityDialog = class extends import_react2.default.Component {
679
705
  "data-index": index,
680
706
  onError: this.handleImageError
681
707
  }
682
- ), /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.info }, /* @__PURE__ */ import_react2.default.createElement("strong", null, name), /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.secondary }, this.t("id", { id: metadata.id || entry.ids.join(", ") })), metadata.version && /* @__PURE__ */ import_react2.default.createElement("span", { className: style_names_default.version }, `v${metadata.version}`), metadata.author && /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.secondary }, this.t("author", { author: metadata.author })), metadata.updatedAt && /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.secondary }, this.t("updated", { updated: metadata.updatedAt })), metadata.description && /* @__PURE__ */ import_react2.default.createElement("div", null, metadata.description), metadata.license && /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.secondary }, this.t("license", { license: metadata.license })), /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.secondary }, sourceLink), !row.status && /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.secondary }, this.t("pending")), row.status === "failed" && /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.error }, this.t("failed")), required && /* @__PURE__ */ import_react2.default.createElement("div", { id: reasonId, className: style_names_default.warningText }, this.t("required")), row.invalidated && !this.state.choices[entry.url] && /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.warningText, role: "status" }, this.t("choiceReset")))), MODES.map((mode) => /* @__PURE__ */ import_react2.default.createElement(
708
+ ), /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.info }, /* @__PURE__ */ import_react2.default.createElement("strong", null, name), /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.secondary }, this.t("id", { id: metadata.id || entry.ids.join(", ") })), metadata.version && /* @__PURE__ */ import_react2.default.createElement("span", { className: style_names_default.version }, `v${metadata.version}`), metadata.author && /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.secondary }, this.t("author", { author: metadata.author })), metadata.updatedAt && /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.secondary }, this.t("updated", { updated: metadata.updatedAt })), description && /* @__PURE__ */ import_react2.default.createElement("div", null, description), metadata.license && /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.secondary }, this.t("license", { license: metadata.license })), /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.secondary }, sourceLink), !row.status && /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.secondary }, this.t("pending")), row.status === "failed" && /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.error }, this.t("failed")), required && /* @__PURE__ */ import_react2.default.createElement("div", { id: reasonId, className: style_names_default.warningText }, this.t("required")), row.invalidated && !this.state.choices[entry.url] && /* @__PURE__ */ import_react2.default.createElement("div", { className: style_names_default.warningText, role: "status" }, this.t("choiceReset")))), MODES.map((mode) => /* @__PURE__ */ import_react2.default.createElement(
683
709
  "label",
684
710
  {
685
711
  className: style_names_default.choice,
package/dist/react.mjs CHANGED
@@ -255,6 +255,19 @@ var parseExtensionMetadata = (source) => {
255
255
  };
256
256
  const header = source.slice(0, 16384).match(/^\s*(?:(?:\/\/[^\n]*(?:\n|$))|(?:\/\*[\s\S]*?\*\/)\s*)*/)[0];
257
257
  for (const line of header.split("\n")) {
258
+ if (/^\s*(?:\/\/|\/\*|\*)\s*Bundled extensions:\s*$/i.test(line)) break;
259
+ const localized = /^\s*(?:\/\/|\/\*|\*) ?Description\[([a-z]{2,8}(?:-[a-z0-9]{1,8})*)\]:\s*(.*?)\s*(?:\*\/)?$/i.exec(line);
260
+ if (localized) {
261
+ const locale = localized[1].toLowerCase();
262
+ const description = localized[2].slice(0, 1e3);
263
+ if (description.trim() && !/[\r\n\u2028\u2029]/.test(description)) {
264
+ if (!metadata.descriptions) metadata.descriptions = {};
265
+ if (!Object.prototype.hasOwnProperty.call(metadata.descriptions, locale)) {
266
+ metadata.descriptions[locale] = description;
267
+ }
268
+ }
269
+ continue;
270
+ }
258
271
  const match = /^\s*(?:\/\/|\/\*|\*)\s*([\w -]+):\s*(.*?)\s*(?:\*\/)?$/.exec(line);
259
272
  const field = match && Object.prototype.hasOwnProperty.call(fields, match[1].toLowerCase()) && fields[match[1].toLowerCase()];
260
273
  if (field && !Object.prototype.hasOwnProperty.call(metadata, field)) {
@@ -263,6 +276,18 @@ var parseExtensionMetadata = (source) => {
263
276
  }
264
277
  return metadata;
265
278
  };
279
+ var getExtensionDescription = (metadata, locale) => {
280
+ const normalized = typeof locale === "string" ? locale.trim().toLowerCase() : "";
281
+ const descriptions = metadata.descriptions || {};
282
+ if (/^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/.test(normalized)) {
283
+ for (const candidate of [normalized, normalized.split("-")[0]]) {
284
+ if (Object.prototype.hasOwnProperty.call(descriptions, candidate) && typeof descriptions[candidate] === "string" && descriptions[candidate].trim()) {
285
+ return descriptions[candidate];
286
+ }
287
+ }
288
+ }
289
+ return metadata.description || "";
290
+ };
266
291
  var decodeExtensionDataURL = (url) => {
267
292
  const comma = url.indexOf(",");
268
293
  if (comma < 0) throw new Error("Invalid extension data URL");
@@ -606,6 +631,7 @@ var ExtensionSecurityDialog = class extends React2.Component {
606
631
  renderRow(entry, index) {
607
632
  const row = this.state.rows[entry.url] || {};
608
633
  const metadata = row.metadata || {};
634
+ const description = getExtensionDescription(metadata, this.props.locale);
609
635
  const required = requiresUnsandboxed(metadata);
610
636
  const reasonId = `extension-mode-reason-${index}`;
611
637
  const requiredId = `extension-mode-required-${index}`;
@@ -648,7 +674,7 @@ var ExtensionSecurityDialog = class extends React2.Component {
648
674
  "data-index": index,
649
675
  onError: this.handleImageError
650
676
  }
651
- ), /* @__PURE__ */ React2.createElement("div", { className: style_names_default.info }, /* @__PURE__ */ React2.createElement("strong", null, name), /* @__PURE__ */ React2.createElement("div", { className: style_names_default.secondary }, this.t("id", { id: metadata.id || entry.ids.join(", ") })), metadata.version && /* @__PURE__ */ React2.createElement("span", { className: style_names_default.version }, `v${metadata.version}`), metadata.author && /* @__PURE__ */ React2.createElement("div", { className: style_names_default.secondary }, this.t("author", { author: metadata.author })), metadata.updatedAt && /* @__PURE__ */ React2.createElement("div", { className: style_names_default.secondary }, this.t("updated", { updated: metadata.updatedAt })), metadata.description && /* @__PURE__ */ React2.createElement("div", null, metadata.description), metadata.license && /* @__PURE__ */ React2.createElement("div", { className: style_names_default.secondary }, this.t("license", { license: metadata.license })), /* @__PURE__ */ React2.createElement("div", { className: style_names_default.secondary }, sourceLink), !row.status && /* @__PURE__ */ React2.createElement("div", { className: style_names_default.secondary }, this.t("pending")), row.status === "failed" && /* @__PURE__ */ React2.createElement("div", { className: style_names_default.error }, this.t("failed")), required && /* @__PURE__ */ React2.createElement("div", { id: reasonId, className: style_names_default.warningText }, this.t("required")), row.invalidated && !this.state.choices[entry.url] && /* @__PURE__ */ React2.createElement("div", { className: style_names_default.warningText, role: "status" }, this.t("choiceReset")))), MODES.map((mode) => /* @__PURE__ */ React2.createElement(
677
+ ), /* @__PURE__ */ React2.createElement("div", { className: style_names_default.info }, /* @__PURE__ */ React2.createElement("strong", null, name), /* @__PURE__ */ React2.createElement("div", { className: style_names_default.secondary }, this.t("id", { id: metadata.id || entry.ids.join(", ") })), metadata.version && /* @__PURE__ */ React2.createElement("span", { className: style_names_default.version }, `v${metadata.version}`), metadata.author && /* @__PURE__ */ React2.createElement("div", { className: style_names_default.secondary }, this.t("author", { author: metadata.author })), metadata.updatedAt && /* @__PURE__ */ React2.createElement("div", { className: style_names_default.secondary }, this.t("updated", { updated: metadata.updatedAt })), description && /* @__PURE__ */ React2.createElement("div", null, description), metadata.license && /* @__PURE__ */ React2.createElement("div", { className: style_names_default.secondary }, this.t("license", { license: metadata.license })), /* @__PURE__ */ React2.createElement("div", { className: style_names_default.secondary }, sourceLink), !row.status && /* @__PURE__ */ React2.createElement("div", { className: style_names_default.secondary }, this.t("pending")), row.status === "failed" && /* @__PURE__ */ React2.createElement("div", { className: style_names_default.error }, this.t("failed")), required && /* @__PURE__ */ React2.createElement("div", { id: reasonId, className: style_names_default.warningText }, this.t("required")), row.invalidated && !this.state.choices[entry.url] && /* @__PURE__ */ React2.createElement("div", { className: style_names_default.warningText, role: "status" }, this.t("choiceReset")))), MODES.map((mode) => /* @__PURE__ */ React2.createElement(
652
678
  "label",
653
679
  {
654
680
  className: style_names_default.choice,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubohiroya/turbowarp-extension-security-dialog",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Batch extension security review, previews and per-project defaults for TurboWarp hosts",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-only",
@@ -20,9 +20,10 @@
20
20
  "files": ["dist", "react.js", "turbowarp.js", "styles.css", "webpack4.css", "README.md", "README.ja.md", "NOTICE.md", "LICENSE"],
21
21
  "scripts": {
22
22
  "build": "node scripts/build.mjs",
23
- "test": "npm run build && node --test test/*.test.cjs",
24
- "prepack": "npm run build",
25
- "prepare": "npm run build"
23
+ "test": "pnpm run build && node --test test/*.test.cjs",
24
+ "check": "pnpm run test && npm pack --dry-run --ignore-scripts",
25
+ "prepack": "pnpm run build",
26
+ "prepare": "pnpm run build"
26
27
  },
27
28
  "dependencies": {"@turbowarp/jszip": "3.11.1"},
28
29
  "peerDependencies": {"react": "^16.14.0 || ^17 || ^18 || ^19", "react-dom": "^16.14.0 || ^17 || ^18 || ^19"},
@@ -33,6 +34,7 @@
33
34
  "react": "16.14.0",
34
35
  "react-dom": "16.14.0"
35
36
  },
36
- "engines": {"node": ">=22"},
37
+ "packageManager": "pnpm@11.28.8",
38
+ "engines": {"node": ">=24"},
37
39
  "publishConfig": {"access": "public"}
38
40
  }