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 | 译匠
为 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/JaxI18n.App/Pages/DashboardPage.xaml.cs b/src/LocaleSmith.App/Pages/DashboardPage.xaml.cs
similarity index 96%
rename from src/JaxI18n.App/Pages/DashboardPage.xaml.cs
rename to src/LocaleSmith.App/Pages/DashboardPage.xaml.cs
index ad9b982..9f7ac2f 100644
--- a/src/JaxI18n.App/Pages/DashboardPage.xaml.cs
+++ b/src/LocaleSmith.App/Pages/DashboardPage.xaml.cs
@@ -1,4 +1,4 @@
-using JaxI18n.Presentation.ViewModels;
+using LocaleSmith.Presentation.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -6,7 +6,7 @@
using Windows.Storage.Pickers;
using WinRT.Interop;
-namespace JaxI18n.App.Pages;
+namespace LocaleSmith.App.Pages;
public sealed partial class DashboardPage : Page
{
diff --git a/src/LocaleSmith.App/Pages/LogsPage.xaml b/src/LocaleSmith.App/Pages/LogsPage.xaml
new file mode 100644
index 0000000..92f38c9
--- /dev/null
+++ b/src/LocaleSmith.App/Pages/LogsPage.xaml
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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" />
-
+
-
-
+
+
+
+