From b8fd63df550ccf54b072080c1c5e7a2291679fd1 Mon Sep 17 00:00:00 2001 From: DZXH-TX Date: Mon, 10 Aug 2026 22:08:06 +0800 Subject: [PATCH 1/2] refactor: complete LocaleSmith rename and migration --- JaxI18n.slnx | 23 -- LocaleSmith.slnx | 23 ++ README.md | 46 ++-- native/jax_i18n_core/include/jax_i18n_core.h | 63 ----- .../.gitignore | 0 .../Cargo.lock | 2 +- .../Cargo.toml | 5 +- .../include/localesmith_core.h | 63 +++++ .../src/classfile.rs | 0 .../src/error.rs | 0 .../src/ffi.rs | 32 +-- .../src/lib.rs | 2 +- .../src/metadata.rs | 0 .../src/model.rs | 0 .../src/path_safety.rs | 0 .../src/scanner.rs | 0 .../tests/archive_scan.rs | 16 +- package.json | 4 +- .../Assets/AppIconMaster.png | Bin .../Assets/Square150x150Logo.png | Bin .../Assets/Square44x44Logo.png | Bin .../Assets/StoreLogo.png | Bin .../Assets/Wide310x150Logo.png | Bin .../LocaleSmith.Package.wapproj} | 6 +- .../Package.appxmanifest | 10 +- .../Strings/en-US/Resources.resw | 2 +- .../Strings/zh-CN/Resources.resw | 8 +- src/JaxI18n.App/JaxI18n.App.csproj | 44 --- .../JaxI18n.Application.csproj | 15 - src/JaxI18n.Archive/JaxI18n.Archive.csproj | 19 -- src/JaxI18n.NativeInterop/NativeMethods.cs | 29 -- .../JaxI18n.Presentation.csproj | 16 -- src/{JaxI18n.App => LocaleSmith.App}/App.xaml | 4 +- .../App.xaml.cs | 71 +++-- .../Behaviors/AppMotion.cs | 4 +- .../BooleanToVisibilityConverter.cs | 2 +- .../Dialogs/CliConfirmationDialog.xaml | 4 +- .../Dialogs/CliConfirmationDialog.xaml.cs | 4 +- src/LocaleSmith.App/LocaleSmith.App.csproj | 48 ++++ .../MainWindow.xaml | 6 +- .../MainWindow.xaml.cs | 64 +++-- .../Pages/AssistantPage.xaml | 4 +- .../Pages/AssistantPage.xaml.cs | 10 +- .../Pages/DashboardPage.xaml | 4 +- .../Pages/DashboardPage.xaml.cs | 4 +- .../Pages/ModelSourcesPage.xaml | 4 +- .../Pages/ModelSourcesPage.xaml.cs | 4 +- .../Pages/OnboardingPage.xaml | 4 +- .../Pages/OnboardingPage.xaml.cs | 6 +- .../Pages/SettingsPage.xaml | 4 +- .../Pages/SettingsPage.xaml.cs | 10 +- .../Properties/PublishProfiles/win-x64.pubxml | 0 .../Services/AppMotionService.cs | 4 +- .../CliConfirmationViewModelFactory.cs | 12 +- .../Services/DispatcherQueueUiDispatcher.cs | 4 +- .../Services/FileTranslationMemoryStore.cs | 80 +++++- .../Services/LegacyAppDataLocator.cs | 221 +++++++++++++++ .../Services/LegacyAppDataMigrator.cs | 260 ++++++++++++++++++ .../Services/LegacyDefaultPathNormalizer.cs | 53 ++++ .../Services/ModelAssistantService.cs | 16 +- .../PipelineTranslationQueueService.cs | 12 +- .../Services/SecureAppStateService.cs | 25 +- .../Services/WinUiTextProvider.cs | 4 +- .../Strings/en-US/Resources.resw | 0 .../Strings/zh-CN/Resources.resw | 8 +- .../Themes/Controls.xaml | 2 +- .../app.manifest | 2 +- .../Abstractions/IArchiveWorkspace.cs | 6 +- .../Abstractions/IPipelineJobScheduler.cs | 6 +- .../Abstractions/ITranslationEngine.cs | 4 +- .../Abstractions/ITranslationMemoryStore.cs | 4 +- .../LocaleSmith.Application.csproj | 15 + .../Models/PipelineModels.cs | 4 +- .../Models/TranslationMemoryModels.cs | 26 +- .../PipelineException.cs | 4 +- .../Services/ModelToolOrchestrator.cs | 6 +- .../Services/ModelTranslationEngine.cs | 12 +- .../Services/ModelTranslationEngineOptions.cs | 2 +- .../Services/PipelineJobHandle.cs | 4 +- .../Services/PipelineQueue.cs | 6 +- .../Services/PlaceholderValidator.cs | 2 +- .../Services/TranslationJsonModels.cs | 2 +- .../Services/TranslationPipeline.cs | 10 +- .../TranslationContractException.cs | 2 +- .../ArchiveEntrySnapshot.cs | 2 +- .../ArchivePathSafety.cs | 2 +- .../ArchiveWorkspace.cs | 12 +- .../ArchiveWorkspaceBackend.cs | 8 +- .../ArchiveWorkspaceOptions.cs | 2 +- .../ClassFile/ClassFileBinary.cs | 2 +- .../ClassFile/ClassFileRewriteModels.cs | 2 +- .../ClassFile/JavaBytecodeDecoder.cs | 2 +- ...MojangComponentLiteralClassFileRewriter.cs | 2 +- .../ClassFile/ParsedClassFile.cs | 2 +- .../FolderSnapshotBuilder.cs | 2 +- .../IArchiveScanner.cs | 4 +- .../JsonResourceEditor.cs | 2 +- .../LangResourceEditor.cs | 2 +- .../LocaleSmith.Archive.csproj | 19 ++ .../TransactionJournal.cs | 2 +- .../Abstractions/CliContracts.cs | 4 +- .../Abstractions/IModelService.cs | 4 +- .../Abstractions/IModelServiceRegistry.cs | 4 +- .../Abstractions/SecretContracts.cs | 4 +- .../TerminalEnvironmentContracts.cs | 4 +- .../LocaleSmith.Core.csproj} | 0 .../Models/AvailableModelInfo.cs | 2 +- .../Models/CliModels.cs | 2 +- .../Models/ModelMessage.cs | 2 +- .../Models/ModelProviderKind.cs | 2 +- .../Models/ModelProviderPreset.cs | 2 +- .../Models/ModelRequest.cs | 2 +- .../Models/ModelResponse.cs | 2 +- .../Models/ModelServiceException.cs | 2 +- .../Models/ModelSource.cs | 2 +- .../Models/ModelToolModels.cs | 2 +- .../Models/SecretValue.cs | 2 +- .../Models/TerminalEnvironmentContext.cs | 2 +- .../Models/TranslationModels.cs | 2 +- .../Services/IncrementalTranslationPlanner.cs | 4 +- .../Services/ModelServiceRegistry.cs | 6 +- .../Cli/CliApprovalService.cs | 6 +- .../Cli/JsonLinesCliAuditSink.cs | 6 +- .../Cli/RestrictedProcessContracts.cs | 2 +- .../Cli/SafeCliCommandPolicy.cs | 6 +- .../Cli/SafeCliRunner.cs | 6 +- .../Cli/TrustedCliExecutableDiscovery.cs | 2 +- .../Cli/WindowsPrivilegeContext.cs | 4 +- .../Cli/WindowsProcessJob.cs | 2 +- .../Cli/WindowsRestrictedProcessLauncher.cs | 4 +- .../SafeSystemPromptContextProvider.cs | 4 +- .../TerminalEnvironmentDetector.cs | 6 +- .../LocaleSmith.Infrastructure.csproj} | 4 +- .../Models/AnthropicModelService.cs | 6 +- .../Models/HttpModelServiceBase.cs | 6 +- .../Models/OllamaModelService.cs | 6 +- .../Models/OpenAiCompatibleModelService.cs | 6 +- .../Models/SafeModelHttpClientFactory.cs | 2 +- .../CredentialManagerMasterKeyStore.cs | 8 +- .../EncryptedJsonConfigurationStore.cs | 44 ++- .../Security/InMemorySecretStore.cs | 6 +- .../Security/MigratingSecretStore.cs | 187 +++++++++++++ .../Security/SecurityOperationLock.cs | 70 +++++ .../Security/WindowsCredentialSecretStore.cs | 9 +- .../LocaleSmith.Mcp.csproj} | 2 +- .../McpModelToolExecutor.cs | 6 +- .../McpServerOptions.cs | 2 +- .../McpStdioServer.cs | 8 +- .../McpToolCatalog.cs | 6 +- .../OutputSanitizer.cs | 2 +- .../Protocol/BoundedUtf8LineReader.cs | 2 +- .../Protocol/FixedWindowRateLimiter.cs | 2 +- .../Protocol/JsonRpcModels.cs | 2 +- .../Protocol/JsonRpcParser.cs | 2 +- .../Protocol/JsonRpcWriter.cs | 2 +- .../LocaleSmith.McpHost.csproj} | 10 +- .../Program.cs | 10 +- .../Properties/PublishProfiles/win-x64.pubxml | 0 .../ArchiveScanManifest.cs | 2 +- .../LocaleSmith.NativeInterop.csproj} | 4 +- .../NativeCoreClient.cs | 2 +- .../NativeCoreErrorCode.cs | 2 +- .../NativeCoreException.cs | 2 +- .../NativeCoreLibraryResolver.cs | 6 +- .../NativeMethods.cs | 29 ++ .../Abstractions/PresentationServices.cs | 8 +- .../LocaleSmith.Presentation.csproj | 16 ++ .../Models/AnimationPreferencePolicy.cs | 2 +- .../Models/AppConfiguration.cs | 4 +- .../Models/OnboardingModelPath.cs | 2 +- .../Models/TranslationQueueModels.cs | 8 +- .../Services/DefaultOutputPathStrategy.cs | 4 +- .../ViewModels/AssistantViewModel.cs | 8 +- .../ViewModels/CliConfirmationViewModel.cs | 12 +- .../ViewModels/DashboardViewModel.cs | 10 +- .../ViewModels/ModelSourcesViewModel.cs | 8 +- .../ViewModels/OnboardingViewModel.cs | 8 +- .../ViewModels/SettingsViewModel.cs | 6 +- .../ViewModels/ShellViewModel.cs | 6 +- .../ViewModels/ViewModelBase.cs | 4 +- ...ileTranslationMemoryStoreMigrationTests.cs | 63 +++++ .../LegacyAppDataLocatorTests.cs | 137 +++++++++ .../LegacyAppDataMigratorTests.cs | 163 +++++++++++ .../LocaleSmith.App.Tests.csproj} | 4 +- .../ModelAssistantServiceTests.cs | 26 +- .../PipelineTranslationQueueServiceTests.cs | 16 +- .../SecureAppStateServiceTests.cs | 87 +++++- .../LocaleSmith.Application.Tests.csproj} | 6 +- .../ModelToolOrchestratorTests.cs | 8 +- .../ModelTranslationEngineTests.cs | 10 +- .../TranslationMemoryKeyTests.cs | 14 +- .../TranslationPipelineTests.cs | 16 +- .../ArchiveFixture.cs | 4 +- .../ArchiveWorkspaceBackendTests.cs | 14 +- .../ClassFileFixtureBuilder.cs | 2 +- .../FolderFixture.cs | 4 +- .../LocaleSmith.Archive.Tests.csproj} | 8 +- ...gComponentLiteralClassFileRewriterTests.cs | 4 +- .../SafeSubsetExternalizationPipelineTests.cs | 24 +- .../TestArchiveScanner.cs | 6 +- .../LocaleSmith.CliProbe.csproj} | 4 +- .../Program.cs | 2 +- .../CliCommandTests.cs | 4 +- .../IncrementalTranslationPlannerTests.cs | 6 +- .../LocaleSmith.Core.Tests.csproj} | 2 +- .../ModelProviderPresetTests.cs | 4 +- .../ModelServiceRegistryTests.cs | 8 +- .../ModelToolContractTests.cs | 4 +- .../EncryptedConfigurationStoreTests.cs | 66 ++++- .../LocaleSmith.Infrastructure.Tests.csproj} | 6 +- .../MigratingSecretStoreTests.cs | 216 +++++++++++++++ .../ModelAdapterTests.cs | 8 +- .../SafeCliTests.cs | 12 +- .../TemporaryDirectory.cs | 4 +- .../TerminalEnvironmentTests.cs | 6 +- .../LocaleSmith.Mcp.Tests.csproj} | 4 +- .../McpModelToolExecutorTests.cs | 6 +- .../McpStdioServerTests.cs | 6 +- .../LocaleSmith.NativeInterop.Tests.csproj} | 8 +- .../NativeCoreClientTests.cs | 4 +- .../AssistantViewModelTests.cs | 10 +- .../CliConfirmationViewModelTests.cs | 14 +- .../DashboardViewModelTests.cs | 12 +- .../LocaleSmith.Presentation.Tests.csproj} | 2 +- .../ModelSourcesViewModelTests.cs | 10 +- .../OnboardingAndShellTests.cs | 10 +- .../OutputPathAndSettingsTests.cs | 14 +- 227 files changed, 2497 insertions(+), 812 deletions(-) delete mode 100644 JaxI18n.slnx create mode 100644 LocaleSmith.slnx delete mode 100644 native/jax_i18n_core/include/jax_i18n_core.h rename native/{jax_i18n_core => localesmith_core}/.gitignore (100%) rename native/{jax_i18n_core => localesmith_core}/Cargo.lock (99%) rename native/{jax_i18n_core => localesmith_core}/Cargo.toml (82%) create mode 100644 native/localesmith_core/include/localesmith_core.h rename native/{jax_i18n_core => localesmith_core}/src/classfile.rs (100%) rename native/{jax_i18n_core => localesmith_core}/src/error.rs (100%) rename native/{jax_i18n_core => localesmith_core}/src/ffi.rs (87%) rename native/{jax_i18n_core => localesmith_core}/src/lib.rs (91%) rename native/{jax_i18n_core => localesmith_core}/src/metadata.rs (100%) rename native/{jax_i18n_core => localesmith_core}/src/model.rs (100%) rename native/{jax_i18n_core => localesmith_core}/src/path_safety.rs (100%) rename native/{jax_i18n_core => localesmith_core}/src/scanner.rs (100%) rename native/{jax_i18n_core => localesmith_core}/tests/archive_scan.rs (96%) rename packaging/{JaxI18n.Package => LocaleSmith.Package}/Assets/AppIconMaster.png (100%) rename packaging/{JaxI18n.Package => LocaleSmith.Package}/Assets/Square150x150Logo.png (100%) rename packaging/{JaxI18n.Package => LocaleSmith.Package}/Assets/Square44x44Logo.png (100%) rename packaging/{JaxI18n.Package => LocaleSmith.Package}/Assets/StoreLogo.png (100%) rename packaging/{JaxI18n.Package => LocaleSmith.Package}/Assets/Wide310x150Logo.png (100%) rename packaging/{JaxI18n.Package/JaxI18n.Package.wapproj => LocaleSmith.Package/LocaleSmith.Package.wapproj} (90%) rename packaging/{JaxI18n.Package => LocaleSmith.Package}/Package.appxmanifest (80%) rename packaging/{JaxI18n.Package => LocaleSmith.Package}/Strings/en-US/Resources.resw (95%) rename packaging/{JaxI18n.Package => LocaleSmith.Package}/Strings/zh-CN/Resources.resw (80%) delete mode 100644 src/JaxI18n.App/JaxI18n.App.csproj delete mode 100644 src/JaxI18n.Application/JaxI18n.Application.csproj delete mode 100644 src/JaxI18n.Archive/JaxI18n.Archive.csproj delete mode 100644 src/JaxI18n.NativeInterop/NativeMethods.cs delete mode 100644 src/JaxI18n.Presentation/JaxI18n.Presentation.csproj rename src/{JaxI18n.App => LocaleSmith.App}/App.xaml (87%) rename src/{JaxI18n.App => LocaleSmith.App}/App.xaml.cs (76%) rename src/{JaxI18n.App => LocaleSmith.App}/Behaviors/AppMotion.cs (99%) rename src/{JaxI18n.App => LocaleSmith.App}/Converters/BooleanToVisibilityConverter.cs (94%) rename src/{JaxI18n.App => LocaleSmith.App}/Dialogs/CliConfirmationDialog.xaml (98%) rename src/{JaxI18n.App => LocaleSmith.App}/Dialogs/CliConfirmationDialog.xaml.cs (93%) create mode 100644 src/LocaleSmith.App/LocaleSmith.App.csproj rename src/{JaxI18n.App => LocaleSmith.App}/MainWindow.xaml (96%) rename src/{JaxI18n.App => LocaleSmith.App}/MainWindow.xaml.cs (78%) rename src/{JaxI18n.App => LocaleSmith.App}/Pages/AssistantPage.xaml (98%) rename src/{JaxI18n.App => LocaleSmith.App}/Pages/AssistantPage.xaml.cs (96%) rename src/{JaxI18n.App => LocaleSmith.App}/Pages/DashboardPage.xaml (99%) rename src/{JaxI18n.App => LocaleSmith.App}/Pages/DashboardPage.xaml.cs (96%) rename src/{JaxI18n.App => LocaleSmith.App}/Pages/ModelSourcesPage.xaml (99%) rename src/{JaxI18n.App => LocaleSmith.App}/Pages/ModelSourcesPage.xaml.cs (94%) rename src/{JaxI18n.App => LocaleSmith.App}/Pages/OnboardingPage.xaml (99%) rename src/{JaxI18n.App => LocaleSmith.App}/Pages/OnboardingPage.xaml.cs (93%) rename src/{JaxI18n.App => LocaleSmith.App}/Pages/SettingsPage.xaml (97%) rename src/{JaxI18n.App => LocaleSmith.App}/Pages/SettingsPage.xaml.cs (94%) rename src/{JaxI18n.App => LocaleSmith.App}/Properties/PublishProfiles/win-x64.pubxml (100%) rename src/{JaxI18n.App => LocaleSmith.App}/Services/AppMotionService.cs (99%) rename src/{JaxI18n.App => LocaleSmith.App}/Services/CliConfirmationViewModelFactory.cs (80%) rename src/{JaxI18n.App => LocaleSmith.App}/Services/DispatcherQueueUiDispatcher.cs (89%) rename src/{JaxI18n.App => LocaleSmith.App}/Services/FileTranslationMemoryStore.cs (54%) create mode 100644 src/LocaleSmith.App/Services/LegacyAppDataLocator.cs create mode 100644 src/LocaleSmith.App/Services/LegacyAppDataMigrator.cs create mode 100644 src/LocaleSmith.App/Services/LegacyDefaultPathNormalizer.cs rename src/{JaxI18n.App => LocaleSmith.App}/Services/ModelAssistantService.cs (96%) rename src/{JaxI18n.App => LocaleSmith.App}/Services/PipelineTranslationQueueService.cs (94%) rename src/{JaxI18n.App => LocaleSmith.App}/Services/SecureAppStateService.cs (98%) rename src/{JaxI18n.App => LocaleSmith.App}/Services/WinUiTextProvider.cs (89%) rename src/{JaxI18n.App => LocaleSmith.App}/Strings/en-US/Resources.resw (100%) rename src/{JaxI18n.App => LocaleSmith.App}/Strings/zh-CN/Resources.resw (99%) rename src/{JaxI18n.App => LocaleSmith.App}/Themes/Controls.xaml (97%) rename src/{JaxI18n.App => LocaleSmith.App}/app.manifest (93%) rename src/{JaxI18n.Application => LocaleSmith.Application}/Abstractions/IArchiveWorkspace.cs (90%) rename src/{JaxI18n.Application => LocaleSmith.Application}/Abstractions/IPipelineJobScheduler.cs (67%) rename src/{JaxI18n.Application => LocaleSmith.Application}/Abstractions/ITranslationEngine.cs (74%) rename src/{JaxI18n.Application => LocaleSmith.Application}/Abstractions/ITranslationMemoryStore.cs (76%) create mode 100644 src/LocaleSmith.Application/LocaleSmith.Application.csproj rename src/{JaxI18n.Application => LocaleSmith.Application}/Models/PipelineModels.cs (98%) rename src/{JaxI18n.Application => LocaleSmith.Application}/Models/TranslationMemoryModels.cs (80%) rename src/{JaxI18n.Application => LocaleSmith.Application}/PipelineException.cs (87%) rename src/{JaxI18n.Application => LocaleSmith.Application}/Services/ModelToolOrchestrator.cs (98%) rename src/{JaxI18n.Application => LocaleSmith.Application}/Services/ModelTranslationEngine.cs (97%) rename src/{JaxI18n.Application => LocaleSmith.Application}/Services/ModelTranslationEngineOptions.cs (94%) rename src/{JaxI18n.Application => LocaleSmith.Application}/Services/PipelineJobHandle.cs (91%) rename src/{JaxI18n.Application => LocaleSmith.Application}/Services/PipelineQueue.cs (97%) rename src/{JaxI18n.Application => LocaleSmith.Application}/Services/PlaceholderValidator.cs (97%) rename src/{JaxI18n.Application => LocaleSmith.Application}/Services/TranslationJsonModels.cs (95%) rename src/{JaxI18n.Application => LocaleSmith.Application}/Services/TranslationPipeline.cs (99%) rename src/{JaxI18n.Application => LocaleSmith.Application}/TranslationContractException.cs (89%) rename src/{JaxI18n.Archive => LocaleSmith.Archive}/ArchiveEntrySnapshot.cs (96%) rename src/{JaxI18n.Archive => LocaleSmith.Archive}/ArchivePathSafety.cs (99%) rename src/{JaxI18n.Archive => LocaleSmith.Archive}/ArchiveWorkspace.cs (99%) rename src/{JaxI18n.Archive => LocaleSmith.Archive}/ArchiveWorkspaceBackend.cs (97%) rename src/{JaxI18n.Archive => LocaleSmith.Archive}/ArchiveWorkspaceOptions.cs (98%) rename src/{JaxI18n.Archive => LocaleSmith.Archive}/ClassFile/ClassFileBinary.cs (99%) rename src/{JaxI18n.Archive => LocaleSmith.Archive}/ClassFile/ClassFileRewriteModels.cs (97%) rename src/{JaxI18n.Archive => LocaleSmith.Archive}/ClassFile/JavaBytecodeDecoder.cs (99%) rename src/{JaxI18n.Archive => LocaleSmith.Archive}/ClassFile/MojangComponentLiteralClassFileRewriter.cs (99%) rename src/{JaxI18n.Archive => LocaleSmith.Archive}/ClassFile/ParsedClassFile.cs (99%) rename src/{JaxI18n.Archive => LocaleSmith.Archive}/FolderSnapshotBuilder.cs (99%) rename src/{JaxI18n.Archive => LocaleSmith.Archive}/IArchiveScanner.cs (89%) rename src/{JaxI18n.Archive => LocaleSmith.Archive}/JsonResourceEditor.cs (99%) rename src/{JaxI18n.Archive => LocaleSmith.Archive}/LangResourceEditor.cs (98%) create mode 100644 src/LocaleSmith.Archive/LocaleSmith.Archive.csproj rename src/{JaxI18n.Archive => LocaleSmith.Archive}/TransactionJournal.cs (97%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Abstractions/CliContracts.cs (93%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Abstractions/IModelService.cs (88%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Abstractions/IModelServiceRegistry.cs (90%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Abstractions/SecretContracts.cs (93%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Abstractions/TerminalEnvironmentContracts.cs (79%) rename src/{JaxI18n.Core/JaxI18n.Core.csproj => LocaleSmith.Core/LocaleSmith.Core.csproj} (100%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Models/AvailableModelInfo.cs (85%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Models/CliModels.cs (99%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Models/ModelMessage.cs (98%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Models/ModelProviderKind.cs (70%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Models/ModelProviderPreset.cs (99%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Models/ModelRequest.cs (98%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Models/ModelResponse.cs (97%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Models/ModelServiceException.cs (95%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Models/ModelSource.cs (98%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Models/ModelToolModels.cs (98%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Models/SecretValue.cs (97%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Models/TerminalEnvironmentContext.cs (91%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Models/TranslationModels.cs (98%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Services/IncrementalTranslationPlanner.cs (97%) rename src/{JaxI18n.Core => LocaleSmith.Core}/Services/ModelServiceRegistry.cs (98%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Cli/CliApprovalService.cs (95%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Cli/JsonLinesCliAuditSink.cs (93%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Cli/RestrictedProcessContracts.cs (93%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Cli/SafeCliCommandPolicy.cs (99%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Cli/SafeCliRunner.cs (99%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Cli/TrustedCliExecutableDiscovery.cs (98%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Cli/WindowsPrivilegeContext.cs (95%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Cli/WindowsProcessJob.cs (99%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Cli/WindowsRestrictedProcessLauncher.cs (99%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Environment/SafeSystemPromptContextProvider.cs (93%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Environment/TerminalEnvironmentDetector.cs (98%) rename src/{JaxI18n.Infrastructure/JaxI18n.Infrastructure.csproj => LocaleSmith.Infrastructure/LocaleSmith.Infrastructure.csproj} (68%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Models/AnthropicModelService.cs (98%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Models/HttpModelServiceBase.cs (99%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Models/OllamaModelService.cs (98%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Models/OpenAiCompatibleModelService.cs (98%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Models/SafeModelHttpClientFactory.cs (96%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Security/CredentialManagerMasterKeyStore.cs (91%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Security/EncryptedJsonConfigurationStore.cs (82%) rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Security/InMemorySecretStore.cs (95%) create mode 100644 src/LocaleSmith.Infrastructure/Security/MigratingSecretStore.cs create mode 100644 src/LocaleSmith.Infrastructure/Security/SecurityOperationLock.cs rename src/{JaxI18n.Infrastructure => LocaleSmith.Infrastructure}/Security/WindowsCredentialSecretStore.cs (96%) rename src/{JaxI18n.Mcp/JaxI18n.Mcp.csproj => LocaleSmith.Mcp/LocaleSmith.Mcp.csproj} (78%) rename src/{JaxI18n.Mcp => LocaleSmith.Mcp}/McpModelToolExecutor.cs (97%) rename src/{JaxI18n.Mcp => LocaleSmith.Mcp}/McpServerOptions.cs (98%) rename src/{JaxI18n.Mcp => LocaleSmith.Mcp}/McpStdioServer.cs (99%) rename src/{JaxI18n.Mcp => LocaleSmith.Mcp}/McpToolCatalog.cs (99%) rename src/{JaxI18n.Mcp => LocaleSmith.Mcp}/OutputSanitizer.cs (98%) rename src/{JaxI18n.Mcp => LocaleSmith.Mcp}/Protocol/BoundedUtf8LineReader.cs (98%) rename src/{JaxI18n.Mcp => LocaleSmith.Mcp}/Protocol/FixedWindowRateLimiter.cs (96%) rename src/{JaxI18n.Mcp => LocaleSmith.Mcp}/Protocol/JsonRpcModels.cs (95%) rename src/{JaxI18n.Mcp => LocaleSmith.Mcp}/Protocol/JsonRpcParser.cs (99%) rename src/{JaxI18n.Mcp => LocaleSmith.Mcp}/Protocol/JsonRpcWriter.cs (98%) rename src/{JaxI18n.McpHost/JaxI18n.McpHost.csproj => LocaleSmith.McpHost/LocaleSmith.McpHost.csproj} (58%) rename src/{JaxI18n.McpHost => LocaleSmith.McpHost}/Program.cs (90%) rename src/{JaxI18n.McpHost => LocaleSmith.McpHost}/Properties/PublishProfiles/win-x64.pubxml (100%) rename src/{JaxI18n.NativeInterop => LocaleSmith.NativeInterop}/ArchiveScanManifest.cs (99%) rename src/{JaxI18n.NativeInterop/JaxI18n.NativeInterop.csproj => LocaleSmith.NativeInterop/LocaleSmith.NativeInterop.csproj} (62%) rename src/{JaxI18n.NativeInterop => LocaleSmith.NativeInterop}/NativeCoreClient.cs (99%) rename src/{JaxI18n.NativeInterop => LocaleSmith.NativeInterop}/NativeCoreErrorCode.cs (86%) rename src/{JaxI18n.NativeInterop => LocaleSmith.NativeInterop}/NativeCoreException.cs (87%) rename src/{JaxI18n.NativeInterop => LocaleSmith.NativeInterop}/NativeCoreLibraryResolver.cs (92%) create mode 100644 src/LocaleSmith.NativeInterop/NativeMethods.cs rename src/{JaxI18n.Presentation => LocaleSmith.Presentation}/Abstractions/PresentationServices.cs (94%) create mode 100644 src/LocaleSmith.Presentation/LocaleSmith.Presentation.csproj rename src/{JaxI18n.Presentation => LocaleSmith.Presentation}/Models/AnimationPreferencePolicy.cs (92%) rename src/{JaxI18n.Presentation => LocaleSmith.Presentation}/Models/AppConfiguration.cs (97%) rename src/{JaxI18n.Presentation => LocaleSmith.Presentation}/Models/OnboardingModelPath.cs (63%) rename src/{JaxI18n.Presentation => LocaleSmith.Presentation}/Models/TranslationQueueModels.cs (89%) rename src/{JaxI18n.Presentation => LocaleSmith.Presentation}/Services/DefaultOutputPathStrategy.cs (98%) rename src/{JaxI18n.Presentation => LocaleSmith.Presentation}/ViewModels/AssistantViewModel.cs (98%) rename src/{JaxI18n.Presentation => LocaleSmith.Presentation}/ViewModels/CliConfirmationViewModel.cs (96%) rename src/{JaxI18n.Presentation => LocaleSmith.Presentation}/ViewModels/DashboardViewModel.cs (99%) rename src/{JaxI18n.Presentation => LocaleSmith.Presentation}/ViewModels/ModelSourcesViewModel.cs (99%) rename src/{JaxI18n.Presentation => LocaleSmith.Presentation}/ViewModels/OnboardingViewModel.cs (98%) rename src/{JaxI18n.Presentation => LocaleSmith.Presentation}/ViewModels/SettingsViewModel.cs (97%) rename src/{JaxI18n.Presentation => LocaleSmith.Presentation}/ViewModels/ShellViewModel.cs (95%) rename src/{JaxI18n.Presentation => LocaleSmith.Presentation}/ViewModels/ViewModelBase.cs (92%) create mode 100644 tests/LocaleSmith.App.Tests/FileTranslationMemoryStoreMigrationTests.cs create mode 100644 tests/LocaleSmith.App.Tests/LegacyAppDataLocatorTests.cs create mode 100644 tests/LocaleSmith.App.Tests/LegacyAppDataMigratorTests.cs rename tests/{JaxI18n.App.Tests/JaxI18n.App.Tests.csproj => LocaleSmith.App.Tests/LocaleSmith.App.Tests.csproj} (83%) rename tests/{JaxI18n.App.Tests => LocaleSmith.App.Tests}/ModelAssistantServiceTests.cs (97%) rename tests/{JaxI18n.App.Tests => LocaleSmith.App.Tests}/PipelineTranslationQueueServiceTests.cs (93%) rename tests/{JaxI18n.App.Tests => LocaleSmith.App.Tests}/SecureAppStateServiceTests.cs (90%) rename tests/{JaxI18n.Application.Tests/JaxI18n.Application.Tests.csproj => LocaleSmith.Application.Tests/LocaleSmith.Application.Tests.csproj} (81%) rename tests/{JaxI18n.Application.Tests => LocaleSmith.Application.Tests}/ModelToolOrchestratorTests.cs (98%) rename tests/{JaxI18n.Application.Tests => LocaleSmith.Application.Tests}/ModelTranslationEngineTests.cs (98%) rename tests/{JaxI18n.Application.Tests => LocaleSmith.Application.Tests}/TranslationMemoryKeyTests.cs (80%) rename tests/{JaxI18n.Application.Tests => LocaleSmith.Application.Tests}/TranslationPipelineTests.cs (98%) rename tests/{JaxI18n.Archive.Tests => LocaleSmith.Archive.Tests}/ArchiveFixture.cs (93%) rename tests/{JaxI18n.Archive.Tests => LocaleSmith.Archive.Tests}/ArchiveWorkspaceBackendTests.cs (99%) rename tests/{JaxI18n.Archive.Tests => LocaleSmith.Archive.Tests}/ClassFileFixtureBuilder.cs (99%) rename tests/{JaxI18n.Archive.Tests => LocaleSmith.Archive.Tests}/FolderFixture.cs (95%) rename tests/{JaxI18n.NativeInterop.Tests/JaxI18n.NativeInterop.Tests.csproj => LocaleSmith.Archive.Tests/LocaleSmith.Archive.Tests.csproj} (73%) rename tests/{JaxI18n.Archive.Tests => LocaleSmith.Archive.Tests}/MojangComponentLiteralClassFileRewriterTests.cs (99%) rename tests/{JaxI18n.Archive.Tests => LocaleSmith.Archive.Tests}/SafeSubsetExternalizationPipelineTests.cs (97%) rename tests/{JaxI18n.Archive.Tests => LocaleSmith.Archive.Tests}/TestArchiveScanner.cs (98%) rename tests/{JaxI18n.CliProbe/JaxI18n.CliProbe.csproj => LocaleSmith.CliProbe/LocaleSmith.CliProbe.csproj} (64%) rename tests/{JaxI18n.CliProbe => LocaleSmith.CliProbe}/Program.cs (99%) rename tests/{JaxI18n.Core.Tests => LocaleSmith.Core.Tests}/CliCommandTests.cs (94%) rename tests/{JaxI18n.Core.Tests => LocaleSmith.Core.Tests}/IncrementalTranslationPlannerTests.cs (96%) rename tests/{JaxI18n.Core.Tests/JaxI18n.Core.Tests.csproj => LocaleSmith.Core.Tests/LocaleSmith.Core.Tests.csproj} (89%) rename tests/{JaxI18n.Core.Tests => LocaleSmith.Core.Tests}/ModelProviderPresetTests.cs (98%) rename tests/{JaxI18n.Core.Tests => LocaleSmith.Core.Tests}/ModelServiceRegistryTests.cs (95%) rename tests/{JaxI18n.Core.Tests => LocaleSmith.Core.Tests}/ModelToolContractTests.cs (97%) rename tests/{JaxI18n.Infrastructure.Tests => LocaleSmith.Infrastructure.Tests}/EncryptedConfigurationStoreTests.cs (58%) rename tests/{JaxI18n.Infrastructure.Tests/JaxI18n.Infrastructure.Tests.csproj => LocaleSmith.Infrastructure.Tests/LocaleSmith.Infrastructure.Tests.csproj} (72%) create mode 100644 tests/LocaleSmith.Infrastructure.Tests/MigratingSecretStoreTests.cs rename tests/{JaxI18n.Infrastructure.Tests => LocaleSmith.Infrastructure.Tests}/ModelAdapterTests.cs (99%) rename tests/{JaxI18n.Infrastructure.Tests => LocaleSmith.Infrastructure.Tests}/SafeCliTests.cs (98%) rename tests/{JaxI18n.Infrastructure.Tests => LocaleSmith.Infrastructure.Tests}/TemporaryDirectory.cs (85%) rename tests/{JaxI18n.Infrastructure.Tests => LocaleSmith.Infrastructure.Tests}/TerminalEnvironmentTests.cs (94%) rename tests/{JaxI18n.Mcp.Tests/JaxI18n.Mcp.Tests.csproj => LocaleSmith.Mcp.Tests/LocaleSmith.Mcp.Tests.csproj} (81%) rename tests/{JaxI18n.Mcp.Tests => LocaleSmith.Mcp.Tests}/McpModelToolExecutorTests.cs (97%) rename tests/{JaxI18n.Mcp.Tests => LocaleSmith.Mcp.Tests}/McpStdioServerTests.cs (99%) rename tests/{JaxI18n.Archive.Tests/JaxI18n.Archive.Tests.csproj => LocaleSmith.NativeInterop.Tests/LocaleSmith.NativeInterop.Tests.csproj} (72%) rename tests/{JaxI18n.NativeInterop.Tests => LocaleSmith.NativeInterop.Tests}/NativeCoreClientTests.cs (96%) rename tests/{JaxI18n.Presentation.Tests => LocaleSmith.Presentation.Tests}/AssistantViewModelTests.cs (98%) rename tests/{JaxI18n.Presentation.Tests => LocaleSmith.Presentation.Tests}/CliConfirmationViewModelTests.cs (95%) rename tests/{JaxI18n.Presentation.Tests => LocaleSmith.Presentation.Tests}/DashboardViewModelTests.cs (98%) rename tests/{JaxI18n.Presentation.Tests/JaxI18n.Presentation.Tests.csproj => LocaleSmith.Presentation.Tests/LocaleSmith.Presentation.Tests.csproj} (87%) rename tests/{JaxI18n.Presentation.Tests => LocaleSmith.Presentation.Tests}/ModelSourcesViewModelTests.cs (98%) rename tests/{JaxI18n.Presentation.Tests => LocaleSmith.Presentation.Tests}/OnboardingAndShellTests.cs (97%) rename tests/{JaxI18n.Presentation.Tests => LocaleSmith.Presentation.Tests}/OutputPathAndSettingsTests.cs (96%) 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..b599c4f 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@
LocaleSmith 标志 -

LocaleSmith · 译匠

+

LocaleSmith | 译匠

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

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

@@ -34,7 +34,7 @@ ## 项目概览 -LocaleSmith(译匠)面向 Minecraft: Java Edition 模组、资源包与光影包,将**安全扫描、增量翻译、结构验证和事务重建**整合进一个 Windows 原生桌面工作台。 +**LocaleSmith | 译匠**,面向 Minecraft: Java Edition 模组、资源包与光影包,将**安全扫描、增量翻译、结构验证和事务重建**整合进一个 Windows 原生桌面工作台。 项目采用 Rust 原生扫描核心与 .NET 10 / WinUI 3 桌面应用:Rust 负责 JAR、ZIP、Loader 元数据和受支持字节码模式的解析,.NET 负责归档事务、模型接入、安全存储、翻译队列与用户界面。 @@ -96,24 +96,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 +121,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、双语资源和图标 ``` @@ -167,7 +167,7 @@ MCP stdio Host 只暴露 `system.context` 与 `cli.propose`,不暴露 `cli.exe
MSIX 开发包状态 -当前 manifest 版本为 `0.1.0.1`,开发包已完成 payload、PRI、MCP Host、开发签名和 loose AppsFolder 启动验证。但其证书为未受信任、无可信时间戳的自签名开发证书,尚未完成生产证书替换及干净机安装、升级、修复和卸载验证。 +当前 manifest 版本为 `0.1.0.2`。MSIX 已完成 payload、PRI、MCP Host、SignPath Authenticode 签名、本机安装与启动验证;微软商店上架前仍需使用 Partner Center 分配的正式包身份重新生成清单并完成商店认证。
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.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.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.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 76% rename from src/JaxI18n.App/App.xaml.cs rename to src/LocaleSmith.App/App.xaml.cs index e274d05..f21c9cd 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; -namespace JaxI18n.App; +namespace LocaleSmith.App; public partial class App : Microsoft.UI.Xaml.Application { @@ -40,7 +40,24 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args) { try { - _host = BuildHost(); + var currentLocalAppData = System.Environment.GetFolderPath( + System.Environment.SpecialFolder.LocalApplicationData); + var unredirectedLocalAppData = LegacyAppDataLocator + .GetUnredirectedLocalApplicationDataPath(); + var currentAppDataRoot = Path.Combine(currentLocalAppData, "LocaleSmith"); + var currentCredentialStore = new WindowsCredentialSecretStore("LocaleSmith"); + var legacyCredentialStore = new WindowsCredentialSecretStore("JaxI18n"); + foreach (var legacyAppDataRoot in LegacyAppDataLocator.FindLegacyRoots(unredirectedLocalAppData)) + { + var migrator = new LegacyAppDataMigrator( + legacyAppDataRoot, + currentAppDataRoot, + legacyCredentialStore, + currentCredentialStore); + await migrator.MigrateAsync().ConfigureAwait(true); + } + + _host = BuildHost(currentAppDataRoot, currentCredentialStore, legacyCredentialStore); await _host.StartAsync().ConfigureAwait(true); var state = _host.Services.GetRequiredService(); await state.InitializeAsync().ConfigureAwait(true); @@ -61,13 +78,15 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args) } } - private static IHost BuildHost() + private static IHost BuildHost( + string appDataRoot, + WindowsCredentialSecretStore currentCredentialStore, + WindowsCredentialSecretStore legacyCredentialStore) { 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"); @@ -75,14 +94,14 @@ private static IHost BuildHost() 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(); 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/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 96% rename from src/JaxI18n.App/MainWindow.xaml rename to src/LocaleSmith.App/MainWindow.xaml index 05f36f9..fc42e61 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) @@ -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; 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/JaxI18n.App/Pages/DashboardPage.xaml b/src/LocaleSmith.App/Pages/DashboardPage.xaml similarity index 99% rename from src/JaxI18n.App/Pages/DashboardPage.xaml rename to src/LocaleSmith.App/Pages/DashboardPage.xaml index 49f2f78..1341587 100644 --- a/src/JaxI18n.App/Pages/DashboardPage.xaml +++ b/src/LocaleSmith.App/Pages/DashboardPage.xaml @@ -1,8 +1,8 @@ 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/JaxI18n.App/Pages/ModelSourcesPage.xaml b/src/LocaleSmith.App/Pages/ModelSourcesPage.xaml similarity index 99% rename from src/JaxI18n.App/Pages/ModelSourcesPage.xaml rename to src/LocaleSmith.App/Pages/ModelSourcesPage.xaml index c3c2e6a..1617912 100644 --- a/src/JaxI18n.App/Pages/ModelSourcesPage.xaml +++ b/src/LocaleSmith.App/Pages/ModelSourcesPage.xaml @@ -1,8 +1,8 @@ diff --git a/src/JaxI18n.App/Pages/ModelSourcesPage.xaml.cs b/src/LocaleSmith.App/Pages/ModelSourcesPage.xaml.cs similarity index 94% rename from src/JaxI18n.App/Pages/ModelSourcesPage.xaml.cs rename to src/LocaleSmith.App/Pages/ModelSourcesPage.xaml.cs index a028c1f..c27c6e3 100644 --- a/src/JaxI18n.App/Pages/ModelSourcesPage.xaml.cs +++ b/src/LocaleSmith.App/Pages/ModelSourcesPage.xaml.cs @@ -1,9 +1,9 @@ -using JaxI18n.Presentation.ViewModels; +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 ModelSourcesPage : Page { diff --git a/src/JaxI18n.App/Pages/OnboardingPage.xaml b/src/LocaleSmith.App/Pages/OnboardingPage.xaml similarity index 99% rename from src/JaxI18n.App/Pages/OnboardingPage.xaml rename to src/LocaleSmith.App/Pages/OnboardingPage.xaml index a71b1b1..8805869 100644 --- a/src/JaxI18n.App/Pages/OnboardingPage.xaml +++ b/src/LocaleSmith.App/Pages/OnboardingPage.xaml @@ -1,8 +1,8 @@ diff --git a/src/JaxI18n.App/Pages/OnboardingPage.xaml.cs b/src/LocaleSmith.App/Pages/OnboardingPage.xaml.cs similarity index 93% rename from src/JaxI18n.App/Pages/OnboardingPage.xaml.cs rename to src/LocaleSmith.App/Pages/OnboardingPage.xaml.cs index 1650b84..839716a 100644 --- a/src/JaxI18n.App/Pages/OnboardingPage.xaml.cs +++ b/src/LocaleSmith.App/Pages/OnboardingPage.xaml.cs @@ -1,10 +1,10 @@ -using JaxI18n.Presentation.Models; -using JaxI18n.Presentation.ViewModels; +using LocaleSmith.Presentation.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 OnboardingPage : Page { diff --git a/src/JaxI18n.App/Pages/SettingsPage.xaml b/src/LocaleSmith.App/Pages/SettingsPage.xaml similarity index 97% rename from src/JaxI18n.App/Pages/SettingsPage.xaml rename to src/LocaleSmith.App/Pages/SettingsPage.xaml index d9a79bb..e1668ad 100644 --- a/src/JaxI18n.App/Pages/SettingsPage.xaml +++ b/src/LocaleSmith.App/Pages/SettingsPage.xaml @@ -1,8 +1,8 @@ diff --git a/src/JaxI18n.App/Pages/SettingsPage.xaml.cs b/src/LocaleSmith.App/Pages/SettingsPage.xaml.cs similarity index 94% rename from src/JaxI18n.App/Pages/SettingsPage.xaml.cs rename to src/LocaleSmith.App/Pages/SettingsPage.xaml.cs index fdbf85b..4bfebfd 100644 --- a/src/JaxI18n.App/Pages/SettingsPage.xaml.cs +++ b/src/LocaleSmith.App/Pages/SettingsPage.xaml.cs @@ -1,13 +1,13 @@ -using JaxI18n.App.Dialogs; -using JaxI18n.App.Services; -using JaxI18n.Presentation.Models; -using JaxI18n.Presentation.ViewModels; +using LocaleSmith.App.Dialogs; +using LocaleSmith.App.Services; +using LocaleSmith.Presentation.Models; +using LocaleSmith.Presentation.ViewModels; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Windows.Globalization; -namespace JaxI18n.App.Pages; +namespace LocaleSmith.App.Pages; public sealed partial class SettingsPage : Page { diff --git a/src/JaxI18n.App/Properties/PublishProfiles/win-x64.pubxml b/src/LocaleSmith.App/Properties/PublishProfiles/win-x64.pubxml similarity index 100% rename from src/JaxI18n.App/Properties/PublishProfiles/win-x64.pubxml rename to src/LocaleSmith.App/Properties/PublishProfiles/win-x64.pubxml diff --git a/src/JaxI18n.App/Services/AppMotionService.cs b/src/LocaleSmith.App/Services/AppMotionService.cs similarity index 99% rename from src/JaxI18n.App/Services/AppMotionService.cs rename to src/LocaleSmith.App/Services/AppMotionService.cs index a7f68a9..6eb4e3c 100644 --- a/src/JaxI18n.App/Services/AppMotionService.cs +++ b/src/LocaleSmith.App/Services/AppMotionService.cs @@ -1,13 +1,13 @@ using System.Runtime.CompilerServices; using System.Runtime.Versioning; -using JaxI18n.Presentation.Models; +using LocaleSmith.Presentation.Models; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Animation; using Windows.Foundation.Metadata; using Windows.UI.ViewManagement; -namespace JaxI18n.App.Services; +namespace LocaleSmith.App.Services; /// /// Owns the app's short transform/opacity transitions and gates them against the Windows diff --git a/src/JaxI18n.App/Services/CliConfirmationViewModelFactory.cs b/src/LocaleSmith.App/Services/CliConfirmationViewModelFactory.cs similarity index 80% rename from src/JaxI18n.App/Services/CliConfirmationViewModelFactory.cs rename to src/LocaleSmith.App/Services/CliConfirmationViewModelFactory.cs index 9737412..520f110 100644 --- a/src/JaxI18n.App/Services/CliConfirmationViewModelFactory.cs +++ b/src/LocaleSmith.App/Services/CliConfirmationViewModelFactory.cs @@ -1,8 +1,8 @@ -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; -using JaxI18n.Presentation.ViewModels; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; +using LocaleSmith.Presentation.ViewModels; -namespace JaxI18n.App.Services; +namespace LocaleSmith.App.Services; public sealed class CliConfirmationViewModelFactory( ICliCommandPolicy policy, @@ -10,8 +10,8 @@ public sealed class CliConfirmationViewModelFactory( ICliRunner runner, ITerminalEnvironmentDetector terminalEnvironmentDetector, string auditLogPath, - JaxI18n.Presentation.Abstractions.IUiTextProvider text) : - JaxI18n.Presentation.Abstractions.ICliDiagnosticRequestFactory + LocaleSmith.Presentation.Abstractions.IUiTextProvider text) : + LocaleSmith.Presentation.Abstractions.ICliDiagnosticRequestFactory { public Task CreateAsync( string sandboxPath, diff --git a/src/JaxI18n.App/Services/DispatcherQueueUiDispatcher.cs b/src/LocaleSmith.App/Services/DispatcherQueueUiDispatcher.cs similarity index 89% rename from src/JaxI18n.App/Services/DispatcherQueueUiDispatcher.cs rename to src/LocaleSmith.App/Services/DispatcherQueueUiDispatcher.cs index 597659e..6b10f84 100644 --- a/src/JaxI18n.App/Services/DispatcherQueueUiDispatcher.cs +++ b/src/LocaleSmith.App/Services/DispatcherQueueUiDispatcher.cs @@ -1,7 +1,7 @@ -using JaxI18n.Presentation.Abstractions; +using LocaleSmith.Presentation.Abstractions; using Microsoft.UI.Dispatching; -namespace JaxI18n.App.Services; +namespace LocaleSmith.App.Services; public sealed class DispatcherQueueUiDispatcher : IUiDispatcher { diff --git a/src/JaxI18n.App/Services/FileTranslationMemoryStore.cs b/src/LocaleSmith.App/Services/FileTranslationMemoryStore.cs similarity index 54% rename from src/JaxI18n.App/Services/FileTranslationMemoryStore.cs rename to src/LocaleSmith.App/Services/FileTranslationMemoryStore.cs index ed2b80f..d1ad912 100644 --- a/src/JaxI18n.App/Services/FileTranslationMemoryStore.cs +++ b/src/LocaleSmith.App/Services/FileTranslationMemoryStore.cs @@ -1,10 +1,10 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; -using JaxI18n.Application.Abstractions; -using JaxI18n.Application.Models; +using LocaleSmith.Application.Abstractions; +using LocaleSmith.Application.Models; -namespace JaxI18n.App.Services; +namespace LocaleSmith.App.Services; public sealed class FileTranslationMemoryStore : ITranslationMemoryStore, IDisposable { @@ -27,23 +27,35 @@ public async Task LoadAsync( await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); try { - var path = GetPath(normalized); - if (!File.Exists(path)) + var path = GetPath(normalized, normalized.PackageIdentity); + var legacyPath = GetPath(normalized, normalized.LegacyPackageIdentity); + var sourcePath = File.Exists(path) + ? path + : File.Exists(legacyPath) + ? legacyPath + : null; + if (sourcePath is null) { return TranslationMemorySnapshot.Empty(normalized); } await using var stream = new FileStream( - path, + sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read, 16 * 1024, FileOptions.Asynchronous | FileOptions.SequentialScan); - return await JsonSerializer + var snapshot = await JsonSerializer .DeserializeAsync(stream, SerializerOptions, cancellationToken) .ConfigureAwait(false) ?? throw new InvalidDataException("The translation-memory snapshot is empty."); + if (!string.Equals(sourcePath, path, StringComparison.OrdinalIgnoreCase)) + { + await CopyFileAtomicallyAsync(sourcePath, path, cancellationToken).ConfigureAwait(false); + } + + return snapshot; } catch (JsonException exception) { @@ -60,7 +72,8 @@ public async Task SaveAsync( CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(snapshot); - var path = GetPath(snapshot.Key.Normalize()); + var normalizedKey = snapshot.Key.Normalize(); + var path = GetPath(normalizedKey, normalizedKey.PackageIdentity); await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); try { @@ -100,12 +113,59 @@ await JsonSerializer } } - private string GetPath(TranslationMemoryKey key) + private string GetPath(TranslationMemoryKey key, string packageIdentity) { - var identity = $"{key.PackageIdentity}\0{key.TargetLanguage}"; + var identity = $"{packageIdentity}\0{key.TargetLanguage}"; var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(identity))).ToLowerInvariant(); return Path.Combine(_directory, $"{hash}.json"); } + private static async Task CopyFileAtomicallyAsync( + string sourcePath, + string destinationPath, + CancellationToken cancellationToken) + { + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath) + ?? throw new InvalidOperationException("The translation-memory path has no parent directory.")); + var temporaryPath = $"{destinationPath}.{Guid.NewGuid():N}.migrating"; + try + { + await using (var source = new FileStream( + sourcePath, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + 16 * 1024, + FileOptions.Asynchronous | FileOptions.SequentialScan)) + await using (var destination = new FileStream( + temporaryPath, + FileMode.CreateNew, + FileAccess.Write, + FileShare.None, + 16 * 1024, + FileOptions.Asynchronous | FileOptions.WriteThrough)) + { + await source.CopyToAsync(destination, cancellationToken).ConfigureAwait(false); + await destination.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + try + { + File.Move(temporaryPath, destinationPath, overwrite: false); + } + catch (IOException) when (File.Exists(destinationPath)) + { + // Another process completed the same idempotent migration first. + } + } + finally + { + if (File.Exists(temporaryPath)) + { + File.Delete(temporaryPath); + } + } + } + public void Dispose() => _gate.Dispose(); } diff --git a/src/LocaleSmith.App/Services/LegacyAppDataLocator.cs b/src/LocaleSmith.App/Services/LegacyAppDataLocator.cs new file mode 100644 index 0000000..7c03bd0 --- /dev/null +++ b/src/LocaleSmith.App/Services/LegacyAppDataLocator.cs @@ -0,0 +1,221 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using Windows.Management.Deployment; + +namespace LocaleSmith.App.Services; + +/// +/// Locates legacy state outside the current package's AppData virtualization boundary. +/// +public static class LegacyAppDataLocator +{ + private const string LegacyPackageName = "JaxI18n.Desktop"; + private const string LegacyDirectoryName = "JaxI18n"; + private const uint NoPackageRedirection = 0x00010000; + private static readonly Guid LocalAppDataFolderId = + new("F1B32785-6FBA-4FCF-9D55-7B8E7F157091"); + + public static string GetUnredirectedLocalApplicationDataPath() + { + var folderId = LocalAppDataFolderId; + var result = SHGetKnownFolderPath( + ref folderId, + NoPackageRedirection, + nint.Zero, + out var rawPath); + try + { + if (result >= 0 && rawPath != nint.Zero) + { + var path = Marshal.PtrToStringUni(rawPath); + if (!string.IsNullOrWhiteSpace(path)) + { + return Path.GetFullPath(path); + } + } + } + finally + { + if (rawPath != nint.Zero) + { + Marshal.FreeCoTaskMem(rawPath); + } + } + + var environmentPath = System.Environment.GetEnvironmentVariable("LOCALAPPDATA"); + if (!string.IsNullOrWhiteSpace(environmentPath)) + { + return Path.GetFullPath(environmentPath); + } + + Marshal.ThrowExceptionForHR(result); + throw new InvalidOperationException("The unredirected LocalAppData folder is unavailable."); + } + + public static IReadOnlyList FindLegacyRoots(string unredirectedLocalAppDataPath) + { + return FindLegacyRoots( + unredirectedLocalAppDataPath, + FindRegisteredLegacyPackageFamilyNames()); + } + + internal static IReadOnlyList FindLegacyRoots( + string unredirectedLocalAppDataPath, + IEnumerable registeredPackageFamilyNames) + { + ArgumentException.ThrowIfNullOrWhiteSpace(unredirectedLocalAppDataPath); + ArgumentNullException.ThrowIfNull(registeredPackageFamilyNames); + var localAppDataRoot = Path.GetFullPath(unredirectedLocalAppDataPath); + var roots = new List(); + var uniqueRoots = new HashSet(StringComparer.OrdinalIgnoreCase); + var packagesRoot = Path.Combine(localAppDataRoot, "Packages"); + + if (Directory.Exists(packagesRoot)) + { + foreach (var packageFamilyName in registeredPackageFamilyNames + .Where(IsSafeLegacyPackageFamilyName) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(static name => name, StringComparer.OrdinalIgnoreCase) + .ThenBy(static name => name, StringComparer.Ordinal)) + { + var candidate = Path.Combine( + packagesRoot, + packageFamilyName, + "LocalCache", + "Local", + LegacyDirectoryName); + AddIfSafeExistingDirectory(candidate, packagesRoot, roots, uniqueRoots); + } + } + + AddIfSafeExistingDirectory( + Path.Combine(localAppDataRoot, LegacyDirectoryName), + localAppDataRoot, + roots, + uniqueRoots); + return roots; + } + + private static string[] FindRegisteredLegacyPackageFamilyNames() + { + if (!OperatingSystem.IsWindows()) + { + return []; + } + + try + { + return new PackageManager() + .FindPackagesForUser(string.Empty) + .Where(static package => string.Equals( + package.Id.Name, + LegacyPackageName, + StringComparison.OrdinalIgnoreCase)) + .Select(static package => package.Id.FamilyName) + .Where(IsSafeLegacyPackageFamilyName) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(static name => name, StringComparer.OrdinalIgnoreCase) + .ThenBy(static name => name, StringComparer.Ordinal) + .ToArray(); + } + catch (Exception exception) when (exception is + COMException or + IOException or + InvalidOperationException or + PlatformNotSupportedException or + UnauthorizedAccessException) + { + // Package registration discovery is best effort. The unpackaged fallback is still checked. + return []; + } + } + + private static bool IsSafeLegacyPackageFamilyName(string? packageFamilyName) + { + const int MaximumPackageFamilyNameLength = 255; + var expectedPrefix = $"{LegacyPackageName}_"; + return !string.IsNullOrWhiteSpace(packageFamilyName) + && packageFamilyName.Length <= MaximumPackageFamilyNameLength + && packageFamilyName.StartsWith(expectedPrefix, StringComparison.OrdinalIgnoreCase) + && packageFamilyName.Length > expectedPrefix.Length + && packageFamilyName.IndexOfAny(Path.GetInvalidFileNameChars()) < 0 + && string.Equals(Path.GetFileName(packageFamilyName), packageFamilyName, StringComparison.Ordinal) + && packageFamilyName.IndexOfAny( + [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar]) < 0; + } + + private static void AddIfSafeExistingDirectory( + string candidate, + string containmentRoot, + List roots, + HashSet uniqueRoots) + { + var fullCandidate = Path.GetFullPath(candidate); + var fullContainmentRoot = Path.TrimEndingDirectorySeparator(Path.GetFullPath(containmentRoot)); + if (!IsWithin(fullCandidate, fullContainmentRoot) + || !Directory.Exists(fullCandidate) + || ContainsReparsePoint(fullCandidate, fullContainmentRoot)) + { + return; + } + + if (uniqueRoots.Add(fullCandidate)) + { + roots.Add(fullCandidate); + } + } + + private static bool ContainsReparsePoint(string path, string containmentRoot) + { + try + { + for (var current = new DirectoryInfo(path); + current is not null + && IsWithin(current.FullName, containmentRoot); + current = current.Parent) + { + if ((current.Attributes & FileAttributes.ReparsePoint) != 0) + { + return true; + } + + if (PathsEqual(current.FullName, containmentRoot)) + { + break; + } + } + } + catch (IOException) + { + return true; + } + catch (UnauthorizedAccessException) + { + return true; + } + + return false; + } + + private static bool IsWithin(string candidate, string root) + { + candidate = Path.TrimEndingDirectorySeparator(Path.GetFullPath(candidate)); + root = Path.TrimEndingDirectorySeparator(Path.GetFullPath(root)); + return PathsEqual(candidate, root) + || candidate.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase); + } + + private static bool PathsEqual(string first, string second) => + string.Equals( + Path.TrimEndingDirectorySeparator(Path.GetFullPath(first)), + Path.TrimEndingDirectorySeparator(Path.GetFullPath(second)), + StringComparison.OrdinalIgnoreCase); + + [SuppressMessage("Interoperability", "SYSLIB1054:Use LibraryImportAttribute instead of DllImportAttribute")] + [DllImport("shell32.dll", ExactSpelling = true)] + private static extern int SHGetKnownFolderPath( + ref Guid folderId, + uint flags, + nint token, + out nint path); +} diff --git a/src/LocaleSmith.App/Services/LegacyAppDataMigrator.cs b/src/LocaleSmith.App/Services/LegacyAppDataMigrator.cs new file mode 100644 index 0000000..9925e86 --- /dev/null +++ b/src/LocaleSmith.App/Services/LegacyAppDataMigrator.cs @@ -0,0 +1,260 @@ +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Infrastructure.Security; +using LocaleSmith.Presentation.Models; + +namespace LocaleSmith.App.Services; + +/// +/// Copies pre-LocaleSmith application state into the new storage namespace without deleting +/// the legacy state. The operation is idempotent so rollback to an older build remains possible. +/// +public sealed class LegacyAppDataMigrator +{ + public const string CurrentConfigurationFileName = "settings.localesmithcfg"; + public const string CurrentConfigurationPurpose = "LocaleSmith.ApplicationSettings.v1"; + public const string LegacyConfigurationFileName = "settings.jaxcfg"; + public const string LegacyConfigurationPurpose = "JaxI18n.ApplicationSettings.v1"; + public const string LegacyAssociatedDataNamespace = "JaxI18n.Config"; + + private readonly string _legacyRoot; + private readonly string _currentRoot; + private readonly ISecretStore _legacySecretStore; + private readonly ISecretStore _currentSecretStore; + + public LegacyAppDataMigrator( + string legacyRoot, + string currentRoot, + ISecretStore legacySecretStore, + ISecretStore currentSecretStore) + { + ArgumentException.ThrowIfNullOrWhiteSpace(legacyRoot); + ArgumentException.ThrowIfNullOrWhiteSpace(currentRoot); + _legacyRoot = Path.GetFullPath(legacyRoot); + _currentRoot = Path.GetFullPath(currentRoot); + if (PathsOverlap(_legacyRoot, _currentRoot)) + { + throw new ArgumentException("Legacy and current application-data roots must be separate and non-nested."); + } + + RejectReparsePoint(_legacyRoot); + RejectReparsePoint(_currentRoot); + + _legacySecretStore = legacySecretStore ?? throw new ArgumentNullException(nameof(legacySecretStore)); + _currentSecretStore = currentSecretStore ?? throw new ArgumentNullException(nameof(currentSecretStore)); + } + + public async Task MigrateAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + await MigrateConfigurationAsync(cancellationToken).ConfigureAwait(false); + await CopyDirectoryMissingFilesBestEffortAsync("translation-memory", cancellationToken).ConfigureAwait(false); + await CopyDirectoryMissingFilesBestEffortAsync("logs", cancellationToken).ConfigureAwait(false); + } + + private async Task MigrateConfigurationAsync(CancellationToken cancellationToken) + { + var legacyPath = Path.Combine(_legacyRoot, LegacyConfigurationFileName); + var currentPath = Path.Combine(_currentRoot, CurrentConfigurationFileName); + if (File.Exists(currentPath) || !File.Exists(legacyPath)) + { + return; + } + + var legacyMasterKeys = new CredentialManagerMasterKeyStore(_legacySecretStore); + var currentMasterKeys = new CredentialManagerMasterKeyStore(_currentSecretStore); + using var legacyConfiguration = new EncryptedJsonConfigurationStore( + legacyPath, + LegacyConfigurationPurpose, + legacyMasterKeys, + associatedDataNamespace: LegacyAssociatedDataNamespace); + using var currentConfiguration = new EncryptedJsonConfigurationStore( + currentPath, + CurrentConfigurationPurpose, + currentMasterKeys); + + var configuration = await legacyConfiguration.LoadAsync(cancellationToken).ConfigureAwait(false); + if (configuration is null) + { + return; + } + + configuration = LegacyDefaultPathNormalizer.Normalize(configuration, out _); + + foreach (var reference in configuration.ModelSources + .Select(static profile => profile.CredentialReference) + .Where(static reference => !string.IsNullOrWhiteSpace(reference)) + .Distinct(StringComparer.Ordinal)) + { + await MigrateSecretAsync(reference!, cancellationToken).ConfigureAwait(false); + } + + await currentConfiguration.SaveIfAbsentAsync(configuration, cancellationToken).ConfigureAwait(false); + } + + private async Task MigrateSecretAsync(string reference, CancellationToken cancellationToken) + { + var migratingStore = new MigratingSecretStore(_currentSecretStore, _legacySecretStore); + using var migrated = await migratingStore + .ResolveAsync(reference, cancellationToken) + .ConfigureAwait(false); + } + + private async Task CopyDirectoryMissingFilesBestEffortAsync( + string relativeDirectory, + CancellationToken cancellationToken) + { + var sourceRoot = Path.Combine(_legacyRoot, relativeDirectory); + if (!Directory.Exists(sourceRoot)) + { + return; + } + + var destinationRoot = Path.Combine(_currentRoot, relativeDirectory); + var enumeration = new EnumerationOptions + { + RecurseSubdirectories = true, + IgnoreInaccessible = false, + AttributesToSkip = FileAttributes.ReparsePoint + }; + + IEnumerable sourcePaths; + try + { + sourcePaths = Directory.EnumerateFiles(sourceRoot, "*", enumeration).ToArray(); + } + catch (IOException) + { + return; + } + catch (UnauthorizedAccessException) + { + return; + } + + foreach (var sourcePath in sourcePaths) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + var relativePath = Path.GetRelativePath(sourceRoot, sourcePath); + var destinationPath = ResolveContainedPath(destinationRoot, relativePath); + if (File.Exists(destinationPath)) + { + continue; + } + + var destinationDirectory = Path.GetDirectoryName(destinationPath) + ?? throw new InvalidOperationException("A migrated file has no parent directory."); + EnsureNoReparsePoint(destinationRoot, destinationDirectory); + Directory.CreateDirectory(destinationDirectory); + EnsureNoReparsePoint(destinationRoot, destinationDirectory); + var temporaryPath = $"{destinationPath}.{Guid.NewGuid():N}.migrating"; + try + { + await using (var source = new FileStream( + sourcePath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite | FileShare.Delete, + 16 * 1024, + FileOptions.Asynchronous | FileOptions.SequentialScan)) + await using (var destination = new FileStream( + temporaryPath, + FileMode.CreateNew, + FileAccess.Write, + FileShare.None, + 16 * 1024, + FileOptions.Asynchronous | FileOptions.WriteThrough)) + { + await source.CopyToAsync(destination, cancellationToken).ConfigureAwait(false); + await destination.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + try + { + File.Move(temporaryPath, destinationPath, overwrite: false); + } + catch (IOException) when (File.Exists(destinationPath)) + { + // A concurrent application instance completed the same migration first. + } + } + finally + { + if (File.Exists(temporaryPath)) + { + File.Delete(temporaryPath); + } + } + } + catch (IOException) + { + // Non-critical caches and logs remain in the legacy root and are retried next launch. + } + catch (UnauthorizedAccessException) + { + // Non-critical caches and logs must not prevent secure configuration migration. + } + } + } + + private static string ResolveContainedPath(string root, string relativePath) + { + var normalizedRoot = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar) + + Path.DirectorySeparatorChar; + var candidate = Path.GetFullPath(Path.Combine(normalizedRoot, relativePath)); + if (!candidate.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidDataException("A legacy application-data path escaped its migration root."); + } + + return candidate; + } + + private static bool PathsOverlap(string first, string second) + { + var normalizedFirst = first.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + var normalizedSecond = second.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + return normalizedFirst.StartsWith(normalizedSecond, StringComparison.OrdinalIgnoreCase) + || normalizedSecond.StartsWith(normalizedFirst, StringComparison.OrdinalIgnoreCase); + } + + private static void RejectReparsePoint(string path) + { + if (Directory.Exists(path) + && (File.GetAttributes(path) & FileAttributes.ReparsePoint) != 0) + { + throw new InvalidDataException("An application-data migration root cannot be a reparse point."); + } + } + + private static void EnsureNoReparsePoint(string root, string candidateDirectory) + { + var normalizedRoot = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar) + + Path.DirectorySeparatorChar; + var candidate = Path.GetFullPath(candidateDirectory); + if (!candidate.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase) + && !string.Equals( + candidate.TrimEnd(Path.DirectorySeparatorChar), + root.TrimEnd(Path.DirectorySeparatorChar), + StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidDataException("A migration destination escaped its application-data root."); + } + + for (var current = new DirectoryInfo(candidate); + current is not null + && (current.FullName.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase) + || string.Equals( + current.FullName.TrimEnd(Path.DirectorySeparatorChar), + root.TrimEnd(Path.DirectorySeparatorChar), + StringComparison.OrdinalIgnoreCase)); + current = current.Parent) + { + if (current.Exists && (current.Attributes & FileAttributes.ReparsePoint) != 0) + { + throw new InvalidDataException("A migration destination ancestor cannot be a reparse point."); + } + } + } +} diff --git a/src/LocaleSmith.App/Services/LegacyDefaultPathNormalizer.cs b/src/LocaleSmith.App/Services/LegacyDefaultPathNormalizer.cs new file mode 100644 index 0000000..2638962 --- /dev/null +++ b/src/LocaleSmith.App/Services/LegacyDefaultPathNormalizer.cs @@ -0,0 +1,53 @@ +using LocaleSmith.Presentation.Models; + +namespace LocaleSmith.App.Services; + +internal static class LegacyDefaultPathNormalizer +{ + private const string LegacyProductDirectoryName = "JaxI18n"; + private const string CurrentProductDirectoryName = "LocaleSmith"; + + public static AppConfiguration Normalize(AppConfiguration configuration, out bool changed) + { + ArgumentNullException.ThrowIfNull(configuration); + + var documentsRoot = System.Environment.GetFolderPath( + System.Environment.SpecialFolder.MyDocuments); + var legacyWorkspace = string.IsNullOrWhiteSpace(documentsRoot) + ? null + : Path.Combine(documentsRoot, LegacyProductDirectoryName); + var currentWorkspace = string.IsNullOrWhiteSpace(documentsRoot) + ? null + : Path.Combine(documentsRoot, CurrentProductDirectoryName); + var temporaryRoot = Path.GetTempPath(); + var legacySandbox = Path.Combine(temporaryRoot, LegacyProductDirectoryName, "Sandbox"); + var currentSandbox = Path.Combine(temporaryRoot, CurrentProductDirectoryName, "Sandbox"); + + var migrateWorkspace = legacyWorkspace is not null + && PathsEqual(configuration.WorkspacePath, legacyWorkspace); + var migrateSandbox = PathsEqual(configuration.SandboxPath, legacySandbox); + changed = migrateWorkspace || migrateSandbox; + if (!changed) + { + return configuration; + } + + return configuration with + { + WorkspacePath = migrateWorkspace ? currentWorkspace! : configuration.WorkspacePath, + SandboxPath = migrateSandbox ? currentSandbox : configuration.SandboxPath + }; + } + + private static bool PathsEqual(string candidate, string expected) + { + if (string.IsNullOrWhiteSpace(candidate)) + { + return false; + } + + var normalizedCandidate = Path.TrimEndingDirectorySeparator(Path.GetFullPath(candidate)); + var normalizedExpected = Path.TrimEndingDirectorySeparator(Path.GetFullPath(expected)); + return string.Equals(normalizedCandidate, normalizedExpected, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/JaxI18n.App/Services/ModelAssistantService.cs b/src/LocaleSmith.App/Services/ModelAssistantService.cs similarity index 96% rename from src/JaxI18n.App/Services/ModelAssistantService.cs rename to src/LocaleSmith.App/Services/ModelAssistantService.cs index 586efeb..3c36db1 100644 --- a/src/JaxI18n.App/Services/ModelAssistantService.cs +++ b/src/LocaleSmith.App/Services/ModelAssistantService.cs @@ -1,12 +1,12 @@ using System.Text.Json; -using JaxI18n.Application.Services; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; -using JaxI18n.Mcp; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; - -namespace JaxI18n.App.Services; +using LocaleSmith.Application.Services; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; +using LocaleSmith.Mcp; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; + +namespace LocaleSmith.App.Services; public sealed class ModelAssistantService( IModelServiceRegistry modelRegistry, diff --git a/src/JaxI18n.App/Services/PipelineTranslationQueueService.cs b/src/LocaleSmith.App/Services/PipelineTranslationQueueService.cs similarity index 94% rename from src/JaxI18n.App/Services/PipelineTranslationQueueService.cs rename to src/LocaleSmith.App/Services/PipelineTranslationQueueService.cs index 8229926..92fd981 100644 --- a/src/JaxI18n.App/Services/PipelineTranslationQueueService.cs +++ b/src/LocaleSmith.App/Services/PipelineTranslationQueueService.cs @@ -1,10 +1,10 @@ -using JaxI18n.Application.Abstractions; -using JaxI18n.Application.Models; -using JaxI18n.Core.Models; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; +using LocaleSmith.Application.Abstractions; +using LocaleSmith.Application.Models; +using LocaleSmith.Core.Models; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; -namespace JaxI18n.App.Services; +namespace LocaleSmith.App.Services; public sealed class PipelineTranslationQueueService : ITranslationQueueService { diff --git a/src/JaxI18n.App/Services/SecureAppStateService.cs b/src/LocaleSmith.App/Services/SecureAppStateService.cs similarity index 98% rename from src/JaxI18n.App/Services/SecureAppStateService.cs rename to src/LocaleSmith.App/Services/SecureAppStateService.cs index 678fce7..136e52e 100644 --- a/src/JaxI18n.App/Services/SecureAppStateService.cs +++ b/src/LocaleSmith.App/Services/SecureAppStateService.cs @@ -2,14 +2,14 @@ using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; -using JaxI18n.Core.Services; -using JaxI18n.Infrastructure.Models; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; +using LocaleSmith.Core.Services; +using LocaleSmith.Infrastructure.Models; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; -namespace JaxI18n.App.Services; +namespace LocaleSmith.App.Services; public sealed partial class SecureAppStateService : IAppConfigurationService, @@ -61,9 +61,16 @@ public async Task InitializeAsync(CancellationToken cancellationToken = default) return; } - _configuration = await _configurationStore.LoadAsync(cancellationToken).ConfigureAwait(false) + var loaded = await _configurationStore.LoadAsync(cancellationToken).ConfigureAwait(false) ?? new AppConfiguration(); - ValidateConfiguration(_configuration); + var normalized = LegacyDefaultPathNormalizer.Normalize(loaded, out var defaultsChanged); + ValidateConfiguration(normalized); + if (defaultsChanged) + { + await _configurationStore.SaveAsync(normalized, cancellationToken).ConfigureAwait(false); + } + + _configuration = normalized; ApplySandboxRoots(_configuration); selectionStateChanged = ReconcileRegistry(_configuration); _initialized = true; diff --git a/src/JaxI18n.App/Services/WinUiTextProvider.cs b/src/LocaleSmith.App/Services/WinUiTextProvider.cs similarity index 89% rename from src/JaxI18n.App/Services/WinUiTextProvider.cs rename to src/LocaleSmith.App/Services/WinUiTextProvider.cs index 6c34357..d24cf2c 100644 --- a/src/JaxI18n.App/Services/WinUiTextProvider.cs +++ b/src/LocaleSmith.App/Services/WinUiTextProvider.cs @@ -1,7 +1,7 @@ -using JaxI18n.Presentation.Abstractions; +using LocaleSmith.Presentation.Abstractions; using Windows.ApplicationModel.Resources; -namespace JaxI18n.App.Services; +namespace LocaleSmith.App.Services; public sealed class WinUiTextProvider : IUiTextProvider { diff --git a/src/JaxI18n.App/Strings/en-US/Resources.resw b/src/LocaleSmith.App/Strings/en-US/Resources.resw similarity index 100% rename from src/JaxI18n.App/Strings/en-US/Resources.resw rename to src/LocaleSmith.App/Strings/en-US/Resources.resw diff --git a/src/JaxI18n.App/Strings/zh-CN/Resources.resw b/src/LocaleSmith.App/Strings/zh-CN/Resources.resw similarity index 99% rename from src/JaxI18n.App/Strings/zh-CN/Resources.resw rename to src/LocaleSmith.App/Strings/zh-CN/Resources.resw index b6213df..a52e02c 100644 --- a/src/JaxI18n.App/Strings/zh-CN/Resources.resw +++ b/src/LocaleSmith.App/Strings/zh-CN/Resources.resw @@ -4,7 +4,7 @@ 2.0 System.Resources.ResXResourceReader, System.Windows.Forms System.Resources.ResXResourceWriter, System.Windows.Forms - LocaleSmith(译匠) + LocaleSmith | 译匠 处理队列 模型源 应用无法安全启动 @@ -13,7 +13,7 @@ 首次设置进度 需要处理 设置状态 - 欢迎使用 LocaleSmith(译匠) + 欢迎使用 LocaleSmith | 译匠 批量提取、增量翻译并重建 Minecraft 模组与资源包;每个任务选择一种翻译风格,并生成一个对应产物。 本地 Ollama 可完全在本机工作;云端模型仅在你配置后使用。API Key 不写入配置或日志。 选择首次模型连接方式 @@ -134,8 +134,8 @@ 强制启用应用动画 遵循系统设置 已强制启用 - 强制启用 LocaleSmith(译匠)应用动画 - 默认遵循 Windows“动画效果”设置。开启后,即使系统关闭动画,LocaleSmith(译匠)自己的页面与状态过渡仍会运行;不会修改 Windows 或其他应用的设置。 + 强制启用 LocaleSmith | 译匠应用动画 + 默认遵循 Windows“动画效果”设置。开启后,即使系统关闭动画,LocaleSmith | 译匠自己的页面与状态过渡仍会运行;不会修改 Windows 或其他应用的设置。 输出工作区 翻译工作区根目录 翻译工作区根目录路径 diff --git a/src/JaxI18n.App/Themes/Controls.xaml b/src/LocaleSmith.App/Themes/Controls.xaml similarity index 97% rename from src/JaxI18n.App/Themes/Controls.xaml rename to src/LocaleSmith.App/Themes/Controls.xaml index 0d72f06..e9f7e5c 100644 --- a/src/JaxI18n.App/Themes/Controls.xaml +++ b/src/LocaleSmith.App/Themes/Controls.xaml @@ -1,7 +1,7 @@ + xmlns:behaviors="using:LocaleSmith.App.Behaviors"> diff --git a/src/JaxI18n.App/app.manifest b/src/LocaleSmith.App/app.manifest similarity index 93% rename from src/JaxI18n.App/app.manifest rename to src/LocaleSmith.App/app.manifest index ea21b7a..7064957 100644 --- a/src/JaxI18n.App/app.manifest +++ b/src/LocaleSmith.App/app.manifest @@ -1,6 +1,6 @@ - + diff --git a/src/JaxI18n.Application/Abstractions/IArchiveWorkspace.cs b/src/LocaleSmith.Application/Abstractions/IArchiveWorkspace.cs similarity index 90% rename from src/JaxI18n.Application/Abstractions/IArchiveWorkspace.cs rename to src/LocaleSmith.Application/Abstractions/IArchiveWorkspace.cs index 18e0fc9..bb727bf 100644 --- a/src/JaxI18n.Application/Abstractions/IArchiveWorkspace.cs +++ b/src/LocaleSmith.Application/Abstractions/IArchiveWorkspace.cs @@ -1,7 +1,7 @@ -using JaxI18n.Application.Models; -using JaxI18n.Core.Models; +using LocaleSmith.Application.Models; +using LocaleSmith.Core.Models; -namespace JaxI18n.Application.Abstractions; +namespace LocaleSmith.Application.Abstractions; public interface IArchiveWorkspaceBackend { diff --git a/src/JaxI18n.Application/Abstractions/IPipelineJobScheduler.cs b/src/LocaleSmith.Application/Abstractions/IPipelineJobScheduler.cs similarity index 67% rename from src/JaxI18n.Application/Abstractions/IPipelineJobScheduler.cs rename to src/LocaleSmith.Application/Abstractions/IPipelineJobScheduler.cs index 68dceae..b9888a8 100644 --- a/src/JaxI18n.Application/Abstractions/IPipelineJobScheduler.cs +++ b/src/LocaleSmith.Application/Abstractions/IPipelineJobScheduler.cs @@ -1,7 +1,7 @@ -using JaxI18n.Application.Models; -using JaxI18n.Application.Services; +using LocaleSmith.Application.Models; +using LocaleSmith.Application.Services; -namespace JaxI18n.Application.Abstractions; +namespace LocaleSmith.Application.Abstractions; public interface IPipelineJobScheduler : IAsyncDisposable { diff --git a/src/JaxI18n.Application/Abstractions/ITranslationEngine.cs b/src/LocaleSmith.Application/Abstractions/ITranslationEngine.cs similarity index 74% rename from src/JaxI18n.Application/Abstractions/ITranslationEngine.cs rename to src/LocaleSmith.Application/Abstractions/ITranslationEngine.cs index 1613c0a..3eaac22 100644 --- a/src/JaxI18n.Application/Abstractions/ITranslationEngine.cs +++ b/src/LocaleSmith.Application/Abstractions/ITranslationEngine.cs @@ -1,6 +1,6 @@ -using JaxI18n.Core.Models; +using LocaleSmith.Core.Models; -namespace JaxI18n.Application.Abstractions; +namespace LocaleSmith.Application.Abstractions; public interface ITranslationEngine { diff --git a/src/JaxI18n.Application/Abstractions/ITranslationMemoryStore.cs b/src/LocaleSmith.Application/Abstractions/ITranslationMemoryStore.cs similarity index 76% rename from src/JaxI18n.Application/Abstractions/ITranslationMemoryStore.cs rename to src/LocaleSmith.Application/Abstractions/ITranslationMemoryStore.cs index df155df..fc187c6 100644 --- a/src/JaxI18n.Application/Abstractions/ITranslationMemoryStore.cs +++ b/src/LocaleSmith.Application/Abstractions/ITranslationMemoryStore.cs @@ -1,6 +1,6 @@ -using JaxI18n.Application.Models; +using LocaleSmith.Application.Models; -namespace JaxI18n.Application.Abstractions; +namespace LocaleSmith.Application.Abstractions; public interface ITranslationMemoryStore { diff --git a/src/LocaleSmith.Application/LocaleSmith.Application.csproj b/src/LocaleSmith.Application/LocaleSmith.Application.csproj new file mode 100644 index 0000000..e1e716b --- /dev/null +++ b/src/LocaleSmith.Application/LocaleSmith.Application.csproj @@ -0,0 +1,15 @@ + + + net10.0 + LocaleSmith.Application + LocaleSmith.Application + + + + + + + + + + diff --git a/src/JaxI18n.Application/Models/PipelineModels.cs b/src/LocaleSmith.Application/Models/PipelineModels.cs similarity index 98% rename from src/JaxI18n.Application/Models/PipelineModels.cs rename to src/LocaleSmith.Application/Models/PipelineModels.cs index 84b2005..4115045 100644 --- a/src/JaxI18n.Application/Models/PipelineModels.cs +++ b/src/LocaleSmith.Application/Models/PipelineModels.cs @@ -1,6 +1,6 @@ -using JaxI18n.Core.Models; +using LocaleSmith.Core.Models; -namespace JaxI18n.Application.Models; +namespace LocaleSmith.Application.Models; public enum ArchiveSignatureState { diff --git a/src/JaxI18n.Application/Models/TranslationMemoryModels.cs b/src/LocaleSmith.Application/Models/TranslationMemoryModels.cs similarity index 80% rename from src/JaxI18n.Application/Models/TranslationMemoryModels.cs rename to src/LocaleSmith.Application/Models/TranslationMemoryModels.cs index 1127753..8a66c28 100644 --- a/src/JaxI18n.Application/Models/TranslationMemoryModels.cs +++ b/src/LocaleSmith.Application/Models/TranslationMemoryModels.cs @@ -1,8 +1,8 @@ using System.Text; using System.Text.Json.Serialization; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Models; -namespace JaxI18n.Application.Models; +namespace LocaleSmith.Application.Models; public static class TranslationPromptContract { @@ -16,7 +16,8 @@ public static class TranslationPromptContract public sealed record TranslationMemoryKey { - private const string StorageKeySchema = "jax-i18n.translation-memory/v2"; + private const string StorageKeySchema = "localesmith.translation-memory/v2"; + private const string LegacyStorageKeySchema = "jax-i18n.translation-memory/v2"; public TranslationMemoryKey( string rawPackageIdentity, @@ -41,7 +42,19 @@ public TranslationMemoryKey( public string PackageIdentity => CreateStoragePackageIdentity( RawPackageIdentity, ModelSourceId, - TranslationContractVersion); + TranslationContractVersion, + StorageKeySchema); + + /// + /// Gets the pre-LocaleSmith identity used only to discover and migrate existing cache files. + /// New snapshots are never written with this identity. + /// + [JsonIgnore] + public string LegacyPackageIdentity => CreateStoragePackageIdentity( + RawPackageIdentity, + ModelSourceId, + TranslationContractVersion, + LegacyStorageKeySchema); public string TargetLanguage { get; } @@ -64,7 +77,8 @@ public TranslationMemoryKey Normalize() private static string CreateStoragePackageIdentity( string rawPackageIdentity, string? modelSourceId, - string translationContractVersion) + string translationContractVersion, + string storageKeySchema) { ArgumentException.ThrowIfNullOrWhiteSpace(rawPackageIdentity); ArgumentException.ThrowIfNullOrWhiteSpace(translationContractVersion); @@ -77,7 +91,7 @@ private static string CreateStoragePackageIdentity( // The leading NUL also keeps this namespace disjoint from legacy package identities, // which originate in archive metadata or file names and cannot contain NUL characters. - return $"\0{StorageKeySchema}\0package:{Encode(rawPackageIdentity.Trim())}" + + return $"\0{storageKeySchema}\0package:{Encode(rawPackageIdentity.Trim())}" + $"\0source:{sourceComponent}\0contract:{Encode(translationContractVersion.Trim())}"; } diff --git a/src/JaxI18n.Application/PipelineException.cs b/src/LocaleSmith.Application/PipelineException.cs similarity index 87% rename from src/JaxI18n.Application/PipelineException.cs rename to src/LocaleSmith.Application/PipelineException.cs index a7f9fb0..a5c58d2 100644 --- a/src/JaxI18n.Application/PipelineException.cs +++ b/src/LocaleSmith.Application/PipelineException.cs @@ -1,6 +1,6 @@ -using JaxI18n.Application.Models; +using LocaleSmith.Application.Models; -namespace JaxI18n.Application; +namespace LocaleSmith.Application; public sealed class PipelineException : Exception { diff --git a/src/JaxI18n.Application/Services/ModelToolOrchestrator.cs b/src/LocaleSmith.Application/Services/ModelToolOrchestrator.cs similarity index 98% rename from src/JaxI18n.Application/Services/ModelToolOrchestrator.cs rename to src/LocaleSmith.Application/Services/ModelToolOrchestrator.cs index 4e83cd5..3c2e90e 100644 --- a/src/JaxI18n.Application/Services/ModelToolOrchestrator.cs +++ b/src/LocaleSmith.Application/Services/ModelToolOrchestrator.cs @@ -1,7 +1,7 @@ -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Application.Services; +namespace LocaleSmith.Application.Services; public sealed class ModelToolOrchestrator { diff --git a/src/JaxI18n.Application/Services/ModelTranslationEngine.cs b/src/LocaleSmith.Application/Services/ModelTranslationEngine.cs similarity index 97% rename from src/JaxI18n.Application/Services/ModelTranslationEngine.cs rename to src/LocaleSmith.Application/Services/ModelTranslationEngine.cs index cf6a2a7..207c592 100644 --- a/src/JaxI18n.Application/Services/ModelTranslationEngine.cs +++ b/src/LocaleSmith.Application/Services/ModelTranslationEngine.cs @@ -1,11 +1,11 @@ using System.Text.Json; -using JaxI18n.Application.Abstractions; -using JaxI18n.Application.Models; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; -using JaxI18n.Core.Services; +using LocaleSmith.Application.Abstractions; +using LocaleSmith.Application.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; +using LocaleSmith.Core.Services; -namespace JaxI18n.Application.Services; +namespace LocaleSmith.Application.Services; public sealed class ModelTranslationEngine : ITranslationEngine { diff --git a/src/JaxI18n.Application/Services/ModelTranslationEngineOptions.cs b/src/LocaleSmith.Application/Services/ModelTranslationEngineOptions.cs similarity index 94% rename from src/JaxI18n.Application/Services/ModelTranslationEngineOptions.cs rename to src/LocaleSmith.Application/Services/ModelTranslationEngineOptions.cs index 38e2e85..55dd6df 100644 --- a/src/JaxI18n.Application/Services/ModelTranslationEngineOptions.cs +++ b/src/LocaleSmith.Application/Services/ModelTranslationEngineOptions.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Application.Services; +namespace LocaleSmith.Application.Services; public sealed record ModelTranslationEngineOptions { diff --git a/src/JaxI18n.Application/Services/PipelineJobHandle.cs b/src/LocaleSmith.Application/Services/PipelineJobHandle.cs similarity index 91% rename from src/JaxI18n.Application/Services/PipelineJobHandle.cs rename to src/LocaleSmith.Application/Services/PipelineJobHandle.cs index 56b2725..05b33c4 100644 --- a/src/JaxI18n.Application/Services/PipelineJobHandle.cs +++ b/src/LocaleSmith.Application/Services/PipelineJobHandle.cs @@ -1,6 +1,6 @@ -using JaxI18n.Application.Models; +using LocaleSmith.Application.Models; -namespace JaxI18n.Application.Services; +namespace LocaleSmith.Application.Services; public sealed class PipelineJobHandle { diff --git a/src/JaxI18n.Application/Services/PipelineQueue.cs b/src/LocaleSmith.Application/Services/PipelineQueue.cs similarity index 97% rename from src/JaxI18n.Application/Services/PipelineQueue.cs rename to src/LocaleSmith.Application/Services/PipelineQueue.cs index 238779e..f46c85c 100644 --- a/src/JaxI18n.Application/Services/PipelineQueue.cs +++ b/src/LocaleSmith.Application/Services/PipelineQueue.cs @@ -1,8 +1,8 @@ using System.Threading.Channels; -using JaxI18n.Application.Abstractions; -using JaxI18n.Application.Models; +using LocaleSmith.Application.Abstractions; +using LocaleSmith.Application.Models; -namespace JaxI18n.Application.Services; +namespace LocaleSmith.Application.Services; public sealed class PipelineJobScheduler : IPipelineJobScheduler { diff --git a/src/JaxI18n.Application/Services/PlaceholderValidator.cs b/src/LocaleSmith.Application/Services/PlaceholderValidator.cs similarity index 97% rename from src/JaxI18n.Application/Services/PlaceholderValidator.cs rename to src/LocaleSmith.Application/Services/PlaceholderValidator.cs index 9672f64..e6ee726 100644 --- a/src/JaxI18n.Application/Services/PlaceholderValidator.cs +++ b/src/LocaleSmith.Application/Services/PlaceholderValidator.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace JaxI18n.Application.Services; +namespace LocaleSmith.Application.Services; internal static partial class PlaceholderValidator { diff --git a/src/JaxI18n.Application/Services/TranslationJsonModels.cs b/src/LocaleSmith.Application/Services/TranslationJsonModels.cs similarity index 95% rename from src/JaxI18n.Application/Services/TranslationJsonModels.cs rename to src/LocaleSmith.Application/Services/TranslationJsonModels.cs index 2c66dde..94d7250 100644 --- a/src/JaxI18n.Application/Services/TranslationJsonModels.cs +++ b/src/LocaleSmith.Application/Services/TranslationJsonModels.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace JaxI18n.Application.Services; +namespace LocaleSmith.Application.Services; internal sealed record PromptTranslationItem( string Id, diff --git a/src/JaxI18n.Application/Services/TranslationPipeline.cs b/src/LocaleSmith.Application/Services/TranslationPipeline.cs similarity index 99% rename from src/JaxI18n.Application/Services/TranslationPipeline.cs rename to src/LocaleSmith.Application/Services/TranslationPipeline.cs index 065605b..8ea1b02 100644 --- a/src/JaxI18n.Application/Services/TranslationPipeline.cs +++ b/src/LocaleSmith.Application/Services/TranslationPipeline.cs @@ -1,9 +1,9 @@ -using JaxI18n.Application.Abstractions; -using JaxI18n.Application.Models; -using JaxI18n.Core.Models; -using JaxI18n.Core.Services; +using LocaleSmith.Application.Abstractions; +using LocaleSmith.Application.Models; +using LocaleSmith.Core.Models; +using LocaleSmith.Core.Services; -namespace JaxI18n.Application.Services; +namespace LocaleSmith.Application.Services; public sealed class TranslationPipeline { diff --git a/src/JaxI18n.Application/TranslationContractException.cs b/src/LocaleSmith.Application/TranslationContractException.cs similarity index 89% rename from src/JaxI18n.Application/TranslationContractException.cs rename to src/LocaleSmith.Application/TranslationContractException.cs index d74bc07..59b54a5 100644 --- a/src/JaxI18n.Application/TranslationContractException.cs +++ b/src/LocaleSmith.Application/TranslationContractException.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Application; +namespace LocaleSmith.Application; public sealed class TranslationContractException : Exception { diff --git a/src/JaxI18n.Archive/ArchiveEntrySnapshot.cs b/src/LocaleSmith.Archive/ArchiveEntrySnapshot.cs similarity index 96% rename from src/JaxI18n.Archive/ArchiveEntrySnapshot.cs rename to src/LocaleSmith.Archive/ArchiveEntrySnapshot.cs index 405f0f3..16096fc 100644 --- a/src/JaxI18n.Archive/ArchiveEntrySnapshot.cs +++ b/src/LocaleSmith.Archive/ArchiveEntrySnapshot.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Archive; +namespace LocaleSmith.Archive; internal sealed record ArchiveEntrySnapshot( int Index, diff --git a/src/JaxI18n.Archive/ArchivePathSafety.cs b/src/LocaleSmith.Archive/ArchivePathSafety.cs similarity index 99% rename from src/JaxI18n.Archive/ArchivePathSafety.cs rename to src/LocaleSmith.Archive/ArchivePathSafety.cs index e104afd..dbb9ea4 100644 --- a/src/JaxI18n.Archive/ArchivePathSafety.cs +++ b/src/LocaleSmith.Archive/ArchivePathSafety.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Archive; +namespace LocaleSmith.Archive; internal static class ArchivePathSafety { diff --git a/src/JaxI18n.Archive/ArchiveWorkspace.cs b/src/LocaleSmith.Archive/ArchiveWorkspace.cs similarity index 99% rename from src/JaxI18n.Archive/ArchiveWorkspace.cs rename to src/LocaleSmith.Archive/ArchiveWorkspace.cs index e37f48e..48529da 100644 --- a/src/JaxI18n.Archive/ArchiveWorkspace.cs +++ b/src/LocaleSmith.Archive/ArchiveWorkspace.cs @@ -3,13 +3,13 @@ using System.IO.Compression; using System.Security.Cryptography; using System.Text; -using JaxI18n.Application.Abstractions; -using JaxI18n.Application.Models; -using JaxI18n.Archive.ClassFile; -using JaxI18n.Core.Models; -using JaxI18n.NativeInterop; +using LocaleSmith.Application.Abstractions; +using LocaleSmith.Application.Models; +using LocaleSmith.Archive.ClassFile; +using LocaleSmith.Core.Models; +using LocaleSmith.NativeInterop; -namespace JaxI18n.Archive; +namespace LocaleSmith.Archive; internal sealed class ArchiveWorkspace : IArchiveWorkspace { diff --git a/src/JaxI18n.Archive/ArchiveWorkspaceBackend.cs b/src/LocaleSmith.Archive/ArchiveWorkspaceBackend.cs similarity index 97% rename from src/JaxI18n.Archive/ArchiveWorkspaceBackend.cs rename to src/LocaleSmith.Archive/ArchiveWorkspaceBackend.cs index 647e7a4..a473b43 100644 --- a/src/JaxI18n.Archive/ArchiveWorkspaceBackend.cs +++ b/src/LocaleSmith.Archive/ArchiveWorkspaceBackend.cs @@ -1,7 +1,7 @@ -using JaxI18n.Application.Abstractions; -using JaxI18n.Application.Models; +using LocaleSmith.Application.Abstractions; +using LocaleSmith.Application.Models; -namespace JaxI18n.Archive; +namespace LocaleSmith.Archive; public sealed class ArchiveWorkspaceBackend : IArchiveWorkspaceBackend { @@ -52,7 +52,7 @@ public async Task BeginAsync( } string tempRoot = ArchivePathSafety.Canonicalize(Path.GetTempPath()); - string productRoot = Path.Combine(tempRoot, "JaxI18n"); + string productRoot = Path.Combine(tempRoot, "LocaleSmith"); string workspacesRoot = Path.Combine(productRoot, "workspaces"); string logsRoot = Path.Combine(productRoot, "logs"); string workspacePath = Path.Combine(workspacesRoot, jobId.ToString("N")); diff --git a/src/JaxI18n.Archive/ArchiveWorkspaceOptions.cs b/src/LocaleSmith.Archive/ArchiveWorkspaceOptions.cs similarity index 98% rename from src/JaxI18n.Archive/ArchiveWorkspaceOptions.cs rename to src/LocaleSmith.Archive/ArchiveWorkspaceOptions.cs index 00a0063..2b92031 100644 --- a/src/JaxI18n.Archive/ArchiveWorkspaceOptions.cs +++ b/src/LocaleSmith.Archive/ArchiveWorkspaceOptions.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Archive; +namespace LocaleSmith.Archive; public sealed record ArchiveWorkspaceOptions { diff --git a/src/JaxI18n.Archive/ClassFile/ClassFileBinary.cs b/src/LocaleSmith.Archive/ClassFile/ClassFileBinary.cs similarity index 99% rename from src/JaxI18n.Archive/ClassFile/ClassFileBinary.cs rename to src/LocaleSmith.Archive/ClassFile/ClassFileBinary.cs index 8fd8464..8f98c09 100644 --- a/src/JaxI18n.Archive/ClassFile/ClassFileBinary.cs +++ b/src/LocaleSmith.Archive/ClassFile/ClassFileBinary.cs @@ -1,7 +1,7 @@ using System.Buffers.Binary; using System.Text; -namespace JaxI18n.Archive.ClassFile; +namespace LocaleSmith.Archive.ClassFile; internal ref struct ClassFileReader { diff --git a/src/JaxI18n.Archive/ClassFile/ClassFileRewriteModels.cs b/src/LocaleSmith.Archive/ClassFile/ClassFileRewriteModels.cs similarity index 97% rename from src/JaxI18n.Archive/ClassFile/ClassFileRewriteModels.cs rename to src/LocaleSmith.Archive/ClassFile/ClassFileRewriteModels.cs index 845e471..33c158b 100644 --- a/src/JaxI18n.Archive/ClassFile/ClassFileRewriteModels.cs +++ b/src/LocaleSmith.Archive/ClassFile/ClassFileRewriteModels.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Archive.ClassFile; +namespace LocaleSmith.Archive.ClassFile; /// /// Describes one string constant loaded by an ldc or ldc_w diff --git a/src/JaxI18n.Archive/ClassFile/JavaBytecodeDecoder.cs b/src/LocaleSmith.Archive/ClassFile/JavaBytecodeDecoder.cs similarity index 99% rename from src/JaxI18n.Archive/ClassFile/JavaBytecodeDecoder.cs rename to src/LocaleSmith.Archive/ClassFile/JavaBytecodeDecoder.cs index 9d941da..7bf72ee 100644 --- a/src/JaxI18n.Archive/ClassFile/JavaBytecodeDecoder.cs +++ b/src/LocaleSmith.Archive/ClassFile/JavaBytecodeDecoder.cs @@ -1,6 +1,6 @@ using System.Buffers.Binary; -namespace JaxI18n.Archive.ClassFile; +namespace LocaleSmith.Archive.ClassFile; internal sealed record JavaInstruction( int Offset, diff --git a/src/JaxI18n.Archive/ClassFile/MojangComponentLiteralClassFileRewriter.cs b/src/LocaleSmith.Archive/ClassFile/MojangComponentLiteralClassFileRewriter.cs similarity index 99% rename from src/JaxI18n.Archive/ClassFile/MojangComponentLiteralClassFileRewriter.cs rename to src/LocaleSmith.Archive/ClassFile/MojangComponentLiteralClassFileRewriter.cs index ec33dd4..e87e06c 100644 --- a/src/JaxI18n.Archive/ClassFile/MojangComponentLiteralClassFileRewriter.cs +++ b/src/LocaleSmith.Archive/ClassFile/MojangComponentLiteralClassFileRewriter.cs @@ -2,7 +2,7 @@ using System.Security.Cryptography; using System.Text; -namespace JaxI18n.Archive.ClassFile; +namespace LocaleSmith.Archive.ClassFile; /// /// Analyzes and rewrites only the verified Mojang Component.literal safe diff --git a/src/JaxI18n.Archive/ClassFile/ParsedClassFile.cs b/src/LocaleSmith.Archive/ClassFile/ParsedClassFile.cs similarity index 99% rename from src/JaxI18n.Archive/ClassFile/ParsedClassFile.cs rename to src/LocaleSmith.Archive/ClassFile/ParsedClassFile.cs index 6da27ce..4a832b9 100644 --- a/src/JaxI18n.Archive/ClassFile/ParsedClassFile.cs +++ b/src/LocaleSmith.Archive/ClassFile/ParsedClassFile.cs @@ -1,6 +1,6 @@ using System.Buffers.Binary; -namespace JaxI18n.Archive.ClassFile; +namespace LocaleSmith.Archive.ClassFile; internal sealed record ParsedExceptionHandler( int StartOffset, diff --git a/src/JaxI18n.Archive/FolderSnapshotBuilder.cs b/src/LocaleSmith.Archive/FolderSnapshotBuilder.cs similarity index 99% rename from src/JaxI18n.Archive/FolderSnapshotBuilder.cs rename to src/LocaleSmith.Archive/FolderSnapshotBuilder.cs index c5d4284..9c4fc18 100644 --- a/src/JaxI18n.Archive/FolderSnapshotBuilder.cs +++ b/src/LocaleSmith.Archive/FolderSnapshotBuilder.cs @@ -6,7 +6,7 @@ using System.Text; using Microsoft.Win32.SafeHandles; -namespace JaxI18n.Archive; +namespace LocaleSmith.Archive; internal static class FolderSnapshotBuilder { diff --git a/src/JaxI18n.Archive/IArchiveScanner.cs b/src/LocaleSmith.Archive/IArchiveScanner.cs similarity index 89% rename from src/JaxI18n.Archive/IArchiveScanner.cs rename to src/LocaleSmith.Archive/IArchiveScanner.cs index 7b723a2..674e414 100644 --- a/src/JaxI18n.Archive/IArchiveScanner.cs +++ b/src/LocaleSmith.Archive/IArchiveScanner.cs @@ -1,6 +1,6 @@ -using JaxI18n.NativeInterop; +using LocaleSmith.NativeInterop; -namespace JaxI18n.Archive; +namespace LocaleSmith.Archive; public interface IArchiveScanner { diff --git a/src/JaxI18n.Archive/JsonResourceEditor.cs b/src/LocaleSmith.Archive/JsonResourceEditor.cs similarity index 99% rename from src/JaxI18n.Archive/JsonResourceEditor.cs rename to src/LocaleSmith.Archive/JsonResourceEditor.cs index e459bf0..3742068 100644 --- a/src/JaxI18n.Archive/JsonResourceEditor.cs +++ b/src/LocaleSmith.Archive/JsonResourceEditor.cs @@ -2,7 +2,7 @@ using System.Text.Json; using System.Text.Json.Nodes; -namespace JaxI18n.Archive; +namespace LocaleSmith.Archive; internal static class JsonResourceEditor { diff --git a/src/JaxI18n.Archive/LangResourceEditor.cs b/src/LocaleSmith.Archive/LangResourceEditor.cs similarity index 98% rename from src/JaxI18n.Archive/LangResourceEditor.cs rename to src/LocaleSmith.Archive/LangResourceEditor.cs index 9d495e5..6933e94 100644 --- a/src/JaxI18n.Archive/LangResourceEditor.cs +++ b/src/LocaleSmith.Archive/LangResourceEditor.cs @@ -1,6 +1,6 @@ using System.Text; -namespace JaxI18n.Archive; +namespace LocaleSmith.Archive; internal static class LangResourceEditor { diff --git a/src/LocaleSmith.Archive/LocaleSmith.Archive.csproj b/src/LocaleSmith.Archive/LocaleSmith.Archive.csproj new file mode 100644 index 0000000..1140eab --- /dev/null +++ b/src/LocaleSmith.Archive/LocaleSmith.Archive.csproj @@ -0,0 +1,19 @@ + + + net10.0-windows10.0.17763.0 + LocaleSmith.Archive + LocaleSmith.Archive + + + + + + + + + + + diff --git a/src/JaxI18n.Archive/TransactionJournal.cs b/src/LocaleSmith.Archive/TransactionJournal.cs similarity index 97% rename from src/JaxI18n.Archive/TransactionJournal.cs rename to src/LocaleSmith.Archive/TransactionJournal.cs index 6f086cc..bea9052 100644 --- a/src/JaxI18n.Archive/TransactionJournal.cs +++ b/src/LocaleSmith.Archive/TransactionJournal.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace JaxI18n.Archive; +namespace LocaleSmith.Archive; internal sealed class TransactionJournal { diff --git a/src/JaxI18n.Core/Abstractions/CliContracts.cs b/src/LocaleSmith.Core/Abstractions/CliContracts.cs similarity index 93% rename from src/JaxI18n.Core/Abstractions/CliContracts.cs rename to src/LocaleSmith.Core/Abstractions/CliContracts.cs index 8bffe74..41c8806 100644 --- a/src/JaxI18n.Core/Abstractions/CliContracts.cs +++ b/src/LocaleSmith.Core/Abstractions/CliContracts.cs @@ -1,6 +1,6 @@ -using JaxI18n.Core.Models; +using LocaleSmith.Core.Models; -namespace JaxI18n.Core.Abstractions; +namespace LocaleSmith.Core.Abstractions; public interface ICliCommandPolicy { diff --git a/src/JaxI18n.Core/Abstractions/IModelService.cs b/src/LocaleSmith.Core/Abstractions/IModelService.cs similarity index 88% rename from src/JaxI18n.Core/Abstractions/IModelService.cs rename to src/LocaleSmith.Core/Abstractions/IModelService.cs index ac68be0..6c94a5f 100644 --- a/src/JaxI18n.Core/Abstractions/IModelService.cs +++ b/src/LocaleSmith.Core/Abstractions/IModelService.cs @@ -1,6 +1,6 @@ -using JaxI18n.Core.Models; +using LocaleSmith.Core.Models; -namespace JaxI18n.Core.Abstractions; +namespace LocaleSmith.Core.Abstractions; public interface IModelService { diff --git a/src/JaxI18n.Core/Abstractions/IModelServiceRegistry.cs b/src/LocaleSmith.Core/Abstractions/IModelServiceRegistry.cs similarity index 90% rename from src/JaxI18n.Core/Abstractions/IModelServiceRegistry.cs rename to src/LocaleSmith.Core/Abstractions/IModelServiceRegistry.cs index 26788b3..4ef4127 100644 --- a/src/JaxI18n.Core/Abstractions/IModelServiceRegistry.cs +++ b/src/LocaleSmith.Core/Abstractions/IModelServiceRegistry.cs @@ -1,6 +1,6 @@ -using JaxI18n.Core.Models; +using LocaleSmith.Core.Models; -namespace JaxI18n.Core.Abstractions; +namespace LocaleSmith.Core.Abstractions; public interface IModelServiceRegistry { diff --git a/src/JaxI18n.Core/Abstractions/SecretContracts.cs b/src/LocaleSmith.Core/Abstractions/SecretContracts.cs similarity index 93% rename from src/JaxI18n.Core/Abstractions/SecretContracts.cs rename to src/LocaleSmith.Core/Abstractions/SecretContracts.cs index b3131b0..d4b33b1 100644 --- a/src/JaxI18n.Core/Abstractions/SecretContracts.cs +++ b/src/LocaleSmith.Core/Abstractions/SecretContracts.cs @@ -1,6 +1,6 @@ -using JaxI18n.Core.Models; +using LocaleSmith.Core.Models; -namespace JaxI18n.Core.Abstractions; +namespace LocaleSmith.Core.Abstractions; public interface ISecretResolver { diff --git a/src/JaxI18n.Core/Abstractions/TerminalEnvironmentContracts.cs b/src/LocaleSmith.Core/Abstractions/TerminalEnvironmentContracts.cs similarity index 79% rename from src/JaxI18n.Core/Abstractions/TerminalEnvironmentContracts.cs rename to src/LocaleSmith.Core/Abstractions/TerminalEnvironmentContracts.cs index d140495..1ce38be 100644 --- a/src/JaxI18n.Core/Abstractions/TerminalEnvironmentContracts.cs +++ b/src/LocaleSmith.Core/Abstractions/TerminalEnvironmentContracts.cs @@ -1,6 +1,6 @@ -using JaxI18n.Core.Models; +using LocaleSmith.Core.Models; -namespace JaxI18n.Core.Abstractions; +namespace LocaleSmith.Core.Abstractions; public interface ITerminalEnvironmentDetector { diff --git a/src/JaxI18n.Core/JaxI18n.Core.csproj b/src/LocaleSmith.Core/LocaleSmith.Core.csproj similarity index 100% rename from src/JaxI18n.Core/JaxI18n.Core.csproj rename to src/LocaleSmith.Core/LocaleSmith.Core.csproj diff --git a/src/JaxI18n.Core/Models/AvailableModelInfo.cs b/src/LocaleSmith.Core/Models/AvailableModelInfo.cs similarity index 85% rename from src/JaxI18n.Core/Models/AvailableModelInfo.cs rename to src/LocaleSmith.Core/Models/AvailableModelInfo.cs index ddc3d5e..7a95941 100644 --- a/src/JaxI18n.Core/Models/AvailableModelInfo.cs +++ b/src/LocaleSmith.Core/Models/AvailableModelInfo.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Core.Models; +namespace LocaleSmith.Core.Models; public sealed record AvailableModelInfo( string Name, diff --git a/src/JaxI18n.Core/Models/CliModels.cs b/src/LocaleSmith.Core/Models/CliModels.cs similarity index 99% rename from src/JaxI18n.Core/Models/CliModels.cs rename to src/LocaleSmith.Core/Models/CliModels.cs index c91f97d..f3e6794 100644 --- a/src/JaxI18n.Core/Models/CliModels.cs +++ b/src/LocaleSmith.Core/Models/CliModels.cs @@ -3,7 +3,7 @@ using System.Text.Json; using System.Text.RegularExpressions; -namespace JaxI18n.Core.Models; +namespace LocaleSmith.Core.Models; public sealed record CliCommand { diff --git a/src/JaxI18n.Core/Models/ModelMessage.cs b/src/LocaleSmith.Core/Models/ModelMessage.cs similarity index 98% rename from src/JaxI18n.Core/Models/ModelMessage.cs rename to src/LocaleSmith.Core/Models/ModelMessage.cs index 218e608..7b11ccc 100644 --- a/src/JaxI18n.Core/Models/ModelMessage.cs +++ b/src/LocaleSmith.Core/Models/ModelMessage.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Core.Models; +namespace LocaleSmith.Core.Models; public enum ModelMessageRole { diff --git a/src/JaxI18n.Core/Models/ModelProviderKind.cs b/src/LocaleSmith.Core/Models/ModelProviderKind.cs similarity index 70% rename from src/JaxI18n.Core/Models/ModelProviderKind.cs rename to src/LocaleSmith.Core/Models/ModelProviderKind.cs index c26d04c..9fcb722 100644 --- a/src/JaxI18n.Core/Models/ModelProviderKind.cs +++ b/src/LocaleSmith.Core/Models/ModelProviderKind.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Core.Models; +namespace LocaleSmith.Core.Models; public enum ModelProviderKind { diff --git a/src/JaxI18n.Core/Models/ModelProviderPreset.cs b/src/LocaleSmith.Core/Models/ModelProviderPreset.cs similarity index 99% rename from src/JaxI18n.Core/Models/ModelProviderPreset.cs rename to src/LocaleSmith.Core/Models/ModelProviderPreset.cs index 2ce8f53..cd7fd7d 100644 --- a/src/JaxI18n.Core/Models/ModelProviderPreset.cs +++ b/src/LocaleSmith.Core/Models/ModelProviderPreset.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace JaxI18n.Core.Models; +namespace LocaleSmith.Core.Models; [JsonConverter(typeof(JsonStringEnumConverter))] public enum OpenAiTokenLimitParameter diff --git a/src/JaxI18n.Core/Models/ModelRequest.cs b/src/LocaleSmith.Core/Models/ModelRequest.cs similarity index 98% rename from src/JaxI18n.Core/Models/ModelRequest.cs rename to src/LocaleSmith.Core/Models/ModelRequest.cs index 7077cb1..eafedd3 100644 --- a/src/JaxI18n.Core/Models/ModelRequest.cs +++ b/src/LocaleSmith.Core/Models/ModelRequest.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Core.Models; +namespace LocaleSmith.Core.Models; public sealed record ModelRequest { diff --git a/src/JaxI18n.Core/Models/ModelResponse.cs b/src/LocaleSmith.Core/Models/ModelResponse.cs similarity index 97% rename from src/JaxI18n.Core/Models/ModelResponse.cs rename to src/LocaleSmith.Core/Models/ModelResponse.cs index ca85255..d94bf33 100644 --- a/src/JaxI18n.Core/Models/ModelResponse.cs +++ b/src/LocaleSmith.Core/Models/ModelResponse.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Core.Models; +namespace LocaleSmith.Core.Models; public sealed record ModelResponse { diff --git a/src/JaxI18n.Core/Models/ModelServiceException.cs b/src/LocaleSmith.Core/Models/ModelServiceException.cs similarity index 95% rename from src/JaxI18n.Core/Models/ModelServiceException.cs rename to src/LocaleSmith.Core/Models/ModelServiceException.cs index 151fbc8..f46c1b3 100644 --- a/src/JaxI18n.Core/Models/ModelServiceException.cs +++ b/src/LocaleSmith.Core/Models/ModelServiceException.cs @@ -1,6 +1,6 @@ using System.Net; -namespace JaxI18n.Core.Models; +namespace LocaleSmith.Core.Models; public sealed class ModelServiceException : Exception { diff --git a/src/JaxI18n.Core/Models/ModelSource.cs b/src/LocaleSmith.Core/Models/ModelSource.cs similarity index 98% rename from src/JaxI18n.Core/Models/ModelSource.cs rename to src/LocaleSmith.Core/Models/ModelSource.cs index 18978fb..6af5def 100644 --- a/src/JaxI18n.Core/Models/ModelSource.cs +++ b/src/LocaleSmith.Core/Models/ModelSource.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Core.Models; +namespace LocaleSmith.Core.Models; public sealed record ModelSource { diff --git a/src/JaxI18n.Core/Models/ModelToolModels.cs b/src/LocaleSmith.Core/Models/ModelToolModels.cs similarity index 98% rename from src/JaxI18n.Core/Models/ModelToolModels.cs rename to src/LocaleSmith.Core/Models/ModelToolModels.cs index 8505d68..1bf9abb 100644 --- a/src/JaxI18n.Core/Models/ModelToolModels.cs +++ b/src/LocaleSmith.Core/Models/ModelToolModels.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace JaxI18n.Core.Models; +namespace LocaleSmith.Core.Models; public sealed record ModelToolDefinition { diff --git a/src/JaxI18n.Core/Models/SecretValue.cs b/src/LocaleSmith.Core/Models/SecretValue.cs similarity index 97% rename from src/JaxI18n.Core/Models/SecretValue.cs rename to src/LocaleSmith.Core/Models/SecretValue.cs index 46cdb2f..a7e0b72 100644 --- a/src/JaxI18n.Core/Models/SecretValue.cs +++ b/src/LocaleSmith.Core/Models/SecretValue.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; using System.Security.Cryptography; -namespace JaxI18n.Core.Models; +namespace LocaleSmith.Core.Models; public sealed class SecretValue : IDisposable { diff --git a/src/JaxI18n.Core/Models/TerminalEnvironmentContext.cs b/src/LocaleSmith.Core/Models/TerminalEnvironmentContext.cs similarity index 91% rename from src/JaxI18n.Core/Models/TerminalEnvironmentContext.cs rename to src/LocaleSmith.Core/Models/TerminalEnvironmentContext.cs index 651c8c3..9136e60 100644 --- a/src/JaxI18n.Core/Models/TerminalEnvironmentContext.cs +++ b/src/LocaleSmith.Core/Models/TerminalEnvironmentContext.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Core.Models; +namespace LocaleSmith.Core.Models; public enum TerminalShellKind { diff --git a/src/JaxI18n.Core/Models/TranslationModels.cs b/src/LocaleSmith.Core/Models/TranslationModels.cs similarity index 98% rename from src/JaxI18n.Core/Models/TranslationModels.cs rename to src/LocaleSmith.Core/Models/TranslationModels.cs index e47a0dd..c3888b6 100644 --- a/src/JaxI18n.Core/Models/TranslationModels.cs +++ b/src/LocaleSmith.Core/Models/TranslationModels.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Core.Models; +namespace LocaleSmith.Core.Models; public enum TranslationStyle { diff --git a/src/JaxI18n.Core/Services/IncrementalTranslationPlanner.cs b/src/LocaleSmith.Core/Services/IncrementalTranslationPlanner.cs similarity index 97% rename from src/JaxI18n.Core/Services/IncrementalTranslationPlanner.cs rename to src/LocaleSmith.Core/Services/IncrementalTranslationPlanner.cs index 59af224..8c222ee 100644 --- a/src/JaxI18n.Core/Services/IncrementalTranslationPlanner.cs +++ b/src/LocaleSmith.Core/Services/IncrementalTranslationPlanner.cs @@ -1,9 +1,9 @@ using System.Buffers.Binary; using System.Security.Cryptography; using System.Text; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Models; -namespace JaxI18n.Core.Services; +namespace LocaleSmith.Core.Services; public sealed record IncrementalTranslationPlan( IReadOnlyList PendingEntries, diff --git a/src/JaxI18n.Core/Services/ModelServiceRegistry.cs b/src/LocaleSmith.Core/Services/ModelServiceRegistry.cs similarity index 98% rename from src/JaxI18n.Core/Services/ModelServiceRegistry.cs rename to src/LocaleSmith.Core/Services/ModelServiceRegistry.cs index f741f8e..8553523 100644 --- a/src/JaxI18n.Core/Services/ModelServiceRegistry.cs +++ b/src/LocaleSmith.Core/Services/ModelServiceRegistry.cs @@ -1,7 +1,7 @@ -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Core.Services; +namespace LocaleSmith.Core.Services; public sealed class ModelServiceRegistry : IModelServiceRegistry, IDisposable { diff --git a/src/JaxI18n.Infrastructure/Cli/CliApprovalService.cs b/src/LocaleSmith.Infrastructure/Cli/CliApprovalService.cs similarity index 95% rename from src/JaxI18n.Infrastructure/Cli/CliApprovalService.cs rename to src/LocaleSmith.Infrastructure/Cli/CliApprovalService.cs index 87d864a..a23ec38 100644 --- a/src/JaxI18n.Infrastructure/Cli/CliApprovalService.cs +++ b/src/LocaleSmith.Infrastructure/Cli/CliApprovalService.cs @@ -1,9 +1,9 @@ using System.Collections.Concurrent; using System.Security.Cryptography; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Infrastructure.Cli; +namespace LocaleSmith.Infrastructure.Cli; public sealed class CliApprovalService : ICliApprovalService { diff --git a/src/JaxI18n.Infrastructure/Cli/JsonLinesCliAuditSink.cs b/src/LocaleSmith.Infrastructure/Cli/JsonLinesCliAuditSink.cs similarity index 93% rename from src/JaxI18n.Infrastructure/Cli/JsonLinesCliAuditSink.cs rename to src/LocaleSmith.Infrastructure/Cli/JsonLinesCliAuditSink.cs index 87d1e28..7b3b590 100644 --- a/src/JaxI18n.Infrastructure/Cli/JsonLinesCliAuditSink.cs +++ b/src/LocaleSmith.Infrastructure/Cli/JsonLinesCliAuditSink.cs @@ -1,8 +1,8 @@ using System.Text.Json; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Infrastructure.Cli; +namespace LocaleSmith.Infrastructure.Cli; public sealed class JsonLinesCliAuditSink : ICliAuditSink, IDisposable { diff --git a/src/JaxI18n.Infrastructure/Cli/RestrictedProcessContracts.cs b/src/LocaleSmith.Infrastructure/Cli/RestrictedProcessContracts.cs similarity index 93% rename from src/JaxI18n.Infrastructure/Cli/RestrictedProcessContracts.cs rename to src/LocaleSmith.Infrastructure/Cli/RestrictedProcessContracts.cs index 6c0aa72..d9bba1f 100644 --- a/src/JaxI18n.Infrastructure/Cli/RestrictedProcessContracts.cs +++ b/src/LocaleSmith.Infrastructure/Cli/RestrictedProcessContracts.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Infrastructure.Cli; +namespace LocaleSmith.Infrastructure.Cli; internal sealed record RestrictedProcessStartRequest( string ExecutablePath, diff --git a/src/JaxI18n.Infrastructure/Cli/SafeCliCommandPolicy.cs b/src/LocaleSmith.Infrastructure/Cli/SafeCliCommandPolicy.cs similarity index 99% rename from src/JaxI18n.Infrastructure/Cli/SafeCliCommandPolicy.cs rename to src/LocaleSmith.Infrastructure/Cli/SafeCliCommandPolicy.cs index a1fb62f..69f0c5d 100644 --- a/src/JaxI18n.Infrastructure/Cli/SafeCliCommandPolicy.cs +++ b/src/LocaleSmith.Infrastructure/Cli/SafeCliCommandPolicy.cs @@ -1,9 +1,9 @@ using System.Collections.ObjectModel; using System.Text.RegularExpressions; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Infrastructure.Cli; +namespace LocaleSmith.Infrastructure.Cli; public sealed class SafeCliCommandPolicy : ICliCommandPolicy, ICliSandboxRootManager, IDisposable { diff --git a/src/JaxI18n.Infrastructure/Cli/SafeCliRunner.cs b/src/LocaleSmith.Infrastructure/Cli/SafeCliRunner.cs similarity index 99% rename from src/JaxI18n.Infrastructure/Cli/SafeCliRunner.cs rename to src/LocaleSmith.Infrastructure/Cli/SafeCliRunner.cs index c65811e..8ac5397 100644 --- a/src/JaxI18n.Infrastructure/Cli/SafeCliRunner.cs +++ b/src/LocaleSmith.Infrastructure/Cli/SafeCliRunner.cs @@ -1,8 +1,8 @@ using System.Text; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Infrastructure.Cli; +namespace LocaleSmith.Infrastructure.Cli; public sealed class SafeCliRunner : ICliRunner { diff --git a/src/JaxI18n.Infrastructure/Cli/TrustedCliExecutableDiscovery.cs b/src/LocaleSmith.Infrastructure/Cli/TrustedCliExecutableDiscovery.cs similarity index 98% rename from src/JaxI18n.Infrastructure/Cli/TrustedCliExecutableDiscovery.cs rename to src/LocaleSmith.Infrastructure/Cli/TrustedCliExecutableDiscovery.cs index c266793..c17d7ac 100644 --- a/src/JaxI18n.Infrastructure/Cli/TrustedCliExecutableDiscovery.cs +++ b/src/LocaleSmith.Infrastructure/Cli/TrustedCliExecutableDiscovery.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Infrastructure.Cli; +namespace LocaleSmith.Infrastructure.Cli; /// /// Discovers the deliberately small set of executables that may be placed on diff --git a/src/JaxI18n.Infrastructure/Cli/WindowsPrivilegeContext.cs b/src/LocaleSmith.Infrastructure/Cli/WindowsPrivilegeContext.cs similarity index 95% rename from src/JaxI18n.Infrastructure/Cli/WindowsPrivilegeContext.cs rename to src/LocaleSmith.Infrastructure/Cli/WindowsPrivilegeContext.cs index f715002..a60f8de 100644 --- a/src/JaxI18n.Infrastructure/Cli/WindowsPrivilegeContext.cs +++ b/src/LocaleSmith.Infrastructure/Cli/WindowsPrivilegeContext.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; -using JaxI18n.Core.Abstractions; +using LocaleSmith.Core.Abstractions; -namespace JaxI18n.Infrastructure.Cli; +namespace LocaleSmith.Infrastructure.Cli; public sealed class WindowsPrivilegeContext : IPrivilegeContext { diff --git a/src/JaxI18n.Infrastructure/Cli/WindowsProcessJob.cs b/src/LocaleSmith.Infrastructure/Cli/WindowsProcessJob.cs similarity index 99% rename from src/JaxI18n.Infrastructure/Cli/WindowsProcessJob.cs rename to src/LocaleSmith.Infrastructure/Cli/WindowsProcessJob.cs index e9ec411..91a5868 100644 --- a/src/JaxI18n.Infrastructure/Cli/WindowsProcessJob.cs +++ b/src/LocaleSmith.Infrastructure/Cli/WindowsProcessJob.cs @@ -3,7 +3,7 @@ using System.Runtime.Versioning; using Microsoft.Win32.SafeHandles; -namespace JaxI18n.Infrastructure.Cli; +namespace LocaleSmith.Infrastructure.Cli; [SupportedOSPlatform("windows")] internal sealed class WindowsProcessJob : IDisposable diff --git a/src/JaxI18n.Infrastructure/Cli/WindowsRestrictedProcessLauncher.cs b/src/LocaleSmith.Infrastructure/Cli/WindowsRestrictedProcessLauncher.cs similarity index 99% rename from src/JaxI18n.Infrastructure/Cli/WindowsRestrictedProcessLauncher.cs rename to src/LocaleSmith.Infrastructure/Cli/WindowsRestrictedProcessLauncher.cs index 56168dc..0d03adc 100644 --- a/src/JaxI18n.Infrastructure/Cli/WindowsRestrictedProcessLauncher.cs +++ b/src/LocaleSmith.Infrastructure/Cli/WindowsRestrictedProcessLauncher.cs @@ -5,7 +5,7 @@ using System.Text; using Microsoft.Win32.SafeHandles; -namespace JaxI18n.Infrastructure.Cli; +namespace LocaleSmith.Infrastructure.Cli; /// /// Starts a CLI process with a low-integrity restricted token on a private desktop and places the @@ -93,7 +93,7 @@ public IRestrictedChildProcess Start(RestrictedProcessStartRequest request) // Microsoft explicitly warns that a restricted-token process must not share the default // desktop with unrestricted applications because of SendMessage/PostMessage shatter attacks: // https://learn.microsoft.com/windows/win32/api/securitybaseapi/nf-securitybaseapi-createrestrictedtoken - var desktopName = $"JaxI18nCli_{Guid.NewGuid():N}"; + var desktopName = $"LocaleSmithCli_{Guid.NewGuid():N}"; desktop = CreatePrivateLowIntegrityDesktop(desktopName); var windowStationName = GetCurrentWindowStationName(); diff --git a/src/JaxI18n.Infrastructure/Environment/SafeSystemPromptContextProvider.cs b/src/LocaleSmith.Infrastructure/Environment/SafeSystemPromptContextProvider.cs similarity index 93% rename from src/JaxI18n.Infrastructure/Environment/SafeSystemPromptContextProvider.cs rename to src/LocaleSmith.Infrastructure/Environment/SafeSystemPromptContextProvider.cs index a7a26cb..b5d03d8 100644 --- a/src/JaxI18n.Infrastructure/Environment/SafeSystemPromptContextProvider.cs +++ b/src/LocaleSmith.Infrastructure/Environment/SafeSystemPromptContextProvider.cs @@ -1,8 +1,8 @@ using System.Text.Json; using System.Text.Json.Serialization; -using JaxI18n.Core.Abstractions; +using LocaleSmith.Core.Abstractions; -namespace JaxI18n.Infrastructure.Environment; +namespace LocaleSmith.Infrastructure.Environment; public sealed class SafeSystemPromptContextProvider : ISystemPromptContextProvider { diff --git a/src/JaxI18n.Infrastructure/Environment/TerminalEnvironmentDetector.cs b/src/LocaleSmith.Infrastructure/Environment/TerminalEnvironmentDetector.cs similarity index 98% rename from src/JaxI18n.Infrastructure/Environment/TerminalEnvironmentDetector.cs rename to src/LocaleSmith.Infrastructure/Environment/TerminalEnvironmentDetector.cs index 7613cda..3d5116e 100644 --- a/src/JaxI18n.Infrastructure/Environment/TerminalEnvironmentDetector.cs +++ b/src/LocaleSmith.Infrastructure/Environment/TerminalEnvironmentDetector.cs @@ -1,10 +1,10 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Text.RegularExpressions; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Infrastructure.Environment; +namespace LocaleSmith.Infrastructure.Environment; public sealed partial class TerminalEnvironmentDetector : ITerminalEnvironmentDetector { diff --git a/src/JaxI18n.Infrastructure/JaxI18n.Infrastructure.csproj b/src/LocaleSmith.Infrastructure/LocaleSmith.Infrastructure.csproj similarity index 68% rename from src/JaxI18n.Infrastructure/JaxI18n.Infrastructure.csproj rename to src/LocaleSmith.Infrastructure/LocaleSmith.Infrastructure.csproj index 0f83fa3..b72a2b9 100644 --- a/src/JaxI18n.Infrastructure/JaxI18n.Infrastructure.csproj +++ b/src/LocaleSmith.Infrastructure/LocaleSmith.Infrastructure.csproj @@ -7,10 +7,10 @@ - + - + diff --git a/src/JaxI18n.Infrastructure/Models/AnthropicModelService.cs b/src/LocaleSmith.Infrastructure/Models/AnthropicModelService.cs similarity index 98% rename from src/JaxI18n.Infrastructure/Models/AnthropicModelService.cs rename to src/LocaleSmith.Infrastructure/Models/AnthropicModelService.cs index 74ef846..371e026 100644 --- a/src/JaxI18n.Infrastructure/Models/AnthropicModelService.cs +++ b/src/LocaleSmith.Infrastructure/Models/AnthropicModelService.cs @@ -1,9 +1,9 @@ using System.Collections.ObjectModel; using System.Net.Http.Json; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Infrastructure.Models; +namespace LocaleSmith.Infrastructure.Models; public sealed class AnthropicModelService : HttpModelServiceBase { diff --git a/src/JaxI18n.Infrastructure/Models/HttpModelServiceBase.cs b/src/LocaleSmith.Infrastructure/Models/HttpModelServiceBase.cs similarity index 99% rename from src/JaxI18n.Infrastructure/Models/HttpModelServiceBase.cs rename to src/LocaleSmith.Infrastructure/Models/HttpModelServiceBase.cs index 41419f1..3754b74 100644 --- a/src/JaxI18n.Infrastructure/Models/HttpModelServiceBase.cs +++ b/src/LocaleSmith.Infrastructure/Models/HttpModelServiceBase.cs @@ -3,10 +3,10 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Infrastructure.Models; +namespace LocaleSmith.Infrastructure.Models; public abstract class HttpModelServiceBase : IModelService { diff --git a/src/JaxI18n.Infrastructure/Models/OllamaModelService.cs b/src/LocaleSmith.Infrastructure/Models/OllamaModelService.cs similarity index 98% rename from src/JaxI18n.Infrastructure/Models/OllamaModelService.cs rename to src/LocaleSmith.Infrastructure/Models/OllamaModelService.cs index be44293..944b8eb 100644 --- a/src/JaxI18n.Infrastructure/Models/OllamaModelService.cs +++ b/src/LocaleSmith.Infrastructure/Models/OllamaModelService.cs @@ -1,10 +1,10 @@ using System.Collections.ObjectModel; using System.Net.Http.Json; using System.Text.Json; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Infrastructure.Models; +namespace LocaleSmith.Infrastructure.Models; public sealed class OllamaModelService : HttpModelServiceBase, IModelCatalogService { diff --git a/src/JaxI18n.Infrastructure/Models/OpenAiCompatibleModelService.cs b/src/LocaleSmith.Infrastructure/Models/OpenAiCompatibleModelService.cs similarity index 98% rename from src/JaxI18n.Infrastructure/Models/OpenAiCompatibleModelService.cs rename to src/LocaleSmith.Infrastructure/Models/OpenAiCompatibleModelService.cs index 0f2cf54..d263891 100644 --- a/src/JaxI18n.Infrastructure/Models/OpenAiCompatibleModelService.cs +++ b/src/LocaleSmith.Infrastructure/Models/OpenAiCompatibleModelService.cs @@ -1,10 +1,10 @@ using System.Collections.ObjectModel; using System.Net.Http.Headers; using System.Net.Http.Json; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Infrastructure.Models; +namespace LocaleSmith.Infrastructure.Models; public sealed class OpenAiCompatibleModelService : HttpModelServiceBase { diff --git a/src/JaxI18n.Infrastructure/Models/SafeModelHttpClientFactory.cs b/src/LocaleSmith.Infrastructure/Models/SafeModelHttpClientFactory.cs similarity index 96% rename from src/JaxI18n.Infrastructure/Models/SafeModelHttpClientFactory.cs rename to src/LocaleSmith.Infrastructure/Models/SafeModelHttpClientFactory.cs index 52d69a0..f5d70fc 100644 --- a/src/JaxI18n.Infrastructure/Models/SafeModelHttpClientFactory.cs +++ b/src/LocaleSmith.Infrastructure/Models/SafeModelHttpClientFactory.cs @@ -1,6 +1,6 @@ using System.Net; -namespace JaxI18n.Infrastructure.Models; +namespace LocaleSmith.Infrastructure.Models; /// /// Creates model clients that never automatically forward a provider request across redirects. diff --git a/src/JaxI18n.Infrastructure/Security/CredentialManagerMasterKeyStore.cs b/src/LocaleSmith.Infrastructure/Security/CredentialManagerMasterKeyStore.cs similarity index 91% rename from src/JaxI18n.Infrastructure/Security/CredentialManagerMasterKeyStore.cs rename to src/LocaleSmith.Infrastructure/Security/CredentialManagerMasterKeyStore.cs index 746319b..9e81afa 100644 --- a/src/JaxI18n.Infrastructure/Security/CredentialManagerMasterKeyStore.cs +++ b/src/LocaleSmith.Infrastructure/Security/CredentialManagerMasterKeyStore.cs @@ -1,9 +1,9 @@ using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Security.Cryptography; -using JaxI18n.Core.Abstractions; +using LocaleSmith.Core.Abstractions; -namespace JaxI18n.Infrastructure.Security; +namespace LocaleSmith.Infrastructure.Security; public sealed class CredentialManagerMasterKeyStore : IMasterKeyStore { @@ -25,6 +25,10 @@ public async ValueTask GetOrCreateKeyAsync( await gate.WaitAsync(cancellationToken).ConfigureAwait(false); try { + using var processGate = await SecurityOperationLock.AcquireAsync( + "master-key", + reference, + cancellationToken).ConfigureAwait(false); using var existing = await _credentialStore.ResolveAsync(reference, cancellationToken).ConfigureAwait(false); if (existing is not null) { diff --git a/src/JaxI18n.Infrastructure/Security/EncryptedJsonConfigurationStore.cs b/src/LocaleSmith.Infrastructure/Security/EncryptedJsonConfigurationStore.cs similarity index 82% rename from src/JaxI18n.Infrastructure/Security/EncryptedJsonConfigurationStore.cs rename to src/LocaleSmith.Infrastructure/Security/EncryptedJsonConfigurationStore.cs index dd3758a..f13360a 100644 --- a/src/JaxI18n.Infrastructure/Security/EncryptedJsonConfigurationStore.cs +++ b/src/LocaleSmith.Infrastructure/Security/EncryptedJsonConfigurationStore.cs @@ -1,17 +1,19 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; -using JaxI18n.Core.Abstractions; +using LocaleSmith.Core.Abstractions; -namespace JaxI18n.Infrastructure.Security; +namespace LocaleSmith.Infrastructure.Security; public sealed class EncryptedJsonConfigurationStore : IConfigurationStore, IDisposable { + public const string DefaultAssociatedDataNamespace = "LocaleSmith.Config"; private const int NonceLength = 12; private const int TagLength = 16; private const long MaximumFileLength = 16 * 1024 * 1024; private readonly string _filePath; private readonly string _purpose; + private readonly string _associatedDataNamespace; private readonly IMasterKeyStore _masterKeyStore; private readonly JsonSerializerOptions _serializerOptions; private readonly SemaphoreSlim _gate = new(1, 1); @@ -21,12 +23,15 @@ public EncryptedJsonConfigurationStore( string filePath, string purpose, IMasterKeyStore masterKeyStore, - JsonSerializerOptions? serializerOptions = null) + JsonSerializerOptions? serializerOptions = null, + string associatedDataNamespace = DefaultAssociatedDataNamespace) { ArgumentException.ThrowIfNullOrWhiteSpace(filePath); ArgumentException.ThrowIfNullOrWhiteSpace(purpose); + ArgumentException.ThrowIfNullOrWhiteSpace(associatedDataNamespace); _filePath = Path.GetFullPath(filePath); _purpose = purpose; + _associatedDataNamespace = associatedDataNamespace; _masterKeyStore = masterKeyStore ?? throw new ArgumentNullException(nameof(masterKeyStore)); _serializerOptions = serializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web); } @@ -104,6 +109,19 @@ public EncryptedJsonConfigurationStore( } public async Task SaveAsync(TConfiguration configuration, CancellationToken cancellationToken = default) + { + await SaveCoreAsync(configuration, overwrite: true, cancellationToken).ConfigureAwait(false); + } + + public Task SaveIfAbsentAsync( + TConfiguration configuration, + CancellationToken cancellationToken = default) => + SaveCoreAsync(configuration, overwrite: false, cancellationToken); + + private async Task SaveCoreAsync( + TConfiguration configuration, + bool overwrite, + CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(configuration); ObjectDisposedException.ThrowIf(_disposed, this); @@ -133,7 +151,7 @@ public async Task SaveAsync(TConfiguration configuration, CancellationToken canc var envelopeBytes = JsonSerializer.SerializeToUtf8Bytes(envelope, _serializerOptions); try { - await WriteAtomicallyAsync(envelopeBytes, cancellationToken).ConfigureAwait(false); + return await WriteAtomicallyAsync(envelopeBytes, overwrite, cancellationToken).ConfigureAwait(false); } finally { @@ -169,9 +187,13 @@ public void Dispose() _disposed = true; } - private byte[] GetAssociatedData() => Encoding.UTF8.GetBytes($"JaxI18n.Config|v1|{_purpose}"); + private byte[] GetAssociatedData() => + Encoding.UTF8.GetBytes($"{_associatedDataNamespace}|v1|{_purpose}"); - private async Task WriteAtomicallyAsync(byte[] content, CancellationToken cancellationToken) + private async Task WriteAtomicallyAsync( + byte[] content, + bool overwrite, + CancellationToken cancellationToken) { var directory = Path.GetDirectoryName(_filePath) ?? throw new InvalidOperationException("The configuration path does not have a parent directory."); @@ -180,7 +202,15 @@ private async Task WriteAtomicallyAsync(byte[] content, CancellationToken cancel try { await File.WriteAllBytesAsync(temporaryPath, content, cancellationToken).ConfigureAwait(false); - File.Move(temporaryPath, _filePath, true); + try + { + File.Move(temporaryPath, _filePath, overwrite); + return true; + } + catch (IOException) when (!overwrite && File.Exists(_filePath)) + { + return false; + } } finally { diff --git a/src/JaxI18n.Infrastructure/Security/InMemorySecretStore.cs b/src/LocaleSmith.Infrastructure/Security/InMemorySecretStore.cs similarity index 95% rename from src/JaxI18n.Infrastructure/Security/InMemorySecretStore.cs rename to src/LocaleSmith.Infrastructure/Security/InMemorySecretStore.cs index ea91841..890234c 100644 --- a/src/JaxI18n.Infrastructure/Security/InMemorySecretStore.cs +++ b/src/LocaleSmith.Infrastructure/Security/InMemorySecretStore.cs @@ -1,10 +1,10 @@ using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Security.Cryptography; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Infrastructure.Security; +namespace LocaleSmith.Infrastructure.Security; /// Cross-platform volatile store intended for tests and ephemeral sessions only. public sealed class InMemorySecretStore : ISecretStore, IDisposable diff --git a/src/LocaleSmith.Infrastructure/Security/MigratingSecretStore.cs b/src/LocaleSmith.Infrastructure/Security/MigratingSecretStore.cs new file mode 100644 index 0000000..76709f5 --- /dev/null +++ b/src/LocaleSmith.Infrastructure/Security/MigratingSecretStore.cs @@ -0,0 +1,187 @@ +using System.Collections.Concurrent; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; + +namespace LocaleSmith.Infrastructure.Security; + +/// +/// Reads from the current secret store first and lazily copies legacy values into it. +/// Legacy values are retained so an upgrade does not make rollback destructive. +/// +public sealed class MigratingSecretStore : ISecretStore +{ + private const string TombstoneValue = "deleted"; + private static readonly ConcurrentDictionary Locks = new(StringComparer.Ordinal); + private readonly ISecretStore _currentStore; + private readonly ISecretStore _legacyStore; + + public MigratingSecretStore(ISecretStore currentStore, ISecretStore legacyStore) + { + _currentStore = currentStore ?? throw new ArgumentNullException(nameof(currentStore)); + _legacyStore = legacyStore ?? throw new ArgumentNullException(nameof(legacyStore)); + } + + public async ValueTask ResolveAsync( + string reference, + CancellationToken cancellationToken = default) + { + ValidateReference(reference); + cancellationToken.ThrowIfCancellationRequested(); + var gate = Locks.GetOrAdd(reference, static _ => new SemaphoreSlim(1, 1)); + await gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + using var processGate = await SecurityOperationLock.AcquireAsync( + "migrating-secret", + reference, + cancellationToken).ConfigureAwait(false); + using var tombstone = await _currentStore + .ResolveAsync(GetTombstoneReference(reference), cancellationToken) + .ConfigureAwait(false); + if (tombstone is not null) + { + return null; + } + + var current = await _currentStore.ResolveAsync(reference, cancellationToken).ConfigureAwait(false); + if (current is not null) + { + return current; + } + + using var legacy = await _legacyStore.ResolveAsync(reference, cancellationToken).ConfigureAwait(false); + if (legacy is null) + { + return null; + } + + using var latestTombstone = await _currentStore + .ResolveAsync(GetTombstoneReference(reference), cancellationToken) + .ConfigureAwait(false); + if (latestTombstone is not null) + { + return null; + } + + var latestCurrent = await _currentStore.ResolveAsync(reference, cancellationToken).ConfigureAwait(false); + if (latestCurrent is not null) + { + return latestCurrent; + } + + var buffer = new char[legacy.Length]; + try + { + legacy.CopyTo(buffer); + await _currentStore.SetAsync(reference, buffer, cancellationToken).ConfigureAwait(false); + return new SecretValue(buffer); + } + finally + { + CryptographicOperations.ZeroMemory(MemoryMarshal.AsBytes(buffer.AsSpan())); + } + } + finally + { + gate.Release(); + } + } + + public async ValueTask SetAsync( + string reference, + ReadOnlyMemory secret, + CancellationToken cancellationToken = default) + { + ValidateReference(reference); + var gate = Locks.GetOrAdd(reference, static _ => new SemaphoreSlim(1, 1)); + await gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + using var processGate = await SecurityOperationLock.AcquireAsync( + "migrating-secret", + reference, + cancellationToken).ConfigureAwait(false); + await _currentStore.SetAsync(reference, secret, cancellationToken).ConfigureAwait(false); + await _currentStore + .DeleteAsync(GetTombstoneReference(reference), cancellationToken) + .ConfigureAwait(false); + } + finally + { + gate.Release(); + } + } + + public async ValueTask DeleteAsync( + string reference, + CancellationToken cancellationToken = default) + { + ValidateReference(reference); + cancellationToken.ThrowIfCancellationRequested(); + var gate = Locks.GetOrAdd(reference, static _ => new SemaphoreSlim(1, 1)); + await gate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + using var processGate = await SecurityOperationLock.AcquireAsync( + "migrating-secret", + reference, + cancellationToken).ConfigureAwait(false); + await _currentStore + .SetAsync(GetTombstoneReference(reference), TombstoneValue.AsMemory(), cancellationToken) + .ConfigureAwait(false); + + Exception? currentFailure = null; + var currentDeleted = false; + try + { + currentDeleted = await _currentStore.DeleteAsync(reference, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception exception) + { + currentFailure = exception; + } + + bool legacyDeleted; + try + { + legacyDeleted = await _legacyStore.DeleteAsync(reference, cancellationToken).ConfigureAwait(false); + } + catch (Exception legacyFailure) when (currentFailure is not null) + { + throw new AggregateException( + "Deleting the secret failed in both the current and legacy stores.", + currentFailure, + legacyFailure); + } + + if (currentFailure is not null) + { + ExceptionDispatchInfo.Capture(currentFailure).Throw(); + } + + return currentDeleted || legacyDeleted; + } + finally + { + gate.Release(); + } + } + + private static string GetTombstoneReference(string reference) + { + var digest = Convert.ToHexString( + SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(reference))) + .ToLowerInvariant(); + return $"migration-tombstone/{digest}"; + } + + private static void ValidateReference(string reference) => + ArgumentException.ThrowIfNullOrWhiteSpace(reference); +} diff --git a/src/LocaleSmith.Infrastructure/Security/SecurityOperationLock.cs b/src/LocaleSmith.Infrastructure/Security/SecurityOperationLock.cs new file mode 100644 index 0000000..2906547 --- /dev/null +++ b/src/LocaleSmith.Infrastructure/Security/SecurityOperationLock.cs @@ -0,0 +1,70 @@ +using System.Security.Cryptography; +using System.Text; + +namespace LocaleSmith.Infrastructure.Security; + +/// +/// A crash-safe cross-process lock backed by an exclusively opened per-user file. +/// Unlike a named mutex, the lease may safely cross async continuation threads. +/// +internal sealed class SecurityOperationLock : IDisposable +{ + private const int SharingViolation = 32; + private const int LockViolation = 33; + private readonly FileStream _lease; + + private SecurityOperationLock(FileStream lease) + { + _lease = lease; + } + + public static async ValueTask AcquireAsync( + string operation, + string key, + CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrWhiteSpace(operation); + ArgumentException.ThrowIfNullOrWhiteSpace(key); + var lockRoot = Path.Combine(Path.GetTempPath(), "LocaleSmith", "SecurityLocks"); + Directory.CreateDirectory(lockRoot); + var path = Path.Combine(lockRoot, $"{GetName(operation, key)}.lock"); + + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + var lease = new FileStream( + path, + FileMode.OpenOrCreate, + FileAccess.ReadWrite, + FileShare.None, + bufferSize: 1, + FileOptions.Asynchronous); + return new SecurityOperationLock(lease); + } + catch (IOException exception) when (IsLockContention(exception)) + { + await Task.Delay(TimeSpan.FromMilliseconds(50), cancellationToken).ConfigureAwait(false); + } + } + } + + public void Dispose() => _lease.Dispose(); + + private static bool IsLockContention(IOException exception) => + (exception.HResult & 0xFFFF) is SharingViolation or LockViolation; + + private static string GetName(string operation, string key) + { + var material = Encoding.UTF8.GetBytes($"{operation}\0{key}"); + try + { + return Convert.ToHexString(SHA256.HashData(material)); + } + finally + { + CryptographicOperations.ZeroMemory(material); + } + } +} diff --git a/src/JaxI18n.Infrastructure/Security/WindowsCredentialSecretStore.cs b/src/LocaleSmith.Infrastructure/Security/WindowsCredentialSecretStore.cs similarity index 96% rename from src/JaxI18n.Infrastructure/Security/WindowsCredentialSecretStore.cs rename to src/LocaleSmith.Infrastructure/Security/WindowsCredentialSecretStore.cs index a432a29..0748547 100644 --- a/src/JaxI18n.Infrastructure/Security/WindowsCredentialSecretStore.cs +++ b/src/LocaleSmith.Infrastructure/Security/WindowsCredentialSecretStore.cs @@ -3,21 +3,22 @@ using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; using Microsoft.Win32.SafeHandles; -namespace JaxI18n.Infrastructure.Security; +namespace LocaleSmith.Infrastructure.Security; public sealed partial class WindowsCredentialSecretStore : ISecretStore { + internal const string DefaultTargetPrefix = "LocaleSmith"; private const uint CredentialTypeGeneric = 1; private const uint CredentialPersistLocalMachine = 2; private const int ErrorNotFound = 1168; private const int MaximumCredentialBlobSize = 2560; private readonly string _targetPrefix; - public WindowsCredentialSecretStore(string targetPrefix = "JaxI18n") + public WindowsCredentialSecretStore(string targetPrefix = DefaultTargetPrefix) { if (!OperatingSystem.IsWindows()) { diff --git a/src/JaxI18n.Mcp/JaxI18n.Mcp.csproj b/src/LocaleSmith.Mcp/LocaleSmith.Mcp.csproj similarity index 78% rename from src/JaxI18n.Mcp/JaxI18n.Mcp.csproj rename to src/LocaleSmith.Mcp/LocaleSmith.Mcp.csproj index ff677a2..d69dcaa 100644 --- a/src/JaxI18n.Mcp/JaxI18n.Mcp.csproj +++ b/src/LocaleSmith.Mcp/LocaleSmith.Mcp.csproj @@ -7,6 +7,6 @@ - + diff --git a/src/JaxI18n.Mcp/McpModelToolExecutor.cs b/src/LocaleSmith.Mcp/McpModelToolExecutor.cs similarity index 97% rename from src/JaxI18n.Mcp/McpModelToolExecutor.cs rename to src/LocaleSmith.Mcp/McpModelToolExecutor.cs index 9047091..3561747 100644 --- a/src/JaxI18n.Mcp/McpModelToolExecutor.cs +++ b/src/LocaleSmith.Mcp/McpModelToolExecutor.cs @@ -1,8 +1,8 @@ using System.Text.Json; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Mcp; +namespace LocaleSmith.Mcp; /// /// Adapts the read-only MCP context and command-proposal tools to provider-native function diff --git a/src/JaxI18n.Mcp/McpServerOptions.cs b/src/LocaleSmith.Mcp/McpServerOptions.cs similarity index 98% rename from src/JaxI18n.Mcp/McpServerOptions.cs rename to src/LocaleSmith.Mcp/McpServerOptions.cs index 7e6d1f7..76ab13a 100644 --- a/src/JaxI18n.Mcp/McpServerOptions.cs +++ b/src/LocaleSmith.Mcp/McpServerOptions.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Mcp; +namespace LocaleSmith.Mcp; public sealed class McpServerOptions { diff --git a/src/JaxI18n.Mcp/McpStdioServer.cs b/src/LocaleSmith.Mcp/McpStdioServer.cs similarity index 99% rename from src/JaxI18n.Mcp/McpStdioServer.cs rename to src/LocaleSmith.Mcp/McpStdioServer.cs index 307c8a6..5804180 100644 --- a/src/JaxI18n.Mcp/McpStdioServer.cs +++ b/src/LocaleSmith.Mcp/McpStdioServer.cs @@ -1,9 +1,9 @@ using System.Collections.Concurrent; using System.Text.Json; -using JaxI18n.Core.Abstractions; -using JaxI18n.Mcp.Protocol; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Mcp.Protocol; -namespace JaxI18n.Mcp; +namespace LocaleSmith.Mcp; public sealed class McpStdioServer : IDisposable { @@ -272,7 +272,7 @@ private async Task HandleInitializeAsync( }, serverInfo = new { - name = "jax-i18n-mcp", + name = "localesmith-mcp", title = "LocaleSmith Local Tools", version = "0.1.0", description = "LocaleSmith local, safety-gated MCP tools for terminal context and CLI proposals." diff --git a/src/JaxI18n.Mcp/McpToolCatalog.cs b/src/LocaleSmith.Mcp/McpToolCatalog.cs similarity index 99% rename from src/JaxI18n.Mcp/McpToolCatalog.cs rename to src/LocaleSmith.Mcp/McpToolCatalog.cs index 8fa2be9..6e0c2a4 100644 --- a/src/JaxI18n.Mcp/McpToolCatalog.cs +++ b/src/LocaleSmith.Mcp/McpToolCatalog.cs @@ -1,9 +1,9 @@ using System.Text.Json; using System.Text.RegularExpressions; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Mcp; +namespace LocaleSmith.Mcp; internal sealed partial class McpToolCatalog { diff --git a/src/JaxI18n.Mcp/OutputSanitizer.cs b/src/LocaleSmith.Mcp/OutputSanitizer.cs similarity index 98% rename from src/JaxI18n.Mcp/OutputSanitizer.cs rename to src/LocaleSmith.Mcp/OutputSanitizer.cs index 9cfb75c..9a4b568 100644 --- a/src/JaxI18n.Mcp/OutputSanitizer.cs +++ b/src/LocaleSmith.Mcp/OutputSanitizer.cs @@ -1,7 +1,7 @@ using System.Text; using System.Text.RegularExpressions; -namespace JaxI18n.Mcp; +namespace LocaleSmith.Mcp; internal static partial class OutputSanitizer { diff --git a/src/JaxI18n.Mcp/Protocol/BoundedUtf8LineReader.cs b/src/LocaleSmith.Mcp/Protocol/BoundedUtf8LineReader.cs similarity index 98% rename from src/JaxI18n.Mcp/Protocol/BoundedUtf8LineReader.cs rename to src/LocaleSmith.Mcp/Protocol/BoundedUtf8LineReader.cs index d65d0e0..9e25e4d 100644 --- a/src/JaxI18n.Mcp/Protocol/BoundedUtf8LineReader.cs +++ b/src/LocaleSmith.Mcp/Protocol/BoundedUtf8LineReader.cs @@ -1,7 +1,7 @@ using System.Buffers; using System.Text; -namespace JaxI18n.Mcp.Protocol; +namespace LocaleSmith.Mcp.Protocol; internal sealed class BoundedUtf8LineReader { diff --git a/src/JaxI18n.Mcp/Protocol/FixedWindowRateLimiter.cs b/src/LocaleSmith.Mcp/Protocol/FixedWindowRateLimiter.cs similarity index 96% rename from src/JaxI18n.Mcp/Protocol/FixedWindowRateLimiter.cs rename to src/LocaleSmith.Mcp/Protocol/FixedWindowRateLimiter.cs index f2b50d6..ed019af 100644 --- a/src/JaxI18n.Mcp/Protocol/FixedWindowRateLimiter.cs +++ b/src/LocaleSmith.Mcp/Protocol/FixedWindowRateLimiter.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Mcp.Protocol; +namespace LocaleSmith.Mcp.Protocol; internal sealed class FixedWindowRateLimiter { diff --git a/src/JaxI18n.Mcp/Protocol/JsonRpcModels.cs b/src/LocaleSmith.Mcp/Protocol/JsonRpcModels.cs similarity index 95% rename from src/JaxI18n.Mcp/Protocol/JsonRpcModels.cs rename to src/LocaleSmith.Mcp/Protocol/JsonRpcModels.cs index dc0ccbd..7edd1dc 100644 --- a/src/JaxI18n.Mcp/Protocol/JsonRpcModels.cs +++ b/src/LocaleSmith.Mcp/Protocol/JsonRpcModels.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace JaxI18n.Mcp.Protocol; +namespace LocaleSmith.Mcp.Protocol; internal readonly record struct JsonRpcId(string Key, JsonElement Value); diff --git a/src/JaxI18n.Mcp/Protocol/JsonRpcParser.cs b/src/LocaleSmith.Mcp/Protocol/JsonRpcParser.cs similarity index 99% rename from src/JaxI18n.Mcp/Protocol/JsonRpcParser.cs rename to src/LocaleSmith.Mcp/Protocol/JsonRpcParser.cs index d76bcdf..aab1dc8 100644 --- a/src/JaxI18n.Mcp/Protocol/JsonRpcParser.cs +++ b/src/LocaleSmith.Mcp/Protocol/JsonRpcParser.cs @@ -1,7 +1,7 @@ using System.Globalization; using System.Text.Json; -namespace JaxI18n.Mcp.Protocol; +namespace LocaleSmith.Mcp.Protocol; internal static class JsonRpcParser { diff --git a/src/JaxI18n.Mcp/Protocol/JsonRpcWriter.cs b/src/LocaleSmith.Mcp/Protocol/JsonRpcWriter.cs similarity index 98% rename from src/JaxI18n.Mcp/Protocol/JsonRpcWriter.cs rename to src/LocaleSmith.Mcp/Protocol/JsonRpcWriter.cs index 0a2a3a9..43fd192 100644 --- a/src/JaxI18n.Mcp/Protocol/JsonRpcWriter.cs +++ b/src/LocaleSmith.Mcp/Protocol/JsonRpcWriter.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace JaxI18n.Mcp.Protocol; +namespace LocaleSmith.Mcp.Protocol; internal sealed class JsonRpcWriter : IDisposable { diff --git a/src/JaxI18n.McpHost/JaxI18n.McpHost.csproj b/src/LocaleSmith.McpHost/LocaleSmith.McpHost.csproj similarity index 58% rename from src/JaxI18n.McpHost/JaxI18n.McpHost.csproj rename to src/LocaleSmith.McpHost/LocaleSmith.McpHost.csproj index 316a08f..3373eac 100644 --- a/src/JaxI18n.McpHost/JaxI18n.McpHost.csproj +++ b/src/LocaleSmith.McpHost/LocaleSmith.McpHost.csproj @@ -2,10 +2,10 @@ Exe net10.0-windows10.0.17763.0 - JaxI18n.McpHost - JaxI18n.McpHost + LocaleSmith.McpHost + LocaleSmith.McpHost LocaleSmith MCP Host - LocaleSmith(译匠) + LocaleSmith | 译匠 LocaleSmith 本地安全 MCP 工具宿主 x64 x64 @@ -13,7 +13,7 @@ - - + + diff --git a/src/JaxI18n.McpHost/Program.cs b/src/LocaleSmith.McpHost/Program.cs similarity index 90% rename from src/JaxI18n.McpHost/Program.cs rename to src/LocaleSmith.McpHost/Program.cs index 4b2b3c2..5fffad8 100644 --- a/src/JaxI18n.McpHost/Program.cs +++ b/src/LocaleSmith.McpHost/Program.cs @@ -1,9 +1,9 @@ using System.Text; -using JaxI18n.Infrastructure.Cli; -using JaxI18n.Infrastructure.Environment; -using JaxI18n.Mcp; +using LocaleSmith.Infrastructure.Cli; +using LocaleSmith.Infrastructure.Environment; +using LocaleSmith.Mcp; -namespace JaxI18n.McpHost; +namespace LocaleSmith.McpHost; internal static class Program { @@ -25,7 +25,7 @@ public static async Task Main() shutdown.Cancel(); }; - var sandboxRoot = Path.Combine(Path.GetTempPath(), "JaxI18n", "McpSandbox"); + var sandboxRoot = Path.Combine(Path.GetTempPath(), "LocaleSmith", "McpSandbox"); Directory.CreateDirectory(sandboxRoot); using var commandPolicy = new SafeCliCommandPolicy( TrustedCliExecutableDiscovery.FindInstalled(), diff --git a/src/JaxI18n.McpHost/Properties/PublishProfiles/win-x64.pubxml b/src/LocaleSmith.McpHost/Properties/PublishProfiles/win-x64.pubxml similarity index 100% rename from src/JaxI18n.McpHost/Properties/PublishProfiles/win-x64.pubxml rename to src/LocaleSmith.McpHost/Properties/PublishProfiles/win-x64.pubxml diff --git a/src/JaxI18n.NativeInterop/ArchiveScanManifest.cs b/src/LocaleSmith.NativeInterop/ArchiveScanManifest.cs similarity index 99% rename from src/JaxI18n.NativeInterop/ArchiveScanManifest.cs rename to src/LocaleSmith.NativeInterop/ArchiveScanManifest.cs index bb8d8f9..df60bfa 100644 --- a/src/JaxI18n.NativeInterop/ArchiveScanManifest.cs +++ b/src/LocaleSmith.NativeInterop/ArchiveScanManifest.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace JaxI18n.NativeInterop; +namespace LocaleSmith.NativeInterop; public sealed record ArchiveScanManifest { diff --git a/src/JaxI18n.NativeInterop/JaxI18n.NativeInterop.csproj b/src/LocaleSmith.NativeInterop/LocaleSmith.NativeInterop.csproj similarity index 62% rename from src/JaxI18n.NativeInterop/JaxI18n.NativeInterop.csproj rename to src/LocaleSmith.NativeInterop/LocaleSmith.NativeInterop.csproj index 95c23b5..116f3a9 100644 --- a/src/JaxI18n.NativeInterop/JaxI18n.NativeInterop.csproj +++ b/src/LocaleSmith.NativeInterop/LocaleSmith.NativeInterop.csproj @@ -1,8 +1,8 @@ net10.0-windows10.0.17763.0 - JaxI18n.NativeInterop - JaxI18n.NativeInterop + LocaleSmith.NativeInterop + LocaleSmith.NativeInterop true diff --git a/src/JaxI18n.NativeInterop/NativeCoreClient.cs b/src/LocaleSmith.NativeInterop/NativeCoreClient.cs similarity index 99% rename from src/JaxI18n.NativeInterop/NativeCoreClient.cs rename to src/LocaleSmith.NativeInterop/NativeCoreClient.cs index b9259e3..0c17169 100644 --- a/src/JaxI18n.NativeInterop/NativeCoreClient.cs +++ b/src/LocaleSmith.NativeInterop/NativeCoreClient.cs @@ -2,7 +2,7 @@ using System.Runtime.InteropServices; using System.Text.Json; -namespace JaxI18n.NativeInterop; +namespace LocaleSmith.NativeInterop; [SuppressMessage( "Performance", diff --git a/src/JaxI18n.NativeInterop/NativeCoreErrorCode.cs b/src/LocaleSmith.NativeInterop/NativeCoreErrorCode.cs similarity index 86% rename from src/JaxI18n.NativeInterop/NativeCoreErrorCode.cs rename to src/LocaleSmith.NativeInterop/NativeCoreErrorCode.cs index 3fdafb2..1ca78ad 100644 --- a/src/JaxI18n.NativeInterop/NativeCoreErrorCode.cs +++ b/src/LocaleSmith.NativeInterop/NativeCoreErrorCode.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.NativeInterop; +namespace LocaleSmith.NativeInterop; public enum NativeCoreErrorCode { diff --git a/src/JaxI18n.NativeInterop/NativeCoreException.cs b/src/LocaleSmith.NativeInterop/NativeCoreException.cs similarity index 87% rename from src/JaxI18n.NativeInterop/NativeCoreException.cs rename to src/LocaleSmith.NativeInterop/NativeCoreException.cs index fc038ce..7a50a98 100644 --- a/src/JaxI18n.NativeInterop/NativeCoreException.cs +++ b/src/LocaleSmith.NativeInterop/NativeCoreException.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.NativeInterop; +namespace LocaleSmith.NativeInterop; public sealed class NativeCoreException : Exception { diff --git a/src/JaxI18n.NativeInterop/NativeCoreLibraryResolver.cs b/src/LocaleSmith.NativeInterop/NativeCoreLibraryResolver.cs similarity index 92% rename from src/JaxI18n.NativeInterop/NativeCoreLibraryResolver.cs rename to src/LocaleSmith.NativeInterop/NativeCoreLibraryResolver.cs index 554d567..87646d0 100644 --- a/src/JaxI18n.NativeInterop/NativeCoreLibraryResolver.cs +++ b/src/LocaleSmith.NativeInterop/NativeCoreLibraryResolver.cs @@ -1,7 +1,7 @@ using System.Reflection; using System.Runtime.InteropServices; -namespace JaxI18n.NativeInterop; +namespace LocaleSmith.NativeInterop; internal static class NativeCoreLibraryResolver { @@ -44,8 +44,8 @@ private static nint ResolveLibrary( var candidates = new[] { - Path.Combine(baseDirectory, "jax_i18n_core.dll"), - Path.Combine(baseDirectory, "runtimes", runtimeIdentifier, "native", "jax_i18n_core.dll") + Path.Combine(baseDirectory, "localesmith_core.dll"), + Path.Combine(baseDirectory, "runtimes", runtimeIdentifier, "native", "localesmith_core.dll") }; foreach (var candidate in candidates) diff --git a/src/LocaleSmith.NativeInterop/NativeMethods.cs b/src/LocaleSmith.NativeInterop/NativeMethods.cs new file mode 100644 index 0000000..fca4d1a --- /dev/null +++ b/src/LocaleSmith.NativeInterop/NativeMethods.cs @@ -0,0 +1,29 @@ +using System.Runtime.InteropServices; + +namespace LocaleSmith.NativeInterop; + +internal static partial class NativeMethods +{ + internal const string LibraryName = "localesmith_core"; + + [LibraryImport(LibraryName, EntryPoint = "localesmith_core_version")] + internal static partial nint CoreVersion(); + + [LibraryImport(LibraryName, EntryPoint = "localesmith_manifest_schema_version")] + internal static partial uint ManifestSchemaVersion(); + + [LibraryImport( + LibraryName, + EntryPoint = "localesmith_scan_archive_json", + StringMarshalling = StringMarshalling.Utf8)] + internal static partial int ScanArchiveJson(string pathUtf8, out nint jsonUtf8); + + [LibraryImport(LibraryName, EntryPoint = "localesmith_string_free")] + internal static partial void StringFree(nint value); + + [LibraryImport(LibraryName, EntryPoint = "localesmith_last_error_code")] + internal static partial int LastErrorCode(); + + [LibraryImport(LibraryName, EntryPoint = "localesmith_last_error_message")] + internal static partial nint LastErrorMessage(); +} diff --git a/src/JaxI18n.Presentation/Abstractions/PresentationServices.cs b/src/LocaleSmith.Presentation/Abstractions/PresentationServices.cs similarity index 94% rename from src/JaxI18n.Presentation/Abstractions/PresentationServices.cs rename to src/LocaleSmith.Presentation/Abstractions/PresentationServices.cs index 395b8b9..fb289bc 100644 --- a/src/JaxI18n.Presentation/Abstractions/PresentationServices.cs +++ b/src/LocaleSmith.Presentation/Abstractions/PresentationServices.cs @@ -1,7 +1,7 @@ -using JaxI18n.Core.Models; -using JaxI18n.Presentation.Models; +using LocaleSmith.Core.Models; +using LocaleSmith.Presentation.Models; -namespace JaxI18n.Presentation.Abstractions; +namespace LocaleSmith.Presentation.Abstractions; public interface IAppConfigurationService { @@ -92,7 +92,7 @@ public interface IUiTextProvider public interface ICliDiagnosticRequestFactory { - Task CreateAsync( + Task CreateAsync( string sandboxPath, CancellationToken cancellationToken = default); } diff --git a/src/LocaleSmith.Presentation/LocaleSmith.Presentation.csproj b/src/LocaleSmith.Presentation/LocaleSmith.Presentation.csproj new file mode 100644 index 0000000..d9c4810 --- /dev/null +++ b/src/LocaleSmith.Presentation/LocaleSmith.Presentation.csproj @@ -0,0 +1,16 @@ + + + net10.0 + LocaleSmith.Presentation + LocaleSmith.Presentation + + + + + + + + + + + diff --git a/src/JaxI18n.Presentation/Models/AnimationPreferencePolicy.cs b/src/LocaleSmith.Presentation/Models/AnimationPreferencePolicy.cs similarity index 92% rename from src/JaxI18n.Presentation/Models/AnimationPreferencePolicy.cs rename to src/LocaleSmith.Presentation/Models/AnimationPreferencePolicy.cs index ea9390e..a314e67 100644 --- a/src/JaxI18n.Presentation/Models/AnimationPreferencePolicy.cs +++ b/src/LocaleSmith.Presentation/Models/AnimationPreferencePolicy.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Presentation.Models; +namespace LocaleSmith.Presentation.Models; /// Pure policy shared by the presentation layer and the WinUI animation service. public static class AnimationPreferencePolicy diff --git a/src/JaxI18n.Presentation/Models/AppConfiguration.cs b/src/LocaleSmith.Presentation/Models/AppConfiguration.cs similarity index 97% rename from src/JaxI18n.Presentation/Models/AppConfiguration.cs rename to src/LocaleSmith.Presentation/Models/AppConfiguration.cs index a1e0264..3467a49 100644 --- a/src/JaxI18n.Presentation/Models/AppConfiguration.cs +++ b/src/LocaleSmith.Presentation/Models/AppConfiguration.cs @@ -1,6 +1,6 @@ -using JaxI18n.Core.Models; +using LocaleSmith.Core.Models; -namespace JaxI18n.Presentation.Models; +namespace LocaleSmith.Presentation.Models; public enum AppThemePreference { diff --git a/src/JaxI18n.Presentation/Models/OnboardingModelPath.cs b/src/LocaleSmith.Presentation/Models/OnboardingModelPath.cs similarity index 63% rename from src/JaxI18n.Presentation/Models/OnboardingModelPath.cs rename to src/LocaleSmith.Presentation/Models/OnboardingModelPath.cs index 783e7ae..cb67b1e 100644 --- a/src/JaxI18n.Presentation/Models/OnboardingModelPath.cs +++ b/src/LocaleSmith.Presentation/Models/OnboardingModelPath.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Presentation.Models; +namespace LocaleSmith.Presentation.Models; public enum OnboardingModelPath { diff --git a/src/JaxI18n.Presentation/Models/TranslationQueueModels.cs b/src/LocaleSmith.Presentation/Models/TranslationQueueModels.cs similarity index 89% rename from src/JaxI18n.Presentation/Models/TranslationQueueModels.cs rename to src/LocaleSmith.Presentation/Models/TranslationQueueModels.cs index 44ed8c7..bb8e13c 100644 --- a/src/JaxI18n.Presentation/Models/TranslationQueueModels.cs +++ b/src/LocaleSmith.Presentation/Models/TranslationQueueModels.cs @@ -1,7 +1,7 @@ -using JaxI18n.Application.Models; -using JaxI18n.Core.Models; +using LocaleSmith.Application.Models; +using LocaleSmith.Core.Models; -namespace JaxI18n.Presentation.Models; +namespace LocaleSmith.Presentation.Models; public sealed record TranslationQueueRequest( string SourcePath, @@ -49,4 +49,4 @@ public sealed record TranslationQueueProgress( public sealed record ModelAssistantCompletion( string Content, - IReadOnlyList ProposedCommands); + IReadOnlyList ProposedCommands); diff --git a/src/JaxI18n.Presentation/Services/DefaultOutputPathStrategy.cs b/src/LocaleSmith.Presentation/Services/DefaultOutputPathStrategy.cs similarity index 98% rename from src/JaxI18n.Presentation/Services/DefaultOutputPathStrategy.cs rename to src/LocaleSmith.Presentation/Services/DefaultOutputPathStrategy.cs index 8ab7fed..a01752b 100644 --- a/src/JaxI18n.Presentation/Services/DefaultOutputPathStrategy.cs +++ b/src/LocaleSmith.Presentation/Services/DefaultOutputPathStrategy.cs @@ -1,6 +1,6 @@ -using JaxI18n.Presentation.Abstractions; +using LocaleSmith.Presentation.Abstractions; -namespace JaxI18n.Presentation.Services; +namespace LocaleSmith.Presentation.Services; public sealed class DefaultOutputPathStrategy : IOutputPathStrategy { diff --git a/src/JaxI18n.Presentation/ViewModels/AssistantViewModel.cs b/src/LocaleSmith.Presentation/ViewModels/AssistantViewModel.cs similarity index 98% rename from src/JaxI18n.Presentation/ViewModels/AssistantViewModel.cs rename to src/LocaleSmith.Presentation/ViewModels/AssistantViewModel.cs index 0704289..f0be1bb 100644 --- a/src/JaxI18n.Presentation/ViewModels/AssistantViewModel.cs +++ b/src/LocaleSmith.Presentation/ViewModels/AssistantViewModel.cs @@ -1,11 +1,11 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using JaxI18n.Core.Models; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; +using LocaleSmith.Core.Models; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; -namespace JaxI18n.Presentation.ViewModels; +namespace LocaleSmith.Presentation.ViewModels; public sealed class AssistantChatMessageViewModel : ObservableObject { diff --git a/src/JaxI18n.Presentation/ViewModels/CliConfirmationViewModel.cs b/src/LocaleSmith.Presentation/ViewModels/CliConfirmationViewModel.cs similarity index 96% rename from src/JaxI18n.Presentation/ViewModels/CliConfirmationViewModel.cs rename to src/LocaleSmith.Presentation/ViewModels/CliConfirmationViewModel.cs index faa891e..f85f4ce 100644 --- a/src/JaxI18n.Presentation/ViewModels/CliConfirmationViewModel.cs +++ b/src/LocaleSmith.Presentation/ViewModels/CliConfirmationViewModel.cs @@ -1,8 +1,8 @@ using CommunityToolkit.Mvvm.Input; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Presentation.ViewModels; +namespace LocaleSmith.Presentation.ViewModels; public sealed class CliConfirmationViewModel : ViewModelBase, IDisposable { @@ -11,7 +11,7 @@ public sealed class CliConfirmationViewModel : ViewModelBase, IDisposable private readonly ICliCommandPolicy _policy; private readonly ICliApprovalService _approvalService; private readonly ICliRunner _runner; - private readonly JaxI18n.Presentation.Abstractions.IUiTextProvider _text; + private readonly LocaleSmith.Presentation.Abstractions.IUiTextProvider _text; private readonly CancellationTokenSource _executionCancellation = new(); private CliPolicyDecision _policyDecision; private bool _riskAcknowledged; @@ -27,13 +27,13 @@ public CliConfirmationViewModel( ICliRunner runner, TerminalEnvironmentContext terminalEnvironment, string auditLogPath, - JaxI18n.Presentation.Abstractions.IUiTextProvider? text = null) + LocaleSmith.Presentation.Abstractions.IUiTextProvider? text = null) { _command = command ?? throw new ArgumentNullException(nameof(command)); _policy = policy ?? throw new ArgumentNullException(nameof(policy)); _approvalService = approvalService ?? throw new ArgumentNullException(nameof(approvalService)); _runner = runner ?? throw new ArgumentNullException(nameof(runner)); - _text = text ?? JaxI18n.Presentation.Abstractions.FallbackUiTextProvider.Instance; + _text = text ?? LocaleSmith.Presentation.Abstractions.FallbackUiTextProvider.Instance; ArgumentNullException.ThrowIfNull(terminalEnvironment); ArgumentException.ThrowIfNullOrWhiteSpace(auditLogPath); diff --git a/src/JaxI18n.Presentation/ViewModels/DashboardViewModel.cs b/src/LocaleSmith.Presentation/ViewModels/DashboardViewModel.cs similarity index 99% rename from src/JaxI18n.Presentation/ViewModels/DashboardViewModel.cs rename to src/LocaleSmith.Presentation/ViewModels/DashboardViewModel.cs index 0dc403e..52a9446 100644 --- a/src/JaxI18n.Presentation/ViewModels/DashboardViewModel.cs +++ b/src/LocaleSmith.Presentation/ViewModels/DashboardViewModel.cs @@ -2,12 +2,12 @@ using System.Globalization; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using JaxI18n.Application.Models; -using JaxI18n.Core.Models; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; +using LocaleSmith.Application.Models; +using LocaleSmith.Core.Models; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; -namespace JaxI18n.Presentation.ViewModels; +namespace LocaleSmith.Presentation.ViewModels; public sealed class ModelSourceOptionViewModel(ModelSource source) : ObservableObject { diff --git a/src/JaxI18n.Presentation/ViewModels/ModelSourcesViewModel.cs b/src/LocaleSmith.Presentation/ViewModels/ModelSourcesViewModel.cs similarity index 99% rename from src/JaxI18n.Presentation/ViewModels/ModelSourcesViewModel.cs rename to src/LocaleSmith.Presentation/ViewModels/ModelSourcesViewModel.cs index 8aec379..5cb5495 100644 --- a/src/JaxI18n.Presentation/ViewModels/ModelSourcesViewModel.cs +++ b/src/LocaleSmith.Presentation/ViewModels/ModelSourcesViewModel.cs @@ -3,11 +3,11 @@ using System.Security.Cryptography; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using JaxI18n.Core.Models; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; +using LocaleSmith.Core.Models; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; -namespace JaxI18n.Presentation.ViewModels; +namespace LocaleSmith.Presentation.ViewModels; public enum ConnectionTestState { diff --git a/src/JaxI18n.Presentation/ViewModels/OnboardingViewModel.cs b/src/LocaleSmith.Presentation/ViewModels/OnboardingViewModel.cs similarity index 98% rename from src/JaxI18n.Presentation/ViewModels/OnboardingViewModel.cs rename to src/LocaleSmith.Presentation/ViewModels/OnboardingViewModel.cs index 8cf8df4..96f7811 100644 --- a/src/JaxI18n.Presentation/ViewModels/OnboardingViewModel.cs +++ b/src/LocaleSmith.Presentation/ViewModels/OnboardingViewModel.cs @@ -1,11 +1,11 @@ using System.Runtime.InteropServices; using System.Security.Cryptography; using CommunityToolkit.Mvvm.Input; -using JaxI18n.Core.Models; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; +using LocaleSmith.Core.Models; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; -namespace JaxI18n.Presentation.ViewModels; +namespace LocaleSmith.Presentation.ViewModels; public sealed class OnboardingViewModel : ViewModelBase { diff --git a/src/JaxI18n.Presentation/ViewModels/SettingsViewModel.cs b/src/LocaleSmith.Presentation/ViewModels/SettingsViewModel.cs similarity index 97% rename from src/JaxI18n.Presentation/ViewModels/SettingsViewModel.cs rename to src/LocaleSmith.Presentation/ViewModels/SettingsViewModel.cs index 68d9545..f1f0a4d 100644 --- a/src/JaxI18n.Presentation/ViewModels/SettingsViewModel.cs +++ b/src/LocaleSmith.Presentation/ViewModels/SettingsViewModel.cs @@ -1,8 +1,8 @@ using CommunityToolkit.Mvvm.Input; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; -namespace JaxI18n.Presentation.ViewModels; +namespace LocaleSmith.Presentation.ViewModels; public sealed class SettingsViewModel : ViewModelBase { diff --git a/src/JaxI18n.Presentation/ViewModels/ShellViewModel.cs b/src/LocaleSmith.Presentation/ViewModels/ShellViewModel.cs similarity index 95% rename from src/JaxI18n.Presentation/ViewModels/ShellViewModel.cs rename to src/LocaleSmith.Presentation/ViewModels/ShellViewModel.cs index 5f16973..7424962 100644 --- a/src/JaxI18n.Presentation/ViewModels/ShellViewModel.cs +++ b/src/LocaleSmith.Presentation/ViewModels/ShellViewModel.cs @@ -1,8 +1,8 @@ using CommunityToolkit.Mvvm.Input; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; -namespace JaxI18n.Presentation.ViewModels; +namespace LocaleSmith.Presentation.ViewModels; public sealed class ShellViewModel : ViewModelBase { diff --git a/src/JaxI18n.Presentation/ViewModels/ViewModelBase.cs b/src/LocaleSmith.Presentation/ViewModels/ViewModelBase.cs similarity index 92% rename from src/JaxI18n.Presentation/ViewModels/ViewModelBase.cs rename to src/LocaleSmith.Presentation/ViewModels/ViewModelBase.cs index 96df5d5..31b610a 100644 --- a/src/JaxI18n.Presentation/ViewModels/ViewModelBase.cs +++ b/src/LocaleSmith.Presentation/ViewModels/ViewModelBase.cs @@ -1,7 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; -using JaxI18n.Presentation.Abstractions; +using LocaleSmith.Presentation.Abstractions; -namespace JaxI18n.Presentation.ViewModels; +namespace LocaleSmith.Presentation.ViewModels; public abstract class ViewModelBase : ObservableObject { diff --git a/tests/LocaleSmith.App.Tests/FileTranslationMemoryStoreMigrationTests.cs b/tests/LocaleSmith.App.Tests/FileTranslationMemoryStoreMigrationTests.cs new file mode 100644 index 0000000..7df1e38 --- /dev/null +++ b/tests/LocaleSmith.App.Tests/FileTranslationMemoryStoreMigrationTests.cs @@ -0,0 +1,63 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using LocaleSmith.App.Services; +using LocaleSmith.Application.Models; +using LocaleSmith.Core.Models; + +namespace LocaleSmith.App.Tests; + +public sealed class FileTranslationMemoryStoreMigrationTests +{ + private static readonly JsonSerializerOptions WebJsonOptions = new(JsonSerializerDefaults.Web); + + [Fact] + public async Task LoadCopiesLegacySlotToLocaleSmithSlotWithoutDeletingLegacyFile() + { + var cancellationToken = TestContext.Current.CancellationToken; + var directory = Directory.CreateTempSubdirectory("localesmith-memory-migration-"); + try + { + var key = new TranslationMemoryKey( + "example-package", + "zh_cn", + "source-a", + TranslationPromptContract.CurrentVersion).Normalize(); + var entry = new TranslatedEntry( + "assets/example/lang/en_us.json", + "example.key", + "source-hash", + [new TranslationVariant(TranslationStyle.Formal, "译文")]); + var snapshot = new TranslationMemorySnapshot( + key, + new Dictionary(StringComparer.Ordinal) + { + ["entry"] = entry + }); + var legacyPath = GetPath(directory.FullName, key.LegacyPackageIdentity, key.TargetLanguage); + var currentPath = GetPath(directory.FullName, key.PackageIdentity, key.TargetLanguage); + await File.WriteAllTextAsync( + legacyPath, + JsonSerializer.Serialize(snapshot, WebJsonOptions), + cancellationToken); + + using var store = new FileTranslationMemoryStore(directory.FullName); + var loaded = await store.LoadAsync(key, cancellationToken); + + Assert.Equal("译文", Assert.Single(loaded.Entries["entry"].Variants).Text); + Assert.True(File.Exists(legacyPath)); + Assert.True(File.Exists(currentPath)); + } + finally + { + directory.Delete(recursive: true); + } + } + + private static string GetPath(string directory, string packageIdentity, string targetLanguage) + { + var identity = $"{packageIdentity}\0{targetLanguage}"; + var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(identity))).ToLowerInvariant(); + return Path.Combine(directory, $"{hash}.json"); + } +} diff --git a/tests/LocaleSmith.App.Tests/LegacyAppDataLocatorTests.cs b/tests/LocaleSmith.App.Tests/LegacyAppDataLocatorTests.cs new file mode 100644 index 0000000..20b04c9 --- /dev/null +++ b/tests/LocaleSmith.App.Tests/LegacyAppDataLocatorTests.cs @@ -0,0 +1,137 @@ +using LocaleSmith.App.Services; + +namespace LocaleSmith.App.Tests; + +public sealed class LegacyAppDataLocatorTests +{ + [Fact] + public void FindLegacyRootsOrdersRegisteredMsixStateBeforeUnpackagedFallback() + { + var root = Directory.CreateTempSubdirectory("localesmith-legacy-locator-"); + try + { + const string firstPackageFamilyName = "JaxI18n.Desktop_apublisher"; + const string secondPackageFamilyName = "JaxI18n.Desktop_zpublisher"; + var secondMsixRoot = CreateMsixLegacyRoot(root.FullName, secondPackageFamilyName); + var firstMsixRoot = CreateMsixLegacyRoot(root.FullName, firstPackageFamilyName); + var unpackagedRoot = Directory.CreateDirectory(Path.Combine(root.FullName, "JaxI18n")).FullName; + _ = CreateMsixLegacyRoot(root.FullName, "JaxI18n.Desktop_unregistered"); + + var roots = LegacyAppDataLocator.FindLegacyRoots( + root.FullName, + [secondPackageFamilyName, firstPackageFamilyName]); + + Assert.Equal([firstMsixRoot, secondMsixRoot, unpackagedRoot], roots); + } + finally + { + root.Delete(recursive: true); + } + } + + [Fact] + public void FindLegacyRootsUsesUnpackagedFallbackWhenNoLegacyPackageIsRegistered() + { + var root = Directory.CreateTempSubdirectory("localesmith-legacy-locator-fallback-"); + try + { + var unpackagedRoot = Directory.CreateDirectory(Path.Combine(root.FullName, "JaxI18n")).FullName; + + var roots = LegacyAppDataLocator.FindLegacyRoots(root.FullName, []); + + Assert.Equal([unpackagedRoot], roots); + } + finally + { + root.Delete(recursive: true); + } + } + + [Fact] + public void FindLegacyRootsReturnsEmptyWhenNoLegacyStateExists() + { + var root = Directory.CreateTempSubdirectory("localesmith-legacy-locator-empty-"); + try + { + Assert.Empty(LegacyAppDataLocator.FindLegacyRoots(root.FullName, [])); + } + finally + { + root.Delete(recursive: true); + } + } + + [Fact] + public void FindLegacyRootsRejectsPackageFamilyNamesThatCouldEscapePackagesRoot() + { + var root = Directory.CreateTempSubdirectory("localesmith-legacy-locator-boundary-"); + try + { + var fallback = Directory.CreateDirectory(Path.Combine(root.FullName, "JaxI18n")).FullName; + var roots = LegacyAppDataLocator.FindLegacyRoots( + root.FullName, + ["JaxI18n.Desktop_..\\..\\escape", "Unrelated.Package_family"]); + + Assert.Equal([fallback], roots); + } + finally + { + root.Delete(recursive: true); + } + } + + [Fact] + public void FindLegacyRootsRejectsReparsePointPackageState() + { + var root = Directory.CreateTempSubdirectory("localesmith-legacy-locator-reparse-"); + var external = Directory.CreateTempSubdirectory("localesmith-legacy-locator-external-"); + const string packageFamilyName = "JaxI18n.Desktop_reparse"; + var packagesRoot = Directory.CreateDirectory(Path.Combine(root.FullName, "Packages")).FullName; + var packageLink = Path.Combine(packagesRoot, packageFamilyName); + try + { + _ = Directory.CreateDirectory(Path.Combine(external.FullName, "LocalCache", "Local", "JaxI18n")); + try + { + _ = Directory.CreateSymbolicLink(packageLink, external.FullName); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + // Symbolic-link creation can be disabled by local Windows policy. + return; + } + + Assert.Empty(LegacyAppDataLocator.FindLegacyRoots(root.FullName, [packageFamilyName])); + } + finally + { + if (Directory.Exists(packageLink)) + { + Directory.Delete(packageLink); + } + + root.Delete(recursive: true); + external.Delete(recursive: true); + } + } + + [Fact] + public void UnredirectedLocalApplicationDataPathIsAbsoluteAndExists() + { + var path = LegacyAppDataLocator.GetUnredirectedLocalApplicationDataPath(); + + Assert.True(Path.IsPathFullyQualified(path)); + Assert.True(Directory.Exists(path)); + } + + private static string CreateMsixLegacyRoot(string localAppDataRoot, string packageFamilyName) + { + return Directory.CreateDirectory(Path.Combine( + localAppDataRoot, + "Packages", + packageFamilyName, + "LocalCache", + "Local", + "JaxI18n")).FullName; + } +} diff --git a/tests/LocaleSmith.App.Tests/LegacyAppDataMigratorTests.cs b/tests/LocaleSmith.App.Tests/LegacyAppDataMigratorTests.cs new file mode 100644 index 0000000..e07e994 --- /dev/null +++ b/tests/LocaleSmith.App.Tests/LegacyAppDataMigratorTests.cs @@ -0,0 +1,163 @@ +using LocaleSmith.App.Services; +using LocaleSmith.Infrastructure.Security; +using LocaleSmith.Presentation.Models; + +namespace LocaleSmith.App.Tests; + +public sealed class LegacyAppDataMigratorTests +{ + [Fact] + public async Task MigratesConfigurationCredentialsMemoryAndLogsWithoutDeletingLegacyState() + { + var cancellationToken = TestContext.Current.CancellationToken; + var root = Directory.CreateTempSubdirectory("localesmith-app-data-migration-"); + using var legacySecrets = new InMemorySecretStore(); + using var currentSecrets = new InMemorySecretStore(); + try + { + var legacyRoot = Path.Combine(root.FullName, "JaxI18n"); + var currentRoot = Path.Combine(root.FullName, "LocaleSmith"); + var credentialReference = "providers/test-source"; + var documentsRoot = System.Environment.GetFolderPath( + System.Environment.SpecialFolder.MyDocuments); + Assert.False(string.IsNullOrWhiteSpace(documentsRoot)); + await legacySecrets.SetAsync(credentialReference, "legacy-api-key".AsMemory(), cancellationToken); + + Directory.CreateDirectory(legacyRoot); + using (var legacyConfiguration = new EncryptedJsonConfigurationStore( + Path.Combine(legacyRoot, LegacyAppDataMigrator.LegacyConfigurationFileName), + LegacyAppDataMigrator.LegacyConfigurationPurpose, + new CredentialManagerMasterKeyStore(legacySecrets), + associatedDataNamespace: LegacyAppDataMigrator.LegacyAssociatedDataNamespace)) + { + await legacyConfiguration.SaveAsync(new AppConfiguration + { + IsOnboardingComplete = true, + WorkspacePath = Path.Combine(documentsRoot, "JaxI18n"), + SandboxPath = Path.Combine(Path.GetTempPath(), "JaxI18n", "Sandbox"), + ModelSources = + [ + new ModelSourceProfile + { + Id = "test-source", + DisplayName = "Test source", + CredentialReference = credentialReference + } + ] + }, cancellationToken); + } + + var legacyMemory = Path.Combine(legacyRoot, "translation-memory", "legacy.json"); + var legacyLog = Path.Combine(legacyRoot, "logs", "cli-audit.jsonl"); + Directory.CreateDirectory(Path.GetDirectoryName(legacyMemory)!); + Directory.CreateDirectory(Path.GetDirectoryName(legacyLog)!); + await File.WriteAllTextAsync(legacyMemory, "legacy-memory", cancellationToken); + await File.WriteAllTextAsync(legacyLog, "legacy-log", cancellationToken); + + var migrator = new LegacyAppDataMigrator( + legacyRoot, + currentRoot, + legacySecrets, + currentSecrets); + await migrator.MigrateAsync(cancellationToken); + + using var currentConfiguration = new EncryptedJsonConfigurationStore( + Path.Combine(currentRoot, LegacyAppDataMigrator.CurrentConfigurationFileName), + LegacyAppDataMigrator.CurrentConfigurationPurpose, + new CredentialManagerMasterKeyStore(currentSecrets)); + var migrated = await currentConfiguration.LoadAsync(cancellationToken); + Assert.NotNull(migrated); + Assert.True(migrated.IsOnboardingComplete); + Assert.Equal(Path.Combine(documentsRoot, "LocaleSmith"), migrated.WorkspacePath); + Assert.Equal( + Path.Combine(Path.GetTempPath(), "LocaleSmith", "Sandbox"), + migrated.SandboxPath); + Assert.Equal(credentialReference, Assert.Single(migrated.ModelSources).CredentialReference); + + using var currentCredential = await currentSecrets.ResolveAsync(credentialReference, cancellationToken); + using var legacyCredential = await legacySecrets.ResolveAsync(credentialReference, cancellationToken); + Assert.Equal("legacy-api-key", currentCredential?.DangerousGetString()); + Assert.Equal("legacy-api-key", legacyCredential?.DangerousGetString()); + Assert.Equal( + "legacy-memory", + await File.ReadAllTextAsync( + Path.Combine(currentRoot, "translation-memory", "legacy.json"), + cancellationToken)); + Assert.Equal( + "legacy-log", + await File.ReadAllTextAsync(Path.Combine(currentRoot, "logs", "cli-audit.jsonl"), cancellationToken)); + Assert.True(File.Exists(legacyMemory)); + Assert.True(File.Exists(legacyLog)); + + await File.WriteAllTextAsync( + Path.Combine(currentRoot, "translation-memory", "legacy.json"), + "current-memory", + cancellationToken); + await migrator.MigrateAsync(cancellationToken); + Assert.Equal( + "current-memory", + await File.ReadAllTextAsync( + Path.Combine(currentRoot, "translation-memory", "legacy.json"), + cancellationToken)); + } + finally + { + root.Delete(recursive: true); + } + } + + [Fact] + public async Task ExistingCurrentConfigurationIsNeverOverwritten() + { + var cancellationToken = TestContext.Current.CancellationToken; + var root = Directory.CreateTempSubdirectory("localesmith-current-config-preservation-"); + using var legacySecrets = new InMemorySecretStore(); + using var currentSecrets = new InMemorySecretStore(); + try + { + var legacyRoot = Directory.CreateDirectory(Path.Combine(root.FullName, "JaxI18n")).FullName; + var currentRoot = Directory.CreateDirectory(Path.Combine(root.FullName, "LocaleSmith")).FullName; + using (var legacyConfiguration = new EncryptedJsonConfigurationStore( + Path.Combine(legacyRoot, LegacyAppDataMigrator.LegacyConfigurationFileName), + LegacyAppDataMigrator.LegacyConfigurationPurpose, + new CredentialManagerMasterKeyStore(legacySecrets), + associatedDataNamespace: LegacyAppDataMigrator.LegacyAssociatedDataNamespace)) + { + await legacyConfiguration.SaveAsync( + new AppConfiguration { Language = "legacy-language" }, + cancellationToken); + } + + var currentPath = Path.Combine( + currentRoot, + LegacyAppDataMigrator.CurrentConfigurationFileName); + using (var currentConfiguration = new EncryptedJsonConfigurationStore( + currentPath, + LegacyAppDataMigrator.CurrentConfigurationPurpose, + new CredentialManagerMasterKeyStore(currentSecrets))) + { + await currentConfiguration.SaveAsync( + new AppConfiguration { Language = "current-language" }, + cancellationToken); + } + + var migrator = new LegacyAppDataMigrator( + legacyRoot, + currentRoot, + legacySecrets, + currentSecrets); + await migrator.MigrateAsync(cancellationToken); + + using var verificationStore = new EncryptedJsonConfigurationStore( + currentPath, + LegacyAppDataMigrator.CurrentConfigurationPurpose, + new CredentialManagerMasterKeyStore(currentSecrets)); + var preserved = await verificationStore.LoadAsync(cancellationToken); + Assert.Equal("current-language", preserved?.Language); + } + finally + { + root.Delete(recursive: true); + } + } +} diff --git a/tests/JaxI18n.App.Tests/JaxI18n.App.Tests.csproj b/tests/LocaleSmith.App.Tests/LocaleSmith.App.Tests.csproj similarity index 83% rename from tests/JaxI18n.App.Tests/JaxI18n.App.Tests.csproj rename to tests/LocaleSmith.App.Tests/LocaleSmith.App.Tests.csproj index aace9bd..026ab7e 100644 --- a/tests/JaxI18n.App.Tests/JaxI18n.App.Tests.csproj +++ b/tests/LocaleSmith.App.Tests/LocaleSmith.App.Tests.csproj @@ -22,7 +22,7 @@ - - + + diff --git a/tests/JaxI18n.App.Tests/ModelAssistantServiceTests.cs b/tests/LocaleSmith.App.Tests/ModelAssistantServiceTests.cs similarity index 97% rename from tests/JaxI18n.App.Tests/ModelAssistantServiceTests.cs rename to tests/LocaleSmith.App.Tests/ModelAssistantServiceTests.cs index dea4f78..b5c3cd0 100644 --- a/tests/JaxI18n.App.Tests/ModelAssistantServiceTests.cs +++ b/tests/LocaleSmith.App.Tests/ModelAssistantServiceTests.cs @@ -1,18 +1,18 @@ using System.Net; using System.Text; using System.Text.Json; -using JaxI18n.App.Services; -using JaxI18n.Application.Services; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; -using JaxI18n.Core.Services; -using JaxI18n.Infrastructure.Models; -using JaxI18n.Infrastructure.Security; -using JaxI18n.Mcp; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; - -namespace JaxI18n.App.Tests; +using LocaleSmith.App.Services; +using LocaleSmith.Application.Services; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; +using LocaleSmith.Core.Services; +using LocaleSmith.Infrastructure.Models; +using LocaleSmith.Infrastructure.Security; +using LocaleSmith.Mcp; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; + +namespace LocaleSmith.App.Tests; public sealed class ModelAssistantServiceTests { @@ -304,7 +304,7 @@ public Task LoadAsync(CancellationToken cancellationToken = de return Task.FromResult(new AppConfiguration { IsOnboardingComplete = true, - WorkspacePath = Path.Combine(Path.GetTempPath(), "JaxI18n", "Workspace"), + WorkspacePath = Path.Combine(Path.GetTempPath(), "LocaleSmith", "Workspace"), SandboxPath = sandboxPath }); } diff --git a/tests/JaxI18n.App.Tests/PipelineTranslationQueueServiceTests.cs b/tests/LocaleSmith.App.Tests/PipelineTranslationQueueServiceTests.cs similarity index 93% rename from tests/JaxI18n.App.Tests/PipelineTranslationQueueServiceTests.cs rename to tests/LocaleSmith.App.Tests/PipelineTranslationQueueServiceTests.cs index 3b9f085..08eca10 100644 --- a/tests/JaxI18n.App.Tests/PipelineTranslationQueueServiceTests.cs +++ b/tests/LocaleSmith.App.Tests/PipelineTranslationQueueServiceTests.cs @@ -1,11 +1,11 @@ -using JaxI18n.App.Services; -using JaxI18n.Application.Abstractions; -using JaxI18n.Application.Models; -using JaxI18n.Application.Services; -using JaxI18n.Core.Models; -using JaxI18n.Presentation.Models; +using LocaleSmith.App.Services; +using LocaleSmith.Application.Abstractions; +using LocaleSmith.Application.Models; +using LocaleSmith.Application.Services; +using LocaleSmith.Core.Models; +using LocaleSmith.Presentation.Models; -namespace JaxI18n.App.Tests; +namespace LocaleSmith.App.Tests; public sealed class PipelineTranslationQueueServiceTests { @@ -16,7 +16,7 @@ public async Task QueuePassesExactlyOneCapturedStyleAndReturnsOneArtifact(Transl { var outputPath = Path.Combine( Path.GetTempPath(), - "JaxI18n", + "LocaleSmith", "queue-style-tests", Guid.NewGuid().ToString("N"), "translated.jar"); diff --git a/tests/JaxI18n.App.Tests/SecureAppStateServiceTests.cs b/tests/LocaleSmith.App.Tests/SecureAppStateServiceTests.cs similarity index 90% rename from tests/JaxI18n.App.Tests/SecureAppStateServiceTests.cs rename to tests/LocaleSmith.App.Tests/SecureAppStateServiceTests.cs index bf6e133..e2b9169 100644 --- a/tests/JaxI18n.App.Tests/SecureAppStateServiceTests.cs +++ b/tests/LocaleSmith.App.Tests/SecureAppStateServiceTests.cs @@ -4,14 +4,14 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; -using JaxI18n.App.Services; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; -using JaxI18n.Core.Services; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; +using LocaleSmith.App.Services; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; +using LocaleSmith.Core.Services; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; -namespace JaxI18n.App.Tests; +namespace LocaleSmith.App.Tests; public sealed class SecureAppStateServiceTests { @@ -52,6 +52,77 @@ public async Task InitializeRestoresPersistedSourcesAndPublishesSelectedSnapshot Assert.Equal("ollama", published.SelectedSource?.Id); } + [Fact] + public async Task ConcurrentInitializationNormalizesLegacyDefaultPathsAndPersistsExactlyOnce() + { + var cancellationToken = TestContext.Current.CancellationToken; + var documentsRoot = System.Environment.GetFolderPath( + System.Environment.SpecialFolder.MyDocuments); + Assert.False(string.IsNullOrWhiteSpace(documentsRoot)); + var events = new ConcurrentQueue(); + var configurationStore = new RecordingConfigurationStore( + new AppConfiguration + { + IsOnboardingComplete = true, + WorkspacePath = Path.Combine(documentsRoot, "JaxI18n"), + SandboxPath = Path.Combine(Path.GetTempPath(), "JaxI18n", "Sandbox") + }, + events); + using var secretStore = new FaultInjectingSecretStore(events); + using var registry = new ModelServiceRegistry(); + using var httpClient = new HttpClient(new RejectingHttpHandler()); + using var service = new SecureAppStateService( + configurationStore, + secretStore, + registry, + httpClient, + new StubSandboxRootManager()); + + await Task.WhenAll(Enumerable.Range(0, 8) + .Select(_ => service.InitializeAsync(cancellationToken))); + var loaded = await service.LoadAsync(cancellationToken); + + Assert.Equal(Path.Combine(documentsRoot, "LocaleSmith"), loaded.WorkspacePath); + Assert.Equal(Path.Combine(Path.GetTempPath(), "LocaleSmith", "Sandbox"), loaded.SandboxPath); + Assert.Equal(loaded.WorkspacePath, configurationStore.Persisted.WorkspacePath); + Assert.Equal(loaded.SandboxPath, configurationStore.Persisted.SandboxPath); + Assert.Equal(1, events.Count(static entry => entry == "configuration:save")); + } + + [Fact] + public async Task InitializationPreservesCustomPathsContainingLegacyProductNameWithoutSaving() + { + var cancellationToken = TestContext.Current.CancellationToken; + var customRoot = Path.Combine(Path.GetTempPath(), $"custom-{Guid.NewGuid():N}"); + var customWorkspace = Path.Combine(customRoot, "JaxI18n"); + var customSandbox = Path.Combine(customRoot, "JaxI18n", "Sandbox"); + var events = new ConcurrentQueue(); + var configurationStore = new RecordingConfigurationStore( + new AppConfiguration + { + IsOnboardingComplete = true, + WorkspacePath = customWorkspace, + SandboxPath = customSandbox + }, + events); + using var secretStore = new FaultInjectingSecretStore(events); + using var registry = new ModelServiceRegistry(); + using var httpClient = new HttpClient(new RejectingHttpHandler()); + using var service = new SecureAppStateService( + configurationStore, + secretStore, + registry, + httpClient, + new StubSandboxRootManager()); + + await service.InitializeAsync(cancellationToken); + var loaded = await service.LoadAsync(cancellationToken); + + Assert.Equal(customWorkspace, loaded.WorkspacePath); + Assert.Equal(customSandbox, loaded.SandboxPath); + Assert.Empty(events); + } + [Fact] public async Task ModelSelectionNotificationCanSynchronouslyReenterStateService() { @@ -832,7 +903,7 @@ private sealed class TemporaryTestDirectory : IDisposable { public TemporaryTestDirectory() { - RootPath = Path.Combine(Path.GetTempPath(), "JaxI18n.App.Tests", Guid.NewGuid().ToString("N")); + RootPath = Path.Combine(Path.GetTempPath(), "LocaleSmith.App.Tests", Guid.NewGuid().ToString("N")); WorkspacePath = Path.Combine(RootPath, "workspace"); SandboxPath = Path.Combine(RootPath, "sandbox"); } diff --git a/tests/JaxI18n.Application.Tests/JaxI18n.Application.Tests.csproj b/tests/LocaleSmith.Application.Tests/LocaleSmith.Application.Tests.csproj similarity index 81% rename from tests/JaxI18n.Application.Tests/JaxI18n.Application.Tests.csproj rename to tests/LocaleSmith.Application.Tests/LocaleSmith.Application.Tests.csproj index 3adf14a..778248d 100644 --- a/tests/JaxI18n.Application.Tests/JaxI18n.Application.Tests.csproj +++ b/tests/LocaleSmith.Application.Tests/LocaleSmith.Application.Tests.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -25,8 +25,8 @@ - - + + diff --git a/tests/JaxI18n.Application.Tests/ModelToolOrchestratorTests.cs b/tests/LocaleSmith.Application.Tests/ModelToolOrchestratorTests.cs similarity index 98% rename from tests/JaxI18n.Application.Tests/ModelToolOrchestratorTests.cs rename to tests/LocaleSmith.Application.Tests/ModelToolOrchestratorTests.cs index c026170..70659f1 100644 --- a/tests/JaxI18n.Application.Tests/ModelToolOrchestratorTests.cs +++ b/tests/LocaleSmith.Application.Tests/ModelToolOrchestratorTests.cs @@ -1,9 +1,9 @@ using System.Text.Json; -using JaxI18n.Application.Services; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Application.Services; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Application.Tests; +namespace LocaleSmith.Application.Tests; public sealed class ModelToolOrchestratorTests { diff --git a/tests/JaxI18n.Application.Tests/ModelTranslationEngineTests.cs b/tests/LocaleSmith.Application.Tests/ModelTranslationEngineTests.cs similarity index 98% rename from tests/JaxI18n.Application.Tests/ModelTranslationEngineTests.cs rename to tests/LocaleSmith.Application.Tests/ModelTranslationEngineTests.cs index 9c997de..7bf65d8 100644 --- a/tests/JaxI18n.Application.Tests/ModelTranslationEngineTests.cs +++ b/tests/LocaleSmith.Application.Tests/ModelTranslationEngineTests.cs @@ -1,10 +1,10 @@ using System.Text.Json; -using JaxI18n.Application.Services; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; -using JaxI18n.Core.Services; +using LocaleSmith.Application.Services; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; +using LocaleSmith.Core.Services; -namespace JaxI18n.Application.Tests; +namespace LocaleSmith.Application.Tests; public sealed class ModelTranslationEngineTests { diff --git a/tests/JaxI18n.Application.Tests/TranslationMemoryKeyTests.cs b/tests/LocaleSmith.Application.Tests/TranslationMemoryKeyTests.cs similarity index 80% rename from tests/JaxI18n.Application.Tests/TranslationMemoryKeyTests.cs rename to tests/LocaleSmith.Application.Tests/TranslationMemoryKeyTests.cs index ebd8886..62f95e6 100644 --- a/tests/JaxI18n.Application.Tests/TranslationMemoryKeyTests.cs +++ b/tests/LocaleSmith.Application.Tests/TranslationMemoryKeyTests.cs @@ -1,7 +1,7 @@ using System.Text.Json; -using JaxI18n.Application.Models; +using LocaleSmith.Application.Models; -namespace JaxI18n.Application.Tests; +namespace LocaleSmith.Application.Tests; public sealed class TranslationMemoryKeyTests { @@ -49,6 +49,16 @@ public void NormalizedKeyRoundTripsThroughPersistentSnapshotJson() Assert.Equal(GetExistingStoreSlot(key), GetExistingStoreSlot(restored!)); } + [Fact] + public void ExposesDistinctLegacyIdentityForOneTimeCacheMigration() + { + var key = Create("cloud-source").Normalize(); + + Assert.Contains("localesmith.translation-memory/v2", key.PackageIdentity, StringComparison.Ordinal); + Assert.Contains("jax-i18n.translation-memory/v2", key.LegacyPackageIdentity, StringComparison.Ordinal); + Assert.NotEqual(key.PackageIdentity, key.LegacyPackageIdentity); + } + private static TranslationMemoryKey Create( string? modelSourceId, string contractVersion = TranslationPromptContract.CurrentVersion) => diff --git a/tests/JaxI18n.Application.Tests/TranslationPipelineTests.cs b/tests/LocaleSmith.Application.Tests/TranslationPipelineTests.cs similarity index 98% rename from tests/JaxI18n.Application.Tests/TranslationPipelineTests.cs rename to tests/LocaleSmith.Application.Tests/TranslationPipelineTests.cs index 24cf5be..7fd8b23 100644 --- a/tests/JaxI18n.Application.Tests/TranslationPipelineTests.cs +++ b/tests/LocaleSmith.Application.Tests/TranslationPipelineTests.cs @@ -1,10 +1,10 @@ -using JaxI18n.Application.Abstractions; -using JaxI18n.Application.Models; -using JaxI18n.Application.Services; -using JaxI18n.Core.Models; -using JaxI18n.Core.Services; +using LocaleSmith.Application.Abstractions; +using LocaleSmith.Application.Models; +using LocaleSmith.Application.Services; +using LocaleSmith.Core.Models; +using LocaleSmith.Core.Services; -namespace JaxI18n.Application.Tests; +namespace LocaleSmith.Application.Tests; public sealed class TranslationPipelineTests { @@ -14,7 +14,7 @@ public sealed class TranslationPipelineTests [Fact] public void PipelineRequestRejectsMultipleOrUnknownStyles() { - var prefix = Path.Combine(Path.GetTempPath(), "jax-i18n-tests"); + var prefix = Path.Combine(Path.GetTempPath(), "localesmith-tests"); Assert.Throws(() => new PipelineRequest( Path.Combine(prefix, "input.jar"), @@ -416,7 +416,7 @@ private static PipelineRequest CreateRequest( HardcodedStringMode hardcodedStringMode = HardcodedStringMode.ScanOnly, TranslationStyle translationStyle = TranslationStyle.Formal) { - var prefix = Path.Combine(Path.GetTempPath(), "jax-i18n-tests"); + var prefix = Path.Combine(Path.GetTempPath(), "localesmith-tests"); return new PipelineRequest( Path.Combine(prefix, "input.jar"), Path.Combine(prefix, "output.jar"), diff --git a/tests/JaxI18n.Archive.Tests/ArchiveFixture.cs b/tests/LocaleSmith.Archive.Tests/ArchiveFixture.cs similarity index 93% rename from tests/JaxI18n.Archive.Tests/ArchiveFixture.cs rename to tests/LocaleSmith.Archive.Tests/ArchiveFixture.cs index 13e4479..4f86d0b 100644 --- a/tests/JaxI18n.Archive.Tests/ArchiveFixture.cs +++ b/tests/LocaleSmith.Archive.Tests/ArchiveFixture.cs @@ -1,7 +1,7 @@ using System.IO.Compression; using System.Text; -namespace JaxI18n.Archive.Tests; +namespace LocaleSmith.Archive.Tests; internal sealed class ArchiveFixture : IDisposable { @@ -11,7 +11,7 @@ internal sealed class ArchiveFixture : IDisposable public ArchiveFixture(string fileName) { - DirectoryPath = Directory.CreateTempSubdirectory("jax-i18n-archive-tests-").FullName; + DirectoryPath = Directory.CreateTempSubdirectory("localesmith-archive-tests-").FullName; ArchivePath = Path.Combine(DirectoryPath, fileName); _stream = new FileStream(ArchivePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None); _archive = new ZipArchive(_stream, ZipArchiveMode.Create, leaveOpen: true, entryNameEncoding: Encoding.UTF8); diff --git a/tests/JaxI18n.Archive.Tests/ArchiveWorkspaceBackendTests.cs b/tests/LocaleSmith.Archive.Tests/ArchiveWorkspaceBackendTests.cs similarity index 99% rename from tests/JaxI18n.Archive.Tests/ArchiveWorkspaceBackendTests.cs rename to tests/LocaleSmith.Archive.Tests/ArchiveWorkspaceBackendTests.cs index ff337e6..8c7594a 100644 --- a/tests/JaxI18n.Archive.Tests/ArchiveWorkspaceBackendTests.cs +++ b/tests/LocaleSmith.Archive.Tests/ArchiveWorkspaceBackendTests.cs @@ -1,12 +1,12 @@ using System.IO.Compression; using System.Text; using System.Text.Json; -using JaxI18n.Application.Models; -using JaxI18n.Archive; -using JaxI18n.Core.Models; -using JaxI18n.NativeInterop; +using LocaleSmith.Application.Models; +using LocaleSmith.Archive; +using LocaleSmith.Core.Models; +using LocaleSmith.NativeInterop; -namespace JaxI18n.Archive.Tests; +namespace LocaleSmith.Archive.Tests; public sealed class ArchiveWorkspaceBackendTests { @@ -595,7 +595,7 @@ await Assert.ThrowsAsync( fixture.DirectoryPath, $".{Path.GetFileName(request.OutputPath)}.{jobId:N}.*.staged")); Assert.False(Directory.Exists(GetWorkspacePath(jobId))); - string journalPath = Path.Combine(Path.GetTempPath(), "JaxI18n", "logs", $"{jobId:N}.jsonl"); + string journalPath = Path.Combine(Path.GetTempPath(), "LocaleSmith", "logs", $"{jobId:N}.jsonl"); Assert.True(File.Exists(journalPath)); Assert.Contains("\"operation\":\"rollback\"", await File.ReadAllTextAsync(journalPath, TestContext.Current.CancellationToken)); } @@ -778,7 +778,7 @@ private static string ReadText(ZipArchive archive, string path) } private static string GetWorkspacePath(Guid jobId) => - Path.Combine(Path.GetTempPath(), "JaxI18n", "workspaces", jobId.ToString("N")); + Path.Combine(Path.GetTempPath(), "LocaleSmith", "workspaces", jobId.ToString("N")); private sealed class FailOnVerificationScanner : IArchiveScanner { diff --git a/tests/JaxI18n.Archive.Tests/ClassFileFixtureBuilder.cs b/tests/LocaleSmith.Archive.Tests/ClassFileFixtureBuilder.cs similarity index 99% rename from tests/JaxI18n.Archive.Tests/ClassFileFixtureBuilder.cs rename to tests/LocaleSmith.Archive.Tests/ClassFileFixtureBuilder.cs index ecdf4ac..32db8fb 100644 --- a/tests/JaxI18n.Archive.Tests/ClassFileFixtureBuilder.cs +++ b/tests/LocaleSmith.Archive.Tests/ClassFileFixtureBuilder.cs @@ -1,7 +1,7 @@ using System.Buffers.Binary; using System.Text; -namespace JaxI18n.Archive.Tests; +namespace LocaleSmith.Archive.Tests; internal enum ClassFileFixtureKind { diff --git a/tests/JaxI18n.Archive.Tests/FolderFixture.cs b/tests/LocaleSmith.Archive.Tests/FolderFixture.cs similarity index 95% rename from tests/JaxI18n.Archive.Tests/FolderFixture.cs rename to tests/LocaleSmith.Archive.Tests/FolderFixture.cs index 841b453..18c4a5d 100644 --- a/tests/JaxI18n.Archive.Tests/FolderFixture.cs +++ b/tests/LocaleSmith.Archive.Tests/FolderFixture.cs @@ -1,13 +1,13 @@ using System.Security.Cryptography; using System.Text; -namespace JaxI18n.Archive.Tests; +namespace LocaleSmith.Archive.Tests; internal sealed class FolderFixture : IDisposable { public FolderFixture(string folderName) { - RootPath = Directory.CreateTempSubdirectory("jax-i18n-folder-tests-").FullName; + RootPath = Directory.CreateTempSubdirectory("localesmith-folder-tests-").FullName; SourcePath = Path.Combine(RootPath, folderName); Directory.CreateDirectory(SourcePath); } diff --git a/tests/JaxI18n.NativeInterop.Tests/JaxI18n.NativeInterop.Tests.csproj b/tests/LocaleSmith.Archive.Tests/LocaleSmith.Archive.Tests.csproj similarity index 73% rename from tests/JaxI18n.NativeInterop.Tests/JaxI18n.NativeInterop.Tests.csproj rename to tests/LocaleSmith.Archive.Tests/LocaleSmith.Archive.Tests.csproj index 063bf2c..13f848a 100644 --- a/tests/JaxI18n.NativeInterop.Tests/JaxI18n.NativeInterop.Tests.csproj +++ b/tests/LocaleSmith.Archive.Tests/LocaleSmith.Archive.Tests.csproj @@ -21,13 +21,13 @@ - + - + Condition="Exists('..\..\native\localesmith_core\target\release\localesmith_core.dll')" /> diff --git a/tests/JaxI18n.Archive.Tests/MojangComponentLiteralClassFileRewriterTests.cs b/tests/LocaleSmith.Archive.Tests/MojangComponentLiteralClassFileRewriterTests.cs similarity index 99% rename from tests/JaxI18n.Archive.Tests/MojangComponentLiteralClassFileRewriterTests.cs rename to tests/LocaleSmith.Archive.Tests/MojangComponentLiteralClassFileRewriterTests.cs index c9781d4..dd11c23 100644 --- a/tests/JaxI18n.Archive.Tests/MojangComponentLiteralClassFileRewriterTests.cs +++ b/tests/LocaleSmith.Archive.Tests/MojangComponentLiteralClassFileRewriterTests.cs @@ -1,6 +1,6 @@ -using JaxI18n.Archive.ClassFile; +using LocaleSmith.Archive.ClassFile; -namespace JaxI18n.Archive.Tests; +namespace LocaleSmith.Archive.Tests; public sealed class MojangComponentLiteralClassFileRewriterTests { diff --git a/tests/JaxI18n.Archive.Tests/SafeSubsetExternalizationPipelineTests.cs b/tests/LocaleSmith.Archive.Tests/SafeSubsetExternalizationPipelineTests.cs similarity index 97% rename from tests/JaxI18n.Archive.Tests/SafeSubsetExternalizationPipelineTests.cs rename to tests/LocaleSmith.Archive.Tests/SafeSubsetExternalizationPipelineTests.cs index 841092d..e3d2d08 100644 --- a/tests/JaxI18n.Archive.Tests/SafeSubsetExternalizationPipelineTests.cs +++ b/tests/LocaleSmith.Archive.Tests/SafeSubsetExternalizationPipelineTests.cs @@ -1,16 +1,16 @@ using System.IO.Compression; using System.Security.Cryptography; using System.Text.Json; -using JaxI18n.Application; -using JaxI18n.Application.Abstractions; -using JaxI18n.Application.Models; -using JaxI18n.Application.Services; -using JaxI18n.Archive.ClassFile; -using JaxI18n.Core.Models; -using JaxI18n.Core.Services; -using JaxI18n.NativeInterop; +using LocaleSmith.Application; +using LocaleSmith.Application.Abstractions; +using LocaleSmith.Application.Models; +using LocaleSmith.Application.Services; +using LocaleSmith.Archive.ClassFile; +using LocaleSmith.Core.Models; +using LocaleSmith.Core.Services; +using LocaleSmith.NativeInterop; -namespace JaxI18n.Archive.Tests; +namespace LocaleSmith.Archive.Tests; public sealed class SafeSubsetExternalizationPipelineTests { @@ -126,7 +126,7 @@ await Assert.ThrowsAsync( Assert.False(File.Exists(request.OutputPath)); Assert.False(Directory.Exists(Path.Combine( Path.GetTempPath(), - "JaxI18n", + "LocaleSmith", "workspaces", jobId.ToString("N")))); Assert.Equal( @@ -179,7 +179,7 @@ public async Task ConflictingExistingFallbackKeyFailsClosedAndRollsBack() Assert.False(File.Exists(request.OutputPath)); Assert.False(Directory.Exists(Path.Combine( Path.GetTempPath(), - "JaxI18n", + "LocaleSmith", "workspaces", jobId.ToString("N")))); Assert.Equal( @@ -217,7 +217,7 @@ public async Task StagingRescanRejectsClassWhoseVerifiedRewriteWasReverted() Assert.False(File.Exists(request.OutputPath)); Assert.False(Directory.Exists(Path.Combine( Path.GetTempPath(), - "JaxI18n", + "LocaleSmith", "workspaces", jobId.ToString("N")))); } diff --git a/tests/JaxI18n.Archive.Tests/TestArchiveScanner.cs b/tests/LocaleSmith.Archive.Tests/TestArchiveScanner.cs similarity index 98% rename from tests/JaxI18n.Archive.Tests/TestArchiveScanner.cs rename to tests/LocaleSmith.Archive.Tests/TestArchiveScanner.cs index 5c00a4e..a34810f 100644 --- a/tests/JaxI18n.Archive.Tests/TestArchiveScanner.cs +++ b/tests/LocaleSmith.Archive.Tests/TestArchiveScanner.cs @@ -3,10 +3,10 @@ using System.Text; using System.Text.Json; using System.Text.RegularExpressions; -using JaxI18n.Archive; -using JaxI18n.NativeInterop; +using LocaleSmith.Archive; +using LocaleSmith.NativeInterop; -namespace JaxI18n.Archive.Tests; +namespace LocaleSmith.Archive.Tests; internal sealed partial class TestArchiveScanner : IArchiveScanner { diff --git a/tests/JaxI18n.CliProbe/JaxI18n.CliProbe.csproj b/tests/LocaleSmith.CliProbe/LocaleSmith.CliProbe.csproj similarity index 64% rename from tests/JaxI18n.CliProbe/JaxI18n.CliProbe.csproj rename to tests/LocaleSmith.CliProbe/LocaleSmith.CliProbe.csproj index 409eb4d..d9003be 100644 --- a/tests/JaxI18n.CliProbe/JaxI18n.CliProbe.csproj +++ b/tests/LocaleSmith.CliProbe/LocaleSmith.CliProbe.csproj @@ -2,8 +2,8 @@ Exe net10.0 - JaxI18n.CliProbe - JaxI18n.CliProbe + LocaleSmith.CliProbe + LocaleSmith.CliProbe false diff --git a/tests/JaxI18n.CliProbe/Program.cs b/tests/LocaleSmith.CliProbe/Program.cs similarity index 99% rename from tests/JaxI18n.CliProbe/Program.cs rename to tests/LocaleSmith.CliProbe/Program.cs index 9ab7719..8ad19d3 100644 --- a/tests/JaxI18n.CliProbe/Program.cs +++ b/tests/LocaleSmith.CliProbe/Program.cs @@ -4,7 +4,7 @@ using System.Runtime.Versioning; using Microsoft.Win32.SafeHandles; -namespace JaxI18n.CliProbe; +namespace LocaleSmith.CliProbe; internal static class Program { diff --git a/tests/JaxI18n.Core.Tests/CliCommandTests.cs b/tests/LocaleSmith.Core.Tests/CliCommandTests.cs similarity index 94% rename from tests/JaxI18n.Core.Tests/CliCommandTests.cs rename to tests/LocaleSmith.Core.Tests/CliCommandTests.cs index d88524a..40f82ba 100644 --- a/tests/JaxI18n.Core.Tests/CliCommandTests.cs +++ b/tests/LocaleSmith.Core.Tests/CliCommandTests.cs @@ -1,6 +1,6 @@ -using JaxI18n.Core.Models; +using LocaleSmith.Core.Models; -namespace JaxI18n.Core.Tests; +namespace LocaleSmith.Core.Tests; public sealed class CliCommandTests { diff --git a/tests/JaxI18n.Core.Tests/IncrementalTranslationPlannerTests.cs b/tests/LocaleSmith.Core.Tests/IncrementalTranslationPlannerTests.cs similarity index 96% rename from tests/JaxI18n.Core.Tests/IncrementalTranslationPlannerTests.cs rename to tests/LocaleSmith.Core.Tests/IncrementalTranslationPlannerTests.cs index db89abc..3f1065b 100644 --- a/tests/JaxI18n.Core.Tests/IncrementalTranslationPlannerTests.cs +++ b/tests/LocaleSmith.Core.Tests/IncrementalTranslationPlannerTests.cs @@ -1,7 +1,7 @@ -using JaxI18n.Core.Models; -using JaxI18n.Core.Services; +using LocaleSmith.Core.Models; +using LocaleSmith.Core.Services; -namespace JaxI18n.Core.Tests; +namespace LocaleSmith.Core.Tests; public sealed class IncrementalTranslationPlannerTests { diff --git a/tests/JaxI18n.Core.Tests/JaxI18n.Core.Tests.csproj b/tests/LocaleSmith.Core.Tests/LocaleSmith.Core.Tests.csproj similarity index 89% rename from tests/JaxI18n.Core.Tests/JaxI18n.Core.Tests.csproj rename to tests/LocaleSmith.Core.Tests/LocaleSmith.Core.Tests.csproj index adece05..e34072e 100644 --- a/tests/JaxI18n.Core.Tests/JaxI18n.Core.Tests.csproj +++ b/tests/LocaleSmith.Core.Tests/LocaleSmith.Core.Tests.csproj @@ -20,6 +20,6 @@ - + diff --git a/tests/JaxI18n.Core.Tests/ModelProviderPresetTests.cs b/tests/LocaleSmith.Core.Tests/ModelProviderPresetTests.cs similarity index 98% rename from tests/JaxI18n.Core.Tests/ModelProviderPresetTests.cs rename to tests/LocaleSmith.Core.Tests/ModelProviderPresetTests.cs index 9bfcb2c..599e51b 100644 --- a/tests/JaxI18n.Core.Tests/ModelProviderPresetTests.cs +++ b/tests/LocaleSmith.Core.Tests/ModelProviderPresetTests.cs @@ -1,6 +1,6 @@ -using JaxI18n.Core.Models; +using LocaleSmith.Core.Models; -namespace JaxI18n.Core.Tests; +namespace LocaleSmith.Core.Tests; public sealed class ModelProviderPresetTests { diff --git a/tests/JaxI18n.Core.Tests/ModelServiceRegistryTests.cs b/tests/LocaleSmith.Core.Tests/ModelServiceRegistryTests.cs similarity index 95% rename from tests/JaxI18n.Core.Tests/ModelServiceRegistryTests.cs rename to tests/LocaleSmith.Core.Tests/ModelServiceRegistryTests.cs index 843e5c3..c92df0a 100644 --- a/tests/JaxI18n.Core.Tests/ModelServiceRegistryTests.cs +++ b/tests/LocaleSmith.Core.Tests/ModelServiceRegistryTests.cs @@ -1,8 +1,8 @@ -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; -using JaxI18n.Core.Services; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; +using LocaleSmith.Core.Services; -namespace JaxI18n.Core.Tests; +namespace LocaleSmith.Core.Tests; public sealed class ModelServiceRegistryTests { diff --git a/tests/JaxI18n.Core.Tests/ModelToolContractTests.cs b/tests/LocaleSmith.Core.Tests/ModelToolContractTests.cs similarity index 97% rename from tests/JaxI18n.Core.Tests/ModelToolContractTests.cs rename to tests/LocaleSmith.Core.Tests/ModelToolContractTests.cs index 73b041a..687e57e 100644 --- a/tests/JaxI18n.Core.Tests/ModelToolContractTests.cs +++ b/tests/LocaleSmith.Core.Tests/ModelToolContractTests.cs @@ -1,7 +1,7 @@ using System.Text.Json; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Models; -namespace JaxI18n.Core.Tests; +namespace LocaleSmith.Core.Tests; public sealed class ModelToolContractTests { diff --git a/tests/JaxI18n.Infrastructure.Tests/EncryptedConfigurationStoreTests.cs b/tests/LocaleSmith.Infrastructure.Tests/EncryptedConfigurationStoreTests.cs similarity index 58% rename from tests/JaxI18n.Infrastructure.Tests/EncryptedConfigurationStoreTests.cs rename to tests/LocaleSmith.Infrastructure.Tests/EncryptedConfigurationStoreTests.cs index 789d2b9..cc8e1c5 100644 --- a/tests/JaxI18n.Infrastructure.Tests/EncryptedConfigurationStoreTests.cs +++ b/tests/LocaleSmith.Infrastructure.Tests/EncryptedConfigurationStoreTests.cs @@ -1,8 +1,8 @@ using System.Security.Cryptography; using System.Text.Json.Nodes; -using JaxI18n.Infrastructure.Security; +using LocaleSmith.Infrastructure.Security; -namespace JaxI18n.Infrastructure.Tests; +namespace LocaleSmith.Infrastructure.Tests; public sealed class EncryptedConfigurationStoreTests { @@ -44,6 +44,30 @@ public async Task EverySaveUsesANewNonce() Assert.NotEqual(first, second); } + [Fact] + public async Task SaveIfAbsentNeverOverwritesAnExistingConfiguration() + { + var cancellationToken = TestContext.Current.CancellationToken; + using var directory = new TemporaryDirectory(); + using var secrets = new InMemorySecretStore(); + var keys = new CredentialManagerMasterKeyStore(secrets); + var path = Path.Combine(directory.Path, "settings.enc"); + using var store = new EncryptedJsonConfigurationStore(path, "settings", keys); + var firstConfiguration = new TestConfiguration("first", "current"); + + var created = await store.SaveIfAbsentAsync(firstConfiguration, cancellationToken); + var originalBytes = await File.ReadAllBytesAsync(path, cancellationToken); + var replaced = await store.SaveIfAbsentAsync( + new TestConfiguration("legacy", "must-not-win"), + cancellationToken); + var retainedBytes = await File.ReadAllBytesAsync(path, cancellationToken); + + Assert.True(created); + Assert.False(replaced); + Assert.Equal(originalBytes, retainedBytes); + Assert.Equal(firstConfiguration, await store.LoadAsync(cancellationToken)); + } + [Fact] public async Task AuthenticationTagRejectsCiphertextTampering() { @@ -84,5 +108,43 @@ public async Task MasterKeyPersistsInSecretStoreAndReturnedCopiesAreIndependent( } } + [Fact] + public async Task LegacyAssociatedDataCanBeReadWhenExplicitlySelected() + { + var cancellationToken = TestContext.Current.CancellationToken; + using var directory = new TemporaryDirectory(); + using var secrets = new InMemorySecretStore(); + var keys = new CredentialManagerMasterKeyStore(secrets); + var path = Path.Combine(directory.Path, "legacy-settings.enc"); + var configuration = new TestConfiguration("legacy-endpoint", "legacy-preference"); + + using (var legacyStore = new EncryptedJsonConfigurationStore( + path, + "settings", + keys, + associatedDataNamespace: "JaxI18n.Config")) + { + await legacyStore.SaveAsync(configuration, cancellationToken); + } + + using (var defaultStore = new EncryptedJsonConfigurationStore(path, "settings", keys)) + { + await Assert.ThrowsAnyAsync( + () => defaultStore.LoadAsync(cancellationToken)); + } + + using var compatibilityStore = new EncryptedJsonConfigurationStore( + path, + "settings", + keys, + associatedDataNamespace: "JaxI18n.Config"); + var loaded = await compatibilityStore.LoadAsync(cancellationToken); + + Assert.Equal(configuration, loaded); + Assert.Equal( + "LocaleSmith.Config", + EncryptedJsonConfigurationStore.DefaultAssociatedDataNamespace); + } + private sealed record TestConfiguration(string OllamaEndpoint, string Preference); } diff --git a/tests/JaxI18n.Infrastructure.Tests/JaxI18n.Infrastructure.Tests.csproj b/tests/LocaleSmith.Infrastructure.Tests/LocaleSmith.Infrastructure.Tests.csproj similarity index 72% rename from tests/JaxI18n.Infrastructure.Tests/JaxI18n.Infrastructure.Tests.csproj rename to tests/LocaleSmith.Infrastructure.Tests/LocaleSmith.Infrastructure.Tests.csproj index 3b3c6c7..56cf035 100644 --- a/tests/JaxI18n.Infrastructure.Tests/JaxI18n.Infrastructure.Tests.csproj +++ b/tests/LocaleSmith.Infrastructure.Tests/LocaleSmith.Infrastructure.Tests.csproj @@ -20,8 +20,8 @@ - - - + + + diff --git a/tests/LocaleSmith.Infrastructure.Tests/MigratingSecretStoreTests.cs b/tests/LocaleSmith.Infrastructure.Tests/MigratingSecretStoreTests.cs new file mode 100644 index 0000000..d410eb9 --- /dev/null +++ b/tests/LocaleSmith.Infrastructure.Tests/MigratingSecretStoreTests.cs @@ -0,0 +1,216 @@ +using System.Runtime.InteropServices; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; +using LocaleSmith.Infrastructure.Security; + +namespace LocaleSmith.Infrastructure.Tests; + +public sealed class MigratingSecretStoreTests +{ + [Fact] + public void WindowsCredentialStoreUsesLocaleSmithPrefixByDefault() + { + Assert.Equal("LocaleSmith", WindowsCredentialSecretStore.DefaultTargetPrefix); + } + + [Fact] + public async Task ResolvePrefersCurrentValue() + { + var cancellationToken = TestContext.Current.CancellationToken; + using var current = new InMemorySecretStore(); + using var legacy = new InMemorySecretStore(); + await current.SetAsync("provider/key", "current".AsMemory(), cancellationToken); + await legacy.SetAsync("provider/key", "legacy".AsMemory(), cancellationToken); + var store = new MigratingSecretStore(current, legacy); + + using var resolved = await store.ResolveAsync("provider/key", cancellationToken); + + Assert.Equal("current", resolved?.DangerousGetString()); + } + + [Fact] + public async Task ResolveCopiesLegacyValueIntoCurrentStoreWithoutDeletingLegacy() + { + var cancellationToken = TestContext.Current.CancellationToken; + using var current = new InMemorySecretStore(); + using var legacy = new InMemorySecretStore(); + await legacy.SetAsync("provider/key", "legacy-secret".AsMemory(), cancellationToken); + var store = new MigratingSecretStore(current, legacy); + + using var resolved = await store.ResolveAsync("provider/key", cancellationToken); + using var copied = await current.ResolveAsync("provider/key", cancellationToken); + using var retained = await legacy.ResolveAsync("provider/key", cancellationToken); + + Assert.Equal("legacy-secret", resolved?.DangerousGetString()); + Assert.Equal("legacy-secret", copied?.DangerousGetString()); + Assert.Equal("legacy-secret", retained?.DangerousGetString()); + } + + [Fact] + public async Task ResolveDoesNotOverwriteAConcurrentCurrentValue() + { + var cancellationToken = TestContext.Current.CancellationToken; + using var current = new InMemorySecretStore(); + using var legacyInner = new InMemorySecretStore(); + await legacyInner.SetAsync("provider/key", "legacy".AsMemory(), cancellationToken); + var legacy = new BlockingResolveSecretStore(legacyInner); + var store = new MigratingSecretStore(current, legacy); + + var resolveTask = store.ResolveAsync("provider/key", cancellationToken).AsTask(); + await legacy.ResolveEntered.Task.WaitAsync(cancellationToken); + await current.SetAsync("provider/key", "concurrent-current".AsMemory(), cancellationToken); + legacy.AllowResolve.TrySetResult(); + using var resolved = await resolveTask; + using var retained = await current.ResolveAsync("provider/key", cancellationToken); + + Assert.Equal("concurrent-current", resolved?.DangerousGetString()); + Assert.Equal("concurrent-current", retained?.DangerousGetString()); + } + + [Fact] + public async Task ResolveClearsItsMigrationBuffer() + { + var cancellationToken = TestContext.Current.CancellationToken; + var current = new CapturingSecretStore(); + using var legacy = new InMemorySecretStore(); + await legacy.SetAsync("provider/key", "legacy-secret".AsMemory(), cancellationToken); + var store = new MigratingSecretStore(current, legacy); + + using var resolved = await store.ResolveAsync("provider/key", cancellationToken); + + Assert.Equal("legacy-secret", resolved?.DangerousGetString()); + Assert.NotNull(current.CapturedSetBuffer); + Assert.All(current.CapturedSetBuffer, character => Assert.Equal('\0', character)); + } + + [Fact] + public async Task SetWritesOnlyToCurrentStore() + { + var cancellationToken = TestContext.Current.CancellationToken; + using var current = new InMemorySecretStore(); + using var legacy = new InMemorySecretStore(); + var store = new MigratingSecretStore(current, legacy); + + await store.SetAsync("provider/key", "new-secret".AsMemory(), cancellationToken); + using var currentValue = await current.ResolveAsync("provider/key", cancellationToken); + using var legacyValue = await legacy.ResolveAsync("provider/key", cancellationToken); + + Assert.Equal("new-secret", currentValue?.DangerousGetString()); + Assert.Null(legacyValue); + } + + [Fact] + public async Task DeleteRemovesCurrentAndLegacyValues() + { + var cancellationToken = TestContext.Current.CancellationToken; + using var current = new InMemorySecretStore(); + using var legacy = new InMemorySecretStore(); + await current.SetAsync("provider/key", "current".AsMemory(), cancellationToken); + await legacy.SetAsync("provider/key", "legacy".AsMemory(), cancellationToken); + var store = new MigratingSecretStore(current, legacy); + + var deleted = await store.DeleteAsync("provider/key", cancellationToken); + using var currentValue = await current.ResolveAsync("provider/key", cancellationToken); + using var legacyValue = await legacy.ResolveAsync("provider/key", cancellationToken); + + Assert.True(deleted); + Assert.Null(currentValue); + Assert.Null(legacyValue); + } + + [Fact] + public async Task FailedLegacyDeleteCannotResurrectTheDeletedCredential() + { + var cancellationToken = TestContext.Current.CancellationToken; + using var current = new InMemorySecretStore(); + using var legacyInner = new InMemorySecretStore(); + await legacyInner.SetAsync("provider/key", "legacy".AsMemory(), cancellationToken); + var legacy = new DeleteFailingSecretStore(legacyInner); + var store = new MigratingSecretStore(current, legacy); + + await Assert.ThrowsAsync( + () => store.DeleteAsync("provider/key", cancellationToken).AsTask()); + using var resolved = await store.ResolveAsync("provider/key", cancellationToken); + using var retainedLegacy = await legacyInner.ResolveAsync("provider/key", cancellationToken); + + Assert.Null(resolved); + Assert.Equal("legacy", retainedLegacy?.DangerousGetString()); + + await store.SetAsync("provider/key", "replacement".AsMemory(), cancellationToken); + using var replacement = await store.ResolveAsync("provider/key", cancellationToken); + Assert.Equal("replacement", replacement?.DangerousGetString()); + } + + private sealed class CapturingSecretStore : ISecretStore + { + public char[]? CapturedSetBuffer { get; private set; } + + public ValueTask ResolveAsync( + string reference, + CancellationToken cancellationToken = default) => + ValueTask.FromResult(null); + + public ValueTask SetAsync( + string reference, + ReadOnlyMemory secret, + CancellationToken cancellationToken = default) + { + Assert.True(MemoryMarshal.TryGetArray(secret, out var segment)); + CapturedSetBuffer = segment.Array; + return ValueTask.CompletedTask; + } + + public ValueTask DeleteAsync( + string reference, + CancellationToken cancellationToken = default) => + ValueTask.FromResult(false); + } + + private sealed class BlockingResolveSecretStore(InMemorySecretStore inner) : ISecretStore + { + public TaskCompletionSource ResolveEntered { get; } = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + public TaskCompletionSource AllowResolve { get; } = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + public async ValueTask ResolveAsync( + string reference, + CancellationToken cancellationToken = default) + { + ResolveEntered.TrySetResult(); + await AllowResolve.Task.WaitAsync(cancellationToken); + return await inner.ResolveAsync(reference, cancellationToken); + } + + public ValueTask SetAsync( + string reference, + ReadOnlyMemory secret, + CancellationToken cancellationToken = default) => + inner.SetAsync(reference, secret, cancellationToken); + + public ValueTask DeleteAsync( + string reference, + CancellationToken cancellationToken = default) => + inner.DeleteAsync(reference, cancellationToken); + } + + private sealed class DeleteFailingSecretStore(InMemorySecretStore inner) : ISecretStore + { + public ValueTask ResolveAsync( + string reference, + CancellationToken cancellationToken = default) => + inner.ResolveAsync(reference, cancellationToken); + + public ValueTask SetAsync( + string reference, + ReadOnlyMemory secret, + CancellationToken cancellationToken = default) => + inner.SetAsync(reference, secret, cancellationToken); + + public ValueTask DeleteAsync( + string reference, + CancellationToken cancellationToken = default) => + ValueTask.FromException(new IOException("Simulated legacy deletion failure.")); + } +} diff --git a/tests/JaxI18n.Infrastructure.Tests/ModelAdapterTests.cs b/tests/LocaleSmith.Infrastructure.Tests/ModelAdapterTests.cs similarity index 99% rename from tests/JaxI18n.Infrastructure.Tests/ModelAdapterTests.cs rename to tests/LocaleSmith.Infrastructure.Tests/ModelAdapterTests.cs index ce8f976..22c5f11 100644 --- a/tests/JaxI18n.Infrastructure.Tests/ModelAdapterTests.cs +++ b/tests/LocaleSmith.Infrastructure.Tests/ModelAdapterTests.cs @@ -1,11 +1,11 @@ using System.Net; using System.Text; using System.Text.Json; -using JaxI18n.Core.Models; -using JaxI18n.Infrastructure.Models; -using JaxI18n.Infrastructure.Security; +using LocaleSmith.Core.Models; +using LocaleSmith.Infrastructure.Models; +using LocaleSmith.Infrastructure.Security; -namespace JaxI18n.Infrastructure.Tests; +namespace LocaleSmith.Infrastructure.Tests; public sealed class ModelAdapterTests { diff --git a/tests/JaxI18n.Infrastructure.Tests/SafeCliTests.cs b/tests/LocaleSmith.Infrastructure.Tests/SafeCliTests.cs similarity index 98% rename from tests/JaxI18n.Infrastructure.Tests/SafeCliTests.cs rename to tests/LocaleSmith.Infrastructure.Tests/SafeCliTests.cs index eaf6683..6a87bb5 100644 --- a/tests/JaxI18n.Infrastructure.Tests/SafeCliTests.cs +++ b/tests/LocaleSmith.Infrastructure.Tests/SafeCliTests.cs @@ -1,11 +1,11 @@ using System.ComponentModel; using System.Diagnostics; using System.Text.RegularExpressions; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; -using JaxI18n.Infrastructure.Cli; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; +using LocaleSmith.Infrastructure.Cli; -namespace JaxI18n.Infrastructure.Tests; +namespace LocaleSmith.Infrastructure.Tests; public sealed class SafeCliTests { @@ -309,7 +309,7 @@ public async Task RealRestrictedChildRunsLowIntegrityInsideJobOnPrivateDesktop() Assert.True(result.Status == CliExecutionStatus.Completed, result.Reason); Assert.Contains("integrity=S-1-16-4096", result.StandardOutput, StringComparison.Ordinal); Assert.Contains("inJob=True", result.StandardOutput, StringComparison.Ordinal); - Assert.Matches(@"desktop=JaxI18nCli_[a-f0-9]{32}", result.StandardOutput); + Assert.Matches(@"desktop=LocaleSmithCli_[a-f0-9]{32}", result.StandardOutput); Assert.Collection( audit.Records, started => Assert.Equal(CliExecutionStatus.Started, started.Status), @@ -472,7 +472,7 @@ public async Task UnavailablePreStartAuditFailsClosedBeforeLauncherRuns() private static string GetProbeExecutable() { - var path = Path.Combine(AppContext.BaseDirectory, "JaxI18n.CliProbe.exe"); + var path = Path.Combine(AppContext.BaseDirectory, "LocaleSmith.CliProbe.exe"); return File.Exists(path) ? path : throw new FileNotFoundException("The CLI integration-test probe was not copied to test output.", path); diff --git a/tests/JaxI18n.Infrastructure.Tests/TemporaryDirectory.cs b/tests/LocaleSmith.Infrastructure.Tests/TemporaryDirectory.cs similarity index 85% rename from tests/JaxI18n.Infrastructure.Tests/TemporaryDirectory.cs rename to tests/LocaleSmith.Infrastructure.Tests/TemporaryDirectory.cs index 30c4939..52b33c5 100644 --- a/tests/JaxI18n.Infrastructure.Tests/TemporaryDirectory.cs +++ b/tests/LocaleSmith.Infrastructure.Tests/TemporaryDirectory.cs @@ -1,4 +1,4 @@ -namespace JaxI18n.Infrastructure.Tests; +namespace LocaleSmith.Infrastructure.Tests; internal sealed class TemporaryDirectory : IDisposable { @@ -7,7 +7,7 @@ internal sealed class TemporaryDirectory : IDisposable public TemporaryDirectory() { _root = System.IO.Path.GetFullPath(System.IO.Path.GetTempPath()); - Path = System.IO.Path.Combine(_root, $"jax-i18n-tests-{Guid.NewGuid():N}"); + Path = System.IO.Path.Combine(_root, $"localesmith-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(Path); } diff --git a/tests/JaxI18n.Infrastructure.Tests/TerminalEnvironmentTests.cs b/tests/LocaleSmith.Infrastructure.Tests/TerminalEnvironmentTests.cs similarity index 94% rename from tests/JaxI18n.Infrastructure.Tests/TerminalEnvironmentTests.cs rename to tests/LocaleSmith.Infrastructure.Tests/TerminalEnvironmentTests.cs index 629257c..c6d5361 100644 --- a/tests/JaxI18n.Infrastructure.Tests/TerminalEnvironmentTests.cs +++ b/tests/LocaleSmith.Infrastructure.Tests/TerminalEnvironmentTests.cs @@ -1,7 +1,7 @@ -using JaxI18n.Core.Models; -using JaxI18n.Infrastructure.Environment; +using LocaleSmith.Core.Models; +using LocaleSmith.Infrastructure.Environment; -namespace JaxI18n.Infrastructure.Tests; +namespace LocaleSmith.Infrastructure.Tests; public sealed class TerminalEnvironmentTests { diff --git a/tests/JaxI18n.Mcp.Tests/JaxI18n.Mcp.Tests.csproj b/tests/LocaleSmith.Mcp.Tests/LocaleSmith.Mcp.Tests.csproj similarity index 81% rename from tests/JaxI18n.Mcp.Tests/JaxI18n.Mcp.Tests.csproj rename to tests/LocaleSmith.Mcp.Tests/LocaleSmith.Mcp.Tests.csproj index 7b5c94b..73db1af 100644 --- a/tests/JaxI18n.Mcp.Tests/JaxI18n.Mcp.Tests.csproj +++ b/tests/LocaleSmith.Mcp.Tests/LocaleSmith.Mcp.Tests.csproj @@ -20,7 +20,7 @@ - - + + diff --git a/tests/JaxI18n.Mcp.Tests/McpModelToolExecutorTests.cs b/tests/LocaleSmith.Mcp.Tests/McpModelToolExecutorTests.cs similarity index 97% rename from tests/JaxI18n.Mcp.Tests/McpModelToolExecutorTests.cs rename to tests/LocaleSmith.Mcp.Tests/McpModelToolExecutorTests.cs index 57a0c27..7dbe018 100644 --- a/tests/JaxI18n.Mcp.Tests/McpModelToolExecutorTests.cs +++ b/tests/LocaleSmith.Mcp.Tests/McpModelToolExecutorTests.cs @@ -1,8 +1,8 @@ using System.Text.Json; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Mcp.Tests; +namespace LocaleSmith.Mcp.Tests; public sealed class McpModelToolExecutorTests { diff --git a/tests/JaxI18n.Mcp.Tests/McpStdioServerTests.cs b/tests/LocaleSmith.Mcp.Tests/McpStdioServerTests.cs similarity index 99% rename from tests/JaxI18n.Mcp.Tests/McpStdioServerTests.cs rename to tests/LocaleSmith.Mcp.Tests/McpStdioServerTests.cs index 39848f6..a652492 100644 --- a/tests/JaxI18n.Mcp.Tests/McpStdioServerTests.cs +++ b/tests/LocaleSmith.Mcp.Tests/McpStdioServerTests.cs @@ -1,9 +1,9 @@ using System.Text; using System.Text.Json; -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; -namespace JaxI18n.Mcp.Tests; +namespace LocaleSmith.Mcp.Tests; public sealed class McpStdioServerTests { diff --git a/tests/JaxI18n.Archive.Tests/JaxI18n.Archive.Tests.csproj b/tests/LocaleSmith.NativeInterop.Tests/LocaleSmith.NativeInterop.Tests.csproj similarity index 72% rename from tests/JaxI18n.Archive.Tests/JaxI18n.Archive.Tests.csproj rename to tests/LocaleSmith.NativeInterop.Tests/LocaleSmith.NativeInterop.Tests.csproj index 9532048..5febfea 100644 --- a/tests/JaxI18n.Archive.Tests/JaxI18n.Archive.Tests.csproj +++ b/tests/LocaleSmith.NativeInterop.Tests/LocaleSmith.NativeInterop.Tests.csproj @@ -21,13 +21,13 @@ - + - + Condition="Exists('..\..\native\localesmith_core\target\release\localesmith_core.dll')" /> diff --git a/tests/JaxI18n.NativeInterop.Tests/NativeCoreClientTests.cs b/tests/LocaleSmith.NativeInterop.Tests/NativeCoreClientTests.cs similarity index 96% rename from tests/JaxI18n.NativeInterop.Tests/NativeCoreClientTests.cs rename to tests/LocaleSmith.NativeInterop.Tests/NativeCoreClientTests.cs index 22df498..4505f94 100644 --- a/tests/JaxI18n.NativeInterop.Tests/NativeCoreClientTests.cs +++ b/tests/LocaleSmith.NativeInterop.Tests/NativeCoreClientTests.cs @@ -1,6 +1,6 @@ using System.IO.Compression; -namespace JaxI18n.NativeInterop.Tests; +namespace LocaleSmith.NativeInterop.Tests; public sealed class NativeCoreClientTests { @@ -68,7 +68,7 @@ private sealed class ArchiveFixture : IDisposable public ArchiveFixture(string fileName) { - _directory = Directory.CreateTempSubdirectory("jax-i18n-native-tests-"); + _directory = Directory.CreateTempSubdirectory("localesmith-native-tests-"); ArchivePath = Path.Combine(_directory.FullName, fileName); _stream = new FileStream(ArchivePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None); _archive = new ZipArchive(_stream, ZipArchiveMode.Create, leaveOpen: true); diff --git a/tests/JaxI18n.Presentation.Tests/AssistantViewModelTests.cs b/tests/LocaleSmith.Presentation.Tests/AssistantViewModelTests.cs similarity index 98% rename from tests/JaxI18n.Presentation.Tests/AssistantViewModelTests.cs rename to tests/LocaleSmith.Presentation.Tests/AssistantViewModelTests.cs index 263a3bd..3c5e6a1 100644 --- a/tests/JaxI18n.Presentation.Tests/AssistantViewModelTests.cs +++ b/tests/LocaleSmith.Presentation.Tests/AssistantViewModelTests.cs @@ -1,9 +1,9 @@ -using JaxI18n.Core.Models; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; -using JaxI18n.Presentation.ViewModels; +using LocaleSmith.Core.Models; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; +using LocaleSmith.Presentation.ViewModels; -namespace JaxI18n.Presentation.Tests; +namespace LocaleSmith.Presentation.Tests; public sealed class AssistantViewModelTests { diff --git a/tests/JaxI18n.Presentation.Tests/CliConfirmationViewModelTests.cs b/tests/LocaleSmith.Presentation.Tests/CliConfirmationViewModelTests.cs similarity index 95% rename from tests/JaxI18n.Presentation.Tests/CliConfirmationViewModelTests.cs rename to tests/LocaleSmith.Presentation.Tests/CliConfirmationViewModelTests.cs index 51531ad..d4e78da 100644 --- a/tests/JaxI18n.Presentation.Tests/CliConfirmationViewModelTests.cs +++ b/tests/LocaleSmith.Presentation.Tests/CliConfirmationViewModelTests.cs @@ -1,9 +1,9 @@ -using JaxI18n.Core.Abstractions; -using JaxI18n.Core.Models; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.ViewModels; +using LocaleSmith.Core.Abstractions; +using LocaleSmith.Core.Models; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.ViewModels; -namespace JaxI18n.Presentation.Tests; +namespace LocaleSmith.Presentation.Tests; public sealed class CliConfirmationViewModelTests { @@ -108,7 +108,7 @@ public async Task RunnerFailureUsesGenericMessageWithoutLeakingExceptionText() "7.5", Path.GetTempPath(), new Dictionary()), - Path.Combine(Path.GetTempPath(), "jax-i18n-cli-audit.jsonl")); + Path.Combine(Path.GetTempPath(), "localesmith-cli-audit.jsonl")); viewModel.RiskAcknowledged = true; await viewModel.ExecuteAsync(); @@ -138,7 +138,7 @@ private static CliConfirmationViewModel Create( "7.5", Path.GetTempPath(), new Dictionary()), - Path.Combine(Path.GetTempPath(), "jax-i18n-cli-audit.jsonl"), + Path.Combine(Path.GetTempPath(), "localesmith-cli-audit.jsonl"), text); } diff --git a/tests/JaxI18n.Presentation.Tests/DashboardViewModelTests.cs b/tests/LocaleSmith.Presentation.Tests/DashboardViewModelTests.cs similarity index 98% rename from tests/JaxI18n.Presentation.Tests/DashboardViewModelTests.cs rename to tests/LocaleSmith.Presentation.Tests/DashboardViewModelTests.cs index 50d66f8..56c851a 100644 --- a/tests/JaxI18n.Presentation.Tests/DashboardViewModelTests.cs +++ b/tests/LocaleSmith.Presentation.Tests/DashboardViewModelTests.cs @@ -1,10 +1,10 @@ -using JaxI18n.Application.Models; -using JaxI18n.Core.Models; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; -using JaxI18n.Presentation.ViewModels; +using LocaleSmith.Application.Models; +using LocaleSmith.Core.Models; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; +using LocaleSmith.Presentation.ViewModels; -namespace JaxI18n.Presentation.Tests; +namespace LocaleSmith.Presentation.Tests; public sealed class DashboardViewModelTests { diff --git a/tests/JaxI18n.Presentation.Tests/JaxI18n.Presentation.Tests.csproj b/tests/LocaleSmith.Presentation.Tests/LocaleSmith.Presentation.Tests.csproj similarity index 87% rename from tests/JaxI18n.Presentation.Tests/JaxI18n.Presentation.Tests.csproj rename to tests/LocaleSmith.Presentation.Tests/LocaleSmith.Presentation.Tests.csproj index b849b2a..1b95578 100644 --- a/tests/JaxI18n.Presentation.Tests/JaxI18n.Presentation.Tests.csproj +++ b/tests/LocaleSmith.Presentation.Tests/LocaleSmith.Presentation.Tests.csproj @@ -20,6 +20,6 @@ - + diff --git a/tests/JaxI18n.Presentation.Tests/ModelSourcesViewModelTests.cs b/tests/LocaleSmith.Presentation.Tests/ModelSourcesViewModelTests.cs similarity index 98% rename from tests/JaxI18n.Presentation.Tests/ModelSourcesViewModelTests.cs rename to tests/LocaleSmith.Presentation.Tests/ModelSourcesViewModelTests.cs index 59bc757..14b1f41 100644 --- a/tests/JaxI18n.Presentation.Tests/ModelSourcesViewModelTests.cs +++ b/tests/LocaleSmith.Presentation.Tests/ModelSourcesViewModelTests.cs @@ -1,10 +1,10 @@ using System.Text.Json; -using JaxI18n.Core.Models; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; -using JaxI18n.Presentation.ViewModels; +using LocaleSmith.Core.Models; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; +using LocaleSmith.Presentation.ViewModels; -namespace JaxI18n.Presentation.Tests; +namespace LocaleSmith.Presentation.Tests; public sealed class ModelSourcesViewModelTests { diff --git a/tests/JaxI18n.Presentation.Tests/OnboardingAndShellTests.cs b/tests/LocaleSmith.Presentation.Tests/OnboardingAndShellTests.cs similarity index 97% rename from tests/JaxI18n.Presentation.Tests/OnboardingAndShellTests.cs rename to tests/LocaleSmith.Presentation.Tests/OnboardingAndShellTests.cs index 8d90ffa..9d7155e 100644 --- a/tests/JaxI18n.Presentation.Tests/OnboardingAndShellTests.cs +++ b/tests/LocaleSmith.Presentation.Tests/OnboardingAndShellTests.cs @@ -1,9 +1,9 @@ -using JaxI18n.Core.Models; -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; -using JaxI18n.Presentation.ViewModels; +using LocaleSmith.Core.Models; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; +using LocaleSmith.Presentation.ViewModels; -namespace JaxI18n.Presentation.Tests; +namespace LocaleSmith.Presentation.Tests; public sealed class OnboardingAndShellTests { diff --git a/tests/JaxI18n.Presentation.Tests/OutputPathAndSettingsTests.cs b/tests/LocaleSmith.Presentation.Tests/OutputPathAndSettingsTests.cs similarity index 96% rename from tests/JaxI18n.Presentation.Tests/OutputPathAndSettingsTests.cs rename to tests/LocaleSmith.Presentation.Tests/OutputPathAndSettingsTests.cs index 2f5253a..a755f86 100644 --- a/tests/JaxI18n.Presentation.Tests/OutputPathAndSettingsTests.cs +++ b/tests/LocaleSmith.Presentation.Tests/OutputPathAndSettingsTests.cs @@ -1,9 +1,9 @@ -using JaxI18n.Presentation.Abstractions; -using JaxI18n.Presentation.Models; -using JaxI18n.Presentation.Services; -using JaxI18n.Presentation.ViewModels; +using LocaleSmith.Presentation.Abstractions; +using LocaleSmith.Presentation.Models; +using LocaleSmith.Presentation.Services; +using LocaleSmith.Presentation.ViewModels; -namespace JaxI18n.Presentation.Tests; +namespace LocaleSmith.Presentation.Tests; public sealed class OutputPathAndSettingsTests { @@ -248,12 +248,12 @@ public async Task SettingsLoadAndSaveRoundTripsForcedAppAnimationPreference() { IsOnboardingComplete = true, WorkspacePath = workspacePath, - SandboxPath = Path.Combine(Path.GetTempPath(), "JaxI18n", "Sandbox") + SandboxPath = Path.Combine(Path.GetTempPath(), "LocaleSmith", "Sandbox") }; private static string CreateTestRoot() { - var path = Path.Combine(Path.GetTempPath(), "JaxI18n.Tests", Guid.NewGuid().ToString("N")); + var path = Path.Combine(Path.GetTempPath(), "LocaleSmith.Tests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(path); return path; } From 14eab915f24b1d6c17486c9cad68dabefb550fec Mon Sep 17 00:00:00 2001 From: DZXH-TX Date: Tue, 11 Aug 2026 01:43:19 +0800 Subject: [PATCH 2/2] fix: stabilize translation workflow and add diagnostics --- README.md | 22 +- src/LocaleSmith.App/App.xaml.cs | 77 +- .../TranslationLogViewModeDisplayConverter.cs | 41 + src/LocaleSmith.App/MainWindow.xaml | 5 + src/LocaleSmith.App/MainWindow.xaml.cs | 75 +- .../ModelOptionSelectionMap.cs | 49 + src/LocaleSmith.App/Pages/DashboardPage.xaml | 468 +++----- src/LocaleSmith.App/Pages/LogsPage.xaml | 144 +++ src/LocaleSmith.App/Pages/LogsPage.xaml.cs | 34 + .../Pages/ModelSourcesPage.xaml | 20 +- .../Pages/ModelSourcesPage.xaml.cs | 50 +- src/LocaleSmith.App/Pages/OnboardingPage.xaml | 90 +- .../Pages/OnboardingPage.xaml.cs | 58 + src/LocaleSmith.App/Pages/SettingsPage.xaml | 40 +- .../Pages/SettingsPage.xaml.cs | 45 +- .../CliConfirmationViewModelFactory.cs | 16 +- .../Services/CliSandboxDirectory.cs | 72 ++ .../Services/FileTranslationMemoryStore.cs | 352 +++++- .../LegacyAppDataMigrationCoordinator.cs | 90 ++ .../Services/LegacyAppDataMigrator.cs | 165 +-- .../Services/LegacyDefaultPathNormalizer.cs | 62 +- .../PipelineTranslationQueueService.cs | 199 +++- .../Services/SecureAppStateService.cs | 66 +- .../Services/TranslationLogService.cs | 1029 +++++++++++++++++ .../Services/TranslationLogsViewModel.cs | 373 ++++++ .../Services/WinUiTextProvider.cs | 16 +- src/LocaleSmith.App/ShellNavigationMap.cs | 74 ++ .../Strings/en-US/Resources.resw | 46 +- .../Strings/zh-CN/Resources.resw | 46 +- .../Models/PipelineModels.cs | 15 +- .../Services/PipelineQueue.cs | 2 +- src/LocaleSmith.Archive/ArchivePathSafety.cs | 12 +- src/LocaleSmith.Archive/ArchiveWorkspace.cs | 607 ++++++---- .../ArchiveWorkspaceBackend.cs | 178 ++- .../DirectoryMutationGuard.cs | 1015 ++++++++++++++++ .../FolderSnapshotBuilder.cs | 327 +++--- src/LocaleSmith.Archive/TransactionJournal.cs | 32 +- .../Models/ModelProviderPreset.cs | 10 +- src/LocaleSmith.Core/Models/ModelSource.cs | 2 +- .../Cli/SafeCliCommandPolicy.cs | 117 +- .../Cli/TrustedCliExecutableDiscovery.cs | 59 +- .../Models/OpenAiCompatibleModelService.cs | 3 +- .../Security/SecurityOperationLock.cs | 19 +- src/LocaleSmith.McpHost/Program.cs | 6 +- .../Abstractions/PresentationServices.cs | 11 +- .../Models/AppConfiguration.cs | 79 +- .../ViewModels/DashboardViewModel.cs | 20 + .../ViewModels/ModelSourcesViewModel.cs | 50 +- .../ViewModels/OnboardingViewModel.cs | 72 +- .../ViewModels/SettingsViewModel.cs | 375 ++++-- .../CliSandboxDirectoryTests.cs | 96 ++ ...ileTranslationMemoryStoreMigrationTests.cs | 471 +++++++- .../LegacyAppDataMigrationCoordinatorTests.cs | 114 ++ .../LegacyAppDataMigratorTests.cs | 80 +- .../ModelAssistantServiceTests.cs | 5 + .../ModelOptionSelectionMapTests.cs | 76 ++ .../PipelineTranslationQueueServiceTests.cs | 5 +- .../SecureAppStateServiceTests.cs | 189 ++- .../SettingsPersistenceIntegrationTests.cs | 359 ++++++ .../ShellNavigationMapTests.cs | 65 ++ .../TranslationLogServiceTests.cs | 580 ++++++++++ ...slationLogViewModeDisplayConverterTests.cs | 29 + .../WinUiTextProviderTests.cs | 39 + .../TranslationPipelineTests.cs | 14 +- .../ArchiveWorkspaceBackendTests.cs | 598 ++++++++++ .../SafeSubsetExternalizationPipelineTests.cs | 2 +- .../ModelProviderPresetTests.cs | 10 +- .../ModelAdapterTests.cs | 31 + .../SafeCliTests.cs | 189 ++- .../TemporaryDirectory.cs | 9 +- .../DashboardViewModelTests.cs | 5 + .../ModelSourcesViewModelTests.cs | 142 +++ .../OnboardingAndShellTests.cs | 72 +- .../OutputPathAndSettingsTests.cs | 68 ++ 74 files changed, 8708 insertions(+), 1375 deletions(-) create mode 100644 src/LocaleSmith.App/Converters/TranslationLogViewModeDisplayConverter.cs create mode 100644 src/LocaleSmith.App/ModelOptionSelectionMap.cs create mode 100644 src/LocaleSmith.App/Pages/LogsPage.xaml create mode 100644 src/LocaleSmith.App/Pages/LogsPage.xaml.cs create mode 100644 src/LocaleSmith.App/Services/CliSandboxDirectory.cs create mode 100644 src/LocaleSmith.App/Services/LegacyAppDataMigrationCoordinator.cs create mode 100644 src/LocaleSmith.App/Services/TranslationLogService.cs create mode 100644 src/LocaleSmith.App/Services/TranslationLogsViewModel.cs create mode 100644 src/LocaleSmith.App/ShellNavigationMap.cs create mode 100644 src/LocaleSmith.Archive/DirectoryMutationGuard.cs create mode 100644 tests/LocaleSmith.App.Tests/CliSandboxDirectoryTests.cs create mode 100644 tests/LocaleSmith.App.Tests/LegacyAppDataMigrationCoordinatorTests.cs create mode 100644 tests/LocaleSmith.App.Tests/ModelOptionSelectionMapTests.cs create mode 100644 tests/LocaleSmith.App.Tests/SettingsPersistenceIntegrationTests.cs create mode 100644 tests/LocaleSmith.App.Tests/ShellNavigationMapTests.cs create mode 100644 tests/LocaleSmith.App.Tests/TranslationLogServiceTests.cs create mode 100644 tests/LocaleSmith.App.Tests/TranslationLogViewModeDisplayConverterTests.cs create mode 100644 tests/LocaleSmith.App.Tests/WinUiTextProviderTests.cs diff --git a/README.md b/README.md index b599c4f..f9e4ff5 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ 项目概览 · 核心能力 · 快速开始 · + 翻译日志 · 安全边界 · 开源许可

@@ -45,7 +46,9 @@ | **安全归档扫描** | 识别路径穿越、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 形式还会在写盘前再次脱敏。 + ## 快速开始 ### 环境要求 @@ -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.2`。MSIX 已完成 payload、PRI、MCP Host、SignPath Authenticode 签名、本机安装与启动验证;微软商店上架前仍需使用 Partner Center 分配的正式包身份重新生成清单并完成商店认证。 +当前 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/src/LocaleSmith.App/App.xaml.cs b/src/LocaleSmith.App/App.xaml.cs index f21c9cd..a56ac91 100644 --- a/src/LocaleSmith.App/App.xaml.cs +++ b/src/LocaleSmith.App/App.xaml.cs @@ -17,7 +17,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; -using Windows.Globalization; +using Microsoft.Windows.Globalization; namespace LocaleSmith.App; @@ -42,26 +42,35 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args) { var currentLocalAppData = System.Environment.GetFolderPath( System.Environment.SpecialFolder.LocalApplicationData); - var unredirectedLocalAppData = LegacyAppDataLocator - .GetUnredirectedLocalApplicationDataPath(); - var currentAppDataRoot = Path.Combine(currentLocalAppData, "LocaleSmith"); + 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"); - foreach (var legacyAppDataRoot in LegacyAppDataLocator.FindLegacyRoots(unredirectedLocalAppData)) - { - var migrator = new LegacyAppDataMigrator( - legacyAppDataRoot, + var legacyAppDataRoots = await LegacyAppDataMigrationCoordinator + .MigrateAsync( currentAppDataRoot, legacyCredentialStore, - currentCredentialStore); - await migrator.MigrateAsync().ConfigureAwait(true); - } - - _host = BuildHost(currentAppDataRoot, currentCredentialStore, 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); @@ -81,7 +90,8 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args) private static IHost BuildHost( string appDataRoot, WindowsCredentialSecretStore currentCredentialStore, - WindowsCredentialSecretStore legacyCredentialStore) + WindowsCredentialSecretStore legacyCredentialStore, + IReadOnlyList legacyTranslationMemoryPaths) { var builder = Host.CreateApplicationBuilder(); var configPath = Path.Combine( @@ -89,8 +99,7 @@ private static IHost BuildHost( 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(); @@ -119,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(); @@ -136,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 => @@ -157,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(); @@ -178,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/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/LocaleSmith.App/MainWindow.xaml b/src/LocaleSmith.App/MainWindow.xaml index fc42e61..2596042 100644 --- a/src/LocaleSmith.App/MainWindow.xaml +++ b/src/LocaleSmith.App/MainWindow.xaml @@ -35,6 +35,11 @@ + + + + + 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; } @@ -90,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); @@ -186,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/LocaleSmith.App/Pages/DashboardPage.xaml b/src/LocaleSmith.App/Pages/DashboardPage.xaml index 1341587..9662d13 100644 --- a/src/LocaleSmith.App/Pages/DashboardPage.xaml +++ b/src/LocaleSmith.App/Pages/DashboardPage.xaml @@ -4,6 +4,115 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:behaviors="using:LocaleSmith.App.Behaviors" NavigationCacheMode="Required"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -11,22 +120,26 @@ - - - - - + + + + + - + @@ -74,9 +187,14 @@ - + + + + + - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + 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/LocaleSmith.App/Pages/ModelSourcesPage.xaml b/src/LocaleSmith.App/Pages/ModelSourcesPage.xaml index 1617912..5687b33 100644 --- a/src/LocaleSmith.App/Pages/ModelSourcesPage.xaml +++ b/src/LocaleSmith.App/Pages/ModelSourcesPage.xaml @@ -123,7 +123,8 @@ + SelectedItem="{Binding SelectedTokenLimitParameterOption, Mode=OneWay}" + SelectionChanged="OnTokenLimitParameterSelectionChanged" /> - + - - + + + +