From 6c23732883f955cf74e3f846ebcb4560a0b607b4 Mon Sep 17 00:00:00 2001 From: piratf Date: Thu, 29 Jan 2026 22:40:43 +0800 Subject: [PATCH 01/27] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=A4=9A?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E6=94=AF=E6=8C=81=20(i18n)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 gettext + babel 实现国际化 - 支持英文和中文 - 自动检测系统语言 - 支持命令行参数 --lang 手动指定语言 - 添加 README.md (英文) 和 README.zh-CN.md (中文) - 配置 polint 检查翻译文件质量 - 添加 pre-commit hooks 自动提取和检查翻译 - 支持通过 python -m remark.cli 运行模块 --- .gitignore | 7 + .pre-commit-config.yaml | 17 + README.zh-CN.md | 142 ++++++++ babel.cfg | 11 + locale/zh/LC_MESSAGES/messages.po | 520 ++++++++++++++++++++++++++++++ pyproject.toml | 14 + readme.md | 123 +++---- remark/cli/__main__.py | 8 + remark/cli/commands.py | 184 ++++++----- remark/core/folder_handler.py | 47 +-- remark/i18n.py | 154 +++++++++ remark/storage/desktop_ini.py | 26 +- remark/utils/platform.py | 8 +- 13 files changed, 1083 insertions(+), 178 deletions(-) create mode 100644 README.zh-CN.md create mode 100644 babel.cfg create mode 100644 locale/zh/LC_MESSAGES/messages.po create mode 100644 remark/cli/__main__.py create mode 100644 remark/i18n.py diff --git a/.gitignore b/.gitignore index 813cfc4..aa2188c 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,10 @@ dmypy.json *.whl *.tar.gz /tools/upx/ + +# i18n - 编译后的翻译文件(只追踪源 .po 文件) +locale/*/LC_MESSAGES/*.mo +messages.pot + +# bv (beads viewer) local config and caches +.bv/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a5d922..9f90dd6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -65,6 +65,23 @@ repos: stages: [pre-push] pass_filenames: false + # i18n 相关检查 + - id: extract-translations + name: Extract i18n messages + entry: bash -c 'pybabel extract -F babel.cfg -o messages.pot remark/ && echo "Translations extracted successfully"' + language: system + types: [python] + stages: [pre-commit] + pass_filenames: false + require_serial: true + + - id: check-po-files + name: Check PO files with polint + entry: bash -c 'if compgen -G "locale/*/LC_MESSAGES/*.po" > /dev/null; then polint locale/*/LC_MESSAGES/*.po || true; fi' + language: system + types: [po] + stages: [pre-commit] + # 全局排除 exclude: | ^(?: diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..1b6bd95 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,142 @@ +# Windows 文件夹备注工具 + +一个通过修改 `Desktop.ini` 文件为 Windows 文件夹添加备注/注释的命令行工具。 + +[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) +[![zh-cn](https://img.shields.io/badge/lang-zh--cn-blue.svg)](README.zh-CN.md) + +## 特性 + +- 支持中文等多语言字符(UTF-16 编码) +- 命令行模式和交互模式 +- 自动编码检测和修复 +- 自动更新检查,保持最新版本 +- 右键菜单集成,快速访问 +- 单文件 exe 打包,无需 Python 环境 + +## 安装 + +### 方式一:使用 exe 文件(推荐) + +下载 [releases](https://github.com/piratf/windows-folder-remark/releases) 中的 `windows-folder-remark.exe`,直接使用。 + +### 方式二:从源码安装 + +```bash +# 克隆仓库 +git clone https://github.com/piratf/windows-folder-remark.git +cd windows-folder-remark + +# 安装依赖(无外部依赖) +pip install -e . + +# 运行 +python -m remark.cli --help +``` + +## 使用方法 + +### 命令行模式 + +```bash +# 添加备注 +windows-folder-remark.exe "C:\MyFolder" "这是我的文件夹" + +# 查看备注 +windows-folder-remark.exe --view "C:\MyFolder" + +# 删除备注 +windows-folder-remark.exe --delete "C:\MyFolder" + +# 检查更新 +windows-folder-remark.exe --update + +# 安装右键菜单 +windows-folder-remark.exe --install + +# 卸载右键菜单 +windows-folder-remark.exe --uninstall +``` + +### 交互模式 + +```bash +# 运行后根据提示操作 +windows-folder-remark.exe +``` + +### 右键菜单(推荐) + +安装右键菜单后,可以直接在文件资源管理器中右键文件夹添加备注: + +```bash +# 安装右键菜单 +windows-folder-remark.exe --install +``` + +- **Windows 10**: 右键文件夹可直接看到「添加文件夹备注」 +- **Windows 11**: 右键文件夹 → 点击「显示更多选项」→ 添加文件夹备注 + +### 自动更新 + +程序会在退出时自动检查更新(每 24 小时一次),如有新版本会提示是否立即更新。 + +也可以手动检查更新: + +```bash +windows-folder-remark.exe --update +``` + +## 编码检测 + +当使用 `--view` 查看备注时,如果检测到 `desktop.ini` 文件不是标准的 UTF-16 编码,工具会提醒你: + +``` +警告: desktop.ini 文件编码为 utf-8,不是标准的 UTF-16。 +这可能导致中文等特殊字符显示异常。 +是否修复编码为 UTF-16?[Y/n]: +``` + +选择 `Y` 可自动修复编码。 + +## 开发 + +```bash +# 安装开发依赖 +pip install -e ".[dev]" + +# 运行测试 +pytest + +# 代码检查 +ruff check . +ruff format . + +# 类型检查 +mypy remark/ + +# 本地打包 exe +python -m scripts.build +``` + +## 原理说明 + +该工具通过以下步骤实现文件夹备注: + +1. 在文件夹中创建/修改 `Desktop.ini` 文件 +2. 写入 `[.ShellClassInfo]` 段落和 `InfoTip` 属性 +3. 使用 UTF-16 编码保存文件 +4. 将 `Desktop.ini` 设置为隐藏和系统属性 +5. 将文件夹设置为只读属性(使 Windows 读取 `Desktop.ini`) + +参考:[Microsoft 官方文档](https://learn.microsoft.com/en-us/windows/win32/shell/how-to-customize-folders-with-desktop-ini) + +## 注意事项 + +- 修改后可能需要几分钟才能在资源管理器中显示 +- 某些文件管理器可能不支持显示文件夹备注 +- 工具会修改文件夹的系统属性 + +## 许可证 + +MIT License diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..db84d47 --- /dev/null +++ b/babel.cfg @@ -0,0 +1,11 @@ +# Babel extraction configuration +# https://babel.pocoo.org/en/latest/messages.html + +# Extract translatable strings from Python files +[python: remark/**/*.py] + +# Keyword list - recognize strings in these function calls as translatable +keywords = _, gettext, ngettext + +# Encoding +encoding = utf-8 diff --git a/locale/zh/LC_MESSAGES/messages.po b/locale/zh/LC_MESSAGES/messages.po new file mode 100644 index 0000000..b67eeb1 --- /dev/null +++ b/locale/zh/LC_MESSAGES/messages.po @@ -0,0 +1,520 @@ +# Chinese translations for PROJECT. +# Copyright (C) 2026 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2026-01-29 22:34+0800\n" +"PO-Revision-Date: 2026-01-29 22:35+0800\n" +"Last-Translator: FULL NAME \n" +"Language: zh\n" +"Language-Team: zh \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.17.0\n" + +#: remark/cli/commands.py:55 +#, python-brace-format +msgid "Path does not exist: {path}" +msgstr "路径不存在: {path}" + +#: remark/cli/commands.py:58 +#, python-brace-format +msgid "Path is not a folder: {path}" +msgstr "路径不是文件夹: {path}" + +#: remark/cli/commands.py:80 +#, python-brace-format +msgid "Current version: {version}" +msgstr "当前版本: {version}" + +#: remark/cli/commands.py:81 +msgid "Checking for updates..." +msgstr "正在检查更新..." + +#: remark/cli/commands.py:86 +#, python-brace-format +msgid "" +"\n" +"New version found: {tag_name}" +msgstr "" +"\n" +"发现新版本: {tag_name}" + +#: remark/cli/commands.py:87 remark/cli/commands.py:115 +#, python-brace-format +msgid "Update notes: {notes}" +msgstr "更新说明: {notes}" + +#: remark/cli/commands.py:88 remark/cli/commands.py:116 +#, python-brace-format +msgid "Full changelog: {url}" +msgstr "完整更新日志: {url}" + +#: remark/cli/commands.py:89 +msgid "" +"\n" +"Update now? [Y/n]: " +msgstr "" +"\n" +"是否立即更新? [Y/n]: " + +#: remark/cli/commands.py:94 +msgid "Already at the latest version" +msgstr "已是最新版本" + +#: remark/cli/commands.py:111 +#, python-brace-format +msgid "" +"\n" +"New version available: {tag_name} (Current version: {version})" +msgstr "" +"\n" +"发现新版本: {tag_name} (当前版本: {version})" + +#: remark/cli/commands.py:117 +msgid "Update now? [Y/n]: " +msgstr "是否立即更新? [Y/n]: " + +#: remark/cli/commands.py:124 +msgid "Downloading new version..." +msgstr "正在下载新版本..." + +#: remark/cli/commands.py:131 +msgid "Download complete, preparing update..." +msgstr "下载完成,准备更新..." + +#: remark/cli/commands.py:135 +msgid "Update program has started, the application will exit..." +msgstr "更新程序已启动,程序即将退出..." + +#: remark/cli/commands.py:136 +msgid "Please wait a few moments, the update will complete automatically." +msgstr "请等待几秒钟,更新将自动完成。" + +#: remark/cli/commands.py:142 +msgid "Download failed: Connection reset by server" +msgstr "下载失败:连接被服务器断开" + +#: remark/cli/commands.py:143 +msgid "Please try again later, or visit the following link to download manually:" +msgstr "请稍后重试,或访问以下链接手动下载:" + +#: remark/cli/commands.py:146 +msgid "Download failed: Request timeout" +msgstr "下载失败:请求超时" + +#: remark/cli/commands.py:147 remark/cli/commands.py:151 +msgid "" +"Please check your network connection, or visit the following link to " +"download manually:" +msgstr "请检查网络连接,或访问以下链接手动下载:" + +#: remark/cli/commands.py:150 +msgid "Download failed: Unable to connect to server" +msgstr "下载失败:无法连接到服务器" + +#: remark/cli/commands.py:154 +msgid "Download failed, please check your network or download manually" +msgstr "下载失败,请检查网络连接或手动下载更新" + +#: remark/cli/commands.py:157 +#, python-brace-format +msgid "Update failed: {error}" +msgstr "更新失败: {error}" + +#: remark/cli/commands.py:158 +#, python-brace-format +msgid "Manual download: {url}" +msgstr "手动下载: {url}" + +#: remark/cli/commands.py:175 +msgid "Right-click menu installed successfully" +msgstr "右键菜单安装成功" + +#: remark/cli/commands.py:177 +msgid "Usage Instructions:" +msgstr "使用说明:" + +#: remark/cli/commands.py:178 +msgid " Windows 10: Right-click folder to see 'Add Folder Remark'" +msgstr " Windows 10: 右键文件夹可直接看到「添加文件夹备注」" + +#: remark/cli/commands.py:179 +msgid "" +" Windows 11: Right-click folder → Click 'Show more options' → Add Folder" +" Remark" +msgstr " Windows 11: 右键文件夹 → 点击「显示更多选项」→ 添加文件夹备注" + +#: remark/cli/commands.py:182 +msgid "Right-click menu installation failed" +msgstr "右键菜单安装失败" + +#: remark/cli/commands.py:188 +msgid "Right-click menu uninstalled" +msgstr "右键菜单已卸载" + +#: remark/cli/commands.py:191 +msgid "Right-click menu uninstallation failed" +msgstr "右键菜单卸载失败" + +#: remark/cli/commands.py:218 remark/storage/desktop_ini.py:309 +#, python-brace-format +msgid "Warning: desktop.ini file encoding is {encoding}, not standard UTF-16." +msgstr "警告: desktop.ini 文件编码为 {encoding},不是标准的 UTF-16。" + +#: remark/cli/commands.py:219 remark/storage/desktop_ini.py:310 +msgid "unknown" +msgstr "未知" + +#: remark/cli/commands.py:221 +msgid "This may cause Chinese and other special characters to display abnormally." +msgstr "这可能导致中文等特殊字符显示异常。" + +#: remark/cli/commands.py:225 +msgid "Fix encoding to UTF-16? [Y/n]: " +msgstr "是否修复编码为 UTF-16?[Y/n]: " + +#: remark/cli/commands.py:228 +msgid "Fixed to UTF-16 encoding" +msgstr "已修复为 UTF-16 编码" + +#: remark/cli/commands.py:230 +msgid "Failed to fix encoding" +msgstr "修复失败" + +#: remark/cli/commands.py:233 +msgid "Skip encoding fix" +msgstr "跳过编码修复" + +#: remark/cli/commands.py:236 remark/storage/desktop_ini.py:335 +msgid "Please enter Y or n" +msgstr "请输入 Y 或 n" + +#: remark/cli/commands.py:241 +#, python-brace-format +msgid "Current remark: {remark}" +msgstr "当前备注: {remark}" + +#: remark/cli/commands.py:243 remark/core/folder_handler.py:85 +msgid "This folder has no remark" +msgstr "该文件夹没有备注" + +#: remark/cli/commands.py:248 +#, python-brace-format +msgid "Windows Folder Remark Tool v{version}" +msgstr "Windows 文件夹备注工具 v{version}" + +#: remark/cli/commands.py:249 +msgid "Tip: Press Ctrl + C to exit" +msgstr "提示: 按 Ctrl + C 退出程序" + +#: remark/cli/commands.py:251 +msgid "Enter folder path (or drag here): " +msgstr "请输入文件夹路径(或拖动到这里): " + +#: remark/cli/commands.py:252 +msgid "Enter remark:" +msgstr "请输入备注:" + +#: remark/cli/commands.py:259 +msgid "Path does not exist, please re-enter" +msgstr "路径不存在,请重新输入" + +#: remark/cli/commands.py:263 +msgid "This is a 'file', currently only supports adding remarks to 'folders'" +msgstr "这是一个「文件」,当前仅支持为「文件夹」添加备注" + +#: remark/cli/commands.py:268 +msgid "Remark cannot be empty" +msgstr "备注不要为空哦" + +#: remark/cli/commands.py:274 +msgid " ❤ Thank you for using" +msgstr " ❤ 感谢使用" + +#: remark/cli/commands.py:276 +msgid "Continue processing or press Ctrl + C to exit" +msgstr "继续处理或按 Ctrl + C 退出程序" + +#: remark/cli/commands.py:280 +msgid "Windows Folder Remark Tool" +msgstr "Windows 文件夹备注工具" + +#: remark/cli/commands.py:281 +msgid "Usage:" +msgstr "使用方法:" + +#: remark/cli/commands.py:282 +msgid " Interactive mode: python remark.py" +msgstr " 交互模式: python remark.py" + +#: remark/cli/commands.py:283 +msgid " Command line mode: python remark.py [options] [arguments]" +msgstr " 命令行模式: python remark.py [选项] [参数]" + +#: remark/cli/commands.py:284 +msgid "Options:" +msgstr "选项:" + +#: remark/cli/commands.py:285 +msgid " --install Install right-click menu" +msgstr " --install 安装右键菜单" + +#: remark/cli/commands.py:286 +msgid " --uninstall Uninstall right-click menu" +msgstr " --uninstall 卸载右键菜单" + +#: remark/cli/commands.py:287 +msgid " --update Check for updates" +msgstr " --update 检查更新" + +#: remark/cli/commands.py:288 +msgid " --gui GUI mode (called from right-click menu)" +msgstr " --gui <路径> GUI 模式(右键菜单调用)" + +#: remark/cli/commands.py:289 +msgid " --delete Delete remark" +msgstr " --delete <路径> 删除备注" + +#: remark/cli/commands.py:290 +msgid " --view View remark" +msgstr " --view <路径> 查看备注" + +#: remark/cli/commands.py:291 +msgid " --help, -h Show help information" +msgstr " --help, -h 显示帮助信息" + +#: remark/cli/commands.py:292 +msgid "Examples:" +msgstr "示例:" + +#: remark/cli/commands.py:293 +msgid " [Add remark] python remark.py \"C:\\\\MyFolder\" \"My Folder\"" +msgstr " [添加备注] python remark.py \"C:\\\\MyFolder\" \"这是我的文件夹\"" + +#: remark/cli/commands.py:294 +msgid " [Delete remark] python remark.py --delete \"C:\\\\MyFolder\"" +msgstr " [删除备注] python remark.py --delete \"C:\\\\MyFolder\"" + +#: remark/cli/commands.py:295 +msgid " [View current remark] python remark.py --view \"C:\\\\MyFolder\"" +msgstr " [查看当前备注] python remark.py --view \"C:\\\\MyFolder\"" + +#: remark/cli/commands.py:296 +msgid " [Install right-click menu] python remark.py --install" +msgstr " [安装右键菜单] python remark.py --install" + +#: remark/cli/commands.py:297 +msgid " [Check for updates] python remark.py --update" +msgstr " [检查更新] python remark.py --update" + +#: remark/cli/commands.py:313 +msgid "Error: Path does not exist or not quoted" +msgstr "错误: 路径不存在或未使用引号" + +#: remark/cli/commands.py:314 +msgid "Hint: Use quotes when path contains spaces" +msgstr "提示: 路径包含空格时请使用引号" + +#: remark/cli/commands.py:315 +msgid " windows-folder-remark \"C:\\\\My Documents\" \"Remark content\"" +msgstr " windows-folder-remark \"C:\\\\My Documents\" \"备注内容\"" + +#: remark/cli/commands.py:320 +#, python-brace-format +msgid "Detected path: {path}" +msgstr "检测到路径: {path}" + +#: remark/cli/commands.py:323 +msgid "Error: This is a file, the tool can only set remarks for folders" +msgstr "错误: 这是一个文件,工具只能为文件夹设置备注" + +#: remark/cli/commands.py:328 +#, python-brace-format +msgid "Remark content: {remark}" +msgstr "备注内容: {remark}" + +#: remark/cli/commands.py:330 +msgid "(Will view existing remark)" +msgstr "(将查看现有备注)" + +#: remark/cli/commands.py:332 +msgid "Continue? [Y/n]: " +msgstr "是否继续? [Y/n]: " + +#: remark/cli/commands.py:338 +msgid "Detected multiple possible paths, please select:" +msgstr "检测到多个可能的路径,请选择:" + +#: remark/cli/commands.py:340 +msgid " [file]" +msgstr " [文件]" + +#: remark/cli/commands.py:341 +#, python-brace-format +msgid "" +"\n" +"[{index}] Path: {path}{type_mark}" +msgstr "" +"\n" +"[{index}] 路径: {path}{type_mark}" + +#: remark/cli/commands.py:343 +#, python-brace-format +msgid " Remaining remarks: {remarks}" +msgstr " 剩余备注: {remarks}" + +#: remark/cli/commands.py:345 +msgid " (Will view existing remark)" +msgstr " (将查看现有备注)" + +#: remark/cli/commands.py:346 +msgid "" +"\n" +"[0] Cancel" +msgstr "" +"\n" +"[0] 取消" + +#: remark/cli/commands.py:349 +#, python-brace-format +msgid "" +"\n" +"Please select [0-{max}]: " +msgstr "" +"\n" +"请选择 [0-{max}]: " + +#: remark/cli/commands.py:355 +msgid "" +"\n" +"Error: This is a file, the tool can only set remarks for folders, please " +"reselect" +msgstr "" +"\n" +"错误: 这是一个文件,工具只能为文件夹设置备注,请重新选择" + +#: remark/cli/commands.py:358 +msgid "Invalid selection, please try again" +msgstr "无效选择,请重试" + +#: remark/cli/commands.py:419 +msgid "" +"\n" +"Operation cancelled" +msgstr "" +"\n" +"操作已取消" + +#: remark/cli/commands.py:422 +#, python-brace-format +msgid "An error occurred: {error}" +msgstr "发生错误: {error}" + +#: remark/core/folder_handler.py:24 +#, python-brace-format +msgid "Path is not a folder: {folder_path}" +msgstr "路径不是文件夹: {folder_path}" + +#: remark/core/folder_handler.py:29 +#, python-brace-format +msgid "Remark length exceeds limit, maximum length is {length} characters" +msgstr "备注长度超过限制,最大长度为 {length} 个字符" + +#: remark/core/folder_handler.py:47 remark/core/folder_handler.py:90 +msgid "Failed to clear file attributes" +msgstr "清除文件属性失败" + +#: remark/core/folder_handler.py:52 +msgid "Failed to write desktop.ini" +msgstr "写入 desktop.ini 失败" + +#: remark/core/folder_handler.py:57 +msgid "Failed to set file attributes" +msgstr "设置文件属性失败" + +#: remark/core/folder_handler.py:62 +msgid "Failed to set folder attributes" +msgstr "设置文件夹属性失败" + +#: remark/core/folder_handler.py:66 +#, python-brace-format +msgid "Remark [{remark}] has been set for folder [{folder_path}]" +msgstr "已经为文件夹 [{folder_path}] 设置备注 [{remark}]" + +#: remark/core/folder_handler.py:70 +msgid "Remark added successfully, may take a few minutes to display" +msgstr "备注添加成功,可能需要过几分钟才会显示" + +#: remark/core/folder_handler.py:73 +#, python-brace-format +msgid "Failed to set remark: {error}" +msgstr "设置备注失败: {error}" + +#: remark/core/folder_handler.py:95 +msgid "Failed to remove remark" +msgstr "移除备注失败" + +#: remark/core/folder_handler.py:102 +msgid "Failed to restore file attributes" +msgstr "恢复文件属性失败" + +#: remark/core/folder_handler.py:105 +msgid "Remark deleted successfully" +msgstr "备注删除成功" + +#: remark/storage/desktop_ini.py:313 +msgid "This file needs to be converted to UTF-16 encoding before modification." +msgstr "修改此文件前需要先转换为 UTF-16 编码。" + +#: remark/storage/desktop_ini.py:314 +msgid "" +"The original content will be preserved, only the encoding format will " +"change." +msgstr "原内容会被保留,仅改变编码格式。" + +#: remark/storage/desktop_ini.py:321 +msgid "" +"\n" +"Current file content:" +msgstr "" +"\n" +"当前文件内容:" + +#: remark/storage/desktop_ini.py:328 +msgid "" +"\n" +"Convert to UTF-16 encoding and continue? [Y/n]: " +msgstr "" +"\n" +"是否转换为 UTF-16 编码后继续?[Y/n]: " + +#: remark/storage/desktop_ini.py:332 remark/storage/desktop_ini.py:347 +msgid "Operation cancelled." +msgstr "操作已取消。" + +#: remark/storage/desktop_ini.py:341 +msgid "Converted to UTF-16 encoding." +msgstr "已转换为 UTF-16 编码。" + +#: remark/storage/desktop_ini.py:346 +#, python-brace-format +msgid "Conversion failed: {error}" +msgstr "转换失败: {error}" + +#: remark/utils/platform.py:13 +msgid "" +"Error: This tool adds remarks to files/folders on Windows, other systems " +"are not supported." +msgstr "错误: 此工具为 Windows 系统中的文件/文件夹添加备注,暂不支持其他系统。" + +#: remark/utils/platform.py:14 +#, python-brace-format +msgid "Current system: {system}" +msgstr "当前系统: {system}" diff --git a/pyproject.toml b/pyproject.toml index b254f31..8264ebe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,8 @@ dev = [ "pyfakefs>=5.0.0", "pre-commit>=3.0.0", "pyinstaller>=6.0.0", + "babel>=2.16.0", + "polint>=0.3.0", ] # 命令行入口 @@ -170,3 +172,15 @@ exclude_lines = [ "if TYPE_CHECKING:", "@abstractmethod", ] + + +# Babel - 国际化配置 +# https://babel.pocoo.org/ +[tool.babel] +# 目录配置 +locale_dir = "locale" +domain = "messages" +# 源文件编码 +input_encoding = "utf-8" +# 输出文件编码 +output_encoding = "utf-8" diff --git a/readme.md b/readme.md index 79c8510..1dc71bc 100644 --- a/readme.md +++ b/readme.md @@ -1,139 +1,142 @@ -# Windows 文件夹备注工具 +# Windows Folder Remark Tool -一个通过修改 `Desktop.ini` 文件为 Windows 文件夹添加备注/注释的命令行工具。 +A command-line tool that adds remarks/comments to Windows folders by modifying the `Desktop.ini` file. -## 特性 +[![en](https://img.shields.io/badge/lang-en-blue.svg)](README.md) +[![zh-cn](https://img.shields.io/badge/lang-zh--cn-red.svg)](README.zh-CN.md) -- 支持中文等多语言字符(UTF-16 编码) -- 命令行模式和交互模式 -- 自动编码检测和修复 -- 自动更新检查,保持最新版本 -- 右键菜单集成,快速访问 -- 单文件 exe 打包,无需 Python 环境 +## Features -## 安装 +- Multi-language character support (UTF-16 encoding) +- Command-line and interactive modes +- Automatic encoding detection and repair +- Automatic update checking to stay current +- Right-click menu integration for quick access +- Single-file exe packaging, no Python environment required -### 方式一:使用 exe 文件(推荐) +## Installation -下载 [releases](https://github.com/piratf/windows-folder-remark/releases) 中的 `windows-folder-remark.exe`,直接使用。 +### Method 1: Using exe file (Recommended) -### 方式二:从源码安装 +Download `windows-folder-remark.exe` from [releases](https://github.com/piratf/windows-folder-remark/releases) and use directly. + +### Method 2: Install from source ```bash -# 克隆仓库 +# Clone repository git clone https://github.com/piratf/windows-folder-remark.git cd windows-folder-remark -# 安装依赖(无外部依赖) +# Install dependencies (no external dependencies) pip install -e . -# 运行 +# Run python -m remark.cli --help ``` -## 使用方法 +## Usage -### 命令行模式 +### Command-line Mode ```bash -# 添加备注 -windows-folder-remark.exe "C:\MyFolder" "这是我的文件夹" +# Add remark +windows-folder-remark.exe "C:\MyFolder" "This is my folder" -# 查看备注 +# View remark windows-folder-remark.exe --view "C:\MyFolder" -# 删除备注 +# Delete remark windows-folder-remark.exe --delete "C:\MyFolder" -# 检查更新 +# Check updates windows-folder-remark.exe --update -# 安装右键菜单 +# Install right-click menu windows-folder-remark.exe --install -# 卸载右键菜单 +# Uninstall right-click menu windows-folder-remark.exe --uninstall ``` -### 交互模式 +### Interactive Mode ```bash -# 运行后根据提示操作 +# Follow prompts after running windows-folder-remark.exe ``` -### 右键菜单(推荐) +### Right-click Menu (Recommended) -安装右键菜单后,可以直接在文件资源管理器中右键文件夹添加备注: +After installing the right-click menu, you can add remarks directly in File Explorer by right-clicking folders: ```bash -# 安装右键菜单 +# Install right-click menu windows-folder-remark.exe --install ``` -- **Windows 10**: 右键文件夹可直接看到「添加文件夹备注」 -- **Windows 11**: 右键文件夹 → 点击「显示更多选项」→ 添加文件夹备注 +- **Windows 10**: Right-click folder to see "Add Folder Remark" +- **Windows 11**: Right-click folder → Click "Show more options" → Add Folder Remark -### 自动更新 +### Auto Update -程序会在退出时自动检查更新(每 24 小时一次),如有新版本会提示是否立即更新。 +The program automatically checks for updates on exit (once every 24 hours) and prompts if a new version is available. -也可以手动检查更新: +You can also manually check for updates: ```bash windows-folder-remark.exe --update ``` -## 编码检测 +## Encoding Detection -当使用 `--view` 查看备注时,如果检测到 `desktop.ini` 文件不是标准的 UTF-16 编码,工具会提醒你: +When viewing remarks with `--view`, if the `desktop.ini` file is not in standard UTF-16 encoding, the tool will prompt you: ``` -警告: desktop.ini 文件编码为 utf-8,不是标准的 UTF-16。 -这可能导致中文等特殊字符显示异常。 -是否修复编码为 UTF-16?[Y/n]: +Warning: desktop.ini file encoding is utf-8, not standard UTF-16. +This may cause Chinese and other special characters to display abnormally. +Fix encoding to UTF-16? [Y/n]: ``` -选择 `Y` 可自动修复编码。 +Select `Y` to automatically fix the encoding. -## 开发 +## Development ```bash -# 安装开发依赖 +# Install development dependencies pip install -e ".[dev]" -# 运行测试 +# Run tests pytest -# 代码检查 +# Code check ruff check . ruff format . -# 类型检查 +# Type check mypy remark/ -# 本地打包 exe +# Build exe locally python -m scripts.build ``` -## 原理说明 +## How It Works -该工具通过以下步骤实现文件夹备注: +This tool implements folder remarks through these steps: -1. 在文件夹中创建/修改 `Desktop.ini` 文件 -2. 写入 `[.ShellClassInfo]` 段落和 `InfoTip` 属性 -3. 使用 UTF-16 编码保存文件 -4. 将 `Desktop.ini` 设置为隐藏和系统属性 -5. 将文件夹设置为只读属性(使 Windows 读取 `Desktop.ini`) +1. Create/modify `Desktop.ini` file in the folder +2. Write `[.ShellClassInfo]` section and `InfoTip` property +3. Save file with UTF-16 encoding +4. Set `Desktop.ini` as hidden and system attributes +5. Set folder as read-only (makes Windows read `Desktop.ini`) -参考:[Microsoft 官方文档](https://learn.microsoft.com/en-us/windows/win32/shell/how-to-customize-folders-with-desktop-ini) +Reference: [Microsoft Official Documentation](https://learn.microsoft.com/en-us/windows/win32/shell/how-to-customize-folders-with-desktop-ini) -## 注意事项 +## Notes -- 修改后可能需要几分钟才能在资源管理器中显示 -- 某些文件管理器可能不支持显示文件夹备注 -- 工具会修改文件夹的系统属性 +- May take a few minutes to display in File Explorer after modification +- Some file managers may not support folder remarks +- The tool modifies system attributes of folders -## 许可证 +## License MIT License diff --git a/remark/cli/__main__.py b/remark/cli/__main__.py new file mode 100644 index 0000000..7803fcd --- /dev/null +++ b/remark/cli/__main__.py @@ -0,0 +1,8 @@ +""" +Main entry point for running remark.cli as a module. +""" + +from remark.cli.commands import main + +if __name__ == "__main__": + main() diff --git a/remark/cli/commands.py b/remark/cli/commands.py index 9a6356f..5ea3bb1 100644 --- a/remark/cli/commands.py +++ b/remark/cli/commands.py @@ -11,6 +11,7 @@ from remark.core.folder_handler import FolderCommentHandler from remark.gui import remark_dialog +from remark.i18n import _ as _, set_language from remark.utils import registry from remark.utils.path_resolver import find_candidates from remark.utils.platform import check_platform @@ -48,13 +49,13 @@ def __init__(self): else: self._update_check_done.set() # 不需要检查,直接标记完成 - def _validate_folder(self, path): + def _validate_folder(self, path: str) -> bool: """验证路径是否为文件夹""" if not os.path.exists(path): - print("路径不存在:", path) + print(_("Path does not exist: {path}").format(path=path)) return False if not self.handler.supports(path): - print("路径不是文件夹:", path) + print(_("Path is not a folder: {path}").format(path=path)) return False return True @@ -76,21 +77,21 @@ def check_update_now(self) -> bool: Returns: True 如果有新版本,False 否则 """ - print(f"当前版本: {get_version()}") - print("正在检查更新...") + print(_("Current version: {version}").format(version=get_version())) + print(_("Checking for updates...")) update = check_updates_manual(get_version()) if update: - print(f"\n发现新版本: {update['tag_name']}") - print(f"更新说明: {update['body'][:300]}...") - print(f"完整更新日志: {update['html_url']}") - response = input("\n是否立即更新? [Y/n]: ").lower() + print(_("\nNew version found: {tag_name}").format(tag_name=update["tag_name"])) + print(_("Update notes: {notes}").format(notes=update["body"][:300])) + print(_("Full changelog: {url}").format(url=update["html_url"])) + response = input(_("\nUpdate now? [Y/n]: ")).lower() if response in ("", "y", "yes"): self._perform_update(update) return True else: - print("已是最新版本") + print(_("Already at the latest version")) return False def _wait_for_update_check(self, timeout: float = 2.0) -> None: @@ -106,51 +107,55 @@ def _prompt_update(self) -> None: update = self.pending_update if update is None: return - print(f"\n发现新版本: {update['tag_name']} (当前版本: {get_version()})") - print(f"更新说明: {update['body'][:200]}...") - print(f"完整更新日志: {update['html_url']}") - response = input("是否立即更新? [Y/n]: ").lower() + print( + _("\nNew version available: {tag_name} (Current version: {version})").format( + tag_name=update["tag_name"], version=get_version() + ) + ) + print(_("Update notes: {notes}").format(notes=update["body"][:200])) + print(_("Full changelog: {url}").format(url=update["html_url"])) + response = input(_("Update now? [Y/n]: ")).lower() if response in ("", "y", "yes"): self._perform_update(update) def _perform_update(self, update: dict) -> None: """执行更新流程""" try: - print("正在下载新版本...") + print(_("Downloading new version...")) # 下载到临时目录 new_exe = os.path.join( tempfile.gettempdir(), f"windows-folder-remark-{update['tag_name']}.exe" ) download_update(update["download_url"], new_exe) - print("下载完成,准备更新...") + print(_("Download complete, preparing update...")) old_exe = get_executable_path() script_path = create_update_script(old_exe, new_exe) - print("更新程序已启动,程序即将退出...") - print("请等待几秒钟,更新将自动完成。") + print(_("Update program has started, the application will exit...")) + print(_("Please wait a few moments, the update will complete automatically.")) trigger_update(script_path) sys.exit(0) except urllib.error.URLError as e: err_msg = str(e) if "closed connection" in err_msg.lower() or "connection reset" in err_msg.lower(): - print("下载失败:连接被服务器断开") - print("请稍后重试,或访问以下链接手动下载:") + print(_("Download failed: Connection reset by server")) + print(_("Please try again later, or visit the following link to download manually:")) print(f" {update['html_url']}") elif "timeout" in err_msg.lower(): - print("下载失败:请求超时") - print("请检查网络连接,或访问以下链接手动下载:") + print(_("Download failed: Request timeout")) + print(_("Please check your network connection, or visit the following link to download manually:")) print(f" {update['html_url']}") elif "no route to host" in err_msg.lower() or "hostname" in err_msg.lower(): - print("下载失败:无法连接到服务器") - print("请检查网络连接,或访问以下链接手动下载:") + print(_("Download failed: Unable to connect to server")) + print(_("Please check your network connection, or visit the following link to download manually:")) print(f" {update['html_url']}") else: - print("下载失败,请检查网络连接或手动下载更新") + print(_("Download failed, please check your network or download manually")) print(f" {update['html_url']}") except Exception as e: - print(f"更新失败: {e}") - print(f"手动下载: {update['html_url']}") + print(_("Update failed: {error}").format(error=e)) + print(_("Manual download: {url}").format(url=update["html_url"])) def add_comment(self, path, comment): """添加备注""" @@ -167,23 +172,23 @@ def delete_comment(self, path): def install_menu(self) -> bool: """安装右键菜单""" if registry.install_context_menu(): - print("右键菜单安装成功") + print(_("Right-click menu installed successfully")) print("") - print("使用说明:") - print(" Windows 10: 右键文件夹可直接看到「添加文件夹备注」") - print(" Windows 11: 右键文件夹 → 点击「显示更多选项」→ 添加文件夹备注") + print(_("Usage Instructions:")) + print(_(" Windows 10: Right-click folder to see 'Add Folder Remark'")) + print(_(" Windows 11: Right-click folder → Click 'Show more options' → Add Folder Remark")) return True else: - print("右键菜单安装失败") + print(_("Right-click menu installation failed")) return False def uninstall_menu(self) -> bool: """卸载右键菜单""" if registry.uninstall_context_menu(): - print("右键菜单已卸载") + print(_("Right-click menu uninstalled")) return True else: - print("右键菜单卸载失败") + print(_("Right-click menu uninstallation failed")) return False def gui_mode(self, folder_path: str) -> bool: @@ -198,7 +203,7 @@ def gui_mode(self, folder_path: str) -> bool: return result is not False return False - def view_comment(self, path): + def view_comment(self, path: str) -> None: """查看备注""" if self._validate_folder(path): # 检查 desktop.ini 编码 @@ -209,85 +214,87 @@ def view_comment(self, path): detected_encoding, is_utf16 = DesktopIniHandler.detect_encoding(desktop_ini_path) if not is_utf16: print( - f"警告: desktop.ini 文件编码为 {detected_encoding or '未知'},不是标准的 UTF-16。" + _( + "Warning: desktop.ini file encoding is {encoding}, not standard UTF-16." + ).format(encoding=detected_encoding or _("unknown")) ) - print("这可能导致中文等特殊字符显示异常。") + print(_("This may cause Chinese and other special characters to display abnormally.")) # 询问是否修复 while True: - response = input("是否修复编码为 UTF-16?[Y/n]: ").strip().lower() + response = input(_("Fix encoding to UTF-16? [Y/n]: ")).strip().lower() if response in ("", "y", "yes"): if DesktopIniHandler.fix_encoding(desktop_ini_path, detected_encoding): - print("✓ 已修复为 UTF-16 编码") + print(_("Fixed to UTF-16 encoding")) else: - print("✗ 修复失败") + print(_("Failed to fix encoding")) break elif response in ("n", "no"): - print("跳过编码修复") + print(_("Skip encoding fix")) break else: - print("请输入 Y 或 n") + print(_("Please enter Y or n")) print() # 空行分隔 comment = self.handler.get_comment(path) if comment: - print("当前备注:", comment) + print(_("Current remark: {remark}").format(remark=comment)) else: - print("该文件夹没有备注") + print(_("This folder has no remark")) - def interactive_mode(self): + def interactive_mode(self) -> None: """交互模式""" version = get_version() - print("Windows 文件夹备注工具 v" + version) - print("提示: 按 Ctrl + C 退出程序" + os.linesep) + print(_("Windows Folder Remark Tool v{version}").format(version=version)) + print(_("Tip: Press Ctrl + C to exit") + os.linesep) - input_path_msg = "请输入文件夹路径(或拖动到这里): " - input_comment_msg = "请输入备注:" + input_path_msg = _("Enter folder path (or drag here): ") + input_comment_msg = _("Enter remark:") while True: try: path = input(input_path_msg).replace('"', "").strip() if not os.path.exists(path): - print("路径不存在,请重新输入") + print(_("Path does not exist, please re-enter")) continue if not os.path.isdir(path): - print('这是一个"文件",当前仅支持为"文件夹"添加备注') + print(_("This is a 'file', currently only supports adding remarks to 'folders'")) continue comment = input(input_comment_msg) while not comment: - print("备注不要为空哦") + print(_("Remark cannot be empty")) comment = input(input_comment_msg) self.add_comment(path, comment) except KeyboardInterrupt: - print(" ❤ 感谢使用") + print(_(" ❤ Thank you for using")) break - print(os.linesep + "继续处理或按 Ctrl + C 退出程序" + os.linesep) + print(os.linesep + _("Continue processing or press Ctrl + C to exit") + os.linesep) - def show_help(self): + def show_help(self) -> None: """显示帮助信息""" - print("Windows 文件夹备注工具") - print("使用方法:") - print(" 交互模式: python remark.py") - print(" 命令行模式: python remark.py [选项] [参数]") - print("选项:") - print(" --install 安装右键菜单") - print(" --uninstall 卸载右键菜单") - print(" --update 检查更新") - print(" --gui <路径> GUI 模式(右键菜单调用)") - print(" --delete <路径> 删除备注") - print(" --view <路径> 查看备注") - print(" --help, -h 显示帮助信息") - print("示例:") - print(' [添加备注] python remark.py "C:\\\\MyFolder" "这是我的文件夹"') - print(' [删除备注] python remark.py --delete "C:\\\\MyFolder"') - print(' [查看当前备注] python remark.py --view "C:\\\\MyFolder"') - print(" [安装右键菜单] python remark.py --install") - print(" [检查更新] python remark.py --update") + print(_("Windows Folder Remark Tool")) + print(_("Usage:")) + print(_(" Interactive mode: python remark.py")) + print(_(" Command line mode: python remark.py [options] [arguments]")) + print(_("Options:")) + print(_(" --install Install right-click menu")) + print(_(" --uninstall Uninstall right-click menu")) + print(_(" --update Check for updates")) + print(_(" --gui GUI mode (called from right-click menu)")) + print(_(" --delete Delete remark")) + print(_(" --view View remark")) + print(_(" --help, -h Show help information")) + print(_("Examples:")) + print(_(' [Add remark] python remark.py "C:\\\\MyFolder" "My Folder"')) + print(_(' [Delete remark] python remark.py --delete "C:\\\\MyFolder"')) + print(_(' [View current remark] python remark.py --view "C:\\\\MyFolder"')) + print(_(" [Install right-click menu] python remark.py --install")) + print(_(" [Check for updates] python remark.py --update")) def _select_from_multiple_candidates( self, candidates: list, show_remaining: bool = False @@ -344,26 +351,26 @@ def _handle_ambiguous_path(self, args_list: list[str]) -> tuple[str | None, str candidates = find_candidates(args_list) if not candidates: - print("错误: 路径不存在或未使用引号") - print("提示: 路径包含空格时请使用引号") - print(' windows-folder-remark "C:\\\\My Documents" "备注内容"') + print(_("Error: Path does not exist or not quoted")) + print(_("Hint: Use quotes when path contains spaces")) + print(_(' windows-folder-remark "C:\\\\My Documents" "Remark content"')) return None, None if len(candidates) == 1: path, remaining, path_type = candidates[0] - print(f"检测到路径: {path}") + print(_("Detected path: {path}").format(path=path)) if path_type == "file": - print("错误: 这是一个文件,工具只能为文件夹设置备注") + print(_("Error: This is a file, the tool can only set remarks for folders")) return None, None if remaining: comment = " ".join(remaining) - print(f"备注内容: {comment}") + print(_("Remark content: {remark}").format(remark=comment)) else: - print("(将查看现有备注)") + print(_("(Will view existing remark)")) - if input("是否继续? [Y/n]: ").lower() in ("", "y", "yes"): + if input(_("Continue? [Y/n]: ")).lower() in ("", "y", "yes"): return str(path), " ".join(remaining) if remaining else None return None, None @@ -396,7 +403,7 @@ def _resolve_path_from_ambiguous_args(self, args_list: list[str]) -> str | None: if path_type == "folder": return str(path) else: - print("错误: 这是一个文件,工具只能为文件夹设置备注") + print(_("Error: This is a file, the tool can only set remarks for folders")) return None result = self._select_from_multiple_candidates(candidates, show_remaining=False) @@ -404,7 +411,7 @@ def _resolve_path_from_ambiguous_args(self, args_list: list[str]) -> str | None: return result[0] return None - def run(self, argv=None): + def run(self, argv=None) -> None: """运行 CLI""" if not check_platform(): sys.exit(1) @@ -418,9 +425,14 @@ def run(self, argv=None): parser.add_argument("--delete", metavar="PATH", help="删除备注") parser.add_argument("--view", metavar="PATH", help="查看备注") parser.add_argument("--help", "-h", action="store_true", help="显示帮助信息") + parser.add_argument("--lang", "-L", metavar="LANG", help="设置语言 (en, zh_CN)", dest="lang") args = parser.parse_args(argv) + # 设置语言 + if args.lang: + set_language(args.lang) + if args.help: self.show_help() elif args.install: @@ -464,16 +476,16 @@ def run(self, argv=None): self.interactive_mode() -def main(): +def main() -> None: """主入口""" cli = CLI() try: cli.run() except KeyboardInterrupt: - print("\n操作已取消") + print(_("\nOperation cancelled")) sys.exit(0) except Exception as e: - print("发生错误:", str(e)) + print(_("An error occurred: {error}").format(error=str(e))) sys.exit(1) finally: # 等待后台检测完成(最多等待 2 秒) diff --git a/remark/core/folder_handler.py b/remark/core/folder_handler.py index b1b5459..286ad04 100644 --- a/remark/core/folder_handler.py +++ b/remark/core/folder_handler.py @@ -10,6 +10,7 @@ import os from remark.core.base import CommentHandler +from remark.i18n import _ as _ from remark.storage.desktop_ini import DesktopIniHandler from remark.utils.constants import MAX_COMMENT_LENGTH @@ -17,20 +18,24 @@ class FolderCommentHandler(CommentHandler): """文件夹备注处理器""" - def set_comment(self, folder_path, comment): + def set_comment(self, folder_path: str, comment: str) -> bool: """设置文件夹备注""" if not os.path.isdir(folder_path): - print("路径不是文件夹:", folder_path) + print(_("Path is not a folder: {folder_path}").format(folder_path=folder_path)) return False if len(comment) > MAX_COMMENT_LENGTH: - print("备注长度超过限制,最大长度为", MAX_COMMENT_LENGTH, "个字符") + print( + _("Remark length exceeds limit, maximum length is {length} characters").format( + length=MAX_COMMENT_LENGTH + ) + ) comment = comment[:MAX_COMMENT_LENGTH] return self._set_comment_desktop_ini(folder_path, comment) @staticmethod - def _set_comment_desktop_ini(folder_path, comment): + def _set_comment_desktop_ini(folder_path: str, comment: str) -> bool: """使用 desktop.ini 设置备注""" desktop_ini_path = DesktopIniHandler.get_path(folder_path) @@ -39,63 +44,67 @@ def _set_comment_desktop_ini(folder_path, comment): if DesktopIniHandler.exists( folder_path ) and not DesktopIniHandler.clear_file_attributes(desktop_ini_path): - print("清除文件属性失败") + print(_("Failed to clear file attributes")) return False # 使用 UTF-16 编码写入 desktop.ini if not DesktopIniHandler.write_info_tip(folder_path, comment): - print("写入 desktop.ini 失败") + print(_("Failed to write desktop.ini")) return False # 设置 desktop.ini 文件为隐藏和系统属性 if not DesktopIniHandler.set_file_hidden_system_attributes(desktop_ini_path): - print("设置文件属性失败") + print(_("Failed to set file attributes")) return False # 设置文件夹为只读属性(使 desktop.ini 生效) if not DesktopIniHandler.set_folder_system_attributes(folder_path): - print("设置文件夹属性失败") + print(_("Failed to set folder attributes")) return False - print(f"已经为文件夹 [{folder_path}] 设置备注 [{comment}]") - print("备注添加成功,可能需要过几分钟才会显示") + print( + _("Remark [{remark}] has been set for folder [{folder_path}]").format( + remark=comment, folder_path=folder_path + ) + ) + print(_("Remark added successfully, may take a few minutes to display")) return True except Exception as e: - print("设置备注失败:", str(e)) + print(_("Failed to set remark: {error}").format(error=str(e))) return False - def get_comment(self, folder_path): + def get_comment(self, folder_path: str) -> str | None: """获取文件夹备注""" return DesktopIniHandler.read_info_tip(folder_path) - def delete_comment(self, folder_path): + def delete_comment(self, folder_path: str) -> bool: """删除文件夹备注""" desktop_ini_path = DesktopIniHandler.get_path(folder_path) if not DesktopIniHandler.exists(folder_path): - print("该文件夹没有备注") + print(_("This folder has no remark")) return True # 清除文件属性以便修改 if not DesktopIniHandler.clear_file_attributes(desktop_ini_path): - print("清除文件属性失败") + print(_("Failed to clear file attributes")) return False # 移除 InfoTip 行(保留其他设置如 IconResource) if not DesktopIniHandler.remove_info_tip(folder_path): - print("移除备注失败") + print(_("Failed to remove remark")) return False # 如果 desktop.ini 仍存在,恢复文件属性 if DesktopIniHandler.exists( folder_path ) and not DesktopIniHandler.set_file_hidden_system_attributes(desktop_ini_path): - print("恢复文件属性失败") + print(_("Failed to restore file attributes")) return False - print("备注删除成功") + print(_("Remark deleted successfully")) return True - def supports(self, path): + def supports(self, path: str) -> bool: """检查是否支持该路径""" return os.path.isdir(path) diff --git a/remark/i18n.py b/remark/i18n.py new file mode 100644 index 0000000..78340d2 --- /dev/null +++ b/remark/i18n.py @@ -0,0 +1,154 @@ +""" +Internationalization (i18n) module for Windows Folder Remark tool. + +This module provides translation support using gettext. +""" +from __future__ import annotations + +import gettext +import locale +import os +from pathlib import Path +from typing import Final + +# 项目根目录 +PROJECT_ROOT: Final = Path(__file__).parent.parent + +# 翻译文件目录 +LOCALE_DIR: Final = PROJECT_ROOT / "locale" + +# 翻译域 +DOMAIN: Final = "messages" + +# 支持的语言列表 +SUPPORTED_LANGUAGES: Final = ("en", "zh") + + +def get_system_language() -> str: + """ + 获取系统语言设置. + + Returns: + 语言代码(如 'en', 'zh_CN'),如果不支持则返回默认的 'en' + """ + # 尝试从环境变量获取 + lang = os.environ.get("LANG", "") + if lang: + # 提取语言代码(如 zh_CN.UTF-8 -> zh_CN) + lang_code = lang.split(".")[0].split("_")[0] + if lang_code in SUPPORTED_LANGUAGES: + return lang_code + # 处理完整语言代码(如 zh_CN) + if "_" in lang: + full_lang = lang.split(".")[0] + if full_lang in SUPPORTED_LANGUAGES: + return full_lang + + # 尝试从 locale 获取 + try: + loc = locale.getlocale()[0] + if loc: + # locale 格式可能是 'zh_CN' 或 'zh-CN' + normalized = loc.replace("-", "_") + if normalized in SUPPORTED_LANGUAGES: + return normalized + # 尝试只取语言部分 + lang_code = normalized.split("_")[0] + if lang_code in SUPPORTED_LANGUAGES: + return lang_code + except (ValueError, AttributeError): + pass + + # 默认返回英文 + return "en" + + +def init_translation(language: str | None = None) -> gettext.GNUTranslations: + """ + 初始化翻译. + + Args: + language: 语言代码,如果为 None 则使用系统语言 + + Returns: + 翻译函数 + """ + if language is None: + language = get_system_language() + + # 确保语言受支持 + if language not in SUPPORTED_LANGUAGES: + language = "en" + + # 尝试加载翻译 + try: + translator = gettext.translation( + domain=DOMAIN, + localedir=str(LOCALE_DIR), + languages=[language], + fallback=True, + ) + return translator + except Exception: + # 如果加载失败,使用空翻译(返回原字符串) + return gettext.NullTranslations() + + +# 全局翻译函数 +_translator: gettext.GNUTranslations | gettext.NullTranslations | None = None + + +def get_translator() -> gettext.GNUTranslations | gettext.NullTranslations: + """ + 获取当前翻译器. + + Returns: + 翻译器实例 + """ + global _translator + if _translator is None: + _translator = init_translation() + return _translator + + +def set_language(language: str) -> None: + """ + 设置当前语言. + + Args: + language: 语言代码(如 'en', 'zh_CN') + """ + global _translator + _translator = init_translation(language) + + +def gettext_function(message: str) -> str: + """ + 翻译函数(用于在代码中标记可翻译字符串). + + Args: + message: 要翻译的字符串 + + Returns: + 翻译后的字符串 + """ + return get_translator().gettext(message) + + +def ngettext_function(singular: str, plural: str, n: int) -> str: + """ + 复数形式翻译函数. + + Args: + singular: 单数形式 + plural: 复数形式 + n: 数量 + + Returns: + 翻译后的字符串 + """ + return get_translator().ngettext(singular, plural, n) + + +# 默认导出的翻译函数 +_ = gettext_function diff --git a/remark/storage/desktop_ini.py b/remark/storage/desktop_ini.py index f62effe..51c9f7e 100644 --- a/remark/storage/desktop_ini.py +++ b/remark/storage/desktop_ini.py @@ -14,6 +14,8 @@ import codecs import os +from remark.i18n import _ as _ + class EncodingConversionCanceled(Exception): # noqa: N818 """编码转换被用户取消""" @@ -303,42 +305,46 @@ def ensure_utf16_encoding(file_path): return # 已经是 UTF-16 # 文件不是 UTF-16,需要用户确认 - print(f"警告:desktop.ini 文件编码为 {encoding or '未知'},不是标准的 UTF-16。") - print("修改此文件前需要先转换为 UTF-16 编码。") - print("原内容会被保留,仅改变编码格式。") + print( + _("Warning: desktop.ini file encoding is {encoding}, not standard UTF-16.").format( + encoding=encoding or _("unknown") + ) + ) + print(_("This file needs to be converted to UTF-16 encoding before modification.")) + print(_("The original content will be preserved, only the encoding format will change.")) try: # 显示文件预览 with codecs.open(file_path, "r", encoding=encoding or "utf-8") as f: content = f.read() - print("\n当前文件内容:") + print(_("\nCurrent file content:")) print("-" * 40) print(content) print("-" * 40) # 用户确认 while True: - response = input("\n是否转换为 UTF-16 编码后继续?[Y/n]: ").strip().lower() + response = input(_("\nConvert to UTF-16 encoding and continue? [Y/n]: ")).strip().lower() if response in ("", "y", "yes"): break elif response in ("n", "no"): - print("操作已取消。") + print(_("Operation cancelled.")) raise EncodingConversionCanceled("用户拒绝编码转换") else: - print("请输入 Y 或 n") + print(_("Please enter Y or n")) # 执行转换 with codecs.open(file_path, "w", encoding=DESKTOP_INI_ENCODING) as f: f.write(content) - print("✓ 已转换为 UTF-16 编码。") + print(_("Converted to UTF-16 encoding.")) except EncodingConversionCanceled: raise except Exception as e: - print(f"转换失败: {e}") - print("操作已取消。") + print(_("Conversion failed: {error}").format(error=e)) + print(_("Operation cancelled.")) raise EncodingConversionCanceled(f"编码转换失败: {e}") from e @staticmethod diff --git a/remark/utils/platform.py b/remark/utils/platform.py index 7141acc..2714a39 100644 --- a/remark/utils/platform.py +++ b/remark/utils/platform.py @@ -4,11 +4,13 @@ import platform +from remark.i18n import _ as _ -def check_platform(): + +def check_platform() -> bool: """检查是否为 Windows 系统""" if platform.system() != "Windows": - print("错误: 此工具为 Windows 系统中的文件/文件夹添加备注,暂不支持其他系统。") - print("当前系统:", platform.system()) + print(_("Error: This tool adds remarks to files/folders on Windows, other systems are not supported.")) + print(_("Current system: {system}").format(system=platform.system())) return False return True From 5b45b3ec9c966af134ba2fda6a7c92c1ca7f90e2 Mon Sep 17 00:00:00 2001 From: piratf Date: Thu, 29 Jan 2026 22:54:32 +0800 Subject: [PATCH 02/27] chore: improve i18n workflow and fix README naming - Track messages.pot file for translation completeness checking - Use pybabel update --check instead of custom script - Rename README.zh-CN.md to README.zh.md (W3C standard) - Update language badges in README files - Add i18n completeness check to pre-commit hooks --- .gitignore | 3 +- .pre-commit-config.yaml | 10 +- README.zh-CN.md => README.zh.md | 2 +- messages.pot | 500 ++++++++++++++++++++++++++++++++ readme.md | 2 +- 5 files changed, 512 insertions(+), 5 deletions(-) rename README.zh-CN.md => README.zh.md (97%) create mode 100644 messages.pot diff --git a/.gitignore b/.gitignore index aa2188c..58fe1c2 100644 --- a/.gitignore +++ b/.gitignore @@ -34,9 +34,8 @@ dmypy.json *.tar.gz /tools/upx/ -# i18n - 编译后的翻译文件(只追踪源 .po 文件) +# i18n - 只忽略编译后的翻译文件 locale/*/LC_MESSAGES/*.mo -messages.pot # bv (beads viewer) local config and caches .bv/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9f90dd6..f9a0f42 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -68,13 +68,21 @@ repos: # i18n 相关检查 - id: extract-translations name: Extract i18n messages - entry: bash -c 'pybabel extract -F babel.cfg -o messages.pot remark/ && echo "Translations extracted successfully"' + entry: bash -c 'pybabel extract -k "_" -k "gettext" -k "ngettext" -o messages.pot remark/ && echo "Translations extracted successfully"' language: system types: [python] stages: [pre-commit] pass_filenames: false require_serial: true + - id: check-i18n-completeness + name: Check i18n completeness + entry: bash -c 'pybabel update -i messages.pot -d locale --check' + language: system + types: [python] + stages: [pre-commit] + pass_filenames: false + - id: check-po-files name: Check PO files with polint entry: bash -c 'if compgen -G "locale/*/LC_MESSAGES/*.po" > /dev/null; then polint locale/*/LC_MESSAGES/*.po || true; fi' diff --git a/README.zh-CN.md b/README.zh.md similarity index 97% rename from README.zh-CN.md rename to README.zh.md index 1b6bd95..5ede7d4 100644 --- a/README.zh-CN.md +++ b/README.zh.md @@ -3,7 +3,7 @@ 一个通过修改 `Desktop.ini` 文件为 Windows 文件夹添加备注/注释的命令行工具。 [![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) -[![zh-cn](https://img.shields.io/badge/lang-zh--cn-blue.svg)](README.zh-CN.md) +[![zh](https://img.shields.io/badge/lang-zh-blue.svg)](README.zh.md) ## 特性 diff --git a/messages.pot b/messages.pot new file mode 100644 index 0000000..a572213 --- /dev/null +++ b/messages.pot @@ -0,0 +1,500 @@ +# Translations template for PROJECT. +# Copyright (C) 2026 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2026-01-29 22:53+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.17.0\n" + +#: remark/cli/commands.py:55 +#, python-brace-format +msgid "Path does not exist: {path}" +msgstr "" + +#: remark/cli/commands.py:58 +#, python-brace-format +msgid "Path is not a folder: {path}" +msgstr "" + +#: remark/cli/commands.py:80 +#, python-brace-format +msgid "Current version: {version}" +msgstr "" + +#: remark/cli/commands.py:81 +msgid "Checking for updates..." +msgstr "" + +#: remark/cli/commands.py:86 +#, python-brace-format +msgid "" +"\n" +"New version found: {tag_name}" +msgstr "" + +#: remark/cli/commands.py:87 remark/cli/commands.py:115 +#, python-brace-format +msgid "Update notes: {notes}" +msgstr "" + +#: remark/cli/commands.py:88 remark/cli/commands.py:116 +#, python-brace-format +msgid "Full changelog: {url}" +msgstr "" + +#: remark/cli/commands.py:89 +msgid "" +"\n" +"Update now? [Y/n]: " +msgstr "" + +#: remark/cli/commands.py:94 +msgid "Already at the latest version" +msgstr "" + +#: remark/cli/commands.py:111 +#, python-brace-format +msgid "" +"\n" +"New version available: {tag_name} (Current version: {version})" +msgstr "" + +#: remark/cli/commands.py:117 +msgid "Update now? [Y/n]: " +msgstr "" + +#: remark/cli/commands.py:124 +msgid "Downloading new version..." +msgstr "" + +#: remark/cli/commands.py:131 +msgid "Download complete, preparing update..." +msgstr "" + +#: remark/cli/commands.py:135 +msgid "Update program has started, the application will exit..." +msgstr "" + +#: remark/cli/commands.py:136 +msgid "Please wait a few moments, the update will complete automatically." +msgstr "" + +#: remark/cli/commands.py:142 +msgid "Download failed: Connection reset by server" +msgstr "" + +#: remark/cli/commands.py:143 +msgid "Please try again later, or visit the following link to download manually:" +msgstr "" + +#: remark/cli/commands.py:146 +msgid "Download failed: Request timeout" +msgstr "" + +#: remark/cli/commands.py:147 remark/cli/commands.py:151 +msgid "" +"Please check your network connection, or visit the following link to " +"download manually:" +msgstr "" + +#: remark/cli/commands.py:150 +msgid "Download failed: Unable to connect to server" +msgstr "" + +#: remark/cli/commands.py:154 +msgid "Download failed, please check your network or download manually" +msgstr "" + +#: remark/cli/commands.py:157 +#, python-brace-format +msgid "Update failed: {error}" +msgstr "" + +#: remark/cli/commands.py:158 +#, python-brace-format +msgid "Manual download: {url}" +msgstr "" + +#: remark/cli/commands.py:175 +msgid "Right-click menu installed successfully" +msgstr "" + +#: remark/cli/commands.py:177 +msgid "Usage Instructions:" +msgstr "" + +#: remark/cli/commands.py:178 +msgid " Windows 10: Right-click folder to see 'Add Folder Remark'" +msgstr "" + +#: remark/cli/commands.py:179 +msgid "" +" Windows 11: Right-click folder → Click 'Show more options' → Add Folder" +" Remark" +msgstr "" + +#: remark/cli/commands.py:182 +msgid "Right-click menu installation failed" +msgstr "" + +#: remark/cli/commands.py:188 +msgid "Right-click menu uninstalled" +msgstr "" + +#: remark/cli/commands.py:191 +msgid "Right-click menu uninstallation failed" +msgstr "" + +#: remark/cli/commands.py:218 remark/storage/desktop_ini.py:309 +#, python-brace-format +msgid "Warning: desktop.ini file encoding is {encoding}, not standard UTF-16." +msgstr "" + +#: remark/cli/commands.py:219 remark/storage/desktop_ini.py:310 +msgid "unknown" +msgstr "" + +#: remark/cli/commands.py:221 +msgid "This may cause Chinese and other special characters to display abnormally." +msgstr "" + +#: remark/cli/commands.py:225 +msgid "Fix encoding to UTF-16? [Y/n]: " +msgstr "" + +#: remark/cli/commands.py:228 +msgid "Fixed to UTF-16 encoding" +msgstr "" + +#: remark/cli/commands.py:230 +msgid "Failed to fix encoding" +msgstr "" + +#: remark/cli/commands.py:233 +msgid "Skip encoding fix" +msgstr "" + +#: remark/cli/commands.py:236 remark/storage/desktop_ini.py:335 +msgid "Please enter Y or n" +msgstr "" + +#: remark/cli/commands.py:241 +#, python-brace-format +msgid "Current remark: {remark}" +msgstr "" + +#: remark/cli/commands.py:243 remark/core/folder_handler.py:85 +msgid "This folder has no remark" +msgstr "" + +#: remark/cli/commands.py:248 +#, python-brace-format +msgid "Windows Folder Remark Tool v{version}" +msgstr "" + +#: remark/cli/commands.py:249 +msgid "Tip: Press Ctrl + C to exit" +msgstr "" + +#: remark/cli/commands.py:251 +msgid "Enter folder path (or drag here): " +msgstr "" + +#: remark/cli/commands.py:252 +msgid "Enter remark:" +msgstr "" + +#: remark/cli/commands.py:259 +msgid "Path does not exist, please re-enter" +msgstr "" + +#: remark/cli/commands.py:263 +msgid "This is a 'file', currently only supports adding remarks to 'folders'" +msgstr "" + +#: remark/cli/commands.py:268 +msgid "Remark cannot be empty" +msgstr "" + +#: remark/cli/commands.py:274 +msgid " ❤ Thank you for using" +msgstr "" + +#: remark/cli/commands.py:276 +msgid "Continue processing or press Ctrl + C to exit" +msgstr "" + +#: remark/cli/commands.py:280 +msgid "Windows Folder Remark Tool" +msgstr "" + +#: remark/cli/commands.py:281 +msgid "Usage:" +msgstr "" + +#: remark/cli/commands.py:282 +msgid " Interactive mode: python remark.py" +msgstr "" + +#: remark/cli/commands.py:283 +msgid " Command line mode: python remark.py [options] [arguments]" +msgstr "" + +#: remark/cli/commands.py:284 +msgid "Options:" +msgstr "" + +#: remark/cli/commands.py:285 +msgid " --install Install right-click menu" +msgstr "" + +#: remark/cli/commands.py:286 +msgid " --uninstall Uninstall right-click menu" +msgstr "" + +#: remark/cli/commands.py:287 +msgid " --update Check for updates" +msgstr "" + +#: remark/cli/commands.py:288 +msgid " --gui GUI mode (called from right-click menu)" +msgstr "" + +#: remark/cli/commands.py:289 +msgid " --delete Delete remark" +msgstr "" + +#: remark/cli/commands.py:290 +msgid " --view View remark" +msgstr "" + +#: remark/cli/commands.py:291 +msgid " --help, -h Show help information" +msgstr "" + +#: remark/cli/commands.py:292 +msgid "Examples:" +msgstr "" + +#: remark/cli/commands.py:293 +msgid " [Add remark] python remark.py \"C:\\\\MyFolder\" \"My Folder\"" +msgstr "" + +#: remark/cli/commands.py:294 +msgid " [Delete remark] python remark.py --delete \"C:\\\\MyFolder\"" +msgstr "" + +#: remark/cli/commands.py:295 +msgid " [View current remark] python remark.py --view \"C:\\\\MyFolder\"" +msgstr "" + +#: remark/cli/commands.py:296 +msgid " [Install right-click menu] python remark.py --install" +msgstr "" + +#: remark/cli/commands.py:297 +msgid " [Check for updates] python remark.py --update" +msgstr "" + +#: remark/cli/commands.py:313 +msgid "Error: Path does not exist or not quoted" +msgstr "" + +#: remark/cli/commands.py:314 +msgid "Hint: Use quotes when path contains spaces" +msgstr "" + +#: remark/cli/commands.py:315 +msgid " windows-folder-remark \"C:\\\\My Documents\" \"Remark content\"" +msgstr "" + +#: remark/cli/commands.py:320 +#, python-brace-format +msgid "Detected path: {path}" +msgstr "" + +#: remark/cli/commands.py:323 +msgid "Error: This is a file, the tool can only set remarks for folders" +msgstr "" + +#: remark/cli/commands.py:328 +#, python-brace-format +msgid "Remark content: {remark}" +msgstr "" + +#: remark/cli/commands.py:330 +msgid "(Will view existing remark)" +msgstr "" + +#: remark/cli/commands.py:332 +msgid "Continue? [Y/n]: " +msgstr "" + +#: remark/cli/commands.py:338 +msgid "Detected multiple possible paths, please select:" +msgstr "" + +#: remark/cli/commands.py:340 +msgid " [file]" +msgstr "" + +#: remark/cli/commands.py:341 +#, python-brace-format +msgid "" +"\n" +"[{index}] Path: {path}{type_mark}" +msgstr "" + +#: remark/cli/commands.py:343 +#, python-brace-format +msgid " Remaining remarks: {remarks}" +msgstr "" + +#: remark/cli/commands.py:345 +msgid " (Will view existing remark)" +msgstr "" + +#: remark/cli/commands.py:346 +msgid "" +"\n" +"[0] Cancel" +msgstr "" + +#: remark/cli/commands.py:349 +#, python-brace-format +msgid "" +"\n" +"Please select [0-{max}]: " +msgstr "" + +#: remark/cli/commands.py:355 +msgid "" +"\n" +"Error: This is a file, the tool can only set remarks for folders, please " +"reselect" +msgstr "" + +#: remark/cli/commands.py:358 +msgid "Invalid selection, please try again" +msgstr "" + +#: remark/cli/commands.py:419 +msgid "" +"\n" +"Operation cancelled" +msgstr "" + +#: remark/cli/commands.py:422 +#, python-brace-format +msgid "An error occurred: {error}" +msgstr "" + +#: remark/core/folder_handler.py:24 +#, python-brace-format +msgid "Path is not a folder: {folder_path}" +msgstr "" + +#: remark/core/folder_handler.py:29 +#, python-brace-format +msgid "Remark length exceeds limit, maximum length is {length} characters" +msgstr "" + +#: remark/core/folder_handler.py:47 remark/core/folder_handler.py:90 +msgid "Failed to clear file attributes" +msgstr "" + +#: remark/core/folder_handler.py:52 +msgid "Failed to write desktop.ini" +msgstr "" + +#: remark/core/folder_handler.py:57 +msgid "Failed to set file attributes" +msgstr "" + +#: remark/core/folder_handler.py:62 +msgid "Failed to set folder attributes" +msgstr "" + +#: remark/core/folder_handler.py:66 +#, python-brace-format +msgid "Remark [{remark}] has been set for folder [{folder_path}]" +msgstr "" + +#: remark/core/folder_handler.py:70 +msgid "Remark added successfully, may take a few minutes to display" +msgstr "" + +#: remark/core/folder_handler.py:73 +#, python-brace-format +msgid "Failed to set remark: {error}" +msgstr "" + +#: remark/core/folder_handler.py:95 +msgid "Failed to remove remark" +msgstr "" + +#: remark/core/folder_handler.py:102 +msgid "Failed to restore file attributes" +msgstr "" + +#: remark/core/folder_handler.py:105 +msgid "Remark deleted successfully" +msgstr "" + +#: remark/storage/desktop_ini.py:313 +msgid "This file needs to be converted to UTF-16 encoding before modification." +msgstr "" + +#: remark/storage/desktop_ini.py:314 +msgid "" +"The original content will be preserved, only the encoding format will " +"change." +msgstr "" + +#: remark/storage/desktop_ini.py:321 +msgid "" +"\n" +"Current file content:" +msgstr "" + +#: remark/storage/desktop_ini.py:328 +msgid "" +"\n" +"Convert to UTF-16 encoding and continue? [Y/n]: " +msgstr "" + +#: remark/storage/desktop_ini.py:332 remark/storage/desktop_ini.py:347 +msgid "Operation cancelled." +msgstr "" + +#: remark/storage/desktop_ini.py:341 +msgid "Converted to UTF-16 encoding." +msgstr "" + +#: remark/storage/desktop_ini.py:346 +#, python-brace-format +msgid "Conversion failed: {error}" +msgstr "" + +#: remark/utils/platform.py:13 +msgid "" +"Error: This tool adds remarks to files/folders on Windows, other systems " +"are not supported." +msgstr "" + +#: remark/utils/platform.py:14 +#, python-brace-format +msgid "Current system: {system}" +msgstr "" + diff --git a/readme.md b/readme.md index 1dc71bc..3f10c28 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ A command-line tool that adds remarks/comments to Windows folders by modifying the `Desktop.ini` file. [![en](https://img.shields.io/badge/lang-en-blue.svg)](README.md) -[![zh-cn](https://img.shields.io/badge/lang-zh--cn-red.svg)](README.zh-CN.md) +[![zh](https://img.shields.io/badge/lang-zh-red.svg)](README.zh.md) ## Features From 4a2b20912a4b6fc342bd2ff588a3e79178377acc Mon Sep 17 00:00:00 2001 From: piratf Date: Thu, 29 Jan 2026 23:03:46 +0800 Subject: [PATCH 03/27] =?UTF-8?q?feat:=20=E6=94=B9=E8=BF=9B=20Windows=20?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E6=A3=80=E6=B5=8B=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20GetUserDefaultLocaleName=20API=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 _get_windows_locale() 函数,使用 ctypes 调用 Windows API - 优先使用 GetUserDefaultLocaleName 获取用户默认区域设置 - 改进 get_system_language() 处理各种 locale 格式 - 添加 i18n 单元测试(31 个测试用例) - 修复 --lang 参数帮助文本(zh_CN -> zh) --- remark/cli/commands.py | 2 +- remark/i18n.py | 58 ++++++++++- tests/unit/test_i18n.py | 206 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 tests/unit/test_i18n.py diff --git a/remark/cli/commands.py b/remark/cli/commands.py index 5ea3bb1..45f6942 100644 --- a/remark/cli/commands.py +++ b/remark/cli/commands.py @@ -425,7 +425,7 @@ def run(self, argv=None) -> None: parser.add_argument("--delete", metavar="PATH", help="删除备注") parser.add_argument("--view", metavar="PATH", help="查看备注") parser.add_argument("--help", "-h", action="store_true", help="显示帮助信息") - parser.add_argument("--lang", "-L", metavar="LANG", help="设置语言 (en, zh_CN)", dest="lang") + parser.add_argument("--lang", "-L", metavar="LANG", help="设置语言 (en, zh)", dest="lang") args = parser.parse_args(argv) diff --git a/remark/i18n.py b/remark/i18n.py index 78340d2..bffe71b 100644 --- a/remark/i18n.py +++ b/remark/i18n.py @@ -5,9 +5,11 @@ """ from __future__ import annotations +import ctypes import gettext import locale import os +import platform from pathlib import Path from typing import Final @@ -24,21 +26,66 @@ SUPPORTED_LANGUAGES: Final = ("en", "zh") +def _get_windows_locale() -> str | None: + """ + 在 Windows 上使用 Windows API 获取用户默认区域设置名称. + + Returns: + 区域设置名称(如 'zh-CN', 'en-US'),如果获取失败则返回 None + """ + try: + # GetUserDefaultLocaleName 返回 locale 名称(如 'zh-CN', 'en-US') + # 缓冲区大小为 LOCALE_NAME_MAX_LENGTH (85) + buffer_size: int = 85 + buffer: ctypes.Array[ctypes.c_wchar] = ctypes.create_unicode_buffer(buffer_size) + + # kernel32.dll 中的 GetUserDefaultLocaleName 函数 + # 原型: int GetUserDefaultLocaleName(LPWSTR lpLocaleName, int cchLocaleName) + kernel32 = ctypes.windll.kernel32 + kernel32.GetUserDefaultLocaleName.restype = ctypes.c_int + kernel32.GetUserDefaultLocaleName.argtypes = [ + ctypes.POINTER(ctypes.c_wchar), + ctypes.c_int, + ] + + result: int = kernel32.GetUserDefaultLocaleName(buffer, buffer_size) + + if result > 0: + return buffer.value.strip() + except (AttributeError, OSError, ValueError): + pass + + return None + + def get_system_language() -> str: """ 获取系统语言设置. + 在 Windows 上优先使用 GetUserDefaultLocaleName API, + 在其他平台上使用环境变量和 locale.getlocale()。 + Returns: - 语言代码(如 'en', 'zh_CN'),如果不支持则返回默认的 'en' + 语言代码(如 'en', 'zh'),如果不支持则返回默认的 'en' """ + # Windows 平台优先使用 Windows API + if platform.system() == "Windows": + windows_locale = _get_windows_locale() + if windows_locale: + # Windows locale 格式为 'zh-CN', 'en-US' 等 + # 提取语言部分(zh-CN -> zh) + lang_code = windows_locale.split("-")[0] + if lang_code in SUPPORTED_LANGUAGES: + return lang_code + # 尝试从环境变量获取 lang = os.environ.get("LANG", "") if lang: - # 提取语言代码(如 zh_CN.UTF-8 -> zh_CN) + # 提取语言代码(如 zh_CN.UTF-8 -> zh) lang_code = lang.split(".")[0].split("_")[0] if lang_code in SUPPORTED_LANGUAGES: return lang_code - # 处理完整语言代码(如 zh_CN) + # 处理完整语言代码(如 zh_CN -> zh) if "_" in lang: full_lang = lang.split(".")[0] if full_lang in SUPPORTED_LANGUAGES: @@ -48,7 +95,7 @@ def get_system_language() -> str: try: loc = locale.getlocale()[0] if loc: - # locale 格式可能是 'zh_CN' 或 'zh-CN' + # locale 格式可能是 'zh_CN', 'zh-CN', 'Chinese_China' 等 normalized = loc.replace("-", "_") if normalized in SUPPORTED_LANGUAGES: return normalized @@ -56,6 +103,9 @@ def get_system_language() -> str: lang_code = normalized.split("_")[0] if lang_code in SUPPORTED_LANGUAGES: return lang_code + # 处理 Windows 特殊格式(如 'Chinese_China' -> 'zh') + if normalized.startswith("Chinese"): + return "zh" except (ValueError, AttributeError): pass diff --git a/tests/unit/test_i18n.py b/tests/unit/test_i18n.py new file mode 100644 index 0000000..e799ef0 --- /dev/null +++ b/tests/unit/test_i18n.py @@ -0,0 +1,206 @@ +"""国际化 (i18n) 单元测试""" + +from unittest.mock import MagicMock, patch + +import pytest + +from remark.i18n import ( + SUPPORTED_LANGUAGES, + _get_windows_locale, + get_system_language, + gettext_function, + init_translation, + ngettext_function, + set_language, +) + + +@pytest.mark.unit +class TestGetWindowsLocale: + """Windows locale 获取测试""" + + @pytest.mark.parametrize( + "locale_name,expected", + [ + ("zh-CN", "zh-CN"), + ("en-US", "en-US"), + ("zh-TW", "zh-TW"), + ], + ) + def test_get_windows_locale_success(self, locale_name, expected): + """测试成功获取 Windows locale""" + mock_buffer = MagicMock() + mock_buffer.value = locale_name + mock_buffer.strip.return_value = locale_name + + with patch("ctypes.create_unicode_buffer", return_value=mock_buffer): + with patch("ctypes.windll.kernel32.GetUserDefaultLocaleName", return_value=len(locale_name)): + result = _get_windows_locale() + assert result == expected + + def test_get_windows_locale_api_fails(self): + """测试 Windows API 调用失败""" + with patch("ctypes.windll.kernel32.GetUserDefaultLocaleName", return_value=0): + result = _get_windows_locale() + assert result is None + + def test_get_windows_locale_exception(self): + """测试 Windows API 抛出异常""" + with patch("ctypes.windll.kernel32.GetUserDefaultLocaleName", side_effect=OSError): + result = _get_windows_locale() + assert result is None + + +@pytest.mark.unit +class TestGetSystemLanguage: + """系统语言获取测试""" + + @pytest.mark.parametrize( + "windows_locale,expected", + [ + ("zh-CN", "zh"), + ("en-US", "en"), + ("zh-TW", "zh"), + ], + ) + def test_windows_platform_priority(self, windows_locale, expected): + """测试 Windows 平台优先使用 Windows API""" + with patch("remark.i18n.platform.system", return_value="Windows"): + with patch("remark.i18n._get_windows_locale", return_value=windows_locale): + result = get_system_language() + assert result in SUPPORTED_LANGUAGES + assert result == expected + + @pytest.mark.parametrize( + "windows_locale", + ["ja-JP", "ko-KR", "fr-FR"], + ) + def test_windows_unsupported_locale_fallback(self, windows_locale): + """测试不支持的语言回退到默认""" + with patch("remark.i18n.platform.system", return_value="Windows"): + with patch("remark.i18n._get_windows_locale", return_value=windows_locale): + with patch.dict("os.environ", {}, clear=True): + with patch("remark.i18n.locale.getlocale", return_value=(None, None)): + result = get_system_language() + assert result == "en" + + def test_windows_api_null_fallback_to_locale(self): + """测试 Windows API 返回 None 时回退到 locale.getlocale()""" + with patch("remark.i18n.platform.system", return_value="Windows"): + with patch("remark.i18n._get_windows_locale", return_value=None): + with patch.dict("os.environ", {}, clear=True): + with patch("remark.i18n.locale.getlocale", return_value=("zh_CN", "cp1252")): + result = get_system_language() + assert result == "zh" + + @pytest.mark.parametrize( + "locale_value,expected", + [ + ("zh_CN", "zh"), + ("zh-CN", "zh"), + ("Chinese_China", "zh"), + ("en_US", "en"), + ("en-GB", "en"), + ("English_United States", "en"), + ], + ) + def test_locale_getlocale_variations(self, locale_value, expected): + """测试 locale.getlocale() 的各种返回值格式""" + with patch("remark.i18n.platform.system", return_value="Linux"): + with patch("remark.i18n._get_windows_locale", return_value=None): + with patch.dict("os.environ", {}, clear=True): + with patch("remark.i18n.locale.getlocale", return_value=(locale_value, "cp1252")): + result = get_system_language() + assert result == expected + + @pytest.mark.parametrize( + "env_lang,expected", + [ + ("zh_CN.UTF-8", "zh"), + ("en_US.UTF-8", "en"), + ("zh.UTF-8", "zh"), + ("en", "en"), + ], + ) + def test_lang_environment_variable(self, env_lang, expected): + """测试 LANG 环境变量""" + with patch("remark.i18n.platform.system", return_value="Linux"): + with patch("remark.i18n._get_windows_locale", return_value=None): + with patch.dict("os.environ", {"LANG": env_lang}): + with patch("remark.i18n.locale.getlocale", return_value=(None, None)): + result = get_system_language() + assert result == expected + + def test_all_methods_fallback_to_default(self): + """测试所有方法都失败时回退到默认语言""" + with patch("remark.i18n.platform.system", return_value="Linux"): + with patch("remark.i18n._get_windows_locale", return_value=None): + with patch.dict("os.environ", {}, clear=True): + with patch("remark.i18n.locale.getlocale", return_value=(None, None)): + result = get_system_language() + assert result == "en" + + +@pytest.mark.unit +class TestInitTranslation: + """翻译初始化测试""" + + @pytest.mark.parametrize( + "language,expected_domain", + [ + ("en", "messages"), + ("zh", "messages"), + ], + ) + def test_init_translation_supported_language(self, language, expected_domain): + """测试支持的语言初始化""" + translator = init_translation(language) + assert translator is not None + assert translator.gettext("test") == "test" + + def test_init_translation_unsupported_language_fallback(self): + """测试不支持的语言回退到英文""" + translator = init_translation("fr") + assert translator is not None + # 应该回退到 NullTranslations 或英文翻译 + assert translator.gettext("test") == "test" + + def test_init_translation_none_uses_system(self): + """测试 None 作为参数使用系统语言""" + with patch("remark.i18n.get_system_language", return_value="zh"): + translator = init_translation(None) + assert translator is not None + + +@pytest.mark.unit +class TestSetLanguage: + """设置语言测试""" + + def test_set_language_updates_translator(self): + """测试设置语言更新翻译器""" + set_language("zh") + # 验证语言已被设置(通过调用 gettext_function) + result = gettext_function("Windows Folder Remark Tool") + # 如果成功加载中文翻译,应该返回中文字符串 + # 否则返回原字符串 + assert isinstance(result, str) + + +@pytest.mark.unit +class TestGetTextFunction: + """翻译函数测试""" + + def test_gettext_function_returns_string(self): + """测试 gettext_function 返回字符串""" + result = gettext_function("test message") + assert isinstance(result, str) + + def test_ngettext_function_singular(self): + """测试 ngettext 单数形式""" + result = ngettext_function("one item", "many items", 1) + assert isinstance(result, str) + + def test_ngettext_function_plural(self): + """测试 ngettext 复数形式""" + result = ngettext_function("one item", "many items", 10) + assert isinstance(result, str) From e75f3dcc5f803d576defff26d397ca3c89eb4f7c Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 00:33:10 +0800 Subject: [PATCH 04/27] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=89=93?= =?UTF-8?q?=E5=8C=85=E5=90=8E=E8=AF=AD=E8=A8=80=E5=88=87=E6=8D=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=92=8C=E6=8E=A7=E5=88=B6=E5=8F=B0=E8=BE=93=E5=87=BA?= =?UTF-8?q?=E4=B9=B1=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要更改: - remark.spec: 添加 locale_datas 收集 .mo 文件到打包 - remark/i18n.py: 修复 _get_locale_dir() 路径解析,打包后使用 sys._MEIPASS/locale - remark/cli/commands.py: 在 main() 中强制设置控制台编码为 UTF-8 --- remark.spec | 11 ++++++++++- remark/cli/commands.py | 7 +++++++ remark/i18n.py | 30 ++++++++++++++++++++++++------ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/remark.spec b/remark.spec index 8bff250..9c946a7 100644 --- a/remark.spec +++ b/remark.spec @@ -82,11 +82,20 @@ hiddenimports = collect_submodules('remark') + [ 'packaging.version', ] +# Collect locale translation files (.mo) +# PyInstaller 会将这些文件复制到 exe 的临时目录 +locale_datas = [] +for lang in ['zh', 'en']: + lang_dir = os.path.join('locale', lang, 'LC_MESSAGES') + mo_file = os.path.join(lang_dir, 'messages.mo') + if os.path.exists(mo_file): + locale_datas.append((mo_file, lang_dir)) + a = Analysis( [os.path.join("remark", "cli", "commands.py")], pathex=[], binaries=[], - datas=[], + datas=locale_datas, hiddenimports=hiddenimports, hookspath=[], hooksconfig={}, diff --git a/remark/cli/commands.py b/remark/cli/commands.py index 45f6942..759c6fb 100644 --- a/remark/cli/commands.py +++ b/remark/cli/commands.py @@ -478,6 +478,13 @@ def run(self, argv=None) -> None: def main() -> None: """主入口""" + # 强制设置控制台编码为 UTF-8,支持中文等特殊字符输出 + # 这对于 Windows 系统特别重要,因为默认控制台编码可能是 GBK + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="replace") + cli = CLI() try: cli.run() diff --git a/remark/i18n.py b/remark/i18n.py index bffe71b..f44480b 100644 --- a/remark/i18n.py +++ b/remark/i18n.py @@ -10,15 +10,10 @@ import locale import os import platform +import sys from pathlib import Path from typing import Final -# 项目根目录 -PROJECT_ROOT: Final = Path(__file__).parent.parent - -# 翻译文件目录 -LOCALE_DIR: Final = PROJECT_ROOT / "locale" - # 翻译域 DOMAIN: Final = "messages" @@ -26,6 +21,29 @@ SUPPORTED_LANGUAGES: Final = ("en", "zh") +def _get_locale_dir() -> Path: + """ + 获取翻译文件目录路径. + + 支持 PyInstaller 打包环境: + - 打包后:sys._MEIPASS/locale(文件解压到临时目录的 locale 子目录) + - 开发环境:使用项目根目录下的 locale 目录 + + Returns: + locale 目录的路径 + """ + # PyInstaller 打包后的临时目录,文件被解压到 _MEIPASS/locale/ + if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + return Path(sys._MEIPASS) / "locale" + + # 开发环境:使用项目根目录 + return Path(__file__).parent.parent / "locale" + + +# 翻译文件目录(运行时计算) +LOCALE_DIR: Final = _get_locale_dir() + + def _get_windows_locale() -> str | None: """ 在 Windows 上使用 Windows API 获取用户默认区域设置名称. From 2b7d2c2d66107c940a3e93d19f65369630815c0f Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 00:43:04 +0800 Subject: [PATCH 05/27] docs: update README with tool advantages and fix filename case --- readme.md => README.md | 7 +++++++ README.zh.md | 7 +++++++ 2 files changed, 14 insertions(+) rename readme.md => README.md (91%) diff --git a/readme.md b/README.md similarity index 91% rename from readme.md rename to README.md index 3f10c28..2546af7 100644 --- a/readme.md +++ b/README.md @@ -8,12 +8,19 @@ A command-line tool that adds remarks/comments to Windows folders by modifying t ## Features - Multi-language character support (UTF-16 encoding) +- Multi-language interface support (English, Chinese) - Command-line and interactive modes - Automatic encoding detection and repair - Automatic update checking to stay current - Right-click menu integration for quick access - Single-file exe packaging, no Python environment required +## Why This Tool + +- **No Background Processes**: Runs when needed, exits when done — zero background footprint +- **Lightweight**: Only ~12 MB, minimal system resource usage +- **Privacy-First**: Completely local operation, no data sent to any server + ## Installation ### Method 1: Using exe file (Recommended) diff --git a/README.zh.md b/README.zh.md index 5ede7d4..09c6a80 100644 --- a/README.zh.md +++ b/README.zh.md @@ -8,12 +8,19 @@ ## 特性 - 支持中文等多语言字符(UTF-16 编码) +- 支持中英文界面切换 - 命令行模式和交互模式 - 自动编码检测和修复 - 自动更新检查,保持最新版本 - 右键菜单集成,快速访问 - 单文件 exe 打包,无需 Python 环境 +## 工具优势 + +- **用完即走**:需要时运行,用完即退出,无后台进程占用 +- **轻量小巧**:仅约 12 MB,不占用系统资源 +- **隐私优先**:完全本地运行,数据不传输至任何服务器 + ## 安装 ### 方式一:使用 exe 文件(推荐) From 5cda23b6f130ebd5e3010a429115c5bd9fb3e1b1 Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 01:27:06 +0800 Subject: [PATCH 06/27] chore: bump version to 2.0.6 and enhance SEO - Bump version from 2.0.5 to 2.0.6 - Add 'comment' keyword to pyproject.toml for SEO - Update README with dual-language H1 title - Add badges (Python version, License) - Add Star Us section - Enhance description with remark/comment keywords - Update GitHub About description via gh CLI - Add 12 topics: windows, folder, remark, comment, desktop, command-line, utility, python, portable, cli, automation, tool --- README.md | 24 +++++++++++++++--------- README.zh.md | 28 +++++++++++++++++----------- pyproject.toml | 4 ++-- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 2546af7..a9d225c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,22 @@ -# Windows Folder Remark Tool - -A command-line tool that adds remarks/comments to Windows folders by modifying the `Desktop.ini` file. +# Windows Folder Remark/Comment Tool - Windows 文件夹备注工具 +[![PyPI](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) +[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![en](https://img.shields.io/badge/lang-en-blue.svg)](README.md) [![zh](https://img.shields.io/badge/lang-zh-red.svg)](README.zh.md) +A lightweight CLI tool to add remarks/comments to Windows folders via Desktop.ini. No background processes, privacy-first, ~12MB portable exe. Perfect for organizing your files with custom descriptions. + +## ⭐ Star Us + +If you find this tool helpful, please consider giving it a star on GitHub! + +## Why This Tool + +- **No Background Processes**: Runs when needed, exits when done — zero background footprint +- **Lightweight**: Only ~12 MB, minimal system resource usage +- **Privacy-First**: Completely local operation, no data sent to any server + ## Features - Multi-language character support (UTF-16 encoding) @@ -15,12 +27,6 @@ A command-line tool that adds remarks/comments to Windows folders by modifying t - Right-click menu integration for quick access - Single-file exe packaging, no Python environment required -## Why This Tool - -- **No Background Processes**: Runs when needed, exits when done — zero background footprint -- **Lightweight**: Only ~12 MB, minimal system resource usage -- **Privacy-First**: Completely local operation, no data sent to any server - ## Installation ### Method 1: Using exe file (Recommended) diff --git a/README.zh.md b/README.zh.md index 09c6a80..10ca027 100644 --- a/README.zh.md +++ b/README.zh.md @@ -1,10 +1,22 @@ -# Windows 文件夹备注工具 - -一个通过修改 `Desktop.ini` 文件为 Windows 文件夹添加备注/注释的命令行工具。 +# Windows Folder Remark/Comment Tool - Windows 文件夹备注工具 +[![PyPI](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) +[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) [![zh](https://img.shields.io/badge/lang-zh-blue.svg)](README.zh.md) +一个轻量级的命令行工具,通过 Desktop.ini 为 Windows 文件夹添加备注/评论。无后台进程,隐私优先,约 12MB 便携版 exe。适合为文件添加自定义描述来更好地组织文件。 + +## ⭐ 支持 + +如果这个工具对你有帮助,请在 GitHub 上给个 Star! + +## 工具优势 + +- **用完即走**:需要时运行,用完即退出,无后台进程占用 +- **轻量小巧**:仅约 12 MB,不占用系统资源 +- **隐私优先**:完全本地运行,数据不传输至任何服务器 + ## 特性 - 支持中文等多语言字符(UTF-16 编码) @@ -15,12 +27,6 @@ - 右键菜单集成,快速访问 - 单文件 exe 打包,无需 Python 环境 -## 工具优势 - -- **用完即走**:需要时运行,用完即退出,无后台进程占用 -- **轻量小巧**:仅约 12 MB,不占用系统资源 -- **隐私优先**:完全本地运行,数据不传输至任何服务器 - ## 安装 ### 方式一:使用 exe 文件(推荐) @@ -81,8 +87,8 @@ windows-folder-remark.exe windows-folder-remark.exe --install ``` -- **Windows 10**: 右键文件夹可直接看到「添加文件夹备注」 -- **Windows 11**: 右键文件夹 → 点击「显示更多选项」→ 添加文件夹备注 +- **Windows 10**:右键文件夹可直接看到「添加文件夹备注」 +- **Windows 11**:右键文件夹 → 点击「显示更多选项」→ 添加文件夹备注 ### 自动更新 diff --git a/pyproject.toml b/pyproject.toml index 8264ebe..7c2aa5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [project] name = "windows-folder-remark" -version = "2.0.5" +version = "2.0.6" description = "Windows 文件夹备注工具" readme = "README.md" requires-python = ">=3.11" @@ -11,7 +11,7 @@ license = {text = "MIT"} authors = [ {name = "Piratf"} ] -keywords = ["windows", "folder", "remark", "desktop.ini"] +keywords = ["windows", "folder", "remark", "comment", "desktop.ini"] classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", From 129325ed27cc15c7c1efb0fcc18a9f2e524eae49 Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 02:26:02 +0800 Subject: [PATCH 07/27] feat: add VitePress bilingual documentation site - Add VitePress configuration with English and Chinese locales - Create documentation structure: /en/ and /zh/ - Add getting started, usage, and API reference pages - Add documentation links to README.md - Update .gitignore to exclude node_modules --- .gitignore | 3 + README.md | 134 +- docs/.vitepress/config.mjs | 51 + docs/en/guide/api.md | 21 + docs/en/guide/getting-started.md | 33 + docs/en/guide/usage.md | 40 + docs/en/index.md | 33 + docs/zh/guide/api.md | 21 + docs/zh/guide/getting-started.md | 33 + docs/zh/guide/usage.md | 40 + docs/zh/index.md | 33 + package-lock.json | 2513 ++++++++++++++++++++++++++++++ package.json | 14 + 13 files changed, 2903 insertions(+), 66 deletions(-) create mode 100644 docs/.vitepress/config.mjs create mode 100644 docs/en/guide/api.md create mode 100644 docs/en/guide/getting-started.md create mode 100644 docs/en/guide/usage.md create mode 100644 docs/en/index.md create mode 100644 docs/zh/guide/api.md create mode 100644 docs/zh/guide/getting-started.md create mode 100644 docs/zh/guide/usage.md create mode 100644 docs/zh/index.md create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore index 58fe1c2..b9a79c0 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ locale/*/LC_MESSAGES/*.mo # bv (beads viewer) local config and caches .bv/ + +# Node.js +node_modules/ diff --git a/README.md b/README.md index a9d225c..bbf8577 100644 --- a/README.md +++ b/README.md @@ -5,151 +5,153 @@ [![en](https://img.shields.io/badge/lang-en-blue.svg)](README.md) [![zh](https://img.shields.io/badge/lang-zh-red.svg)](README.zh.md) -A lightweight CLI tool to add remarks/comments to Windows folders via Desktop.ini. No background processes, privacy-first, ~12MB portable exe. Perfect for organizing your files with custom descriptions. +A lightweight CLI tool to add remarks/comments to Windows folders via Desktop.ini. No background processes, privacy-first, ~12MB portable exe. / 一个轻量级的命令行工具,通过 Desktop.ini 为 Windows 文件夹添加备注/评论。无后台进程,隐私优先,约 12MB 便携版 exe。 -## ⭐ Star Us +**Documentation**: [Full Documentation](https://piratf.github.io/windows-folder-remark/en/) | [完整文档](https://piratf.github.io/windows-folder-remark/zh/) -If you find this tool helpful, please consider giving it a star on GitHub! +## ⭐ 支持 -## Why This Tool +如果这个工具对你有帮助,请在 GitHub 上给个 Star! -- **No Background Processes**: Runs when needed, exits when done — zero background footprint -- **Lightweight**: Only ~12 MB, minimal system resource usage -- **Privacy-First**: Completely local operation, no data sent to any server +## 工具优势 -## Features +- **用完即走**:需要时运行,用完即退出,无后台进程占用 +- **轻量小巧**:仅约 12 MB,不占用系统资源 +- **隐私优先**:完全本地运行,数据不传输至任何服务器 -- Multi-language character support (UTF-16 encoding) -- Multi-language interface support (English, Chinese) -- Command-line and interactive modes -- Automatic encoding detection and repair -- Automatic update checking to stay current -- Right-click menu integration for quick access -- Single-file exe packaging, no Python environment required +## 特性 -## Installation +- 支持中文等多语言字符(UTF-16 编码) +- 支持中英文界面切换 +- 命令行模式和交互模式 +- 自动编码检测和修复 +- 自动更新检查,保持最新版本 +- 右键菜单集成,快速访问 +- 单文件 exe 打包,无需 Python 环境 -### Method 1: Using exe file (Recommended) +## 安装 -Download `windows-folder-remark.exe` from [releases](https://github.com/piratf/windows-folder-remark/releases) and use directly. +### 方式一:使用 exe 文件(推荐) -### Method 2: Install from source +下载 [releases](https://github.com/piratf/windows-folder-remark/releases) 中的 `windows-folder-remark.exe`,直接使用。 + +### 方式二:从源码安装 ```bash -# Clone repository +# 克隆仓库 git clone https://github.com/piratf/windows-folder-remark.git cd windows-folder-remark -# Install dependencies (no external dependencies) +# 安装依赖(无外部依赖) pip install -e . -# Run +# 运行 python -m remark.cli --help ``` -## Usage +## 使用方法 -### Command-line Mode +### 命令行模式 ```bash -# Add remark -windows-folder-remark.exe "C:\MyFolder" "This is my folder" +# 添加备注 +windows-folder-remark.exe "C:\MyFolder" "这是我的文件夹" -# View remark +# 查看备注 windows-folder-remark.exe --view "C:\MyFolder" -# Delete remark +# 删除备注 windows-folder-remark.exe --delete "C:\MyFolder" -# Check updates +# 检查更新 windows-folder-remark.exe --update -# Install right-click menu +# 安装右键菜单 windows-folder-remark.exe --install -# Uninstall right-click menu +# 卸载右键菜单 windows-folder-remark.exe --uninstall ``` -### Interactive Mode +### 交互模式 ```bash -# Follow prompts after running +# 运行后根据提示操作 windows-folder-remark.exe ``` -### Right-click Menu (Recommended) +### 右键菜单(推荐) -After installing the right-click menu, you can add remarks directly in File Explorer by right-clicking folders: +安装右键菜单后,可以直接在文件资源管理器中右键文件夹添加备注: ```bash -# Install right-click menu +# 安装右键菜单 windows-folder-remark.exe --install ``` -- **Windows 10**: Right-click folder to see "Add Folder Remark" -- **Windows 11**: Right-click folder → Click "Show more options" → Add Folder Remark +- **Windows 10**:右键文件夹可直接看到「添加文件夹备注」 +- **Windows 11**:右键文件夹 → 点击「显示更多选项」→ 添加文件夹备注 -### Auto Update +### 自动更新 -The program automatically checks for updates on exit (once every 24 hours) and prompts if a new version is available. +程序会在退出时自动检查更新(每 24 小时一次),如有新版本会提示是否立即更新。 -You can also manually check for updates: +也可以手动检查更新: ```bash windows-folder-remark.exe --update ``` -## Encoding Detection +## 编码检测 -When viewing remarks with `--view`, if the `desktop.ini` file is not in standard UTF-16 encoding, the tool will prompt you: +当使用 `--view` 查看备注时,如果检测到 `desktop.ini` 文件不是标准的 UTF-16 编码,工具会提醒你: ``` -Warning: desktop.ini file encoding is utf-8, not standard UTF-16. -This may cause Chinese and other special characters to display abnormally. -Fix encoding to UTF-16? [Y/n]: +警告: desktop.ini 文件编码为 utf-8,不是标准的 UTF-16。 +这可能导致中文等特殊字符显示异常。 +是否修复编码为 UTF-16?[Y/n]: ``` -Select `Y` to automatically fix the encoding. +选择 `Y` 可自动修复编码。 -## Development +## 开发 ```bash -# Install development dependencies +# 安装开发依赖 pip install -e ".[dev]" -# Run tests +# 运行测试 pytest -# Code check +# 代码检查 ruff check . ruff format . -# Type check +# 类型检查 mypy remark/ -# Build exe locally +# 本地打包 exe python -m scripts.build ``` -## How It Works +## 原理说明 -This tool implements folder remarks through these steps: +该工具通过以下步骤实现文件夹备注: -1. Create/modify `Desktop.ini` file in the folder -2. Write `[.ShellClassInfo]` section and `InfoTip` property -3. Save file with UTF-16 encoding -4. Set `Desktop.ini` as hidden and system attributes -5. Set folder as read-only (makes Windows read `Desktop.ini`) +1. 在文件夹中创建/修改 `Desktop.ini` 文件 +2. 写入 `[.ShellClassInfo]` 段落和 `InfoTip` 属性 +3. 使用 UTF-16 编码保存文件 +4. 将 `Desktop.ini` 设置为隐藏和系统属性 +5. 将文件夹设置为只读属性(使 Windows 读取 `Desktop.ini`) -Reference: [Microsoft Official Documentation](https://learn.microsoft.com/en-us/windows/win32/shell/how-to-customize-folders-with-desktop-ini) +参考:[Microsoft 官方文档](https://learn.microsoft.com/en-us/windows/win32/shell/how-to-customize-folders-with-desktop-ini) -## Notes +## 注意事项 -- May take a few minutes to display in File Explorer after modification -- Some file managers may not support folder remarks -- The tool modifies system attributes of folders +- 修改后可能需要几分钟才能在资源管理器中显示 +- 某些文件管理器可能不支持显示文件夹备注 +- 工具会修改文件夹的系统属性 -## License +## 许可证 MIT License diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs new file mode 100644 index 0000000..c953748 --- /dev/null +++ b/docs/.vitepress/config.mjs @@ -0,0 +1,51 @@ +import { defineConfig } from 'vitepress' + +export default defineConfig({ + title: 'Windows Folder Remark Tool', + description: 'A lightweight CLI tool to add remarks/comments to Windows folders via Desktop.ini', + lang: 'en-US', + + locales: { + root: { + label: 'English', + lang: 'en-US' + }, + zh: { + label: '简体中文', + lang: 'zh-CN', + link: '/zh/' + } + }, + + themeConfig: { + nav: root => [ + { text: 'Guide', link: root === 'zh' ? '/zh/guide/' : '/en/guide/' }, + { text: root === 'zh' ? 'English' : '中文', link: root === 'zh' ? '/en/' : '/zh/' } + ], + + sidebar: { + '/en/': [ + { + text: 'Guide', + items: [ + { text: 'Introduction', link: '/en/' }, + { text: 'Getting Started', link: '/en/guide/getting-started' }, + { text: 'Usage', link: '/en/guide/usage' }, + { text: 'API Reference', link: '/en/guide/api' } + ] + } + ], + '/zh/': [ + { + text: '指南', + items: [ + { text: '介绍', link: '/zh/' }, + { text: '快速开始', link: '/zh/guide/getting-started' }, + { text: '使用方法', link: '/zh/guide/usage' }, + { text: 'API 参考', link: '/zh/guide/api' } + ] + } + ] + } + } +}) diff --git a/docs/en/guide/api.md b/docs/en/guide/api.md new file mode 100644 index 0000000..337445c --- /dev/null +++ b/docs/en/guide/api.md @@ -0,0 +1,21 @@ +# API Reference + +## Command-line Arguments + +| Argument | Short | Description | +|---|---|---| +| `--help` | `-h` | Show help information | +| `--install` | | Install right-click menu | +| `--uninstall` | | Uninstall right-click menu | +| `--update` | | Check for updates | +| `--view ` | | View folder remark | +| `--delete ` | | Delete folder remark | +| `--gui ` | | GUI mode | +| `--lang ` | `-L` | Set language (en, zh) | + +## Exit Codes + +| Code | Description | +|---|---| +| 0 | Success | +| 1 | Error | diff --git a/docs/en/guide/getting-started.md b/docs/en/guide/getting-started.md new file mode 100644 index 0000000..a2d9315 --- /dev/null +++ b/docs/en/guide/getting-started.md @@ -0,0 +1,33 @@ +# Getting Started + +## Download + +Download `windows-folder-remark.exe` from [GitHub Releases](https://github.com/piratf/windows-folder-remark/releases). + +## Basic Usage + +### Add Remark + +```bash +windows-folder-remark.exe "C:\MyFolder" "This is my folder" +``` + +### View Remark + +```bash +windows-folder-remark.exe --view "C:\MyFolder" +``` + +### Delete Remark + +```bash +windows-folder-remark.exe --delete "C:\MyFolder" +``` + +## Install Right-click Menu + +```bash +windows-folder-remark.exe --install +``` + +After installation, you can add remarks directly in File Explorer by right-clicking folders. diff --git a/docs/en/guide/usage.md b/docs/en/guide/usage.md new file mode 100644 index 0000000..50a7883 --- /dev/null +++ b/docs/en/guide/usage.md @@ -0,0 +1,40 @@ +# Usage + +## Command-line Mode + +```bash +# Add remark +windows-folder-remark.exe "C:\MyFolder" "My Folder" + +# View remark +windows-folder-remark.exe --view "C:\MyFolder" + +# Delete remark +windows-folder-remark.exe --delete "C:\MyFolder" +``` + +## Interactive Mode + +```bash +windows-folder-remark.exe +``` + +## Language Switch + +```bash +# Use English +windows-folder-remark.exe --lang en + +# Use Chinese +windows-folder-remark.exe --lang zh +``` + +## Auto Update + +The program automatically checks for updates on exit (once every 24 hours). + +Manual update check: + +```bash +windows-folder-remark.exe --update +``` diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 0000000..e8de906 --- /dev/null +++ b/docs/en/index.md @@ -0,0 +1,33 @@ +--- +layout: home + +hero: + name: Windows Folder Remark Tool + text: A Lightweight CLI Tool for Windows Folder Remarks + tagline: Add remarks/comments to Windows folders via Desktop.ini + actions: + - theme: brand + text: Get Started + link: /en/guide/getting-started + - theme: alt + text: GitHub + link: https://github.com/piratf/windows-folder-remark + +features: + - title: No Background Processes + details: Runs when needed, exits when done — zero background footprint + - title: Lightweight + details: Only ~12 MB, minimal system resource usage + - title: Privacy-First + details: Completely local operation, no data sent to any server + - title: Multi-language Support + details: English and Chinese interface with auto language detection + - title: UTF-16 Encoding + details: Full support for Chinese and other special characters + - title: Portable exe + details: Single-file packaging, no Python environment required +--- + +## ⭐ Star Us + +If you find this tool helpful, please give it a star on [GitHub](https://github.com/piratf/windows-folder-remark)! diff --git a/docs/zh/guide/api.md b/docs/zh/guide/api.md new file mode 100644 index 0000000..e5c7ced --- /dev/null +++ b/docs/zh/guide/api.md @@ -0,0 +1,21 @@ +# API 参考 + +## 命令行参数 + +| 参数 | 简写 | 说明 | +|---|---|---| +| `--help` | `-h` | 显示帮助信息 | +| `--install` | | 安装右键菜单 | +| `--uninstall` | | 卸载右键菜单 | +| `--update` | | 检查更新 | +| `--view ` | | 查看文件夹备注 | +| `--delete ` | | 删除文件夹备注 | +| `--gui ` | | GUI 模式 | +| `--lang ` | `-L` | 设置语言 (en, zh) | + +## 退出码 + +| 代码 | 说明 | +|---|---| +| 0 | 成功 | +| 1 | 错误 | diff --git a/docs/zh/guide/getting-started.md b/docs/zh/guide/getting-started.md new file mode 100644 index 0000000..415329a --- /dev/null +++ b/docs/zh/guide/getting-started.md @@ -0,0 +1,33 @@ +# 快速开始 + +## 下载 + +从 [GitHub Releases](https://github.com/piratf/windows-folder-remark/releases) 下载 `windows-folder-remark.exe`。 + +## 基本使用 + +### 添加备注 + +```bash +windows-folder-remark.exe "C:\MyFolder" "这是我的文件夹" +``` + +### 查看备注 + +```bash +windows-folder-remark.exe --view "C:\MyFolder" +``` + +### 删除备注 + +```bash +windows-folder-remark.exe --delete "C:\MyFolder" +``` + +## 安装右键菜单 + +```bash +windows-folder-remark.exe --install +``` + +安装后可以在文件资源管理器中右键文件夹直接添加备注。 diff --git a/docs/zh/guide/usage.md b/docs/zh/guide/usage.md new file mode 100644 index 0000000..da64470 --- /dev/null +++ b/docs/zh/guide/usage.md @@ -0,0 +1,40 @@ +# 使用方法 + +## 命令行模式 + +```bash +# 添加备注 +windows-folder-remark.exe "C:\MyFolder" "我的文件夹" + +# 查看备注 +windows-folder-remark.exe --view "C:\MyFolder" + +# 删除备注 +windows-folder-remark.exe --delete "C:\MyFolder" +``` + +## 交互模式 + +```bash +windows-folder-remark.exe +``` + +## 语言切换 + +```bash +# 使用中文 +windows-folder-remark.exe --lang zh + +# 使用英文 +windows-folder-remark.exe --lang en +``` + +## 自动更新 + +程序会在退出时自动检查更新(每 24 小时一次)。 + +手动检查更新: + +```bash +windows-folder-remark.exe --update +``` diff --git a/docs/zh/index.md b/docs/zh/index.md new file mode 100644 index 0000000..b4a2d04 --- /dev/null +++ b/docs/zh/index.md @@ -0,0 +1,33 @@ +--- +layout: home + +hero: + name: Windows Folder Remark Tool + text: 轻量级 Windows 文件夹备注工具 + tagline: 通过 Desktop.ini 为 Windows 文件夹添加备注/评论 + actions: + - theme: brand + text: 快速开始 + link: /zh/guide/getting-started + - theme: alt + text: GitHub + link: https://github.com/piratf/windows-folder-remark + +features: + - title: 用完即走 + details: 需要时运行,用完即退出,无后台进程占用 + - title: 轻量小巧 + details: 仅约 12 MB,不占用系统资源 + - title: 隐私优先 + details: 完全本地运行,数据不传输至任何服务器 + - title: 多语言支持 + details: 支持中英文界面,自动检测系统语言 + - title: UTF-16 编码 + details: 支持中文等多语言字符 + - title: 便携版 exe + details: 单文件打包,无需 Python 环境 +--- + +## ⭐ 支持 + +如果这个工具对你有帮助,请在 [GitHub](https://github.com/piratf/windows-folder-remark) 上给个 Star! diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f51189c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2513 @@ +{ + "name": "windows-folder-remark-docs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "windows-folder-remark-docs", + "version": "1.0.0", + "devDependencies": { + "vitepress": "^1.0.0" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.13.0.tgz", + "integrity": "sha512-Zrqam12iorp3FjiKMXSTpedGYznZ3hTEOAr2oCxI8tbF8bS1kQHClyDYNq/eV0ewMNLyFkgZVWjaS+8spsOYiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.47.0.tgz", + "integrity": "sha512-aOpsdlgS9xTEvz47+nXmw8m0NtUiQbvGWNuSEb7fA46iPL5FxOmOUZkh8PREBJpZ0/H8fclSc7BMJCVr+Dn72w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.47.0.tgz", + "integrity": "sha512-EcF4w7IvIk1sowrO7Pdy4Ako7x/S8+nuCgdk6En+u5jsaNQM4rTT09zjBPA+WQphXkA2mLrsMwge96rf6i7Mow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.47.0.tgz", + "integrity": "sha512-Wzg5Me2FqgRDj0lFuPWFK05UOWccSMsIBL2YqmTmaOzxVlLZ+oUqvKbsUSOE5ud8Fo1JU7JyiLmEXBtgDKzTwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.47.0.tgz", + "integrity": "sha512-Ci+cn/FDIsDxSKMRBEiyKrqybblbk8xugo6ujDN1GSTv9RIZxwxqZYuHfdLnLEwLlX7GB8pqVyqrUSlRnR+sJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.47.0.tgz", + "integrity": "sha512-gsLnHPZmWcX0T3IigkDL2imCNtsQ7dR5xfnwiFsb+uTHCuYQt+IwSNjsd8tok6HLGLzZrliSaXtB5mfGBtYZvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.47.0.tgz", + "integrity": "sha512-PDOw0s8WSlR2fWFjPQldEpmm/gAoUgLigvC3k/jCSi/DzigdGX6RdC0Gh1RR1P8Cbk5KOWYDuL3TNzdYwkfDyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.47.0.tgz", + "integrity": "sha512-b5hlU69CuhnS2Rqgsz7uSW0t4VqrLMLTPbUpEl0QVz56rsSwr1Sugyogrjb493sWDA+XU1FU5m9eB8uH7MoI0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.47.0.tgz", + "integrity": "sha512-WvwwXp5+LqIGISK3zHRApLT1xkuEk320/EGeD7uYy+K8WwDd5OjXnhjuXRhYr1685KnkvWkq1rQ/ihCJjOfHpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.47.0.tgz", + "integrity": "sha512-j2EUFKAlzM0TE4GRfkDE3IDfkVeJdcbBANWzK16Tb3RHz87WuDfQ9oeEW6XiRE1/bEkq2xf4MvZesvSeQrZRDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.47.0.tgz", + "integrity": "sha512-+kTSE4aQ1ARj2feXyN+DMq0CIDHJwZw1kpxIunedkmpWUg8k3TzFwWsMCzJVkF2nu1UcFbl7xsIURz3Q3XwOXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.47.0.tgz", + "integrity": "sha512-Ja+zPoeSA2SDowPwCNRbm5Q2mzDvVV8oqxCQ4m6SNmbKmPlCfe30zPfrt9ho3kBHnsg37pGucwOedRIOIklCHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.47.0.tgz", + "integrity": "sha512-N6nOvLbaR4Ge+oVm7T4W/ea1PqcSbsHR4O58FJ31XtZjFPtOyxmnhgCmGCzP9hsJI6+x0yxJjkW5BMK/XI8OvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.47.0.tgz", + "integrity": "sha512-z1oyLq5/UVkohVXNDEY70mJbT/sv/t6HYtCvCwNrOri6pxBJDomP9R83KOlwcat+xqBQEdJHjbrPh36f1avmZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", + "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", + "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/react": "3.8.2", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", + "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.2", + "algoliasearch": "^5.14.2" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.68", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.68.tgz", + "integrity": "sha512-bQPl1zuZlX6AnofreA1v7J+hoPncrFMppqGboe/SH54jZO37meiBUGBqNOxEpc0HKfZGxJaVVJwZd4gdMYu3hw==", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", + "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", + "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", + "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", + "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", + "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", + "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", + "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.27", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", + "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", + "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.27", + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", + "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", + "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", + "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", + "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/runtime-core": "3.5.27", + "@vue/shared": "3.5.27", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", + "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "vue": "3.5.27" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", + "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/algoliasearch": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.47.0.tgz", + "integrity": "sha512-AGtz2U7zOV4DlsuYV84tLp2tBbA7RPtLA44jbVH4TTpDcc1dIWmULjHSsunlhscbzDydnjuFlNhflR3nV4VJaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.13.0", + "@algolia/client-abtesting": "5.47.0", + "@algolia/client-analytics": "5.47.0", + "@algolia/client-common": "5.47.0", + "@algolia/client-insights": "5.47.0", + "@algolia/client-personalization": "5.47.0", + "@algolia/client-query-suggestions": "5.47.0", + "@algolia/client-search": "5.47.0", + "@algolia/ingestion": "1.47.0", + "@algolia/monitoring": "1.47.0", + "@algolia/recommend": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-to-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", + "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.28.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", + "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/shiki": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", + "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/langs": "2.5.0", + "@shikijs/themes": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", + "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-sfc": "3.5.27", + "@vue/runtime-dom": "3.5.27", + "@vue/server-renderer": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5b1dc3e --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "windows-folder-remark-docs", + "version": "1.0.0", + "type": "module", + "description": "Documentation for Windows Folder Remark Tool", + "scripts": { + "docs:dev": "vitepress dev docs", + "docs:build": "vitepress build docs", + "docs:preview": "vitepress preview docs" + }, + "devDependencies": { + "vitepress": "^1.0.0" + } +} From f565791b546447cd7225d4836ec6c5ec22ee1c28 Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 02:35:28 +0800 Subject: [PATCH 08/27] refactor: restructure README files and add GitHub Pages deployment - Rename README.zh.md to README.en.md (English version) - Update README.md to be the primary Chinese version with English summary - Update language switch badges to point to README.md and README.en.md - Add GitHub Actions workflow to deploy VitePress site to GitHub Pages --- .github/workflows/deploy-docs.yml | 49 ++++++++++ README.en.md | 155 ++++++++++++++++++++++++++++++ README.md | 4 +- README.zh.md | 155 ------------------------------ 4 files changed, 206 insertions(+), 157 deletions(-) create mode 100644 .github/workflows/deploy-docs.yml create mode 100644 README.en.md delete mode 100644 README.zh.md diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..e0e1c85 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,49 @@ +name: Deploy VitePress site to Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: docs/package-lock.json + + - name: Install dependencies + run: npm ci + working-directory: ./docs + + - name: Build VitePress site + run: npm run docs:build + working-directory: ./docs + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..85d98cd --- /dev/null +++ b/README.en.md @@ -0,0 +1,155 @@ +# Windows Folder Remark/Comment Tool + +[![PyPI](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) +[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) +[![en](https://img.shields.io/badge/lang-en-blue.svg)](README.en.md) +[![zh](https://img.shields.io/badge/lang-zh-red.svg)](README.md) + +A lightweight CLI tool to add remarks/comments to Windows folders via Desktop.ini. No background processes, privacy-first, ~12MB portable exe. Perfect for organizing your files with custom descriptions. + +## ⭐ Star Us + +If you find this tool helpful, please consider giving it a star on GitHub! + +## Why This Tool + +- **No Background Processes**: Runs when needed, exits when done — zero background footprint +- **Lightweight**: Only ~12 MB, minimal system resource usage +- **Privacy-First**: Completely local operation, no data sent to any server + +## Features + +- Multi-language character support (UTF-16 encoding) +- Multi-language interface support (English, Chinese) +- Command-line and interactive modes +- Automatic encoding detection and repair +- Automatic update checking to stay current +- Right-click menu integration for quick access +- Single-file exe packaging, no Python environment required + +## Installation + +### Method 1: Using exe file (Recommended) + +Download `windows-folder-remark.exe` from [releases](https://github.com/piratf/windows-folder-remark/releases) and use directly. + +### Method 2: Install from source + +```bash +# Clone repository +git clone https://github.com/piratf/windows-folder-remark.git +cd windows-folder-remark + +# Install dependencies (no external dependencies) +pip install -e . + +# Run +python -m remark.cli --help +``` + +## Usage + +### Command-line Mode + +```bash +# Add remark +windows-folder-remark.exe "C:\MyFolder" "This is my folder" + +# View remark +windows-folder-remark.exe --view "C:\MyFolder" + +# Delete remark +windows-folder-remark.exe --delete "C:\MyFolder" + +# Check updates +windows-folder-remark.exe --update + +# Install right-click menu +windows-folder-remark.exe --install + +# Uninstall right-click menu +windows-folder-remark.exe --uninstall +``` + +### Interactive Mode + +```bash +# Follow prompts after running +windows-folder-remark.exe +``` + +### Right-click Menu (Recommended) + +After installing the right-click menu, you can add remarks directly in File Explorer by right-clicking folders: + +```bash +# Install right-click menu +windows-folder-remark.exe --install +``` + +- **Windows 10**: Right-click folder to see "Add Folder Remark" +- **Windows 11**: Right-click folder → Click "Show more options" → Add Folder Remark + +### Auto Update + +The program automatically checks for updates on exit (once every 24 hours) and prompts if a new version is available. + +You can also manually check for updates: + +```bash +windows-folder-remark.exe --update +``` + +## Encoding Detection + +When viewing remarks with `--view`, if the `desktop.ini` file is not in standard UTF-16 encoding, the tool will prompt you: + +``` +Warning: desktop.ini file encoding is utf-8, not standard UTF-16. +This may cause Chinese and other special characters to display abnormally. +Fix encoding to UTF-16? [Y/n]: +``` + +Select `Y` to automatically fix the encoding. + +## Development + +```bash +# Install development dependencies +pip install -e ".[dev]" + +# Run tests +pytest + +# Code check +ruff check . +ruff format . + +# Type check +mypy remark/ + +# Build exe locally +python -m scripts.build +``` + +## How It Works + +This tool implements folder remarks through these steps: + +1. Create/modify `Desktop.ini` file in the folder +2. Write `[.ShellClassInfo]` section and `InfoTip` property +3. Save file with UTF-16 encoding +4. Set `Desktop.ini` as hidden and system attributes +5. Set folder as read-only (makes Windows read `Desktop.ini`) + +Reference: [Microsoft Official Documentation](https://learn.microsoft.com/en-us/windows/win32/shell/how-to-customize-folders-with-desktop-ini) + +## Notes + +- May take a few minutes to display in File Explorer after modification +- Some file managers may not support folder remarks +- The tool modifies system attributes of folders + +## License + +MIT License diff --git a/README.md b/README.md index bbf8577..263d350 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![PyPI](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) -[![en](https://img.shields.io/badge/lang-en-blue.svg)](README.md) -[![zh](https://img.shields.io/badge/lang-zh-red.svg)](README.zh.md) +[![en](https://img.shields.io/badge/lang-en-blue.svg)](README.en.md) +[![zh](https://img.shields.io/badge/lang-zh-red.svg)](README.md) A lightweight CLI tool to add remarks/comments to Windows folders via Desktop.ini. No background processes, privacy-first, ~12MB portable exe. / 一个轻量级的命令行工具,通过 Desktop.ini 为 Windows 文件夹添加备注/评论。无后台进程,隐私优先,约 12MB 便携版 exe。 diff --git a/README.zh.md b/README.zh.md deleted file mode 100644 index 10ca027..0000000 --- a/README.zh.md +++ /dev/null @@ -1,155 +0,0 @@ -# Windows Folder Remark/Comment Tool - Windows 文件夹备注工具 - -[![PyPI](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) -[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) -[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) -[![zh](https://img.shields.io/badge/lang-zh-blue.svg)](README.zh.md) - -一个轻量级的命令行工具,通过 Desktop.ini 为 Windows 文件夹添加备注/评论。无后台进程,隐私优先,约 12MB 便携版 exe。适合为文件添加自定义描述来更好地组织文件。 - -## ⭐ 支持 - -如果这个工具对你有帮助,请在 GitHub 上给个 Star! - -## 工具优势 - -- **用完即走**:需要时运行,用完即退出,无后台进程占用 -- **轻量小巧**:仅约 12 MB,不占用系统资源 -- **隐私优先**:完全本地运行,数据不传输至任何服务器 - -## 特性 - -- 支持中文等多语言字符(UTF-16 编码) -- 支持中英文界面切换 -- 命令行模式和交互模式 -- 自动编码检测和修复 -- 自动更新检查,保持最新版本 -- 右键菜单集成,快速访问 -- 单文件 exe 打包,无需 Python 环境 - -## 安装 - -### 方式一:使用 exe 文件(推荐) - -下载 [releases](https://github.com/piratf/windows-folder-remark/releases) 中的 `windows-folder-remark.exe`,直接使用。 - -### 方式二:从源码安装 - -```bash -# 克隆仓库 -git clone https://github.com/piratf/windows-folder-remark.git -cd windows-folder-remark - -# 安装依赖(无外部依赖) -pip install -e . - -# 运行 -python -m remark.cli --help -``` - -## 使用方法 - -### 命令行模式 - -```bash -# 添加备注 -windows-folder-remark.exe "C:\MyFolder" "这是我的文件夹" - -# 查看备注 -windows-folder-remark.exe --view "C:\MyFolder" - -# 删除备注 -windows-folder-remark.exe --delete "C:\MyFolder" - -# 检查更新 -windows-folder-remark.exe --update - -# 安装右键菜单 -windows-folder-remark.exe --install - -# 卸载右键菜单 -windows-folder-remark.exe --uninstall -``` - -### 交互模式 - -```bash -# 运行后根据提示操作 -windows-folder-remark.exe -``` - -### 右键菜单(推荐) - -安装右键菜单后,可以直接在文件资源管理器中右键文件夹添加备注: - -```bash -# 安装右键菜单 -windows-folder-remark.exe --install -``` - -- **Windows 10**:右键文件夹可直接看到「添加文件夹备注」 -- **Windows 11**:右键文件夹 → 点击「显示更多选项」→ 添加文件夹备注 - -### 自动更新 - -程序会在退出时自动检查更新(每 24 小时一次),如有新版本会提示是否立即更新。 - -也可以手动检查更新: - -```bash -windows-folder-remark.exe --update -``` - -## 编码检测 - -当使用 `--view` 查看备注时,如果检测到 `desktop.ini` 文件不是标准的 UTF-16 编码,工具会提醒你: - -``` -警告: desktop.ini 文件编码为 utf-8,不是标准的 UTF-16。 -这可能导致中文等特殊字符显示异常。 -是否修复编码为 UTF-16?[Y/n]: -``` - -选择 `Y` 可自动修复编码。 - -## 开发 - -```bash -# 安装开发依赖 -pip install -e ".[dev]" - -# 运行测试 -pytest - -# 代码检查 -ruff check . -ruff format . - -# 类型检查 -mypy remark/ - -# 本地打包 exe -python -m scripts.build -``` - -## 原理说明 - -该工具通过以下步骤实现文件夹备注: - -1. 在文件夹中创建/修改 `Desktop.ini` 文件 -2. 写入 `[.ShellClassInfo]` 段落和 `InfoTip` 属性 -3. 使用 UTF-16 编码保存文件 -4. 将 `Desktop.ini` 设置为隐藏和系统属性 -5. 将文件夹设置为只读属性(使 Windows 读取 `Desktop.ini`) - -参考:[Microsoft 官方文档](https://learn.microsoft.com/en-us/windows/win32/shell/how-to-customize-folders-with-desktop-ini) - -## 注意事项 - -- 修改后可能需要几分钟才能在资源管理器中显示 -- 某些文件管理器可能不支持显示文件夹备注 -- 工具会修改文件夹的系统属性 - -## 许可证 - -MIT License From cf0788a57fbf94f907bd3b6870ff2d7ac6ae4e01 Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 02:44:20 +0800 Subject: [PATCH 09/27] fix: correct workflow path for package.json --- .github/workflows/deploy-docs.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index e0e1c85..88aa271 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -22,15 +22,13 @@ jobs: with: node-version: '20' cache: 'npm' - cache-dependency-path: docs/package-lock.json + cache-dependency-path: package-lock.json - name: Install dependencies run: npm ci - working-directory: ./docs - name: Build VitePress site run: npm run docs:build - working-directory: ./docs - name: Upload artifact uses: actions/upload-pages-artifact@v3 From 260f276fa987490e61ade510b05618987e0fd3d1 Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 02:48:48 +0800 Subject: [PATCH 10/27] test: add feat-i18n branch trigger for testing --- .github/workflows/deploy-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 88aa271..0328722 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -2,7 +2,7 @@ name: Deploy VitePress site to Pages on: push: - branches: [main] + branches: [main, feat-i18n] workflow_dispatch: permissions: From 604c13ecb25fa88e4ca378c5d3feb8b2c533bdfc Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 02:51:13 +0800 Subject: [PATCH 11/27] test: remove feat-i18n branch trigger after testing --- .github/workflows/deploy-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 0328722..88aa271 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -2,7 +2,7 @@ name: Deploy VitePress site to Pages on: push: - branches: [main, feat-i18n] + branches: [main] workflow_dispatch: permissions: From 49cc14e880621b8e7d19b39f72bfc80c7f51c6f8 Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 03:07:06 +0800 Subject: [PATCH 12/27] fix: add base path for GitHub Pages --- docs/.vitepress/config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index c953748..f4533e1 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -3,6 +3,7 @@ import { defineConfig } from 'vitepress' export default defineConfig({ title: 'Windows Folder Remark Tool', description: 'A lightweight CLI tool to add remarks/comments to Windows folders via Desktop.ini', + base: '/windows-folder-remark/', lang: 'en-US', locales: { From 0700775387364df90afa768efdc02d5bd10989be Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 03:08:56 +0800 Subject: [PATCH 13/27] fix: simplify locale config and nav links --- docs/.vitepress/config.mjs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index f4533e1..0e191f2 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -9,7 +9,8 @@ export default defineConfig({ locales: { root: { label: 'English', - lang: 'en-US' + lang: 'en-US', + link: '/' }, zh: { label: '简体中文', @@ -19,9 +20,9 @@ export default defineConfig({ }, themeConfig: { - nav: root => [ - { text: 'Guide', link: root === 'zh' ? '/zh/guide/' : '/en/guide/' }, - { text: root === 'zh' ? 'English' : '中文', link: root === 'zh' ? '/en/' : '/zh/' } + nav: () => [ + { text: 'Guide', link: '/en/guide/' }, + { text: '中文', link: '/zh/' } ], sidebar: { From ec4bb7451a0ad00466299f75d5f921a0dc5b78a0 Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 03:11:57 +0800 Subject: [PATCH 14/27] revert: restore working config for VitePress build --- docs/.vitepress/config.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index 0e191f2..024c6ae 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -9,8 +9,7 @@ export default defineConfig({ locales: { root: { label: 'English', - lang: 'en-US', - link: '/' + lang: 'en-US' }, zh: { label: '简体中文', From 1c2873bdd67439d868ac5aa50d4b2e47b84d948b Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 03:24:03 +0800 Subject: [PATCH 15/27] docs: add prominent language switch links to READMEs --- README.en.md | 2 ++ README.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.en.md b/README.en.md index 85d98cd..f567efc 100644 --- a/README.en.md +++ b/README.en.md @@ -1,5 +1,7 @@ # Windows Folder Remark/Comment Tool +**[English](README.en.md)** | [中文文档](README.md) + [![PyPI](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![en](https://img.shields.io/badge/lang-en-blue.svg)](README.en.md) diff --git a/README.md b/README.md index 263d350..0054f0e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Windows Folder Remark/Comment Tool - Windows 文件夹备注工具 +**[English Documentation](README.en.md)** | [中文文档](README.md) + [![PyPI](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![en](https://img.shields.io/badge/lang-en-blue.svg)](README.en.md) From 31172afb3a8d534be72091454a47d6409a51db75 Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 03:35:54 +0800 Subject: [PATCH 16/27] fix: set default language to zh for tests --- tests/conftest.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 4510db3..8ea3523 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,6 +24,18 @@ def pytest_configure(config): if sys.stderr.encoding.lower() not in ("utf-8", "utf-16", "utf-16-le", "utf-16-be"): sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") + # 设置默认语言为中文,确保测试输出匹配 + from remark.i18n import set_language + set_language("zh") + + +@pytest.fixture(autouse=True) +def reset_language_to_zh(): + """每个测试前重置语言为中文""" + from remark.i18n import set_language + set_language("zh") + yield + def pytest_collection_modifyitems(config, items): """自动跳过非 Windows 平台上的 Windows 测试""" From 91764e7902fc0a968f639fc84fa009f16d3417ea Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 03:38:40 +0800 Subject: [PATCH 17/27] fix: set LANG env var before imports for consistent i18n in tests --- tests/conftest.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8ea3523..a5bd87f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,8 @@ """共享测试配置""" +import os import sys + from pathlib import Path import pytest @@ -8,6 +10,9 @@ # 项目根目录添加到路径 sys.path.insert(0, str(Path(__file__).parent.parent)) +# 在导入任何模块之前设置语言环境变量,确保翻译使用中文 +os.environ["LANG"] = "zh" + def pytest_configure(config): """pytest 配置钩子 - 定义 markers""" @@ -24,18 +29,6 @@ def pytest_configure(config): if sys.stderr.encoding.lower() not in ("utf-8", "utf-16", "utf-16-le", "utf-16-be"): sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") - # 设置默认语言为中文,确保测试输出匹配 - from remark.i18n import set_language - set_language("zh") - - -@pytest.fixture(autouse=True) -def reset_language_to_zh(): - """每个测试前重置语言为中文""" - from remark.i18n import set_language - set_language("zh") - yield - def pytest_collection_modifyitems(config, items): """自动跳过非 Windows 平台上的 Windows 测试""" From 3f269c858269893177c284c293b4bb3af98a51b4 Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 03:43:01 +0800 Subject: [PATCH 18/27] fix: prioritize LANG env var for consistent i18n in tests Modify get_system_language() to check LANG environment variable first before Windows API. This allows tests to reliably control language by setting LANG environment variable in conftest.py. Also update test_windows_platform_priority to clear LANG env var to properly test Windows API fallback behavior. --- remark/i18n.py | 29 ++++++++++++++++------------- tests/unit/test_i18n.py | 9 +++++---- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/remark/i18n.py b/remark/i18n.py index f44480b..bbe64bb 100644 --- a/remark/i18n.py +++ b/remark/i18n.py @@ -80,23 +80,16 @@ def get_system_language() -> str: """ 获取系统语言设置. - 在 Windows 上优先使用 GetUserDefaultLocaleName API, - 在其他平台上使用环境变量和 locale.getlocale()。 + 优先级顺序: + 1. 环境变量 LANG(最容易被测试控制) + 2. Windows 平台的 Windows API + 3. locale.getlocale() + 4. 默认返回英文 Returns: 语言代码(如 'en', 'zh'),如果不支持则返回默认的 'en' """ - # Windows 平台优先使用 Windows API - if platform.system() == "Windows": - windows_locale = _get_windows_locale() - if windows_locale: - # Windows locale 格式为 'zh-CN', 'en-US' 等 - # 提取语言部分(zh-CN -> zh) - lang_code = windows_locale.split("-")[0] - if lang_code in SUPPORTED_LANGUAGES: - return lang_code - - # 尝试从环境变量获取 + # 优先从环境变量获取(便于测试控制) lang = os.environ.get("LANG", "") if lang: # 提取语言代码(如 zh_CN.UTF-8 -> zh) @@ -109,6 +102,16 @@ def get_system_language() -> str: if full_lang in SUPPORTED_LANGUAGES: return full_lang + # Windows 平台使用 Windows API + if platform.system() == "Windows": + windows_locale = _get_windows_locale() + if windows_locale: + # Windows locale 格式为 'zh-CN', 'en-US' 等 + # 提取语言部分(zh-CN -> zh) + lang_code = windows_locale.split("-")[0] + if lang_code in SUPPORTED_LANGUAGES: + return lang_code + # 尝试从 locale 获取 try: loc = locale.getlocale()[0] diff --git a/tests/unit/test_i18n.py b/tests/unit/test_i18n.py index e799ef0..0337c80 100644 --- a/tests/unit/test_i18n.py +++ b/tests/unit/test_i18n.py @@ -64,12 +64,13 @@ class TestGetSystemLanguage: ], ) def test_windows_platform_priority(self, windows_locale, expected): - """测试 Windows 平台优先使用 Windows API""" + """测试 Windows 平台优先使用 Windows API(当 LANG 未设置时)""" with patch("remark.i18n.platform.system", return_value="Windows"): with patch("remark.i18n._get_windows_locale", return_value=windows_locale): - result = get_system_language() - assert result in SUPPORTED_LANGUAGES - assert result == expected + with patch.dict("os.environ", {}, clear=True): + result = get_system_language() + assert result in SUPPORTED_LANGUAGES + assert result == expected @pytest.mark.parametrize( "windows_locale", From 070009bf6c44362899b94794e8afd610efd3b7f9 Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 03:45:41 +0800 Subject: [PATCH 19/27] fix: force reinitialize translator to Chinese in pytest_configure Add explicit set_language("zh") call in pytest_configure to ensure all tests use Chinese translations. This fixes test failures where tests expected Chinese output but got English. --- tests/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index a5bd87f..40aca36 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,6 +29,10 @@ def pytest_configure(config): if sys.stderr.encoding.lower() not in ("utf-8", "utf-16", "utf-16-le", "utf-16-be"): sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") + # 强制重新初始化翻译器为中文 + from remark.i18n import set_language + set_language("zh") + def pytest_collection_modifyitems(config, items): """自动跳过非 Windows 平台上的 Windows 测试""" From 059b52da7ff056e346f936194a12776dd3e156e5 Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 03:51:02 +0800 Subject: [PATCH 20/27] fix: ensure sys is imported before use in pytest_configure Move sys import to the beginning of pytest_configure function to avoid UnboundLocalError. --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index 40aca36..6f05726 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,7 @@ def pytest_configure(config): """pytest 配置钩子 - 定义 markers""" + import sys config.addinivalue_line("markers", "unit: 单元测试(使用 mock)") config.addinivalue_line("markers", "integration: 集成测试(真实文件系统)") config.addinivalue_line("markers", "windows: 仅在 Windows 上运行") From 5125ff5077821a4ba8926e8e5144c0ca14c232c8 Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 03:53:40 +0800 Subject: [PATCH 21/27] fix: add autouse fixture to set Chinese language before each test Add an autouse fixture that calls set_language("zh") before each test. This ensures the translator is initialized to Chinese even if modules were imported before pytest_configure ran. --- tests/conftest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 6f05726..b5d402b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,3 +42,11 @@ def pytest_collection_modifyitems(config, items): for item in items: if "windows" in item.keywords: item.add_marker(skip_windows) + + +@pytest.fixture(autouse=True) +def set_chinese_language(): + """在每个测试前强制设置语言为中文""" + from remark.i18n import set_language + set_language("zh") + yield From 6560a06e0f65be058a49f72df62747a374e54004 Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 03:57:48 +0800 Subject: [PATCH 22/27] fix: set language at module level before any imports Move set_language("zh") call to module level in conftest.py, executing before any test modules are imported. This ensures the translator is initialized to Chinese globally. Also change autouse fixture scope to session for efficiency. --- tests/conftest.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b5d402b..d43e52b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,10 @@ # 在导入任何模块之前设置语言环境变量,确保翻译使用中文 os.environ["LANG"] = "zh" +# 立即设置翻译器为中文,确保在导入任何模块之前生效 +from remark.i18n import set_language +set_language("zh") + def pytest_configure(config): """pytest 配置钩子 - 定义 markers""" @@ -44,9 +48,9 @@ def pytest_collection_modifyitems(config, items): item.add_marker(skip_windows) -@pytest.fixture(autouse=True) +@pytest.fixture(autouse=True, scope="session") def set_chinese_language(): - """在每个测试前强制设置语言为中文""" + """在整个测试会话开始时设置语言为中文""" from remark.i18n import set_language set_language("zh") yield From e48846462bd4c1246fb22bb01339b1ca85721076 Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 11:22:55 +0800 Subject: [PATCH 23/27] test: add detailed debug output for translation loading Add debug output in conftest.py to help diagnose why translation files are not being loaded correctly in CI. --- tests/conftest.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index d43e52b..9870fee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,26 @@ os.environ["LANG"] = "zh" # 立即设置翻译器为中文,确保在导入任何模块之前生效 -from remark.i18n import set_language +from remark.i18n import set_language, LOCALE_DIR, init_translation +import pathlib + +locale_dir_path = pathlib.Path(LOCALE_DIR) +zh_messages_path = locale_dir_path / "zh" / "LC_MESSAGES" / "messages.mo" + +print(f"[DEBUG] LOCALE_DIR: {LOCALE_DIR}", file=sys.stderr) +print(f"[DEBUG] LOCALE_DIR exists: {locale_dir_path.exists()}", file=sys.stderr) +print(f"[DEBUG] zh/LC_MESSAGES exists: {(locale_dir_path / 'zh' / 'LC_MESSAGES').exists()}", file=sys.stderr) +print(f"[DEBUG] messages.mo exists: {zh_messages_path.exists()}", file=sys.stderr) + +# 验证翻译加载 +try: + translator = init_translation("zh") + test_result = translator.gettext("Path does not exist: {path}") + print(f"[DEBUG] Translation test result: {test_result!r}", file=sys.stderr) + print(f"[DEBUG] Contains English: {'Path does not exist' in test_result}", file=sys.stderr) +except Exception as e: + print(f"[DEBUG] Translation failed: {e}", file=sys.stderr) + set_language("zh") From 9928107cbf944cb04ed976517768e02a6bb7c89c Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 11:25:57 +0800 Subject: [PATCH 24/27] fix: include compiled .mo translation files in repository - Remove .mo files from .gitignore to allow them in repository - Add locale/zh/LC_MESSAGES/messages.mo to enable translations in CI - This fixes the issue where tests in CI were outputting English instead of Chinese due to missing translation files The .mo file is compiled from .po source using pybabel compile. Including compiled files in the repository ensures translations work in all environments without requiring build tools. --- .gitignore | 4 ++-- locale/zh/LC_MESSAGES/messages.mo | Bin 0 -> 10087 bytes 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 locale/zh/LC_MESSAGES/messages.mo diff --git a/.gitignore b/.gitignore index b9a79c0..fadc527 100644 --- a/.gitignore +++ b/.gitignore @@ -34,8 +34,8 @@ dmypy.json *.tar.gz /tools/upx/ -# i18n - 只忽略编译后的翻译文件 -locale/*/LC_MESSAGES/*.mo +# i18n - 不再忽略 .mo 文件,需要提交到仓库以便 CI 使用 +# .mo 文件由 pybabel compile 生成,从 .po 文件编译而来 # bv (beads viewer) local config and caches .bv/ diff --git a/locale/zh/LC_MESSAGES/messages.mo b/locale/zh/LC_MESSAGES/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..95fb5d694ce8628b16c996fbc1906096f571c38d GIT binary patch literal 10087 zcmbVQeQ;FQb-($T#8sNuPLs5D>*NNv5!kE{q$c(nu(1%fJbXn66N;((wEHBzX!pI% zdvB2xDvuxtNeCe@NPI*#V8oYgN!SpOkc1ph+CMU#OsCVSJ(D_}iTAx-b*81B$+Xk{ z(e`)lz5Dj9v?8rFf}h^I=bn4+x##@OdG}Xe{_Ft5PXhn`2><@_GxSvW`Nz*Owj7Ti z0>1{l_w$U^0>1@(3fKnxI&cE`72pTJ2Z8^hp6|Jbu`l5HA>bn565uz08-N^`06q-t z1Fii!1ilCSW8iM(|_XB^X z#{U6G@*e|9&%IxixEM(D*8(enF9N>`>;uxi&I8GxKLLcVDw(b;1V_nB!8a;()qm$B>Ue7eiL{HNcP@;pOjk$r02~* z+J92L?*Y<&t^mpI_kfQ8KLFDCeFjb-{pCQ4gEc@LA=?V1^LY_S`t|}z{#78w+fRYp zfxiO&9&qKC8LI(44}1oA97yN&K5!NAehASzn}B4G4kW!V14;fgkkhuoCz@a5s?j{}f2;eh9=b`wjlX608g_9|wLN zNYC4WNEOx$To1H?bZ!&CKLP$dkmBsWfZd;heK?Hec<#k!U;>*4)&c(xxDr?nqsZ<* z2g22C4-iw?A@%+|5TVQ71=7C%21xdPtniCiocz56_ylkxko?l6a7^L*K#GU|0h0VT zq2w`OHIM`M0Lf27z#YI0ko2vAarC|wxCHnvko@t_z)Il52&PrQIv~a4Q6TyM$3Tk1 z5g?A1{S}b>`hmiK1yWpp7UV+jBMPg47-BnsN;0+8}&T0PGK@ykAiBp`b^ zkoNy)Kx~2SQ_rsf;d1uZzzX0$0m6G~JDvYSz{h}_fHZGE@HybCz;6S6Ald&QoIvweDXayO zznXw#=YAmZ-8%~31yX+g0!aIO0EbHP@*UvkfPVpGWmT5BTen^AT6|mG`pEK?TsLD@ z+%OxtX2yBUGF`(=>re5WJC>Qdq8u~Qwym2kZ!i+N7{^1Dl~voeWk-2kli_guYGQmT zcbjzRwh}z1ncOlHZQRjaZtF?SZgzNsW#eBWuG`L1o=WJNqhrj`6MD>LWt;RC4&xld zGP$-}GZI>TLXYx2uGaXHsU`JxzBq4;62qYSt9?oF{01v+#=~=2+2)jPYc45=Z81HO z(BrIZi!?%-z;{+g_9V5|cCqnoskr9q+_YLk-q=~Wi&u&1tZe5V!;I^Y=36wk35%rw z?R(s|l>QR!x1DkPEZ%M;5`4FT&FQU%1NSy6e=HH>weXE$7T!8gr2It+B9S=U13Olc z_BEuDuaj@2l#ppm8({&+JiD!iC#|^77soW(DsQl@B)5&mCN~mG7_nxa)Xg-R5Q#MD ziPTaaX~H!5v$e@;;q;apCeAD=+{Q@MFdY~A@`9Y#s26iJ3+XiWRV2SHcu`tjy1byh zs!4~X6gFuwhw+TltMJiHle5aM7U~qQv zotoX4hBsVi7u467F5)ES8oL*o3Zd;dd8@_Y%PXV2Hsn(o+>~98uJiJ>@i<>E;+@xu z6ethRU0!nTuMfP!tHc47i(^Pyw$7EU<(x+6X@$}jX;4HO5Ru#Di>jh8zPPcCud`xu zgNyhgX-Gbk7cst*^vMak=DSI$kh{QiXj6Y<=Dg!6p&$~GM+fnuxGgEFV!e1#%v>nS zN-9w?hiM`zVv4{P++rjm0&3o(#CV8^U%xsGn$yf?-e#p`EILS6cDKY{o3M2)-o`aB zEf_HYB5~Y2xv{k-*NHy46%)o0QHdArOn8*YAQbZz6%`D;Er^+?Y%8WaWB`YP6xh?@ zRj!@jkMb&tJ8+B}NEe7p^1h(f1~980UQ(6>P{jPAfQ5^^7jP3SRRd0&nY79ugGX4p9Nxa$3ex32~xpf+T7IL7RzoXS0bOKQZPpv6xTL)ew4$BChVsYLRHJ< z^;F($Y;?YCt^R{Fow;iybt~=87uaS}c_W{r=#pB=8b#V@KW3pS(x$43o>UPPPUDNo zcw1`(VcYr=lx!&?!#&DWq22-qVvo9mg67IfM7ySkAEYguNnY?1SM(wUq6g68aYr$W zL%XAda+XPoa?l;*X>=*KkR>*k5rk#}j7Nq!=~T+HVH>vHs?kw9QMO)$ukagcH(Wt| ziRQYtQJ+SZg~l6<)}Xryy@q2faJ%$R(R;@wu3cCm9ru9d1lL%`OD%%|gnN)Wt_@CWge==AvLwiw{3<1ytO~YS zR@x{l5rlziTaTawBb7=B1(`3#&~8$6pyd?a#-RM;r4(YkUCy5`qXuQaSAqgm7K+^> zF?Jh{A;}xc6Bg=gBefIFbQ1HZ0y~p>#SZumky-h7fzL!4uo3K^RC|jCldx?tLedfq zi)^?h<)H_lb61qewX|zd<3{aYTLr5X{RiKvdP_8|O`5~&b;N)OYZxWH2$8Tmcjady znGtSII!5KE0U_45lqa<|?rP1*KWP*`9gu>;MTV0~Xl?n)=(y>~>UFLsQ*Plk0uL_3 z3Ce72&&$g`pm@3bSVA`&!DV`DOxM9L2_tE^WJ;@%OeeV-#rf}%f3{O9P(z0=3l{Fx zsQ*QOEBaIUT_Eekfv}`naOzBKUxlow^DGk^U9X22TjiGKIGgy-)+SC|XpA170bpsn?-M^u9&| zqKEQ7uSc>Q1&(Ru3A9iz%4wbQrCc>!Fi(B}N)`vL$HjN1JZcdox@*CZz6(7Q7980IGNB!WwNYDRKw_M6Q&nsHIZq-R1dh?BAYt->^;kb5Ma4Jff zco${cHJiTwqG9%zXBHeSg>(L)eW@&tE}Wy;aa9*8SpJ)2efWce*WqLQE<=2G@CD7% zW-~r^O}53hz7O7y)Wjn%$j@>3vv%|I)m3%;h3eX^HJdk;)#_B4BO9GYBOZAs-RMN> ztSGPExVC1)(`(nQtF7L;wQS4gx=0oJV2Yr~I<&x1zM^u)%1Gt%$ciWUiWSjs|LLQZ zk5^VgFjA`%;VvZin3UU~Ic}uR)=UQtHQhd}-?m``-?VmPHD6V<^qR5_sD5d9Jj#FA z6g))gbPb*A51aU^4I7?L7!{V?xCU1MXZLsm^~i0@3mUKI zN@n_$e1rLlzKZW=q;_#T<8_~%JvA)F@nIf`h`X7P0GF3D{^fJtRR5i+Zm<8^?D5fD z|B%-=kh}V$>~XleN6egeFXNxQogKdAjgNWl@EaPtTW|dky2C?|^rtr-deY15>mq>Aqm!-qGpIe-P zfARYF`2$z!@+EU?ICJZjg{D9tPjYneBJBXJC^Le39oCCbO@JY>)!BbX)qMfElf$}(ENYDiIKjNSxWaXQlorZ z;$oxteu6KO+3a^8&7R&L>`~|momL4wp$nOCtd^8q=ssqCfR&uC0+aS8l1tLUf;(L3 zB1Y~`UB&qO&*ZMYo|&FOqOipax%>ckr?lR>V;} z^RyJrz5b>@)aM;M=bgIj4~}?K9dlMVjT+#;y)Sp8(?4-z?wEh#tbc7lYQnAqO-!Dv zT0UQ+$b4wa-9F}ZUI+qzJ<7 zp*LhydQ$^l|ILErC#|}H27hE&R%z-1@(d_rA~QY`r2n#DOJOF#8TqI8!BGGBn15)m z*Lfo|IVdMn+@YRiZucS@#ye5Al!muo^E)od_+^FmzXFU zh~G9aJmG~`3wIs!66nAwoLfdTD{uy8sA znv1&~It^K2qHIod1*$-2GJ5Aq1lb$RB>t0a;P3=S!{79U$#aAx0Z_t;5)w{n3Zw49 z+~~~giP6mD1!UIjvD=jDp*+7cb-Iu+xCOxkZql3V^fDL|6UuNpGYzov&q!xs*G@`;gc2T`m&3C?E1e1&ILsnccE0$5O zW(Q`xTNlA^SZDUs5wH6OZFH!^8@?omC|ZNd3}W?>6WO=0PN;8G^;nQ{iGloj;Tf{^ zD=`Dzw-`f|T!Tp}p9Bjq@9-J9F*pItotb$xJ8%l-W+prR;}c&0NhyuwKyORuf_%dO z2u#IdPoMOu*V%oSVwf8%(6j%}xu79u;V&3!&P0HtA)^x$Up~SR;yE}i5b%a4AQL7< zQY4Sq8=J~?T?dUq7ve43GvSXOlpa>y_S8Oae4O@@XM(%512R146n+`Bxv}g1$-~G* zb-SQihmzkga)#_N=xgZ67=j&pRB!sOAp>L|A}X@zg_M3K`N$ufKw$)tj9m1N9xT3E zkX|eBWZ`CkP7;Lz%yAYWe*l$5eR9(F^Myy9B#2u^wD^9!`)K%<0cA3K<^ua)=3c1T literal 0 HcmV?d00001 From 1182caaeb75f4511ae3e3d4eaf48e1d71005d296 Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 11:30:57 +0800 Subject: [PATCH 25/27] test: remove debug output from conftest.py --- tests/conftest.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9870fee..d43e52b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,26 +14,7 @@ os.environ["LANG"] = "zh" # 立即设置翻译器为中文,确保在导入任何模块之前生效 -from remark.i18n import set_language, LOCALE_DIR, init_translation -import pathlib - -locale_dir_path = pathlib.Path(LOCALE_DIR) -zh_messages_path = locale_dir_path / "zh" / "LC_MESSAGES" / "messages.mo" - -print(f"[DEBUG] LOCALE_DIR: {LOCALE_DIR}", file=sys.stderr) -print(f"[DEBUG] LOCALE_DIR exists: {locale_dir_path.exists()}", file=sys.stderr) -print(f"[DEBUG] zh/LC_MESSAGES exists: {(locale_dir_path / 'zh' / 'LC_MESSAGES').exists()}", file=sys.stderr) -print(f"[DEBUG] messages.mo exists: {zh_messages_path.exists()}", file=sys.stderr) - -# 验证翻译加载 -try: - translator = init_translation("zh") - test_result = translator.gettext("Path does not exist: {path}") - print(f"[DEBUG] Translation test result: {test_result!r}", file=sys.stderr) - print(f"[DEBUG] Contains English: {'Path does not exist' in test_result}", file=sys.stderr) -except Exception as e: - print(f"[DEBUG] Translation failed: {e}", file=sys.stderr) - +from remark.i18n import set_language set_language("zh") From a1a817accca7da82aa792c822f8dee6591550d53 Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 12:40:36 +0800 Subject: [PATCH 26/27] =?UTF-8?q?docs:=20=E4=BC=98=E5=8C=96=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=8F=8F=E8=BF=B0=EF=BC=8C=E5=8E=BB=E9=99=A4=E7=94=9F?= =?UTF-8?q?=E7=A1=AC=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 "约 12MB" 等具体版本号信息 - 移除 "隐私优先" 等不够具体的描述 - 统一修改为: - "无系统驻留,无数据上传,安全放心,用完即走" (中文) - "No system residency, no data upload, safe and secure, use it when you need it" (英文) 修改文件: - README.md - README.en.md - docs/zh/index.md - docs/en/index.md --- README.en.md | 8 ++++---- README.md | 8 ++++---- docs/en/index.md | 16 ++++++++-------- docs/zh/index.md | 14 +++++++------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/README.en.md b/README.en.md index f567efc..9e36cdc 100644 --- a/README.en.md +++ b/README.en.md @@ -7,7 +7,7 @@ [![en](https://img.shields.io/badge/lang-en-blue.svg)](README.en.md) [![zh](https://img.shields.io/badge/lang-zh-red.svg)](README.md) -A lightweight CLI tool to add remarks/comments to Windows folders via Desktop.ini. No background processes, privacy-first, ~12MB portable exe. Perfect for organizing your files with custom descriptions. +A lightweight CLI tool to add remarks/comments to Windows folders via Desktop.ini. No system residency, no data upload, safe and secure, use it when you need it. Perfect for organizing your files with custom descriptions. ## ⭐ Star Us @@ -15,9 +15,9 @@ If you find this tool helpful, please consider giving it a star on GitHub! ## Why This Tool -- **No Background Processes**: Runs when needed, exits when done — zero background footprint -- **Lightweight**: Only ~12 MB, minimal system resource usage -- **Privacy-First**: Completely local operation, no data sent to any server +- **Use and Go**: Runs when needed, exits when done — no system residency +- **Safe & Secure**: Completely local operation, no data upload, privacy protected +- **Portable**: Single-file exe packaging, no installation required ## Features diff --git a/README.md b/README.md index 0054f0e..a3c0b68 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![en](https://img.shields.io/badge/lang-en-blue.svg)](README.en.md) [![zh](https://img.shields.io/badge/lang-zh-red.svg)](README.md) -A lightweight CLI tool to add remarks/comments to Windows folders via Desktop.ini. No background processes, privacy-first, ~12MB portable exe. / 一个轻量级的命令行工具,通过 Desktop.ini 为 Windows 文件夹添加备注/评论。无后台进程,隐私优先,约 12MB 便携版 exe。 +A lightweight CLI tool to add remarks/comments to Windows folders via Desktop.ini. No system residency, no data upload, safe and secure, use it when you need it. / 一个轻量级的命令行工具,通过 Desktop.ini 为 Windows 文件夹添加备注/评论。无系统驻留,无数据上传,安全放心,用完即走。 **Documentation**: [Full Documentation](https://piratf.github.io/windows-folder-remark/en/) | [完整文档](https://piratf.github.io/windows-folder-remark/zh/) @@ -17,9 +17,9 @@ A lightweight CLI tool to add remarks/comments to Windows folders via Desktop.in ## 工具优势 -- **用完即走**:需要时运行,用完即退出,无后台进程占用 -- **轻量小巧**:仅约 12 MB,不占用系统资源 -- **隐私优先**:完全本地运行,数据不传输至任何服务器 +- **用完即走**:需要时运行,用完即退出,无系统驻留 +- **安全放心**:完全本地运行,无数据上传,保护隐私 +- **轻量便携**:单文件 exe 打包,无需安装,随处可用 ## 特性 diff --git a/docs/en/index.md b/docs/en/index.md index e8de906..2e7f6e8 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -14,18 +14,18 @@ hero: link: https://github.com/piratf/windows-folder-remark features: - - title: No Background Processes - details: Runs when needed, exits when done — zero background footprint - - title: Lightweight - details: Only ~12 MB, minimal system resource usage - - title: Privacy-First - details: Completely local operation, no data sent to any server + - title: Use and Go + details: Runs when needed, exits when done — no system residency + - title: Safe & Secure + details: Completely local operation, no data upload, privacy protected + - title: Portable + details: Single-file exe packaging, no installation required - title: Multi-language Support details: English and Chinese interface with auto language detection - title: UTF-16 Encoding details: Full support for Chinese and other special characters - - title: Portable exe - details: Single-file packaging, no Python environment required + - title: Auto Update + details: Built-in update checking to stay current --- ## ⭐ Star Us diff --git a/docs/zh/index.md b/docs/zh/index.md index b4a2d04..c4d7b59 100644 --- a/docs/zh/index.md +++ b/docs/zh/index.md @@ -15,17 +15,17 @@ hero: features: - title: 用完即走 - details: 需要时运行,用完即退出,无后台进程占用 - - title: 轻量小巧 - details: 仅约 12 MB,不占用系统资源 - - title: 隐私优先 - details: 完全本地运行,数据不传输至任何服务器 + details: 需要时运行,用完即退出,无系统驻留 + - title: 安全放心 + details: 完全本地运行,无数据上传,保护隐私 + - title: 轻量便携 + details: 单文件 exe 打包,无需安装,随处可用 - title: 多语言支持 details: 支持中英文界面,自动检测系统语言 - title: UTF-16 编码 details: 支持中文等多语言字符 - - title: 便携版 exe - details: 单文件打包,无需 Python 环境 + - title: 自动更新 + details: 内置更新检查,保持最新版本 --- ## ⭐ 支持 From ac80f510f4c4c3448dd8e4b8ab93b90dc56605ea Mon Sep 17 00:00:00 2001 From: piratf Date: Sat, 31 Jan 2026 13:42:05 +0800 Subject: [PATCH 27/27] =?UTF-8?q?feat(seo):=20=E5=AE=8C=E6=88=90=E9=AB=98?= =?UTF-8?q?=E7=BA=A7=20SEO=20=E4=BC=98=E5=8C=96=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重组文档结构:中文版作为根目录,英文版作为 /en/ 子目录 - 添加 hreflang 标签用于多语言 SEO - 配置 sitemap.xml 生成,修复 base path 问题 - 添加 robots.txt 文件 - 为中英文首页添加 JSON-LD 结构化数据 - 更新文档描述为更自然的表达方式 --- docs/.vitepress/config.mjs | 62 +++++++++++++++++--------- docs/en/index.md | 23 +++++++++- docs/{zh => }/guide/api.md | 0 docs/{zh => }/guide/getting-started.md | 0 docs/{zh => }/guide/usage.md | 0 docs/{zh => }/index.md | 25 ++++++++++- docs/robots.txt | 4 ++ 7 files changed, 89 insertions(+), 25 deletions(-) rename docs/{zh => }/guide/api.md (100%) rename docs/{zh => }/guide/getting-started.md (100%) rename docs/{zh => }/guide/usage.md (100%) rename docs/{zh => }/index.md (53%) create mode 100644 docs/robots.txt diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index 024c6ae..fa8deaa 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -1,30 +1,59 @@ import { defineConfig } from 'vitepress' export default defineConfig({ - title: 'Windows Folder Remark Tool', - description: 'A lightweight CLI tool to add remarks/comments to Windows folders via Desktop.ini', + title: 'Windows 文件夹备注工具', + description: '一个轻量级的命令行工具,通过 Desktop.ini 为 Windows 文件夹添加备注/评论。无系统驻留,无数据上传,安全放心,用完即走。', base: '/windows-folder-remark/', - lang: 'en-US', + lang: 'zh-CN', locales: { root: { - label: 'English', - lang: 'en-US' - }, - zh: { label: '简体中文', - lang: 'zh-CN', - link: '/zh/' + lang: 'zh-CN' + }, + en: { + label: 'English', + lang: 'en-US', + link: '/en/' + } + }, + + head: [ + ['link', { rel: 'alternate', hreflang: 'zh-CN', href: 'https://piratf.github.io/windows-folder-remark/' }], + ['link', { rel: 'alternate', hreflang: 'en-US', href: 'https://piratf.github.io/windows-folder-remark/en/' }], + ['link', { rel: 'alternate', hreflang: 'x-default', href: 'https://piratf.github.io/windows-folder-remark/en/' }] + ], + + sitemap: { + hostname: 'https://piratf.github.io', + transformItems(items) { + return items.map((item) => { + return { + url: '/windows-folder-remark' + (item.url.startsWith('/') ? '' : '/') + item.url, + changefreq: 'weekly', + } + }) } }, themeConfig: { nav: () => [ - { text: 'Guide', link: '/en/guide/' }, - { text: '中文', link: '/zh/' } + { text: '指南', link: '/guide/' }, + { text: 'English', link: '/en/' } ], sidebar: { + '/': [ + { + text: '指南', + items: [ + { text: '介绍', link: '/' }, + { text: '快速开始', link: '/guide/getting-started' }, + { text: '使用方法', link: '/guide/usage' }, + { text: 'API 参考', link: '/guide/api' } + ] + } + ], '/en/': [ { text: 'Guide', @@ -35,17 +64,6 @@ export default defineConfig({ { text: 'API Reference', link: '/en/guide/api' } ] } - ], - '/zh/': [ - { - text: '指南', - items: [ - { text: '介绍', link: '/zh/' }, - { text: '快速开始', link: '/zh/guide/getting-started' }, - { text: '使用方法', link: '/zh/guide/usage' }, - { text: 'API 参考', link: '/zh/guide/api' } - ] - } ] } } diff --git a/docs/en/index.md b/docs/en/index.md index 2e7f6e8..4b150e6 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -1,6 +1,27 @@ --- layout: home +head: + - - script + - type: application/ld+json + - | + { + "@context": "https://schema.org", + "@type": "SoftwareSourceCode", + "name": "Windows Folder Remark Tool", + "description": "A lightweight CLI tool to add remarks/comments to Windows folders via Desktop.ini. No system residency, no data upload, safe and secure, use it when you need it.", + "programmingLanguage": "Python", + "codeRepository": "https://github.com/piratf/windows-folder-remark", + "url": "https://piratf.github.io/windows-folder-remark/en/", + "version": "2.0.6", + "license": "MIT", + "offers": { + "@type": "Offer", + "price": "0", + "priceCurrency": "USD" + } + } + hero: name: Windows Folder Remark Tool text: A Lightweight CLI Tool for Windows Folder Remarks @@ -8,7 +29,7 @@ hero: actions: - theme: brand text: Get Started - link: /en/guide/getting-started + link: /guide/getting-started - theme: alt text: GitHub link: https://github.com/piratf/windows-folder-remark diff --git a/docs/zh/guide/api.md b/docs/guide/api.md similarity index 100% rename from docs/zh/guide/api.md rename to docs/guide/api.md diff --git a/docs/zh/guide/getting-started.md b/docs/guide/getting-started.md similarity index 100% rename from docs/zh/guide/getting-started.md rename to docs/guide/getting-started.md diff --git a/docs/zh/guide/usage.md b/docs/guide/usage.md similarity index 100% rename from docs/zh/guide/usage.md rename to docs/guide/usage.md diff --git a/docs/zh/index.md b/docs/index.md similarity index 53% rename from docs/zh/index.md rename to docs/index.md index c4d7b59..cc223c7 100644 --- a/docs/zh/index.md +++ b/docs/index.md @@ -1,14 +1,35 @@ --- layout: home +head: + - - script + - type: application/ld+json + - | + { + "@context": "https://schema.org", + "@type": "SoftwareSourceCode", + "name": "Windows 文件夹备注工具", + "description": "一个轻量级的命令行工具,通过 Desktop.ini 为 Windows 文件夹添加备注/评论。无系统驻留,无数据上传,安全放心,用完即走。", + "programmingLanguage": "Python", + "codeRepository": "https://github.com/piratf/windows-folder-remark", + "url": "https://piratf.github.io/windows-folder-remark/", + "version": "2.0.6", + "license": "MIT", + "offers": { + "@type": "Offer", + "price": "0", + "priceCurrency": "USD" + } + } + hero: - name: Windows Folder Remark Tool + name: Windows 文件夹备注工具 text: 轻量级 Windows 文件夹备注工具 tagline: 通过 Desktop.ini 为 Windows 文件夹添加备注/评论 actions: - theme: brand text: 快速开始 - link: /zh/guide/getting-started + link: /guide/getting-started - theme: alt text: GitHub link: https://github.com/piratf/windows-folder-remark diff --git a/docs/robots.txt b/docs/robots.txt new file mode 100644 index 0000000..8bd9712 --- /dev/null +++ b/docs/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://piratf.github.io/windows-folder-remark/sitemap.xml