omni-viewer-core 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/dist/i18n/catalog.de.d.ts +1 -0
- package/dist/i18n/catalog.de.js +68 -0
- package/dist/i18n/catalog.en.js +3 -0
- package/dist/i18n/catalog.fr.d.ts +1 -0
- package/dist/i18n/catalog.fr.js +68 -0
- package/dist/i18n/catalog.it.d.ts +1 -0
- package/dist/i18n/catalog.it.js +68 -0
- package/dist/i18n/catalog.ja.d.ts +1 -0
- package/dist/i18n/catalog.ja.js +288 -0
- package/dist/i18n/catalog.ko.js +372 -26
- package/dist/i18n/catalog.th.d.ts +1 -0
- package/dist/i18n/catalog.th.js +68 -0
- package/dist/i18n/catalog.zh-cn.d.ts +1 -0
- package/dist/i18n/catalog.zh-cn.js +265 -0
- package/dist/i18n/catalog.zh-tw.d.ts +1 -0
- package/dist/i18n/catalog.zh-tw.js +52 -0
- package/dist/i18n/index.d.ts +7 -0
- package/dist/i18n/index.js +35 -2
- package/dist/parsers/archive/index.d.ts +16 -2
- package/dist/parsers/archive/index.js +39 -11
- package/dist/parsers/archive/model.d.ts +20 -0
- package/dist/parsers/audio/index.d.ts +3 -0
- package/dist/parsers/audio/index.js +5 -0
- package/dist/parsers/video/index.d.ts +3 -0
- package/dist/parsers/video/index.js +3 -0
- package/dist/styles/archive.css +1 -0
- package/dist/styles/audio.css +8 -0
- package/dist/styles/ppt.css +1 -1
- package/dist/viewers/archive/index.d.ts +17 -1
- package/dist/viewers/archive/index.js +124 -21
- package/dist/viewers/archive/styles.d.ts +1 -1
- package/dist/viewers/archive/styles.js +1 -0
- package/dist/viewers/audio/styles.js +8 -0
- package/dist/viewers/ppt/chart.js +11 -4
- package/dist/viewers/ppt/render.js +30 -3
- package/dist/viewers/ppt/styles.d.ts +1 -1
- package/dist/viewers/ppt/styles.js +1 -1
- package/package.json +2 -2
- package/dist/viewers/hwp/self-loading.d.ts +0 -2
- package/dist/viewers/hwp/self-loading.js +0 -6
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// Simplified Chinese messages owned by core. Keep the key set aligned with CATALOG_EN.
|
|
2
|
+
export const CATALOG_ZH_CN = {
|
|
3
|
+
'audio.title': '音频', 'video.title': '视频', 'dbc.title': 'CAN 数据库',
|
|
4
|
+
'audio.play': '播放', 'audio.pause': '暂停', 'audio.stop': '停止',
|
|
5
|
+
'audio.loop': '循环', 'audio.clearRegions': '清除选区', 'audio.volume': '音量',
|
|
6
|
+
'audio.zoom': '缩放', 'audio.zoomIn': '放大', 'audio.zoomOut': '缩小', 'audio.zoomFit': '适应窗口',
|
|
7
|
+
'audio.visualization': '视图', 'audio.vis.waveform': '波形', 'audio.vis.spectrogram': '频谱图',
|
|
8
|
+
'audio.info.duration': '时长', 'audio.info.sampleRate': '采样率', 'audio.info.channels': '声道',
|
|
9
|
+
'audio.info.bitDepth': '位深度', 'audio.info.format': '格式', 'audio.info.fileSize': '文件大小',
|
|
10
|
+
'audio.loading': '正在解码音频…', 'audio.status.region': '选区 {start} – {end}({duration}秒)',
|
|
11
|
+
'audio.status.looping': '正在循环选区', 'audio.status.loopTrack': '正在循环音轨',
|
|
12
|
+
'audio.fallback': '波形引擎不可用,正在使用基础播放器。',
|
|
13
|
+
'audio.error.decode': '浏览器无法解码此音频编解码器。请使用支持所需编解码器的环境,或转换文件格式。',
|
|
14
|
+
'video.play': '播放', 'video.pause': '暂停', 'video.stop': '停止',
|
|
15
|
+
'video.skipBack': '⏪ -{seconds}秒', 'video.skipForward': '⏩ +{seconds}秒',
|
|
16
|
+
'video.speed': '速度', 'video.zoom': '缩放', 'video.zoomIn': '放大', 'video.zoomOut': '缩小', 'video.zoomFit': '适应窗口',
|
|
17
|
+
'video.info.duration': '时长', 'video.info.resolution': '分辨率', 'video.info.format': '格式', 'video.info.fileSize': '文件大小',
|
|
18
|
+
'video.error.empty': '媒体文件为空。', 'video.error.tooLarge': '文件超过浏览器预览限制({limit})。',
|
|
19
|
+
'video.error.source': '此环境无法创建浏览器媒体源。',
|
|
20
|
+
'video.error.decode': '浏览器无法解码此视频编解码器。请使用支持所需编解码器的环境,或转换文件格式。',
|
|
21
|
+
'arxml.title': 'AUTOSAR XML', 'a2l.title': 'ASAM A2L', 'asc.title': 'Vector ASC 日志',
|
|
22
|
+
'blf.title': 'Vector BLF 日志', 'mf4.title': 'ASAM MDF 4', 'pcap.title': 'PCAP 捕获',
|
|
23
|
+
'pcapng.title': 'PCAPNG 捕获', 'mermaid.title': 'Mermaid 图表', 'plantuml.title': 'PlantUML 图表',
|
|
24
|
+
'shapefile.title': 'ESRI Shapefile', 'psd.title': 'Photoshop 文档',
|
|
25
|
+
'shapefile.zoomIn': '放大', 'shapefile.zoomOut': '缩小', 'shapefile.reset': '重置视图',
|
|
26
|
+
'shapefile.projection': '投影:{name}', 'shapefile.attributes': '要素属性', 'shapefile.featureLabel': '要素 {id}',
|
|
27
|
+
'shapefile.noSelection': '点击地图上的要素以查看其属性。',
|
|
28
|
+
'shapefile.noAttributes': '此 Shapefile 未提供属性表(.dbf)。',
|
|
29
|
+
'shapefile.noRecord': '此要素没有匹配的属性记录。',
|
|
30
|
+
'shapefile.dbfFailed': '无法读取属性表(.dbf):',
|
|
31
|
+
'shapefile.reprojectFailed': '重投影失败,将以原始投影显示坐标。',
|
|
32
|
+
'avro.title': 'Apache Avro', 'bag.title': 'ROS bag', 'stp.title': 'STEP 模型',
|
|
33
|
+
'db3.title': 'SQLite 数据库', 'reqif.title': '需求交换格式',
|
|
34
|
+
'automotive.search': '搜索表格…', 'automotive.copyJson': '复制 JSON',
|
|
35
|
+
'automotive.rawPreview': '原始预览', 'automotive.noData': '没有可用数据。',
|
|
36
|
+
'automotive.rowsShown': '显示 {shown} / {total} 行',
|
|
37
|
+
'mat.title': 'MATLAB MAT 文件', 'mat.search': '搜索变量和元数据…',
|
|
38
|
+
'mat.copyJson': '复制 JSON', 'mat.rawPreview': '原始预览',
|
|
39
|
+
'mat.noMatches': '没有符合当前搜索条件的行。', 'mat.rowsShown': '显示 {shown} / {total} 行',
|
|
40
|
+
'hdf5.title': 'HDF5', 'hdf5.search': '搜索表格…', 'hdf5.copyJson': '复制 JSON',
|
|
41
|
+
'hdf5.structure': '结构', 'hdf5.noData': '没有可用数据。',
|
|
42
|
+
'hdf5.matchingRows': '{count} 个匹配行', 'hdf5.matchingRowsLimited': '显示 {visible} / {total} 个匹配行',
|
|
43
|
+
'proto.title': 'Protocol Buffer', 'proto.summary': '{syntax} · {package} · {messages} 条消息 · {services} 个服务',
|
|
44
|
+
'proto.noPackage': '无软件包', 'proto.search': '搜索消息、字段、枚举或 RPC', 'proto.copyPanel': '复制面板',
|
|
45
|
+
'proto.panel.tree': '消息树', 'proto.panel.types': '类型', 'proto.panel.relationships': '关系',
|
|
46
|
+
'proto.panel.reverse': '谁在使用?', 'proto.panel.json': 'JSON 示例', 'proto.panel.breaking': '破坏性变更',
|
|
47
|
+
'proto.panel.imports': '导入', 'proto.panel.grpc': 'gRPC', 'proto.panel.docs': '文档',
|
|
48
|
+
'proto.kind.message': '消息', 'proto.kind.enum': '枚举', 'proto.kind.service': '服务', 'proto.kind.field': '字段',
|
|
49
|
+
'proto.kind.repeated': '重复', 'proto.kind.oneof': 'oneof', 'proto.kind.value': '值', 'proto.kind.rpc': 'RPC',
|
|
50
|
+
'proto.kind.stream': '流', 'proto.kind.import': '导入',
|
|
51
|
+
'proto.reverseRow': '{from} 通过 {name} 使用 {to}', 'proto.noReferences': '未找到对 {name} 的引用。',
|
|
52
|
+
'proto.noMessages': '未找到消息。', 'proto.noDocumentation': '没有文档注释。',
|
|
53
|
+
'proto.noDeclarations': '未找到声明。', 'proto.breaking.placeholder': '在此粘贴之前的 .proto 内容',
|
|
54
|
+
'proto.breaking.compare': '比较', 'proto.breaking.breaking': '破坏性',
|
|
55
|
+
'proto.breaking.none': '结构比较未检测到破坏性变更。',
|
|
56
|
+
'proto.breaking.messageRemoved': '消息已删除:{name}。',
|
|
57
|
+
'proto.breaking.fieldRemoved': '字段已删除:{message}.{field}(编号 {number})。',
|
|
58
|
+
'proto.breaking.fieldChanged': '{message} 中编号为 {number} 的字段名称或类型已更改。',
|
|
59
|
+
'parquet.title': 'Parquet', 'parquet.search': '搜索行…', 'parquet.raw': '原始', 'parquet.table': '表格',
|
|
60
|
+
'parquet.copyJson': '复制 JSON', 'parquet.exportJson': '导出 JSON',
|
|
61
|
+
'parquet.loadMore': '加载接下来的 {count} 行', 'parquet.loading': '正在加载…',
|
|
62
|
+
'parquet.loadFailed': '无法加载更多行:{message}', 'parquet.rows': '行', 'parquet.columns': '列',
|
|
63
|
+
'parquet.limited': '大文件预览:已加载 {loaded} / {total} 行。',
|
|
64
|
+
'parquet.copyCell': '复制单元格值', 'parquet.copyRow': '将行复制为 JSON', 'parquet.copyColumn': '复制列值',
|
|
65
|
+
'parquet.autoFit': '自动调整列宽', 'parquet.resize': '拖动调整大小 · 双击自动调整',
|
|
66
|
+
'parquet.copyTable': '复制表格', 'parquet.copied': '已复制到剪贴板',
|
|
67
|
+
'word.title': 'Word 文档', 'word.zoomIn': '放大', 'word.zoomOut': '缩小',
|
|
68
|
+
'word.reset': '重置', 'word.print': '打印', 'word.printUnavailable': '此平台不支持打印。',
|
|
69
|
+
'word.empty': '此文档没有可读取的文本。',
|
|
70
|
+
'diag.word.invalid-format': '无法将此文件作为 Word 文档读取。',
|
|
71
|
+
'diag.word.missing-dependency': '此平台未安装 DOCX 渲染器。',
|
|
72
|
+
'diag.word.limit-exceeded': '此 Word 文档超过 50 MB 限制。',
|
|
73
|
+
'diag.word.decompression-limit': '此 Word 文档声明的解压后数据量超过安全打开限制。',
|
|
74
|
+
'diag.word.legacy-partial': '部分旧版 Word 格式无法显示。',
|
|
75
|
+
'diag.word.embedded-partial': '部分嵌入的 Word 对象无法显示。', 'word.image': '嵌入图像',
|
|
76
|
+
'hwp.title': 'HWP 文档', 'hwp.zoomIn': '放大', 'hwp.zoomOut': '缩小', 'hwp.reset': '重置',
|
|
77
|
+
'hwp.pages': '{count} 页', 'hwp.pageLabel': '第 {page} 页', 'hwp.empty': '此文档没有页面。',
|
|
78
|
+
'hwp.loadMorePages': '加载后续页面', 'hwp.loading': '正在使用 rhwp 加载 HWP/HWPX…',
|
|
79
|
+
'diag.hwp.missing-dependency': '此平台未安装 HWP 渲染器。',
|
|
80
|
+
'diag.hwp.invalid-format': '无法将此文件作为 HWP 文档读取。',
|
|
81
|
+
'diag.hwp.corrupted': '此 HWP 文档已损坏或包含无效的页面数据。',
|
|
82
|
+
'diag.hwp.limit-exceeded': '此 HWP 文档超过 50 MB 限制。',
|
|
83
|
+
'diag.hwp.page-limit': '大文档预览:已渲染 {total} 页中的前 {rendered} 页。',
|
|
84
|
+
'ppt.title': 'PowerPoint', 'ppt.previous': '上一张幻灯片', 'ppt.next': '下一张幻灯片',
|
|
85
|
+
'ppt.jump': '转到幻灯片', 'ppt.mode.single': '单张幻灯片', 'ppt.mode.continuous': '连续显示',
|
|
86
|
+
'ppt.empty': '此演示文稿不包含幻灯片。',
|
|
87
|
+
'diag.ppt.invalid-format': '无法将此文件作为 PowerPoint 演示文稿读取。',
|
|
88
|
+
'diag.ppt.empty': '此演示文稿没有可渲染的幻灯片。',
|
|
89
|
+
'diag.ppt.pdf-missing-dependency': 'PDF 回退功能需要此平台提供 PDF.js。',
|
|
90
|
+
'diag.ppt.limit-exceeded': '只能加载此演示文稿的一部分。',
|
|
91
|
+
'markdown.title': 'Markdown', 'markdown.preview': '预览', 'markdown.split': '拆分', 'markdown.source': '源代码',
|
|
92
|
+
'markdown.render': '渲染', 'markdown.copyHtml': '复制 HTML', 'markdown.copySource': '复制源代码',
|
|
93
|
+
'markdown.undo': '撤销', 'markdown.redo': '重做', 'markdown.ready': '就绪',
|
|
94
|
+
'markdown.rendering': '正在渲染', 'markdown.rendered': '已渲染', 'markdown.renderFailed': '渲染失败',
|
|
95
|
+
'markdown.invalid': '无效', 'markdown.modified': '已修改', 'markdown.editable': '可编辑',
|
|
96
|
+
'markdown.edited': '已编辑', 'markdown.htmlCopied': '已复制 HTML', 'markdown.sourceCopied': '已复制源代码',
|
|
97
|
+
'markdown.summary': '{lines} 行,{words} 个词', 'markdown.renderedDiagrams': '已渲染 · {count} 个图表',
|
|
98
|
+
'markdown.diagramFailed': '无法渲染图表。', 'markdown.toc': '目录',
|
|
99
|
+
'markdown.copyCode': '复制代码', 'markdown.assetUnavailable': '图像不可用',
|
|
100
|
+
'diag.markdown.limit-exceeded': 'Markdown 文档超过支持的限制。',
|
|
101
|
+
'archive.title': '压缩包', 'archive.search': '搜索条目', 'archive.entries': '{count} 个条目',
|
|
102
|
+
'archive.previewUnavailable': '无法预览', 'archive.mediaUnsupported': '不支持媒体预览。',
|
|
103
|
+
'archive.encrypted': '此压缩包已加密。', 'diag.archive.invalid-format': '无法将此文件作为压缩包读取。',
|
|
104
|
+
'diag.archive.limit-exceeded': '压缩包超过支持的限制。',
|
|
105
|
+
'archive.preview': '压缩包预览', 'archive.summary': '正在以只读预览模式浏览压缩包结构。',
|
|
106
|
+
'archive.filter': '筛选', 'archive.path': '路径', 'archive.type': '类型',
|
|
107
|
+
'archive.compressedSize': '压缩后大小', 'archive.originalSize': '原始大小', 'archive.modified': '修改时间',
|
|
108
|
+
'archive.files': '文件', 'archive.directories': '目录', 'archive.visible': '可见',
|
|
109
|
+
'archive.file': '文件', 'archive.directory': '目录', 'archive.noMatches': '未找到匹配的条目。',
|
|
110
|
+
'archive.entryPreview': '条目预览', 'archive.chooseFile': '选择文件', 'archive.idle': '空闲',
|
|
111
|
+
'archive.selectHint': '选择压缩包条目以在此查看其内容。', 'archive.noSelection': '未选择文件。',
|
|
112
|
+
'archive.loading': '正在加载', 'archive.loadingHint': '正在从压缩包获取文件内容以快速预览。',
|
|
113
|
+
'archive.loadingContent': '正在加载预览…', 'archive.ready': '就绪',
|
|
114
|
+
'archive.textReady': '已将所选压缩包条目渲染为纯文本。',
|
|
115
|
+
'archive.binaryReady': '已将所选压缩包条目渲染为十六进制预览。',
|
|
116
|
+
'archive.androidBinaryXmlReady': '已从 Android 二进制 XML 解码。',
|
|
117
|
+
'archive.audioReady': '正在播放所选音频条目。',
|
|
118
|
+
'archive.imageReady': '正在显示所选图像条目。',
|
|
119
|
+
'archive.videoReady': '正在播放所选视频条目。',
|
|
120
|
+
'archive.directoryHint': '目录条目没有可内嵌显示的文件内容。',
|
|
121
|
+
'archive.directoryContent': '此条目包含子项,而非文件内容。',
|
|
122
|
+
'archive.saveEntry': '保存条目', 'archive.savingEntry': '正在保存条目…',
|
|
123
|
+
'archive.saveFailed': '无法保存所选条目。',
|
|
124
|
+
'diag.toml.invalid-table': 'TOML 表声明无效。',
|
|
125
|
+
'diag.toml.invalid-assignment': '应为 TOML 键值赋值。', 'diag.toml.invalid-key': 'TOML 键无效。',
|
|
126
|
+
'diag.toml.invalid-inline-table': 'TOML 内联表条目无效。', 'diag.toml.duplicate-key': 'TOML 键重复。',
|
|
127
|
+
'diag.toml.max-depth': 'TOML 嵌套过深。', 'diag.toml.node-limit': '已达到 TOML 节点限制。',
|
|
128
|
+
'diag.toml.table-limit': '已达到 TOML 表限制。', 'diag.toml.array-limit': '已达到 TOML 数组限制。',
|
|
129
|
+
'diag.toml.duplicate-table': 'TOML 表重复。', 'diag.toml.unclosed-value': 'TOML 值未闭合。',
|
|
130
|
+
'diag.jsonl.invalid-line': '第 {line} 行不是有效的 JSON。',
|
|
131
|
+
'diag.jsonl.line-limit': '第 {line} 行超过 JSONL 行限制。',
|
|
132
|
+
'diag.yaml.missing-dependency': '此平台未安装 YAML 解析器。',
|
|
133
|
+
'diag.yaml.invalid': '无法将此文件解析为 YAML。',
|
|
134
|
+
'diag.yaml.document-limit': '仅解析了前 {count} 个 YAML 文档。',
|
|
135
|
+
'diag.yaml.node-limit': '已达到 YAML 节点限制。', 'diag.yaml.depth-limit': '已达到 YAML 嵌套深度限制。',
|
|
136
|
+
'diag.yaml.alias-limit': '已达到 YAML 别名限制。',
|
|
137
|
+
'structured.tree': '树', 'structured.flat': '平铺', 'structured.json': 'JSON', 'structured.raw': '原始',
|
|
138
|
+
'structured.expandAll': '全部展开', 'structured.collapseAll': '全部折叠',
|
|
139
|
+
'structured.expand': '展开', 'structured.collapse': '折叠', 'structured.search': '搜索',
|
|
140
|
+
'structured.copy': '复制', 'structured.copyPath': '路径', 'structured.copyValue': '值', 'structured.copyJson': 'JSON',
|
|
141
|
+
'structured.source': '源代码', 'structured.sourceDescription': '可编辑的原始文本',
|
|
142
|
+
'structured.treeDescription': '结构化预览',
|
|
143
|
+
'toml.treeView': 'TOML', 'yaml.treeView': 'YAML', 'jsonl.title': 'JSON Lines',
|
|
144
|
+
'jsonl.loadMore': '加载更多', 'jsonl.loadAll': '全部加载', 'jsonl.save': '保存',
|
|
145
|
+
'jsonl.editUnavailable': '编辑需要完整文件。', 'diag.jsonl.load-failed': '无法加载更多 JSON Lines 数据。',
|
|
146
|
+
'jsonl.add': '添加行', 'jsonl.delete': '删除所选项', 'jsonl.edit': '编辑', 'jsonl.apply': '应用',
|
|
147
|
+
'jsonl.cancel': '取消', 'jsonl.preview': '搜索和选择仅适用于已加载的预览。',
|
|
148
|
+
'jsonl.showMore': '再显示 5,000 行', 'jsonl.invalidSave': '请先修复无效的编辑行再保存。',
|
|
149
|
+
'jsonl.rowValid': '第 {line} 行,有效 JSON', 'jsonl.rowInvalid': '第 {line} 行,无效 JSON',
|
|
150
|
+
'diag.limit-exceeded.rows': '已达到行数限制,仅解析前 {count} 行。',
|
|
151
|
+
'diag.limit-exceeded.input': '文件超过支持的最大大小({maxBytes} 字节)。',
|
|
152
|
+
'diag.limit-exceeded.decompressed': '压缩包声明的解压后数据超过支持的限制({maxBytes} 字节)。',
|
|
153
|
+
'diag.aborted': '解析已取消。',
|
|
154
|
+
'diag.csv.ragged-rows': '{count} 行的列数不同,已填充或截断。',
|
|
155
|
+
'diag.limit-exceeded.nodes': '已达到节点限制,仅解析前 {count} 个节点。',
|
|
156
|
+
'diag.json.invalid': '无法将此文件解析为 JSON。',
|
|
157
|
+
'diag.json.invalid-token': 'JSON 中存在意外字符,解析已在此停止。',
|
|
158
|
+
'diag.json.unexpected-eof': 'JSON 意外结束。',
|
|
159
|
+
'diag.json.unterminated-string': '字符串在输入结束前未闭合。',
|
|
160
|
+
'diag.json.duplicate-key': '键“{key}”重复,两个条目均会保留。',
|
|
161
|
+
'diag.json.trailing-content': '已忽略 JSON 值后的额外内容。',
|
|
162
|
+
'diag.json.max-depth': '嵌套过深,解析已在深度限制处停止。',
|
|
163
|
+
'diag.json.invalid-escape': '字符串中的转义序列无效。',
|
|
164
|
+
'diag.json.invalid-unicode-escape': '\\u 转义无效,需要四个十六进制数字。',
|
|
165
|
+
'diag.json.control-char': '字符串中包含未转义的控制字符。',
|
|
166
|
+
'diag.limit-exceeded.cells': '已达到单元格限制,仅解析前 {count} 个单元格。',
|
|
167
|
+
'diag.excel.invalid-format': '无法将此文件作为电子表格读取。',
|
|
168
|
+
'diag.excel.password-required': '此电子表格受密码保护。',
|
|
169
|
+
'diag.excel.empty-workbook': '工作簿中没有数据。',
|
|
170
|
+
'diag.excel.missing-dependency': '无法在此平台加载电子表格引擎。',
|
|
171
|
+
'common.rows': '{count} 行', 'common.columns': '{count} 列', 'common.page': '第 {page} / {pages} 页',
|
|
172
|
+
'common.prevPage': '上一页', 'common.nextPage': '下一页',
|
|
173
|
+
'common.rowsMatched': '匹配 {matched} / {total} 行', 'common.copied': '已复制到剪贴板',
|
|
174
|
+
'common.noClipboard': '此平台无法复制',
|
|
175
|
+
'common.copyTooLarge': '内容过大,无法复制到剪贴板({size} 字节)。请改用文件导出。',
|
|
176
|
+
'common.noFileSave': '此平台无法保存文件', 'common.saved': '已保存 {name}',
|
|
177
|
+
'common.savedToOriginal': '已保存', 'common.saveFailed': '保存失败',
|
|
178
|
+
'common.unsaved': '未保存的更改', 'common.noWriteback': '此处无法保存到原文件,请使用“导出文件”。',
|
|
179
|
+
'csv.delimiter': '分隔符', 'csv.delimiter.auto': '自动检测',
|
|
180
|
+
'csv.delimiter.comma': '逗号(,)', 'csv.delimiter.semicolon': '分号(;)',
|
|
181
|
+
'csv.delimiter.tab': '制表符', 'csv.delimiter.pipe': '竖线(|)',
|
|
182
|
+
'csv.copyTsv': '复制为 TSV', 'csv.copyJson': '复制为 JSON', 'csv.exportFile': '导出',
|
|
183
|
+
'csv.exportFile.hint': '将当前搜索和排序结果保存为新文件',
|
|
184
|
+
'csv.editUnavailable': '编辑需要完整解析文件', 'csv.addRow': '添加行', 'csv.addColumn': '添加列',
|
|
185
|
+
'csv.insertRowBelow': '在下方插入行', 'csv.deleteRow': '删除行',
|
|
186
|
+
'csv.insertColumnLeft': '在左侧插入列', 'csv.insertColumnRight': '在右侧插入列',
|
|
187
|
+
'csv.deleteColumn': '删除列', 'csv.undo': '撤销', 'csv.redo': '重做',
|
|
188
|
+
'csv.save': '保存', 'csv.saveAs': '另存为', 'csv.statistics': '统计',
|
|
189
|
+
'csv.rawView': '原始', 'csv.tableView': '表格', 'csv.sort.hint': '点击排序',
|
|
190
|
+
'csv.header.hint': '点击排序 · 双击重命名', 'csv.resizeColumn': '拖动调整大小 · 双击重置',
|
|
191
|
+
'csv.search': '搜索', 'csv.noMatches': '已加载范围内没有匹配行。',
|
|
192
|
+
'csv.rowNumbers': '行号', 'csv.pageSize': '每页行数',
|
|
193
|
+
'csv.stats.compact.count': 'n={count}', 'csv.stats.compact.nulls': '空值 {percent}',
|
|
194
|
+
'csv.stats.compact.numeric': 'μ{mean} · {min}~{max}', 'csv.stats.count': '数量',
|
|
195
|
+
'csv.stats.nulls': '空值', 'csv.stats.mean': '平均值', 'csv.stats.min': '最小值', 'csv.stats.max': '最大值',
|
|
196
|
+
'csv.empty': '此文件没有行。',
|
|
197
|
+
'excel.tableView': '电子表格', 'excel.sheet': '工作表', 'excel.search': '在工作表中搜索…',
|
|
198
|
+
'excel.viewMode': '视图', 'excel.tableViewLabel': '表格', 'excel.gridView': '网格', 'excel.rawView': '原始',
|
|
199
|
+
'excel.gridTruncated': '工作表较大,仅显示前 {count} 行。', 'excel.tableViewToggle': '表格视图',
|
|
200
|
+
'excel.copyTsv': '复制为 TSV', 'excel.copyJson': '复制为 JSON', 'excel.export': '导出 XLSX',
|
|
201
|
+
'excel.matchedCells': '{count} 个匹配单元格', 'excel.sortHint': '点击排序 · 双击重命名',
|
|
202
|
+
'excel.resizeColumn': '拖动调整大小 · 双击重置', 'excel.editHint': '双击编辑',
|
|
203
|
+
'excel.editUnavailable': '编辑需要完整解析的电子表格。', 'excel.undo': '撤销', 'excel.redo': '重做',
|
|
204
|
+
'excel.insertRowBelow': '在下方插入行', 'excel.deleteRow': '删除行', 'excel.empty': '此工作表没有单元格。',
|
|
205
|
+
'json.treeView': 'JSON', 'json.view.tree': '树', 'json.view.source': '源代码',
|
|
206
|
+
'json.save': '保存', 'json.saveAs': '另存为', 'json.editor.title': '编辑器',
|
|
207
|
+
'json.editor.description': '编辑源文本并应用操作', 'json.preview.title': '预览',
|
|
208
|
+
'json.preview.description': '带语法高亮的 JSON', 'json.form.verbatim': '原始',
|
|
209
|
+
'json.form.pretty': '美化', 'json.form.minified': '压缩', 'json.expandAll': '全部展开',
|
|
210
|
+
'json.collapseAll': '全部折叠', 'json.expandNode': '展开', 'json.collapseNode': '折叠',
|
|
211
|
+
'json.search': '搜索', 'json.matchInfo': '{current} / {total}', 'json.noMatches': '无匹配项',
|
|
212
|
+
'json.prevMatch': '上一个匹配项', 'json.nextMatch': '下一个匹配项',
|
|
213
|
+
'json.copyPath': '路径', 'json.copyValue': '值', 'json.stats': '{total} 个节点 · 深度 {depth}', 'json.empty': '空',
|
|
214
|
+
'json.tool.format': '格式化', 'json.tool.minify': '压缩', 'json.tool.sort': '键排序',
|
|
215
|
+
'json.tool.validate': '验证', 'json.tool.escape': '转义', 'json.tool.unescape': '取消转义',
|
|
216
|
+
'json.tool.base64Encode': 'Base64 →', 'json.tool.base64Decode': 'Base64 ←',
|
|
217
|
+
'json.tool.toCsv': '→ CSV', 'json.tool.toXml': '→ XML', 'json.tool.toYaml': '→ YAML',
|
|
218
|
+
'json.tool.formatted': '已格式化', 'json.tool.minified': '已压缩', 'json.tool.sorted': '键已排序',
|
|
219
|
+
'json.tool.escaped': '已转义', 'json.tool.unescaped': '已取消转义',
|
|
220
|
+
'json.tool.base64Encoded': '已进行 Base64 编码', 'json.tool.base64Decoded': '已进行 Base64 解码',
|
|
221
|
+
'json.tool.valid': '有效的 JSON:{summary}', 'json.tool.invalid': '无效的 JSON',
|
|
222
|
+
'json.tool.applied': '已应用到编辑器', 'json.tool.failed': '操作失败',
|
|
223
|
+
'json.tool.unescapeFailed': '无法取消文本转义', 'json.tool.base64Failed': '文本不是有效的 Base64',
|
|
224
|
+
'json.tool.csvRequiresObjects': 'CSV 需要对象或对象数组', 'json.tool.csvOutput': 'CSV 输出',
|
|
225
|
+
'json.tool.xmlOutput': 'XML 输出', 'json.tool.yamlOutput': 'YAML 输出',
|
|
226
|
+
'json.result.copy': '复制', 'json.result.replace': '替换编辑器内容', 'json.result.close': '关闭',
|
|
227
|
+
'pdf.title': 'PDF', 'pdf.loading': '正在加载 PDF…', 'pdf.loadFailed': '无法加载此 PDF。',
|
|
228
|
+
'pdf.pages': '页', 'pdf.goToPage': '转到页面', 'pdf.zoomIn': '放大', 'pdf.zoomOut': '缩小',
|
|
229
|
+
'pdf.viewOptions': '视图选项', 'pdf.fitWidth': '适合宽度', 'pdf.fitHeight': '适合高度',
|
|
230
|
+
'pdf.singlePage': '单页', 'pdf.twoPagesOdd': '双页(奇数页在左)', 'pdf.twoPagesEven': '双页(偶数页在左)',
|
|
231
|
+
'pdf.matchTheme': '匹配主题颜色', 'pdf.resetPages': '重置页面',
|
|
232
|
+
'pdf.annotationText': '添加文本', 'pdf.annotationSize': '文本大小', 'pdf.annotationColor': '文本颜色',
|
|
233
|
+
'pdf.save': '保存', 'pdf.saveAs': '另存为', 'pdf.merge': '合并 PDF', 'pdf.merging': '正在合并 PDF…',
|
|
234
|
+
'pdf.mergeComplete': 'PDF 已合并。请保存或另存为以保留更改。', 'pdf.mergeFailed': '无法合并此 PDF。',
|
|
235
|
+
'pdf.signature': '签名', 'pdf.signatureColor': '签名颜色', 'pdf.signatureClear': '清除',
|
|
236
|
+
'pdf.signatureUse': '使用签名', 'pdf.signaturePlace': '点击页面以放置签名。',
|
|
237
|
+
'pdf.highlight': '高亮', 'pdf.highlightColor': '高亮颜色', 'pdf.highlightSelect': '选择要高亮的文本。',
|
|
238
|
+
'pdf.markupOptions': '文本标记选项', 'pdf.underline': '下划线', 'pdf.underlineColor': '下划线颜色',
|
|
239
|
+
'pdf.strikeout': '删除线', 'pdf.strikeoutColor': '删除线颜色', 'pdf.cancel': '取消',
|
|
240
|
+
'pdf.textPlace': '点击页面以放置文本。', 'pdf.deletePage': '删除页面', 'pdf.deleteAnnotation': '删除批注',
|
|
241
|
+
'pdf.password': '密码', 'pdf.passwordRequired': '此 PDF 需要密码。',
|
|
242
|
+
'pdf.passwordIncorrect': '密码错误,请重试。', 'pdf.passwordSubmit': '打开',
|
|
243
|
+
'pdf.editingUnavailable': '此平台无法编辑 PDF', 'pdf.noFilePick': '此平台无法选择要合并的文件',
|
|
244
|
+
'image.title': '图像', 'image.loading': '正在加载图像…', 'image.loadFailed': '无法显示此图像。',
|
|
245
|
+
'image.status': '缩放 {zoom}%,旋转 {rotation} 度', 'image.zoomIn': '放大', 'image.zoomOut': '缩小',
|
|
246
|
+
'image.fit': '适应窗口', 'image.rotate': '旋转 90°', 'image.flipH': '水平翻转', 'image.flipV': '垂直翻转',
|
|
247
|
+
'image.grid': '网格', 'image.reset': '重置', 'image.filter': '滤镜',
|
|
248
|
+
'image.preset.original': '原始', 'image.preset.bright': '明亮', 'image.preset.dark': '暗色',
|
|
249
|
+
'image.preset.vintage': '复古', 'image.preset.bw': '黑白',
|
|
250
|
+
'diag.image.invalid-format': '无法将此文件解码为图像。',
|
|
251
|
+
'diag.image.corrupted': '图像数据已损坏,无法完整解码。',
|
|
252
|
+
'diag.image.animated-first-frame': '动画图像,仅显示第一帧。',
|
|
253
|
+
'diag.image.svg-external-blocked': '为确保安全,已阻止此 SVG 中的外部引用。',
|
|
254
|
+
'diag.image.limit-exceeded.pixels': '图像过大,正在显示缩小后的预览。',
|
|
255
|
+
'diag.image.limit-exceeded.canvas': '图像过大,无法在此平台编辑或导出。',
|
|
256
|
+
'image.tool.removeBackground': '移除背景', 'image.tool.removeBackground.unavailable': '此平台无法移除背景',
|
|
257
|
+
'image.bgRemoval.working': '正在移除背景…', 'image.bgRemoval.failed': '移除背景失败。',
|
|
258
|
+
'image.tool.detectSprites': '检测精灵图', 'image.tool.detectSprites.unavailable': '此平台无法检测精灵图',
|
|
259
|
+
'image.spriteDetection.working': '正在检测精灵图…', 'image.spriteDetection.failed': '精灵图检测失败。',
|
|
260
|
+
'image.saveAsPng': '另存为 PNG', 'image.saveAsPng.note': '编辑后的图像将保存为 PNG(.png)。',
|
|
261
|
+
'fallback.title.text': '文本预览', 'fallback.title.hex': '十六进制预览',
|
|
262
|
+
'fallback.truncated': '预览已截断,仅显示前 {bytes} 字节。',
|
|
263
|
+
'fallback.reason.missing-dependency': '此平台无法加载完整查看器。',
|
|
264
|
+
'fallback.reason.unsupported': '没有可用于此文件类型的查看器。'
|
|
265
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CATALOG_ZH_TW: Record<string, string>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { CATALOG_ZH_CN } from './catalog.zh-cn.js';
|
|
2
|
+
// Traditional Chinese uses the complete Simplified Chinese catalog as its
|
|
3
|
+
// semantic source, then applies Taiwan UI terminology before character
|
|
4
|
+
// conversion. This keeps both Chinese catalogs structurally synchronized.
|
|
5
|
+
const TAIWAN_TERMS = [
|
|
6
|
+
['软件包', '套件'], ['演示文稿', '簡報'], ['幻灯片', '投影片'],
|
|
7
|
+
['电子表格', '試算表'], ['工作簿', '活頁簿'], ['工作表', '工作表'],
|
|
8
|
+
['单元格', '儲存格'], ['源代码', '原始碼'], ['压缩包', '壓縮檔'],
|
|
9
|
+
['剪贴板', '剪貼簿'], ['文件大小', '檔案大小'], ['原文件', '原始檔案'],
|
|
10
|
+
['文件内容', '檔案內容'], ['选择文件', '選擇檔案'], ['保存文件', '儲存檔案'],
|
|
11
|
+
['文件类型', '檔案類型'], ['此文件', '此檔案'], ['新文件', '新檔案'],
|
|
12
|
+
['文件格式', '檔案格式'], ['完整文件', '完整檔案'], ['文件', '檔案'],
|
|
13
|
+
['保存', '儲存'], ['加载', '載入'], ['打印', '列印'], ['查看器', '檢視器'],
|
|
14
|
+
['视频', '影片'], ['图像', '影像'], ['源文本', '原始文字'], ['纯文本', '純文字'],
|
|
15
|
+
['十六进制', '十六進位'], ['字节', '位元組'], ['制表符', '定位字元'],
|
|
16
|
+
['另存为', '另存新檔'], ['设置', '設定'], ['链接', '連結'],
|
|
17
|
+
['条目', '項目'], ['高亮', '醒目提示'], ['下划线', '底線'],
|
|
18
|
+
['删除线', '刪除線'], ['批注', '註解'], ['精灵图', 'Sprite 圖'],
|
|
19
|
+
['自动检测', '自動偵測'], ['检测', '偵測'], ['重置', '重設'],
|
|
20
|
+
['适应窗口', '符合視窗'], ['匹配主题颜色', '符合主題色彩'],
|
|
21
|
+
['空闲', '閒置'], ['就绪', '就緒'], ['回退功能', '備援功能'],
|
|
22
|
+
['元数据', '中繼資料'], ['数据库', '資料庫'], ['数据', '資料'],
|
|
23
|
+
['文件夹', '資料夾'], ['目录', '目錄'], ['路径', '路徑'],
|
|
24
|
+
['屏幕', '螢幕'], ['网络', '網路'], ['信息', '資訊']
|
|
25
|
+
];
|
|
26
|
+
const TRADITIONAL_CHARS = {
|
|
27
|
+
'频': '頻', '暂': '暫', '选': '選', '区': '區', '缩': '縮', '视': '視', '图': '圖', '谱': '譜', '时': '時', '长': '長',
|
|
28
|
+
'声': '聲', '码': '碼', '浏': '瀏', '览': '覽', '无': '無', '编': '編', '请': '請', '转': '轉', '换': '換', '过': '過',
|
|
29
|
+
'数': '數', '库': '庫', '志': '誌', '获': '獲', '属': '屬', '击': '擊', '记': '記', '录': '錄', '读': '讀', '标': '標',
|
|
30
|
+
'压': '壓', '变': '變', '结': '結', '条': '條', '务': '務', '软': '軟', '举': '舉', '关': '關', '系': '係', '谁': '誰',
|
|
31
|
+
'导': '導', '贴': '貼', '检': '檢', '删': '刪', '号': '號', '类': '類', '载': '載', '复': '複', '单': '單', '测': '測',
|
|
32
|
+
'动': '動', '调': '調', '双': '雙', '显': '顯', '旧': '舊', '页': '頁', '续': '續', '汇': '彙', '绪': '緒', '毁': '毀',
|
|
33
|
+
'档': '檔', '筛': '篩', '径': '徑', '间': '間', '见': '見', '择': '擇', '纯': '純', '内': '內', '储': '儲', '键': '鍵',
|
|
34
|
+
'节': '節', '点': '點', '组': '組', '闭': '閉', '达': '達', '别': '別', '电': '電', '拟': '擬', '竖': '豎', '统': '統',
|
|
35
|
+
'计': '計', '称': '稱', '叠': '疊', '验': '驗', '输': '輸', '宽': '寬', '题': '題', '颜': '顏', '并': '併', '签': '簽',
|
|
36
|
+
'划': '劃', '线': '線', '释': '釋', '滤': '濾', '镜': '鏡', '画': '畫', '帧': '幀', '挡': '擋', '灵': '靈', '这': '這',
|
|
37
|
+
'为': '為', '仅': '僅', '与': '與', '将': '將', '从': '從', '个': '個', '开': '開', '该': '該', '当': '當', '对': '對',
|
|
38
|
+
'应': '應', '给': '給', '处': '處', '块': '塊', '层': '層', '亿': '億', '万': '萬', '还': '還', '进': '進', '终': '終',
|
|
39
|
+
'确': '確', '误': '誤', '损': '損', '坏': '壞', '异': '異', '余': '餘', '额': '額', '满': '滿', '边': '邊', '义': '義',
|
|
40
|
+
'实': '實', '现': '現', '术': '術', '问': '問', '启': '啟', '简': '簡', '体': '體', '冲': '衝', '构': '構', '备': '備',
|
|
41
|
+
'隐': '隱', '递': '遞', '归': '歸', '陈': '陳', '语': '語', '历': '歷', '权': '權', '级': '級', '网': '網', '报': '報',
|
|
42
|
+
'错': '錯', '锁': '鎖', '写': '寫', '临': '臨', '鉴': '鑑', '纸': '紙', '顺': '順', '布': '佈', '离': '離',
|
|
43
|
+
'发': '發', '华': '華', '广': '廣', '后': '後', '里': '裡', '种': '種', '项': '項', '传': '傳', '优': '優', '栏': '欄',
|
|
44
|
+
'设': '設', '试': '試', '词': '詞', '会': '會', '虑': '慮', '拥': '擁', '险': '險'
|
|
45
|
+
};
|
|
46
|
+
function toTraditionalTaiwan(value) {
|
|
47
|
+
let localized = value;
|
|
48
|
+
for (const [source, replacement] of TAIWAN_TERMS)
|
|
49
|
+
localized = localized.split(source).join(replacement);
|
|
50
|
+
return Array.from(localized, (char) => TRADITIONAL_CHARS[char] ?? char).join('');
|
|
51
|
+
}
|
|
52
|
+
export const CATALOG_ZH_TW = Object.fromEntries(Object.entries(CATALOG_ZH_CN).map(([key, value]) => [key, toTraditionalTaiwan(value)]));
|
package/dist/i18n/index.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { I18nService } from '../host/index.js';
|
|
2
2
|
export { CATALOG_EN } from './catalog.en.js';
|
|
3
|
+
export { CATALOG_JA } from './catalog.ja.js';
|
|
3
4
|
export { CATALOG_KO } from './catalog.ko.js';
|
|
5
|
+
export { CATALOG_FR } from './catalog.fr.js';
|
|
6
|
+
export { CATALOG_DE } from './catalog.de.js';
|
|
7
|
+
export { CATALOG_TH } from './catalog.th.js';
|
|
8
|
+
export { CATALOG_IT } from './catalog.it.js';
|
|
9
|
+
export { CATALOG_ZH_CN } from './catalog.zh-cn.js';
|
|
10
|
+
export { CATALOG_ZH_TW } from './catalog.zh-tw.js';
|
|
4
11
|
/**
|
|
5
12
|
* Render a catalog template with `{name}` substitutions. Unknown placeholders
|
|
6
13
|
* are left verbatim so missing args are visible instead of silently dropped.
|
package/dist/i18n/index.js
CHANGED
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
import { CATALOG_EN } from './catalog.en.js';
|
|
2
|
+
import { CATALOG_JA } from './catalog.ja.js';
|
|
2
3
|
import { CATALOG_KO } from './catalog.ko.js';
|
|
4
|
+
import { CATALOG_FR } from './catalog.fr.js';
|
|
5
|
+
import { CATALOG_DE } from './catalog.de.js';
|
|
6
|
+
import { CATALOG_TH } from './catalog.th.js';
|
|
7
|
+
import { CATALOG_IT } from './catalog.it.js';
|
|
8
|
+
import { CATALOG_ZH_CN } from './catalog.zh-cn.js';
|
|
9
|
+
import { CATALOG_ZH_TW } from './catalog.zh-tw.js';
|
|
3
10
|
export { CATALOG_EN } from './catalog.en.js';
|
|
11
|
+
export { CATALOG_JA } from './catalog.ja.js';
|
|
4
12
|
export { CATALOG_KO } from './catalog.ko.js';
|
|
13
|
+
export { CATALOG_FR } from './catalog.fr.js';
|
|
14
|
+
export { CATALOG_DE } from './catalog.de.js';
|
|
15
|
+
export { CATALOG_TH } from './catalog.th.js';
|
|
16
|
+
export { CATALOG_IT } from './catalog.it.js';
|
|
17
|
+
export { CATALOG_ZH_CN } from './catalog.zh-cn.js';
|
|
18
|
+
export { CATALOG_ZH_TW } from './catalog.zh-tw.js';
|
|
19
|
+
/**
|
|
20
|
+
* Language subtag → catalog for locales without region-specific variants.
|
|
21
|
+
* Chinese is resolved separately below because it needs script/region
|
|
22
|
+
* detection (Hant vs Hans). Each catalog falls back to CATALOG_EN per key.
|
|
23
|
+
*/
|
|
24
|
+
const SIMPLE_CATALOGS = {
|
|
25
|
+
ko: CATALOG_KO,
|
|
26
|
+
ja: CATALOG_JA,
|
|
27
|
+
fr: CATALOG_FR,
|
|
28
|
+
de: CATALOG_DE,
|
|
29
|
+
th: CATALOG_TH,
|
|
30
|
+
it: CATALOG_IT
|
|
31
|
+
};
|
|
5
32
|
/**
|
|
6
33
|
* Render a catalog template with `{name}` substitutions. Unknown placeholders
|
|
7
34
|
* are left verbatim so missing args are visible instead of silently dropped.
|
|
@@ -27,8 +54,14 @@ export function resolveCatalogMessage(key, args) {
|
|
|
27
54
|
return formatMessage(template, args);
|
|
28
55
|
}
|
|
29
56
|
export function resolveLocalizedCatalogMessage(locale, key, args) {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
57
|
+
const normalizedLocale = locale.trim().toLowerCase().replace(/_/g, '-');
|
|
58
|
+
const language = normalizedLocale.split('-', 1)[0] ?? '';
|
|
59
|
+
const catalog = language === 'zh'
|
|
60
|
+
? /(?:^|-)hant(?:-|$)/.test(normalizedLocale) || /^zh-(?:tw|hk|mo)(?:-|$)/.test(normalizedLocale)
|
|
61
|
+
? CATALOG_ZH_TW
|
|
62
|
+
: CATALOG_ZH_CN
|
|
63
|
+
: SIMPLE_CATALOGS[language] ?? CATALOG_EN;
|
|
64
|
+
const template = catalog[key] ?? CATALOG_EN[key];
|
|
32
65
|
if (template === undefined)
|
|
33
66
|
return key;
|
|
34
67
|
return formatMessage(template, args);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ParseOutcome, ParseOptions } from '../types.js';
|
|
2
|
-
import type { ArchiveDecoder, ArchiveDocument, OpenArchiveHandle } from './model.js';
|
|
1
|
+
import type { ParseOutcome, ParseOptions, ResourceLimits } from '../types.js';
|
|
2
|
+
import type { ArchiveDecoder, ArchiveDocument, ArchiveStreamDecoder, OpenArchiveHandle } from './model.js';
|
|
3
3
|
export * from './model.js';
|
|
4
4
|
export declare const ARCHIVE_DEFAULT_LIMITS: {
|
|
5
5
|
readonly maxInputBytes: number;
|
|
@@ -16,7 +16,21 @@ export declare class ArchiveError extends Error {
|
|
|
16
16
|
export interface ArchiveParseOptions extends ParseOptions {
|
|
17
17
|
fileName?: string;
|
|
18
18
|
}
|
|
19
|
+
export interface ArchiveStreamOptions {
|
|
20
|
+
fileName?: string;
|
|
21
|
+
totalSize?: number;
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
limits?: ResourceLimits;
|
|
24
|
+
}
|
|
19
25
|
export declare function parseArchive(data: Uint8Array, decoder: ArchiveDecoder, options?: ArchiveParseOptions): Promise<{
|
|
20
26
|
outcome: ParseOutcome<ArchiveDocument>;
|
|
21
27
|
handle?: OpenArchiveHandle;
|
|
22
28
|
}>;
|
|
29
|
+
/** Streaming counterpart of {@link parseArchive}: the adapter's path-based
|
|
30
|
+
* decoder opens the archive without core ever holding the full bytes. The
|
|
31
|
+
* returned handle is still wrapped with the decompressed-bytes budget, so the
|
|
32
|
+
* zip-bomb guard and per-entry limits stay in force on the streaming path. */
|
|
33
|
+
export declare function openArchiveStream(source: ArchiveStreamDecoder, options?: ArchiveStreamOptions): Promise<{
|
|
34
|
+
outcome: ParseOutcome<ArchiveDocument>;
|
|
35
|
+
handle?: OpenArchiveHandle;
|
|
36
|
+
}>;
|
|
@@ -13,25 +13,53 @@ export async function parseArchive(data, decoder, options = {}) {
|
|
|
13
13
|
const started = Date.now();
|
|
14
14
|
const max = options.limits?.maxInputBytes ?? ARCHIVE_DEFAULT_LIMITS.maxInputBytes;
|
|
15
15
|
if (data.byteLength > max)
|
|
16
|
-
return
|
|
16
|
+
return failed('limit-exceeded', 'diag.archive.limit-exceeded', started, false, true);
|
|
17
17
|
if (options.signal?.aborted)
|
|
18
|
-
return
|
|
18
|
+
return failed('aborted', 'diag.aborted', started, true, true);
|
|
19
19
|
if (options.fileName?.toLowerCase().endsWith('.dmg') && !hasDmgTrailer(data))
|
|
20
20
|
return failed('invalid-format', 'diag.archive.invalid-format', started);
|
|
21
21
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const entries = raw.entries.slice(0, options.limits?.maxEntries ?? ARCHIVE_DEFAULT_LIMITS.maxEntries);
|
|
25
|
-
const partial = entries.length !== raw.entries.length;
|
|
26
|
-
const handle = limitHandle(raw, options.limits?.maxDecompressedBytes ?? ARCHIVE_DEFAULT_LIMITS.maxDecompressedBytes);
|
|
27
|
-
return { handle, outcome: { result: partial ? { status: 'partial', document: { entries }, diagnostics: [{ severity: 'warning', code: 'archive.limit-exceeded', messageKey: 'diag.archive.limit-exceeded' }] } : { status: 'ok', document: { entries }, diagnostics: [] }, execution: { workerUsed: false, hardLimitEnforced: false, elapsedMillis: Date.now() - started } } };
|
|
22
|
+
const raw = await decoder.openArchive(data, openOptionsFrom(options.limits, options.signal));
|
|
23
|
+
return finalizeOpen(raw, options.limits, started);
|
|
28
24
|
}
|
|
29
25
|
catch (error) {
|
|
30
|
-
|
|
31
|
-
return { outcome: { result: { status: 'failed', failure: { code: known.code, retryable: known.code === 'aborted' || known.code === 'password-required', messageKey: known.messageKey }, diagnostics: [] }, execution: { workerUsed: false, hardLimitEnforced: false, elapsedMillis: Date.now() - started } } };
|
|
26
|
+
return openError(error, started);
|
|
32
27
|
}
|
|
33
28
|
}
|
|
34
|
-
|
|
29
|
+
/** Streaming counterpart of {@link parseArchive}: the adapter's path-based
|
|
30
|
+
* decoder opens the archive without core ever holding the full bytes. The
|
|
31
|
+
* returned handle is still wrapped with the decompressed-bytes budget, so the
|
|
32
|
+
* zip-bomb guard and per-entry limits stay in force on the streaming path. */
|
|
33
|
+
export async function openArchiveStream(source, options = {}) {
|
|
34
|
+
const started = Date.now();
|
|
35
|
+
// maxInputBytes is a *buffering* guard: the streaming path never holds the
|
|
36
|
+
// whole file, so it is only enforced when the caller sets it explicitly
|
|
37
|
+
// (no default). Otherwise a multi-GB archive — exactly what streaming is
|
|
38
|
+
// for — would be rejected. Per-entry memory stays bounded by limitHandle
|
|
39
|
+
// (maxDecompressedBytes) and the viewer's preview caps.
|
|
40
|
+
const declaredMax = options.limits?.maxInputBytes;
|
|
41
|
+
if (declaredMax !== undefined && options.totalSize !== undefined && options.totalSize > declaredMax)
|
|
42
|
+
return failed('limit-exceeded', 'diag.archive.limit-exceeded', started, false, true);
|
|
43
|
+
if (options.signal?.aborted)
|
|
44
|
+
return failed('aborted', 'diag.aborted', started, true, true);
|
|
45
|
+
try {
|
|
46
|
+
const raw = await source.openArchive(openOptionsFrom(options.limits, options.signal));
|
|
47
|
+
return finalizeOpen(raw, options.limits, started);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return openError(error, started);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function openOptionsFrom(limits, signal) { return { maxEntries: limits?.maxEntries ?? ARCHIVE_DEFAULT_LIMITS.maxEntries, maxDecompressedBytes: limits?.maxDecompressedBytes ?? ARCHIVE_DEFAULT_LIMITS.maxDecompressedBytes, ...(signal ? { signal } : {}) }; }
|
|
54
|
+
function finalizeOpen(raw, limits, started) {
|
|
55
|
+
const maxEntries = limits?.maxEntries ?? ARCHIVE_DEFAULT_LIMITS.maxEntries;
|
|
56
|
+
const entries = raw.entries.slice(0, maxEntries);
|
|
57
|
+
const partial = entries.length !== raw.entries.length;
|
|
58
|
+
const handle = limitHandle(raw, limits?.maxDecompressedBytes ?? ARCHIVE_DEFAULT_LIMITS.maxDecompressedBytes);
|
|
59
|
+
return { handle, outcome: { result: partial ? { status: 'partial', document: { entries }, diagnostics: [{ severity: 'warning', code: 'archive.limit-exceeded', messageKey: 'diag.archive.limit-exceeded' }] } : { status: 'ok', document: { entries }, diagnostics: [] }, execution: { workerUsed: false, hardLimitEnforced: false, elapsedMillis: Date.now() - started } } };
|
|
60
|
+
}
|
|
61
|
+
function openError(error, started) { const known = error instanceof ArchiveError ? error : new ArchiveError('invalid-format', 'diag.archive.invalid-format'); return { outcome: { result: { status: 'failed', failure: { code: known.code, retryable: known.code === 'aborted' || known.code === 'password-required', messageKey: known.messageKey }, diagnostics: [] }, execution: { workerUsed: false, hardLimitEnforced: false, elapsedMillis: Date.now() - started } } }; }
|
|
62
|
+
function failed(code, messageKey, started, retryable = false, hardLimitEnforced = true) { return { outcome: { result: { status: 'failed', failure: { code, retryable, messageKey }, diagnostics: [] }, execution: { workerUsed: false, hardLimitEnforced, elapsedMillis: Date.now() - started } } }; }
|
|
35
63
|
function hasDmgTrailer(data) { return data.byteLength >= 512 && [0x6b, 0x6f, 0x6c, 0x79].every((x, i) => data[data.byteLength - 512 + i] === x); }
|
|
36
64
|
function limitHandle(raw, maxTotal) { let used = 0; let reserved = 0; return { entries: raw.entries, async extract(id, opts) { const allowance = Math.min(opts.maxBytes, maxTotal - used - reserved); if (allowance <= 0)
|
|
37
65
|
throw new ArchiveError('limit-exceeded', 'diag.archive.limit-exceeded'); reserved += allowance; try {
|
|
@@ -27,3 +27,23 @@ export interface ArchiveDecoder {
|
|
|
27
27
|
maxDecompressedBytes?: number;
|
|
28
28
|
}): Promise<OpenArchiveHandle>;
|
|
29
29
|
}
|
|
30
|
+
/** Path/handle-based decoder that opens an archive without materializing the
|
|
31
|
+
* whole file in memory (e.g. an adapter wrapping the system `7z`/`tar`). Core
|
|
32
|
+
* drives listing and single-entry extraction lazily through the returned
|
|
33
|
+
* handle; it never receives the archive bytes. */
|
|
34
|
+
export interface ArchiveStreamDecoder {
|
|
35
|
+
openArchive(options?: {
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
maxEntries?: number;
|
|
38
|
+
maxDecompressedBytes?: number;
|
|
39
|
+
}): Promise<OpenArchiveHandle>;
|
|
40
|
+
}
|
|
41
|
+
/** Streams one archive entry to an adapter-chosen destination without loading
|
|
42
|
+
* the entry into memory. Core never sees the bytes — the adapter owns the
|
|
43
|
+
* destination picker and the pipe (e.g. `7z e -so entry | createWriteStream`).
|
|
44
|
+
* Resolves to the saved file name, or null when the user cancels. */
|
|
45
|
+
export interface ArchiveEntrySaver {
|
|
46
|
+
saveEntry(entry: ArchiveEntry, options: {
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
}): Promise<string | null>;
|
|
49
|
+
}
|
|
@@ -6,4 +6,7 @@ export interface AudioInfo {
|
|
|
6
6
|
bitsPerSample?: number;
|
|
7
7
|
warnings: string[];
|
|
8
8
|
}
|
|
9
|
+
/** Returns the audio MIME type for a file name's extension, or undefined when
|
|
10
|
+
* the extension is not a recognized audio format. */
|
|
11
|
+
export declare function audioMimeType(fileName: string): string | undefined;
|
|
9
12
|
export declare function parseAudioInfo(fileName: string, data: Uint8Array): AudioInfo;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
const MIME = { mp3: 'audio/mpeg', wav: 'audio/wav', pcm: 'audio/L16', aiff: 'audio/aiff', aif: 'audio/aiff', aifc: 'audio/aiff', amr: 'audio/amr', awb: 'audio/amr-wb', ogg: 'audio/ogg', flac: 'audio/flac', ac3: 'audio/ac3', aac: 'audio/aac', m4a: 'audio/mp4' };
|
|
2
|
+
/** Returns the audio MIME type for a file name's extension, or undefined when
|
|
3
|
+
* the extension is not a recognized audio format. */
|
|
4
|
+
export function audioMimeType(fileName) {
|
|
5
|
+
return MIME[fileName.toLowerCase().split('.').pop() ?? ''];
|
|
6
|
+
}
|
|
2
7
|
export function parseAudioInfo(fileName, data) {
|
|
3
8
|
const ext = fileName.toLowerCase().split('.').pop() ?? '';
|
|
4
9
|
const info = { format: ext.toUpperCase() || 'AUDIO', mimeType: MIME[ext] ?? 'application/octet-stream', warnings: [] };
|
|
@@ -4,3 +4,6 @@ export interface VideoInfo {
|
|
|
4
4
|
warnings: string[];
|
|
5
5
|
}
|
|
6
6
|
export declare function parseVideoInfo(fileName: string, data: Uint8Array): VideoInfo;
|
|
7
|
+
/** Returns the video MIME type for a file name's extension, or undefined when
|
|
8
|
+
* the extension is not a recognized video format. */
|
|
9
|
+
export declare function videoMimeType(fileName: string): string | undefined;
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
const MIME = { mp4: 'video/mp4', mts: 'video/mp2t', m2ts: 'video/mp2t', avi: 'video/x-msvideo', mov: 'video/quicktime', wmv: 'video/x-ms-wmv', flv: 'video/x-flv', webm: 'video/webm', mkv: 'video/x-matroska' };
|
|
2
2
|
export function parseVideoInfo(fileName, data) { const ext = fileName.toLowerCase().split('.').pop() ?? ''; return { format: ext.toUpperCase() || 'VIDEO', mimeType: MIME[ext] ?? 'application/octet-stream', warnings: data.byteLength ? [] : ['The video file is empty.'] }; }
|
|
3
|
+
/** Returns the video MIME type for a file name's extension, or undefined when
|
|
4
|
+
* the extension is not a recognized video format. */
|
|
5
|
+
export function videoMimeType(fileName) { return MIME[fileName.toLowerCase().split('.').pop() ?? '']; }
|
package/dist/styles/archive.css
CHANGED
|
@@ -7,4 +7,5 @@
|
|
|
7
7
|
.omni-archive__workspace{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(320px,.95fr);gap:16px;align-items:start}.omni-archive__table-wrap{overflow:auto;min-width:0;max-height:70vh}.omni-archive__table{width:100%;table-layout:fixed;border-collapse:collapse}.omni-archive__table th,.omni-archive__table td{padding:12px 14px;border-bottom:1px solid var(--omni-border,#3c3c3c);text-align:left;vertical-align:top;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.omni-archive__table th:first-child{width:42%}.omni-archive__table th{position:sticky;top:0;font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--omni-muted-fg,#9d9d9d);background:var(--omni-bg-secondary,#252526);z-index:1}.omni-archive__path{font-family:var(--omni-mono-font,"SFMono-Regular",Consolas,monospace)}.omni-archive__entry{height:43px;cursor:pointer;transition:background-color 120ms ease}.omni-archive__entry:hover{background:color-mix(in srgb,var(--omni-accent,#d97706) 8%,transparent)}.omni-archive__entry:focus-visible{outline:2px solid var(--omni-accent,#d97706);outline-offset:-2px}.omni-archive__entry.is-selected{background:color-mix(in srgb,var(--omni-accent,#d97706) 16%,transparent)}.omni-archive__empty{padding:26px 18px;text-align:center;color:var(--omni-muted-fg,#9d9d9d)}
|
|
8
8
|
.omni-archive__preview-panel{padding:18px;min-height:420px;display:flex;flex-direction:column;gap:14px}.omni-archive__preview-header{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.omni-archive__preview-kicker{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--omni-muted-fg,#9d9d9d);margin-bottom:6px}.omni-archive__preview-header h2{margin:0;font-size:18px;line-height:1.35;word-break:break-word}.omni-archive__preview-badge{border-radius:999px;padding:8px 12px;font-size:12px;font-weight:700;background:var(--omni-badge-bg,#3a3d41);white-space:nowrap}.omni-archive__preview-meta{margin:0}.omni-archive__preview{margin:0;flex:1;overflow:auto;white-space:pre-wrap;word-break:break-word;border-radius:14px;border:1px solid var(--omni-border,#3c3c3c);background:var(--omni-code-bg,#181818);padding:14px;color:var(--omni-fg,#d4d4d4);font:12px/1.55 var(--omni-mono-font,"SFMono-Regular",Consolas,monospace)}
|
|
9
9
|
.omni-archive__save{align-self:flex-start;padding:8px 12px;border:1px solid var(--omni-border,#3c3c3c);border-radius:6px;background:var(--omni-button-bg,#3a3d41);color:var(--omni-fg,#d4d4d4);font:inherit;cursor:pointer}.omni-archive__save:hover:not(:disabled){background:var(--omni-button-hover-bg,#45494e)}.omni-archive__save:disabled{opacity:.55;cursor:not-allowed}.omni-archive__spacer td{padding:0!important;border:0!important}
|
|
10
|
+
.omni-archive__preview-media{flex:1;display:flex;align-items:center;justify-content:center;border-radius:14px;border:1px solid var(--omni-border,#3c3c3c);background:var(--omni-code-bg,#181818);padding:14px;overflow:auto}.omni-archive__audio{width:100%}.omni-archive__video{max-width:100%;max-height:100%}.omni-archive__image{max-width:100%;max-height:100%;object-fit:contain}
|
|
10
11
|
@media(max-width:720px){.omni-viewer--archive{padding:16px 10px 24px}.omni-archive__hero{flex-direction:column}.omni-archive__pills{justify-content:flex-start}.omni-archive__table th:nth-child(n+3),.omni-archive__table td:nth-child(n+3){display:none}.omni-archive__workspace{grid-template-columns:1fr}.omni-archive__preview-panel{min-height:280px}}
|
package/dist/styles/audio.css
CHANGED
|
@@ -55,6 +55,14 @@
|
|
|
55
55
|
.omni-audio__spectrogram { min-height: 0; }
|
|
56
56
|
.omni-audio__spectrogram--active { min-height: 200px; }
|
|
57
57
|
|
|
58
|
+
/* Keep the WaveSurfer playhead (cursor + played-region overlay) pinned to the
|
|
59
|
+
waveform strip. Some wavesurfer spectrogram builds render the spectrogram
|
|
60
|
+
inside the main wrapper instead of our dedicated container; without this the
|
|
61
|
+
full-height playhead sweeps down across the spectrogram ("progress bar"
|
|
62
|
+
running along the bottom) rather than staying with the waveform at the top. */
|
|
63
|
+
.omni-audio__waveform ::part(cursor),
|
|
64
|
+
.omni-audio__waveform ::part(progress) { height: 128px !important; }
|
|
65
|
+
|
|
58
66
|
.omni-audio__status { color: var(--omni-muted, #9d9d9d); font-size: 12px; min-height: 16px; }
|
|
59
67
|
|
|
60
68
|
.omni-audio__loading { display: flex; justify-content: center; align-items: center; min-height: 120px; color: var(--omni-muted, #9d9d9d); }
|