Documentation

Toucan architecture

Current project boundaries, host composition, desktop state, project persistence, plugins, and AI integration, with implementation links and Mermaid diagrams.

activeUpdated 2026-10-07Edit on GitHub ↗

Toucan is a .NET 10 translation resource editor with an Avalonia desktop host and a console host. Both compose the same format engine, provider registry, validation pipeline, and plugin system. The desktop adds project lifecycle orchestration, editor state, platform dialogs, and visual composition.

This document describes the checked-in implementation. Future editor highlighting is tracked separately in the syntax highlighting plan. The Core interop architecture covers translation normalization and format integration; plugin authoring and AI integration cover their respective contracts and configuration.

#Project boundaries

Arrows below mean project references. Runtime registration does not create a Core-to-module dependency.

flowchart TD
    Desktop["Toucan.Avalonia"] --> Core["Toucan.Core"]
    CLI["Toucan.CLI"] --> Core
    Desktop --> Defaults["Toucan.Modules.Defaults"]
    CLI --> Defaults
    Defaults --> Formats["Formats.Json / Xml / Text / Data"]
    Defaults --> Providers["Modules.Providers"]
    Defaults --> Validation["Modules.Validation"]
    Defaults --> Frameworks["Modules.Frameworks"]
    Core --> Common["Toucan.Core.Common"]
    Core --> Contracts["Toucan.Plugins.Abstractions"]
    Formats --> Common
    Providers --> Common
    Validation --> Common
    Frameworks --> Common
    Formats --> Contracts
    Providers --> Contracts
    Validation --> Contracts
    Frameworks --> Contracts
    Common --> Contracts
    Plugin["External plugin"] --> Contracts
ProjectResponsibilityBoundary
Toucan.Plugins.AbstractionsPlugin entry point, format/provider/rule/profile contracts, shared DTOsNo Core or UI dependency. Some public types retain Toucan.Core.* namespaces for compatibility.
Toucan.Core.CommonShared file/helpers code and built-in module registration supportReferences Abstractions; never Core or a module.
Toucan.CoreProject IO and lifecycle implementations, registries, validation pipeline, AI policy, plugin host, editing servicesNo Avalonia dependency and no reference to built-in modules.
Toucan.Modules.*Concrete formats, providers, validation rules, framework profilesLeaf modules reference Common and Abstractions, not Core or sibling modules.
Toucan.Modules.DefaultsRegisters the seven built-in modulesThe aggregation point that references every module.
Toucan.AvaloniaDesktop startup, views, view models, platform services, application stateComposes Core and Defaults; uses Avalonia and FluentAvalonia.
Toucan.CLIConsole commands for checking, translation, export, key access, and plugin policyComposes Core and Defaults without constructing the desktop lifecycle.

These boundaries are executable rules in ArchitectureTests. Assembly placement matters more than namespaces: built-in strategies still use Toucan.Core.Services.* namespaces while their code lives in module projects.

#Composition and startup

The composition sequence is shared, but the desktop and CLI do not register identical service sets. AddToucanCore installs shared registries, factories, IO, validation, AI services, project services, and comments. It does not register the complete desktop editor or lifecycle graph.

flowchart LR
    Host["Host logging and adapters"] --> Core["AddToucanCore"]
    Core --> Defaults["AddToucanDefaults"]
    Defaults --> Plugins["AddToucanPlugins"]
    Plugins --> Specific["Host-specific services"]
    Specific --> Container["BuildServiceProvider"]
    Container --> Runtime["Resolve host entry points"]

AddToucanDefaults registers formats, frameworks, providers, and validation rules, recording their built-in identities before external plugins load. Plugins contribute to the same service collection before it is built. Their IDs cannot replace reserved built-in registrations.

The desktop composition root is App.ConfigureServices. Most project/editor services and the main/status view models are singletons; dialogs such as Options and AI settings use transient view models. Host adapters supply dialogs, messages, preferences, recent projects, and provider settings. The lifecycle receives a lazy reference through auto-save to avoid constructing a circular dependency.

After framework initialization, the desktop applies theme/color/font preferences, registers eight side panels, creates the window, and attaches unsaved-change and external-change handlers. A dispatcher callback marshals reload/merge work to the UI thread and refreshes the editor from the store. Startup onboarding precedes opening the selected or last project; pending-plugin prompts follow opening. File activation on macOS also routes to the project-open command.

CLI Program builds the shared container for its commands. It applies plugin policy without desktop prompts, and supports a per-run plugin allowance. Changes to shared composition should be checked against CompositionRootTests, not only desktop startup.

#Dialog and message integration

IDialogService is a desktop-layer contract, not a Core or plugin contract. It returns desktop view models and exposes file/folder pickers, prompts, and feature dialogs. DialogService implements it using Avalonia StorageProvider pickers and owned modal windows. View models receive the interface through DI, including MainWindowServices.Dialogs, so commands can await a user result without constructing windows themselves.

sequenceDiagram
    actor User
    participant VM as Desktop view model
    participant Dialog as IDialogService / DialogService
    participant Owners as AppWindows
    participant UI as StorageProvider or modal window
    User->>VM: Run command
    VM->>Dialog: Await picker or feature dialog
    Dialog->>Owners: Resolve active owner
    Owners-->>Dialog: Active or visible window; main window fallback
    Dialog->>UI: Open picker or ShowDialog(owner)
    UI-->>Dialog: Accepted result or cancellation
    Dialog-->>VM: Path, view model, options, boolean, or null
    VM->>VM: Apply accepted result or stop on cancellation

App.ConfigureServices registers IDialogService as a singleton. The implementation resolves transient view models from DI for New Project, Options, Provider Settings, and Onboarding; it directly constructs others with request-specific state, or accepts an already-prepared view model such as Pre-Translate. Dialog lifetime and view-model lifetime are therefore separate from the singleton service lifetime.

AppWindows.Active chooses the active window, then a visible window, then the main window. Nested dialogs and pickers consequently use the current dialog as their owner. DialogService requires an available owner and throws if no window exists. New callers should run on the UI thread; unlike the message service, this implementation does not add dispatcher marshaling around its methods. Pickers return local paths when available, and result-bearing dialogs use null or false for cancellation. Shutdown closes the main window so its unsaved-change guard still runs.

Messages use a separate boundary: IAsyncMessageService and MessageService. They provide information, confirmation, and three-way choices using FluentAvalonia FAContentDialog overlays. DI exposes the same MessageService instance through both IAsyncMessageService and the Core IMessageService contract. The implementation marshals display to the UI thread. Its synchronous confirmation adapter runs a nested dispatcher frame; async desktop commands should prefer ConfirmAsync or ChooseAsync.

The lifecycle's unsaved-change and external-change handlers use these message interactions rather than depending on desktop IDialogService. App.Dialogs is a static bridge for view/menu code that is not created through DI; it is not the preferred dependency path for view models. TestHost substitutes fake dialog/message implementations so command behavior can be checked without opening native pickers or blocking on modal windows.

#Translation model and format integration

The canonical record is TranslationItem, identified in the editing store by (Language, Namespace). It includes the value and translation metadata; format loaders flatten resources into these records. LanguageGroupViewModel groups them into editor cards, including plural variants. The UI tree and paged cards are projections of the store, not separate persistence formats.

ComponentIntegration point
ILoadStrategyEnumerates translations from a folder.
ISaveStrategyWrites a SaveContext; declares display name, extensions, default paths, language files, comment handling, and detection rules.
TranslationStrategyFactoryResolves registered strategies by string format ID.
FormatDetectorEvaluates save strategies' detection rules for folders without settings.
ProjectModeResolverDistinguishes a folder containing toucan.tproj from a folder scan.
ManifestLoadStrategyLoads configured translation package paths when manifest mode is selected.
ProjectServiceCoordinates settings, format selection, language aliases, load/save, and output text settings.

See ProjectService and the Core format guide. An unavailable external format raises FormatUnavailableException; it is not silently interpreted as JSON. Built-in formats without loaders retain a legacy JSON fallback, so a listed save format does not imply equivalent loading support.

#Project lifecycle and persistence

The desktop delegates guarded project operations to ProjectLifecycleService. Lower-level callers such as the CLI can use ProjectService directly and therefore do not automatically receive lifecycle validation, sidecar orchestration, auto-save, or UI prompts.

sequenceDiagram
    actor User
    participant VM as MainWindowViewModel
    participant Life as ProjectLifecycleService
    participant IO as ProjectService
    participant Store as TranslationManagementService
    participant Support as Watcher / audit / comments
    User->>VM: Open project
    VM->>Life: OpenProjectAsync(folder)
    opt Current project is dirty
        Life->>Life: CloseProjectAsync with unsaved-change handler
    end
    Life->>IO: LoadProject on background task
    IO->>IO: Load settings or detect format; select loader
    IO-->>Life: Settings and translations
    Life->>Store: Initialize items and baselines
    Life->>Support: Watch folder; load audit and comments
    Life->>Life: Set project; snapshot; start configured auto-save
    Life-->>VM: ProjectChanged(Opened)
    VM->>VM: Rebuild editor projections

Open handles cancellation, invalid manifests, missing folders, and unavailable formats as explicit outcomes. Format scanning receives cancellation/progress through ScanContext.

Editing flows through TranslationItemViewModel: its commit records undo information, updates direct-edit metadata, and calls NotifyValueChanged. TranslationManagementService compares values/comments against baselines and debounces dirty-state notifications. UI actions that modify data must use the store and refresh projections consistently; writing a view model field alone is insufficient.

flowchart TD
    Save["SaveProjectAsync"] --> Validate["Run validation pipeline"]
    Validate --> Errors{"Error severity results?"}
    Errors -->|Yes| Stop["Return ValidationErrors without writing"]
    Errors -->|No| Write["ProjectService.Save through format strategy"]
    Write --> Text["Apply configured encoding and line endings"]
    Text --> Meta["Persist comments, audit, and manifest packages"]
    Meta --> Baseline["MarkAllSaved and update snapshots"]
    Baseline --> Timer["Reset auto-save timer and publish Saved"]

The save strategy owns translation serialization. Project text settings are applied afterward to language files reported by built-in strategies; Java properties retains Latin-1 encoding. The post-processing does not apply arbitrary encoding changes to plugin formats. Comment sidecars are used when StoresCommentsInline is false. Audit uses .toucan-metadata.json.

Save spans multiple files and sidecars; it is not a transaction across the project. Do not describe successful individual file writes as an atomic project commit. The lifecycle marks baselines saved only after its persistence steps complete.

External changes are coordinated by ProjectLifecycleService.ExternalChanges. A clean project reloads automatically. A dirty project offers Reload, Merge, or Ignore through the host handler. Merge uses the saved snapshot, in-memory records, and disk records; it applies non-conflicting entries before asking for conflict resolution. Cancelling that dialog therefore does not undo already-applied entries.

#Desktop shell and editor state

MainWindow owns the title/navigation bar, two activity rails, a shared rounded workspace, and the status bar. Its code-behind handles platform chrome, panel view creation/cache, splitter widths, adaptive panel fitting, and rounded content clipping. Business operations remain in the view models and Core services.

flowchart TD
    Nav["Top navigation: Editor / Review / Audit"] --> VM["MainWindowViewModel"]
    VM <--> Layout["PanelService: mode and layout"]
    Rails["Activity bars"] --> Layout
    Layout --> Registry["SidePanelRegistry"]
    Registry --> Window["MainWindow panel hosts"]
    Window --> Left["Explorer / Search / Issues / Source Code"]
    Window --> Right["Languages / Inspector / Translation / Memory"]
    VM --> Editor["TranslationEditorView"]
    Editor --> Cards["List or one-key focused card"]
    VM --> Zen["ZenEditorView overlay"]
    VM --> Status["StatusBarViewModel and StatusBarService"]

The eight panels are registered in App.RegisterSidePanels. SidePanelRegistry tracks active panels and slot visibility; PanelService provides commands and persists layout. These are process-wide singleton bridges rather than dynamically injected plugin views. Adding an ordinary panel requires both registry registration and a corresponding view mapping in MainWindow.GetPanel.

Editor/Review/Audit are application modes selected in the top navigation. Audit disables editing; Review changes the working set and review actions. One-key focused editing is a separate presentation of the current filtered data: navigation clamps to the data bounds, synchronizes selection, and returns to the page containing the current card. Zen uses its own overlay and hides surrounding layout. These states are not interchangeable.

The status bar combines registered panels with desktop behavior. Encoding and LF/CRLF menus modify project settings used on save; the notification menu combines untranslated entries with validation results. Settings use reusable SettingsGroup, SettingsRow, and SettingsList controls, with shared styles in AppStyles. UI labels use the locale resources, while translation values belong to the project data.

#Providers, AI, and source integration

PretranslationService builds translation jobs, resolves a provider from TranslationProviderRegistry, protects placeholders, calls the provider, and restores placeholders/capitalization. With PreviewOnly, it returns results without applying values; otherwise it can update supplied target records. The desktop must integrate accepted results with its dirty tracking and editor refresh.

Machine-translation providers and AI backends are different contracts. The public plugin context accepts formats, translation providers, validation rules, and framework profiles; it does not expose backend or desktop-view registration. AI-powered translation uses the provider workflow, while Analyze and Clarity call the shared AI service.

flowchart LR
    Features["AI translation / Analyze / Clarity"] --> Policy["IAiService"]
    Settings["IAiSettingsStore"] --> Policy
    Prompts["IPromptLibrary"] --> Policy
    Secrets["ISecretService and environment"] --> Policy
    Policy --> Backend["IAiBackend from provider module"]
    Backend --> API["Configured remote endpoint"]
    Source["ISourceCodeService"] --> Usages["Key usages and source context"]
    Usages --> Features
    Usages --> Panel["Source Code panel and external editor"]

AiService enforces the app-wide and feature switches, resolves backend/model/endpoint/key, renders the prompt, and calls the backend. Prompt precedence is project .toucan/prompts, user Documents/Toucan/prompts, then built-in prompts. Secrets use the shared encrypted store with backend environment variables as a fallback. TOUCAN_AI_BACKEND can enable/select an AI backend for a CLI process without changing saved settings. See AI integration for feature configuration.

Source scanning is a separate desktop service. It supplies key usages, unused-key filtering, navigation, and context; it does not introduce a source-code language server. Syntax-colored source previews and editable highlighting remain planned.

#Plugin loading and trust

PluginHost discovers child folders containing plugin.json, validates manifests and identities, checks enabled/trusted policy and content hashes, checks signatures through its verifier seam, then activates accepted plugins. Registration is staged in PluginContext and committed before applying it to the host service collection. Failures are reported per plugin rather than aborting all loading.

PluginLoadContext isolates private assembly resolution while sharing host contracts and DI/logging abstractions. This is in-process dependency isolation, not a security sandbox. Load contexts are non-collectible; enable/trust changes should not be described as live unloading. GUI prompts and CLI policy controls wrap the same host machinery. See plugin authoring and policy.

#Configuration ownership

Paths using Documents and application data resolve through .NET special folders rather than hard-coded operating-system paths.

LocationOwner and purpose
<project>/toucan.tprojProjectSettings: format, languages, aliases, packages, source/editor/provider preferences, auto-save, output encoding and line endings. Legacy saveStyle values migrate to a string saveFormat.
Translation filesFormat strategies: serialized translation resources.
Format-relative *.comments.jsonCommentPersistenceService: comments for formats without inline storage.
<project>/.toucan-metadata.jsonAuditService: persisted translation metadata.
Documents/Toucan/settings.jsonAppOptions and preference services: global desktop preferences.
Documents/Toucan/layout.jsonPanelService: panel visibility, widths, active IDs, status visibility, and editor mode. Zen is not restored as a persisted active mode.
Documents/Toucan/ai.json and prompts/AI configuration and user prompt overrides.
Per-user application-data secret storeSecretService and SecureStorageService: encrypted API keys, shared across relevant features.
Documents/Toucan/plugins/ and plugin-policy.jsonExternal plugin payloads and enable/trust decisions; the CLI supports root/policy overrides.

#Change integration and verification

ChangeRequired integration
Built-in formatAdd strategies to the appropriate family module; expose path/comment/detection metadata; update module snapshots and round-trip tests.
Provider or validation ruleImplement the Abstractions contract and register in its module. Registries/pipeline and settings consume registered definitions.
AI featureAdd a feature definition and prompt; route requests through IAiService so settings, secrets, and feature switches apply.
Desktop panelRegister its descriptor, map its view, and check layout restoration and narrow/collapsed states.
Desktop dialogAdd the operation to IDialogService and its implementation; define ownership, accepted/cancelled results, view-model construction, and a fake implementation for command tests. Use the message service for confirmations.
New project settingUpdate persistence and migration behavior, the UI binding, and the service that consumes it. A status/menu label alone does not implement the setting.
Shared serviceReview both composition roots and keep module dependency boundaries intact.

Core coverage lives in Toucan.Core.Tests, including architecture, composition, format, plugin trust, lifecycle, validation, and AI behavior. Toucan.Avalonia.Tests uses the real container and headless Avalonia for bindings, settings controls, editor behavior, and rendered shell layouts. Run focused tests for changed boundaries; use shell rendering for padding, clipping, and responsive changes.

The docs index is generated from status headers. After architecture changes, refresh it with python3 .agents/skills/doc-status/scripts/docs_status.py index and run the companion check command. Keep planned work in specs and link it here without presenting it as implemented architecture.