diff --git a/CHANGELOG.md b/CHANGELOG.md index 321add5..ded94f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 更新日志 +## [3.0.1] - 2026-09-29 + +### 修复 + +- 补充简体中文语言声明,使 Edge 商店正确识别扩展语言并提供中文介绍页。 +- 打包流程纳入语言文件,检查缺失的语言消息引用。 + ## [3.0.0] - 2026-09-29 ### 新增 @@ -29,3 +36,4 @@ 更早版本尚未整理为独立发行记录,历史改动见 [Git 提交记录](https://github.com/aredddd/GlassNote/commits/main/)。 [3.0.0]: https://github.com/aredddd/GlassNote/releases/tag/v3.0.0 +[3.0.1]: https://github.com/aredddd/GlassNote/releases/tag/v3.0.1 diff --git a/PRIVACY.md b/PRIVACY.md index e1b17b8..545561a 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,6 +1,6 @@ # GlassNote 隐私说明 -更新日期:2026 年 9 月 29 日。适用于 GlassNote 3.0.0。 +更新日期:2026 年 9 月 29 日。适用于 GlassNote 3.0.1。 GlassNote 是网页标注与笔记扩展。笔记保存在你使用的浏览器中;扩展没有账号系统、开发者服务器、广告或使用分析服务,不向开发者或第三方上传网页内容和笔记。 diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json new file mode 100644 index 0000000..22557dd --- /dev/null +++ b/_locales/zh_CN/messages.json @@ -0,0 +1,8 @@ +{ + "extensionName": { + "message": "GlassNote" + }, + "extensionDescription": { + "message": "轻盈的网页高亮与笔记工具。自动找回原文位置,本地保存,随时整理和导出。" + } +} diff --git a/docs/EDGE_STORE.md b/docs/EDGE_STORE.md index acbeede..8c3defa 100644 --- a/docs/EDGE_STORE.md +++ b/docs/EDGE_STORE.md @@ -1,6 +1,6 @@ # Edge 商店发布材料 -对应版本:3.0.0。此文件是提交材料,不代表已经提交审核或已经上架。 +对应版本:3.0.1。此文件是提交材料,不代表已经提交审核或已经上架。 ## 商店字段 @@ -57,7 +57,7 @@ GlassNote 用于在网页中标注文字、记录笔记,并在以后打开同 ## 资源规格与官方入口 -安装包位于 dist/GlassNote-v3.0.0.zip;商店图标可用 assets/icon-128.png。截图使用扩展实际运行界面,尺寸为 1280 × 800,不放入用户的私人笔记或第三方宣传素材。 +安装包位于 dist/GlassNote-v3.0.1.zip;商店图标可用 assets/icon-128.png。截图使用扩展实际运行界面,尺寸为 1280 × 800,不放入用户的私人笔记或第三方宣传素材。 - [Edge 开发者注册](https://learn.microsoft.com/en-us/microsoft-edge/extensions/publish/create-dev-account) - [Edge 扩展发布说明](https://learn.microsoft.com/en-us/microsoft-edge/extensions/publish/publish-extension) diff --git a/manifest.json b/manifest.json index 07e3b7a..57233fe 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,10 @@ { "manifest_version": 3, - "name": "GlassNote", - "version": "3.0.0", + "name": "__MSG_extensionName__", + "version": "3.0.1", + "default_locale": "zh_CN", "minimum_chrome_version": "105", - "description": "轻盈的网页高亮与笔记工具。自动找回原文位置,本地保存,随时整理和导出。", + "description": "__MSG_extensionDescription__", "permissions": ["activeTab", "storage", "scripting"], "background": { "service_worker": "src/background/background.js" }, "options_page": "src/library/library.html", diff --git a/package-lock.json b/package-lock.json index a1dc9f6..3f493a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "glassnote", - "version": "3.0.0", + "version": "3.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "glassnote", - "version": "3.0.0", + "version": "3.0.1", "devDependencies": { "playwright": "1.63.0", "prettier": "3.9.9" diff --git a/package.json b/package.json index 683bd73..dcf10fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "glassnote", - "version": "3.0.0", + "version": "3.0.1", "private": true, "description": "轻盈、可靠的本地网页笔记工具", "scripts": { @@ -8,8 +8,8 @@ "test:e2e": "node --test tests/extension.e2e.cjs", "test:live": "node tests/live-site.cjs", "check": "node scripts/check.cjs", - "format": "prettier --write src styles scripts tests .github docs manifest.json package.json package-lock.json \"*.md\"", - "format:check": "prettier --check src styles scripts tests .github docs manifest.json package.json package-lock.json \"*.md\"", + "format": "prettier --write src styles _locales scripts tests .github docs manifest.json package.json package-lock.json \"*.md\"", + "format:check": "prettier --check src styles _locales scripts tests .github docs manifest.json package.json package-lock.json \"*.md\"", "package": "node scripts/package.cjs", "package:verify": "node scripts/verify-package.cjs" }, diff --git a/scripts/check.cjs b/scripts/check.cjs index 1174739..7cd8ada 100644 --- a/scripts/check.cjs +++ b/scripts/check.cjs @@ -12,6 +12,30 @@ function walk(directory) { } walk(path.join(root, 'src')); const manifest = JSON.parse(fs.readFileSync(path.join(root, 'manifest.json'), 'utf8')); +const localeReferences = [...JSON.stringify(manifest).matchAll(/__MSG_([A-Za-z0-9_]+)__/g)].map( + (match) => match[1], +); +const localeRoot = path.join(root, '_locales'); +if ((localeReferences.length || fs.existsSync(localeRoot)) && !manifest.default_locale) + throw new Error('使用语言包时必须声明 default_locale'); +if (manifest.default_locale) { + if (!/^[A-Za-z0-9_]+$/.test(manifest.default_locale)) + throw new Error('default_locale 必须是合法的语言目录名'); + const messagesPath = path.join(localeRoot, manifest.default_locale, 'messages.json'); + if (!fs.existsSync(messagesPath)) throw new Error('缺少默认语言包:' + messagesPath); + const messages = JSON.parse(fs.readFileSync(messagesPath, 'utf8')); + if (!messages || typeof messages !== 'object' || Array.isArray(messages)) + throw new Error('语言包必须是消息名称到消息内容的对象'); + const normalizedMessages = new Map(); + for (const [key, value] of Object.entries(messages)) { + if (!value || typeof value.message !== 'string' || !value.message.trim()) + throw new Error('语言包消息内容不能为空:' + key); + normalizedMessages.set(key.toLowerCase(), value.message); + } + for (const key of localeReferences) + if (!normalizedMessages.has(key.toLowerCase())) + throw new Error('默认语言包缺少 manifest 引用的消息:' + key); +} const files = [ manifest.background.service_worker, manifest.action.default_popup, @@ -31,4 +55,4 @@ for (const [size, file] of Object.entries(manifest.icons)) { ) throw new Error('扩展图标必须是正确尺寸的 PNG:' + file); } -console.log('JavaScript 语法和扩展入口检查通过。'); +console.log('JavaScript 语法、扩展入口和语言包检查通过。'); diff --git a/scripts/package-lib.cjs b/scripts/package-lib.cjs index 6b9bbf7..dc8958b 100644 --- a/scripts/package-lib.cjs +++ b/scripts/package-lib.cjs @@ -13,6 +13,7 @@ const REQUIRED_FILES = [ 'SECURITY.md', ]; const TREES = { + _locales: /\.json$/, src: /\.(js|html|css)$/, styles: /\.css$/, assets: /\.(png|svg|ico|webp)$/, diff --git a/tests/extension.e2e.cjs b/tests/extension.e2e.cjs index b3d5410..55434c6 100644 --- a/tests/extension.e2e.cjs +++ b/tests/extension.e2e.cjs @@ -98,6 +98,20 @@ after(async () => { fs.rmSync(profile, { recursive: true, force: true }); }); +test('真实扩展:默认简体中文语言资源可解析', async () => { + const localized = await worker.evaluate(() => ({ + name: chrome.runtime.getManifest().name, + description: chrome.runtime.getManifest().description, + message: chrome.i18n.getMessage('extensionDescription'), + })); + assert.equal(localized.name, 'GlassNote'); + assert.equal( + localized.description, + '轻盈的网页高亮与笔记工具。自动找回原文位置,本地保存,随时整理和导出。', + ); + assert.equal(localized.message, localized.description); +}); + test('真实扩展:跨节点标注不破坏链接,隐藏保留原文,刷新和关页重开恢复', async () => { await openArticle(); const beforeHTML = await page.locator('#linked').innerHTML(); diff --git a/tests/package.test.cjs b/tests/package.test.cjs index 921859a..9f3e0e0 100644 --- a/tests/package.test.cjs +++ b/tests/package.test.cjs @@ -4,6 +4,7 @@ const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const { createHash } = require('node:crypto'); +const { spawnSync } = require('node:child_process'); const { packageExtension, releaseVersion, @@ -18,7 +19,23 @@ function fixture(t) { fs.mkdirSync(path.dirname(path.join(root, name)), { recursive: true }); fs.writeFileSync(path.join(root, name), value); }; - write('manifest.json', JSON.stringify({ manifest_version: 3, version: '3.2.1' })); + write( + 'manifest.json', + JSON.stringify({ + manifest_version: 3, + version: '3.2.1', + name: '__MSG_extensionName__', + description: '__MSG_extensionDescription__', + default_locale: 'zh_CN', + }), + ); + write( + '_locales/zh_CN/messages.json', + JSON.stringify({ + extensionName: { message: 'GlassNote' }, + extensionDescription: { message: '轻盈的网页高亮与笔记工具。' }, + }), + ); write('package.json', JSON.stringify({ version: '3.2.1' })); write( 'package-lock.json', @@ -56,17 +73,23 @@ test('发布包按版本命名,附许可证和隐私声明,排除依赖、 assert.equal(path.basename(result.archive), 'GlassNote-v3.2.1.zip'); const archive = fs.readFileSync(result.archive); const entries = readArchive(archive); - assert.equal(entries.size, 11); + assert.equal(entries.size, 12); for (const name of [ 'LICENSE', 'PRIVACY.md', 'THIRD_PARTY_NOTICES.md', 'CONTRIBUTING.md', 'SECURITY.md', + '_locales/zh_CN/messages.json', 'docs/安装说明.md', ]) assert.ok(entries.has(name), name); assert.equal(entries.get('docs/安装说明.md').toString(), '中文文档应当正确保存在 ZIP 中。\n'); + const manifest = JSON.parse(entries.get('manifest.json').toString()); + const messages = JSON.parse(entries.get('_locales/zh_CN/messages.json').toString()); + assert.equal(manifest.default_locale, 'zh_CN'); + assert.equal(messages.extensionName.message, 'GlassNote'); + assert.equal(messages.extensionDescription.message, '轻盈的网页高亮与笔记工具。'); assert.ok( [...entries.keys()].every((name) => !/node_modules|work\/|tests\/|\.env|debug/.test(name)), ); @@ -125,3 +148,32 @@ test('发布白名单内的符号链接不能将外部文件带入安装包', (t fs.symlinkSync(path.join(root, 'LICENSE'), path.join(root, 'src/linked.js')); assert.throws(() => packageExtension(root, output), /符号链接/); }); + +test('扩展检查拒绝缺失默认语言声明、语言包或 manifest 引用的消息', (t) => { + const { root, write } = fixture(t); + write('scripts/check.cjs', fs.readFileSync(path.join(__dirname, '../scripts/check.cjs'))); + const manifest = JSON.parse(fs.readFileSync(path.join(root, 'manifest.json'), 'utf8')); + Object.assign(manifest, { + background: { service_worker: 'src/background/background.js' }, + action: { default_popup: 'src/background/background.js' }, + options_page: 'src/background/background.js', + icons: {}, + content_scripts: [], + }); + const saveManifest = () => write('manifest.json', JSON.stringify(manifest)); + const check = () => + spawnSync(process.execPath, [path.join(root, 'scripts/check.cjs')], { encoding: 'utf8' }); + saveManifest(); + assert.equal(check().status, 0); + delete manifest.default_locale; + saveManifest(); + assert.match(check().stderr, /必须声明 default_locale/); + manifest.default_locale = 'zh_CN'; + manifest.name = '__MSG_missingName__'; + saveManifest(); + assert.match(check().stderr, /缺少 manifest 引用的消息:missingName/); + write('_locales/zh_CN/messages.json', JSON.stringify({ extensionName: { message: '' } })); + assert.match(check().stderr, /消息内容不能为空/); + fs.rmSync(path.join(root, '_locales/zh_CN/messages.json')); + assert.match(check().stderr, /缺少默认语言包/); +});