diff --git a/JaxI18n.slnx b/JaxI18n.slnx deleted file mode 100644 index 2b99dba..0000000 --- a/JaxI18n.slnx +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LocaleSmith.slnx b/LocaleSmith.slnx new file mode 100644 index 0000000..0353832 --- /dev/null +++ b/LocaleSmith.slnx @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index b20dca5..f9e4ff5 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@
LocaleSmith 标志 -

LocaleSmith · 译匠

+

LocaleSmith | 译匠

为 Minecraft Java 内容打造的 Windows 原生 AI 本地化工作台

安全扫描模组与资源包,连接本地或云端模型,以可验证、可回滚的流水线生成本地化产物。

@@ -24,6 +24,7 @@ 项目概览 · 核心能力 · 快速开始 · + 翻译日志 · 安全边界 · 开源许可

@@ -34,7 +35,7 @@ ## 项目概览 -LocaleSmith(译匠)面向 Minecraft: Java Edition 模组、资源包与光影包,将**安全扫描、增量翻译、结构验证和事务重建**整合进一个 Windows 原生桌面工作台。 +**LocaleSmith | 译匠**,面向 Minecraft: Java Edition 模组、资源包与光影包,将**安全扫描、增量翻译、结构验证和事务重建**整合进一个 Windows 原生桌面工作台。 项目采用 Rust 原生扫描核心与 .NET 10 / WinUI 3 桌面应用:Rust 负责 JAR、ZIP、Loader 元数据和受支持字节码模式的解析,.NET 负责归档事务、模型接入、安全存储、翻译队列与用户界面。 @@ -45,7 +46,9 @@ LocaleSmith(译匠)面向 Minecraft: Java Edition 模组、资源包与光 | **安全归档扫描** | 识别路径穿越、Loader 元数据、语言资源、签名证据与受支持的 Java 字符串引用。 | | **增量翻译流水线** | 按内容哈希复用译文,校验占位符与结构,并在失败时回滚整个作业。 | | **多模型接入** | 统一支持 Ollama、OpenAI-compatible Chat Completions 与 Anthropic Messages。 | -| **原生桌面体验** | 提供首次引导、翻译队列、双语助手、模型源管理、设置和 CLI 风险确认。 | +| **提供方预设** | DeepSeek、Qwen、MiMo、MiniMax、OpenAI、豆包、智谱与 Kimi 等预设会同步填充服务地址、模型名和推荐 Token 字段;也可明确选择不发送 Token 上限字段。 | +| **持久化诊断日志** | 日志目录可写时,每次翻译在后台有界写入独立的 Debug 与 All levels `.log`;可在左侧“日志”页查看并在引导或设置中修改目录。 | +| **原生桌面体验** | 提供首次引导、翻译队列、双语助手、模型源管理、日志、设置和 CLI 风险确认。 | | **凭据与配置保护** | API Key 存入 Windows Credential Manager,其他配置使用 AES-256-GCM 加密。 | | **受控 MCP / CLI** | 模型只能读取安全上下文并提出命令;执行必须经过策略复核和用户明确确认。 | @@ -75,6 +78,12 @@ flowchart LR 每个作业只生成用户选择的一种翻译风格。另一种风格可以单独入队,并复用相同原文哈希下已经缓存的对应译文。 +## 翻译日志与持久化设置 + +左侧导航中的“日志”页按翻译作业列出持久化记录,并默认显示 Debug 视图;切换到 All levels 可查看包含细粒度进度在内的完整级别记录。日志是最大限度的后台诊断功能:目录正常可写且写入器有容量时,作业会创建一对 `.debug.log` / `.all.log` 文件并增量刷新到磁盘;慢设备或队列已满时可能跳过文件或丢弃部分诊断条目,但不会阻塞翻译。进程异常退出后,已经成功刷新的内容仍可用于定位最后一个阶段。 + +默认目录为 `%LOCALAPPDATA%\LocaleSmith\logs\translations`。首次引导和“设置”页都可以浏览或手动修改为本地目录;更改保存后从下一次翻译起生效,并会在软件关闭时与语言、主题、工作区等最后一次有效设置一起写入加密配置。程序只保留并列出最新 500 次会话;清理仅匹配 LocaleSmith 自有命名格式,不删除目录内的其他文件。日志仅记录任务 ID、包文件名、阶段、进度、结果与错误类型,不写入 API Key、完整提示词或用户选择路径的父目录;常见 Bearer / Token / API Key 形式还会在写盘前再次脱敏。 + ## 快速开始 ### 环境要求 @@ -96,24 +105,24 @@ flowchart LR git clone https://github.com/DZXH-TX/LocaleSmith.git Set-Location LocaleSmith -cargo build --manifest-path native/jax_i18n_core/Cargo.toml --release -dotnet restore JaxI18n.slnx -dotnet build JaxI18n.slnx -c Release +cargo build --manifest-path native/localesmith_core/Cargo.toml --release +dotnet restore LocaleSmith.slnx +dotnet build LocaleSmith.slnx -c Release ``` > [!NOTE] -> `dotnet build JaxI18n.slnx` 不会生成 WAP / MSIX;打包工程位于 `packaging/JaxI18n.Package`,需要在具备对应 Visual Studio targets 的开发环境中单独构建。 +> `dotnet build LocaleSmith.slnx` 不会生成 WAP / MSIX;打包工程位于 `packaging/LocaleSmith.Package`,需要在具备对应 Visual Studio targets 的开发环境中单独构建。
运行完整验证门 ```powershell -cargo fmt --manifest-path native/jax_i18n_core/Cargo.toml --all -- --check -cargo clippy --manifest-path native/jax_i18n_core/Cargo.toml --all-targets --all-features -- -D warnings -cargo test --manifest-path native/jax_i18n_core/Cargo.toml --all-targets +cargo fmt --manifest-path native/localesmith_core/Cargo.toml --all -- --check +cargo clippy --manifest-path native/localesmith_core/Cargo.toml --all-targets --all-features -- -D warnings +cargo test --manifest-path native/localesmith_core/Cargo.toml --all-targets -dotnet test JaxI18n.slnx -c Release -dotnet format JaxI18n.slnx --verify-no-changes --no-restore +dotnet test LocaleSmith.slnx -c Release +dotnet format LocaleSmith.slnx --verify-no-changes --no-restore ```
@@ -121,16 +130,16 @@ dotnet format JaxI18n.slnx --verify-no-changes --no-restore ## 源码结构 ```text -native/jax_i18n_core/ Rust ZIP/JAR、metadata 与 classfile 扫描核心 -src/JaxI18n.Core/ 领域模型和统一服务契约 -src/JaxI18n.NativeInterop/ C ABI 投影、DLL 解析与类型化清单 -src/JaxI18n.Application/ 翻译编排、增量计划、队列与事务边界 -src/JaxI18n.Archive/ 安全快照、提取、重建、验证与回滚 -src/JaxI18n.Infrastructure/ 模型适配、凭据、加密、CLI 与环境检测 -src/JaxI18n.Mcp/ MCP JSON-RPC / stdio 协议与工具目录 -src/JaxI18n.McpHost/ 独立 MCP 控制台宿主 -src/JaxI18n.Presentation/ 可测试的 MVVM ViewModel 与 UI 契约 -src/JaxI18n.App/ WinUI 3 视图、组合根和本地应用服务 +native/localesmith_core/ Rust ZIP/JAR、metadata 与 classfile 扫描核心 +src/LocaleSmith.Core/ 领域模型和统一服务契约 +src/LocaleSmith.NativeInterop/ C ABI 投影、DLL 解析与类型化清单 +src/LocaleSmith.Application/ 翻译编排、增量计划、队列与事务边界 +src/LocaleSmith.Archive/ 安全快照、提取、重建、验证与回滚 +src/LocaleSmith.Infrastructure/ 模型适配、凭据、加密、CLI 与环境检测 +src/LocaleSmith.Mcp/ MCP JSON-RPC / stdio 协议与工具目录 +src/LocaleSmith.McpHost/ 独立 MCP 控制台宿主 +src/LocaleSmith.Presentation/ 可测试的 MVVM ViewModel 与 UI 契约 +src/LocaleSmith.App/ WinUI 3 视图、组合根和本地应用服务 tests/ 八个 .NET 测试项目和受限 CLI probe packaging/ x64 WAP / MSIX manifest、双语资源和图标 ``` @@ -141,6 +150,7 @@ packaging/ x64 WAP / MSIX manifest、双语资源和图标 - **模型不能授权命令执行。** Provider 工具循环只允许读取安全上下文和提出命令,完整命令仍需用户查看、勾选风险确认并批准。 - **签名 JAR 默认禁止修改。** 没有原作者私钥就无法保持原签名;项目只会阻断修改,或在用户明确选择后生成 unsigned copy。 +- **CLI 不搜索进程 PATH。** 默认不信任任何进程型可执行文件;后续显式白名单必须使用已批准的绝对路径。私有 CLI 沙箱默认位于 `%LOCALAPPDATA%\LocaleSmith\CliSandbox`,并在创建前后检查重解析点。 - **Low IL 不等于 AppContainer。** 受限 token、私有 desktop 与 Job Object 会缩小执行面,但不会自动阻断网络,也不能阻止读取当前用户 ACL 已允许的文件。
@@ -160,14 +170,14 @@ packaging/ x64 WAP / MSIX manifest、双语资源和图标
模型工具与 CLI 隔离 -MCP stdio Host 只暴露 `system.context` 与 `cli.propose`,不暴露 `cli.execute`。允许执行的命令必须通过动态白名单、绝对拒绝规则、工作目录与敏感参数检查,并绑定一次性确认 token;启动前审计不可写时不会启动进程。Kimi 的私有 `reasoning_content` 仅在同一 Kimi 工具循环内有界回放,不进入用户可见内容,也不会发送给其他 Provider。 +MCP stdio Host 只暴露 `system.context` 与 `cli.propose`,不暴露 `cli.execute`。允许执行的命令必须来自已经批准的绝对路径,并通过绝对拒绝规则、工作目录与敏感参数检查,再绑定一次性确认 token;启动前审计不可写时不会启动进程。Kimi 的私有 `reasoning_content` 仅在同一 Kimi 工具循环内有界回放,不进入用户可见内容,也不会发送给其他 Provider。
MSIX 开发包状态 -当前 manifest 版本为 `0.1.0.1`,开发包已完成 payload、PRI、MCP Host、开发签名和 loose AppsFolder 启动验证。但其证书为未受信任、无可信时间戳的自签名开发证书,尚未完成生产证书替换及干净机安装、升级、修复和卸载验证。 +当前 manifest 版本为 `0.1.0.2`。仓库中的历史开发包曾完成 payload、PRI、MCP Host、SignPath Authenticode 签名与本机启动验证,但该证据不覆盖本次源码修复;当前提交需重新生成、签名和安装验证 MSIX,微软商店上架前还需使用 Partner Center 分配的正式包身份。
@@ -177,10 +187,10 @@ MCP stdio Host 只暴露 `system.context` 与 `cli.propose`,不暴露 `cli.exe | 检查项 | 基线 | | --- | --- | -| .NET Release | `260 / 260` tests,`0` warnings,`0` errors | +| .NET Release | `391 / 391` tests,`0` warnings,`0` errors | | Rust | `26 / 26` tests,`rustfmt` 与 `clippy -D warnings` 通过 | -| 双语资源 | `zh-CN` / `en-US` 各 `332` 个 key,完全对齐 | -| 源码安全审计 | FINAL GREEN,P0 / P1 / P2 均为 `0` | +| 双语资源 | `zh-CN` / `en-US` 各 `366` 个 key,完全对齐 | +| 源码安全审计 | 本地路径、归档、CLI、凭据和迁移回归门通过;GitHub CodeQL 结果以当前提交的远端重扫为准,不在 README 中宣称零告警 | 这些结果证明当前自动化覆盖的源码行为,不替代外部渗透测试、真实 Provider 验证或 Minecraft / Loader 运行时兼容测试。 diff --git a/native/jax_i18n_core/include/jax_i18n_core.h b/native/jax_i18n_core/include/jax_i18n_core.h deleted file mode 100644 index a2c77ff..0000000 --- a/native/jax_i18n_core/include/jax_i18n_core.h +++ /dev/null @@ -1,63 +0,0 @@ -#ifndef JAX_I18N_CORE_H -#define JAX_I18N_CORE_H - -#include - -#if defined(_WIN32) -# define JAX_I18N_API __declspec(dllimport) -#else -# define JAX_I18N_API -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -/* Stable return/error codes. */ -typedef enum jax_i18n_error_code { - JAX_I18N_OK = 0, - JAX_I18N_ERROR_NULL_POINTER = 1, - JAX_I18N_ERROR_INVALID_UTF8 = 2, - JAX_I18N_ERROR_IO = 3, - JAX_I18N_ERROR_INVALID_ARCHIVE = 4, - JAX_I18N_ERROR_LIMIT_EXCEEDED = 5, - JAX_I18N_ERROR_UNSAFE_ARCHIVE_PATH = 6, - JAX_I18N_ERROR_SERIALIZATION = 7, - JAX_I18N_ERROR_PANIC = 8, - JAX_I18N_ERROR_INVALID_ARGUMENT = 9 -} jax_i18n_error_code; - -/* Process-lifetime UTF-8 string. Do not free. */ -JAX_I18N_API const char *jax_i18n_core_version(void); - -/* Current JSON manifest schema version. */ -JAX_I18N_API uint32_t jax_i18n_manifest_schema_version(void); - -/* - * Scan one ZIP/JAR path encoded as NUL-terminated UTF-8. - * - * On success, returns JAX_I18N_OK and stores a newly allocated UTF-8 JSON - * string in *out_json_utf8. Release it once with jax_i18n_string_free. - * On failure, *out_json_utf8 is null and the thread-local error is updated. - */ -JAX_I18N_API int32_t jax_i18n_scan_archive_json( - const char *path_utf8, - char **out_json_utf8); - -/* Free a non-static string returned by this library. Null is accepted. */ -JAX_I18N_API void jax_i18n_string_free(char *value); - -/* Calling-thread error code from the most recent scan call. */ -JAX_I18N_API int32_t jax_i18n_last_error_code(void); - -/* - * Copy the calling-thread error message into a newly allocated UTF-8 string. - * Release the result once with jax_i18n_string_free. - */ -JAX_I18N_API char *jax_i18n_last_error_message(void); - -#ifdef __cplusplus -} -#endif - -#endif /* JAX_I18N_CORE_H */ diff --git a/native/jax_i18n_core/.gitignore b/native/localesmith_core/.gitignore similarity index 100% rename from native/jax_i18n_core/.gitignore rename to native/localesmith_core/.gitignore diff --git a/native/jax_i18n_core/Cargo.lock b/native/localesmith_core/Cargo.lock similarity index 99% rename from native/jax_i18n_core/Cargo.lock rename to native/localesmith_core/Cargo.lock index fdde024..6011ee6 100644 --- a/native/jax_i18n_core/Cargo.lock +++ b/native/localesmith_core/Cargo.lock @@ -99,7 +99,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "jax_i18n_core" +name = "localesmith_core" version = "0.1.0" dependencies = [ "flate2", diff --git a/native/jax_i18n_core/Cargo.toml b/native/localesmith_core/Cargo.toml similarity index 82% rename from native/jax_i18n_core/Cargo.toml rename to native/localesmith_core/Cargo.toml index 45edb5a..c7fa673 100644 --- a/native/jax_i18n_core/Cargo.toml +++ b/native/localesmith_core/Cargo.toml @@ -1,12 +1,13 @@ [package] -name = "jax_i18n_core" +name = "localesmith_core" version = "0.1.0" edition = "2024" rust-version = "1.85" publish = false -description = "Panic-safe native archive inspection core for JAX i18n" +description = "Panic-safe native archive inspection core for LocaleSmith" [lib] +name = "localesmith_core" crate-type = ["cdylib", "rlib"] [dependencies] diff --git a/native/localesmith_core/include/localesmith_core.h b/native/localesmith_core/include/localesmith_core.h new file mode 100644 index 0000000..47ed3c7 --- /dev/null +++ b/native/localesmith_core/include/localesmith_core.h @@ -0,0 +1,63 @@ +#ifndef LOCALESMITH_CORE_H +#define LOCALESMITH_CORE_H + +#include + +#if defined(_WIN32) +# define LOCALESMITH_API __declspec(dllimport) +#else +# define LOCALESMITH_API +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* Stable return/error codes. */ +typedef enum localesmith_error_code { + LOCALESMITH_OK = 0, + LOCALESMITH_ERROR_NULL_POINTER = 1, + LOCALESMITH_ERROR_INVALID_UTF8 = 2, + LOCALESMITH_ERROR_IO = 3, + LOCALESMITH_ERROR_INVALID_ARCHIVE = 4, + LOCALESMITH_ERROR_LIMIT_EXCEEDED = 5, + LOCALESMITH_ERROR_UNSAFE_ARCHIVE_PATH = 6, + LOCALESMITH_ERROR_SERIALIZATION = 7, + LOCALESMITH_ERROR_PANIC = 8, + LOCALESMITH_ERROR_INVALID_ARGUMENT = 9 +} localesmith_error_code; + +/* Process-lifetime UTF-8 string. Do not free. */ +LOCALESMITH_API const char *localesmith_core_version(void); + +/* Current JSON manifest schema version. */ +LOCALESMITH_API uint32_t localesmith_manifest_schema_version(void); + +/* + * Scan one ZIP/JAR path encoded as NUL-terminated UTF-8. + * + * On success, returns LOCALESMITH_OK and stores a newly allocated UTF-8 JSON + * string in *out_json_utf8. Release it once with localesmith_string_free. + * On failure, *out_json_utf8 is null and the thread-local error is updated. + */ +LOCALESMITH_API int32_t localesmith_scan_archive_json( + const char *path_utf8, + char **out_json_utf8); + +/* Free a non-static string returned by this library. Null is accepted. */ +LOCALESMITH_API void localesmith_string_free(char *value); + +/* Calling-thread error code from the most recent scan call. */ +LOCALESMITH_API int32_t localesmith_last_error_code(void); + +/* + * Copy the calling-thread error message into a newly allocated UTF-8 string. + * Release the result once with localesmith_string_free. + */ +LOCALESMITH_API char *localesmith_last_error_message(void); + +#ifdef __cplusplus +} +#endif + +#endif /* LOCALESMITH_CORE_H */ diff --git a/native/jax_i18n_core/src/classfile.rs b/native/localesmith_core/src/classfile.rs similarity index 100% rename from native/jax_i18n_core/src/classfile.rs rename to native/localesmith_core/src/classfile.rs diff --git a/native/jax_i18n_core/src/error.rs b/native/localesmith_core/src/error.rs similarity index 100% rename from native/jax_i18n_core/src/error.rs rename to native/localesmith_core/src/error.rs diff --git a/native/jax_i18n_core/src/ffi.rs b/native/localesmith_core/src/ffi.rs similarity index 87% rename from native/jax_i18n_core/src/ffi.rs rename to native/localesmith_core/src/ffi.rs index 08d3f86..2060e30 100644 --- a/native/jax_i18n_core/src/ffi.rs +++ b/native/localesmith_core/src/ffi.rs @@ -32,15 +32,15 @@ thread_local! { /// Return the library version as a process-lifetime UTF-8 C string. /// /// The returned pointer is static and must not be passed to -/// [`jax_i18n_string_free`]. +/// [`localesmith_string_free`]. #[unsafe(no_mangle)] -pub extern "C" fn jax_i18n_core_version() -> *const c_char { +pub extern "C" fn localesmith_core_version() -> *const c_char { VERSION_C_STRING.as_ptr().cast() } /// Return the current JSON manifest schema version. #[unsafe(no_mangle)] -pub extern "C" fn jax_i18n_manifest_schema_version() -> u32 { +pub extern "C" fn localesmith_manifest_schema_version() -> u32 { MANIFEST_SCHEMA_VERSION } @@ -55,9 +55,9 @@ pub extern "C" fn jax_i18n_manifest_schema_version() -> u32 { /// duration of this call. /// - `out_json_utf8` must point to writable storage for one `char *`. /// - On success the caller owns `*out_json_utf8` and must release it exactly -/// once with [`jax_i18n_string_free`]. +/// once with [`localesmith_string_free`]. #[unsafe(no_mangle)] -pub unsafe extern "C" fn jax_i18n_scan_archive_json( +pub unsafe extern "C" fn localesmith_scan_archive_json( path_utf8: *const c_char, out_json_utf8: *mut *mut c_char, ) -> i32 { @@ -90,7 +90,7 @@ pub unsafe extern "C" fn jax_i18n_scan_archive_json( /// already been freed. Passing any other pointer or freeing twice is undefined /// behavior. #[unsafe(no_mangle)] -pub unsafe extern "C" fn jax_i18n_string_free(value: *mut c_char) { +pub unsafe extern "C" fn localesmith_string_free(value: *mut c_char) { if value.is_null() { return; } @@ -101,17 +101,17 @@ pub unsafe extern "C" fn jax_i18n_string_free(value: *mut c_char) { /// Return the calling thread's most recent error code. #[unsafe(no_mangle)] -pub extern "C" fn jax_i18n_last_error_code() -> i32 { +pub extern "C" fn localesmith_last_error_code() -> i32 { LAST_ERROR.with(|slot| slot.borrow().code as i32) } /// Copy the calling thread's most recent error message into a new C string. /// /// The returned value is never the version's static pointer. It must be freed -/// with [`jax_i18n_string_free`]. A null result means allocation/conversion +/// with [`localesmith_string_free`]. A null result means allocation/conversion /// panicked; no panic crosses the ABI. #[unsafe(no_mangle)] -pub extern "C" fn jax_i18n_last_error_message() -> *mut c_char { +pub extern "C" fn localesmith_last_error_message() -> *mut c_char { catch_unwind(|| { let message = LAST_ERROR.with(|slot| slot.borrow().message.clone()); let without_nuls = message.replace('\0', "\\0"); @@ -196,8 +196,8 @@ fn panic_message(payload: Box) -> String { #[cfg(test)] mod tests { use super::{ - jax_i18n_core_version, jax_i18n_last_error_code, jax_i18n_last_error_message, - jax_i18n_scan_archive_json, jax_i18n_string_free, + localesmith_core_version, localesmith_last_error_code, localesmith_last_error_message, + localesmith_scan_archive_json, localesmith_string_free, }; use crate::error::ErrorCode; use std::ffi::CStr; @@ -205,7 +205,7 @@ mod tests { #[test] fn version_is_static_utf8() { - let pointer = jax_i18n_core_version(); + let pointer = localesmith_core_version(); assert!(!pointer.is_null()); // SAFETY: The version API promises a process-lifetime NUL-terminated // pointer and this test does not mutate or free it. @@ -218,18 +218,18 @@ mod tests { let mut output = ptr::null_mut(); // SAFETY: `output` is valid writable pointer storage; a null input path // is explicitly handled by the API. - let code = unsafe { jax_i18n_scan_archive_json(ptr::null(), &mut output) }; + let code = unsafe { localesmith_scan_archive_json(ptr::null(), &mut output) }; assert_eq!(code, ErrorCode::NullPointer as i32); - assert_eq!(jax_i18n_last_error_code(), ErrorCode::NullPointer as i32); + assert_eq!(localesmith_last_error_code(), ErrorCode::NullPointer as i32); assert!(output.is_null()); - let message = jax_i18n_last_error_message(); + let message = localesmith_last_error_message(); assert!(!message.is_null()); // SAFETY: `message` came from the library and remains owned until the // matching free directly below. let text = unsafe { CStr::from_ptr(message) }.to_string_lossy(); assert!(text.contains("path_utf8")); // SAFETY: The pointer was returned by this library and is freed once. - unsafe { jax_i18n_string_free(message) }; + unsafe { localesmith_string_free(message) }; } } diff --git a/native/jax_i18n_core/src/lib.rs b/native/localesmith_core/src/lib.rs similarity index 91% rename from native/jax_i18n_core/src/lib.rs rename to native/localesmith_core/src/lib.rs index fed2fd1..d659cef 100644 --- a/native/jax_i18n_core/src/lib.rs +++ b/native/localesmith_core/src/lib.rs @@ -1,4 +1,4 @@ -//! Native, panic-contained ZIP/JAR inspection for the JAX i18n desktop app. +//! Native, panic-contained ZIP/JAR inspection for the LocaleSmith desktop app. //! //! The crate deliberately performs no extraction or repacking. It validates //! archive paths, enforces declared-size limits, inventories central-directory diff --git a/native/jax_i18n_core/src/metadata.rs b/native/localesmith_core/src/metadata.rs similarity index 100% rename from native/jax_i18n_core/src/metadata.rs rename to native/localesmith_core/src/metadata.rs diff --git a/native/jax_i18n_core/src/model.rs b/native/localesmith_core/src/model.rs similarity index 100% rename from native/jax_i18n_core/src/model.rs rename to native/localesmith_core/src/model.rs diff --git a/native/jax_i18n_core/src/path_safety.rs b/native/localesmith_core/src/path_safety.rs similarity index 100% rename from native/jax_i18n_core/src/path_safety.rs rename to native/localesmith_core/src/path_safety.rs diff --git a/native/jax_i18n_core/src/scanner.rs b/native/localesmith_core/src/scanner.rs similarity index 100% rename from native/jax_i18n_core/src/scanner.rs rename to native/localesmith_core/src/scanner.rs diff --git a/native/jax_i18n_core/tests/archive_scan.rs b/native/localesmith_core/tests/archive_scan.rs similarity index 96% rename from native/jax_i18n_core/tests/archive_scan.rs rename to native/localesmith_core/tests/archive_scan.rs index 29d24e3..23f1a93 100644 --- a/native/jax_i18n_core/tests/archive_scan.rs +++ b/native/localesmith_core/tests/archive_scan.rs @@ -7,9 +7,9 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use jax_i18n_core::{ +use localesmith_core::{ CoreError, ErrorCode, ModLoader, ResourceKind, ScanLimits, SignatureEvidenceStatus, - ffi::{jax_i18n_last_error_code, jax_i18n_scan_archive_json, jax_i18n_string_free}, + ffi::{localesmith_last_error_code, localesmith_scan_archive_json, localesmith_string_free}, scan_archive, scan_archive_with_limits, }; use zip::{ @@ -32,7 +32,7 @@ impl TestArchive { .unwrap() .as_nanos(); let directory = std::env::temp_dir().join(format!( - "jax-i18n-core-test-{}-{counter}-{nanos}", + "localesmith-core-test-{}-{counter}-{nanos}", std::process::id() )); fs::create_dir_all(&directory).unwrap(); @@ -416,9 +416,9 @@ fn c_abi_returns_owned_json_and_reports_errors() { // SAFETY: `path` is a live NUL-terminated string and `output` is writable // pointer storage. The returned allocation is freed once below. - let code = unsafe { jax_i18n_scan_archive_json(path.as_ptr(), &mut output) }; + let code = unsafe { localesmith_scan_archive_json(path.as_ptr(), &mut output) }; assert_eq!(code, ErrorCode::Ok as i32); - assert_eq!(jax_i18n_last_error_code(), ErrorCode::Ok as i32); + assert_eq!(localesmith_last_error_code(), ErrorCode::Ok as i32); assert!(!output.is_null()); // SAFETY: On success the ABI guarantees a live NUL-terminated allocation. let json = unsafe { CStr::from_ptr(output) }.to_str().unwrap(); @@ -426,17 +426,17 @@ fn c_abi_returns_owned_json_and_reports_errors() { assert_eq!(value["schema_version"], 1); assert_eq!(value["mod_metadata"]["primary_mod_id"], "ffi_demo"); // SAFETY: The allocation was returned by this library and is freed once. - unsafe { jax_i18n_string_free(output) }; + unsafe { localesmith_string_free(output) }; let missing = CString::new(path_to_utf8(&archive.directory.join("missing.jar"))).unwrap(); let mut failed_output = std::ptr::null_mut(); // SAFETY: Both pointers satisfy the ABI storage/lifetime contract. - let code = unsafe { jax_i18n_scan_archive_json(missing.as_ptr(), &mut failed_output) }; + let code = unsafe { localesmith_scan_archive_json(missing.as_ptr(), &mut failed_output) }; assert_eq!(code, ErrorCode::Io as i32); assert!(failed_output.is_null()); } -fn assert_limit_error(result: Result) { +fn assert_limit_error(result: Result) { assert!(matches!(result, Err(CoreError::LimitExceeded { .. }))); } diff --git a/package.json b/package.json index a784261..c5423aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@jax/locale-smith", + "name": "@localesmith/workspace", "version": "0.1.0", "private": true, - "description": "LocaleSmith (译匠) desktop localization workspace" + "description": "LocaleSmith desktop localization workspace" } diff --git a/packaging/JaxI18n.Package/Assets/AppIconMaster.png b/packaging/LocaleSmith.Package/Assets/AppIconMaster.png similarity index 100% rename from packaging/JaxI18n.Package/Assets/AppIconMaster.png rename to packaging/LocaleSmith.Package/Assets/AppIconMaster.png diff --git a/packaging/JaxI18n.Package/Assets/Square150x150Logo.png b/packaging/LocaleSmith.Package/Assets/Square150x150Logo.png similarity index 100% rename from packaging/JaxI18n.Package/Assets/Square150x150Logo.png rename to packaging/LocaleSmith.Package/Assets/Square150x150Logo.png diff --git a/packaging/JaxI18n.Package/Assets/Square44x44Logo.png b/packaging/LocaleSmith.Package/Assets/Square44x44Logo.png similarity index 100% rename from packaging/JaxI18n.Package/Assets/Square44x44Logo.png rename to packaging/LocaleSmith.Package/Assets/Square44x44Logo.png diff --git a/packaging/JaxI18n.Package/Assets/StoreLogo.png b/packaging/LocaleSmith.Package/Assets/StoreLogo.png similarity index 100% rename from packaging/JaxI18n.Package/Assets/StoreLogo.png rename to packaging/LocaleSmith.Package/Assets/StoreLogo.png diff --git a/packaging/JaxI18n.Package/Assets/Wide310x150Logo.png b/packaging/LocaleSmith.Package/Assets/Wide310x150Logo.png similarity index 100% rename from packaging/JaxI18n.Package/Assets/Wide310x150Logo.png rename to packaging/LocaleSmith.Package/Assets/Wide310x150Logo.png diff --git a/packaging/JaxI18n.Package/JaxI18n.Package.wapproj b/packaging/LocaleSmith.Package/LocaleSmith.Package.wapproj similarity index 90% rename from packaging/JaxI18n.Package/JaxI18n.Package.wapproj rename to packaging/LocaleSmith.Package/LocaleSmith.Package.wapproj index 76a2fdd..3e3213f 100644 --- a/packaging/JaxI18n.Package/JaxI18n.Package.wapproj +++ b/packaging/LocaleSmith.Package/LocaleSmith.Package.wapproj @@ -28,17 +28,17 @@ zh-CN false Never - ..\..\src\JaxI18n.App\JaxI18n.App.csproj + ..\..\src\LocaleSmith.App\LocaleSmith.App.csproj x64 - + true Properties\PublishProfiles\win-x64.pubxml - + true Properties\PublishProfiles\win-x64.pubxml diff --git a/packaging/JaxI18n.Package/Package.appxmanifest b/packaging/LocaleSmith.Package/Package.appxmanifest similarity index 80% rename from packaging/JaxI18n.Package/Package.appxmanifest rename to packaging/LocaleSmith.Package/Package.appxmanifest index ee4e86a..63c9760 100644 --- a/packaging/JaxI18n.Package/Package.appxmanifest +++ b/packaging/LocaleSmith.Package/Package.appxmanifest @@ -5,9 +5,9 @@ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" IgnorableNamespaces="uap rescap"> ms-resource:PackageDisplayName @@ -20,6 +20,10 @@ + diff --git a/packaging/JaxI18n.Package/Strings/en-US/Resources.resw b/packaging/LocaleSmith.Package/Strings/en-US/Resources.resw similarity index 95% rename from packaging/JaxI18n.Package/Strings/en-US/Resources.resw rename to packaging/LocaleSmith.Package/Strings/en-US/Resources.resw index 60326c9..13b53a4 100644 --- a/packaging/JaxI18n.Package/Strings/en-US/Resources.resw +++ b/packaging/LocaleSmith.Package/Strings/en-US/Resources.resw @@ -5,7 +5,7 @@ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 LocaleSmith - LocaleSmith Development + CR Tech LocaleSmith LocaleSmith — Minecraft mod and resource-pack localization workspace diff --git a/packaging/JaxI18n.Package/Strings/zh-CN/Resources.resw b/packaging/LocaleSmith.Package/Strings/zh-CN/Resources.resw similarity index 80% rename from packaging/JaxI18n.Package/Strings/zh-CN/Resources.resw rename to packaging/LocaleSmith.Package/Strings/zh-CN/Resources.resw index 1bf585b..c40537f 100644 --- a/packaging/JaxI18n.Package/Strings/zh-CN/Resources.resw +++ b/packaging/LocaleSmith.Package/Strings/zh-CN/Resources.resw @@ -4,8 +4,8 @@ 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - LocaleSmith(译匠) - LocaleSmith(译匠)开发版 - LocaleSmith(译匠) - LocaleSmith(译匠)— Minecraft 模组与资源包本地化工作台 + LocaleSmith | 译匠 + CR Tech + LocaleSmith | 译匠 + LocaleSmith | 译匠 — Minecraft 模组与资源包本地化工作台 diff --git a/src/JaxI18n.App/JaxI18n.App.csproj b/src/JaxI18n.App/JaxI18n.App.csproj deleted file mode 100644 index d930fc7..0000000 --- a/src/JaxI18n.App/JaxI18n.App.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - WinExe - net10.0-windows10.0.19041.0 - 10.0.17763.0 - JaxI18n.App - JaxI18n.App - LocaleSmith(译匠) - LocaleSmith(译匠) - LocaleSmith(译匠)— Minecraft 模组与资源包本地化工作台 - app.manifest - x64 - x64 - win-x64 - true - None - true - zh-CN - $(MSBuildThisFileDirectory)..\..\native\jax_i18n_core\target\release\jax_i18n_core.dll - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/JaxI18n.App/Pages/DashboardPage.xaml b/src/JaxI18n.App/Pages/DashboardPage.xaml deleted file mode 100644 index 49f2f78..0000000 --- a/src/JaxI18n.App/Pages/DashboardPage.xaml +++ /dev/null @@ -1,441 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/JaxI18n.App/Pages/ModelSourcesPage.xaml.cs b/src/JaxI18n.App/Pages/ModelSourcesPage.xaml.cs deleted file mode 100644 index a028c1f..0000000 --- a/src/JaxI18n.App/Pages/ModelSourcesPage.xaml.cs +++ /dev/null @@ -1,48 +0,0 @@ -using JaxI18n.Presentation.ViewModels; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; - -namespace JaxI18n.App.Pages; - -public sealed partial class ModelSourcesPage : Page -{ - private ModelSourcesViewModel ViewModel { get; } - private bool _loaded; - - public ModelSourcesPage() - { - InitializeComponent(); - ViewModel = App.Services.GetRequiredService(); - ViewModel.SecretInputConsumed += OnSecretInputConsumed; - DataContext = ViewModel; - Loaded += OnLoaded; - } - - private async void OnLoaded(object sender, RoutedEventArgs args) - { - if (_loaded) - { - return; - } - - _loaded = true; - await ViewModel.LoadAsync().ConfigureAwait(true); - } - - private void OnSecretInputConsumed(object? sender, EventArgs args) => ApiKeyInput.Password = string.Empty; - - private async void OnDeleteClicked(object sender, RoutedEventArgs args) - { - if (ViewModel.SelectedSource is null) - { - return; - } - - var result = await DeleteDialog.ShowAsync(); - if (result == ContentDialogResult.Primary) - { - await ViewModel.DeleteSelectedAsync().ConfigureAwait(true); - } - } -} diff --git a/src/JaxI18n.App/Pages/OnboardingPage.xaml.cs b/src/JaxI18n.App/Pages/OnboardingPage.xaml.cs deleted file mode 100644 index 1650b84..0000000 --- a/src/JaxI18n.App/Pages/OnboardingPage.xaml.cs +++ /dev/null @@ -1,63 +0,0 @@ -using JaxI18n.Presentation.Models; -using JaxI18n.Presentation.ViewModels; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; - -namespace JaxI18n.App.Pages; - -public sealed partial class OnboardingPage : Page -{ - private OnboardingViewModel ViewModel { get; } - private bool _secretEventSubscribed; - - public OnboardingPage() - { - InitializeComponent(); - ViewModel = App.Services.GetRequiredService(); - DataContext = ViewModel; - Loaded += OnLoaded; - Unloaded += OnUnloaded; - } - - private void OnLoaded(object sender, RoutedEventArgs args) - { - if (!_secretEventSubscribed) - { - ViewModel.SecretInputConsumed += OnSecretInputConsumed; - _secretEventSubscribed = true; - } - } - - private void OnUnloaded(object sender, RoutedEventArgs args) - { - if (_secretEventSubscribed) - { - ViewModel.SecretInputConsumed -= OnSecretInputConsumed; - _secretEventSubscribed = false; - } - } - - private void OnModelPathChecked(object sender, RoutedEventArgs args) - { - if (sender is RadioButton { IsChecked: true, Tag: string tag } && - Enum.TryParse(tag, ignoreCase: false, out var path)) - { - ViewModel.SelectModelPath(path); - } - } - - private void OnNetworkApiKeyChanged(object sender, RoutedEventArgs args) - { - if (sender is PasswordBox passwordBox) - { - ViewModel.SetNetworkApiKeyPresent(!string.IsNullOrWhiteSpace(passwordBox.Password)); - } - } - - private void OnSecretInputConsumed(object? sender, EventArgs args) - { - OnboardingNetworkApiKeyInput.Password = string.Empty; - ViewModel.SetNetworkApiKeyPresent(false); - } -} diff --git a/src/JaxI18n.App/Services/FileTranslationMemoryStore.cs b/src/JaxI18n.App/Services/FileTranslationMemoryStore.cs deleted file mode 100644 index ed2b80f..0000000 --- a/src/JaxI18n.App/Services/FileTranslationMemoryStore.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using JaxI18n.Application.Abstractions; -using JaxI18n.Application.Models; - -namespace JaxI18n.App.Services; - -public sealed class FileTranslationMemoryStore : ITranslationMemoryStore, IDisposable -{ - private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web); - private readonly string _directory; - private readonly SemaphoreSlim _gate = new(1, 1); - - public FileTranslationMemoryStore(string directory) - { - ArgumentException.ThrowIfNullOrWhiteSpace(directory); - _directory = Path.GetFullPath(directory); - } - - public async Task LoadAsync( - TranslationMemoryKey key, - CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(key); - var normalized = key.Normalize(); - await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); - try - { - var path = GetPath(normalized); - if (!File.Exists(path)) - { - return TranslationMemorySnapshot.Empty(normalized); - } - - await using var stream = new FileStream( - path, - FileMode.Open, - FileAccess.Read, - FileShare.Read, - 16 * 1024, - FileOptions.Asynchronous | FileOptions.SequentialScan); - return await JsonSerializer - .DeserializeAsync(stream, SerializerOptions, cancellationToken) - .ConfigureAwait(false) - ?? throw new InvalidDataException("The translation-memory snapshot is empty."); - } - catch (JsonException exception) - { - throw new InvalidDataException("The translation-memory snapshot is invalid.", exception); - } - finally - { - _gate.Release(); - } - } - - public async Task SaveAsync( - TranslationMemorySnapshot snapshot, - CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(snapshot); - var path = GetPath(snapshot.Key.Normalize()); - await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); - try - { - Directory.CreateDirectory(_directory); - var temporaryPath = Path.Combine( - _directory, - $".{Path.GetFileName(path)}.{Guid.NewGuid():N}.tmp"); - try - { - await using (var stream = new FileStream( - temporaryPath, - FileMode.CreateNew, - FileAccess.Write, - FileShare.None, - 16 * 1024, - FileOptions.Asynchronous | FileOptions.WriteThrough)) - { - await JsonSerializer - .SerializeAsync(stream, snapshot, SerializerOptions, cancellationToken) - .ConfigureAwait(false); - await stream.FlushAsync(cancellationToken).ConfigureAwait(false); - } - - File.Move(temporaryPath, path, overwrite: true); - } - finally - { - if (File.Exists(temporaryPath)) - { - File.Delete(temporaryPath); - } - } - } - finally - { - _gate.Release(); - } - } - - private string GetPath(TranslationMemoryKey key) - { - var identity = $"{key.PackageIdentity}\0{key.TargetLanguage}"; - var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(identity))).ToLowerInvariant(); - return Path.Combine(_directory, $"{hash}.json"); - } - - public void Dispose() => _gate.Dispose(); -} diff --git a/src/JaxI18n.App/Services/PipelineTranslationQueueService.cs b/src/JaxI18n.App/Services/PipelineTranslationQueueService.cs deleted file mode 100644 index 8229926..0000000 --- a/src/JaxI18n.App/Services/PipelineTranslationQueueService.cs +++ /dev/null @@ -1,113 +0,0 @@ -using JaxI18n.Application.Abstractions; -using JaxI18n.Application.Models; -using JaxI18n.Core.Models; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; - -namespace JaxI18n.App.Services; - -public sealed class PipelineTranslationQueueService : ITranslationQueueService -{ - private readonly IPipelineJobScheduler _scheduler; - - public PipelineTranslationQueueService(IPipelineJobScheduler scheduler) - { - _scheduler = scheduler ?? throw new ArgumentNullException(nameof(scheduler)); - _scheduler.ProgressChanged += OnPipelineProgressChanged; - } - - public event EventHandler? ProgressChanged; - - public async ValueTask EnqueueAsync( - TranslationQueueRequest request, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(request); - var outputDirectory = Path.GetDirectoryName(Path.GetFullPath(request.OutputPath)) - ?? throw new InvalidOperationException("The output path must have a parent directory."); - Directory.CreateDirectory(outputDirectory); - - var pipelineHandle = await _scheduler.EnqueueAsync( - new PipelineRequest( - request.SourcePath, - request.OutputPath, - targetLanguage: "zh_CN", - styles: new HashSet { request.Style }, - // Signed archives remain blocked until a dedicated, explicit signature-choice dialog exists. - signedArchiveHandling: SignedArchiveHandling.Block, - hardcodedStringMode: HardcodedStringMode.ExternalizeRecognizedSafePatterns, - modelSourceId: request.ModelSourceId), - cancellationToken).ConfigureAwait(false); - - return new TranslationQueueHandle( - pipelineHandle.JobId, - ConvertResultAsync(pipelineHandle.Completion, request.Style), - pipelineHandle.Cancel, - () => TranslateProgress(pipelineHandle.LatestProgress)); - } - - private static async Task ConvertResultAsync( - Task completion, - TranslationStyle requestedStyle) - { - var result = await completion.ConfigureAwait(false); - if (result.Artifacts.Count != 1 || result.Artifacts[0].Style != requestedStyle) - { - throw new InvalidDataException( - "A processing-queue job must produce exactly one artifact in its captured translation style."); - } - - return new TranslationQueueResult( - result.JobId, - result.OutputPath, - result.Inspection.ModId, - result.Inspection.Loader, - result.Artifacts.Select(static artifact => artifact.Path).ToArray(), - result.HardcodedCandidates.ToArray(), - result.Externalization.ExternalizedCount, - requestedStyle); - } - - private void OnPipelineProgressChanged(object? sender, PipelineProgress progress) - { - var handlers = ProgressChanged; - if (handlers is null) - { - return; - } - - var translated = TranslateProgress(progress); - if (translated is null) - { - return; - } - - foreach (EventHandler handler in handlers.GetInvocationList()) - { - try - { - handler(this, translated); - } - catch - { - // A view subscriber must not terminate the transactional worker. - } - } - } - - private static TranslationQueueProgress? TranslateProgress(PipelineProgress? progress) - { - if (progress is null) - { - return null; - } - - return new TranslationQueueProgress( - progress.JobId, - progress.Stage, - progress.Fraction, - progress.NextStage, - progress.Stages?.ToArray(), - progress.RollbackStatus); - } -} diff --git a/src/JaxI18n.App/Services/WinUiTextProvider.cs b/src/JaxI18n.App/Services/WinUiTextProvider.cs deleted file mode 100644 index 6c34357..0000000 --- a/src/JaxI18n.App/Services/WinUiTextProvider.cs +++ /dev/null @@ -1,20 +0,0 @@ -using JaxI18n.Presentation.Abstractions; -using Windows.ApplicationModel.Resources; - -namespace JaxI18n.App.Services; - -public sealed class WinUiTextProvider : IUiTextProvider -{ - private readonly ResourceLoader _resourceLoader = ResourceLoader.GetForViewIndependentUse(); - - public string GetText(string key, string fallback, params object?[] arguments) - { - ArgumentException.ThrowIfNullOrWhiteSpace(key); - ArgumentNullException.ThrowIfNull(fallback); - var localized = _resourceLoader.GetString(key); - var template = string.IsNullOrWhiteSpace(localized) ? fallback : localized; - return arguments.Length == 0 - ? template - : string.Format(System.Globalization.CultureInfo.CurrentCulture, template, arguments); - } -} diff --git a/src/JaxI18n.Application/JaxI18n.Application.csproj b/src/JaxI18n.Application/JaxI18n.Application.csproj deleted file mode 100644 index a29f0b8..0000000 --- a/src/JaxI18n.Application/JaxI18n.Application.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net10.0 - JaxI18n.Application - JaxI18n.Application - - - - - - - - - - diff --git a/src/JaxI18n.Archive/JaxI18n.Archive.csproj b/src/JaxI18n.Archive/JaxI18n.Archive.csproj deleted file mode 100644 index 55ae58e..0000000 --- a/src/JaxI18n.Archive/JaxI18n.Archive.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - net10.0-windows10.0.17763.0 - JaxI18n.Archive - JaxI18n.Archive - - - - - - - - - - - diff --git a/src/JaxI18n.Infrastructure/Cli/TrustedCliExecutableDiscovery.cs b/src/JaxI18n.Infrastructure/Cli/TrustedCliExecutableDiscovery.cs deleted file mode 100644 index c266793..0000000 --- a/src/JaxI18n.Infrastructure/Cli/TrustedCliExecutableDiscovery.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace JaxI18n.Infrastructure.Cli; - -/// -/// Discovers the deliberately small set of executables that may be placed on -/// the application's initial CLI allowlist. User PATH entries are never trusted. -/// -public static class TrustedCliExecutableDiscovery -{ - public static IReadOnlyList FindInstalled() - { - if (!OperatingSystem.IsWindows()) - { - return []; - } - - var programFiles = System.Environment.GetFolderPath(System.Environment.SpecialFolder.ProgramFiles); - if (string.IsNullOrWhiteSpace(programFiles)) - { - return []; - } - - // Keep the default capability intentionally narrow. Additional tools must - // be added through the dynamic allowlist after an explicit user decision. - string[] relativeCandidates = [Path.Combine("dotnet", "dotnet.exe")]; - return relativeCandidates - .Select(relative => Path.GetFullPath(Path.Combine(programFiles, relative))) - .Where(path => IsTrustedRegularFile(path, programFiles)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - } - - private static bool IsTrustedRegularFile(string candidate, string programFiles) - { - try - { - var root = Path.TrimEndingDirectorySeparator(Path.GetFullPath(programFiles)); - var path = Path.GetFullPath(candidate); - if (!path.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - var current = root; - foreach (var segment in Path.GetRelativePath(root, path).Split( - [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar], - StringSplitOptions.RemoveEmptyEntries)) - { - current = Path.Combine(current, segment); - var attributes = File.GetAttributes(current); - if ((attributes & FileAttributes.ReparsePoint) != 0) - { - return false; - } - } - - return File.Exists(path) - && (File.GetAttributes(path) & FileAttributes.Directory) == 0; - } - catch (Exception exception) when (exception is IOException - or UnauthorizedAccessException - or ArgumentException - or NotSupportedException) - { - return false; - } - } -} diff --git a/src/JaxI18n.NativeInterop/NativeMethods.cs b/src/JaxI18n.NativeInterop/NativeMethods.cs deleted file mode 100644 index 5cb38ed..0000000 --- a/src/JaxI18n.NativeInterop/NativeMethods.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Runtime.InteropServices; - -namespace JaxI18n.NativeInterop; - -internal static partial class NativeMethods -{ - internal const string LibraryName = "jax_i18n_core"; - - [LibraryImport(LibraryName, EntryPoint = "jax_i18n_core_version")] - internal static partial nint CoreVersion(); - - [LibraryImport(LibraryName, EntryPoint = "jax_i18n_manifest_schema_version")] - internal static partial uint ManifestSchemaVersion(); - - [LibraryImport( - LibraryName, - EntryPoint = "jax_i18n_scan_archive_json", - StringMarshalling = StringMarshalling.Utf8)] - internal static partial int ScanArchiveJson(string pathUtf8, out nint jsonUtf8); - - [LibraryImport(LibraryName, EntryPoint = "jax_i18n_string_free")] - internal static partial void StringFree(nint value); - - [LibraryImport(LibraryName, EntryPoint = "jax_i18n_last_error_code")] - internal static partial int LastErrorCode(); - - [LibraryImport(LibraryName, EntryPoint = "jax_i18n_last_error_message")] - internal static partial nint LastErrorMessage(); -} diff --git a/src/JaxI18n.Presentation/JaxI18n.Presentation.csproj b/src/JaxI18n.Presentation/JaxI18n.Presentation.csproj deleted file mode 100644 index a722e86..0000000 --- a/src/JaxI18n.Presentation/JaxI18n.Presentation.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - net10.0 - JaxI18n.Presentation - JaxI18n.Presentation - - - - - - - - - - - diff --git a/src/JaxI18n.Presentation/Models/AppConfiguration.cs b/src/JaxI18n.Presentation/Models/AppConfiguration.cs deleted file mode 100644 index a1e0264..0000000 --- a/src/JaxI18n.Presentation/Models/AppConfiguration.cs +++ /dev/null @@ -1,108 +0,0 @@ -using JaxI18n.Core.Models; - -namespace JaxI18n.Presentation.Models; - -public enum AppThemePreference -{ - System, - Light, - Dark -} - -public sealed record ModelSourceProfile -{ - public string Id { get; init; } = string.Empty; - - public string DisplayName { get; init; } = string.Empty; - - public ModelProviderKind Provider { get; init; } - - /// - /// Stable editable-default preset metadata. Legacy profiles without this field deserialize as Custom. - /// - public string PresetId { get; init; } = ModelProviderPresets.CustomId; - - /// - /// Optional for backward compatibility. Missing legacy values use the selected preset default, - /// or max_tokens for Custom OpenAI-compatible sources. - /// - public OpenAiTokenLimitParameter? TokenLimitParameter { get; init; } - - public string Endpoint { get; init; } = string.Empty; - - public string ModelName { get; init; } = string.Empty; - - /// A Windows Credential Manager reference. This is never an API key value. - public string? CredentialReference { get; init; } - - /// A non-secret, truncated SHA-256 fingerprint used only to identify a saved credential. - public string? CredentialFingerprint { get; init; } -} - -public sealed record AppConfiguration -{ - public const int CurrentSchemaVersion = 1; - - public int SchemaVersion { get; init; } = CurrentSchemaVersion; - - public bool IsOnboardingComplete { get; init; } - - public string WorkspacePath { get; init; } = string.Empty; - - public string SandboxPath { get; init; } = Path.Combine( - Path.GetTempPath(), - "LocaleSmith", - "Sandbox"); - - public string Language { get; init; } = "zh-CN"; - - public AppThemePreference Theme { get; init; } = AppThemePreference.System; - - /// - /// Allows LocaleSmith's own short transitions to run when Windows UI animations are disabled. - /// System and other application animation preferences are never changed. - /// - public bool ForceAppAnimations { get; init; } - - public string? SelectedModelSourceId { get; init; } - - public IReadOnlyList ModelSources { get; init; } = []; -} - -public sealed record OnboardingSubmission( - string WorkspacePath, - string SandboxPath, - bool ConfigureOllama, - Uri OllamaEndpoint, - string OllamaModelName, - string? NetworkPresetId = null, - Uri? NetworkEndpoint = null, - string? NetworkModelName = null, - ReadOnlyMemory NetworkApiKey = default, - OpenAiTokenLimitParameter? NetworkTokenLimitParameter = null); - -public sealed record ModelSourceDraft( - string? Id, - string DisplayName, - ModelProviderKind Provider, - Uri Endpoint, - string ModelName, - string? CredentialReference, - string PresetId = ModelProviderPresets.CustomId, - OpenAiTokenLimitParameter? TokenLimitParameter = null); - -public sealed record ModelConnectionResult(bool IsSuccessful, string Message) -{ - public static ModelConnectionResult Success(string message) => new(true, message); - - public static ModelConnectionResult Failure(string message) => new(false, message); -} - -public enum ShellSection -{ - Onboarding, - Dashboard, - Assistant, - ModelSources, - Settings -} diff --git a/src/JaxI18n.Presentation/ViewModels/SettingsViewModel.cs b/src/JaxI18n.Presentation/ViewModels/SettingsViewModel.cs deleted file mode 100644 index 68d9545..0000000 --- a/src/JaxI18n.Presentation/ViewModels/SettingsViewModel.cs +++ /dev/null @@ -1,177 +0,0 @@ -using CommunityToolkit.Mvvm.Input; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; - -namespace JaxI18n.Presentation.ViewModels; - -public sealed class SettingsViewModel : ViewModelBase -{ - private readonly IAppConfigurationService _configurationService; - private readonly IUiTextProvider _text; - private readonly ICliDiagnosticRequestFactory? _cliDiagnosticFactory; - private AppConfiguration? _loadedConfiguration; - private string _language = "zh-CN"; - private AppThemePreference _theme; - private bool _forceAppAnimations; - private string _workspacePath = string.Empty; - private string _sandboxPath = string.Empty; - - public SettingsViewModel( - IAppConfigurationService configurationService, - IUiTextProvider? text = null, - ICliDiagnosticRequestFactory? cliDiagnosticFactory = null) - { - _configurationService = configurationService ?? throw new ArgumentNullException(nameof(configurationService)); - _text = text ?? FallbackUiTextProvider.Instance; - _cliDiagnosticFactory = cliDiagnosticFactory; - SaveCommand = new AsyncRelayCommand(SaveAsync, () => !IsBusy); - OpenCliDiagnosticCommand = new AsyncRelayCommand( - OpenCliDiagnosticAsync, - () => _cliDiagnosticFactory is not null && !IsBusy); - } - - public event EventHandler? CliConfirmationRequested; - - public IReadOnlyList LanguageOptions { get; } = ["zh-CN", "en-US"]; - - public IReadOnlyList ThemeOptions { get; } = - Enum.GetValues(); - - public IAsyncRelayCommand SaveCommand { get; } - - public IAsyncRelayCommand OpenCliDiagnosticCommand { get; } - - public string Language - { - get => _language; - set => SetProperty(ref _language, value); - } - - public AppThemePreference Theme - { - get => _theme; - set => SetProperty(ref _theme, value); - } - - public bool ForceAppAnimations - { - get => _forceAppAnimations; - set => SetProperty(ref _forceAppAnimations, value); - } - - public string SandboxPath - { - get => _sandboxPath; - set => SetProperty(ref _sandboxPath, value); - } - - public string WorkspacePath - { - get => _workspacePath; - set => SetProperty(ref _workspacePath, value); - } - - public async Task LoadAsync(CancellationToken cancellationToken = default) - { - IsBusy = true; - try - { - _loadedConfiguration = await _configurationService.LoadAsync(cancellationToken).ConfigureAwait(true); - Language = _loadedConfiguration.Language; - Theme = _loadedConfiguration.Theme; - ForceAppAnimations = _loadedConfiguration.ForceAppAnimations; - WorkspacePath = _loadedConfiguration.WorkspacePath; - SandboxPath = _loadedConfiguration.SandboxPath; - ErrorMessage = null; - } - catch (Exception exception) when (exception is not OperationCanceledException) - { - ErrorMessage = Text("SettingsLoadFailed", "Settings could not be loaded: {0}", exception.Message); - } - finally - { - IsBusy = false; - SaveCommand.NotifyCanExecuteChanged(); - OpenCliDiagnosticCommand.NotifyCanExecuteChanged(); - } - } - - private async Task SaveAsync() - { - if (_loadedConfiguration is null) - { - ErrorMessage = Text("SettingsLoadBeforeSave", "Load settings before saving changes."); - return; - } - - if (string.IsNullOrWhiteSpace(WorkspacePath) || string.IsNullOrWhiteSpace(SandboxPath)) - { - ErrorMessage = Text( - "SettingsWorkspaceAndSandboxRequired", - "Workspace and sandbox paths are required."); - return; - } - - IsBusy = true; - SaveCommand.NotifyCanExecuteChanged(); - OpenCliDiagnosticCommand.NotifyCanExecuteChanged(); - try - { - var updated = _loadedConfiguration with - { - Language = Language, - Theme = Theme, - ForceAppAnimations = ForceAppAnimations, - WorkspacePath = Path.GetFullPath(WorkspacePath), - SandboxPath = Path.GetFullPath(SandboxPath) - }; - await _configurationService.SaveAsync(updated).ConfigureAwait(true); - _loadedConfiguration = updated; - ErrorMessage = null; - StatusMessage = Text("SettingsSaved", "Settings saved securely."); - } - catch (Exception exception) when (exception is not OperationCanceledException) - { - ErrorMessage = Text("SettingsSaveFailed", "Settings could not be saved: {0}", exception.Message); - StatusMessage = null; - } - finally - { - IsBusy = false; - SaveCommand.NotifyCanExecuteChanged(); - OpenCliDiagnosticCommand.NotifyCanExecuteChanged(); - } - } - - private async Task OpenCliDiagnosticAsync() - { - if (_cliDiagnosticFactory is null || string.IsNullOrWhiteSpace(SandboxPath)) - { - ErrorMessage = Text("SettingsSandboxRequired", "A sandbox path is required."); - return; - } - - try - { - var confirmation = await _cliDiagnosticFactory - .CreateAsync(SandboxPath) - .ConfigureAwait(true); - CliConfirmationRequested?.Invoke( - this, - new CliConfirmationRequestedEventArgs(confirmation)); - } - catch (Exception exception) when (exception is not OperationCanceledException) - { - ErrorMessage = Text("CliDiagnosticCreateFailed", "The command review could not be opened: {0}", exception.Message); - } - } - - private string Text(string key, string fallback, params object?[] arguments) => - _text.GetText(key, fallback, arguments); -} - -public sealed class CliConfirmationRequestedEventArgs(CliConfirmationViewModel confirmation) : EventArgs -{ - public CliConfirmationViewModel Confirmation { get; } = - confirmation ?? throw new ArgumentNullException(nameof(confirmation)); -} diff --git a/src/JaxI18n.App/App.xaml b/src/LocaleSmith.App/App.xaml similarity index 87% rename from src/JaxI18n.App/App.xaml rename to src/LocaleSmith.App/App.xaml index 5ee7a10..0880e98 100644 --- a/src/JaxI18n.App/App.xaml +++ b/src/LocaleSmith.App/App.xaml @@ -1,8 +1,8 @@ + xmlns:converters="using:LocaleSmith.App.Converters"> diff --git a/src/JaxI18n.App/App.xaml.cs b/src/LocaleSmith.App/App.xaml.cs similarity index 63% rename from src/JaxI18n.App/App.xaml.cs rename to src/LocaleSmith.App/App.xaml.cs index e274d05..a56ac91 100644 --- a/src/JaxI18n.App/App.xaml.cs +++ b/src/LocaleSmith.App/App.xaml.cs @@ -1,25 +1,25 @@ -using JaxI18n.App.Services; -using JaxI18n.Application.Abstractions; -using JaxI18n.Application.Services; -using JaxI18n.Archive; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Services; -using JaxI18n.Infrastructure.Cli; -using JaxI18n.Infrastructure.Environment; -using JaxI18n.Infrastructure.Models; -using JaxI18n.Infrastructure.Security; -using JaxI18n.Mcp; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; -using JaxI18n.Presentation.Services; -using JaxI18n.Presentation.ViewModels; +using LocaleSmith.App.Services; +using LocaleSmith.Application.Abstractions; +using LocaleSmith.Application.Services; +using LocaleSmith.Archive; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Services; +using LocaleSmith.Infrastructure.Cli; +using LocaleSmith.Infrastructure.Environment; +using LocaleSmith.Infrastructure.Models; +using LocaleSmith.Infrastructure.Security; +using LocaleSmith.Mcp; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; +using LocaleSmith.Presentation.Services; +using LocaleSmith.Presentation.ViewModels; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; -using Windows.Globalization; +using Microsoft.Windows.Globalization; -namespace JaxI18n.App; +namespace LocaleSmith.App; public partial class App : Microsoft.UI.Xaml.Application { @@ -40,11 +40,37 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args) { try { - _host = BuildHost(); + var currentLocalAppData = System.Environment.GetFolderPath( + System.Environment.SpecialFolder.LocalApplicationData); + if (string.IsNullOrWhiteSpace(currentLocalAppData)) + { + throw new InvalidOperationException("The per-user application-data directory is unavailable."); + } + + var currentAppDataRoot = Path.Combine(Path.GetFullPath(currentLocalAppData), "LocaleSmith"); + var currentCredentialStore = new WindowsCredentialSecretStore("LocaleSmith"); + var legacyCredentialStore = new WindowsCredentialSecretStore("JaxI18n"); + var legacyAppDataRoots = await LegacyAppDataMigrationCoordinator + .MigrateAsync( + currentAppDataRoot, + legacyCredentialStore, + currentCredentialStore) + .ConfigureAwait(true); + var legacyTranslationMemoryPaths = legacyAppDataRoots + .Select(static root => Path.Combine(root, "translation-memory")) + .ToArray(); + + _host = BuildHost( + currentAppDataRoot, + currentCredentialStore, + legacyCredentialStore, + legacyTranslationMemoryPaths); await _host.StartAsync().ConfigureAwait(true); var state = _host.Services.GetRequiredService(); await state.InitializeAsync().ConfigureAwait(true); var configuration = await state.LoadAsync().ConfigureAwait(true); + // This unpackaged app must set its process-wide MRT Core language before DI resolves + // WinUiTextProvider or MainWindow and their localized resources are constructed. ApplicationLanguages.PrimaryLanguageOverride = configuration.Language; _host.Services.GetRequiredService() .SetForceAppAnimations(configuration.ForceAppAnimations); @@ -61,28 +87,30 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args) } } - private static IHost BuildHost() + private static IHost BuildHost( + string appDataRoot, + WindowsCredentialSecretStore currentCredentialStore, + WindowsCredentialSecretStore legacyCredentialStore, + IReadOnlyList legacyTranslationMemoryPaths) { var builder = Host.CreateApplicationBuilder(); - var appDataRoot = Path.Combine( - System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData), - "JaxI18n"); - var configPath = Path.Combine(appDataRoot, "settings.jaxcfg"); + var configPath = Path.Combine( + appDataRoot, + LegacyAppDataMigrator.CurrentConfigurationFileName); var translationMemoryPath = Path.Combine(appDataRoot, "translation-memory"); var auditPath = Path.Combine(appDataRoot, "logs", "cli-audit.jsonl"); - var defaultSandbox = Path.Combine(Path.GetTempPath(), "LocaleSmith", "Sandbox"); - Directory.CreateDirectory(defaultSandbox); + var defaultSandbox = CliSandboxDirectory.CreateUnderAppDataRoot(appDataRoot); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(static services => - services.GetRequiredService()); + builder.Services.AddSingleton(currentCredentialStore); + builder.Services.AddSingleton(_ => + new MigratingSecretStore(currentCredentialStore, legacyCredentialStore)); builder.Services.AddSingleton(); builder.Services.AddSingleton>(services => new EncryptedJsonConfigurationStore( configPath, - "JaxI18n.ApplicationSettings.v1", + LegacyAppDataMigrator.CurrentConfigurationPurpose, services.GetRequiredService())); builder.Services.AddSingleton(SafeModelHttpClientFactory.Create()); builder.Services.AddSingleton(); @@ -100,10 +128,11 @@ private static IHost BuildHost() builder.Services.AddSingleton(); builder.Services.AddSingleton(_ => - new FileTranslationMemoryStore(translationMemoryPath)); + new FileTranslationMemoryStore(translationMemoryPath, legacyTranslationMemoryPaths)); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -117,10 +146,15 @@ private static IHost BuildHost() builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(_ => new JsonLinesCliAuditSink(auditPath)); - builder.Services.AddSingleton(_ => new SafeCliCommandPolicy( - TrustedCliExecutableDiscovery.FindInstalled(), - [defaultSandbox], - maximumTimeout: TimeSpan.FromSeconds(30))); + builder.Services.AddSingleton(_ => + { + var validatedSandbox = CliSandboxDirectory.ValidateExisting(appDataRoot, defaultSandbox); + return new SafeCliCommandPolicy( + TrustedCliExecutableDiscovery.FindInstalled(), + [validatedSandbox], + temporaryRoot: validatedSandbox, + maximumTimeout: TimeSpan.FromSeconds(30)); + }); builder.Services.AddSingleton(static services => services.GetRequiredService()); builder.Services.AddSingleton(static services => @@ -138,14 +172,12 @@ private static IHost BuildHost() _.GetRequiredService(), auditPath, _.GetRequiredService())); - builder.Services.AddSingleton(static services => - services.GetRequiredService()); - builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); return builder.Build(); @@ -159,6 +191,22 @@ private void OnMainWindowClosed(object sender, WindowEventArgs args) return; } + using (var flushTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(8))) + { + try + { + host.Services + .GetRequiredService() + .FlushPendingChangesAsync(flushTimeout.Token) + .GetAwaiter() + .GetResult(); + } + catch (OperationCanceledException) + { + // A stalled settings write must not keep the application process alive indefinitely. + } + } + ShutdownHostAsync(host).GetAwaiter().GetResult(); } diff --git a/src/JaxI18n.App/Behaviors/AppMotion.cs b/src/LocaleSmith.App/Behaviors/AppMotion.cs similarity index 99% rename from src/JaxI18n.App/Behaviors/AppMotion.cs rename to src/LocaleSmith.App/Behaviors/AppMotion.cs index 5187785..12d2a66 100644 --- a/src/JaxI18n.App/Behaviors/AppMotion.cs +++ b/src/LocaleSmith.App/Behaviors/AppMotion.cs @@ -1,11 +1,11 @@ using System.Runtime.CompilerServices; -using JaxI18n.App.Services; +using LocaleSmith.App.Services; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Input; -namespace JaxI18n.App.Behaviors; +namespace LocaleSmith.App.Behaviors; /// Opt-in hooks for consistent app-owned reveal, status, expand, and press feedback. public static class AppMotion diff --git a/src/JaxI18n.App/Converters/BooleanToVisibilityConverter.cs b/src/LocaleSmith.App/Converters/BooleanToVisibilityConverter.cs similarity index 94% rename from src/JaxI18n.App/Converters/BooleanToVisibilityConverter.cs rename to src/LocaleSmith.App/Converters/BooleanToVisibilityConverter.cs index 64e697a..dd4c7c2 100644 --- a/src/JaxI18n.App/Converters/BooleanToVisibilityConverter.cs +++ b/src/LocaleSmith.App/Converters/BooleanToVisibilityConverter.cs @@ -1,7 +1,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Data; -namespace JaxI18n.App.Converters; +namespace LocaleSmith.App.Converters; public sealed class BooleanToVisibilityConverter : IValueConverter { diff --git a/src/LocaleSmith.App/Converters/TranslationLogViewModeDisplayConverter.cs b/src/LocaleSmith.App/Converters/TranslationLogViewModeDisplayConverter.cs new file mode 100644 index 0000000..fc6ffca --- /dev/null +++ b/src/LocaleSmith.App/Converters/TranslationLogViewModeDisplayConverter.cs @@ -0,0 +1,41 @@ +using Microsoft.UI.Xaml.Data; +using Microsoft.Windows.ApplicationModel.Resources; + +namespace LocaleSmith.App.Converters; + +public sealed class TranslationLogViewModeDisplayConverter : IValueConverter +{ + private readonly Func _getResourceString; + + public TranslationLogViewModeDisplayConverter() + : this(new ResourceLoader().GetString) + { + } + + internal TranslationLogViewModeDisplayConverter(Func getResourceString) + { + _getResourceString = getResourceString ?? throw new ArgumentNullException(nameof(getResourceString)); + } + + public object Convert(object value, Type targetType, object parameter, string language) + { + var key = value?.ToString() switch + { + "Debug" => "LogViewModeDebug", + "AllLevels" => "LogViewModeAllLevels", + _ => null + }; + if (key is null) + { + return value?.ToString() ?? string.Empty; + } + + var localized = _getResourceString(key); + return string.IsNullOrWhiteSpace(localized) + ? key == "LogViewModeDebug" ? "Debug" : "All levels" + : localized; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) => + throw new NotSupportedException(); +} diff --git a/src/JaxI18n.App/Dialogs/CliConfirmationDialog.xaml b/src/LocaleSmith.App/Dialogs/CliConfirmationDialog.xaml similarity index 98% rename from src/JaxI18n.App/Dialogs/CliConfirmationDialog.xaml rename to src/LocaleSmith.App/Dialogs/CliConfirmationDialog.xaml index e8aa4a4..47ebcb7 100644 --- a/src/JaxI18n.App/Dialogs/CliConfirmationDialog.xaml +++ b/src/LocaleSmith.App/Dialogs/CliConfirmationDialog.xaml @@ -1,9 +1,9 @@ + + WinExe + net10.0-windows10.0.19041.0 + 10.0.17763.0 + LocaleSmith.App + LocaleSmith.App + LocaleSmith | 译匠 + LocaleSmith | 译匠 + LocaleSmith | 译匠 — Minecraft 模组与资源包本地化工作台 + app.manifest + x64 + x64 + win-x64 + true + None + true + zh-CN + $(MSBuildThisFileDirectory)..\..\native\localesmith_core\target\release\localesmith_core.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/JaxI18n.App/MainWindow.xaml b/src/LocaleSmith.App/MainWindow.xaml similarity index 90% rename from src/JaxI18n.App/MainWindow.xaml rename to src/LocaleSmith.App/MainWindow.xaml index 05f36f9..2596042 100644 --- a/src/JaxI18n.App/MainWindow.xaml +++ b/src/LocaleSmith.App/MainWindow.xaml @@ -1,9 +1,9 @@ + xmlns:local="using:LocaleSmith.App" + Title="LocaleSmith | 译匠"> + + + + + ElementTheme.Light, AppThemePreference.Dark => ElementTheme.Dark, _ => ElementTheme.Default }; - RootSurface.DispatcherQueue.TryEnqueue(SynchronizeWindowChrome); + RootSurface.RequestedTheme = requestedTheme; + RootNavigation.RequestedTheme = requestedTheme; + QueueWindowChromeSynchronization(); } private void OnNavigationRequested(object? sender, ShellSection section) { - RootNavigation.IsPaneVisible = section != ShellSection.Onboarding; - var pageType = section switch - { - ShellSection.Onboarding => typeof(OnboardingPage), - ShellSection.Dashboard => typeof(DashboardPage), - ShellSection.Assistant => typeof(AssistantPage), - ShellSection.ModelSources => typeof(ModelSourcesPage), - ShellSection.Settings => typeof(SettingsPage), - _ => typeof(DashboardPage) - }; - if (ContentFrame.CurrentSourcePageType == pageType) + var target = ShellNavigationMap.GetTarget(section); + if (ContentFrame.CurrentSourcePageType == target.PageType) { + ApplyNavigationPresentation(section); return; } @@ -83,11 +82,19 @@ private void OnNavigationRequested(object? sender, ShellSection section) _motion?.Cancel(outgoingPage); } - ContentFrame.Navigate(pageType, null, new SuppressNavigationTransitionInfo()); + if (!ContentFrame.Navigate(target.PageType, null, new SuppressNavigationTransitionInfo())) + { + SynchronizeNavigationPresentationWithFrame(); + } } private void OnContentFrameNavigated(object sender, NavigationEventArgs args) { + if (ShellNavigationMap.TryGetSection(args.SourcePageType, out var section)) + { + ApplyNavigationPresentation(section); + } + if (args.Content is FrameworkElement incomingPage) { _motion?.AnimatePageEntrance(incomingPage); @@ -95,10 +102,40 @@ private void OnContentFrameNavigated(object sender, NavigationEventArgs args) } private void OnActualThemeChanged(FrameworkElement sender, object args) => - SynchronizeWindowChrome(); + QueueWindowChromeSynchronization(); private void OnRootSurfaceLoaded(object sender, RoutedEventArgs args) => - SynchronizeWindowChrome(); + QueueWindowChromeSynchronization(); + + private void QueueWindowChromeSynchronization() + { + if (_isClosed || _chromeSynchronizationQueued) + { + return; + } + + _chromeSynchronizationQueued = true; + if (!RootSurface.DispatcherQueue.TryEnqueue( + DispatcherQueuePriority.Low, + () => + { + _chromeSynchronizationQueued = false; + if (!_isClosed) + { + SynchronizeWindowChrome(); + } + })) + { + _chromeSynchronizationQueued = false; + } + } + + private void OnClosedForChromeSynchronization(object sender, WindowEventArgs args) + { + _isClosed = true; + RootSurface.ActualThemeChanged -= OnActualThemeChanged; + Closed -= OnClosedForChromeSynchronization; + } private void SynchronizeWindowChrome() { @@ -111,15 +148,6 @@ TitleBarPressedProbe.Background is not SolidColorBrush pressed || return; } - // NavigationView uses separate brushes for expanded, compact/overlay, and top panes. - // Keep all of them on the same semantic surface as the window and content host. - RootNavigation.Resources["NavigationViewExpandedPaneBackground"] = - new SolidColorBrush(background.Color); - RootNavigation.Resources["NavigationViewDefaultPaneBackground"] = - new SolidColorBrush(background.Color); - RootNavigation.Resources["NavigationViewTopPaneBackground"] = - new SolidColorBrush(background.Color); - if (!AppWindowTitleBar.IsCustomizationSupported()) { return; @@ -158,13 +186,60 @@ private void OnNavigationItemInvoked(NavigationView sender, NavigationViewItemIn if (args.IsSettingsInvoked) { _viewModel.NavigateCommand.Execute(ShellSection.Settings); + SynchronizeNavigationPresentationWithFrame(); return; } - if (args.InvokedItemContainer?.Tag is string tag && - Enum.TryParse(tag, ignoreCase: false, out var section)) + if (ShellNavigationMap.TryGetMenuSection( + args.InvokedItemContainer?.Tag as string, + out var section)) { _viewModel.NavigateCommand.Execute(section); } + + // NavigationView updates its visual selection before the command runs. If navigation is + // temporarily unavailable (for example while encrypted settings are still loading), put + // the selection back on the page that is actually displayed. + SynchronizeNavigationPresentationWithFrame(); + } + + private void SynchronizeNavigationPresentationWithFrame() + { + if (ShellNavigationMap.TryGetSection(ContentFrame.CurrentSourcePageType, out var section)) + { + ApplyNavigationPresentation(section); + } + } + + private void ApplyNavigationPresentation(ShellSection section) + { + var target = ShellNavigationMap.GetTarget(section); + RootNavigation.IsPaneVisible = target.IsPaneVisible; + object? selectedItem = target.Selection switch + { + ShellNavigationSelection.None => null, + ShellNavigationSelection.Settings => RootNavigation.SettingsItem, + ShellNavigationSelection.MenuItem => FindNavigationItem(target.MenuTag), + _ => throw new ArgumentOutOfRangeException(nameof(section), section, "Unknown selection behavior.") + }; + + if (!ReferenceEquals(RootNavigation.SelectedItem, selectedItem)) + { + RootNavigation.SelectedItem = selectedItem; + } + } + + private object? FindNavigationItem(string? tag) + { + foreach (var item in RootNavigation.MenuItems) + { + if (item is NavigationViewItem { Tag: string itemTag } && + string.Equals(itemTag, tag, StringComparison.Ordinal)) + { + return item; + } + } + + return null; } } diff --git a/src/LocaleSmith.App/ModelOptionSelectionMap.cs b/src/LocaleSmith.App/ModelOptionSelectionMap.cs new file mode 100644 index 0000000..678ff36 --- /dev/null +++ b/src/LocaleSmith.App/ModelOptionSelectionMap.cs @@ -0,0 +1,49 @@ +using LocaleSmith.Core.Models; +using LocaleSmith.Presentation.ViewModels; + +namespace LocaleSmith.App; + +internal static class ModelOptionSelectionMap +{ + public static bool TryResolvePreset( + object? selectedItem, + IReadOnlyList availableOptions, + out ModelProviderPreset preset) + { + ArgumentNullException.ThrowIfNull(availableOptions); + if (selectedItem is ModelProviderPreset selectedPreset) + { + var canonical = availableOptions.FirstOrDefault(option => + string.Equals(option.Id, selectedPreset.Id, StringComparison.OrdinalIgnoreCase)); + if (canonical is not null) + { + preset = canonical; + return true; + } + } + + preset = null!; + return false; + } + + public static bool TryResolveTokenLimitParameter( + object? selectedItem, + IReadOnlyList availableOptions, + out TokenLimitParameterOption option) + { + ArgumentNullException.ThrowIfNull(availableOptions); + if (selectedItem is TokenLimitParameterOption selectedOption) + { + var canonical = availableOptions.FirstOrDefault(option => + option.Value == selectedOption.Value); + if (canonical is not null) + { + option = canonical; + return true; + } + } + + option = null!; + return false; + } +} diff --git a/src/JaxI18n.App/Pages/AssistantPage.xaml b/src/LocaleSmith.App/Pages/AssistantPage.xaml similarity index 98% rename from src/JaxI18n.App/Pages/AssistantPage.xaml rename to src/LocaleSmith.App/Pages/AssistantPage.xaml index de27468..1c9187d 100644 --- a/src/JaxI18n.App/Pages/AssistantPage.xaml +++ b/src/LocaleSmith.App/Pages/AssistantPage.xaml @@ -1,8 +1,8 @@ diff --git a/src/JaxI18n.App/Pages/AssistantPage.xaml.cs b/src/LocaleSmith.App/Pages/AssistantPage.xaml.cs similarity index 96% rename from src/JaxI18n.App/Pages/AssistantPage.xaml.cs rename to src/LocaleSmith.App/Pages/AssistantPage.xaml.cs index 49b3dac..e79231b 100644 --- a/src/JaxI18n.App/Pages/AssistantPage.xaml.cs +++ b/src/LocaleSmith.App/Pages/AssistantPage.xaml.cs @@ -1,13 +1,13 @@ using System.Collections.Specialized; -using JaxI18n.App.Dialogs; -using JaxI18n.App.Services; -using JaxI18n.Core.Models; -using JaxI18n.Presentation.ViewModels; +using LocaleSmith.App.Dialogs; +using LocaleSmith.App.Services; +using LocaleSmith.Core.Models; +using LocaleSmith.Presentation.ViewModels; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -namespace JaxI18n.App.Pages; +namespace LocaleSmith.App.Pages; public sealed partial class AssistantPage : Page { diff --git a/src/LocaleSmith.App/Pages/DashboardPage.xaml b/src/LocaleSmith.App/Pages/DashboardPage.xaml new file mode 100644 index 0000000..9662d13 --- /dev/null +++ b/src/LocaleSmith.App/Pages/DashboardPage.xaml @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/LocaleSmith.App/Pages/LogsPage.xaml.cs b/src/LocaleSmith.App/Pages/LogsPage.xaml.cs new file mode 100644 index 0000000..0cd2df6 --- /dev/null +++ b/src/LocaleSmith.App/Pages/LogsPage.xaml.cs @@ -0,0 +1,34 @@ +using LocaleSmith.App.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace LocaleSmith.App.Pages; + +public sealed partial class LogsPage : Page +{ + private TranslationLogsViewModel ViewModel { get; } + + public LogsPage() + { + InitializeComponent(); + ViewModel = App.Services.GetRequiredService(); + DataContext = ViewModel; + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + ViewModel.SetActive(true); + if (ViewModel.RefreshCommand.CanExecute(null)) + { + ViewModel.RefreshCommand.Execute(null); + } + } + + protected override void OnNavigatedFrom(NavigationEventArgs e) + { + ViewModel.SetActive(false); + base.OnNavigatedFrom(e); + } +} diff --git a/src/JaxI18n.App/Pages/ModelSourcesPage.xaml b/src/LocaleSmith.App/Pages/ModelSourcesPage.xaml similarity index 94% rename from src/JaxI18n.App/Pages/ModelSourcesPage.xaml rename to src/LocaleSmith.App/Pages/ModelSourcesPage.xaml index c3c2e6a..5687b33 100644 --- a/src/JaxI18n.App/Pages/ModelSourcesPage.xaml +++ b/src/LocaleSmith.App/Pages/ModelSourcesPage.xaml @@ -1,8 +1,8 @@ @@ -123,7 +123,8 @@ + SelectedItem="{Binding SelectedTokenLimitParameterOption, Mode=OneWay}" + SelectionChanged="OnTokenLimitParameterSelectionChanged" /> - + - - + + + +