From f3ca9e00735a0704f7f1592af781afbec99c249b Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Tue, 18 Aug 2026 07:51:37 +0000 Subject: [PATCH 01/20] feat(linux): add Linux CMake build scaffolding Top-level linux/CMakeLists.txt driver building the core static library (engine and gui subdirectories slot in later), per-OS source selection in core/CMakeLists.txt with curl/OpenSSL/libsecret/Threads on Linux, and build instructions in linux/README.md. --- .gitignore | 1 + core/CMakeLists.txt | 37 +++++++++++++++++++++++++++++-------- linux/CMakeLists.txt | 34 ++++++++++++++++++++++++++++++++++ linux/README.md | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 linux/CMakeLists.txt create mode 100644 linux/README.md diff --git a/.gitignore b/.gitignore index f007558..7c9d58a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Build outputs windows/build/ windows/packages/ +linux/build/ # Temporary scripts and artifacts temp/ diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index f858146..7cf1559 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,10 +1,9 @@ # Agent Redactor core library — OS-agnostic proxy / NER / redaction engine. # -# Status (PR 1 — restructure): this file is scaffolding introduced alongside the -# AgentRedactor/ -> windows/ move. It currently builds on Windows only; several -# sources still carry Windows-specific code (localization.cpp uses MRT resources, -# utils.cpp / logging.h use Win32 APIs). The Linux port (PR 3) peels those shims -# behind a platform interface and makes this target cross-platform. +# The Windows build compiles these sources via the vcxproj files under +# windows/; this CMake target is used by the Linux build (linux/CMakeLists.txt). +# localization.cpp is MRT/WinRT-bound and stays Windows-only; the Linux engine +# uses the English-only LocString shim pattern (see windows/engine/engine_loc.cpp). cmake_minimum_required(VERSION 3.24) project(agentredactor-core LANGUAGES CXX) @@ -12,6 +11,7 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(nlohmann_json CONFIG REQUIRED) +find_package(Threads REQUIRED) # onnxruntime: no official CMake config on all platforms; locate via vcpkg or # a system install. Include dir must contain onnxruntime_cxx_api.h. @@ -22,12 +22,13 @@ if (NOT ONNXRUNTIME_INCLUDE_DIR OR NOT ONNXRUNTIME_LIB) message(FATAL_ERROR "onnxruntime not found — install via vcpkg or set ONNXRUNTIME_INCLUDE_DIR/ONNXRUNTIME_LIB") endif() -add_library(agentredactor-core STATIC +set(AR_CORE_SOURCES src/api_key_profile.cpp src/bpe_tokenizer.cpp + src/cli.cpp + src/control_server.cpp src/http_server.cpp src/keyword_engine.cpp - src/localization.cpp src/log_manager.cpp src/model_downloader.cpp src/pii_detector.cpp @@ -39,6 +40,18 @@ add_library(agentredactor-core STATIC src/migrations/settings_migrator.cpp ) +if (WIN32) + # MRT/WinRT-bound localization compiles on Windows only. + list(APPEND AR_CORE_SOURCES src/localization.cpp) +else() + find_package(CURL REQUIRED) + find_package(OpenSSL REQUIRED) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LIBSECRET REQUIRED IMPORTED_TARGET libsecret-1) +endif() + +add_library(agentredactor-core STATIC ${AR_CORE_SOURCES}) + target_include_directories(agentredactor-core PUBLIC include PRIVATE ${ONNXRUNTIME_INCLUDE_DIR} @@ -46,5 +59,13 @@ target_include_directories(agentredactor-core target_link_libraries(agentredactor-core PUBLIC nlohmann_json::nlohmann_json - PRIVATE ${ONNXRUNTIME_LIB} + PRIVATE ${ONNXRUNTIME_LIB} Threads::Threads ) + +if (NOT WIN32) + target_include_directories(agentredactor-core PRIVATE ${CURL_INCLUDE_DIRS}) + target_link_libraries(agentredactor-core + PRIVATE CURL::libcurl OpenSSL::SSL OpenSSL::Crypto PkgConfig::LIBSECRET + ) + target_compile_options(agentredactor-core PRIVATE -Wall -Wextra) +endif() diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..ec7beaf --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,34 @@ +# Agent Redactor — Linux build driver. +# Builds the OS-agnostic core static library plus the Linux engine/CLI binary. +# +# cmake -B build -G Ninja \ +# -DONNXRUNTIME_INCLUDE_DIR=~/onnxruntime/include \ +# -DONNXRUNTIME_LIB=~/onnxruntime/lib/libonnxruntime.so +# cmake --build build +# +# onnxruntime has no apt package; download the official linux-x64 tarball +# (tested with 1.29.0) and point the two knobs at it. +cmake_minimum_required(VERSION 3.24) +project(agentredactor-linux LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +# Version source of truth is shared with the Windows build. +file(STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/../windows/version.txt AR_VERSION LIMIT_COUNT 1) +string(STRIP "${AR_VERSION}" AR_VERSION) +message(STATUS "Agent Redactor version: ${AR_VERSION}") + +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../core ${CMAKE_BINARY_DIR}/core) + +if (EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/engine/CMakeLists.txt) + add_subdirectory(engine) +endif() + +if (EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/gui/CMakeLists.txt) + add_subdirectory(gui) +endif() diff --git a/linux/README.md b/linux/README.md new file mode 100644 index 0000000..1435977 --- /dev/null +++ b/linux/README.md @@ -0,0 +1,32 @@ +# Agent Redactor — Linux build + +Build the engine/CLI (`agentredactor`) on Linux: + +```bash +sudo apt install -y build-essential cmake ninja-build pkg-config \ + libsecret-1-dev libcurl4-openssl-dev libssl-dev nlohmann-json3-dev \ + python3-pytest python3-aiohttp python3-psutil + +# onnxruntime is not packaged in apt; use the official linux-x64 tarball +# (developed/tested against 1.29.0): +mkdir -p ~/onnxruntime +curl -sL https://github.com/microsoft/onnxruntime/releases/download/v1.29.0/onnxruntime-linux-x64-1.29.0.tgz \ + | tar xz -C ~/onnxruntime --strip-components=1 + +cmake -B build -G Ninja \ + -DONNXRUNTIME_INCLUDE_DIR=~/onnxruntime/include \ + -DONNXRUNTIME_LIB=~/onnxruntime/lib/libonnxruntime.so +cmake --build build +``` + +The engine also needs the NER model files. `config.json`, `tokenizer.json`, +`viterbi_calibration.json` and `onnx/model_quantized.onnx` live in +`windows/models/`; the ~1.6 GB `onnx/model_quantized.onnx_data` weights are +downloaded automatically on first run (or grab them from the models endpoint +used by the Windows CI). + +Run the tests from the repo root: + +```bash +python -m pytest tests/cli tests/migration tests/linux -q +``` From e318562d6c3793af54d08e48515fda9627dd671e Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Tue, 18 Aug 2026 10:05:54 +0000 Subject: [PATCH 02/20] feat(linux): port core library to Linux - platform_compat.h: MSVC/POSIX shims (_snwprintf, _wtof, localtime_s, _wgetenv, SOCKET typedef, Winsock constants), keeping the public SOCKET-based http_server surface unchanged - http_server: POSIX sockets (select nfds fix, socklen_t); behavior (loopbackOnly, dual-stack, streaming SendChunk) identical - utils: UTF-8<->wstring via codecvt, XDG config path fallback (~/.config/agentredactor, AGENTREDACTOR_CONFIG_DIR still wins), /proc/self/exe, RFC4122 v4 UUID, libcurl implementations of HttpGetString/HttpDownloadFile/HttpDownloadFileSegmented - proxy_engine: libcurl upstream client (streaming SSE, credential header substitution, gzip/deflate) behind #ifdef; WinHTTP branch untouched - control_server: token via RAND_bytes, control.json with 0600 perms - secure_storage: shared interface moves to core/include; Windows impl (DPAPI/CNG/Hello) unchanged; new Linux impl with OpenSSL AES-256-GCM, libsecret machine key with /etc/machine-id PBKDF2 fallback, and typed-master-password protection (PBKDF2-HMAC-SHA256 wrapped session key) - settings_manager: per-OS password entry points - localization.h/constants.h/log_manager/model_downloader/pii_detector: de-Windows-ified headers, XDG model fallback dir, CPU-only EP on Linux, UTF-8 model path for ORT - core-smoke dev target verifies SettingsManager + RegexEngine against a temp config dir --- core/CMakeLists.txt | 1 + core/include/constants.h | 8 +- core/include/http_server.h | 6 +- core/include/localization.h | 4 + core/include/log_manager.h | 1 - core/include/logging.h | 14 +- core/include/platform_compat.h | 64 ++++ core/include/proxy_engine.h | 5 +- {windows => core}/include/secure_storage.h | 65 +++- core/include/settings_manager.h | 7 + core/include/utils.h | 4 +- core/src/control_server.cpp | 32 ++ core/src/http_server.cpp | 19 +- core/src/log_manager.cpp | 1 + core/src/model_downloader.cpp | 30 +- core/src/pii_detector.cpp | 14 + core/src/platform_compat.cpp | 15 + core/src/proxy_engine.cpp | 228 +++++++++++ core/src/secure_storage_linux.cpp | 415 ++++++++++++++++++++ core/src/settings_manager.cpp | 20 + core/src/utils.cpp | 422 ++++++++++++++++++++- linux/engine/CMakeLists.txt | 3 + linux/engine/core_smoke.cpp | 28 ++ windows/AgentRedactor.vcxproj | 2 +- 24 files changed, 1364 insertions(+), 44 deletions(-) create mode 100644 core/include/platform_compat.h rename {windows => core}/include/secure_storage.h (52%) create mode 100644 core/src/platform_compat.cpp create mode 100644 core/src/secure_storage_linux.cpp create mode 100644 linux/engine/CMakeLists.txt create mode 100644 linux/engine/core_smoke.cpp diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 7cf1559..d99ed31 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -48,6 +48,7 @@ else() find_package(OpenSSL REQUIRED) find_package(PkgConfig REQUIRED) pkg_check_modules(LIBSECRET REQUIRED IMPORTED_TARGET libsecret-1) + list(APPEND AR_CORE_SOURCES src/secure_storage_linux.cpp src/platform_compat.cpp) endif() add_library(agentredactor-core STATIC ${AR_CORE_SOURCES}) diff --git a/core/include/constants.h b/core/include/constants.h index d9adba5..6450ee1 100644 --- a/core/include/constants.h +++ b/core/include/constants.h @@ -3,7 +3,7 @@ #include #include #include -#include +#include "platform_compat.h" namespace AgentRedactor { @@ -18,7 +18,9 @@ constexpr const wchar_t* APP_VERSION = AR_STRINGIZE(AR_VERSION_STRING); #else constexpr const wchar_t* APP_VERSION = L"1.0.0"; #endif +#ifdef _WIN32 constexpr UINT WM_TRAYICON = WM_USER + 1; +#endif constexpr size_t MAX_TOKENS_PER_CHUNK = 128000; constexpr size_t TOKEN_OVERLAP = 128; @@ -51,12 +53,14 @@ inline const std::vector PII_CATEGORIES = { {L"DIGITAL", L"Digital & Secrets", {L"private_url", L"secret"}}, }; +#ifdef _WIN32 enum MenuIDs : UINT { ID_TRAY_OPEN = 1001, ID_TRAY_LANGUAGE_FIRST = 2000, ID_TRAY_START_ON_BOOT = 3001, ID_TRAY_QUIT, }; +#endif struct SupportedLanguage { std::wstring tag; @@ -153,6 +157,7 @@ inline bool LanguageMatches(const std::wstring& current, const std::wstring& sup } // namespace AgentRedactor +#ifdef _WIN32 inline void RegisterStartupTask() { HKEY hKey; if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_WRITE, &hKey) == ERROR_SUCCESS) { @@ -171,3 +176,4 @@ inline void UnregisterStartupTask() { RegCloseKey(hKey); } } +#endif diff --git a/core/include/http_server.h b/core/include/http_server.h index 1f6f35b..44e5fdf 100644 --- a/core/include/http_server.h +++ b/core/include/http_server.h @@ -9,11 +9,7 @@ #include #include #include -#include -#include -#include - -#pragma comment(lib, "ws2_32.lib") +#include "platform_compat.h" namespace AgentRedactor { diff --git a/core/include/localization.h b/core/include/localization.h index 6d374e3..9770231 100644 --- a/core/include/localization.h +++ b/core/include/localization.h @@ -3,7 +3,9 @@ #include #include #include +#ifdef _WIN32 #include +#endif namespace AgentRedactor { @@ -31,8 +33,10 @@ std::wstring GetLanguageOverride(); // (e.g. Arabic, Hebrew, Urdu). bool IsCurrentLanguageRtl(); +#ifdef _WIN32 // Apply RightToLeft/LeftToRight FlowDirection to a FrameworkElement based on // the current effective UI language. Call after InitializeComponent(). void ApplyCurrentFlowDirection(const winrt::Windows::Foundation::IInspectable& element); +#endif } // namespace AgentRedactor diff --git a/core/include/log_manager.h b/core/include/log_manager.h index 6fe0318..48c76cb 100644 --- a/core/include/log_manager.h +++ b/core/include/log_manager.h @@ -4,7 +4,6 @@ #include #include #include -#include namespace AgentRedactor { diff --git a/core/include/logging.h b/core/include/logging.h index 5d77848..7612726 100644 --- a/core/include/logging.h +++ b/core/include/logging.h @@ -1,7 +1,7 @@ #pragma once -#include #include +#include "platform_compat.h" // Runtime-gated logging (controlled by the app's logging options, not // compile-time flags): @@ -20,11 +20,23 @@ namespace AgentRedactor { namespace Utils { template std::wstring FormatString(const wchar_t* fmt, Args... args) { +#ifdef _WIN32 int size = _snwprintf(nullptr, 0, fmt, args...); if (size <= 0) return L""; std::wstring result(size, L'\0'); _snwprintf(result.data(), result.size() + 1, fmt, args...); return result; +#else + // glibc swprintf reports no would-be size on truncation; grow instead. + for (size_t size = 256;; size *= 2) { + std::wstring result(size, L'\0'); + int n = swprintf(result.data(), result.size(), fmt, args...); + if (n >= 0 && static_cast(n) < result.size()) { + result.resize(static_cast(n)); + return result; + } + } +#endif } } } diff --git a/core/include/platform_compat.h b/core/include/platform_compat.h new file mode 100644 index 0000000..770c5a3 --- /dev/null +++ b/core/include/platform_compat.h @@ -0,0 +1,64 @@ +#pragma once + +// MSVC/POSIX compatibility shims for the OS-agnostic core. On Windows this +// just pulls in the Win32 headers the core was written against; elsewhere it +// provides the small set of MSVC-isms the core uses (_snwprintf, _wtof, +// localtime_s with MSVC argument order, _wgetenv, _stricmp, OutputDebugStringW) +// plus the Winsock type/constant names that leak into public core interfaces. + +#ifdef _WIN32 + +#include + +// POSIX name for the address-length type Winsock exposes as int. +typedef int ar_socklen_t; + +#else // POSIX + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Winsock names used by http_server's public interface. +typedef int SOCKET; +#define INVALID_SOCKET (-1) +#define SOCKET_ERROR (-1) +#define SD_SEND SHUT_WR +#define closesocket close +#define WSAGetLastError() errno + +typedef socklen_t ar_socklen_t; +typedef unsigned char BYTE; + +// MSVC returns a negative value on truncation; swprintf has the same contract. +#define _snwprintf swprintf + +inline double _wtof(const wchar_t* s) { return std::wcstod(s, nullptr); } + +// MSVC argument order (out, in) — the inverse of POSIX localtime_r. +inline int localtime_s(struct tm* out, const std::time_t* t) { + return localtime_r(t, out) ? 0 : 1; +} + +inline int _stricmp(const char* a, const char* b) { return strcasecmp(a, b); } + +inline void OutputDebugStringW(const wchar_t*) {} + +// Narrow env lookup converted to wide. Only ASCII/UTF-8 variable names are +// used in practice (AGENTREDACTOR_CONFIG_DIR). The returned pointer is valid +// until the next call on the same thread. +const wchar_t* _wgetenv(const wchar_t* name); + +#endif // _WIN32 diff --git a/core/include/proxy_engine.h b/core/include/proxy_engine.h index c02da31..8680e72 100644 --- a/core/include/proxy_engine.h +++ b/core/include/proxy_engine.h @@ -8,8 +8,7 @@ #include #include #include -#include -#include +#include "platform_compat.h" #include #include "api_key_profile.h" #include "pii_detector.h" @@ -17,7 +16,9 @@ #include "keyword_engine.h" #include "log_manager.h" +#ifdef _WIN32 #pragma comment(lib, "winhttp.lib") +#endif namespace AgentRedactor { diff --git a/windows/include/secure_storage.h b/core/include/secure_storage.h similarity index 52% rename from windows/include/secure_storage.h rename to core/include/secure_storage.h index c97960e..96b24ea 100644 --- a/windows/include/secure_storage.h +++ b/core/include/secure_storage.h @@ -1,10 +1,23 @@ #pragma once -#include +// OS-agnostic secure-storage interface for settings secrets (API keys, +// keywords, regex patterns). The class declaration is shared; each platform +// provides its own implementation: +// Windows: windows/src/secure_storage.cpp — DPAPI + CNG AES-GCM, protection +// is Windows-Hello-only (no typed password). +// Linux: core/src/secure_storage_linux.cpp — OpenSSL AES-GCM; unprotected +// fields use a machine key from the libsecret keyring (with a +// machine-id-derived fallback on headless servers); protection is +// a typed master password whose PBKDF2-HMAC-SHA256 key wraps the +// AES session key. +// The on-disk envelope (_enc/_mode/_iv/_tag, base64) is identical on both. + #include #include #include +#include #include +#include "platform_compat.h" using json = nlohmann::json; @@ -13,10 +26,8 @@ namespace AgentRedactor { class SecureStorage { public: // Initialize from the "master_password" config block in settings.json. - // Protection is Windows-Hello-only (no typed password). A legacy config - // that enabled protection with a typed password but has no Hello blob - // degrades to unprotected (the Hello blob cannot be created without the - // user's Windows Hello verification). + // A legacy/foreign config whose protection blob this platform cannot + // unwrap degrades to unprotected so the user can re-enable protection. bool Initialize(const json& config); bool IsInitialized() const { return initialized_; } @@ -33,6 +44,7 @@ class SecureStorage { // Get the master_password config block to save to settings.json. json GetConfig() const; +#ifdef _WIN32 // Enable Windows-Hello-only protection: a random AES key is generated and // stored only as the DPAPI-wrapped Hello blob. Windows Hello is the only // way to unlock. @@ -44,18 +56,28 @@ class SecureStorage { // Unlock the storage with the Windows Hello blob (the UserConsentVerifier // consent prompt is driven by the caller via hello_unlock.cpp). bool UnlockWithHello(); +#else + // Linux: typed-master-password protection (there is no Windows Hello). + // A random AES-256-GCM session key is wrapped by a key derived from the + // password via PBKDF2-HMAC-SHA256; only the wrapped blob is persisted. + bool EnableMasterPassword(const std::wstring& password); + void DisableMasterPassword(); + bool UnlockWithPassword(const std::wstring& password); +#endif // Lock the session again without discarding the in-memory AES key: the - // storage is marked uninitialized so reads/decrypts fail until - // UnlockWithHello succeeds (used after the GUI quits while the engine - // keeps running, so the next open must re-authenticate). + // storage is marked uninitialized so reads/decrypts fail until unlock + // succeeds (used after the GUI quits while the engine keeps running, so + // the next open must re-authenticate). void Lock(); private: bool initialized_ = false; bool masterPasswordEnabled_ = false; - bool helloEnabled_ = false; + bool helloEnabled_ = false; // Windows Hello; always false on Linux std::vector aesKey_; // 32 bytes, only valid when initialized + +#ifdef _WIN32 std::vector helloBlob_; // DPAPI-wrapped copy of aesKey_, used by UnlockWithHello // DPAPI @@ -63,19 +85,34 @@ class SecureStorage { static std::optional> DpapiUnprotect(const std::vector& data); static std::optional> DpapiEncrypt(const std::wstring& plaintext); static std::optional DpapiDecrypt(const std::vector& ciphertext); - - // AES-256-GCM via Windows CNG +#else + // Persisted password verification block (under master_password.password). + std::vector passwordSalt_; + uint32_t passwordIterations_ = 0; + std::vector wrappedKey_; // AES-GCM(aesKey_) under the PBKDF2 KEK + std::vector wrappedKeyIv_; + std::vector wrappedKeyTag_; + + // The unprotected at-rest key (DPAPI counterpart): a random key from the + // libsecret keyring, or PBKDF2-derived from /etc/machine-id when no + // keyring is available (headless servers). + static std::optional> MachineKey(); + + static std::vector Pbkdf2(const std::string& password, const std::vector& salt, uint32_t iterations); +#endif + + // AES-256-GCM (CNG on Windows, OpenSSL EVP on Linux) static bool AesGcmEncrypt(const std::vector& plaintext, const std::vector& key, std::vector& ciphertext, std::vector& iv, std::vector& tag); static std::optional> AesGcmDecrypt(const std::vector& ciphertext, const std::vector& key, const std::vector& iv, const std::vector& tag); - // Random bytes via BCryptGenRandom + // Random bytes (BCryptGenRandom on Windows, RAND_bytes on Linux) static std::vector GenerateRandomBytes(size_t count); - // Base64 via CryptBinaryToString/CryptStringToBinary + // Base64 (CryptBinaryToString on Windows, EVP on Linux) static std::string Base64Encode(const std::vector& data); static std::vector Base64Decode(const std::string& str); }; -} // namespace AgentRedactor \ No newline at end of file +} // namespace AgentRedactor diff --git a/core/include/settings_manager.h b/core/include/settings_manager.h index 1cfb120..73af8d4 100644 --- a/core/include/settings_manager.h +++ b/core/include/settings_manager.h @@ -42,9 +42,16 @@ class SettingsManager { bool IsMasterPasswordEnabled() const; bool IsUnlocked() const; bool IsHelloEnabled() const; +#ifdef _WIN32 bool EnableMasterPassword(); void DisableMasterPassword(); bool UnlockWithHello(); +#else + // Linux: typed-master-password protection (no Windows Hello). + bool EnableMasterPassword(const std::wstring& password); + void DisableMasterPassword(); + bool UnlockWithPassword(const std::wstring& password); +#endif void Lock(); bool IsLoggingEnabled() const; diff --git a/core/include/utils.h b/core/include/utils.h index f9e5a74..060d6fd 100644 --- a/core/include/utils.h +++ b/core/include/utils.h @@ -5,7 +5,7 @@ #include #include #include -#include +#include "platform_compat.h" #include "logging.h" namespace AgentRedactor { @@ -65,7 +65,7 @@ double ParseLocalizedFloat(const std::wstring& text); std::wstring FormatLocalizedTime(const std::time_t& time); std::wstring FormatLocalizedDateTime(const std::time_t& time); -// Simple synchronous HTTP(S) GET helpers (WinHTTP), shared by the update +// Simple synchronous HTTP(S) GET helpers, shared by the update // manager and the first-run model downloader. Follow redirects across hosts // (the update feed and GitHub release assets both 302). Never throw. bool HttpGetString(const std::wstring& url, std::string& outBody); diff --git a/core/src/control_server.cpp b/core/src/control_server.cpp index b00a5c4..9f425d2 100644 --- a/core/src/control_server.cpp +++ b/core/src/control_server.cpp @@ -1,11 +1,17 @@ #include "control_server.h" #include "utils.h" #include "logging.h" +#ifdef _WIN32 #include #include #include #pragma comment(lib, "bcrypt.lib") +#else +#include +#include +#include +#endif namespace AgentRedactor { @@ -90,9 +96,15 @@ HttpResponse ControlServer::HandleRequest(const HttpRequest& request) { std::wstring ControlServer::GenerateToken() const { unsigned char bytes[16] = {}; +#ifdef _WIN32 if (BCryptGenRandom(nullptr, bytes, sizeof(bytes), BCRYPT_USE_SYSTEM_PREFERRED_RNG) != 0) { return L""; } +#else + if (RAND_bytes(bytes, sizeof(bytes)) != 1) { + return L""; + } +#endif static const wchar_t hex[] = L"0123456789abcdef"; std::wstring token; token.reserve(sizeof(bytes) * 2); @@ -104,6 +116,7 @@ std::wstring ControlServer::GenerateToken() const { } bool ControlServer::WriteControlFile(const std::filesystem::path& path) const { +#ifdef _WIN32 // Build a security descriptor that grants full control to the current // user only (D:P = protected DACL, no inheritance). std::wstring sddl = L"D:P(A;;FA;;;"; @@ -150,6 +163,25 @@ bool ControlServer::WriteControlFile(const std::filesystem::path& path) const { if (tokenUser) LocalFree(tokenUser); if (tokenHandle) CloseHandle(tokenHandle); return ok; +#else + // Owner-only file (0600), replacing the Windows per-user ACL. umask may + // widen nothing here: open() with 0600 then fchmod to strip any umask + // bits is unnecessary (umask only removes bits), so 0600 is guaranteed. + std::string content = "{\"port\": " + std::to_string(port_) + + ", \"token\": \"" + Utils::WideToUtf8(token_) + "\"" + + ", \"pid\": " + std::to_string(getpid()) + "}\n"; + int fd = open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) return false; + bool ok = true; + size_t written = 0; + while (written < content.size()) { + ssize_t n = write(fd, content.data() + written, content.size() - written); + if (n <= 0) { ok = false; break; } + written += static_cast(n); + } + close(fd); + return ok; +#endif } } // namespace AgentRedactor diff --git a/core/src/http_server.cpp b/core/src/http_server.cpp index 54a13b2..743ea9a 100644 --- a/core/src/http_server.cpp +++ b/core/src/http_server.cpp @@ -9,13 +9,17 @@ namespace AgentRedactor { HttpServer::HttpServer() { +#ifdef _WIN32 WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsaData); +#endif } HttpServer::~HttpServer() { Stop(); +#ifdef _WIN32 WSACleanup(); +#endif } bool HttpServer::Start(int port, std::function handler, bool loopbackOnly) { @@ -66,7 +70,7 @@ bool HttpServer::Start(int port, std::function // Resolve the actual bound port (relevant when port 0 was requested). { sockaddr_in6 bound = {}; - int boundLen = sizeof(bound); + ar_socklen_t boundLen = sizeof(bound); if (getsockname(listenSocket_, (sockaddr*)&bound, &boundLen) == 0) { port_ = ntohs(bound.sin6_port); } @@ -113,12 +117,13 @@ void HttpServer::RunListener() { FD_ZERO(&readSet); FD_SET(listenSocket_, &readSet); timeval tv = {0, 100000}; // 100ms timeout - int selectResult = select(0, &readSet, nullptr, nullptr, &tv); + // nfds is ignored on Windows; POSIX requires the highest fd + 1. + int selectResult = select(static_cast(listenSocket_) + 1, &readSet, nullptr, nullptr, &tv); if (selectResult <= 0) continue; if (!FD_ISSET(listenSocket_, &readSet)) continue; sockaddr_in6 clientAddr; - int addrLen = sizeof(clientAddr); + ar_socklen_t addrLen = sizeof(clientAddr); SOCKET clientSocket = accept(listenSocket_, (sockaddr*)&clientAddr, &addrLen); if (clientSocket == INVALID_SOCKET) continue; @@ -189,7 +194,7 @@ void HttpServer::HandleClient(SOCKET clientSocket) { FD_ZERO(&readSet); FD_SET(clientSocket, &readSet); timeval tv = {0, 50000}; // 50ms - if (select(0, &readSet, nullptr, nullptr, &tv) > 0 && FD_ISSET(clientSocket, &readSet)) { + if (select(static_cast(clientSocket) + 1, &readSet, nullptr, nullptr, &tv) > 0 && FD_ISSET(clientSocket, &readSet)) { drainResult = recv(clientSocket, drain, sizeof(drain), 0); if (drainResult == 0 || drainResult == SOCKET_ERROR) break; } @@ -417,14 +422,18 @@ bool HttpServer::SendChunkedEnd(SOCKET clientSocket) { } bool IsPortAvailable(int port) { +#ifdef _WIN32 WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { return false; } +#endif SOCKET testSocket = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); if (testSocket == INVALID_SOCKET) { +#ifdef _WIN32 WSACleanup(); +#endif return false; } @@ -442,7 +451,9 @@ bool IsPortAvailable(int port) { bool available = (bind(testSocket, (sockaddr*)&addr, sizeof(addr)) == 0); closesocket(testSocket); +#ifdef _WIN32 WSACleanup(); +#endif return available; } diff --git a/core/src/log_manager.cpp b/core/src/log_manager.cpp index 6449083..a4962b0 100644 --- a/core/src/log_manager.cpp +++ b/core/src/log_manager.cpp @@ -1,5 +1,6 @@ #include "log_manager.h" #include "utils.h" +#include #include #include #include diff --git a/core/src/model_downloader.cpp b/core/src/model_downloader.cpp index 442463a..28d773b 100644 --- a/core/src/model_downloader.cpp +++ b/core/src/model_downloader.cpp @@ -1,8 +1,10 @@ #include "model_downloader.h" #include "constants.h" #include "utils.h" +#ifdef _WIN32 #include #include +#endif #include namespace AgentRedactor { @@ -15,7 +17,7 @@ namespace { constexpr const wchar_t* kWeightsUrls[] = { L"https://api.agentredactor.negativestarinnovators.com/models/model_quantized.onnx_data", }; -constexpr const wchar_t* kWeightsRelativePath = L"onnx\\model_quantized.onnx_data"; +constexpr const wchar_t* kWeightsRelativePath = L"onnx/model_quantized.onnx_data"; // Small companion files that ship with the app next to the exe and are copied // (not downloaded) into the fallback directory when missing. @@ -23,7 +25,7 @@ constexpr const wchar_t* kCompanionFiles[] = { L"tokenizer.json", L"config.json", L"viterbi_calibration.json", - L"onnx\\model_quantized.onnx", + L"onnx/model_quantized.onnx", }; std::filesystem::path WeightsPath(const std::filesystem::path& modelDir) { @@ -44,11 +46,23 @@ std::filesystem::path WeightsFilePath(const std::filesystem::path& modelDir) { } std::filesystem::path GetFallbackModelDir() { +#ifdef _WIN32 wchar_t path[MAX_PATH]; if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, path))) { return std::filesystem::path(path) / L"AgentRedactor" / MODEL_DIR; } return std::filesystem::path(L"C:\\AgentRedactor") / MODEL_DIR; +#else + // XDG: $XDG_DATA_HOME/agentredactor/models, defaulting to + // ~/.local/share/agentredactor/models. + if (const char* xdg = std::getenv("XDG_DATA_HOME"); xdg && *xdg) { + return std::filesystem::path(xdg) / "agentredactor" / "models"; + } + if (const char* home = std::getenv("HOME"); home && *home) { + return std::filesystem::path(home) / ".local" / "share" / "agentredactor" / "models"; + } + return std::filesystem::path("/tmp/agentredactor") / "models"; +#endif } bool HasModelWeights(const std::filesystem::path& modelDir) { @@ -63,7 +77,7 @@ bool HasModelWeights(const std::filesystem::path& modelDir) { // instead of failing to initialize the detector forever. LOGF_LIFECYCLE(L"[ModelDownloader] Deleting corrupt weights (size %llu, expected %llu): %s", static_cast(size), static_cast(kWeightsExpectedBytes), - weights.c_str()); + weights.wstring().c_str()); std::filesystem::remove(weights, ec); return false; } @@ -89,19 +103,19 @@ bool EnsureModelFiles(const std::filesystem::path& fallbackModelDir, if (std::filesystem::exists(dest, ec)) continue; auto src = exeModels / relative; if (!std::filesystem::exists(src, ec)) { - LOGF_LIFECYCLE(L"[ModelDownloader] Companion file missing next to exe: %s", src.c_str()); + LOGF_LIFECYCLE(L"[ModelDownloader] Companion file missing next to exe: %s", src.wstring().c_str()); return false; } std::filesystem::create_directories(dest.parent_path(), ec); if (ec) { LOGF_LIFECYCLE(L"[ModelDownloader] Failed to create %s: %s", - dest.parent_path().c_str(), Utils::Utf8ToWide(ec.message()).c_str()); + dest.parent_path().wstring().c_str(), Utils::Utf8ToWide(ec.message()).c_str()); return false; } std::filesystem::copy_file(src, dest, ec); if (ec) { LOGF_LIFECYCLE(L"[ModelDownloader] Failed to copy %s: %s", - src.c_str(), Utils::Utf8ToWide(ec.message()).c_str()); + src.wstring().c_str(), Utils::Utf8ToWide(ec.message()).c_str()); return false; } } @@ -117,7 +131,7 @@ bool EnsureModelFiles(const std::filesystem::path& fallbackModelDir, std::error_code ec; std::filesystem::create_directories(weightsDest.parent_path(), ec); if (ec) { - LOGF_LIFECYCLE(L"[ModelDownloader] Failed to create %s", weightsDest.parent_path().c_str()); + LOGF_LIFECYCLE(L"[ModelDownloader] Failed to create %s", weightsDest.parent_path().wstring().c_str()); return false; } @@ -158,7 +172,7 @@ bool EnsureModelFiles(const std::filesystem::path& fallbackModelDir, if (ec) { // Keep the complete .partial; the next retry finalizes it without // downloading again. - LOGF_LIFECYCLE(L"[ModelDownloader] Failed to finalize %s", weightsDest.c_str()); + LOGF_LIFECYCLE(L"[ModelDownloader] Failed to finalize %s", weightsDest.wstring().c_str()); return false; } diff --git a/core/src/pii_detector.cpp b/core/src/pii_detector.cpp index 7ff74b0..f73462b 100644 --- a/core/src/pii_detector.cpp +++ b/core/src/pii_detector.cpp @@ -5,7 +5,9 @@ #include "logging.h" #include #include +#ifdef _WIN32 #include +#endif #include #include #include @@ -78,6 +80,9 @@ bool PIIDetector::LoadModel() { currentProvider_ = L"CPU"; if (preferredProvider_ != L"cpu") { +#ifdef _WIN32 + // GPU providers (DirectML, CUDA) are only wired up on Windows; + // the Linux build is CPU-only. std::vector availableProviders = Ort::GetAvailableProviders(); bool gpuEnabled = false; if (preferredProvider_ == L"auto" || preferredProvider_ == L"gpu") { @@ -114,9 +119,18 @@ bool PIIDetector::LoadModel() { if (!gpuEnabled) { LOG(L"[PIIDetector] No GPU provider available, using CPU"); } +#else + LOG(L"[PIIDetector] GPU providers are not supported in this build, using CPU"); +#endif } +#ifdef _WIN32 session_ = std::make_unique(*g_onnx_env, modelFile.c_str(), sessionOptions); +#else + // ORTCHAR_T is narrow on Linux: the session path must be UTF-8. + const std::string modelFileUtf8 = Utils::WideToUtf8(modelFile.wstring()); + session_ = std::make_unique(*g_onnx_env, modelFileUtf8.c_str(), sessionOptions); +#endif LOGF_LIFECYCLE(L"[PIIDetector] Model loaded with provider: %s", currentProvider_.c_str()); return true; } catch (const std::exception& e) { diff --git a/core/src/platform_compat.cpp b/core/src/platform_compat.cpp new file mode 100644 index 0000000..272ed60 --- /dev/null +++ b/core/src/platform_compat.cpp @@ -0,0 +1,15 @@ +#include "platform_compat.h" + +#ifndef _WIN32 + +#include "utils.h" + +const wchar_t* _wgetenv(const wchar_t* name) { + static thread_local std::wstring value; + const char* narrow = std::getenv(AgentRedactor::Utils::WideToUtf8(name).c_str()); + if (!narrow) return nullptr; + value = AgentRedactor::Utils::Utf8ToWide(narrow); + return value.c_str(); +} + +#endif diff --git a/core/src/proxy_engine.cpp b/core/src/proxy_engine.cpp index 33413f6..9ce344f 100644 --- a/core/src/proxy_engine.cpp +++ b/core/src/proxy_engine.cpp @@ -11,6 +11,7 @@ using json = nlohmann::json; +#ifdef _WIN32 #ifndef WINHTTP_OPTION_DECOMPRESSION #define WINHTTP_OPTION_DECOMPRESSION 118 #endif @@ -23,6 +24,9 @@ using json = nlohmann::json; #ifndef WINHTTP_DECOMPRESSION_FLAG_ALL #define WINHTTP_DECOMPRESSION_FLAG_ALL (WINHTTP_DECOMPRESSION_FLAG_GZIP | WINHTTP_DECOMPRESSION_FLAG_DEFLATE) #endif +#else +#include +#endif namespace AgentRedactor { @@ -930,6 +934,7 @@ bool ProxyEngine::ForwardToUpstreamStreaming(const std::wstring& upstreamUrl, co std::function onBodyChunk, std::function>& headers)> onHeaders) { +#ifdef _WIN32 URL_COMPONENTS urlComp = { sizeof(URL_COMPONENTS) }; urlComp.dwSchemeLength = (DWORD)-1; urlComp.dwHostNameLength = (DWORD)-1; @@ -1131,6 +1136,229 @@ bool ProxyEngine::ForwardToUpstreamStreaming(const std::wstring& upstreamUrl, co WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return true; + +#else // POSIX: libcurl upstream client + // NOTE: the path-concatenation, credential-substitution and logging logic + // below mirrors the WinHTTP branch above; keep the two in sync. + + // Crack upstreamUrl into scheme/host[:port]/path (WinHttpCrackUrl + // counterpart; curl re-parses the port from the rebuilt URL). + const std::string upstreamNarrow = Utils::WideToUtf8(upstreamUrl); + std::string scheme = "http"; + std::string hostPort; + std::wstring urlPath; + { + std::string rest = upstreamNarrow; + const size_t schemeEnd = rest.find("://"); + if (schemeEnd != std::string::npos) { + scheme = rest.substr(0, schemeEnd); + rest = rest.substr(schemeEnd + 3); + } + const size_t slash = rest.find('/'); + hostPort = (slash == std::string::npos) ? rest : rest.substr(0, slash); + urlPath = Utils::Utf8ToWide((slash == std::string::npos) ? "" : rest.substr(slash)); + } + + // Smart path concatenation: deduplicate overlapping segments + // e.g. upstream=/api/v1 + incoming=/v1/chat → /api/v1/chat + std::wstring fullPath = urlPath; + if (!fullPath.empty() && fullPath.back() == L'/') fullPath.pop_back(); + + if (!path.empty()) { + size_t maxCommon = std::min(fullPath.length(), path.length()); + size_t common = 0; + for (size_t i = 1; i <= maxCommon; ++i) { + if (fullPath.substr(fullPath.length() - i) == path.substr(0, i)) { + common = i; + } + } + if (common > 0) { + fullPath = fullPath.substr(0, fullPath.length() - common); + } + if (!fullPath.empty() && !path.empty() && fullPath.back() == L'/' && path.front() == L'/') { + fullPath.pop_back(); + } + fullPath += path; + } + std::wstring wMethod = Utils::Utf8ToWide(method); + + // Build headers, stripping hop-by-hop and accept-encoding headers. + // Client credential headers (Authorization, x-api-key, api-key) are + // forwarded with the real upstream key substituted for whatever value the + // client sent — agents commonly send placeholders (e.g. Claude Code's + // dummy ANTHROPIC_AUTH_TOKEN) and expect the proxy to hold the real key. + // The client's auth style is preserved (Authorization keeps its scheme, + // x-api-key/api-key take the raw key), so both OpenAI-style Bearer and + // Anthropic-style x-api-key upstreams work without agent sniffing. If the + // client sent no credential header at all, default to Authorization: + // Bearer as before. + auto isCredentialHeader = [](const std::wstring& lowerName) { + return lowerName == L"authorization" || lowerName == L"x-api-key" || lowerName == L"api-key"; + }; + auto substituteKey = [](const std::wstring& lowerName, const std::wstring& value, const std::wstring& key) { + if (lowerName == L"authorization") { + size_t sp = value.find(L' '); + if (sp != std::wstring::npos) { + // Preserve the client's scheme (e.g. Bearer) when present. + return value.substr(0, sp) + L" " + key; + } + } + return key; + }; + + std::wstring headerString; + std::wstring logHeaderString; // log-safe headers (API key masked unless show-sensitive mode is on) + bool clientSentCredential = false; + for (const auto& [name, value] : headers) { + std::wstring lowerName = Utils::ToLower(name); + if (lowerName == L"host" || lowerName == L"proxy-authorization") continue; + if (lowerName == L"connection" || lowerName == L"keep-alive" || lowerName == L"proxy-connection" || lowerName == L"content-length" || lowerName == L"accept-encoding") continue; + if (isCredentialHeader(lowerName)) { + clientSentCredential = true; + headerString += name + L": " + substituteKey(lowerName, value, apiKey) + L"\r\n"; + logHeaderString += name + L": " + substituteKey(lowerName, value, L"") + L"\r\n"; + continue; + } + headerString += name + L": " + value + L"\r\n"; + logHeaderString += name + L": " + value + L"\r\n"; + } + if (!clientSentCredential) { + headerString += L"Authorization: Bearer " + apiKey + L"\r\n"; + logHeaderString += L"Authorization: Bearer \r\n"; + } + const std::wstring& headersForLog = logManager_->IsShowSensitive() ? headerString : logHeaderString; + + // Log what we're about to send upstream + std::wstring upstreamBodyPreview = Utils::Utf8ToWide(body); + if (upstreamBodyPreview.length() > 50000) upstreamBodyPreview = upstreamBodyPreview.substr(0, 50000) + L"...[truncated]"; + LOG(L"[Upstream] Request: " + wMethod + L" " + fullPath); + LOG(L"[Upstream] Request headers:\n" + headersForLog); + LOG(L"[Upstream] Request body:\n" + upstreamBodyPreview); + LOG_TRAFFIC(L"UPSTREAM_OUT", + wMethod + L" " + fullPath + L"\r\n" + + headersForLog + L"\r\n" + + Utils::Utf8ToWide(body)); + logManager_->AddLog(profileAlias, LogDirection::ProxyToLLM, + L"Upstream request: " + wMethod + L" " + fullPath, + L"=== HEADERS SENT TO UPSTREAM ===\n" + headersForLog + + L"\n=== BODY SENT TO UPSTREAM ===\n" + upstreamBodyPreview); + + struct UpstreamCurlContext { + CURL* curl = nullptr; + int statusCode = 0; + std::vector>* responseHeaders = nullptr; + std::function* onBodyChunk = nullptr; + std::function>& )>* onHeaders = nullptr; + bool abortedByConsumer = false; + size_t totalBytes = 0; + }; + + CURL* curl = curl_easy_init(); + if (!curl) return false; + + UpstreamCurlContext ctx; + ctx.curl = curl; + ctx.responseHeaders = &responseHeaders; + ctx.onBodyChunk = &onBodyChunk; + ctx.onHeaders = &onHeaders; + + const std::string fullUrl = scheme + "://" + hostPort + Utils::WideToUtf8(fullPath.empty() ? L"/" : fullPath); + curl_easy_setopt(curl, CURLOPT_URL, fullUrl.c_str()); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "AgentRedactor/1.0"); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.data()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(body.size())); + // Explicit upstream timeouts so a stalled upstream cannot hang the proxy + // indefinitely (30 s connect; abort when the transfer stalls below + // 1 byte/s for 300 s, matching the WinHTTP receive timeout). + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 30000L); + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1L); + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 300L); + // Automatic gzip/deflate decompression (curl adds its own Accept-Encoding; + // any client-provided one is stripped above, like the WinHTTP branch). + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); + + struct curl_slist* headerList = nullptr; + for (const auto& h : Utils::Split(headerString, L'\n')) { + const std::string narrow = Utils::WideToUtf8(Utils::Trim(h)); + if (!narrow.empty()) headerList = curl_slist_append(headerList, narrow.c_str()); + } + // WinHTTP never sends Expect: 100-continue; suppress curl's automatic one + // so onHeaders fires exactly once with the final response. + headerList = curl_slist_append(headerList, "Expect:"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList); + + auto headerCb = +[](char* buffer, size_t size, size_t nitems, void* userdata) -> size_t { + const size_t len = size * nitems; + auto* c = static_cast(userdata); + std::string line(buffer, len); + while (!line.empty() && (line.back() == '\r' || line.back() == '\n')) line.pop_back(); + if (line.empty()) { + // End of a header block: invoke onHeaders with the final status + // (interim 1xx blocks are skipped). + long status = 0; + curl_easy_getinfo(c->curl, CURLINFO_RESPONSE_CODE, &status); + if (status >= 200) { + c->statusCode = static_cast(status); + if (c->onHeaders && *c->onHeaders) { + (*c->onHeaders)(c->statusCode, *c->responseHeaders); + } + } + return len; + } + const size_t colon = line.find(':'); + if (colon != std::string::npos && colon > 0) { + std::string name = line.substr(0, colon); + std::string value = line.substr(colon + 1); + const size_t start = value.find_first_not_of(" \t"); + if (start != std::string::npos) value = value.substr(start); + c->responseHeaders->push_back({Utils::Utf8ToWide(name), Utils::Utf8ToWide(value)}); + } + return len; + }; + auto writeCb = +[](char* ptr, size_t size, size_t nmemb, void* userdata) -> size_t { + const size_t len = size * nmemb; + auto* c = static_cast(userdata); + if (len > 0 && c->onBodyChunk && *c->onBodyChunk) { + if (!(*c->onBodyChunk)(ptr, len)) { + c->abortedByConsumer = true; + return 0; + } + c->totalBytes += len; + } + return len; + }; + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headerCb); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &ctx); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ctx); + + const CURLcode res = curl_easy_perform(curl); + statusCode = ctx.statusCode; + curl_slist_free_all(headerList); + curl_easy_cleanup(curl); + + if (res != CURLE_OK && !ctx.abortedByConsumer) { + logManager_->AddLog(profileAlias, LogDirection::LLMToProxy, + L"Upstream response FAILED", + L"curl_easy_perform failed: " + Utils::Utf8ToWide(curl_easy_strerror(res))); + return false; + } + if (ctx.abortedByConsumer) { + LOG(L"[Upstream] Streaming aborted by consumer"); + } + + std::wstring headerSummary; + for (const auto& [name, value] : responseHeaders) { + headerSummary += name + L": " + value + L"; "; + } + LOG(L"[Upstream] Response headers: " + headerSummary); + LOG(L"[Upstream] Streamed response: " + std::to_wstring(statusCode) + L" | " + std::to_wstring(ctx.totalBytes) + L" bytes"); + logManager_->AddLog(profileAlias, LogDirection::LLMToProxy, + L"Upstream streaming response: " + std::to_wstring(statusCode) + L" | " + std::to_wstring(ctx.totalBytes) + L" bytes", + L"=== HEADERS FROM UPSTREAM ===\n" + headerSummary); + return true; +#endif } void ProxyEngine::UpdateStats(const ApiKeyProfile& profile, size_t piiCount, size_t regexCount, size_t keywordCount) { diff --git a/core/src/secure_storage_linux.cpp b/core/src/secure_storage_linux.cpp new file mode 100644 index 0000000..97583d0 --- /dev/null +++ b/core/src/secure_storage_linux.cpp @@ -0,0 +1,415 @@ +// Linux SecureStorage: OpenSSL AES-256-GCM + PBKDF2-HMAC-SHA256. +// +// Two modes, mirroring the Windows semantics: +// - Unprotected: fields are encrypted with a random 32-byte machine key +// stored in the libsecret keyring (envelope _mode "machine"; the DPAPI +// counterpart). On headless servers without a keyring the key is derived +// from /etc/machine-id via PBKDF2, which only obscures secrets from other +// users on the same machine — a first-run warning is logged. +// - Protected: a typed master password. A random 32-byte AES session key is +// wrapped with a PBKDF2-HMAC-SHA256 key derived from the password and only +// the wrapped blob is persisted (master_password.password in settings.json). +// The session starts locked; UnlockWithPassword recovers the session key. + +#include "secure_storage.h" +#include "utils.h" +#include "logging.h" + +#ifndef _WIN32 + +#include +#include +#include +#include +#include +#include + +namespace AgentRedactor { + +namespace { + +constexpr uint32_t kPbkdf2Iterations = 600000; +constexpr size_t kSessionKeyBytes = 32; // AES-256 +constexpr size_t kSaltBytes = 16; +constexpr size_t kGcmIvBytes = 12; +constexpr size_t kGcmTagBytes = 16; + +// libsecret schema for the machine key entry. +const SecretSchema* MachineKeySchema() { + static const SecretSchema schema = { + "com.negativestarinnovators.AgentRedactor", SECRET_SCHEMA_NONE, + { + { "app", SECRET_SCHEMA_ATTRIBUTE_STRING }, + { nullptr, SECRET_SCHEMA_ATTRIBUTE_STRING }, + } + }; + return &schema; +} + +} // anonymous namespace + +// ============================================================================ +// Public API +// ============================================================================ + +bool SecureStorage::Initialize(const json& config) { + initialized_ = false; + masterPasswordEnabled_ = false; + helloEnabled_ = false; + aesKey_.clear(); + passwordSalt_.clear(); + passwordIterations_ = 0; + wrappedKey_.clear(); + wrappedKeyIv_.clear(); + wrappedKeyTag_.clear(); + + if (config.contains("enabled") && config["enabled"].get()) { + // Typed-master-password protection. A config written on another + // platform (Windows Hello blob) or a legacy block this build cannot + // unwrap degrades to unprotected so the user can re-enable. + if (config.contains("password") && config["password"].is_object()) { + const auto& pw = config["password"]; + auto salt = Base64Decode(pw.value("salt", "")); + auto wrapped = Base64Decode(pw.value("wrapped_key", "")); + auto iv = Base64Decode(pw.value("iv", "")); + auto tag = Base64Decode(pw.value("tag", "")); + uint32_t iterations = pw.value("iterations", 0u); + if (!salt.empty() && !wrapped.empty() && !iv.empty() && !tag.empty() && iterations > 0) { + masterPasswordEnabled_ = true; + passwordSalt_ = std::move(salt); + passwordIterations_ = iterations; + wrappedKey_ = std::move(wrapped); + wrappedKeyIv_ = std::move(iv); + wrappedKeyTag_ = std::move(tag); + } + } + } + + // The storage is ready to encrypt/decrypt in the current mode (machine + // key when unprotected; a protected session stays locked until + // UnlockWithPassword). Sensitive fields stay encrypted until unlocked. + initialized_ = !masterPasswordEnabled_; + return true; +} + +json SecureStorage::Encrypt(const std::wstring& plaintext) const { + if (!initialized_) { + return json{{"_enc", ""}, {"_mode", "none"}}; + } + + if (plaintext.empty()) { + return json{{"_enc", ""}, {"_mode", masterPasswordEnabled_ ? "aes" : "machine"}}; + } + + std::string utf8Plain = Utils::WideToUtf8(plaintext); + std::vector plainBytes(utf8Plain.begin(), utf8Plain.end()); + + std::vector key; + std::string mode; + if (masterPasswordEnabled_) { + key = aesKey_; + mode = "aes"; + } else { + auto machineKey = MachineKey(); + if (!machineKey) { + return json{{"_enc", ""}, {"_mode", "machine"}}; + } + key = std::move(*machineKey); + mode = "machine"; + } + + std::vector ciphertext, iv, tag; + if (!AesGcmEncrypt(plainBytes, key, ciphertext, iv, tag)) { + return json{{"_enc", ""}, {"_mode", mode}}; + } + return json{ + {"_enc", Base64Encode(ciphertext)}, + {"_mode", mode}, + {"_iv", Base64Encode(iv)}, + {"_tag", Base64Encode(tag)} + }; +} + +std::optional SecureStorage::Decrypt(const json& fieldJson) const { + if (!initialized_) { + return std::nullopt; + } + + if (!fieldJson.is_object() || !fieldJson.contains("_enc")) { + return std::nullopt; + } + + std::string mode = fieldJson.value("_mode", "machine"); + std::vector ciphertext = Base64Decode(fieldJson.value("_enc", "")); + if (ciphertext.empty()) { + return L""; + } + + std::vector key; + if (mode == "aes") { + key = aesKey_; + } else { + auto machineKey = MachineKey(); + if (!machineKey) return std::nullopt; + key = std::move(*machineKey); + } + + std::vector iv = Base64Decode(fieldJson.value("_iv", "")); + std::vector tag = Base64Decode(fieldJson.value("_tag", "")); + auto decrypted = AesGcmDecrypt(ciphertext, key, iv, tag); + if (!decrypted) return std::nullopt; + std::string utf8Result(decrypted->begin(), decrypted->end()); + return Utils::Utf8ToWide(utf8Result); +} + +json SecureStorage::GetConfig() const { + json config; + config["enabled"] = masterPasswordEnabled_; + if (masterPasswordEnabled_) { + config["password"] = json{ + {"salt", Base64Encode(passwordSalt_)}, + {"iterations", passwordIterations_}, + {"wrapped_key", Base64Encode(wrappedKey_)}, + {"iv", Base64Encode(wrappedKeyIv_)}, + {"tag", Base64Encode(wrappedKeyTag_)}, + }; + } + return config; +} + +bool SecureStorage::EnableMasterPassword(const std::wstring& password) { + if (masterPasswordEnabled_) return false; + if (password.empty()) return false; + + auto sessionKey = GenerateRandomBytes(kSessionKeyBytes); + auto salt = GenerateRandomBytes(kSaltBytes); + if (sessionKey.size() != kSessionKeyBytes || salt.size() != kSaltBytes) return false; + + auto kek = Pbkdf2(Utils::WideToUtf8(password), salt, kPbkdf2Iterations); + std::vector wrapped, iv, tag; + if (!AesGcmEncrypt(sessionKey, kek, wrapped, iv, tag)) return false; + + aesKey_ = sessionKey; + passwordSalt_ = salt; + passwordIterations_ = kPbkdf2Iterations; + wrappedKey_ = std::move(wrapped); + wrappedKeyIv_ = std::move(iv); + wrappedKeyTag_ = std::move(tag); + masterPasswordEnabled_ = true; + initialized_ = true; + return true; +} + +void SecureStorage::DisableMasterPassword() { + masterPasswordEnabled_ = false; + initialized_ = true; + aesKey_.clear(); + passwordSalt_.clear(); + passwordIterations_ = 0; + wrappedKey_.clear(); + wrappedKeyIv_.clear(); + wrappedKeyTag_.clear(); +} + +bool SecureStorage::UnlockWithPassword(const std::wstring& password) { + if (!masterPasswordEnabled_ || passwordSalt_.empty() || wrappedKey_.empty()) return false; + auto kek = Pbkdf2(Utils::WideToUtf8(password), passwordSalt_, passwordIterations_); + auto sessionKey = AesGcmDecrypt(wrappedKey_, kek, wrappedKeyIv_, wrappedKeyTag_); + if (!sessionKey || sessionKey->size() != kSessionKeyBytes) return false; + aesKey_ = *sessionKey; + initialized_ = true; + return true; +} + +void SecureStorage::Lock() { + initialized_ = false; +} + +// ============================================================================ +// Machine key (unprotected at-rest encryption) +// ============================================================================ + +std::optional> SecureStorage::MachineKey() { + // Cached: keyring round-trips on every field encrypt/decrypt would be + // needlessly slow. + static std::optional> cached; + static bool warnedFallback = false; + if (cached) return cached; + + GError* error = nullptr; + gchar* stored = secret_password_lookup_sync(MachineKeySchema(), nullptr, &error, + "app", "agentredactor", nullptr); + if (stored) { + auto key = Base64Decode(stored); + secret_password_free(stored); + if (key.size() == kSessionKeyBytes) { + cached = key; + return cached; + } + } + if (error) { + g_error_free(error); + error = nullptr; + } + + auto key = GenerateRandomBytes(kSessionKeyBytes); + if (key.size() == kSessionKeyBytes) { + const std::string encoded = Base64Encode(key); + if (secret_password_store_sync(MachineKeySchema(), SECRET_COLLECTION_DEFAULT, + "Agent Redactor machine key", encoded.c_str(), nullptr, &error, + "app", "agentredactor", nullptr)) { + cached = key; + return cached; + } + if (error) { + g_error_free(error); + error = nullptr; + } + } + + // No keyring (headless server): derive a stable key from the machine id. + // This only protects secrets from other local users; anyone reading both + // /etc/machine-id and settings.json can unwrap them. + if (!warnedFallback) { + warnedFallback = true; + LOG_LIFECYCLE(L"[SecureStorage] No secret keyring available; deriving the at-rest key " + L"from the machine id. Secrets are only obfuscated, not protected. " + L"Install gnome-keyring (or another Secret Service provider) for stronger storage."); + } + std::ifstream machineId("/etc/machine-id", std::ios::binary); + if (!machineId) return std::nullopt; + std::string id((std::istreambuf_iterator(machineId)), std::istreambuf_iterator()); + if (id.empty()) return std::nullopt; + static const std::vector kMachineSalt = { + 'a','g','e','n','t','r','e','d','a','c','t','o','r','-','m','k' + }; + auto derived = Pbkdf2(id, kMachineSalt, 10000); + if (derived.size() != kSessionKeyBytes) return std::nullopt; + cached = derived; + return cached; +} + +// ============================================================================ +// PBKDF2-HMAC-SHA256 (OpenSSL) +// ============================================================================ + +std::vector SecureStorage::Pbkdf2(const std::string& password, const std::vector& salt, uint32_t iterations) { + std::vector out(kSessionKeyBytes); + if (PKCS5_PBKDF2_HMAC(password.data(), static_cast(password.size()), + salt.data(), static_cast(salt.size()), + static_cast(iterations), EVP_sha256(), + static_cast(out.size()), out.data()) != 1) { + return {}; + } + return out; +} + +// ============================================================================ +// AES-256-GCM (OpenSSL EVP) +// ============================================================================ + +bool SecureStorage::AesGcmEncrypt(const std::vector& plaintext, const std::vector& key, + std::vector& ciphertext, std::vector& iv, std::vector& tag) { + if (key.size() != kSessionKeyBytes) return false; + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return false; + bool ok = false; + do { + if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) != 1) break; + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, kGcmIvBytes, nullptr) != 1) break; + iv = GenerateRandomBytes(kGcmIvBytes); + if (iv.size() != kGcmIvBytes) break; + if (EVP_EncryptInit_ex(ctx, nullptr, nullptr, key.data(), iv.data()) != 1) break; + + ciphertext.resize(plaintext.size()); + int len = 0; + if (EVP_EncryptUpdate(ctx, ciphertext.data(), &len, + plaintext.data(), static_cast(plaintext.size())) != 1) break; + int total = len; + if (EVP_EncryptFinal_ex(ctx, ciphertext.data() + total, &len) != 1) break; + total += len; + ciphertext.resize(static_cast(total)); + + tag.resize(kGcmTagBytes); + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, kGcmTagBytes, tag.data()) != 1) break; + ok = true; + } while (false); + EVP_CIPHER_CTX_free(ctx); + return ok; +} + +std::optional> SecureStorage::AesGcmDecrypt(const std::vector& ciphertext, + const std::vector& key, const std::vector& iv, const std::vector& tag) { + if (key.size() != kSessionKeyBytes || iv.empty() || tag.size() != kGcmTagBytes) return std::nullopt; + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return std::nullopt; + std::optional> result; + do { + if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) != 1) break; + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, static_cast(iv.size()), nullptr) != 1) break; + if (EVP_DecryptInit_ex(ctx, nullptr, nullptr, key.data(), iv.data()) != 1) break; + + std::vector plaintext(ciphertext.size()); + int len = 0; + if (EVP_DecryptUpdate(ctx, plaintext.data(), &len, + ciphertext.data(), static_cast(ciphertext.size())) != 1) break; + int total = len; + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, static_cast(tag.size()), + const_cast(tag.data())) != 1) break; + // A wrong key/tag fails here (GCM authentication). + if (EVP_DecryptFinal_ex(ctx, plaintext.data() + total, &len) != 1) break; + total += len; + plaintext.resize(static_cast(total)); + result = std::move(plaintext); + } while (false); + EVP_CIPHER_CTX_free(ctx); + return result; +} + +// ============================================================================ +// Random bytes (OpenSSL RAND) +// ============================================================================ + +std::vector SecureStorage::GenerateRandomBytes(size_t count) { + std::vector result(count); + if (count == 0) return result; + if (RAND_bytes(result.data(), static_cast(result.size())) != 1) { + result.clear(); + } + return result; +} + +// ============================================================================ +// Base64 (OpenSSL EVP) +// ============================================================================ + +std::string SecureStorage::Base64Encode(const std::vector& data) { + if (data.empty()) return ""; + std::string result(4 * ((data.size() + 2) / 3), '\0'); + int len = EVP_EncodeBlock(reinterpret_cast(result.data()), + data.data(), static_cast(data.size())); + if (len < 0) return ""; + result.resize(static_cast(len)); + return result; +} + +std::vector SecureStorage::Base64Decode(const std::string& str) { + if (str.empty()) return {}; + std::vector result(3 * str.size() / 4 + 1); + int len = EVP_DecodeBlock(result.data(), + reinterpret_cast(str.data()), static_cast(str.size())); + if (len < 0) return {}; + size_t out = static_cast(len); + // EVP_DecodeBlock counts padding bytes; strip them. + size_t pad = 0; + if (!str.empty() && str.back() == '=') ++pad; + if (str.size() > 1 && str[str.size() - 2] == '=') ++pad; + result.resize(out >= pad ? out - pad : 0); + return result; +} + +} // namespace AgentRedactor + +#endif // !_WIN32 diff --git a/core/src/settings_manager.cpp b/core/src/settings_manager.cpp index c7e0a5c..d425a27 100644 --- a/core/src/settings_manager.cpp +++ b/core/src/settings_manager.cpp @@ -170,6 +170,7 @@ bool SettingsManager::IsHelloEnabled() const { return secureStorage_.IsHelloEnabled(); } +#ifdef _WIN32 bool SettingsManager::EnableMasterPassword() { std::unique_lock lock(mutex_); if (!secureStorage_.EnableMasterPassword()) return false; @@ -177,6 +178,15 @@ bool SettingsManager::EnableMasterPassword() { SaveSettings(); return true; } +#else +bool SettingsManager::EnableMasterPassword(const std::wstring& password) { + std::unique_lock lock(mutex_); + if (!secureStorage_.EnableMasterPassword(password)) return false; + settings_["master_password"] = secureStorage_.GetConfig(); + SaveSettings(); + return true; +} +#endif void SettingsManager::DisableMasterPassword() { std::unique_lock lock(mutex_); @@ -185,6 +195,7 @@ void SettingsManager::DisableMasterPassword() { SaveSettings(); } +#ifdef _WIN32 bool SettingsManager::UnlockWithHello() { std::unique_lock lock(mutex_); if (!secureStorage_.IsMasterPasswordEnabled()) return true; @@ -192,6 +203,15 @@ bool SettingsManager::UnlockWithHello() { DecryptSensitiveFields(); return true; } +#else +bool SettingsManager::UnlockWithPassword(const std::wstring& password) { + std::unique_lock lock(mutex_); + if (!secureStorage_.IsMasterPasswordEnabled()) return true; + if (!secureStorage_.UnlockWithPassword(password)) return false; + DecryptSensitiveFields(); + return true; +} +#endif void SettingsManager::Lock() { std::unique_lock lock(mutex_); diff --git a/core/src/utils.cpp b/core/src/utils.cpp index a63ccba..d4a6a66 100644 --- a/core/src/utils.cpp +++ b/core/src/utils.cpp @@ -1,7 +1,15 @@ #include "utils.h" +#ifdef _WIN32 #include #include #include +#else +#include +#include +#include +#include +#include +#endif #include #include #include @@ -17,10 +25,10 @@ namespace AgentRedactor { namespace Utils { -static std::wstring g_logFilePath; +static std::filesystem::path g_logFilePath; static std::mutex g_logMutex; -static std::wstring g_debugTrafficLogFilePath; +static std::filesystem::path g_debugTrafficLogFilePath; static std::mutex g_debugTrafficLogMutex; // Runtime gate for LOG/LOGF/LOG_TRAFFIC. LogManager is the single source of @@ -43,8 +51,8 @@ void InitializeLogging(const std::filesystem::path& logDirOverride) { auto sessionsDir = logDir / L"sessions"; g_logFilePath = logDir / L"agent_redactor.log"; - CreateDirectoryW(logDir.c_str(), nullptr); - CreateDirectoryW(sessionsDir.c_str(), nullptr); + CreateDirectoryRecursive(logDir); + CreateDirectoryRecursive(sessionsDir); // Rotate previous session log if it exists and has content try { @@ -88,7 +96,7 @@ void InitializeLogging(const std::filesystem::path& logDirOverride) { void InitializeDebugTrafficLogging(const std::filesystem::path& logDirOverride) { auto logDir = logDirOverride.empty() ? GetAppDataPath() : logDirOverride; g_debugTrafficLogFilePath = logDir / L"agent_redactor_debug.log"; - CreateDirectoryW(logDir.c_str(), nullptr); + CreateDirectoryRecursive(logDir); } void LogTrafficMessage(const std::wstring& direction, const std::wstring& message) { @@ -139,20 +147,47 @@ void LogLifecycleMessage(const std::wstring& message) { std::wstring Utf8ToWide(const std::string& utf8) { if (utf8.empty()) return L""; +#ifdef _WIN32 int size_needed = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0); if (size_needed <= 0) return L""; std::wstring result(size_needed - 1, L'\0'); MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, result.data(), size_needed); return result; +#else + // wchar_t is 32-bit on Linux; wstrings stay opaque UTF-16-ish containers + // (supplementary characters become surrogate pairs), exactly matching the + // Windows representation the rest of the core assumes. + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdeprecated-declarations" + try { + std::wstring_convert> conv; + return conv.from_bytes(utf8); + } catch (...) { + return L""; + } + #pragma GCC diagnostic pop +#endif } std::string WideToUtf8(const std::wstring& wide) { if (wide.empty()) return ""; +#ifdef _WIN32 int size_needed = WideCharToMultiByte(CP_UTF8, 0, wide.c_str(), -1, nullptr, 0, nullptr, nullptr); if (size_needed <= 0) return ""; std::string result(size_needed - 1, '\0'); WideCharToMultiByte(CP_UTF8, 0, wide.c_str(), -1, result.data(), size_needed, nullptr, nullptr); return result; +#else + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdeprecated-declarations" + try { + std::wstring_convert> conv; + return conv.to_bytes(wide); + } catch (...) { + return ""; + } + #pragma GCC diagnostic pop +#endif } std::wstring ToLower(const std::wstring& str) { @@ -292,11 +327,22 @@ std::filesystem::path GetAppDataPath() { overrideDir && *overrideDir) { return std::filesystem::path(overrideDir); } +#ifdef _WIN32 wchar_t path[MAX_PATH]; if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr, 0, path))) { return std::filesystem::path(path) / L"AgentRedactor"; } return std::filesystem::path(L"C:\\AgentRedactor"); +#else + // XDG: $XDG_CONFIG_HOME/agentredactor, defaulting to ~/.config/agentredactor. + if (const char* xdg = std::getenv("XDG_CONFIG_HOME"); xdg && *xdg) { + return std::filesystem::path(xdg) / "agentredactor"; + } + if (const char* home = std::getenv("HOME"); home && *home) { + return std::filesystem::path(home) / ".config" / "agentredactor"; + } + return std::filesystem::path("/tmp/agentredactor"); +#endif } std::filesystem::path GetCurrentLogFilePath() { @@ -307,9 +353,17 @@ std::filesystem::path GetCurrentLogFilePath() { } std::filesystem::path GetExecutablePath() { +#ifdef _WIN32 wchar_t path[MAX_PATH]; GetModuleFileNameW(nullptr, path, MAX_PATH); return std::filesystem::path(path).parent_path(); +#else + char path[4096]; + ssize_t len = ::readlink("/proc/self/exe", path, sizeof(path) - 1); + if (len <= 0) return std::filesystem::current_path(); + path[len] = '\0'; + return std::filesystem::path(path).parent_path(); +#endif } std::wstring GetCurrentMonth() { @@ -326,6 +380,7 @@ int64_t GetCurrentTimestamp() { } std::wstring GenerateUUID() { +#ifdef _WIN32 UUID uuid = {}; RPC_STATUS status = UuidCreate(&uuid); if (status != RPC_S_OK) { @@ -336,6 +391,25 @@ std::wstring GenerateUUID() { << std::setw(8) << uuid.Data1 << L'-' << std::setw(4) << uuid.Data2 << L'-' << std::setw(4) << uuid.Data3 << L'-'; for (int i = 0; i < 8; ++i) oss << std::setw(2) << static_cast(uuid.Data4[i]); return oss.str(); +#else + // RFC 4122 v4 UUID from OS randomness. + try { + unsigned char b[16]; + std::random_device rd; + for (auto& byte : b) byte = static_cast(rd()); + b[6] = (b[6] & 0x0F) | 0x40; + b[8] = (b[8] & 0x3F) | 0x80; + std::wostringstream oss; + oss << std::hex << std::setfill(L'0'); + for (int i = 0; i < 16; ++i) { + if (i == 4 || i == 6 || i == 8 || i == 10) oss << L'-'; + oss << std::setw(2) << static_cast(b[i]); + } + return oss.str(); + } catch (...) { + return L"uuid_" + std::to_wstring(std::chrono::steady_clock::now().time_since_epoch().count()); + } +#endif } std::wstring FormatSize(size_t size) { @@ -373,6 +447,7 @@ double ParseLocalizedFloat(const std::wstring& text) { return _wtof(normalized.c_str()); } +#ifdef _WIN32 static SYSTEMTIME TmToSystemTime(const struct tm& timeinfo) { SYSTEMTIME st = {}; st.wYear = static_cast(timeinfo.tm_year + 1900); @@ -384,11 +459,13 @@ static SYSTEMTIME TmToSystemTime(const struct tm& timeinfo) { st.wMilliseconds = 0; return st; } +#endif std::wstring FormatLocalizedTime(const std::time_t& time) { std::wstring buffer(64, L'\0'); struct tm timeinfo; localtime_s(&timeinfo, &time); +#ifdef _WIN32 SYSTEMTIME st = TmToSystemTime(timeinfo); int len = GetTimeFormatEx(LOCALE_NAME_USER_DEFAULT, TIME_NOSECONDS, &st, nullptr, buffer.data(), static_cast(buffer.size())); if (len > 0) { @@ -397,6 +474,10 @@ std::wstring FormatLocalizedTime(const std::time_t& time) { wcsftime(buffer.data(), buffer.size(), L"%H:%M:%S", &timeinfo); buffer.resize(wcslen(buffer.c_str())); } +#else + wcsftime(buffer.data(), buffer.size(), L"%H:%M", &timeinfo); + buffer.resize(wcslen(buffer.c_str())); +#endif return buffer; } @@ -404,6 +485,7 @@ std::wstring FormatLocalizedDateTime(const std::time_t& time) { std::wstring buffer(128, L'\0'); struct tm timeinfo; localtime_s(&timeinfo, &time); +#ifdef _WIN32 SYSTEMTIME st = TmToSystemTime(timeinfo); int len = GetDateFormatEx(LOCALE_NAME_USER_DEFAULT, DATE_SHORTDATE, &st, nullptr, buffer.data(), static_cast(buffer.size()), nullptr); if (len > 0) { @@ -412,10 +494,15 @@ std::wstring FormatLocalizedDateTime(const std::time_t& time) { wcsftime(buffer.data(), buffer.size(), L"%Y-%m-%d", &timeinfo); buffer.resize(wcslen(buffer.c_str())); } +#else + wcsftime(buffer.data(), buffer.size(), L"%Y-%m-%d", &timeinfo); + buffer.resize(wcslen(buffer.c_str())); +#endif std::wstring timeStr = FormatLocalizedTime(time); return buffer + L" " + timeStr; } +#ifdef _WIN32 namespace { struct WinHttpHandle { @@ -717,5 +804,330 @@ bool HttpDownloadFileSegmented(const std::wstring& url, const std::filesystem::p return true; } +#else // POSIX: libcurl implementations of the same three helpers + +namespace { + +// Result of the headers callback: fail the request, continue receiving the +// body, or stop here successfully (headers-only probe — mirrors the WinHTTP +// code closing the request handle right after the consume callback returns). +enum class HeadersAction { Fail, Proceed, HeadersOnly }; + +struct CurlGetContext { + CURL* curl = nullptr; + std::function onHeaders; + std::function onData; + std::string location; + HeadersAction action = HeadersAction::Proceed; +}; + +static bool AsciiStartsWithNoCase(const std::string& s, const char* prefix) { + for (size_t i = 0; prefix[i]; ++i) { + if (i >= s.size() || tolower((unsigned char)s[i]) != tolower((unsigned char)prefix[i])) return false; + } + return true; +} + +size_t CurlHeaderCallback(char* buffer, size_t size, size_t nitems, void* userdata) { + const size_t len = size * nitems; + auto* ctx = static_cast(userdata); + const std::string line(buffer, len); + if (AsciiStartsWithNoCase(line, "Location:")) { + std::string value = line.substr(9); + value.erase(0, value.find_first_not_of(" \t\r\n")); + value.erase(value.find_last_not_of(" \t\r\n") + 1); + ctx->location = value; + return len; + } + if (line == "\r\n" || line == "\n") { + // End of the header block. Redirect responses are handled by the + // caller after perform; only invoke onHeaders for the final status. + long status = 0; + curl_off_t contentLength = -1; + curl_easy_getinfo(ctx->curl, CURLINFO_RESPONSE_CODE, &status); + if (status >= 300 && status < 400) return len; + curl_easy_getinfo(ctx->curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &contentLength); + ctx->action = ctx->onHeaders(status, contentLength > 0 ? static_cast(contentLength) : 0); + if (ctx->action != HeadersAction::Proceed) return 0; // abort transfer + } + return len; +} + +size_t CurlWriteCallback(char* ptr, size_t size, size_t nmemb, void* userdata) { + const size_t len = size * nmemb; + auto* ctx = static_cast(userdata); + return ctx->onData(ptr, len) ? len : 0; +} + +// Performs a GET against `url`, following redirects manually (the default +// auto policy is disabled so cross-host chains like worker -> github.com -> +// release asset CDN are explicit). `extraHeaders` (e.g. a Range header) is +// re-sent on every hop of the redirect chain. onHeaders decides whether the +// body is consumed; onData feeds body bytes. Never throws. +bool CurlGet(const std::wstring& url, const std::wstring& extraHeaders, + const std::function& onHeaders, + const std::function& onData) { + std::string current = WideToUtf8(url); + for (int redirect = 0; redirect < 5; ++redirect) { + CURL* curl = curl_easy_init(); + if (!curl) return false; + CurlGetContext ctx; + ctx.curl = curl; + ctx.onHeaders = onHeaders; + ctx.onData = onData; + curl_easy_setopt(curl, CURLOPT_URL, current.c_str()); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "AgentRedactor/1.0"); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); + // WinHTTP-equivalent timeouts: 30 s connect; abort when the transfer + // stalls below 1 byte/s for 300 s (receive timeout for large files). + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 30000L); + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1L); + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 300L); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, CurlHeaderCallback); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &ctx); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ctx); + struct curl_slist* headerList = nullptr; + if (!extraHeaders.empty()) { + for (const auto& h : Split(extraHeaders, L'\n')) { + const std::string narrow = WideToUtf8(Trim(h)); + if (!narrow.empty()) headerList = curl_slist_append(headerList, narrow.c_str()); + } + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList); + } + CURLcode res = curl_easy_perform(curl); + long status = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); + curl_slist_free_all(headerList); + curl_easy_cleanup(curl); + + if (ctx.action == HeadersAction::Fail) return false; + if (ctx.action == HeadersAction::HeadersOnly) return true; + if (status == 301 || status == 302 || status == 303 || status == 307 || status == 308) { + if (ctx.location.empty()) return false; + current = ctx.location; + continue; + } + if (status != 200 && status != 206) return false; + return res == CURLE_OK; + } + return false; +} + +} // anonymous namespace + +bool HttpGetString(const std::wstring& url, std::string& outBody) { + outBody.clear(); + return CurlGet(url, L"", + [](long status, uint64_t) { + return (status == 200 || status == 206) ? HeadersAction::Proceed : HeadersAction::Fail; + }, + [&outBody](const char* data, size_t len) { + outBody.append(data, len); + return true; + }); +} + +bool HttpDownloadFile(const std::wstring& url, const std::filesystem::path& destPath, + const std::function& progress) { + // Resume from an existing partial file when the server honors ranges. + uint64_t existing = 0; + { + std::error_code ec; + auto size = std::filesystem::file_size(destPath, ec); + if (!ec) existing = size; + } + std::wstring rangeHeader; + if (existing > 0) rangeHeader = L"Range: bytes=" + std::to_wstring(existing) + L"-\r\n"; + + struct State { + std::ofstream file; + uint64_t total = 0; + uint64_t downloaded = 0; + bool resuming = false; + } st; + const bool transportOk = CurlGet(url, rangeHeader, + [&](long status, uint64_t contentLength) { + if (status != 200 && status != 206) return HeadersAction::Fail; + // 206: the server honored the Range request, append. 200: it + // answered the whole file instead — discard the partial and + // restart from zero. + st.resuming = (existing > 0 && status == 206); + st.file.open(destPath, std::ios::binary | (st.resuming ? std::ios::app : std::ios::trunc)); + if (!st.file) return HeadersAction::Fail; + st.total = st.resuming ? existing + contentLength : contentLength; + st.downloaded = st.resuming ? existing : 0; + return HeadersAction::Proceed; + }, + [&](const char* data, size_t len) { + st.file.write(data, static_cast(len)); + st.downloaded += len; + if (progress) progress(st.downloaded, st.total); + return st.file.good(); + }); + if (st.file.is_open()) st.file.flush(); + // A cleanly closed connection ends the transfer early; a short file is a + // failed download, not a success. + return transportOk && st.file.good() && (st.total == 0 || st.downloaded == st.total); +} + +bool HttpDownloadFileSegmented(const std::wstring& url, const std::filesystem::path& destPath, + const std::function& progress, + size_t maxSegments) { + constexpr uint64_t kMinSegmentedBytes = 64ull * 1024 * 1024; + + // Probe the total size and range support before committing to segments. + uint64_t totalSize = 0; + CurlGet(url, L"", + [&](long status, uint64_t contentLength) { + if (status != 200 && status != 206) return HeadersAction::Fail; + totalSize = contentLength; + return HeadersAction::HeadersOnly; + }, + [](const char*, size_t) { return true; }); + bool rangesSupported = false; + if (totalSize > 0) { + CurlGet(url, L"Range: bytes=0-0\r\n", + [&](long status, uint64_t) { + rangesSupported = (status == 206); + return HeadersAction::HeadersOnly; + }, + [](const char*, size_t) { return true; }); + } + + size_t segmentCount = static_cast(std::min(maxSegments, totalSize / kMinSegmentedBytes)); + if (!rangesSupported || segmentCount < 2) { + LOGF_LIFECYCLE(L"[Utils] Segmented download: single-stream fallback for %s (ranges %s, size %llu)", + url.c_str(), rangesSupported ? L"supported" : L"unsupported", + static_cast(totalSize)); + return HttpDownloadFile(url, destPath, progress); + } + + LOGF_LIFECYCLE(L"[Utils] Segmented download: %llu bytes in %zu segments from %s", + static_cast(totalSize), segmentCount, url.c_str()); + + const uint64_t segmentSize = totalSize / segmentCount; + std::vector partPaths(segmentCount); + for (size_t i = 0; i < segmentCount; ++i) { + partPaths[i] = destPath; + partPaths[i] += L".part" + std::to_wstring(i); + } + + std::atomic totalDownloaded{ 0 }; + std::mutex progressMutex; + auto reportProgress = [&](uint64_t downloaded) { + if (progress) { + std::lock_guard lock(progressMutex); + progress(downloaded, totalSize); + } + }; + + // One thread per segment, each on its own curl connection with an + // explicit Range header (redirects are followed per segment, like + // CurlGet does for single-stream downloads). + std::vector results(segmentCount, 0); + std::vector threads; + threads.reserve(segmentCount); + try { + for (size_t i = 0; i < segmentCount; ++i) { + threads.emplace_back([&, i] { + try { + const uint64_t begin = i * segmentSize; + const uint64_t end = (i + 1 == segmentCount) ? totalSize - 1 : begin + segmentSize - 1; + const uint64_t expected = end - begin + 1; + const auto& partPath = partPaths[i]; + + // Resume a partially downloaded segment. A complete part + // needs no request; an oversized one is corrupt. + uint64_t existing = 0; + { + std::error_code ec; + auto size = std::filesystem::file_size(partPath, ec); + if (!ec) existing = size; + } + if (existing == expected) { + reportProgress(totalDownloaded.fetch_add(expected) + expected); + results[i] = 1; + return; + } + if (existing > expected) { + std::error_code ec; + std::filesystem::remove(partPath, ec); + existing = 0; + } + + totalDownloaded.fetch_add(existing); + const std::wstring rangeHeader = L"Range: bytes=" + std::to_wstring(begin + existing) + + L"-" + std::to_wstring(end) + L"\r\n"; + uint64_t have = existing; + std::ofstream file; + results[i] = CurlGet(url, rangeHeader, + [&](long status, uint64_t) { + if (status != 206) return HeadersAction::Fail; // server ignored the Range header + file.open(partPath, std::ios::binary | (existing > 0 ? std::ios::app : std::ios::trunc)); + return file ? HeadersAction::Proceed : HeadersAction::Fail; + }, + [&](const char* data, size_t len) { + if (have >= expected) return false; + const size_t chunk = static_cast(std::min(len, expected - have)); + file.write(data, static_cast(chunk)); + have += chunk; + reportProgress(totalDownloaded.fetch_add(chunk) + chunk); + return file.good() && have <= expected; + }) ? 1 : 0; + file.flush(); + if (results[i] && (!file.good() || have != expected)) results[i] = 0; + } catch (...) { + results[i] = 0; + } + }); + } + } catch (...) { + for (auto& t : threads) if (t.joinable()) t.join(); + return false; + } + for (auto& t : threads) t.join(); + for (size_t i = 0; i < segmentCount; ++i) { + if (!results[i]) { + // Part files are kept so the next retry resumes each segment. + LOGF_LIFECYCLE(L"[Utils] Segmented download: segment %zu failed for %s", i, url.c_str()); + return false; + } + } + + // Concatenate the segments in order and verify the final size. On any + // failure the part files survive for the next retry. + { + std::ofstream out(destPath, std::ios::binary | std::ios::trunc); + if (!out) return false; + std::vector buffer(1024 * 1024); + for (size_t i = 0; i < segmentCount; ++i) { + std::ifstream in(partPaths[i], std::ios::binary); + if (!in) return false; + while (in) { + in.read(buffer.data(), static_cast(buffer.size())); + auto got = in.gcount(); + if (got > 0) out.write(buffer.data(), got); + } + } + out.flush(); + if (!out.good()) return false; + } + std::error_code ec; + auto finalSize = std::filesystem::file_size(destPath, ec); + if (ec || finalSize != totalSize) { + LOGF_LIFECYCLE(L"[Utils] Segmented download: size mismatch after concat for %s", url.c_str()); + std::filesystem::remove(destPath, ec); + return false; + } + for (const auto& partPath : partPaths) std::filesystem::remove(partPath, ec); + reportProgress(totalSize); + LOG_LIFECYCLE(L"[Utils] Segmented download complete"); + return true; +} + +#endif // _WIN32 + } // namespace Utils } // namespace AgentRedactor diff --git a/linux/engine/CMakeLists.txt b/linux/engine/CMakeLists.txt new file mode 100644 index 0000000..7bbd1f7 --- /dev/null +++ b/linux/engine/CMakeLists.txt @@ -0,0 +1,3 @@ +# Linux engine/CLI binary (Phase 2) and development smoke targets. +add_executable(core-smoke core_smoke.cpp) +target_link_libraries(core-smoke PRIVATE agentredactor-core) diff --git a/linux/engine/core_smoke.cpp b/linux/engine/core_smoke.cpp new file mode 100644 index 0000000..2941bf9 --- /dev/null +++ b/linux/engine/core_smoke.cpp @@ -0,0 +1,28 @@ +// Phase 1 smoke test: links against the core static library and constructs +// SettingsManager + RegexEngine against a temp config dir. Not shipped — +// built only via `cmake --build build --target core-smoke`. +#include "settings_manager.h" +#include "regex_engine.h" +#include "utils.h" +#include + +int main() { + using namespace AgentRedactor; + SettingsManager settings; // honors AGENTREDACTOR_CONFIG_DIR + RegexEngine regex; + RegexEntry entry; + entry.pattern = L"smoke-[0-9]+"; + regex.SetPatterns({entry}); + auto [redacted, matches] = regex.Redact(std::wstring(L"token smoke-42 here")); + if (matches.empty()) { + std::fprintf(stderr, "smoke: regex redaction matched nothing\n"); + return 1; + } + if (!Utils::FileExists(Utils::GetAppDataPath() / L"settings.json")) { + std::fprintf(stderr, "smoke: settings.json not created\n"); + return 1; + } + std::fprintf(stderr, "smoke: OK (config dir: %s)\n", + Utils::WideToUtf8(Utils::GetAppDataPath().wstring()).c_str()); + return 0; +} diff --git a/windows/AgentRedactor.vcxproj b/windows/AgentRedactor.vcxproj index 8f9b202..3646661 100644 --- a/windows/AgentRedactor.vcxproj +++ b/windows/AgentRedactor.vcxproj @@ -218,7 +218,7 @@ - + From 06fdfb0fbfae6f13869a4e57b34dc439de633765 Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Tue, 18 Aug 2026 10:38:11 +0000 Subject: [PATCH 03/20] feat(linux): add Linux engine + CLI binary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move EngineApp into core (engine_app.h/engine_app.cpp) shared by both platforms; Windows vcxproj and main.cpp include paths updated. - Linux engine main: SIGPIPE ignore, flock-based single-instance lock, config dir chmod 700, --console mode, termios no-echo password reads. - Linux control API client over libcurl (control.json bearer auth). - CLI gains typed master-password flow on Linux: 'password enable' prompts twice, gated commands prompt once, /unlock accepts {"password": ...} (Linux-only control API extension; Windows Hello path unchanged). - fix(http_server): shutdown listen socket before joining listener thread in Stop() — closing first raced with select() and aborted on glibc (FD_SET bit out of range) when restarting listeners; latent on all platforms. - engine version reported from windows/version.txt via global AR_VERSION_STRING compile definition. --- core/CMakeLists.txt | 1 + core/include/cli.h | 9 + .../EngineApp.h => core/include/engine_app.h | 14 +- core/include/platform_compat.h | 1 + core/src/cli.cpp | 81 +++++++-- .../EngineApp.cpp => core/src/engine_app.cpp | 88 ++++++++-- core/src/http_server.cpp | 11 +- linux/CMakeLists.txt | 1 + linux/engine/CMakeLists.txt | 18 +- linux/engine/control_api_client.cpp | 126 ++++++++++++++ linux/engine/control_api_client.h | 38 +++++ linux/engine/engine_loc.cpp | 59 +++++++ linux/engine/main.cpp | 158 ++++++++++++++++++ windows/AgentRedactorEngine.vcxproj | 2 +- windows/engine/main.cpp | 2 +- 15 files changed, 569 insertions(+), 40 deletions(-) rename windows/engine/EngineApp.h => core/include/engine_app.h (90%) rename windows/engine/EngineApp.cpp => core/src/engine_app.cpp (95%) create mode 100644 linux/engine/control_api_client.cpp create mode 100644 linux/engine/control_api_client.h create mode 100644 linux/engine/engine_loc.cpp create mode 100644 linux/engine/main.cpp diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index d99ed31..73dd25a 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -27,6 +27,7 @@ set(AR_CORE_SOURCES src/bpe_tokenizer.cpp src/cli.cpp src/control_server.cpp + src/engine_app.cpp src/http_server.cpp src/keyword_engine.cpp src/log_manager.cpp diff --git a/core/include/cli.h b/core/include/cli.h index ba5b1c7..d9345de 100644 --- a/core/include/cli.h +++ b/core/include/cli.h @@ -59,10 +59,19 @@ struct CliTransport { // unlocks the engine session (POST /unlock). Falls back to // HelloConsentOutcome::Unavailable on platforms without Windows Hello. std::function consent; + // Typed-master-password mode (Linux, where Windows Hello does not exist): + // when set, gated commands prompt for the master password (via + // CliConsole::readSecret) instead of the Hello consent, and this hook + // verifies it via POST /unlock {"password": ...}. Null on Windows, where + // the Hello consent flow above is used. + std::function unlockWithPassword; }; struct CliConsole { std::function print; + // No-echo secret prompt, used only in typed-master-password mode (Linux). + // Returns the entered text; an empty string means no input. + std::function readSecret; }; // Executes one CLI invocation (args excluding argv[0], e.g. {"get","api-key", diff --git a/windows/engine/EngineApp.h b/core/include/engine_app.h similarity index 90% rename from windows/engine/EngineApp.h rename to core/include/engine_app.h index b2e495c..8d4fd70 100644 --- a/windows/engine/EngineApp.h +++ b/core/include/engine_app.h @@ -6,11 +6,9 @@ #include #include #include +#include #include -// winsock2 must come before windows.h (no pch.h in the engine project). -#include -#include -#include +#include "platform_compat.h" #include "settings_manager.h" #include "pii_detector.h" #include "log_manager.h" @@ -65,7 +63,7 @@ class EngineApp { HttpResponse ApiGetMatches(const std::wstring& id); HttpResponse ApiDeleteMatches(const std::wstring& id); HttpResponse ApiUnlockHello(const std::wstring& query); - HttpResponse ApiUnlock(); + HttpResponse ApiUnlock(const std::string& body); HttpResponse ApiHelloVerify(const std::wstring& query); HttpResponse ApiGetLogs(const std::wstring& profileParam); @@ -79,7 +77,11 @@ class EngineApp { std::unordered_set runningPorts_; ControlServer controlServer_; - HANDLE stopEvent_ = nullptr; + // Stop signal for Run() (a Win32 event handle was used before the core + // move; a condition variable is portable and behaves identically). + std::mutex stopMutex_; + std::condition_variable stopCv_; + bool stopRequested_ = false; // First-run model download state (guarded by stateMutex_) mutable std::mutex stateMutex_; diff --git a/core/include/platform_compat.h b/core/include/platform_compat.h index 770c5a3..a791c68 100644 --- a/core/include/platform_compat.h +++ b/core/include/platform_compat.h @@ -36,6 +36,7 @@ typedef int SOCKET; #define INVALID_SOCKET (-1) #define SOCKET_ERROR (-1) #define SD_SEND SHUT_WR +#define SD_BOTH SHUT_RDWR #define closesocket close #define WSAGetLastError() errno diff --git a/core/src/cli.cpp b/core/src/cli.cpp index aec1ab9..f36dfde 100644 --- a/core/src/cli.cpp +++ b/core/src/cli.cpp @@ -146,10 +146,27 @@ struct Ctx { // command anymore). The consent runs IN-PROCESS via the transport (the // client is the active application, so the Windows dialog comes to the // foreground) and on success unlocks the engine session (POST /unlock), - // exactly like the GUI after its in-process prompt. `status`/`help` stay - // open so the CLI remains introspectable. + // exactly like the GUI after its in-process prompt. On Linux the same + // gate is a typed master password prompt (no echo) verified through + // POST /unlock. `status`/`help` stay open so the CLI remains + // introspectable. bool EnsureConsent(const json& status) const { if (!status.value("masterPasswordEnabled", false)) return true; + if (t.unlockWithPassword) { + // Typed-master-password mode (Linux). + if (!c.readSecret) { + Error(L"master password required (no interactive prompt available)"); + return false; + } + const std::wstring password = c.readSecret(L"master password: "); + if (password.empty()) { + Error(L"master password required"); + return false; + } + if (t.unlockWithPassword(password)) return true; + Error(L"wrong master password"); + return false; + } if (!status.value("helloEnabled", false)) { Error(L"windows hello is not configured on this device"); return false; @@ -239,7 +256,7 @@ int CmdStatus(const Ctx& ctx) { if (!ctx.EngineStatus(status)) return 1; ctx.Print(L"engine: " + Utils::Utf8ToWide(status.value("engineVersion", std::string("?")))); - ctx.Print(L"password enabled: " + BoolStr(status.value("helloEnabled", false))); + ctx.Print(L"password enabled: " + BoolStr(status.value("masterPasswordEnabled", false) || status.value("helloEnabled", false))); if (status.value("modelDownloadInProgress", false)) { ctx.Print(L"model download: in progress (" + std::to_wstring(status.value("modelDownloadPercent", 0)) + L"%)"); } else if (status.value("modelDownloadFailed", false)) { @@ -306,9 +323,34 @@ int CmdPassword(const Ctx& ctx) { if (action == L"enable") { if (status.value("masterPasswordEnabled", false)) { - ctx.Error(L"windows hello protection is already enabled"); + ctx.Error(ctx.t.unlockWithPassword + ? L"master password protection is already enabled" + : L"windows hello protection is already enabled"); return 1; } + if (ctx.t.unlockWithPassword) { + // Linux: typed master password (no Windows Hello). Prompt twice. + if (!ctx.c.readSecret) { + ctx.Error(L"no interactive prompt available"); + return 1; + } + const std::wstring pw1 = ctx.c.readSecret(L"new master password: "); + if (pw1.empty()) { + ctx.Error(L"password must not be empty"); + return 1; + } + const std::wstring pw2 = ctx.c.readSecret(L"confirm master password: "); + if (pw1 != pw2) { + ctx.Error(L"passwords do not match"); + return 1; + } + if (!ctx.t.put(L"/settings/enableMasterPassword", json{{"password", Utils::WideToUtf8(pw1)}}, nullptr)) { + ctx.Error(L"failed to enable master password protection"); + return 1; + } + ctx.Print(L"master password protection enabled"); + return 0; + } // Windows-Hello-only protection: no typed password exists. The GUI // asks for the consent prompt before enabling; the CLI (a headless // tool) enables directly — the locked session still demands the Hello @@ -322,8 +364,11 @@ int CmdPassword(const Ctx& ctx) { } if (action == L"disable") { + const bool passwordMode = ctx.t.unlockWithPassword != nullptr; if (!status.value("masterPasswordEnabled", false)) { - ctx.Print(L"windows hello protection is not enabled"); + ctx.Print(passwordMode + ? L"master password protection is not enabled" + : L"windows hello protection is not enabled"); return 0; } // Disabling strips ALL protection, so it demands the same fresh @@ -334,10 +379,14 @@ int CmdPassword(const Ctx& ctx) { if (!ctx.EnsureConsent(status)) return 1; json out; if (!ctx.t.put(L"/settings/disableMasterPassword", json{{"value", true}}, &out) || !out.value("ok", false)) { - ctx.Error(L"failed to disable windows hello protection"); + ctx.Error(passwordMode + ? L"failed to disable master password protection" + : L"failed to disable windows hello protection"); return 1; } - ctx.Print(L"windows hello protection disabled"); + ctx.Print(passwordMode + ? L"master password protection disabled" + : L"windows hello protection disabled"); return 0; } @@ -1033,11 +1082,19 @@ void PrintUsage(const Ctx& ctx) { ctx.Print(L" private_address, private_date, private_email, private_person,"); ctx.Print(L" private_phone, private_url, secret"); ctx.Print(L""); - ctx.Print(L"security (Windows Hello only, no typed password):"); - ctx.Print(L" password enable enable Windows Hello protection"); - ctx.Print(L" password disable disable Windows Hello protection"); - ctx.Print(L" With protection enabled every read/write command demands a"); - ctx.Print(L" fresh Windows Hello consent prompt (status/help stay open)."); + if (ctx.t.unlockWithPassword) { + ctx.Print(L"security (typed master password):"); + ctx.Print(L" password enable enable master password protection"); + ctx.Print(L" password disable disable master password protection"); + ctx.Print(L" With protection enabled every read/write command prompts for"); + ctx.Print(L" the master password (status/help stay open)."); + } else { + ctx.Print(L"security (Windows Hello only, no typed password):"); + ctx.Print(L" password enable enable Windows Hello protection"); + ctx.Print(L" password disable disable Windows Hello protection"); + ctx.Print(L" With protection enabled every read/write command demands a"); + ctx.Print(L" fresh Windows Hello consent prompt (status/help stay open)."); + } ctx.Print(L""); ctx.Print(L"options:"); ctx.Print(L" --profile P profile selector: list number, id, or alias"); diff --git a/windows/engine/EngineApp.cpp b/core/src/engine_app.cpp similarity index 95% rename from windows/engine/EngineApp.cpp rename to core/src/engine_app.cpp index 594714b..aa9887a 100644 --- a/windows/engine/EngineApp.cpp +++ b/core/src/engine_app.cpp @@ -1,10 +1,12 @@ -#include "EngineApp.h" +#include "engine_app.h" #include "utils.h" #include "api_key_profile.h" #include "logging.h" #include "model_downloader.h" +#ifdef _WIN32 #include "hello_unlock.h" #include +#endif #include #include #include @@ -247,7 +249,9 @@ namespace { if (!hasClose) return true; ++pos; } - if (!fullText.ends_with(">>") && EndsWithLabelPrefix(fullText)) { + // (std::string::ends_with is C++20; this project is C++17) + if (!(fullText.size() >= 2 && fullText.compare(fullText.size() - 2, 2, ">>") == 0) && + EndsWithLabelPrefix(fullText)) { return true; } return false; @@ -289,12 +293,6 @@ EngineApp::~EngineApp() { } bool EngineApp::Initialize(const std::filesystem::path& dataDir) { - stopEvent_ = CreateEventW(nullptr, TRUE, FALSE, nullptr); - if (!stopEvent_) { - LOG_LIFECYCLE(L"[EngineApp] Failed to create stop event"); - return false; - } - settings_ = std::make_unique(dataDir); logManager_ = std::make_unique(); @@ -350,19 +348,20 @@ void EngineApp::Shutdown() { LOG(L"=== Agent Redactor Engine Shutdown ==="); StopProxyServers(); controlServer_.Stop(); - if (stopEvent_) { - CloseHandle(stopEvent_); - stopEvent_ = nullptr; - } } void EngineApp::Run() { LOG_LIFECYCLE(L"[EngineApp] Engine running"); - WaitForSingleObject(stopEvent_, INFINITE); + std::unique_lock lock(stopMutex_); + stopCv_.wait(lock, [this]() { return stopRequested_; }); } void EngineApp::RequestStop() { - if (stopEvent_) SetEvent(stopEvent_); + { + std::lock_guard lock(stopMutex_); + stopRequested_ = true; + } + stopCv_.notify_all(); } // --------------------------------------------------------------------------- @@ -851,7 +850,7 @@ HttpResponse EngineApp::HandleControlRequest(const HttpRequest& request) { if (path == L"/profiles" && method == "POST") return ApiPostProfile(request.body); if (path == L"/hello/verify" && method == "POST") return ApiHelloVerify(query); if (path == L"/unlock/hello" && method == "POST") return ApiUnlockHello(query); - if (path == L"/unlock" && method == "POST") return ApiUnlock(); + if (path == L"/unlock" && method == "POST") return ApiUnlock(request.body); if (path == L"/logs" && method == "GET") { std::wstring profileParam; const std::wstring prefix = L"profile="; @@ -870,7 +869,7 @@ HttpResponse EngineApp::HandleControlRequest(const HttpRequest& request) { // Respond first, then stop: Shutdown() joins the listener threads, // which would deadlock if Stop ran inside this request handler. std::thread([this]() { - Sleep(200); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); RequestStop(); }).detach(); return JsonResponse(200, "{\"ok\": true}"); @@ -948,9 +947,20 @@ HttpResponse EngineApp::ApiGetSettings() { // never print a blank line on a fresh install. std::wstring appLanguage = settings_->GetAppLanguage(); if (appLanguage.empty()) { +#ifdef _WIN32 wchar_t langBuf[LOCALE_NAME_MAX_LENGTH] = {}; LCIDToLocaleName(GetUserDefaultUILanguage(), langBuf, LOCALE_NAME_MAX_LENGTH, 0); appLanguage = langBuf; +#else + // Derive a BCP-47-ish tag from LANG (e.g. "de_DE.UTF-8" → "de-DE"). + if (const char* lang = std::getenv("LANG"); lang && *lang) { + appLanguage = Utils::Utf8ToWide(lang); + const size_t dot = appLanguage.find(L'.'); + if (dot != std::wstring::npos) appLanguage.resize(dot); + std::replace(appLanguage.begin(), appLanguage.end(), L'_', L'-'); + } + if (appLanguage.empty() || appLanguage == L"C" || appLanguage == L"POSIX") appLanguage = L"en"; +#endif } j["appLanguage"] = Utils::WideToUtf8(appLanguage); j["masterPasswordEnabled"] = settings_->IsMasterPasswordEnabled(); @@ -962,7 +972,9 @@ HttpResponse EngineApp::ApiGetSettings() { // Defined below (next to the hello endpoints that use it): parses ?hwnd= so // engine-owned consent prompts attach to the caller's window where possible. +#ifdef _WIN32 static HWND ParseHwndQuery(const std::wstring& query); +#endif HttpResponse EngineApp::ApiPutSetting(const std::wstring& key, const std::wstring& query, const std::string& body) { json j = json::parse(body); @@ -988,12 +1000,24 @@ HttpResponse EngineApp::ApiPutSetting(const std::wstring& key, const std::wstrin } else if (key == L"appLanguage") { settings_->SetAppLanguage(Utils::Utf8ToWide(j.at("value").get())); } else if (key == L"enableMasterPassword") { +#ifdef _WIN32 // Windows-Hello-only protection: no typed password exists; the AES // key lives only in the DPAPI-wrapped Hello blob. The GUI prompts // for consent (POST /hello/verify) before sending this. if (!settings_->EnableMasterPassword()) { return JsonResponse(500, "{\"error\": \"failed to enable windows hello\"}"); } +#else + // Linux: typed-master-password protection (no Windows Hello). The + // client supplies the new password in the request body. + const std::wstring password = Utils::Utf8ToWide(j.value("password", std::string(""))); + if (password.empty()) { + return JsonResponse(400, "{\"error\": \"password required\"}"); + } + if (!settings_->EnableMasterPassword(password)) { + return JsonResponse(500, "{\"error\": \"failed to enable master password\"}"); + } +#endif } else if (key == L"lock") { settings_->Lock(); } else if (key == L"disableMasterPassword") { @@ -1020,7 +1044,8 @@ HttpResponse EngineApp::ApiPutSetting(const std::wstring& key, const std::wstrin return JsonResponse(200, "{\"ok\": true}"); } -HttpResponse EngineApp::ApiUnlock() { +HttpResponse EngineApp::ApiUnlock(const std::string& body) { +#ifdef _WIN32 // Same unlock as /unlock/hello but WITHOUT the consent prompt — the // caller (the GUI) has already verified the user with its own // in-process Windows Hello prompt, so this only decrypts and unlocks. @@ -1034,6 +1059,23 @@ HttpResponse EngineApp::ApiUnlock() { return JsonResponse(200, "{\"ok\": true}"); } return JsonResponse(200, "{\"ok\": false, \"error\": \"unlock failed\"}"); +#else + // Linux: unlock with the typed master password supplied in the body. + if (!settings_->IsMasterPasswordEnabled()) { + return JsonResponse(200, "{\"ok\": false, \"error\": \"master password not enabled\"}"); + } + std::wstring password; + try { + password = Utils::Utf8ToWide(json::parse(body.empty() ? std::string("{}") : body) + .value("password", std::string(""))); + } catch (...) { + return JsonResponse(200, "{\"ok\": false, \"error\": \"bad request\"}"); + } + if (!password.empty() && settings_->UnlockWithPassword(password)) { + return JsonResponse(200, "{\"ok\": true}"); + } + return JsonResponse(200, "{\"ok\": false, \"error\": \"wrong password\"}"); +#endif } HttpResponse EngineApp::ApiGetProfiles() { @@ -1129,6 +1171,7 @@ HttpResponse EngineApp::ApiDeleteMatches(const std::wstring& id) { return JsonResponse(200, "{\"ok\": true}"); } +#ifdef _WIN32 // The CLI's transport tags the hello consent requests with the console HWND // (?hwnd=) so the prompt is owned by the CLI's window and comes to // the foreground. HWND values are valid across processes for real top-level @@ -1193,6 +1236,17 @@ HttpResponse EngineApp::ApiUnlockHello(const std::wstring& query) { return JsonResponse(200, "{\"ok\": false, \"error\": \"windows hello failed\"}"); } } +#else +// Linux: no Windows Hello — both endpoints report unavailability (the typed +// master password flow goes through POST /unlock with {"password": ...}). +HttpResponse EngineApp::ApiHelloVerify(const std::wstring&) { + return JsonResponse(200, "{\"ok\": false, \"unavailable\": true}"); +} + +HttpResponse EngineApp::ApiUnlockHello(const std::wstring&) { + return JsonResponse(200, "{\"ok\": false, \"unavailable\": true}"); +} +#endif HttpResponse EngineApp::ApiGetLogs(const std::wstring& profileParam) { std::vector entries; diff --git a/core/src/http_server.cpp b/core/src/http_server.cpp index 743ea9a..fb5b8c0 100644 --- a/core/src/http_server.cpp +++ b/core/src/http_server.cpp @@ -96,14 +96,21 @@ void HttpServer::Stop() { running_ = false; if (listenSocket_ != INVALID_SOCKET) { - closesocket(listenSocket_); - listenSocket_ = INVALID_SOCKET; + // Wake the listener's select() so it can exit; the socket is only + // closed AFTER the thread is joined. Closing it first races with + // FD_SET on POSIX (glibc aborts on the invalidated descriptor). + shutdown(listenSocket_, SD_BOTH); } if (listenerThread_.joinable()) { listenerThread_.join(); } + if (listenSocket_ != INVALID_SOCKET) { + closesocket(listenSocket_); + listenSocket_ = INVALID_SOCKET; + } + // Wait for all client connections to finish std::unique_lock lock(stopMutex_); stopCv_.wait(lock, [this]() { return activeConnections_.load() == 0; }); diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index ec7beaf..c91cc9c 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -22,6 +22,7 @@ endif() file(STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/../windows/version.txt AR_VERSION LIMIT_COUNT 1) string(STRIP "${AR_VERSION}" AR_VERSION) message(STATUS "Agent Redactor version: ${AR_VERSION}") +add_compile_definitions(AR_VERSION_STRING="${AR_VERSION}") add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../core ${CMAKE_BINARY_DIR}/core) diff --git a/linux/engine/CMakeLists.txt b/linux/engine/CMakeLists.txt index 7bbd1f7..486a843 100644 --- a/linux/engine/CMakeLists.txt +++ b/linux/engine/CMakeLists.txt @@ -1,3 +1,19 @@ -# Linux engine/CLI binary (Phase 2) and development smoke targets. +# Linux engine/CLI binary plus development smoke targets. + +add_executable(agentredactor + main.cpp + control_api_client.cpp + engine_loc.cpp +) +target_link_libraries(agentredactor PRIVATE agentredactor-core) + +# Dev builds link the onnxruntime shared lib from the tarball location; record +# an rpath so the binary runs without LD_LIBRARY_PATH. +if (ONNXRUNTIME_LIB) + get_filename_component(AR_ONNXRUNTIME_LIB_DIR "${ONNXRUNTIME_LIB}" DIRECTORY) + set_target_properties(agentredactor PROPERTIES BUILD_RPATH "${AR_ONNXRUNTIME_LIB_DIR}") +endif() + add_executable(core-smoke core_smoke.cpp) target_link_libraries(core-smoke PRIVATE agentredactor-core) +set_target_properties(core-smoke PROPERTIES BUILD_RPATH "${AR_ONNXRUNTIME_LIB_DIR}") diff --git a/linux/engine/control_api_client.cpp b/linux/engine/control_api_client.cpp new file mode 100644 index 0000000..5bd58c6 --- /dev/null +++ b/linux/engine/control_api_client.cpp @@ -0,0 +1,126 @@ +#include "control_api_client.h" +#include "utils.h" +#include + +using namespace AgentRedactor; + +namespace { + +size_t WriteToString(char* ptr, size_t size, size_t nmemb, void* userdata) { + auto* out = static_cast(userdata); + out->append(ptr, size * nmemb); + return size * nmemb; +} + +} // namespace + +bool ControlApiClient::Connect(const std::filesystem::path& configDir) { + port_ = 0; + token_.clear(); + + auto content = Utils::ReadFileAsString(configDir / L"control.json"); + if (!content) return false; + try { + json j = json::parse(Utils::WideToUtf8(*content)); + port_ = j.at("port").get(); + token_ = Utils::Utf8ToWide(j.at("token").get()); + } catch (...) { + port_ = 0; + token_.clear(); + return false; + } + return IsConnected(); +} + +bool ControlApiClient::Get(const std::wstring& path, json& out) const { + long status = 0; + std::string body; + if (!Request(L"GET", path, nullptr, status, body) || status != 200) return false; + try { + out = json::parse(body); + } catch (...) { + return false; + } + return true; +} + +bool ControlApiClient::Post(const std::wstring& path, const json& body, json* out) const { + std::string payload = body.dump(); + long status = 0; + std::string respBody; + if (!Request(L"POST", path, &payload, status, respBody) || status != 200) return false; + if (out) { + try { + *out = json::parse(respBody); + } catch (...) { + return false; + } + } + return true; +} + +bool ControlApiClient::Put(const std::wstring& path, const json& body, json* out) const { + std::string payload = body.dump(); + long status = 0; + std::string respBody; + if (!Request(L"PUT", path, &payload, status, respBody) || status != 200) return false; + if (out) { + try { + *out = json::parse(respBody); + } catch (...) { + return false; + } + } + return true; +} + +bool ControlApiClient::Delete(const std::wstring& path) const { + long status = 0; + std::string body; + return Request(L"DELETE", path, nullptr, status, body) && status == 200; +} + +bool ControlApiClient::UnlockWithPassword(const std::wstring& password) const { + json out; + if (!Post(L"/unlock", json{{"password", Utils::WideToUtf8(password)}}, &out)) return false; + return out.value("ok", false); +} + +bool ControlApiClient::Request(const std::wstring& method, const std::wstring& path, + const std::string* body, long& statusCode, std::string& responseBody) const { + statusCode = 0; + responseBody.clear(); + if (!IsConnected()) return false; + + CURL* curl = curl_easy_init(); + if (!curl) return false; + + const std::string url = "http://127.0.0.1:" + std::to_string(port_) + Utils::WideToUtf8(path); + const std::string auth = "Authorization: Bearer " + Utils::WideToUtf8(token_); + struct curl_slist* headers = nullptr; + headers = curl_slist_append(headers, auth.c_str()); + headers = curl_slist_append(headers, "Content-Type: application/json"); + // The CLI only sends JSON ASCII/UTF-8 bodies; suppress 100-continue so the + // engine's simple HTTP parser never sees an interim response. + headers = curl_slist_append(headers, "Expect:"); + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, Utils::WideToUtf8(method).c_str()); + if (body) { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body->data()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(body->size())); + } + // Short timeouts: the engine is localhost; a hang means it is not running. + // /unlock derives a PBKDF2 key (sub-second) so the default suffices. + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 1500L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 3000L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteToString); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBody); + + const CURLcode res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode); + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + return res == CURLE_OK; +} diff --git a/linux/engine/control_api_client.h b/linux/engine/control_api_client.h new file mode 100644 index 0000000..67933ca --- /dev/null +++ b/linux/engine/control_api_client.h @@ -0,0 +1,38 @@ +#pragma once + +// Linux mirror of windows/engine/control_api_client.h: the CLI-side client +// for the engine's localhost control API, implemented over libcurl. + +#include +#include +#include + +using json = nlohmann::json; + +namespace AgentRedactor { + +class ControlApiClient { +public: + // Reads /control.json (port + bearer token). False when the + // engine is not running. + bool Connect(const std::filesystem::path& configDir); + bool IsConnected() const { return port_ != 0 && !token_.empty(); } + + bool Get(const std::wstring& path, json& out) const; + bool Post(const std::wstring& path, const json& body, json* out) const; + bool Put(const std::wstring& path, const json& body, json* out) const; + bool Delete(const std::wstring& path) const; + + // Typed master password unlock (POST /unlock {"password": ...}). There is + // no Windows Hello on Linux, so this is the only unlock path. + bool UnlockWithPassword(const std::wstring& password) const; + +private: + bool Request(const std::wstring& method, const std::wstring& path, + const std::string* body, long& statusCode, std::string& responseBody) const; + + int port_ = 0; + std::wstring token_; +}; + +} // namespace AgentRedactor diff --git a/linux/engine/engine_loc.cpp b/linux/engine/engine_loc.cpp new file mode 100644 index 0000000..dda75f8 --- /dev/null +++ b/linux/engine/engine_loc.cpp @@ -0,0 +1,59 @@ +// Engine-side shim for LocString/LocFormat. core/localization.cpp is bound +// to the GUI process (WinUI pch, AppState, MRT resource contexts), so the +// engine cannot link it. proxy_engine.cpp only needs a handful of keys for +// log summaries and session-match types; this shim serves the English values +// and substitutes {0}/{1}/... placeholders exactly like LocFormat. +// +// Known Stage A limitation: engine-generated log/match-type strings are +// English-only. Full MRT localization inside the engine process is follow-up +// work (the GUI itself is fully localized as before). +#include "localization.h" +#include + +namespace AgentRedactor { + +void InitializeLocalization() {} + +namespace { +const std::unordered_map& EngineStrings() { + static const std::unordered_map strings = { + { L"MatchType_PII", L"PII" }, + { L"MatchType_Regex", L"Regex" }, + { L"MatchType_Keyword", L"Keyword" }, + { L"ProxyLog_Request", L"Request: {0} {1}" }, + { L"ProxyLog_PII", L"PII: {0}" }, + { L"ProxyLog_Regex", L"Regex: {0}" }, + { L"ProxyLog_Keywords", L"Keywords: {0}" }, + }; + return strings; +} +} // namespace + +std::wstring LocString(std::wstring_view key) { + const auto& strings = EngineStrings(); + auto it = strings.find(key); + if (it != strings.end()) return std::wstring(it->second); + return std::wstring(key); +} + +std::wstring LocFormat(std::wstring_view key, std::initializer_list args) { + std::wstring value = LocString(key); + size_t index = 0; + for (const auto& arg : args) { + std::wstring placeholder = L"{" + std::to_wstring(index) + L"}"; + size_t pos = 0; + while ((pos = value.find(placeholder, pos)) != std::wstring::npos) { + value.replace(pos, placeholder.length(), arg.data(), arg.length()); + pos += arg.length(); + } + ++index; + } + return value; +} + +bool SetLanguageOverride(const std::wstring&) { return true; } +std::wstring GetCurrentLanguage() { return L"en"; } +std::wstring GetLanguageOverride() { return L""; } +bool IsCurrentLanguageRtl() { return false; } + +} // namespace AgentRedactor diff --git a/linux/engine/main.cpp b/linux/engine/main.cpp new file mode 100644 index 0000000..05454ca --- /dev/null +++ b/linux/engine/main.cpp @@ -0,0 +1,158 @@ +// Linux entry point for the dual-mode agentredactor binary (mirror of +// windows/engine/main.cpp): +// - no args / --console -> run the engine in the foreground (the GUI and +// the systemd --user unit launch it detached) +// - any other subcommand -> CLI client over the localhost control API +// - --selftest-migrate-settings -> headless settings-migration test hook +#include "engine_app.h" +#include "cli.h" +#include "control_api_client.h" +#include "utils.h" +#include "logging.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace AgentRedactor; + +namespace { + +// Single-instance guard: the Windows build uses a named mutex; here an flock +// on /engine.lock held for the process lifetime. A second engine +// exits quietly with code 0, mirroring the Windows behavior. +int g_lockFd = -1; + +bool AcquireSingleInstanceLock(const std::filesystem::path& configDir) { + g_lockFd = open((configDir / "engine.lock").c_str(), O_RDWR | O_CREAT, 0600); + if (g_lockFd < 0) return true; // cannot lock -> do not block startup + if (flock(g_lockFd, LOCK_EX | LOCK_NB) != 0) { + close(g_lockFd); + g_lockFd = -1; + return false; + } + return true; +} + +int RunEngine() { + const auto configDir = Utils::GetAppDataPath(); + Utils::CreateDirectoryRecursive(configDir); + chmod(configDir.c_str(), 0700); + + if (!AcquireSingleInstanceLock(configDir)) { + return 0; // another engine instance is already running + } + + Utils::InitializeLogging({}); + LOG_LIFECYCLE(L"[engine] starting (linux)"); + + EngineApp engine; + if (!engine.Initialize({})) { + LOG_LIFECYCLE(L"[engine] EngineApp::Initialize FAILED"); + std::fprintf(stderr, "[engine] EngineApp::Initialize FAILED\n"); + return 1; + } + engine.Run(); + engine.Shutdown(); + Utils::LogShutdown(); + return 0; +} + +std::wstring ReadSecret(const std::wstring& prompt) { + std::fprintf(stderr, "%s", Utils::WideToUtf8(prompt).c_str()); + std::fflush(stderr); + + const bool tty = isatty(STDIN_FILENO); + struct termios oldt {}; + if (tty) { + tcgetattr(STDIN_FILENO, &oldt); + struct termios newt = oldt; + newt.c_lflag &= ~static_cast(ECHO); + tcsetattr(STDIN_FILENO, TCSAFLUSH, &newt); + } + + char* line = nullptr; + size_t cap = 0; + const ssize_t n = getline(&line, &cap, stdin); + + if (tty) { + tcsetattr(STDIN_FILENO, TCSAFLUSH, &oldt); + std::fprintf(stderr, "\n"); + } + if (n <= 0 || !line) { + free(line); + return L""; + } + std::string narrow(line, static_cast(n)); + free(line); + while (!narrow.empty() && (narrow.back() == '\n' || narrow.back() == '\r')) narrow.pop_back(); + return Utils::Utf8ToWide(narrow); +} + +int RunCliCommand(const std::vector& args) { + ControlApiClient client; + client.Connect(Utils::GetAppDataPath()); + + CliTransport transport; + transport.get = [&client](const std::wstring& path, json& out) { return client.Get(path, out); }; + transport.post = [&client](const std::wstring& path, const json& body, json* out) { return client.Post(path, body, out); }; + transport.put = [&client](const std::wstring& path, const json& body, json* out) { return client.Put(path, body, out); }; + transport.del = [&client](const std::wstring& path) { return client.Delete(path); }; + // No Windows Hello on Linux; the typed master password flow is used instead. + transport.consent = []() { return HelloConsentOutcome::Unavailable; }; + transport.unlockWithPassword = [&client](const std::wstring& password) { + return client.UnlockWithPassword(password); + }; + + CliConsole console; + console.print = [](const std::wstring& line) { + std::fputs(Utils::WideToUtf8(line).c_str(), stdout); + std::fputc('\n', stdout); + }; + console.readSecret = [](const std::wstring& prompt) { return ReadSecret(prompt); }; + + return RunCli(args, transport, console); +} + +} // namespace + +int main(int argc, char* argv[]) { + // Upstream writes to a closed client socket must not kill the process. + std::signal(SIGPIPE, SIG_IGN); + + std::vector args; + for (int i = 1; i < argc; ++i) { + args.push_back(Utils::Utf8ToWide(argv[i])); + } + + // Headless test hook: load + migrate + save the settings for the resolved + // config dir (AGENTREDACTOR_CONFIG_DIR honored), print a machine-readable + // result, and exit. Deliberately runs before ANY other init so pytest can + // drive settings-migration tests without a running engine. + for (const auto& arg : args) { + if (arg == L"--selftest-migrate-settings") { + try { + SettingsManager settings({}); + std::printf("SETTINGS_MIGRATION_OK\n"); + return 0; + } catch (const std::exception& e) { + std::printf("SETTINGS_MIGRATION_FAIL %s\n", e.what()); + return 1; + } catch (...) { + std::printf("SETTINGS_MIGRATION_FAIL unknown error\n"); + return 1; + } + } + } + + if (args.empty() || args[0] == L"--console") { + return RunEngine(); + } + return RunCliCommand(args); +} diff --git a/windows/AgentRedactorEngine.vcxproj b/windows/AgentRedactorEngine.vcxproj index cb74275..3abd32e 100644 --- a/windows/AgentRedactorEngine.vcxproj +++ b/windows/AgentRedactorEngine.vcxproj @@ -152,7 +152,7 @@ - + diff --git a/windows/engine/main.cpp b/windows/engine/main.cpp index e7c8021..1108eb0 100644 --- a/windows/engine/main.cpp +++ b/windows/engine/main.cpp @@ -12,7 +12,7 @@ // this file only supplies the Windows console plumbing and the WinHTTP // transport (control_api_client, the engine-side mirror of the GUI's // EngineClient). -#include "EngineApp.h" +#include "engine_app.h" #include "control_api_client.h" #include "cli.h" #include "utils.h" From c48caf8d9034bfb3aec56dce792ae44def87ab56 Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Tue, 18 Aug 2026 10:51:48 +0000 Subject: [PATCH 04/20] test(linux): run CLI/migration suites on Linux, add engine smoke tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tests/cli/conftest.py: engine binary resolves to linux/build/engine/ agentredactor off Windows (AGENTREDACTOR_ENGINE_BIN overrides); run_cli gains input= to pipe a typed master password. - tests/cli/test_cli.py: skip the two Windows-Hello consent tests off Windows; branch the 'protection is not enabled' message per platform. - tests/cli/test_cli_linux_password.py (new): typed-master-password gate mirroring the Hello consent gate — enable/disable round-trip, locked session after restart, EOF/wrong/correct password behavior. - tests/linux/test_engine_smoke.py (new): engine start/stop, control.json 0600, config dir 0700, single-instance lock keeps the first engine's control.json (second instance exits 0 quietly, mirroring Windows). - tests/migration: run --selftest-migrate-settings against the Linux engine binary (AGENTREDACTOR_EXE still overrides). - tests/gui: ignore the whole directory off Windows (no GUI there yet); kill list matches the extensionless Linux engine binary. - READMEs: per-suite pytest invocations (the suites share the 'conftest' module name and cannot be collected in one process) and the python3-pytest-asyncio dependency. --- linux/README.md | 10 +- tests/README.md | 23 ++++ tests/cli/conftest.py | 25 +++- tests/cli/test_cli.py | 10 +- tests/cli/test_cli_linux_password.py | 119 ++++++++++++++++++ tests/gui/conftest.py | 7 ++ tests/gui/gui_process.py | 9 +- tests/linux/test_engine_smoke.py | 139 +++++++++++++++++++++ tests/migration/conftest.py | 15 +-- tests/migration/test_settings_migration.py | 7 +- 10 files changed, 342 insertions(+), 22 deletions(-) create mode 100644 tests/cli/test_cli_linux_password.py create mode 100644 tests/linux/test_engine_smoke.py diff --git a/linux/README.md b/linux/README.md index 1435977..db0dc41 100644 --- a/linux/README.md +++ b/linux/README.md @@ -5,7 +5,7 @@ Build the engine/CLI (`agentredactor`) on Linux: ```bash sudo apt install -y build-essential cmake ninja-build pkg-config \ libsecret-1-dev libcurl4-openssl-dev libssl-dev nlohmann-json3-dev \ - python3-pytest python3-aiohttp python3-psutil + python3-pytest python3-pytest-asyncio python3-aiohttp python3-psutil # onnxruntime is not packaged in apt; use the official linux-x64 tarball # (developed/tested against 1.29.0): @@ -25,8 +25,12 @@ The engine also needs the NER model files. `config.json`, `tokenizer.json`, downloaded automatically on first run (or grab them from the models endpoint used by the Windows CI). -Run the tests from the repo root: +Run the tests from the repo root (one pytest process per suite — the suites +share the `conftest` module name and cannot be collected together): ```bash -python -m pytest tests/cli tests/migration tests/linux -q +cd tests +python -m pytest cli -q +python -m pytest migration/test_settings_migration.py -q +python -m pytest linux -q ``` diff --git a/tests/README.md b/tests/README.md index f2b1929..3f42a63 100644 --- a/tests/README.md +++ b/tests/README.md @@ -49,3 +49,26 @@ pytest -v gui/ - `gui/conftest.py` — backs up the user's real data, creates a clean test profile, and restores it after the test. - `config_factory.py` generates a plaintext `settings.json`. - `mock_llm.py` is an `aiohttp` mock server that echoes redacted text back in OpenAI or Anthropic format. + +## Headless CLI and Linux engine tests + +`cli/`, `migration/`, and `linux/` drive the headless `agentredactor` +binary (no GUI) and also run on Linux, where the binary comes from +`linux/build/engine/agentredactor` (override with `AGENTREDACTOR_ENGINE_BIN` +for `cli/`, `AGENTREDACTOR_EXE` for `migration/`). The `gui/` directory is +ignored on non-Windows. + +```bash +cd tests +pytest -v cli/ +pytest -v migration/test_settings_migration.py +pytest -v linux/ +``` + +On Linux the protection model is a typed master password instead of Windows +Hello: the Hello-consent tests in `cli/test_cli.py` skip, and +`cli/test_cli_linux_password.py` covers the equivalent password flow by +piping the password to stdin. + +Run each suite in its own pytest process (as above): the suites share the +`conftest` module name and cannot be collected in a single invocation. diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py index 72bac53..962e86e 100644 --- a/tests/cli/conftest.py +++ b/tests/cli/conftest.py @@ -30,8 +30,15 @@ from gui_process import _find_free_port, _kill_existing_agent_redactor, _wait_for_port # noqa: E402 PROJECT_ROOT = _tests_root.parent -_BUILD_PLATFORM = "ARM64" if platform.machine().upper() == "ARM64" else "x64" -ENGINE_EXE = PROJECT_ROOT / "windows" / "build" / _BUILD_PLATFORM / "Release" / "agentredactor.exe" +if sys.platform == "win32": + _BUILD_PLATFORM = "ARM64" if platform.machine().upper() == "ARM64" else "x64" + _DEFAULT_ENGINE = ( + PROJECT_ROOT / "windows" / "build" / _BUILD_PLATFORM / "Release" / "agentredactor.exe" + ) +else: + _DEFAULT_ENGINE = PROJECT_ROOT / "linux" / "build" / "engine" / "agentredactor" +# AGENTREDACTOR_ENGINE_BIN overrides the default build-tree location. +ENGINE_EXE = Path(os.environ.get("AGENTREDACTOR_ENGINE_BIN") or _DEFAULT_ENGINE) TEST_API_KEY = "sk-cli-test-key" @@ -88,16 +95,22 @@ def start(self) -> None: self.stop() raise RuntimeError(f"engine did not start listening on port {self.proxy_port}") - def run_cli(self, *args: str) -> subprocess.CompletedProcess: - # stdin=DEVNULL: no interactive console, so a locked engine must - # reject commands instead of prompting (and hanging the test). + def run_cli(self, *args: str, input: str | None = None) -> subprocess.CompletedProcess: + # stdin=DEVNULL by default: no interactive console, so a locked engine + # must reject commands instead of prompting (and hanging the test). + # Pass input= to pipe a typed master password (Linux password mode). + kwargs: dict = {} + if input is None: + kwargs["stdin"] = subprocess.DEVNULL + else: + kwargs["input"] = input return subprocess.run( [str(ENGINE_EXE), *args], env=self._env(), - stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=30, + **kwargs, ) def stop(self) -> None: diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index fbc6b8f..e596e11 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -9,8 +9,11 @@ from __future__ import annotations +import sys import time +import pytest + from conftest import CliEngine, TEST_API_KEY @@ -304,13 +307,17 @@ def test_unlock_command_removed(engine: CliEngine) -> None: r = engine.run_cli("password", "disable") assert r.returncode == 0 - assert "windows hello protection is not enabled" in r.stdout + if sys.platform == "win32": + assert "windows hello protection is not enabled" in r.stdout + else: + assert "master password protection is not enabled" in r.stdout r = engine.run_cli("status") assert r.returncode == 0 assert "password enabled: false" in r.stdout +@pytest.mark.skipif(sys.platform != "win32", reason="Windows Hello consent flow is Windows-only; Linux uses the typed master password (test_cli_linux_password.py)") def test_password_hello_consent_gate(engine: CliEngine) -> None: """Windows-Hello-only protection: `password enable` (no password anywhere), a fresh engine session starts locked, and EVERY gated command demands a @@ -368,6 +375,7 @@ def test_password_hello_consent_gate(engine: CliEngine) -> None: assert "password enabled: false" in r.stdout +@pytest.mark.skipif(sys.platform != "win32", reason="Windows Hello suppress-prompt invariant is Windows-only; Linux uses the typed master password (test_cli_linux_password.py)") def test_hello_suppress_prompt_flag_never_grants_access(engine: CliEngine) -> None: """SECURITY INVARIANT for the AGENTREDACTOR_HELLO_SUPPRESS_PROMPT test hook (set in conftest for the whole suite): the flag must behave exactly diff --git a/tests/cli/test_cli_linux_password.py b/tests/cli/test_cli_linux_password.py new file mode 100644 index 0000000..349087a --- /dev/null +++ b/tests/cli/test_cli_linux_password.py @@ -0,0 +1,119 @@ +"""Linux typed-master-password tests for the CLI surface. + +On Linux there is no Windows Hello; protection is a typed master password +(PBKDF2-wrapped session key). Gated commands prompt on stdin (no echo) and +verify through POST /unlock. These tests mirror the Windows Hello consent +gate in test_cli.py (skipped there on non-Windows) using piped stdin. + +The tests run in file order against the shared module-scoped engine; the +final test strips protection again so the suite ends unlocked. +""" + +from __future__ import annotations + +import sys + +import pytest + +from conftest import CliEngine, TEST_API_KEY + +pytestmark = pytest.mark.skipif( + sys.platform == "win32", + reason="typed master password is the Linux protection mode; Windows uses Hello", +) + +PASSWORD = "cli-test-pw" + + +def test_password_enable_requires_matching_entries(engine: CliEngine) -> None: + # Mismatched confirmation is rejected and protection stays off. + r = engine.run_cli("password", "enable", input=f"{PASSWORD}\nother\n") + assert r.returncode == 1, r.stdout + assert "passwords do not match" in r.stdout + r = engine.run_cli("status") + assert "password enabled: false" in r.stdout + + # Empty password is rejected. + r = engine.run_cli("password", "enable", input="\n\n") + assert r.returncode == 1, r.stdout + assert "password must not be empty" in r.stdout + + +def test_password_enable_and_locked_session_gate(engine: CliEngine) -> None: + r = engine.run_cli("password", "enable", input=f"{PASSWORD}\n{PASSWORD}\n") + assert r.returncode == 0, r.stdout + assert "master password protection enabled" in r.stdout + + # Enabling twice is an error. + r = engine.run_cli("password", "enable", input=f"{PASSWORD}\n{PASSWORD}\n") + assert r.returncode == 1 + assert "already enabled" in r.stdout + + r = engine.run_cli("status") + assert r.returncode == 0 + assert "password enabled: true" in r.stdout + + # Restart the engine: a fresh session starts locked, like a real app open. + engine.stop() + engine.start() + try: + # status/help stay open (ungated by design). + r = engine.run_cli("status") + assert r.returncode == 0, r.stdout + assert "password enabled: true" in r.stdout + r = engine.run_cli("help") + assert r.returncode == 0 + + # With stdin=DEVNULL the prompt hits EOF and every gated command fails + # fast instead of hanging. + for args in (("get", "logging"), ("get", "api-key"), ("set", "logging", "false"), + ("regex", "list"), ("keywords", "list"), ("pii-types", "list"), + ("profiles", "list"), ("get", "confidence-threshold")): + r = engine.run_cli(*args) + assert r.returncode == 1, (args, r.stdout) + assert "master password required" in r.stdout, (args, r.stdout) + + # A wrong piped password is rejected. + r = engine.run_cli("get", "logging", input="wrong-pw\n") + assert r.returncode == 1, r.stdout + assert "wrong master password" in r.stdout + + # The API key was never served. + r = engine.run_cli("get", "api-key") + assert r.stdout.strip() != TEST_API_KEY, r.stdout + + # The correct piped password unlocks the session for the command. + r = engine.run_cli("get", "logging", input=f"{PASSWORD}\n") + assert r.returncode == 0, r.stdout + assert r.stdout.strip() == "true" + + # Disabling protection is gated like every other protected action. + r = engine.run_cli("password", "disable") + assert r.returncode == 1, r.stdout + r = engine.run_cli("status") + assert "password enabled: true" in r.stdout + finally: + # Safety net: if any assertion above failed mid-lock, drop protection + # via settings surgery so later modules start unlocked. + engine.strip_protection() + r = engine.run_cli("status") + assert r.returncode == 0 + assert "password enabled: false" in r.stdout + + +def test_password_disable_with_password(engine: CliEngine) -> None: + r = engine.run_cli("password", "enable", input=f"{PASSWORD}\n{PASSWORD}\n") + assert r.returncode == 0, r.stdout + try: + engine.stop() + engine.start() + r = engine.run_cli("password", "disable", input=f"{PASSWORD}\n") + assert r.returncode == 0, r.stdout + assert "master password protection disabled" in r.stdout + r = engine.run_cli("status") + assert "password enabled: false" in r.stdout + # Ungated again. + r = engine.run_cli("get", "logging") + assert r.returncode == 0, r.stdout + finally: + engine.strip_protection() diff --git a/tests/gui/conftest.py b/tests/gui/conftest.py index 12308db..d11e5a8 100644 --- a/tests/gui/conftest.py +++ b/tests/gui/conftest.py @@ -24,6 +24,13 @@ from gui_process import GuiAppProcess, _find_free_port from windows.gui_driver import quit_app +# The GUI exists on Windows only; on Linux the headless engine/CLI suites +# (tests/cli, tests/linux) cover the port. Ignore this whole directory so a +# bare `pytest tests/` on Linux does not error trying to launch +# AgentRedactorUI.exe. +if sys.platform != "win32": + collect_ignore_glob = ["*"] + @pytest.fixture def proxy_port() -> int: diff --git a/tests/gui/gui_process.py b/tests/gui/gui_process.py index 2efbae7..c3caed6 100644 --- a/tests/gui/gui_process.py +++ b/tests/gui/gui_process.py @@ -26,8 +26,13 @@ # Process image names to clean up between tests: the GUI and the engine it # spawns (the engine owns the proxy ports and the control API, so a stale one -# would leak settings state and ports into the next test). -_PROCESS_NAMES = ("agentredactorui.exe", "agentredactor.exe") +# would leak settings state and ports into the next test). On Linux only the +# headless engine exists (no GUI yet) and binaries carry no .exe suffix. +_PROCESS_NAMES = ( + ("agentredactorui.exe", "agentredactor.exe") + if sys.platform == "win32" + else ("agentredactor",) +) def _find_free_port() -> int: diff --git a/tests/linux/test_engine_smoke.py b/tests/linux/test_engine_smoke.py new file mode 100644 index 0000000..ed80a54 --- /dev/null +++ b/tests/linux/test_engine_smoke.py @@ -0,0 +1,139 @@ +"""Linux engine smoke tests: lifecycle, single-instance lock, file permissions. + +These run the real Linux engine binary against an isolated config dir +(AGENTREDACTOR_CONFIG_DIR) and assert the platform contract that the Windows +suite gets implicitly: the engine starts and stops cleanly, control.json is +0600, the config dir is 0700, and a second engine instance is refused. +""" + +from __future__ import annotations + +import json +import os +import stat +import subprocess +import sys +import time +from pathlib import Path + +import pytest + +_tests_root = Path(__file__).resolve().parent.parent +for _p in (str(_tests_root), str(_tests_root / "gui")): + if _p not in sys.path: + sys.path.insert(0, _p) + +from config_factory import create_settings # noqa: E402 +from gui_process import _find_free_port, _kill_existing_agent_redactor, _wait_for_port # noqa: E402 + +pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="Linux engine smoke tests") + +PROJECT_ROOT = _tests_root.parent +ENGINE_BIN = Path( + os.environ.get("AGENTREDACTOR_ENGINE_BIN") + or PROJECT_ROOT / "linux" / "build" / "engine" / "agentredactor" +) + + +def _wait_for_control_json(config_dir: Path, timeout: float = 90.0) -> None: + deadline = time.monotonic() + timeout + path = config_dir / "control.json" + while time.monotonic() < deadline: + if path.exists() and path.stat().st_size > 0: + return + time.sleep(0.1) + raise RuntimeError(f"control.json did not appear in {config_dir}") + + +@pytest.fixture() +def engine(tmp_path: Path): + """A running engine with one seeded profile; yields (process, config_dir, port).""" + if not ENGINE_BIN.is_file(): + pytest.skip(f"engine binary not built: {ENGINE_BIN}") + config_dir = tmp_path / "config" + proxy_port = _find_free_port() + create_settings( + data_dir=config_dir, + upstream_url="http://127.0.0.1:9", # unreachable on purpose; no traffic sent + api_key="sk-linux-smoke", + proxy_port=proxy_port, + logging_enabled=True, + keywords=[], + regex_patterns=[], + ) + env = dict(os.environ) + env["AGENTREDACTOR_CONFIG_DIR"] = str(config_dir) + _kill_existing_agent_redactor() + proc = subprocess.Popen( + [str(ENGINE_BIN)], + env=env, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + try: + _wait_for_control_json(config_dir) + if not _wait_for_port(proxy_port, timeout=90.0): + raise RuntimeError(f"engine did not start listening on port {proxy_port}") + yield proc, config_dir, proxy_port, env + finally: + if proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + _kill_existing_agent_redactor() + + +def test_engine_starts_and_stops_cleanly(engine) -> None: + proc, config_dir, proxy_port, env = engine + assert proc.poll() is None + + r = subprocess.run( + [str(ENGINE_BIN), "status"], + env=env, + stdin=subprocess.DEVNULL, + capture_output=True, + text=True, + timeout=30, + ) + assert r.returncode == 0, r.stdout + r.stderr + assert "engine:" in r.stdout + assert "proxy running" in r.stdout + + proc.terminate() + proc.wait(timeout=5) + assert proc.returncode is not None + + +def test_control_json_and_config_dir_permissions(engine) -> None: + _, config_dir, _, _ = engine + control = config_dir / "control.json" + assert stat.S_IMODE(control.stat().st_mode) == 0o600 + assert stat.S_IMODE(config_dir.stat().st_mode) == 0o700 + + # control.json carries the bearer token the CLI authenticates with. + data = json.loads(control.read_text(encoding="utf-8")) + assert data.get("token") + + +def test_second_engine_instance_is_refused(engine) -> None: + proc, config_dir, _, env = engine + assert proc.poll() is None + # The single-instance lock (flock on engine.lock, mirroring the Windows + # named mutex) makes a second engine with the same config dir exit quietly + # with code 0 without taking over: control.json must stay the first + # engine's (its bearer token would otherwise no longer authenticate). + control = config_dir / "control.json" + before = control.read_bytes() + r = subprocess.run( + [str(ENGINE_BIN)], + env=env, + stdin=subprocess.DEVNULL, + capture_output=True, + text=True, + timeout=30, + ) + assert r.returncode == 0 + assert control.read_bytes() == before + assert proc.poll() is None # the first engine is unaffected diff --git a/tests/migration/conftest.py b/tests/migration/conftest.py index 5e7a547..c7e90f5 100644 --- a/tests/migration/conftest.py +++ b/tests/migration/conftest.py @@ -10,11 +10,14 @@ import pytest REPO_ROOT = Path(__file__).resolve().parents[2] -DEFAULT_EXE = REPO_ROOT / "windows" / "build" / "x64" / "Release" / "AgentRedactorUI.exe" +if sys.platform == "win32": + DEFAULT_EXE = REPO_ROOT / "windows" / "build" / "x64" / "Release" / "AgentRedactorUI.exe" +else: + DEFAULT_EXE = REPO_ROOT / "linux" / "build" / "engine" / "agentredactor" def resolve_exe() -> Path | None: - """AGENTREDACTOR_EXE wins; otherwise fall back to the local Release build.""" + """AGENTREDACTOR_EXE wins; otherwise fall back to the local build.""" env_path = os.environ.get("AGENTREDACTOR_EXE") if env_path: return Path(env_path) @@ -23,14 +26,12 @@ def resolve_exe() -> Path | None: @pytest.fixture(scope="session") def agentredactor_exe() -> Path: - if sys.platform != "win32": - pytest.skip("AgentRedactorUI.exe is Windows-only") exe = resolve_exe() if exe is None or not exe.is_file(): pytest.skip( - "AgentRedactorUI.exe not found (looked at " - f"{DEFAULT_EXE}). Build the Release configuration or point " - "AGENTREDACTOR_EXE at a built exe." + "agentredactor binary not found (looked at " + f"{DEFAULT_EXE}). Build the Release configuration (Windows) or " + "the linux/build tree, or point AGENTREDACTOR_EXE at a built binary." ) return exe diff --git a/tests/migration/test_settings_migration.py b/tests/migration/test_settings_migration.py index 8f60e77..7b1f393 100644 --- a/tests/migration/test_settings_migration.py +++ b/tests/migration/test_settings_migration.py @@ -1,9 +1,10 @@ """Headless settings-schema migration tests. -Each test drives the real AgentRedactorUI.exe with --selftest-migrate-settings +Each test drives the real agentredactor binary (AgentRedactorUI.exe on +Windows, the Linux engine binary elsewhere) with --selftest-migrate-settings against an isolated config dir (AGENTREDACTOR_CONFIG_DIR) — no GUI session is -needed. The exe path comes from AGENTREDACTOR_EXE or the local Release build; -the whole module skips when no exe is available. See tests/migration/README.md. +needed. The exe path comes from AGENTREDACTOR_EXE or the local build; +the whole module skips when no binary is available. See tests/migration/README.md. """ from __future__ import annotations From b1e2493d31db8a9d8f5a76cfb576b4a348bac9cb Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Tue, 18 Aug 2026 21:51:11 +0000 Subject: [PATCH 05/20] feat(linux): add Qt6 GUI with tray, XDG autostart and systemd unit template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - linux/gui/: Qt6 Widgets app (agentredactor-gui) mirroring the Windows MainWindow/HomePage surface — profile sidebar, profile/regex/keywords/ detection/password/statistics/session-redactions/logs/settings cards, lock overlay with typed master password (Linux has no Windows Hello), blocking model-download dialog, close-to-tray with control-panel fallback when no system tray is available, 10-minute inactivity re-lock, quit confirmation. - AppState mirror: engine spawn via QProcess::startDetached when the control API is unreachable, 1 s /status + /settings poll on a worker thread (profilesRevision diff triggers profile reload; full settings diff refreshes cards), engine stopped on quit only when the GUI spawned it, otherwise PUT /settings/lock when protected. - HTTP stack: reuses the tested curl ControlApiClient from linux/engine/ (one HTTP stack across the Linux product); ControlApiClient gains LastStatus() so the GUI can distinguish 403-while-locked. - XDG autostart: the Start-on-boot toggle and GUI startup reconcile $XDG_CONFIG_HOME/autostart/agentredactor.desktop (Exec= --tray-only) with the persisted setting, so CLI changes apply too. - Localization structure without translations yet: all strings via tr(), TranslatorLoader installs agentredactor_.qm from appLanguage (empty = system locale) and flips RTL via core IsLanguageRtl — the Windows Strings resw files can be converted to .ts later with no code changes. - SIGTERM/SIGINT self-pipe bridge -> graceful quit so the engine stop/lock decision also runs under systemd and loginctl. - linux/systemd/agentredactor.service: user unit template for headless boot-time startup. - tests/linux/test_gui_smoke.py: offscreen (QT_QPA_PLATFORM=offscreen) end-to-end checks — engine spawn/stop ownership, lock-on-quit when protected, autostart reconciliation in both directions. --- linux/README.md | 26 +- linux/engine/control_api_client.cpp | 2 + linux/engine/control_api_client.h | 6 + linux/gui/CMakeLists.txt | 32 + linux/gui/app_state.cpp | 133 +++ linux/gui/app_state.h | 75 ++ linux/gui/assets/app.png | Bin 0 -> 92248 bytes linux/gui/autostart.cpp | 45 ++ linux/gui/autostart.h | 22 + linux/gui/engine_client.h | 88 ++ linux/gui/main.cpp | 84 ++ linux/gui/main_window.cpp | 1169 +++++++++++++++++++++++++++ linux/gui/main_window.h | 173 ++++ linux/gui/password_dialog.cpp | 83 ++ linux/gui/password_dialog.h | 40 + linux/gui/resources.qrc | 7 + linux/gui/translator_loader.cpp | 40 + linux/gui/translator_loader.h | 29 + linux/gui/tray_icon.cpp | 41 + linux/gui/tray_icon.h | 33 + linux/systemd/agentredactor.service | 23 + tests/README.md | 5 + tests/linux/test_gui_smoke.py | 245 ++++++ 23 files changed, 2400 insertions(+), 1 deletion(-) create mode 100644 linux/gui/CMakeLists.txt create mode 100644 linux/gui/app_state.cpp create mode 100644 linux/gui/app_state.h create mode 100644 linux/gui/assets/app.png create mode 100644 linux/gui/autostart.cpp create mode 100644 linux/gui/autostart.h create mode 100644 linux/gui/engine_client.h create mode 100644 linux/gui/main.cpp create mode 100644 linux/gui/main_window.cpp create mode 100644 linux/gui/main_window.h create mode 100644 linux/gui/password_dialog.cpp create mode 100644 linux/gui/password_dialog.h create mode 100644 linux/gui/resources.qrc create mode 100644 linux/gui/translator_loader.cpp create mode 100644 linux/gui/translator_loader.h create mode 100644 linux/gui/tray_icon.cpp create mode 100644 linux/gui/tray_icon.h create mode 100644 linux/systemd/agentredactor.service create mode 100644 tests/linux/test_gui_smoke.py diff --git a/linux/README.md b/linux/README.md index db0dc41..16891f5 100644 --- a/linux/README.md +++ b/linux/README.md @@ -1,10 +1,12 @@ # Agent Redactor — Linux build -Build the engine/CLI (`agentredactor`) on Linux: +Build the engine/CLI (`agentredactor`) and the Qt GUI (`agentredactor-gui`) +on Linux: ```bash sudo apt install -y build-essential cmake ninja-build pkg-config \ libsecret-1-dev libcurl4-openssl-dev libssl-dev nlohmann-json3-dev \ + qt6-base-dev libgl1-mesa-dev \ python3-pytest python3-pytest-asyncio python3-aiohttp python3-psutil # onnxruntime is not packaged in apt; use the official linux-x64 tarball @@ -34,3 +36,25 @@ python -m pytest cli -q python -m pytest migration/test_settings_migration.py -q python -m pytest linux -q ``` + +## GUI, tray and autostart + +`build/gui/agentredactor-gui` is the desktop app (Qt6 Widgets, English-only +UI structured for later `.ts` translations). It spawns the engine +(`agentredactor`, found next to it or in the sibling `engine/` build dir) +when none is running, and stops it on quit only when it spawned it. + +- Tray: `QSystemTrayIcon` (StatusNotifierItem). On desktops without a tray + (plain Wayland GNOME without the AppIndicator extension) the app runs as a + control panel: closing the window exits the GUI but leaves the engine + running. `agentredactor-gui --tray-only` starts hidden (the autostart + mode); it is ignored when no tray exists. +- Autostart: the "Start on boot" toggle writes/removes + `$XDG_CONFIG_HOME/autostart/agentredactor.desktop` (the GUI reconciles the + file with the persisted setting on startup, so CLI changes apply too). +- Headless/boot-time startup without a desktop session: install the systemd + user unit from `linux/systemd/agentredactor.service` (instructions in the + file's header comment). +- Password protection on Linux is a typed master password chosen inside the + app (not your OS login password). The GUI shows a lock overlay with a + password field when the session is locked. diff --git a/linux/engine/control_api_client.cpp b/linux/engine/control_api_client.cpp index 5bd58c6..31667fd 100644 --- a/linux/engine/control_api_client.cpp +++ b/linux/engine/control_api_client.cpp @@ -90,6 +90,7 @@ bool ControlApiClient::Request(const std::wstring& method, const std::wstring& p const std::string* body, long& statusCode, std::string& responseBody) const { statusCode = 0; responseBody.clear(); + lastStatus_ = 0; if (!IsConnected()) return false; CURL* curl = curl_easy_init(); @@ -120,6 +121,7 @@ bool ControlApiClient::Request(const std::wstring& method, const std::wstring& p const CURLcode res = curl_easy_perform(curl); curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode); + lastStatus_ = statusCode; curl_slist_free_all(headers); curl_easy_cleanup(curl); return res == CURLE_OK; diff --git a/linux/engine/control_api_client.h b/linux/engine/control_api_client.h index 67933ca..7698b69 100644 --- a/linux/engine/control_api_client.h +++ b/linux/engine/control_api_client.h @@ -23,6 +23,11 @@ class ControlApiClient { bool Put(const std::wstring& path, const json& body, json* out) const; bool Delete(const std::wstring& path) const; + // HTTP status of the most recent request (0 = transport failure). The + // typed getters collapse non-200 to false; callers that need to + // distinguish e.g. 403-while-locked read this after a false return. + long LastStatus() const { return lastStatus_; } + // Typed master password unlock (POST /unlock {"password": ...}). There is // no Windows Hello on Linux, so this is the only unlock path. bool UnlockWithPassword(const std::wstring& password) const; @@ -33,6 +38,7 @@ class ControlApiClient { int port_ = 0; std::wstring token_; + mutable long lastStatus_ = 0; }; } // namespace AgentRedactor diff --git a/linux/gui/CMakeLists.txt b/linux/gui/CMakeLists.txt new file mode 100644 index 0000000..17be214 --- /dev/null +++ b/linux/gui/CMakeLists.txt @@ -0,0 +1,32 @@ +# Linux GUI — Qt6 Widgets frontend over the engine's loopback control API. +# Mirrors the Windows WinUI 3 app (MainWindow + HomePage surface); a thin +# client — all backend logic lives in core/ and the engine process. + +find_package(Qt6 REQUIRED COMPONENTS Widgets) +find_package(CURL REQUIRED) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +add_executable(agentredactor-gui + main.cpp + app_state.cpp + autostart.cpp + main_window.cpp + password_dialog.cpp + translator_loader.cpp + tray_icon.cpp + ../engine/control_api_client.cpp + resources.qrc +) + +target_include_directories(agentredactor-gui PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../engine +) + +target_link_libraries(agentredactor-gui PRIVATE + agentredactor-core + Qt6::Widgets + CURL::libcurl +) diff --git a/linux/gui/app_state.cpp b/linux/gui/app_state.cpp new file mode 100644 index 0000000..49dcf8e --- /dev/null +++ b/linux/gui/app_state.cpp @@ -0,0 +1,133 @@ +#include "app_state.h" + +#include +#include + +#include "autostart.h" +#include "utils.h" + +using namespace AgentRedactor; + +namespace { + +// Locate the engine binary: next to the GUI in installed layouts, in the +// sibling engine/ dir in the dev build tree (linux/build/gui vs engine). +std::filesystem::path FindEngineBinary() { + const auto appDir = std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()); + const auto direct = appDir / "agentredactor"; + if (std::filesystem::exists(direct)) return direct; + const auto sibling = appDir.parent_path() / "engine" / "agentredactor"; + if (std::filesystem::exists(sibling)) return sibling; + return direct; // let the spawn fail on the canonical name +} + +} // namespace + +AppState::AppState(std::filesystem::path configDir, QObject* parent) + : QObject(parent), configDir_(std::move(configDir)) {} + +AppState::~AppState() { + stopPolling_ = true; + if (pollThread_.joinable()) pollThread_.join(); +} + +bool AppState::EnsureEngineRunning() { + if (client_.Connect(configDir_) && client_.Ping()) return true; + + const auto engine = FindEngineBinary(); + engineSpawned_ = QProcess::startDetached( + QString::fromStdString(engine.string()), {}); + + // The engine loads the ONNX model during startup; allow 30 s. + for (int i = 0; i < 300 && !stopPolling_; ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if (client_.Connect(configDir_) && client_.Ping()) return true; + } + return false; +} + +void AppState::StartPolling() { + stopPolling_ = false; + pollThread_ = std::thread([this] { PollThreadMain(); }); +} + +void AppState::Shutdown(bool protectionEnabled) { + if (shutdownDone_) return; + shutdownDone_ = true; + stopPolling_ = true; + if (pollThread_.joinable()) pollThread_.join(); + if (engineSpawned_) { + // This GUI started the engine, so it owns its lifetime. + client_.StopEngine(); + } else if (protectionEnabled) { + // The engine survives the GUI; lock it so the next open must + // authenticate again. + client_.Lock(); + } +} + +void AppState::PollThreadMain() { + while (!stopPolling_) { + json status, settings; + const bool statusOk = client_.GetStatus(status); + bool settingsOk = false; + if (statusOk) settingsOk = client_.GetSettings(settings); + + QString statusDump, settingsDump; + if (statusOk) statusDump = QString::fromStdString(status.dump()); + if (settingsOk) settingsDump = QString::fromStdString(settings.dump()); + + QMetaObject::invokeMethod(this, "onPolled", Qt::QueuedConnection, + Q_ARG(QString, statusDump), Q_ARG(QString, settingsDump), + Q_ARG(bool, statusOk)); + + // 1 s cadence in 100 ms slices so Shutdown() is not kept waiting. + for (int i = 0; i < 10 && !stopPolling_; ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } +} + +void AppState::onPolled(QString statusDump, QString settingsDump, bool statusOk) { + if (!statusOk) { + emit connectionLost(); + return; + } + + try { + const json status = json::parse(statusDump.toStdString()); + + // Model-download progress is a separate notification so the blocking + // first-run dialog can track it. + std::string modelFields; + for (const char* k : {"modelDownloadRequired", "modelDownloadInProgress", + "modelDownloadFailed", "modelDownloadPercent", "modelDownloadStatus"}) { + modelFields += status.value(k, json()).dump(); + } + const bool modelChanged = modelFields != prevModelFields_; + prevModelFields_ = modelFields; + + lastStatus_ = status; + emit statusUpdated(); + if (modelChanged) emit modelDownloadChanged(); + } catch (...) { + return; + } + + if (!settingsDump.isEmpty() && settingsDump.toStdString() != prevSettingsDump_) { + prevSettingsDump_ = settingsDump.toStdString(); + try { + lastSettings_ = json::parse(prevSettingsDump_); + } catch (...) { + return; + } + // Keep the XDG autostart entry in agreement with the persisted + // setting (e.g. changed via the CLI while the GUI was closed). + const bool startOnBoot = lastSettings_.value("startOnBoot", false); + if (Autostart::IsEnabled() != startOnBoot) { + Autostart::SetEnabled(startOnBoot, + QCoreApplication::applicationFilePath().toStdString()); + } + emit settingsChanged(); + } +} diff --git a/linux/gui/app_state.h b/linux/gui/app_state.h new file mode 100644 index 0000000..f174353 --- /dev/null +++ b/linux/gui/app_state.h @@ -0,0 +1,75 @@ +#pragma once + +// Linux mirror of windows/AppState: owns the EngineClient, the engine +// lifecycle (spawn when unreachable, stop/lock on quit) and the 1-second +// /status + /settings poll loop that keeps the UI in sync with CLI changes. +// +// The poll loop runs on a worker thread (the curl client is blocking); all +// notifications arrive as queued signals on the GUI thread. User-initiated +// mutations go through client() directly on the UI thread — ControlApiClient +// is per-request stateless after Connect, and loopback calls return in +// milliseconds. + +#include +#include +#include + +#include +#include + +#include "engine_client.h" + +class AppState : public QObject { + Q_OBJECT +public: + AppState(std::filesystem::path configDir, QObject* parent = nullptr); + ~AppState() override; + + // Connects to the engine, spawning it (detached, bare argv = engine mode) + // when unreachable; waits up to ~30 s for control.json + /status. + // False = engine could not be started (caller shows an error and exits). + bool EnsureEngineRunning(); + + void StartPolling(); + // protectionEnabled: current masterPasswordEnabled snapshot — when the + // engine survives the GUI, it is locked behind the password again. + void Shutdown(bool protectionEnabled); + + bool engineSpawned() const { return engineSpawned_; } + AgentRedactor::EngineClient& client() { return client_; } + const std::filesystem::path& configDir() const { return configDir_; } + + // Latest snapshots (GUI thread only; written from the slots below). + const json& lastStatus() const { return lastStatus_; } + const json& lastSettings() const { return lastSettings_; } + +signals: + // Every poll tick (stats/matches refresh). + void statusUpdated(); + // Only when the settings JSON actually changed (full reload trigger). + void settingsChanged(); + // Model-download fields changed; payload is the status JSON. + void modelDownloadChanged(); + // The engine stopped answering (post-connect); UI should say so. + void connectionLost(); + +private slots: + void onPolled(QString statusDump, QString settingsDump, bool statusOk); + +private: + void PollThreadMain(); + + std::filesystem::path configDir_; + AgentRedactor::EngineClient client_; + bool engineSpawned_ = false; + + std::thread pollThread_; + std::atomic stopPolling_{false}; + bool shutdownDone_ = false; + + json lastStatus_ = json::object(); + json lastSettings_ = json::object(); + // Worker-thread copies used for diffing before emitting. + std::string prevSettingsDump_; + std::string prevModelFields_; +}; diff --git a/linux/gui/assets/app.png b/linux/gui/assets/app.png new file mode 100644 index 0000000000000000000000000000000000000000..f6f74c2cbed2fd51c9b7daf0dc26e57919abedbe GIT binary patch literal 92248 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 literal 0 HcmV?d00001 diff --git a/linux/gui/autostart.cpp b/linux/gui/autostart.cpp new file mode 100644 index 0000000..fdfd16c --- /dev/null +++ b/linux/gui/autostart.cpp @@ -0,0 +1,45 @@ +#include "autostart.h" + +#include +#include + +namespace { + +std::filesystem::path AutostartDir() { + if (const char* xdg = std::getenv("XDG_CONFIG_HOME"); xdg && *xdg) { + return std::filesystem::path(xdg) / "autostart"; + } + const char* home = std::getenv("HOME"); + return std::filesystem::path(home ? home : ".") / ".config" / "autostart"; +} + +} // namespace + +namespace Autostart { + +std::filesystem::path DesktopFilePath() { + return AutostartDir() / "agentredactor.desktop"; +} + +bool IsEnabled() { + return std::filesystem::exists(DesktopFilePath()); +} + +void SetEnabled(bool enabled, const std::filesystem::path& execPath) { + const auto path = DesktopFilePath(); + if (!enabled) { + std::error_code ec; + std::filesystem::remove(path, ec); + return; + } + std::filesystem::create_directories(path.parent_path()); + std::ofstream f(path, std::ios::trunc); + f << "[Desktop Entry]\n" + << "Type=Application\n" + << "Name=Agent Redactor\n" + << "Exec=" << execPath.string() << " --tray-only\n" + << "X-GNOME-Autostart-enabled=true\n"; + // Desktop entries are user config, not secrets; 644 is conventional. +} + +} // namespace Autostart diff --git a/linux/gui/autostart.h b/linux/gui/autostart.h new file mode 100644 index 0000000..cdd3bbd --- /dev/null +++ b/linux/gui/autostart.h @@ -0,0 +1,22 @@ +#pragma once + +// XDG autostart for the Linux GUI: writes/removes +// $XDG_CONFIG_HOME/autostart/agentredactor.desktop (default +// ~/.config/autostart), mirroring the Windows registry Run-key toggle +// (core/include/constants.h RegisterStartupTask, which stays Windows-only). +// The engine only persists the startOnBoot setting; applying it to the +// autostart dir is GUI-side, exactly as on Windows. + +#include + +namespace Autostart { + +// Path of the .desktop file this install manages. +std::filesystem::path DesktopFilePath(); + +bool IsEnabled(); + +// Writes (execPath --tray-only) or removes the autostart entry. +void SetEnabled(bool enabled, const std::filesystem::path& execPath); + +} // namespace Autostart diff --git a/linux/gui/engine_client.h b/linux/gui/engine_client.h new file mode 100644 index 0000000..d827f46 --- /dev/null +++ b/linux/gui/engine_client.h @@ -0,0 +1,88 @@ +#pragma once + +// GUI-side client for the engine's loopback control API — the Linux mirror +// of windows/EngineClient. Thin typed wrapper over the shared curl +// ControlApiClient (linux/engine/control_api_client.*); plain blocking C++, +// called from AppState's worker thread and (for user-initiated mutations, +// which are sub-millisecond on loopback) the UI thread. + +#include +#include +#include + +#include "control_api_client.h" +#include "utils.h" + +using json = nlohmann::json; + +namespace AgentRedactor { + +class EngineClient { +public: + bool Connect(const std::filesystem::path& configDir) { return client_.Connect(configDir); } + bool IsConnected() const { return client_.IsConnected(); } + + bool Ping() { json s; return GetStatus(s); } + + bool GetStatus(json& out) { return client_.Get(L"/status", out); } + bool GetSettings(json& out) { return client_.Get(L"/settings", out); } + bool GetProfiles(json& out) { return client_.Get(L"/profiles", out); } + + // False + WasLocked() means the session is locked (403). + bool GetApiKey(const std::wstring& profileId, std::wstring& keyOut) { + json out; + if (!client_.Get(L"/profiles/" + profileId + L"/apikey", out)) return false; + keyOut = Utils::Utf8ToWide(out.value("apiKey", std::string(""))); + return true; + } + + bool PostProfile(const json& profile, std::wstring& idOut) { + json out; + if (!client_.Post(L"/profiles", profile, &out)) return false; + idOut = Utils::Utf8ToWide(out.value("id", std::string(""))); + return true; + } + + bool PutProfile(const std::wstring& profileId, const json& profile) { + return client_.Put(L"/profiles/" + profileId, profile, nullptr); + } + + bool DeleteProfile(const std::wstring& profileId) { + return client_.Delete(L"/profiles/" + profileId); + } + + bool PutSetting(const std::wstring& key, const json& value) { + return client_.Put(L"/settings/" + key, json{{"value", value}}, nullptr); + } + + bool Unlock(const std::wstring& password) { return client_.UnlockWithPassword(password); } + bool Lock() { return PutSetting(L"lock", json::object()); } + + bool GetMatches(const std::wstring& profileId, json& out) { + return client_.Get(L"/profiles/" + profileId + L"/matches", out); + } + bool DeleteMatches(const std::wstring& profileId) { + return client_.Delete(L"/profiles/" + profileId + L"/matches"); + } + + bool RestartListeners() { return client_.Post(L"/engine/restart-listeners", json::object(), nullptr); } + bool DownloadModel() { return client_.Post(L"/engine/download-model", json::object(), nullptr); } + bool StopEngine() { return client_.Post(L"/engine/stop", json::object(), nullptr); } + + bool EnableMasterPassword(const std::wstring& password) { + return client_.Put(L"/settings/enableMasterPassword", + json{{"password", Utils::WideToUtf8(password)}}, nullptr); + } + + bool DisableMasterPassword() { + return client_.Put(L"/settings/disableMasterPassword", json{{"value", false}}, nullptr); + } + + long LastStatus() const { return client_.LastStatus(); } + bool WasLocked() const { return client_.LastStatus() == 403; } + +private: + ControlApiClient client_; +}; + +} // namespace AgentRedactor diff --git a/linux/gui/main.cpp b/linux/gui/main.cpp new file mode 100644 index 0000000..ce3b887 --- /dev/null +++ b/linux/gui/main.cpp @@ -0,0 +1,84 @@ +// Linux GUI entry point. Thin shell: constructs the Qt app, ensures the +// engine is running (spawning it detached when not), and shows the main +// window / tray. All backend logic lives in the engine process. + +#include + +#include +#include +#include + +#include +#include + +#include "app_state.h" +#include "main_window.h" +#include "tray_icon.h" +#include "translator_loader.h" +#include "utils.h" + +namespace { + +// Self-pipe SIGTERM/SIGINT bridge: the handler only writes a byte; the +// QSocketNotifier turns it into a graceful QApplication::quit on the GUI +// thread so AppState::Shutdown (engine stop/lock) still runs. +int g_signalFds[2] = {-1, -1}; + +void onSignal(int sig) { + if (g_signalFds[1] >= 0) { + const char b = static_cast(sig); + if (write(g_signalFds[1], &b, 1) < 0) { /* nothing sensible to do */ } + } +} + +} // namespace + +int main(int argc, char* argv[]) { + if (socketpair(AF_UNIX, SOCK_STREAM, 0, g_signalFds) == 0) { + struct sigaction sa{}; + sa.sa_handler = onSignal; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(SIGTERM, &sa, nullptr); + sigaction(SIGINT, &sa, nullptr); + } + + QApplication app(argc, argv); + QApplication::setApplicationName(QStringLiteral("agentredactor")); + QApplication::setOrganizationName(QStringLiteral("NegativeStarInnovators")); + QApplication::setWindowIcon(QIcon(QStringLiteral(":/app.png"))); + // Closing the last window must not quit the app when the tray keeps it + // alive; MainWindow decides when a close is a real quit. + QApplication::setQuitOnLastWindowClosed(false); + + QSocketNotifier notifier(g_signalFds[0], QSocketNotifier::Read); + if (g_signalFds[0] >= 0) { + notifier.setEnabled(true); + QObject::connect(¬ifier, &QSocketNotifier::activated, &app, + [&app] { QApplication::quit(); }); + } + + const bool trayOnly = QApplication::arguments().contains(QLatin1String("--tray-only")); + + TranslatorLoader translator(app); + + AppState appState(AgentRedactor::Utils::GetAppDataPath()); + if (!appState.EnsureEngineRunning()) { + QMessageBox::critical(nullptr, QStringLiteral("Agent Redactor"), + QStringLiteral("The Agent Redactor engine could not be started.")); + return 1; + } + + TrayIcon tray; + MainWindow window(&appState, &tray, &translator, trayOnly); + tray.showIcon(); + QObject::connect(&tray, &TrayIcon::openRequested, &window, &MainWindow::openWindow); + QObject::connect(&tray, &TrayIcon::quitRequested, &window, &MainWindow::onQuitRequested); + QObject::connect(&tray, &TrayIcon::startOnBootToggled, &window, &MainWindow::onStartOnBootToggled); + + appState.StartPolling(); + + const int rc = QApplication::exec(); + if (g_signalFds[0] >= 0) { close(g_signalFds[0]); close(g_signalFds[1]); } + return rc; +} diff --git a/linux/gui/main_window.cpp b/linux/gui/main_window.cpp new file mode 100644 index 0000000..9d13f8a --- /dev/null +++ b/linux/gui/main_window.cpp @@ -0,0 +1,1169 @@ +#include "main_window.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "app_state.h" +#include "autostart.h" +#include "constants.h" +#include "http_server.h" +#include "password_dialog.h" +#include "tray_icon.h" +#include "translator_loader.h" +#include "utils.h" + +using namespace AgentRedactor; + +namespace { + +QString q(const std::wstring& ws) { return QString::fromStdWString(ws); } +std::wstring w(const QString& s) { return s.toStdWString(); } + +// Card container helper: titled group box with a vertical layout. +QGroupBox* makeCard(const QString& title, QVBoxLayout*& layoutOut, QWidget* parent) { + auto* box = new QGroupBox(title, parent); + layoutOut = new QVBoxLayout(box); + return box; +} + +} // namespace + +MainWindow::MainWindow(AppState* appState, TrayIcon* tray, TranslatorLoader* translator, + bool trayOnly, QWidget* parent) + : QMainWindow(parent), appState_(appState), tray_(tray), translator_(translator) { + setWindowTitle(tr("Agent Redactor")); + setWindowIcon(QIcon(QStringLiteral(":/app.png"))); + resize(1000, 900); + + buildUi(); + + auto* fileMenu = menuBar()->addMenu(tr("&File")); + auto* quitAction = fileMenu->addAction(tr("&Quit")); + connect(quitAction, &QAction::triggered, this, &MainWindow::onQuitRequested); + + connect(appState_, &AppState::statusUpdated, this, &MainWindow::onStatusUpdated); + connect(appState_, &AppState::settingsChanged, this, &MainWindow::onSettingsChanged); + connect(appState_, &AppState::modelDownloadChanged, this, &MainWindow::onModelDownloadChanged); + connect(appState_, &AppState::connectionLost, this, &MainWindow::onConnectionLost); + + // 10-minute inactivity re-lock, reset by any key/mouse activity (mirrors + // the Windows message-filter timer). + inactivityTimer_ = new QTimer(this); + inactivityTimer_->setSingleShot(true); + inactivityTimer_->setInterval(10 * 60 * 1000); + connect(inactivityTimer_, &QTimer::timeout, this, [this] { + if (isProtected() && isUnlocked()) appState_->client().Lock(); + }); + qApp->installEventFilter(this); + + // The settings snapshot may not have arrived yet when the window first + // shows; retry the lock enforcement every second until it has (mirrors + // MainWindow::EnsureLockState's DispatcherQueue retry). + lockRetryTimer_ = new QTimer(this); + lockRetryTimer_->setInterval(1000); + connect(lockRetryTimer_, &QTimer::timeout, this, [this] { ensureLockState(false); }); + lockRetryTimer_->start(); + + // Shutdown (engine stop/lock decision) on every quit path: tray/menu Quit, + // window close in control-panel mode, SIGTERM. + connect(qApp, &QApplication::aboutToQuit, this, [this] { + appState_->Shutdown(isProtected()); + }); + + if (!trayOnly || !tray_->available()) { + // Control-panel fallback: without a tray a hidden window would leave + // the user with no UI at all, so --tray-only is ignored there. + openWindow(); + } +} + +// --------------------------------------------------------------------------- +// UI construction +// --------------------------------------------------------------------------- + +void MainWindow::buildUi() { + auto* central = new QWidget(this); + centralStack_ = new QStackedLayout(central); + + // ---- Page 0: content ---- + auto* content = new QWidget(central); + auto* contentLayout = new QHBoxLayout(content); + + auto* splitter = new QSplitter(content); + + // Sidebar: profile list + add/remove + auto* sidebar = new QWidget(splitter); + auto* sidebarLayout = new QVBoxLayout(sidebar); + profileList_ = new QListWidget(sidebar); + connect(profileList_, &QListWidget::currentRowChanged, + this, &MainWindow::onProfileSelectionChanged); + sidebarLayout->addWidget(profileList_); + auto* sideBtns = new QHBoxLayout; + addProfileBtn_ = new QPushButton(sidebar); + removeProfileBtn_ = new QPushButton(sidebar); + connect(addProfileBtn_, &QPushButton::clicked, this, &MainWindow::onAddProfile); + connect(removeProfileBtn_, &QPushButton::clicked, this, &MainWindow::onRemoveProfile); + sideBtns->addWidget(addProfileBtn_); + sideBtns->addWidget(removeProfileBtn_); + sidebarLayout->addLayout(sideBtns); + sidebar->setMinimumWidth(220); + sidebar->setMaximumWidth(280); + splitter->addWidget(sidebar); + + // Cards in a scroll area + auto* scroll = new QScrollArea(splitter); + scroll->setWidgetResizable(true); + auto* cards = new QWidget(scroll); + auto* cardsLayout = new QVBoxLayout(cards); + + auto markDirty = [this] { if (!loading_) dirty_ = true; }; + + // -- Profile card -- + QVBoxLayout* profileLayout; + auto* profileCard = makeCard(QString(), profileLayout, cards); // title set in retranslateUi + profileCard->setObjectName(QStringLiteral("profileCard")); + auto* profileForm = new QFormLayout; + aliasBox_ = new QLineEdit(profileCard); + portBox_ = new QLineEdit(profileCard); + urlBox_ = new QLineEdit(profileCard); + apiKeyBox_ = new QLineEdit(profileCard); + apiKeyBox_->setEchoMode(QLineEdit::Password); + profileForm->addRow(QStringLiteral("Name:"), aliasBox_); + profileForm->addRow(QStringLiteral("Port:"), portBox_); + profileForm->addRow(QStringLiteral("Upstream URL:"), urlBox_); + profileForm->addRow(QStringLiteral("API key:"), apiKeyBox_); + profileLayout->addLayout(profileForm); + connect(aliasBox_, &QLineEdit::textEdited, this, markDirty); + connect(portBox_, &QLineEdit::textEdited, this, markDirty); + connect(urlBox_, &QLineEdit::textEdited, this, markDirty); + connect(apiKeyBox_, &QLineEdit::textEdited, this, markDirty); + + auto* profileBtns = new QHBoxLayout; + showKeyCheck_ = new QCheckBox(profileCard); + connect(showKeyCheck_, &QCheckBox::toggled, this, &MainWindow::onToggleApiKeyVisible); + copyUrlBtn_ = new QPushButton(profileCard); + connect(copyUrlBtn_, &QPushButton::clicked, this, &MainWindow::onCopyUrl); + saveBtn_ = new QPushButton(profileCard); + connect(saveBtn_, &QPushButton::clicked, this, &MainWindow::onSaveProfile); + profileBtns->addWidget(showKeyCheck_); + profileBtns->addStretch(); + profileBtns->addWidget(copyUrlBtn_); + profileBtns->addWidget(saveBtn_); + profileLayout->addLayout(profileBtns); + cardsLayout->addWidget(profileCard); + + // -- Detection card -- + QVBoxLayout* detectionLayout; + auto* detectionCard = makeCard(QString(), detectionLayout, cards); + detectionCard->setObjectName(QStringLiteral("detectionCard")); + auto* detectionForm = new QFormLayout; + useAiCheck_ = new QCheckBox(detectionCard); + confidenceBox_ = new QLineEdit(detectionCard); + detectionForm->addRow(QStringLiteral("Use AI model:"), useAiCheck_); + detectionForm->addRow(QStringLiteral("Confidence threshold:"), confidenceBox_); + detectionLayout->addLayout(detectionForm); + connect(useAiCheck_, &QCheckBox::toggled, this, markDirty); + connect(confidenceBox_, &QLineEdit::textEdited, this, markDirty); + auto* piiGrid = new QGridLayout; + int row = 0, col = 0; + for (const auto& type : DEFAULT_PII_TYPES) { + auto* check = new QCheckBox(piiTypeLabel(type), detectionCard); + connect(check, &QCheckBox::toggled, this, markDirty); + piiChecks_.emplace_back(type, check); + piiGrid->addWidget(check, row, col); + if (++col == 4) { col = 0; ++row; } + } + detectionLayout->addLayout(piiGrid); + cardsLayout->addWidget(detectionCard); + + // -- Regex card -- + QVBoxLayout* regexLayout; + auto* regexCard = makeCard(QString(), regexLayout, cards); + regexCard->setObjectName(QStringLiteral("regexCard")); + regexRows_ = new QVBoxLayout; + regexLayout->addLayout(regexRows_); + auto* newRegexRow = new QHBoxLayout; + newRegexBox_ = new QLineEdit(regexCard); + auto* addRegexBtn = new QPushButton(regexCard); + addRegexBtn->setObjectName(QStringLiteral("addRegexBtn")); + connect(addRegexBtn, &QPushButton::clicked, this, &MainWindow::onAddRegex); + connect(newRegexBox_, &QLineEdit::returnPressed, this, &MainWindow::onAddRegex); + newRegexRow->addWidget(newRegexBox_); + newRegexRow->addWidget(addRegexBtn); + regexLayout->addLayout(newRegexRow); + cardsLayout->addWidget(regexCard); + + // -- Keywords card -- + QVBoxLayout* keywordsLayout; + auto* keywordsCard = makeCard(QString(), keywordsLayout, cards); + keywordsCard->setObjectName(QStringLiteral("keywordsCard")); + keywordRows_ = new QVBoxLayout; + keywordsLayout->addLayout(keywordRows_); + auto* newKeywordRow = new QHBoxLayout; + newKeywordBox_ = new QLineEdit(keywordsCard); + newKeywordCaseCheck_ = new QCheckBox(keywordsCard); + newKeywordCaseCheck_->setObjectName(QStringLiteral("newKeywordCaseCheck")); + auto* addKeywordBtn = new QPushButton(keywordsCard); + addKeywordBtn->setObjectName(QStringLiteral("addKeywordBtn")); + connect(addKeywordBtn, &QPushButton::clicked, this, &MainWindow::onAddKeyword); + connect(newKeywordBox_, &QLineEdit::returnPressed, this, &MainWindow::onAddKeyword); + newKeywordRow->addWidget(newKeywordBox_); + newKeywordRow->addWidget(newKeywordCaseCheck_); + newKeywordRow->addWidget(addKeywordBtn); + keywordsLayout->addLayout(newKeywordRow); + cardsLayout->addWidget(keywordsCard); + + // -- Password card -- + QVBoxLayout* passwordLayout; + auto* passwordCard = makeCard(QString(), passwordLayout, cards); + passwordCard->setObjectName(QStringLiteral("passwordCard")); + requirePasswordCheck_ = new QCheckBox(passwordCard); + connect(requirePasswordCheck_, &QCheckBox::toggled, this, &MainWindow::onRequirePasswordToggled); + passwordLayout->addWidget(requirePasswordCheck_); + cardsLayout->addWidget(passwordCard); + + // -- Statistics card -- + QVBoxLayout* statsLayout; + auto* statsCard = makeCard(QString(), statsLayout, cards); + statsCard->setObjectName(QStringLiteral("statsCard")); + statsLabel_ = new QLabel(statsCard); + statsLayout->addWidget(statsLabel_); + auto* clearStatsBtn = new QPushButton(statsCard); + clearStatsBtn->setObjectName(QStringLiteral("clearStatsBtn")); + connect(clearStatsBtn, &QPushButton::clicked, this, &MainWindow::onClearStatistics); + statsLayout->addWidget(clearStatsBtn, 0, Qt::AlignLeft); + cardsLayout->addWidget(statsCard); + + // -- Session redactions card -- + QVBoxLayout* matchesLayout2; + auto* matchesCard = makeCard(QString(), matchesLayout2, cards); + matchesCard->setObjectName(QStringLiteral("matchesCard")); + matchesList_ = new QListWidget(matchesCard); + matchesList_->setMinimumHeight(120); + matchesLayout2->addWidget(matchesList_); + auto* clearMatchesBtn = new QPushButton(matchesCard); + clearMatchesBtn->setObjectName(QStringLiteral("clearMatchesBtn")); + connect(clearMatchesBtn, &QPushButton::clicked, this, &MainWindow::onClearMatches); + matchesLayout2->addWidget(clearMatchesBtn, 0, Qt::AlignLeft); + cardsLayout->addWidget(matchesCard); + + // -- Logs card -- + QVBoxLayout* logsLayout; + auto* logsCard = makeCard(QString(), logsLayout, cards); + logsCard->setObjectName(QStringLiteral("logsCard")); + loggingCheck_ = new QCheckBox(logsCard); + connect(loggingCheck_, &QCheckBox::toggled, this, &MainWindow::onLoggingToggled); + showSensitiveCheck_ = new QCheckBox(logsCard); + connect(showSensitiveCheck_, &QCheckBox::toggled, this, &MainWindow::onShowSensitiveToggled); + logsLayout->addWidget(loggingCheck_); + logsLayout->addWidget(showSensitiveCheck_); + auto* logBtns = new QHBoxLayout; + auto* openLogBtn = new QPushButton(logsCard); + openLogBtn->setObjectName(QStringLiteral("openLogBtn")); + connect(openLogBtn, &QPushButton::clicked, this, &MainWindow::onOpenLog); + auto* openFolderBtn = new QPushButton(logsCard); + openFolderBtn->setObjectName(QStringLiteral("openFolderBtn")); + connect(openFolderBtn, &QPushButton::clicked, this, &MainWindow::onOpenLogFolder); + auto* clearLogsBtn = new QPushButton(logsCard); + clearLogsBtn->setObjectName(QStringLiteral("clearLogsBtn")); + connect(clearLogsBtn, &QPushButton::clicked, this, &MainWindow::onClearLogs); + logBtns->addWidget(openLogBtn); + logBtns->addWidget(openFolderBtn); + logBtns->addWidget(clearLogsBtn); + logBtns->addStretch(); + logsLayout->addLayout(logBtns); + cardsLayout->addWidget(logsCard); + + // -- Settings card -- + QVBoxLayout* settingsLayout; + auto* settingsCard = makeCard(QString(), settingsLayout, cards); + settingsCard->setObjectName(QStringLiteral("settingsCard")); + startOnBootCheck_ = new QCheckBox(settingsCard); + connect(startOnBootCheck_, &QCheckBox::toggled, this, &MainWindow::onStartOnBootToggled); + settingsLayout->addWidget(startOnBootCheck_); + cardsLayout->addWidget(settingsCard); + + cardsLayout->addStretch(); + scroll->setWidget(cards); + splitter->addWidget(cards); + splitter->setStretchFactor(1, 1); + + contentLayout->addWidget(splitter); + centralStack_->addWidget(content); + + // ---- Page 1: lock overlay (opaque; content page is hidden while shown) ---- + lockOverlay_ = new QWidget(central); + lockOverlay_->setStyleSheet(QStringLiteral("background-color: #202020; color: white;")); + auto* overlayOuter = new QVBoxLayout(lockOverlay_); + overlayOuter->addStretch(); + auto* overlayBox = new QVBoxLayout; + auto* lockTitle = new QLabel(lockOverlay_); + lockTitle->setObjectName(QStringLiteral("lockTitle")); + lockTitle->setAlignment(Qt::AlignCenter); + auto f = lockTitle->font(); + f.setPointSize(16); + f.setBold(true); + lockTitle->setFont(f); + overlayBox->addWidget(lockTitle); + unlockBox_ = new QLineEdit(lockOverlay_); + unlockBox_->setEchoMode(QLineEdit::Password); + unlockBox_->setMaximumWidth(300); + connect(unlockBox_, &QLineEdit::returnPressed, this, &MainWindow::onUnlockClicked); + overlayBox->addWidget(unlockBox_, 0, Qt::AlignHCenter); + unlockError_ = new QLabel(lockOverlay_); + unlockError_->setStyleSheet(QStringLiteral("color: #ff8080")); + unlockError_->setAlignment(Qt::AlignCenter); + unlockError_->setVisible(false); + overlayBox->addWidget(unlockError_); + auto* unlockBtn = new QPushButton(lockOverlay_); + unlockBtn->setObjectName(QStringLiteral("unlockBtn")); + unlockBtn->setMaximumWidth(300); + connect(unlockBtn, &QPushButton::clicked, this, &MainWindow::onUnlockClicked); + overlayBox->addWidget(unlockBtn, 0, Qt::AlignHCenter); + overlayOuter->addLayout(overlayBox); + overlayOuter->addStretch(); + centralStack_->addWidget(lockOverlay_); + + setCentralWidget(central); + centralStack_->setCurrentIndex(0); + + retranslateUi(); +} + +void MainWindow::retranslateUi() { + setWindowTitle(tr("Agent Redactor")); + findChild(QStringLiteral("profileCard"))->setTitle(tr("Profile")); + findChild(QStringLiteral("detectionCard"))->setTitle(tr("Detection")); + findChild(QStringLiteral("regexCard"))->setTitle(tr("Regex patterns")); + findChild(QStringLiteral("keywordsCard"))->setTitle(tr("Keywords")); + findChild(QStringLiteral("passwordCard"))->setTitle(tr("Password")); + findChild(QStringLiteral("statsCard"))->setTitle(tr("Statistics")); + findChild(QStringLiteral("matchesCard"))->setTitle(tr("Session redactions")); + findChild(QStringLiteral("logsCard"))->setTitle(tr("Logs")); + findChild(QStringLiteral("settingsCard"))->setTitle(tr("Settings")); + + addProfileBtn_->setText(tr("Add")); + removeProfileBtn_->setText(tr("Remove")); + showKeyCheck_->setText(tr("Show API key")); + copyUrlBtn_->setText(tr("Copy proxy URL")); + saveBtn_->setText(tr("Save")); + useAiCheck_->setText(tr("Use AI model for PII detection")); + newKeywordCaseCheck_->setText(tr("Case sensitive")); + findChild(QStringLiteral("addRegexBtn"))->setText(tr("Add")); + findChild(QStringLiteral("addKeywordBtn"))->setText(tr("Add")); + requirePasswordCheck_->setText(tr("Require master password")); + findChild(QStringLiteral("clearStatsBtn"))->setText(tr("Clear statistics")); + findChild(QStringLiteral("clearMatchesBtn"))->setText(tr("Clear")); + loggingCheck_->setText(tr("Enable logging")); + showSensitiveCheck_->setText(tr("Show sensitive information in logs")); + findChild(QStringLiteral("openLogBtn"))->setText(tr("Open log")); + findChild(QStringLiteral("openFolderBtn"))->setText(tr("Open folder")); + findChild(QStringLiteral("clearLogsBtn"))->setText(tr("Clear logs")); + startOnBootCheck_->setText(tr("Start on boot")); + unlockBox_->setPlaceholderText(tr("Master password")); + findChild(QStringLiteral("unlockBtn"))->setText(tr("Unlock")); + if (auto* t = findChild(QStringLiteral("lockTitle"))) + t->setText(tr("Agent Redactor is locked")); +} + +QString MainWindow::piiTypeLabel(const std::wstring& type) { + // English display labels for the PII grid (Windows: PII_Type_ resw + // keys). Underscores become spaces, first letter capitalized. + QString s = QString::fromStdWString(type); + s.replace(QLatin1Char('_'), QLatin1Char(' ')); + if (!s.isEmpty()) s[0] = s[0].toUpper(); + return s; +} + +// --------------------------------------------------------------------------- +// Poll-driven refresh +// --------------------------------------------------------------------------- + +void MainWindow::onStatusUpdated() { + statusBar()->clearMessage(); + + const json& status = appState_->lastStatus(); + const bool unlocked = status.value("unlocked", true); + + // Lock state: show the overlay when protection is on and the session is + // locked; the poll also lifts it after an external CLI unlock path. + if (isProtected() && !unlocked) { + showLockOverlay(); + } else if (centralStack_->currentIndex() == 1 && (!isProtected() || unlocked)) { + hideLockOverlay(); + } + if (unlocked) inactivityTimer_->start(); + + // Stats + session matches refresh every tick (Windows: UpdateStats + + // LoadMatchesList on the same cadence). + if (json* p = selectedProfile(); p && !dirty_) { + const json& stats = (*p)["stats"]; + statsLabel_->setText(tr("Requests: %1 PII: %2 Regex: %3 Keywords: %4") + .arg(stats.value("total_requests", 0)) + .arg(stats.value("total_pii_detected", 0)) + .arg(stats.value("total_regex_matches", 0)) + .arg(stats.value("total_keyword_matches", 0))); + + const std::wstring id = w(QString::fromStdString((*p)["id"].get())); + json matches; + if (appState_->client().GetMatches(id, matches) && matches.is_array()) { + matchesList_->clear(); + for (auto it = matches.rbegin(); it != matches.rend(); ++it) { + const QString line = QStringLiteral("[%1] %2 (%3): %4") + .arg(QString::fromStdString(it->value("timestamp", std::string()))) + .arg(QString::fromStdString(it->value("type", std::string()))) + .arg(QString::fromStdString(it->value("detail", std::string()))) + .arg(QString::fromStdString(it->value("matchedText", std::string()))); + matchesList_->addItem(line); + } + } + } +} + +void MainWindow::onSettingsChanged() { + const json& settings = appState_->lastSettings(); + + const bool startOnBoot = settings.value("startOnBoot", false); + { + QSignalBlocker b(startOnBootCheck_); + startOnBootCheck_->setChecked(startOnBoot); + } + tray_->setStartOnBoot(startOnBoot); + + { + QSignalBlocker b1(loggingCheck_); + QSignalBlocker b2(showSensitiveCheck_); + loggingCheck_->setChecked(settings.value("loggingEnabled", false)); + showSensitiveCheck_->setChecked(settings.value("showSensitive", false)); + } + + translator_->applyLanguage( + QString::fromStdString(settings.value("appLanguage", std::string()))); + + { + QSignalBlocker b(requirePasswordCheck_); + requirePasswordCheck_->setChecked(isProtected()); + } + + // Cheap full-reload trigger: profile mutations bump profilesRevision. + const uint64_t revision = settings.value("profilesRevision", uint64_t{0}); + if (revision != prevProfilesRevision_) { + prevProfilesRevision_ = revision; + if (!dirty_) reloadProfiles(true); + } + + ensureLockState(false); +} + +void MainWindow::onModelDownloadChanged() { + updateModelDownloadDialog(); +} + +void MainWindow::onConnectionLost() { + statusBar()->showMessage(tr("Engine is not running — retrying…")); +} + +// --------------------------------------------------------------------------- +// Profiles: load / select / save +// --------------------------------------------------------------------------- + +void MainWindow::reloadProfiles(bool keepSelection) { + json profiles; + if (!appState_->client().GetProfiles(profiles) || !profiles.is_array()) return; + + const QString previousId = keepSelection ? selectedProfileId() : QString(); + profiles_ = profiles; + + loading_ = true; + profileList_->clear(); + int selectRow = 0; + for (size_t i = 0; i < profiles_.size(); ++i) { + const auto& p = profiles_[i]; + profileList_->addItem(QString::fromStdString(p.value("alias", std::string()))); + if (!previousId.isEmpty() && + previousId == QString::fromStdString(p.value("id", std::string()))) { + selectRow = static_cast(i); + } + } + if (!profiles_.empty()) { + profileList_->setCurrentRow(selectRow); + loadProfileIntoForm(selectRow); + } + removeProfileBtn_->setEnabled(profiles_.size() > 1); + loading_ = false; + dirty_ = false; +} + +void MainWindow::loadProfileIntoForm(int index) { + if (index < 0 || index >= static_cast(profiles_.size())) return; + loading_ = true; + const json& p = profiles_[index]; + + aliasBox_->setText(QString::fromStdString(p.value("alias", std::string()))); + portBox_->setText(QString::number(p.value("port", 0))); + urlBox_->setText(QString::fromStdString(p.value("upstream_url", std::string()))); + + // The profiles list only ever serves the masked key; fetch the real one + // (403 while locked — then keep the masked placeholder). + const std::wstring id = w(QString::fromStdString(p.value("id", std::string()))); + std::wstring key; + if (appState_->client().GetApiKey(id, key)) { + apiKeyBox_->setText(q(key)); + } else { + apiKeyBox_->setText(QString::fromStdString(p.value("api_key", std::string()))); + } + apiKeyBox_->setEchoMode(QLineEdit::Password); + { + QSignalBlocker b(showKeyCheck_); + showKeyCheck_->setChecked(false); + } + + useAiCheck_->setChecked(p.value("use_openai_model", true)); + confidenceBox_->setText(QString::number(p.value("pii_confidence_threshold", 0.9))); + + const std::vector enabledTypes = + p.value("enabled_pii_types", std::vector{}); + for (auto& [type, check] : piiChecks_) { + QSignalBlocker b(check); + check->setChecked(std::find(enabledTypes.begin(), enabledTypes.end(), + Utils::WideToUtf8(type)) != enabledTypes.end()); + } + + // Rebuild regex rows (each row is a widget so takeAt/delete cleans up). + while (QLayoutItem* item = regexRows_->takeAt(0)) { + delete item->widget(); + delete item; + } + for (const auto& r : p.value("regex_patterns", json::array())) { + auto* rowWidget = new QWidget; + auto* rowLayout = new QHBoxLayout(rowWidget); + rowLayout->setContentsMargins(0, 0, 0, 0); + auto* enabled = new QCheckBox; + enabled->setChecked(r.value("enabled", true)); + auto* pattern = new QLineEdit(QString::fromStdString(r.value("pattern", std::string()))); + auto* del = new QPushButton(tr("Delete")); + rowLayout->addWidget(enabled); + rowLayout->addWidget(pattern, 1); + rowLayout->addWidget(del); + regexRows_->addWidget(rowWidget); + + connect(enabled, &QCheckBox::toggled, this, [this, pattern, enabled] { + json* p = selectedProfile(); + if (!p) return; + const std::string pat = pattern->text().toStdString(); + for (auto& r : (*p)["regex_patterns"]) { + if (r.value("pattern", std::string()) == pat) r["enabled"] = enabled->isChecked(); + } + appState_->client().PutProfile(w(selectedProfileId()), *p); + }); + connect(pattern, &QLineEdit::editingFinished, this, [this, pattern] { + json* p = selectedProfile(); + if (!p) return; + // Validate before applying (mirrors Windows LostFocus validation). + const std::wstring normalized = + Utils::NormalizeRegexBraces(pattern->text().toStdWString()); + try { + std::regex re(Utils::WideToUtf8(normalized), std::regex_constants::ECMAScript); + } catch (const std::regex_error&) { + QMessageBox::warning(this, tr("Invalid regex"), + tr("The pattern is not a valid regular expression.")); + reloadProfiles(true); + return; + } + dirty_ = true; + onSaveProfile(); + }); + connect(del, &QPushButton::clicked, this, [this, pattern] { + json* p = selectedProfile(); + if (!p) return; + const std::string pat = pattern->text().toStdString(); + auto& arr = (*p)["regex_patterns"]; + arr.erase(std::remove_if(arr.begin(), arr.end(), [&](const json& r) { + return r.value("pattern", std::string()) == pat; + }), arr.end()); + if (appState_->client().PutProfile(w(selectedProfileId()), *p)) { + appState_->client().RestartListeners(); + reloadProfiles(true); + } + }); + } + + // Rebuild keyword rows. + while (QLayoutItem* item = keywordRows_->takeAt(0)) { + delete item->widget(); + delete item; + } + for (const auto& k : p.value("keywords", json::array())) { + auto* rowWidget = new QWidget; + auto* rowLayout = new QHBoxLayout(rowWidget); + rowLayout->setContentsMargins(0, 0, 0, 0); + auto* enabled = new QCheckBox; + enabled->setChecked(k.value("enabled", true)); + auto* caseBtn = new QPushButton(k.value("case_sensitive", true) + ? tr("Case: Yes") : tr("Case: No")); + caseBtn->setFixedWidth(90); + auto* text = new QLineEdit(QString::fromStdString(k.value("text", std::string()))); + auto* del = new QPushButton(tr("Delete")); + rowLayout->addWidget(enabled); + rowLayout->addWidget(caseBtn); + rowLayout->addWidget(text, 1); + rowLayout->addWidget(del); + keywordRows_->addWidget(rowWidget); + + auto mutateKeyword = [this, text](std::function mutate) { + json* p = selectedProfile(); + if (!p) return; + const std::string t = text->text().toStdString(); + for (auto& kw : (*p)["keywords"]) { + if (kw.value("text", std::string()) == t) mutate(kw); + } + appState_->client().PutProfile(w(selectedProfileId()), *p); + }; + connect(enabled, &QCheckBox::toggled, this, [this, mutateKeyword](bool on) { + mutateKeyword([on](json& kw) { kw["enabled"] = on; }); + }); + connect(caseBtn, &QPushButton::clicked, this, [this, caseBtn, mutateKeyword] { + const bool newValue = caseBtn->text() == tr("Case: No"); + mutateKeyword([newValue](json& kw) { kw["case_sensitive"] = newValue; }); + caseBtn->setText(newValue ? tr("Case: Yes") : tr("Case: No")); + }); + connect(text, &QLineEdit::editingFinished, this, [this] { + dirty_ = true; + onSaveProfile(); + }); + connect(del, &QPushButton::clicked, this, [this, text] { + json* p = selectedProfile(); + if (!p) return; + const std::string t = text->text().toStdString(); + auto& arr = (*p)["keywords"]; + arr.erase(std::remove_if(arr.begin(), arr.end(), [&](const json& kw) { + return kw.value("text", std::string()) == t; + }), arr.end()); + if (appState_->client().PutProfile(w(selectedProfileId()), *p)) { + appState_->client().RestartListeners(); + reloadProfiles(true); + } + }); + } + + loading_ = false; + dirty_ = false; +} + +QString MainWindow::selectedProfileId() const { + const int row = profileList_->currentRow(); + if (row < 0 || row >= static_cast(profiles_.size())) return {}; + return QString::fromStdString(profiles_[row].value("id", std::string())); +} + +json* MainWindow::selectedProfile() { + const int row = profileList_->currentRow(); + if (row < 0 || row >= static_cast(profiles_.size())) return nullptr; + return &profiles_[row]; +} + +void MainWindow::onProfileSelectionChanged() { + if (!loading_) loadProfileIntoForm(profileList_->currentRow()); +} + +json MainWindow::gatherProfileFromForm() { + json* base = selectedProfile(); + json p = base ? *base : json::object(); + p["alias"] = aliasBox_->text().toStdString(); + p["port"] = portBox_->text().toInt(); + p["upstream_url"] = urlBox_->text().toStdString(); + p["api_key"] = apiKeyBox_->text().toStdString(); + p["use_openai_model"] = useAiCheck_->isChecked(); + p["pii_confidence_threshold"] = confidenceBox_->text().toDouble(); + + std::vector types; + for (const auto& [type, check] : piiChecks_) { + if (check->isChecked()) types.push_back(Utils::WideToUtf8(type)); + } + p["enabled_pii_types"] = types; + return p; +} + +bool MainWindow::validateForm(QString& error, bool& httpWarning) { + httpWarning = false; + + bool ok = false; + const int port = portBox_->text().toInt(&ok); + if (!ok || port < 1024 || port > 65535) { + error = tr("Port must be between 1024 and 65535."); + return false; + } + for (size_t i = 0; i < profiles_.size(); ++i) { + if (static_cast(i) == profileList_->currentRow()) continue; + if (profiles_[i].value("port", 0) == port) { + error = tr("Another profile already uses this port."); + return false; + } + } + + const QString url = urlBox_->text().trimmed(); + const QUrl parsed(url); + if (url.isEmpty() || + !(url.startsWith(QLatin1String("http://")) || url.startsWith(QLatin1String("https://"))) || + parsed.host().isEmpty()) { + error = tr("Upstream URL must start with http:// or https:// and have a host."); + return false; + } + if (url.startsWith(QLatin1String("http://")) && + parsed.host() != QLatin1String("localhost") && + !parsed.host().startsWith(QLatin1String("127."))) { + httpWarning = true; + } + + const double confidence = confidenceBox_->text().toDouble(&ok); + if (!ok || confidence < 0.0 || confidence > 1.0) { + error = tr("Confidence threshold must be between 0 and 1."); + return false; + } + return true; +} + +void MainWindow::onSaveProfile() { + json* p = selectedProfile(); + if (!p) return; + + QString error; + bool httpWarning = false; + if (!validateForm(error, httpWarning)) { + QMessageBox::warning(this, tr("Invalid profile"), error); + reloadProfiles(true); + return; + } + if (httpWarning) { + const auto answer = QMessageBox::warning(this, tr("Plain HTTP upstream"), + tr("The upstream URL uses plain HTTP to a non-local host. Redacted requests " + "will be readable on the network. Save anyway?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (answer != QMessageBox::Yes) { + reloadProfiles(true); + return; + } + } + + const json updated = gatherProfileFromForm(); + if (!appState_->client().PutProfile(w(selectedProfileId()), updated)) { + QMessageBox::warning(this, tr("Save failed"), + tr("The engine rejected the profile. Check the engine log for details.")); + return; + } + appState_->client().RestartListeners(); + dirty_ = false; + reloadProfiles(true); +} + +void MainWindow::onAddProfile() { + // First free port from 8080, excluding ports used by other profiles. + std::vector exclude; + for (const auto& p : profiles_) exclude.push_back(p.value("port", 0)); + const int port = FindAvailablePort(8080, exclude); + + json profile = { + {"alias", tr("Profile %1").arg(profiles_.size() + 1).toStdString()}, + {"upstream_url", ""}, + {"api_key", ""}, + {"port", port}, + {"use_openai_model", true}, + {"protocol_mode", "none"}, + {"enabled_pii_types", json::array()}, + {"pii_confidence_threshold", 0.9}, + {"regex_patterns", json::array()}, + {"keywords", json::array()}, + {"stats", {{"total_requests", 0}, {"total_pii_detected", 0}, + {"total_regex_matches", 0}, {"total_keyword_matches", 0}, + {"pii_type_breakdown", json::object()}}}, + {"enabled", true}, + }; + for (const auto& t : DEFAULT_PII_TYPES) profile["enabled_pii_types"].push_back(Utils::WideToUtf8(t)); + + std::wstring id; + if (!appState_->client().PostProfile(profile, id)) { + QMessageBox::warning(this, tr("Add profile failed"), + tr("The engine rejected the new profile.")); + return; + } + appState_->client().RestartListeners(); + dirty_ = false; + reloadProfiles(false); + // Select the new profile. + for (int i = 0; i < profileList_->count(); ++i) { + if (QString::fromStdString(profiles_[i].value("id", std::string())) == q(id)) { + profileList_->setCurrentRow(i); + break; + } + } +} + +void MainWindow::onRemoveProfile() { + if (profiles_.size() <= 1) return; // the last profile cannot be deleted + const QString alias = profileList_->currentItem() + ? profileList_->currentItem()->text() : QString(); + const auto answer = QMessageBox::question(this, tr("Remove profile"), + tr("Remove profile \"%1\"?").arg(alias)); + if (answer != QMessageBox::Yes) return; + + if (appState_->client().DeleteProfile(w(selectedProfileId()))) { + appState_->client().RestartListeners(); + dirty_ = false; + reloadProfiles(false); + } +} + +void MainWindow::onCopyUrl() { + QGuiApplication::clipboard()->setText( + QStringLiteral("http://localhost:%1/").arg(portBox_->text())); + statusBar()->showMessage(tr("Proxy URL copied to clipboard"), 3000); +} + +void MainWindow::onToggleApiKeyVisible(bool visible) { + apiKeyBox_->setEchoMode(visible ? QLineEdit::Normal : QLineEdit::Password); +} + +// --------------------------------------------------------------------------- +// Regex / keyword add +// --------------------------------------------------------------------------- + +void MainWindow::onAddRegex() { + json* p = selectedProfile(); + const QString pattern = newRegexBox_->text().trimmed(); + if (!p || pattern.isEmpty()) return; + + const std::wstring normalized = Utils::NormalizeRegexBraces(pattern.toStdWString()); + try { + std::regex re(Utils::WideToUtf8(normalized), std::regex_constants::ECMAScript); + } catch (const std::regex_error&) { + QMessageBox::warning(this, tr("Invalid regex"), + tr("The pattern is not a valid regular expression.")); + return; + } + + (*p)["regex_patterns"].push_back( + {{"pattern", Utils::WideToUtf8(normalized)}, {"enabled", true}}); + if (appState_->client().PutProfile(w(selectedProfileId()), *p)) { + appState_->client().RestartListeners(); + newRegexBox_->clear(); + reloadProfiles(true); + } +} + +void MainWindow::onAddKeyword() { + json* p = selectedProfile(); + const QString text = newKeywordBox_->text().trimmed(); + if (!p || text.isEmpty()) return; + + (*p)["keywords"].push_back({{"text", text.toStdString()}, + {"case_sensitive", newKeywordCaseCheck_->isChecked()}, {"enabled", true}}); + if (appState_->client().PutProfile(w(selectedProfileId()), *p)) { + appState_->client().RestartListeners(); + newKeywordBox_->clear(); + reloadProfiles(true); + } +} + +// --------------------------------------------------------------------------- +// Password card + lock overlay +// --------------------------------------------------------------------------- + +bool MainWindow::isProtected() const { + return appState_->lastSettings().value("masterPasswordEnabled", false); +} + +bool MainWindow::isUnlocked() const { + return appState_->lastStatus().value("unlocked", true); +} + +void MainWindow::onRequirePasswordToggled(bool checked) { + if (loading_) return; + if (checked) { + PasswordEnableDialog dlg(this); + if (dlg.exec() != QDialog::Accepted || + !appState_->client().EnableMasterPassword(dlg.password().toStdWString())) { + QSignalBlocker b(requirePasswordCheck_); + requirePasswordCheck_->setChecked(false); + return; + } + // The session stays unlocked after enabling (Windows parity); the + // poll picks up masterPasswordEnabled and refreshes the card. + } else { + // Disabling strips all protection: unlock first (the engine's disable + // endpoint requires an unlocked session), then disable. + PasswordUnlockDialog dlg(this); + for (;;) { + if (dlg.exec() != QDialog::Accepted) break; + if (!appState_->client().Unlock(dlg.password().toStdWString())) { + dlg.setError(tr("Wrong password.")); + continue; + } + break; + } + if (!appState_->client().DisableMasterPassword()) { + QSignalBlocker b(requirePasswordCheck_); + requirePasswordCheck_->setChecked(true); + return; + } + } +} + +void MainWindow::ensureLockState(bool allowPrompt) { + if (appState_->lastSettings().empty()) return; // snapshot not ready; retry timer runs + lockRetryTimer_->stop(); + + if (isProtected() && !isUnlocked()) { + showLockOverlay(); + if (allowPrompt) unlockBox_->setFocus(); + } +} + +void MainWindow::showLockOverlay() { + if (centralStack_->currentIndex() != 1) { + unlockBox_->clear(); + unlockError_->setVisible(false); + centralStack_->setCurrentIndex(1); + } +} + +void MainWindow::hideLockOverlay() { + centralStack_->setCurrentIndex(0); + inactivityTimer_->start(); +} + +void MainWindow::onUnlockClicked() { + const QString password = unlockBox_->text(); + if (password.isEmpty()) return; + if (appState_->client().Unlock(password.toStdWString())) { + hideLockOverlay(); + } else { + unlockError_->setText(tr("Wrong password.")); + unlockError_->setVisible(true); + unlockBox_->selectAll(); + unlockBox_->setFocus(); + } +} + +// --------------------------------------------------------------------------- +// Statistics / matches / logs cards +// --------------------------------------------------------------------------- + +void MainWindow::onClearStatistics() { + json* p = selectedProfile(); + if (!p) return; + (*p)["stats"] = {{"total_requests", 0}, {"total_pii_detected", 0}, + {"total_regex_matches", 0}, {"total_keyword_matches", 0}, + {"pii_type_breakdown", json::object()}}; + appState_->client().PutProfile(w(selectedProfileId()), *p); + reloadProfiles(true); +} + +void MainWindow::onClearMatches() { + json* p = selectedProfile(); + if (!p) return; + if (appState_->client().DeleteMatches(w(selectedProfileId()))) { + matchesList_->clear(); + } +} + +void MainWindow::onLoggingToggled(bool checked) { + if (loading_) return; + if (!appState_->client().PutSetting(L"loggingEnabled", checked)) { + QSignalBlocker b(loggingCheck_); + loggingCheck_->setChecked(!checked); + } +} + +void MainWindow::onShowSensitiveToggled(bool checked) { + if (loading_) return; + if (checked) { + const auto answer = QMessageBox::warning(this, tr("Show sensitive information"), + tr("Sensitive logging writes raw, unredacted values (including API keys) to the " + "log. Only enable it while debugging."), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (answer != QMessageBox::Yes) { + QSignalBlocker b(showSensitiveCheck_); + showSensitiveCheck_->setChecked(false); + return; + } + } + if (!appState_->client().PutSetting(L"showSensitive", checked)) { + // The engine refuses to arm sensitive logging while logging is off. + QSignalBlocker b(showSensitiveCheck_); + showSensitiveCheck_->setChecked(false); + statusBar()->showMessage(tr("Enable logging first."), 3000); + } +} + +void MainWindow::onOpenLog() { + const auto logPath = appState_->configDir() / "agent_redactor.log"; + QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(logPath.string()))); +} + +void MainWindow::onOpenLogFolder() { + QDesktopServices::openUrl( + QUrl::fromLocalFile(QString::fromStdString(appState_->configDir().string()))); +} + +void MainWindow::onClearLogs() { + const auto answer = QMessageBox::question(this, tr("Clear logs"), + tr("Delete the log files? This cannot be undone.")); + if (answer != QMessageBox::Yes) return; + + // Same files the Windows GUI deletes directly on disk. + std::error_code ec; + std::filesystem::remove(appState_->configDir() / "agent_redactor.log", ec); + const auto sessions = appState_->configDir() / "sessions"; + if (std::filesystem::exists(sessions)) { + for (const auto& entry : std::filesystem::directory_iterator(sessions)) { + std::filesystem::remove(entry.path(), ec); + } + } +} + +// --------------------------------------------------------------------------- +// Settings card / tray +// --------------------------------------------------------------------------- + +void MainWindow::onStartOnBootToggled(bool checked) { + if (loading_) return; + if (appState_->client().PutSetting(L"startOnBoot", checked)) { + Autostart::SetEnabled(checked, QCoreApplication::applicationFilePath().toStdString()); + tray_->setStartOnBoot(checked); + } else { + QSignalBlocker b(startOnBootCheck_); + startOnBootCheck_->setChecked(!checked); + } +} + +// --------------------------------------------------------------------------- +// Model download dialog (blocking, non-dismissible) +// --------------------------------------------------------------------------- + +void MainWindow::updateModelDownloadDialog() { + const json& status = appState_->lastStatus(); + const bool required = status.value("modelDownloadRequired", false); + const bool inProgress = status.value("modelDownloadInProgress", false); + const bool failed = status.value("modelDownloadFailed", false); + + if (!required) { + if (modelDialog_) modelDialog_->hide(); + return; + } + + if (!modelDialog_) { + modelDialog_ = new QDialog(this); + modelDialog_->setModal(true); + // No close button: the app cannot serve traffic until the weights exist. + modelDialog_->setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + auto* layout = new QVBoxLayout(modelDialog_); + modelStatusLabel_ = new QLabel(modelDialog_); + modelProgress_ = new QProgressBar(modelDialog_); + modelProgress_->setRange(0, 100); + modelRetryBtn_ = new QPushButton(modelDialog_); + modelRetryBtn_->setObjectName(QStringLiteral("modelRetryBtn")); + connect(modelRetryBtn_, &QPushButton::clicked, this, [this] { + appState_->client().DownloadModel(); + }); + layout->addWidget(modelStatusLabel_); + layout->addWidget(modelProgress_); + layout->addWidget(modelRetryBtn_, 0, Qt::AlignLeft); + } + modelDialog_->setWindowTitle(tr("Downloading language model")); + modelRetryBtn_->setText(tr("Retry")); + + const int percent = status.value("modelDownloadPercent", 0); + modelProgress_->setValue(percent); + modelStatusLabel_->setText(tr("The PII detection model is downloading (%1%).").arg(percent)); + modelRetryBtn_->setEnabled(failed); + if (failed) { + modelStatusLabel_->setText(tr("The model download failed. Check your connection and retry.")); + } else if (!inProgress) { + // Not started yet — kick it off. + appState_->client().DownloadModel(); + } + if (!modelDialog_->isVisible()) modelDialog_->show(); +} + +// --------------------------------------------------------------------------- +// Close / quit / inactivity +// --------------------------------------------------------------------------- + +bool MainWindow::eventFilter(QObject* watched, QEvent* event) { + switch (event->type()) { + case QEvent::KeyPress: + case QEvent::MouseButtonPress: + case QEvent::Wheel: + if (isUnlocked()) inactivityTimer_->start(); + break; + default: + break; + } + return QMainWindow::eventFilter(watched, event); +} + +void MainWindow::closeEvent(QCloseEvent* event) { + if (quitting_ || !tray_->available()) { + // Real quit (control-panel mode: closing the window exits the GUI; + // the engine keeps running). + event->accept(); + return; + } + // Close-to-tray; re-lock so the next open must authenticate. + if (isProtected() && isUnlocked()) appState_->client().Lock(); + hide(); + event->ignore(); +} + +void MainWindow::openWindow() { + showNormal(); + raise(); + activateWindow(); + ensureLockState(true); +} + +void MainWindow::onQuitRequested() { + const auto answer = QMessageBox::question(this, tr("Quit Agent Redactor"), + tray_->available() || appState_->engineSpawned() + ? tr("Quit Agent Redactor? The proxy will stop.") + : tr("Quit Agent Redactor? The engine keeps running in the background.")); + if (answer != QMessageBox::Yes) return; + + quitting_ = true; + // The actual shutdown (engine stop/lock) runs from aboutToQuit, so a bare + // window close in control-panel mode takes the same path. + QApplication::quit(); +} + +void MainWindow::changeEvent(QEvent* event) { + if (event->type() == QEvent::LanguageChange) retranslateUi(); + QMainWindow::changeEvent(event); +} diff --git a/linux/gui/main_window.h b/linux/gui/main_window.h new file mode 100644 index 0000000..9760ffe --- /dev/null +++ b/linux/gui/main_window.h @@ -0,0 +1,173 @@ +#pragma once + +// Linux mirror of windows/MainWindow + HomePage: a single window with a +// profile sidebar and the settings cards (profile, regex, keywords, +// detection, password, statistics, session redactions, logs), plus the lock +// overlay, the blocking model-download dialog, close-to-tray and the +// inactivity re-lock. All strings go through tr(); retranslateUi() applies +// language changes live (TranslatorLoader drives QEvent::LanguageChange). + +#include + +#include "engine_client.h" + +class AppState; +class TrayIcon; +class TranslatorLoader; + +class QCheckBox; +class QCloseEvent; +class QDialog; +class QLabel; +class QLineEdit; +class QListWidget; +class QProgressBar; +class QPushButton; +class QStackedLayout; +class QTimer; +class QVBoxLayout; + +class MainWindow : public QMainWindow { + Q_OBJECT +public: + MainWindow(AppState* appState, TrayIcon* tray, TranslatorLoader* translator, + bool trayOnly, QWidget* parent = nullptr); + + // Show/raise the window and enforce the lock state (tray Open, launch). + void openWindow(); + +public slots: + // Tray menu entry points. + void onStartOnBootToggled(bool checked); + void onQuitRequested(); + +protected: + void closeEvent(QCloseEvent* event) override; + void changeEvent(QEvent* event) override; + bool eventFilter(QObject* watched, QEvent* event) override; + +private slots: + void onStatusUpdated(); + void onSettingsChanged(); + void onModelDownloadChanged(); + void onConnectionLost(); + + void onProfileSelectionChanged(); + void onAddProfile(); + void onRemoveProfile(); + void onSaveProfile(); + void onCopyUrl(); + void onToggleApiKeyVisible(bool visible); + + void onAddRegex(); + void onAddKeyword(); + + void onRequirePasswordToggled(bool checked); + void onUnlockClicked(); + + void onClearStatistics(); + void onClearMatches(); + + void onLoggingToggled(bool checked); + void onShowSensitiveToggled(bool checked); + void onOpenLog(); + void onOpenLogFolder(); + void onClearLogs(); + +private: + // Loading/saving + void reloadProfiles(bool keepSelection); + void loadProfileIntoForm(int index); + json gatherProfileFromForm(); + bool validateForm(QString& error, bool& httpWarning); + QString selectedProfileId() const; + json* selectedProfile(); + + // Lock overlay + void ensureLockState(bool allowPrompt); + void showLockOverlay(); + void hideLockOverlay(); + bool isProtected() const; // settings.masterPasswordEnabled + bool isUnlocked() const; // status.unlocked + + // Model download + void updateModelDownloadDialog(); + + // UI construction + void buildUi(); + void retranslateUi(); + void setCardsEnabled(bool enabled); + + // PII type display label (English; PII_Type_ keys in Windows resw). + static QString piiTypeLabel(const std::wstring& type); + + AppState* appState_ = nullptr; + TrayIcon* tray_ = nullptr; + TranslatorLoader* translator_ = nullptr; + + // Central stack: page 0 = content, page 1 = lock overlay. + QStackedLayout* centralStack_ = nullptr; + + // Sidebar + QListWidget* profileList_ = nullptr; + QPushButton* addProfileBtn_ = nullptr; + QPushButton* removeProfileBtn_ = nullptr; + + // Profile card + QLineEdit* aliasBox_ = nullptr; + QLineEdit* portBox_ = nullptr; + QLineEdit* urlBox_ = nullptr; + QLineEdit* apiKeyBox_ = nullptr; + QCheckBox* showKeyCheck_ = nullptr; + QPushButton* copyUrlBtn_ = nullptr; + QPushButton* saveBtn_ = nullptr; + + // Detection card + QCheckBox* useAiCheck_ = nullptr; + QLineEdit* confidenceBox_ = nullptr; + std::vector> piiChecks_; + + // Regex / keywords cards (rows owned by layout) + QVBoxLayout* regexRows_ = nullptr; + QVBoxLayout* keywordRows_ = nullptr; + QLineEdit* newRegexBox_ = nullptr; + QLineEdit* newKeywordBox_ = nullptr; + QCheckBox* newKeywordCaseCheck_ = nullptr; + + // Password card + QCheckBox* requirePasswordCheck_ = nullptr; + + // Statistics card + QLabel* statsLabel_ = nullptr; + + // Session redactions card + QListWidget* matchesList_ = nullptr; + + // Logs card + QCheckBox* loggingCheck_ = nullptr; + QCheckBox* showSensitiveCheck_ = nullptr; + + // Settings card + QCheckBox* startOnBootCheck_ = nullptr; + + // Lock overlay widgets + QWidget* lockOverlay_ = nullptr; + QLineEdit* unlockBox_ = nullptr; + QLabel* unlockError_ = nullptr; + + // Model download dialog widgets (a non-dismissible QDialog) + QDialog* modelDialog_ = nullptr; + QLabel* modelStatusLabel_ = nullptr; + QProgressBar* modelProgress_ = nullptr; + QPushButton* modelRetryBtn_ = nullptr; + + json profiles_ = json::array(); + uint64_t prevProfilesRevision_ = 0; + bool loading_ = false; // suppress dirty-tracking while populating + bool dirty_ = false; // form edited since last load/save + bool quitting_ = false; // real quit in progress (vs close-to-tray) + bool lockEnforcedOnce_ = false; + + QTimer* inactivityTimer_ = nullptr; + QTimer* lockRetryTimer_ = nullptr; +}; diff --git a/linux/gui/password_dialog.cpp b/linux/gui/password_dialog.cpp new file mode 100644 index 0000000..b5d8a36 --- /dev/null +++ b/linux/gui/password_dialog.cpp @@ -0,0 +1,83 @@ +#include "password_dialog.h" + +#include +#include +#include +#include +#include + +PasswordEnableDialog::PasswordEnableDialog(QWidget* parent) + : QDialog(parent) { + setWindowTitle(tr("Enable password protection")); + setModal(true); + + auto* layout = new QVBoxLayout(this); + layout->addWidget(new QLabel( + tr("Choose a master password for Agent Redactor. It protects your stored " + "API keys on this machine and is unrelated to your login password."), this)); + auto* form = new QFormLayout; + pw1_ = new QLineEdit(this); + pw1_->setEchoMode(QLineEdit::Password); + pw2_ = new QLineEdit(this); + pw2_->setEchoMode(QLineEdit::Password); + form->addRow(tr("New password:"), pw1_); + form->addRow(tr("Confirm password:"), pw2_); + layout->addLayout(form); + + error_ = new QLabel(this); + error_->setStyleSheet(QStringLiteral("color: red")); + error_->setVisible(false); + layout->addWidget(error_); + + auto* buttons = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(buttons, &QDialogButtonBox::accepted, this, &PasswordEnableDialog::onAccept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + layout->addWidget(buttons); +} + +QString PasswordEnableDialog::password() const { return pw1_->text(); } + +void PasswordEnableDialog::onAccept() { + if (pw1_->text().isEmpty()) { + error_->setText(tr("Password must not be empty.")); + error_->setVisible(true); + return; + } + if (pw1_->text() != pw2_->text()) { + error_->setText(tr("Passwords do not match.")); + error_->setVisible(true); + return; + } + accept(); +} + +PasswordUnlockDialog::PasswordUnlockDialog(QWidget* parent) + : QDialog(parent) { + setWindowTitle(tr("Unlock Agent Redactor")); + setModal(true); + + auto* layout = new QVBoxLayout(this); + layout->addWidget(new QLabel(tr("Enter your master password to unlock."), this)); + pw_ = new QLineEdit(this); + pw_->setEchoMode(QLineEdit::Password); + layout->addWidget(pw_); + + error_ = new QLabel(this); + error_->setStyleSheet(QStringLiteral("color: red")); + error_->setVisible(false); + layout->addWidget(error_); + + auto* buttons = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + layout->addWidget(buttons); +} + +QString PasswordUnlockDialog::password() const { return pw_->text(); } + +void PasswordUnlockDialog::setError(const QString& message) { + error_->setText(message); + error_->setVisible(true); +} diff --git a/linux/gui/password_dialog.h b/linux/gui/password_dialog.h new file mode 100644 index 0000000..0ca7925 --- /dev/null +++ b/linux/gui/password_dialog.h @@ -0,0 +1,40 @@ +#pragma once + +// Typed-master-password dialogs for the Linux GUI (Linux has no Windows +// Hello; the master password is app-specific and unrelated to the OS login +// password). + +#include +#include + +class QLineEdit; +class QLabel; + +// Enable flow: new password + confirmation with mismatch/empty validation. +class PasswordEnableDialog : public QDialog { + Q_OBJECT +public: + explicit PasswordEnableDialog(QWidget* parent = nullptr); + QString password() const; + +private: + void onAccept(); + + QLineEdit* pw1_ = nullptr; + QLineEdit* pw2_ = nullptr; + QLabel* error_ = nullptr; +}; + +// Unlock flow: single password field; the caller verifies via POST /unlock +// and re-shows with an error on failure. +class PasswordUnlockDialog : public QDialog { + Q_OBJECT +public: + explicit PasswordUnlockDialog(QWidget* parent = nullptr); + QString password() const; + void setError(const QString& message); + +private: + QLineEdit* pw_ = nullptr; + QLabel* error_ = nullptr; +}; diff --git a/linux/gui/resources.qrc b/linux/gui/resources.qrc new file mode 100644 index 0000000..cd4240b --- /dev/null +++ b/linux/gui/resources.qrc @@ -0,0 +1,7 @@ + + + assets/app.png + + + diff --git a/linux/gui/translator_loader.cpp b/linux/gui/translator_loader.cpp new file mode 100644 index 0000000..dd7c8e6 --- /dev/null +++ b/linux/gui/translator_loader.cpp @@ -0,0 +1,40 @@ +#include "translator_loader.h" + +#include +#include + +#include "constants.h" // SUPPORTED_LANGUAGES, IsLanguageRtl + +TranslatorLoader::TranslatorLoader(QApplication& app, QObject* parent) + : QObject(parent), app_(app) {} + +void TranslatorLoader::applyLanguage(const QString& tag) { + app_.removeTranslator(&translator_); + + QString effective = tag; + if (effective.isEmpty()) effective = QLocale::system().name(); // e.g. de_DE + + // Only exact supported tags load a translation; a system locale like + // de_DE falls back to its base language (de) when supported. + const auto isSupported = [](const QString& t) { + const std::wstring wt = t.toStdWString(); + for (const auto& lang : AgentRedactor::SUPPORTED_LANGUAGES) { + if (lang.tag == wt) return true; + } + return false; + }; + if (!isSupported(effective)) { + const QString base = effective.section(QLatin1Char('_'), 0, 0); + effective = isSupported(base) ? base : QStringLiteral("en"); + } + + if (effective != QLatin1String("en")) { + // Translation catalogs (when they exist) are embedded under :/i18n. + if (translator_.load(QStringLiteral(":/i18n/agentredactor_") + effective)) { + app_.installTranslator(&translator_); + } + } + + const bool rtl = AgentRedactor::IsLanguageRtl(effective.toStdWString()); + app_.setLayoutDirection(rtl ? Qt::RightToLeft : Qt::LeftToRight); +} diff --git a/linux/gui/translator_loader.h b/linux/gui/translator_loader.h new file mode 100644 index 0000000..a192075 --- /dev/null +++ b/linux/gui/translator_loader.h @@ -0,0 +1,29 @@ +#pragma once + +// Live UI translation for the Linux GUI. All user-visible strings are +// written in English via tr(); this loader installs a QTranslator for +// `agentredactor_.qm` when the app's appLanguage setting names another +// supported language, and flips the layout direction for RTL languages. +// No .qm files ship yet — the structure exists so converting the Windows +// Strings//Resources.resw files to .ts/.qm later needs no code changes. + +#include +#include +#include + +class QApplication; + +class TranslatorLoader : public QObject { + Q_OBJECT +public: + explicit TranslatorLoader(QApplication& app, QObject* parent = nullptr); + +public slots: + // tag: BCP-47 code from settings; empty = system locale. Unknown tags or + // missing .qm files fall back to English source strings (tr() defaults). + void applyLanguage(const QString& tag); + +private: + QApplication& app_; + QTranslator translator_; // kept alive while installed +}; diff --git a/linux/gui/tray_icon.cpp b/linux/gui/tray_icon.cpp new file mode 100644 index 0000000..7a0e7d6 --- /dev/null +++ b/linux/gui/tray_icon.cpp @@ -0,0 +1,41 @@ +#include "tray_icon.h" + +#include +#include +#include + +TrayIcon::TrayIcon(QObject* parent) : QObject(parent) { + if (!QSystemTrayIcon::isSystemTrayAvailable()) return; + + tray_ = new QSystemTrayIcon(QIcon(QStringLiteral(":/app.png")), this); + tray_->setToolTip(tr("Agent Redactor")); + + auto* menu = new QMenu(); + auto* openAction = menu->addAction(tr("Open")); + connect(openAction, &QAction::triggered, this, &TrayIcon::openRequested); + + startOnBootAction_ = menu->addAction(tr("Start on boot")); + startOnBootAction_->setCheckable(true); + connect(startOnBootAction_, &QAction::toggled, + this, &TrayIcon::startOnBootToggled); + + menu->addSeparator(); + auto* quitAction = menu->addAction(tr("Quit")); + connect(quitAction, &QAction::triggered, this, &TrayIcon::quitRequested); + + tray_->setContextMenu(menu); + connect(tray_, &QSystemTrayIcon::activated, this, + [this](QSystemTrayIcon::ActivationReason reason) { + if (reason == QSystemTrayIcon::Trigger) emit openRequested(); + }); +} + +bool TrayIcon::available() const { return tray_ != nullptr; } + +void TrayIcon::showIcon() { + if (tray_) tray_->show(); +} + +void TrayIcon::setStartOnBoot(bool enabled) { + if (startOnBootAction_) startOnBootAction_->setChecked(enabled); +} diff --git a/linux/gui/tray_icon.h b/linux/gui/tray_icon.h new file mode 100644 index 0000000..4717f1a --- /dev/null +++ b/linux/gui/tray_icon.h @@ -0,0 +1,33 @@ +#pragma once + +// System tray for the Linux GUI (QSystemTrayIcon / StatusNotifierItem). +// Mirrors the Windows tray: left-click opens the window; menu has Open, +// Start on boot (checkable) and Quit with a confirmation dialog. When no +// system tray is available (plain Wayland GNOME without the AppIndicator +// extension), the app runs as a control-panel window instead — see +// MainWindow's close handling. + +#include +#include + +class QAction; +class QMenu; + +class TrayIcon : public QObject { + Q_OBJECT +public: + explicit TrayIcon(QObject* parent = nullptr); + + bool available() const; + void showIcon(); + void setStartOnBoot(bool enabled); + +signals: + void openRequested(); + void startOnBootToggled(bool enabled); + void quitRequested(); + +private: + QSystemTrayIcon* tray_ = nullptr; + QAction* startOnBootAction_ = nullptr; +}; diff --git a/linux/systemd/agentredactor.service b/linux/systemd/agentredactor.service new file mode 100644 index 0000000..291b93e --- /dev/null +++ b/linux/systemd/agentredactor.service @@ -0,0 +1,23 @@ +# Agent Redactor engine — systemd user unit (headless / boot-time startup). +# +# Install: +# mkdir -p ~/.config/systemd/user +# cp agentredactor.service ~/.config/systemd/user/ +# # edit ExecStart to wherever the engine binary lives +# systemctl --user daemon-reload +# systemctl --user enable --now agentredactor.service +# +# The engine is single-instance per config dir (flock on engine.lock), so a +# desktop session that also autostarts the GUI will reuse this engine; the +# GUI stops the engine on quit only when the GUI itself spawned it. +[Unit] +Description=Agent Redactor engine +After=default.target + +[Service] +ExecStart=%h/.local/bin/agentredactor --console +Restart=on-failure +RestartSec=2 + +[Install] +WantedBy=default.target diff --git a/tests/README.md b/tests/README.md index 3f42a63..e3dc703 100644 --- a/tests/README.md +++ b/tests/README.md @@ -70,5 +70,10 @@ Hello: the Hello-consent tests in `cli/test_cli.py` skip, and `cli/test_cli_linux_password.py` covers the equivalent password flow by piping the password to stdin. +`linux/test_gui_smoke.py` additionally launches the real Qt GUI +(`linux/build/gui/agentredactor-gui`, override with `AGENTREDACTOR_GUI_BIN`) +offscreen (`QT_QPA_PLATFORM=offscreen`) and asserts engine spawn/stop +ownership, lock-on-quit when protected, and XDG autostart reconciliation. + Run each suite in its own pytest process (as above): the suites share the `conftest` module name and cannot be collected in a single invocation. diff --git a/tests/linux/test_gui_smoke.py b/tests/linux/test_gui_smoke.py new file mode 100644 index 0000000..bb1cb2b --- /dev/null +++ b/tests/linux/test_gui_smoke.py @@ -0,0 +1,245 @@ +"""Linux GUI smoke tests (Qt6, QT_QPA_PLATFORM=offscreen). + +These drive the real agentredactor-gui binary: engine spawn/stop ownership, +lock-on-quit when the typed master password is enabled, and XDG autostart +reconciliation against the persisted startOnBoot setting. UI interactions +themselves are not automatable offscreen; the Windows FlaUI suite covers the +equivalent UI-driven flows on Windows. +""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +import time +import urllib.request +from pathlib import Path + +import psutil +import pytest + +_tests_root = Path(__file__).resolve().parent.parent +for _p in (str(_tests_root), str(_tests_root / "gui")): + if _p not in sys.path: + sys.path.insert(0, _p) + +from config_factory import create_settings # noqa: E402 +from gui_process import _find_free_port, _kill_existing_agent_redactor, _wait_for_port # noqa: E402 + +pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="Linux GUI smoke tests") + +PROJECT_ROOT = _tests_root.parent +GUI_BIN = Path( + os.environ.get("AGENTREDACTOR_GUI_BIN") + or PROJECT_ROOT / "linux" / "build" / "gui" / "agentredactor-gui" +) +ENGINE_BIN = PROJECT_ROOT / "linux" / "build" / "engine" / "agentredactor" + + +def _engine_processes() -> list[psutil.Process]: + return [p for p in psutil.process_iter(["name"]) if p.info["name"] == "agentredactor"] + + +def _wait_engine_gone(timeout: float = 15.0) -> bool: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if not _engine_processes(): + return True + time.sleep(0.2) + return False + + +def _control_api(config_dir: Path, path: str) -> dict: + token = json.loads((config_dir / "control.json").read_text(encoding="utf-8"))["token"] + port = json.loads((config_dir / "control.json").read_text(encoding="utf-8"))["port"] + req = urllib.request.Request( + f"http://127.0.0.1:{port}{path}", + headers={"Authorization": f"Bearer {token}"}, + ) + with urllib.request.urlopen(req, timeout=5) as resp: + return json.loads(resp.read()) + + +class GuiProcess: + """Launches the GUI offscreen with an isolated config/XDG home.""" + + def __init__(self, config_dir: Path, xdg_home: Path) -> None: + self.config_dir = config_dir + self.process: subprocess.Popen | None = None + self.env = dict(os.environ) + self.env["AGENTREDACTOR_CONFIG_DIR"] = str(config_dir) + self.env["XDG_CONFIG_HOME"] = str(xdg_home) + self.env["QT_QPA_PLATFORM"] = "offscreen" + + def start(self, *args: str) -> None: + self.process = subprocess.Popen( + [str(GUI_BIN), *args], + env=self.env, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + def stop(self) -> None: + if self.process and self.process.poll() is None: + self.process.terminate() # SIGTERM -> graceful quit path + try: + self.process.wait(timeout=15) + except subprocess.TimeoutExpired: + self.process.kill() + self.process.wait(timeout=5) + self.process = None + + +@pytest.fixture() +def gui_env(tmp_path: Path): + """Isolated config dir with one seeded profile + isolated XDG home.""" + if not GUI_BIN.is_file(): + pytest.skip(f"GUI binary not built: {GUI_BIN}") + if not ENGINE_BIN.is_file(): + pytest.skip(f"engine binary not built: {ENGINE_BIN}") + _kill_existing_agent_redactor() + config_dir = tmp_path / "config" + xdg_home = tmp_path / "xdg" + proxy_port = _find_free_port() + create_settings( + data_dir=config_dir, + upstream_url="http://127.0.0.1:9", # unreachable on purpose + api_key="sk-gui-smoke", + proxy_port=proxy_port, + logging_enabled=True, + keywords=[], + regex_patterns=[], + ) + yield config_dir, xdg_home, proxy_port + _kill_existing_agent_redactor() + + +def _start_engine(config_dir: Path, proxy_port: int) -> None: + env = dict(os.environ) + env["AGENTREDACTOR_CONFIG_DIR"] = str(config_dir) + proc = subprocess.Popen( + [str(ENGINE_BIN)], env=env, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ) + deadline = time.monotonic() + 90 + while not (config_dir / "control.json").exists(): + if proc.poll() is not None: + raise RuntimeError("engine exited during startup") + if time.monotonic() > deadline: + raise RuntimeError("control.json did not appear") + time.sleep(0.1) + if not _wait_for_port(proxy_port, timeout=90.0): + raise RuntimeError("engine proxy port did not open") + + +def test_gui_connects_to_running_engine(gui_env) -> None: + config_dir, xdg_home, proxy_port = gui_env + _start_engine(config_dir, proxy_port) + + gui = GuiProcess(config_dir, xdg_home) + gui.start() + try: + time.sleep(3) + assert gui.process.poll() is None, "GUI exited while the engine was up" + finally: + gui.stop() + assert _engine_processes(), "GUI stopped an engine it did not spawn" + + +def test_gui_spawns_and_stops_engine(gui_env) -> None: + config_dir, xdg_home, _ = gui_env + assert not (config_dir / "control.json").exists() + + gui = GuiProcess(config_dir, xdg_home) + gui.start() + try: + # The GUI spawns the engine itself (model load takes a few seconds). + deadline = time.monotonic() + 60 + while not (config_dir / "control.json").exists(): + assert gui.process.poll() is None, "GUI exited before spawning the engine" + if time.monotonic() > deadline: + raise RuntimeError("GUI did not spawn the engine") + time.sleep(0.2) + assert _engine_processes() + finally: + gui.stop() + # The GUI spawned the engine, so quitting the GUI stops it. + assert _wait_engine_gone(), "GUI-spawned engine survived the GUI" + + +def test_gui_locks_engine_on_quit_when_protected(gui_env) -> None: + config_dir, xdg_home, proxy_port = gui_env + _start_engine(config_dir, proxy_port) + + # Enable the typed master password via the real CLI (piped stdin). + env = dict(os.environ) + env["AGENTREDACTOR_CONFIG_DIR"] = str(config_dir) + r = subprocess.run( + [str(ENGINE_BIN), "password", "enable"], + env=env, input="gui-smoke-pw\ngui-smoke-pw\n", + capture_output=True, text=True, timeout=30, + ) + assert r.returncode == 0, r.stdout + r.stderr + + gui = GuiProcess(config_dir, xdg_home) + gui.start() + try: + time.sleep(3) + assert gui.process.poll() is None + finally: + gui.stop() + + # The engine survives (the GUI did not spawn it) but is locked again. + assert _engine_processes(), "engine should survive a foreign GUI quit" + status = _control_api(config_dir, "/status") + assert status.get("masterPasswordEnabled") is True + assert status.get("unlocked") is False + + +def test_autostart_file_reconciled_with_setting(gui_env) -> None: + config_dir, xdg_home, proxy_port = gui_env + desktop_file = xdg_home / "autostart" / "agentredactor.desktop" + _start_engine(config_dir, proxy_port) + + # Off by default: a GUI run must not create the file. + gui = GuiProcess(config_dir, xdg_home) + gui.start() + time.sleep(3) + gui.stop() + assert not desktop_file.exists() + + # Turn the setting on via the CLI (as a script/AI agent would); the next + # GUI run reconciles the autostart file with it. + env = dict(os.environ) + env["AGENTREDACTOR_CONFIG_DIR"] = str(config_dir) + r = subprocess.run( + [str(ENGINE_BIN), "set", "start-on-boot", "true"], + env=env, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=30, + ) + assert r.returncode == 0, r.stdout + r.stderr + + gui.start() + deadline = time.monotonic() + 10 + while not desktop_file.exists() and time.monotonic() < deadline: + time.sleep(0.2) + gui.stop() + assert desktop_file.exists() + content = desktop_file.read_text(encoding="utf-8") + assert "--tray-only" in content + assert "Exec=" in content + + # And back off via the CLI. + r = subprocess.run( + [str(ENGINE_BIN), "set", "start-on-boot", "false"], + env=env, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=30, + ) + assert r.returncode == 0, r.stdout + r.stderr + + gui.start() + deadline = time.monotonic() + 10 + while desktop_file.exists() and time.monotonic() < deadline: + time.sleep(0.2) + gui.stop() + assert not desktop_file.exists() From e1141e4990dd9196750e1f6adfbfc34598f088a8 Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Tue, 18 Aug 2026 22:00:49 +0000 Subject: [PATCH 06/20] fix(linux): repair main window layout and initial profile load - buildUi added the cards widget to the QSplitter instead of the QScrollArea, leaving the splitter managing three widgets (sidebar, empty scroll area, cards) and squeezing the cards into a narrow strip. - prevProfilesRevision_ initialized to 0 matched a fresh engine's revision, so the first settings snapshot never triggered the initial profile load and the sidebar stayed empty until the first mutation. --- linux/gui/main_window.cpp | 7 ++++--- linux/gui/main_window.h | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/linux/gui/main_window.cpp b/linux/gui/main_window.cpp index 9d13f8a..38c07e1 100644 --- a/linux/gui/main_window.cpp +++ b/linux/gui/main_window.cpp @@ -139,7 +139,7 @@ void MainWindow::buildUi() { splitter->addWidget(sidebar); // Cards in a scroll area - auto* scroll = new QScrollArea(splitter); + auto* scroll = new QScrollArea; scroll->setWidgetResizable(true); auto* cards = new QWidget(scroll); auto* cardsLayout = new QVBoxLayout(cards); @@ -313,8 +313,9 @@ void MainWindow::buildUi() { cardsLayout->addStretch(); scroll->setWidget(cards); - splitter->addWidget(cards); - splitter->setStretchFactor(1, 1); + splitter->addWidget(scroll); + splitter->setStretchFactor(0, 0); // sidebar keeps its fixed width + splitter->setStretchFactor(1, 1); // cards take the remaining space contentLayout->addWidget(splitter); centralStack_->addWidget(content); diff --git a/linux/gui/main_window.h b/linux/gui/main_window.h index 9760ffe..310d836 100644 --- a/linux/gui/main_window.h +++ b/linux/gui/main_window.h @@ -162,7 +162,9 @@ private slots: QPushButton* modelRetryBtn_ = nullptr; json profiles_ = json::array(); - uint64_t prevProfilesRevision_ = 0; + // Sentinel forces the first settings snapshot to trigger a full profile + // load (a fresh engine's revision can legitimately be 0). + uint64_t prevProfilesRevision_ = UINT64_MAX; bool loading_ = false; // suppress dirty-tracking while populating bool dirty_ = false; // form edited since last load/save bool quitting_ = false; // real quit in progress (vs close-to-tray) From ec557089168d2ad2d92b1e16178c3723d8326b88 Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Wed, 19 Aug 2026 01:27:24 +0000 Subject: [PATCH 07/20] feat(linux): add Velopack self-update, AppImage packaging and update E2E - AppUpdateManager (Velopack C/C++ lib, pinned 1.2.0 via linux/fetch-velopack.sh) behind a new AR_SELFRELEASE CMake option, mirroring AGENTREDACTOR_SELFRELEASE: startup check + Settings 'Check for updates' button, restart now/later prompt, WaitExitThenApplyUpdates + restart. Same loopback-only AGENTREDACTOR_UPDATE_FEED and AGENTREDACTOR_UPDATE_AUTOAPPLY test hooks as Windows. - GUI restarts the engine on version mismatch after a self-update (engine ships inside the AppImage next to the GUI); VelopackApp::Run first in main(). - linux/build-release.sh: Release build, AppDir staging (Qt libs/plugins bundled dynamically + LGPL-Qt-notice.txt, $ORIGIN rpaths), vpk pack channel linux. - First run symlinks ~/.local/bin/agentredactor to the bundled CLI. - Cloudflare worker: allow the linux channel and *.AppImage files. - tests/linux/test_update_feed.py: packs a vNext feed and asserts the shipped AppImage self-updates against a loopback feed (real apply + restart verified). --- .gitignore | 5 +- cloudflare/src/routes/updates.js | 13 ++- linux/CMakeLists.txt | 5 + linux/README.md | 37 ++++++ linux/build-release.sh | 104 +++++++++++++++++ linux/engine/CMakeLists.txt | 7 +- linux/fetch-velopack.sh | 35 ++++++ linux/gui/CMakeLists.txt | 26 +++++ linux/gui/app_state.cpp | 18 ++- linux/gui/main.cpp | 37 ++++++ linux/gui/main_window.cpp | 47 ++++++++ linux/gui/main_window.h | 5 + linux/gui/update_manager.cpp | 128 ++++++++++++++++++++ linux/gui/update_manager.h | 51 ++++++++ tests/linux/test_update_feed.py | 195 +++++++++++++++++++++++++++++++ 15 files changed, 703 insertions(+), 10 deletions(-) create mode 100755 linux/build-release.sh create mode 100755 linux/fetch-velopack.sh create mode 100644 linux/gui/update_manager.cpp create mode 100644 linux/gui/update_manager.h create mode 100644 tests/linux/test_update_feed.py diff --git a/.gitignore b/.gitignore index 7c9d58a..6557643 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ # Build outputs windows/build/ windows/packages/ -linux/build/ +linux/build*/ + +# Vendored prebuilt Velopack C/C++ library (fetched via linux/fetch-velopack.sh) +linux/third_party/ # Temporary scripts and artifacts temp/ diff --git a/cloudflare/src/routes/updates.js b/cloudflare/src/routes/updates.js index ab3182c..43825c9 100644 --- a/cloudflare/src/routes/updates.js +++ b/cloudflare/src/routes/updates.js @@ -1,13 +1,14 @@ // Self-release update channels: x64 builds use the original 'win' channel, -// ARM64 builds use 'win-arm64'. Releases are served exclusively from the -// RELEASES_BUCKET R2 binding at / (R2 is the single host — -// no GitHub fallback). CI uploads each release via `vpk upload s3`. -const CHANNEL_PATTERN = /^(win|win-arm64)$/; +// ARM64 builds use 'win-arm64', Linux x64 builds use 'linux'. Releases are +// served exclusively from the RELEASES_BUCKET R2 binding at / +// (R2 is the single host — no GitHub fallback). CI uploads each release via +// `vpk upload s3`. +const CHANNEL_PATTERN = /^(win|win-arm64|linux)$/; // Strict allowlist for files Velopack requests from the update feed: -// releases..json, *.nupkg, *-Setup.exe, *-Portable.zip +// releases..json, *.nupkg, *-Setup.exe, *-Portable.zip, *.AppImage // Case-sensitive, matched against the basename only. -const FILE_PATTERN = /^(releases\.[a-z0-9-]+\.json|[^/\\]+\.nupkg|[^/\\]+-Setup\.exe|[^/\\]+-Portable\.zip)$/; +const FILE_PATTERN = /^(releases\.[a-z0-9-]+\.json|[^/\\]+\.nupkg|[^/\\]+-Setup\.exe|[^/\\]+-Portable\.zip|[^/\\]+\.AppImage)$/; // Versioned nupkgs are immutable; the fixed-name feed/installer files // (releases.*.json, *-Setup.exe, *-Portable.zip) change every release. diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index c91cc9c..d72e109 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -24,6 +24,11 @@ string(STRIP "${AR_VERSION}" AR_VERSION) message(STATUS "Agent Redactor version: ${AR_VERSION}") add_compile_definitions(AR_VERSION_STRING="${AR_VERSION}") +# Self-release builds (Velopack-packaged AppImage) get the updater; plain dev +# builds carry no update code (mirror of AGENTREDACTOR_SELFRELEASE on Windows). +# Requires linux/fetch-velopack.sh to have been run. +option(AR_SELFRELEASE "Enable Velopack self-update in the GUI" OFF) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../core ${CMAKE_BINARY_DIR}/core) if (EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/engine/CMakeLists.txt) diff --git a/linux/README.md b/linux/README.md index 16891f5..64d7d3f 100644 --- a/linux/README.md +++ b/linux/README.md @@ -58,3 +58,40 @@ when none is running, and stops it on quit only when it spawned it. - Password protection on Linux is a typed master password chosen inside the app (not your OS login password). The GUI shows a lock overlay with a password field when the session is locked. + +## Release packaging and self-update (Velopack) + +Self-release builds package the app as a Velopack AppImage with an in-app +updater, mirroring the Windows self-release flow (channel `linux`, same R2 +bucket, same feed worker). Prereqs: the .NET SDK and the pinned Velopack CLI +(`dotnet tool install -g vpk --version 1.2.0` — keep it in sync with +`linux/fetch-velopack.sh`). + +```bash +linux/build-release.sh # Release build (-DAR_SELFRELEASE=ON) + AppDir + vpk pack +``` + +Artifacts land in `linux/build-release/velopack/`: `AgentRedactor.AppImage` +(fixed-name installer/portable binary), `*-linux-full.nupkg` and +`releases.linux.json` (the update feed). Upload mirrors the Windows workflow: + +```bash +vpk upload s3 --bucket agentredactor-releases \ + --endpoint https://$R2_ACCOUNT_ID.r2.cloudflarestorage.com \ + --keyId "$R2_ACCESS_KEY_ID" --secret "$R2_SECRET_ACCESS_KEY" \ + --prefix linux -c linux --outputDir linux/build-release/velopack +``` + +Updater behavior: check at startup plus a "Check for updates" button in +Settings (self-release builds only); when an update is downloaded the app +offers "Restart now / later", applies via Velopack, and restarts. The engine +binary ships inside the AppImage next to the GUI; on version mismatch the GUI +stops and respawns it. Qt is bundled dynamically linked inside the AppImage +with `LGPL-Qt-notice.txt`. First run symlinks the CLI to +`~/.local/bin/agentredactor`. + +Test hooks (self-release builds only, same contract as Windows): +`AGENTREDACTOR_UPDATE_FEED` overrides the feed URL (loopback http only) and +`AGENTREDACTOR_UPDATE_AUTOAPPLY=1` skips the restart prompt. +`tests/linux/test_update_feed.py` runs the full pack-vNext → update → swap +cycle against a local feed; it skips when the pack output or vpk is missing. diff --git a/linux/build-release.sh b/linux/build-release.sh new file mode 100755 index 0000000..fe1a78f --- /dev/null +++ b/linux/build-release.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# Agent Redactor — Linux release build + Velopack AppImage packaging. +# +# Usage: +# linux/build-release.sh # build + pack into linux/build-release/velopack/ +# +# Produces the Velopack linux channel artifacts (AppImage, zsync, nupkg, +# releases.linux.json). Upload to R2 with (same secrets as the Windows flow): +# +# vpk upload s3 --bucket agentredactor-releases \ +# --endpoint https://$R2_ACCOUNT_ID.r2.cloudflarestorage.com \ +# --keyId "$R2_ACCESS_KEY_ID" --secret "$R2_SECRET_ACCESS_KEY" \ +# --prefix linux -c linux --outputDir linux/build-release/velopack +# +# Prereqs: build deps from linux/README.md, dotnet SDK + `vpk` 1.2.0 +# (dotnet tool install -g vpk --version 1.2.0), onnxruntime tarball. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")" && pwd)" +BUILD="${ROOT}/build-release" +STAGE="${BUILD}/appdir" +OUT="${BUILD}/velopack" +VERSION="$(tr -d '[:space:]' < "${ROOT}/../windows/version.txt")" +ONNX_INCLUDE="${ONNXRUNTIME_INCLUDE_DIR:-$HOME/onnxruntime/include}" +ONNX_LIB="${ONNXRUNTIME_LIB:-$HOME/onnxruntime/lib/libonnxruntime.so}" + +VP_QT_PLUGIN_DIR="${QT6_PLUGIN_DIR:-/usr/lib/x86_64-linux-gnu/qt6/plugins}" + +echo "==> Fetching Velopack lib" +bash "${ROOT}/fetch-velopack.sh" + +echo "==> Building (Release, AR_SELFRELEASE=ON, version ${VERSION})" +cmake -S "${ROOT}" -B "${BUILD}" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DAR_SELFRELEASE=ON \ + -DONNXRUNTIME_INCLUDE_DIR="${ONNX_INCLUDE}" \ + -DONNXRUNTIME_LIB="${ONNX_LIB}" +cmake --build "${BUILD}" + +echo "==> Staging AppDir at ${STAGE}" +rm -rf "${STAGE}" "${OUT}" +mkdir -p "${STAGE}/plugins" + +cp "${BUILD}/gui/agentredactor-gui" "${STAGE}/" +cp "${BUILD}/engine/agentredactor" "${STAGE}/" +# DT_NEEDED records the lib-prefixed name (see gui/CMakeLists.txt). +cp "${ROOT}/third_party/velopack/lib/velopack_libc_linux_x64_gnu.so" \ + "${STAGE}/libvelopack_libc_linux_x64_gnu.so" +cp "${ONNX_LIB}" "${STAGE}/" + +# Bundle the shared libraries the two binaries resolve to, minus the +# AppImage-standard system set that must come from the host. +EXCLUDE='^(linux-vdso|ld-linux|libc|libm|libdl|librt|libpthread|libresolv|libnsl|libutil|libz|libGL|libEGL|libX11|libxcb|libXau|libXdmcp|libdrm|libgbm|libwayland-|libxkbcommon|libfontconfig|libfreetype|libexpat|libdbus-1|libsystemd|libglib-2.0|libgobject-2.0|libgio-2.0)\.so' +for bin in "${STAGE}/agentredactor-gui" "${STAGE}/agentredactor"; do + ldd "${bin}" | awk '/=> \// {print $1, $3}' | while read -r name path; do + if [[ "${name}" =~ ${EXCLUDE} ]]; then continue; fi + cp -n "${path}" "${STAGE}/${name}" || true + done +done + +# Qt plugins the GUI actually uses; qt.conf points Qt at the bundled copy. +for group in platforms platformthemes wayland-shell-integration xcbglintegrations imageformats iconengines tls; do + if [ -d "${VP_QT_PLUGIN_DIR}/${group}" ]; then + cp -r "${VP_QT_PLUGIN_DIR}/${group}" "${STAGE}/plugins/" + fi +done +# Plugins drag in their own deps (xcb-cursor etc.); resolve one more pass. +find "${STAGE}/plugins" -name '*.so' -print0 | while IFS= read -r -d '' so; do + ldd "${so}" | awk '/=> \// {print $1, $3}' | while read -r name path; do + if [[ "${name}" =~ ${EXCLUDE} ]]; then continue; fi + cp -n "${path}" "${STAGE}/${name}" || true + done +done + +cat > "${STAGE}/qt.conf" <<'EOF' +[Paths] +Plugins = plugins +EOF + +# Qt is dynamically linked (LGPL-compliant); ship the required notice. +cat > "${STAGE}/LGPL-Qt-notice.txt" <<'EOF' +This application uses Qt (https://www.qt.io/), licensed under the GNU Lesser +General Public License v3. Qt is dynamically linked; you may replace the +bundled Qt libraries with your own build. Qt source code is available from +https://download.qt.io/official_releases/qt/ and corresponding source for the +exact bundled version on request. +EOF + +echo "==> Packing Velopack release" +vpk pack \ + --packId AgentRedactor \ + --packVersion "${VERSION}" \ + --packDir "${STAGE}" \ + --mainExe agentredactor-gui \ + --runtime linux-x64 \ + --channel linux \ + --packTitle "Agent Redactor" \ + --packAuthors "Negative Star Innovators" \ + --icon "${ROOT}/gui/assets/app.png" \ + --categories "Utility;Security" \ + --outputDir "${OUT}" + +echo "==> Done. Artifacts in ${OUT}:" +ls -la "${OUT}" diff --git a/linux/engine/CMakeLists.txt b/linux/engine/CMakeLists.txt index 486a843..b61c911 100644 --- a/linux/engine/CMakeLists.txt +++ b/linux/engine/CMakeLists.txt @@ -8,10 +8,13 @@ add_executable(agentredactor target_link_libraries(agentredactor PRIVATE agentredactor-core) # Dev builds link the onnxruntime shared lib from the tarball location; record -# an rpath so the binary runs without LD_LIBRARY_PATH. +# an rpath so the binary runs without LD_LIBRARY_PATH. $ORIGIN comes first so +# the bundled copy next to the binary (AppImage staging) wins when present. if (ONNXRUNTIME_LIB) get_filename_component(AR_ONNXRUNTIME_LIB_DIR "${ONNXRUNTIME_LIB}" DIRECTORY) - set_target_properties(agentredactor PROPERTIES BUILD_RPATH "${AR_ONNXRUNTIME_LIB_DIR}") + set_target_properties(agentredactor PROPERTIES + BUILD_RPATH "$ORIGIN;${AR_ONNXRUNTIME_LIB_DIR}" + INSTALL_RPATH "$ORIGIN") endif() add_executable(core-smoke core_smoke.cpp) diff --git a/linux/fetch-velopack.sh b/linux/fetch-velopack.sh new file mode 100755 index 0000000..09b407e --- /dev/null +++ b/linux/fetch-velopack.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Downloads the pinned Velopack C/C++ prebuilt library into linux/third_party/velopack/. +# The binary blob is gitignored; this script is the source of truth for the version. +set -euo pipefail + +VP_VERSION="1.2.0" +VP_SHA256="547262ed7a1ab1ff62f580aa53851ede2f1a451ac61b8974eb7bc01117488835" +URL="https://github.com/velopack/velopack/releases/download/${VP_VERSION}/velopack_libc_${VP_VERSION}.zip" + +ROOT="$(cd "$(dirname "$0")" && pwd)" +DEST="${ROOT}/third_party/velopack" + +if [ -f "${DEST}/include/Velopack.hpp" ] && [ -f "${DEST}/lib/velopack_libc_linux_x64_gnu.so" ]; then + echo "velopack_libc ${VP_VERSION} already present in ${DEST}" + exit 0 +fi + +TMP="$(mktemp -d)" +trap 'rm -rf "${TMP}"' EXIT + +echo "Downloading ${URL}" +curl -fsSL -o "${TMP}/velopack_libc.zip" "${URL}" + +echo "${VP_SHA256} ${TMP}/velopack_libc.zip" | sha256sum -c - + +rm -rf "${DEST}" +mkdir -p "${DEST}" +# Only the Linux x64 pieces this project links/ships; other platforms are +# fetched from the same zip if ever needed. +unzip -q "${TMP}/velopack_libc.zip" \ + 'include/*' \ + 'lib/velopack_libc_linux_x64_gnu.so' \ + -d "${DEST}" + +echo "velopack_libc ${VP_VERSION} extracted to ${DEST}" diff --git a/linux/gui/CMakeLists.txt b/linux/gui/CMakeLists.txt index 17be214..0b09b44 100644 --- a/linux/gui/CMakeLists.txt +++ b/linux/gui/CMakeLists.txt @@ -16,6 +16,7 @@ add_executable(agentredactor-gui password_dialog.cpp translator_loader.cpp tray_icon.cpp + update_manager.cpp ../engine/control_api_client.cpp resources.qrc ) @@ -30,3 +31,28 @@ target_link_libraries(agentredactor-gui PRIVATE Qt6::Widgets CURL::libcurl ) + +if (AR_SELFRELEASE) + set(VP_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../third_party/velopack) + if (NOT EXISTS ${VP_ROOT}/lib/velopack_libc_linux_x64_gnu.so) + message(FATAL_ERROR "AR_SELFRELEASE=ON but Velopack lib is missing; run linux/fetch-velopack.sh") + endif() + target_compile_definitions(agentredactor-gui PRIVATE AR_SELFRELEASE) + target_include_directories(agentredactor-gui PRIVATE ${VP_ROOT}/include) + # The vendored .so has no SONAME and no "lib" prefix. Link via a + # lib-prefixed symlink so DT_NEEDED records just the filename, which + # $ORIGIN then resolves both in the build tree and the staged AppDir. + set(VP_LINK_DIR ${CMAKE_BINARY_DIR}/velopack-link) + file(MAKE_DIRECTORY ${VP_LINK_DIR}) + file(CREATE_LINK ${VP_ROOT}/lib/velopack_libc_linux_x64_gnu.so + ${VP_LINK_DIR}/libvelopack_libc_linux_x64_gnu.so SYMBOLIC) + target_link_directories(agentredactor-gui PRIVATE ${VP_LINK_DIR}) + target_link_libraries(agentredactor-gui PRIVATE velopack_libc_linux_x64_gnu) + # Ship the Velopack lib next to the GUI binary (AppDir staging) and find + # it there at runtime. + install(FILES ${VP_ROOT}/lib/velopack_libc_linux_x64_gnu.so + DESTINATION . RENAME libvelopack_libc_linux_x64_gnu.so) + set_target_properties(agentredactor-gui PROPERTIES + BUILD_RPATH "$ORIGIN;${VP_LINK_DIR}" + INSTALL_RPATH "$ORIGIN") +endif() diff --git a/linux/gui/app_state.cpp b/linux/gui/app_state.cpp index 49dcf8e..4d6c518 100644 --- a/linux/gui/app_state.cpp +++ b/linux/gui/app_state.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "autostart.h" #include "utils.h" @@ -32,7 +33,22 @@ AppState::~AppState() { } bool AppState::EnsureEngineRunning() { - if (client_.Connect(configDir_) && client_.Ping()) return true; + if (client_.Connect(configDir_) && client_.Ping()) { + // After a GUI self-update the still-running engine is the old build + // (the engine binary lives next to the GUI inside the AppImage, so + // the updated GUI must respawn it). Restart on version mismatch. + json status; + if (client_.GetStatus(status) && + status.value("engineVersion", std::string()) == std::string(AR_VERSION_STRING)) { + return true; + } + qInfo("[AppState] engine version mismatch; restarting engine"); + client_.StopEngine(); + for (int i = 0; i < 50; ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if (!client_.Connect(configDir_) || !client_.Ping()) break; + } + } const auto engine = FindEngineBinary(); engineSpawned_ = QProcess::startDetached( diff --git a/linux/gui/main.cpp b/linux/gui/main.cpp index ce3b887..0556c99 100644 --- a/linux/gui/main.cpp +++ b/linux/gui/main.cpp @@ -3,10 +3,12 @@ // window / tray. All backend logic lives in the engine process. #include +#include #include #include #include +#include #include #include @@ -17,6 +19,10 @@ #include "translator_loader.h" #include "utils.h" +#ifdef AR_SELFRELEASE +#include +#endif + namespace { // Self-pipe SIGTERM/SIGINT bridge: the handler only writes a byte; the @@ -31,9 +37,38 @@ void onSignal(int sig) { } } +// Terminal discoverability: expose the bundled engine/CLI binary as +// ~/.local/bin/agentredactor. Best-effort and idempotent; only done in the +// installed layout (engine next to the GUI binary), never in the dev tree. +void EnsureCliSymlink() { + namespace fs = std::filesystem; + const fs::path engine = + fs::path(QCoreApplication::applicationDirPath().toStdString()) / "agentredactor"; + if (!fs::exists(engine)) return; + const fs::path binDir = + fs::path(QStandardPaths::writableLocation(QStandardPaths::HomeLocation).toStdString()) + / ".local" / "bin"; + const fs::path link = binDir / "agentredactor"; + std::error_code ec; + if (fs::exists(link, ec) || fs::is_symlink(link, ec)) { + if (fs::read_symlink(link, ec) == engine) return; // already correct + fs::remove(link, ec); + } + fs::create_directories(binDir, ec); + fs::create_symlink(engine, link, ec); + if (ec) qWarning("[main] could not create CLI symlink %s: %s", + link.c_str(), ec.message().c_str()); +} + } // namespace int main(int argc, char* argv[]) { +#ifdef AR_SELFRELEASE + // Velopack startup logic: handles post-update restart/apply hooks and may + // exit or restart the process. Must run before anything else. + Velopack::VelopackApp::Build().Run(); +#endif + if (socketpair(AF_UNIX, SOCK_STREAM, 0, g_signalFds) == 0) { struct sigaction sa{}; sa.sa_handler = onSignal; @@ -60,6 +95,8 @@ int main(int argc, char* argv[]) { const bool trayOnly = QApplication::arguments().contains(QLatin1String("--tray-only")); + EnsureCliSymlink(); + TranslatorLoader translator(app); AppState appState(AgentRedactor::Utils::GetAppDataPath()); diff --git a/linux/gui/main_window.cpp b/linux/gui/main_window.cpp index 38c07e1..9480a7d 100644 --- a/linux/gui/main_window.cpp +++ b/linux/gui/main_window.cpp @@ -38,6 +38,7 @@ #include "password_dialog.h" #include "tray_icon.h" #include "translator_loader.h" +#include "update_manager.h" #include "utils.h" using namespace AgentRedactor; @@ -98,6 +99,44 @@ MainWindow::MainWindow(AppState* appState, TrayIcon* tray, TranslatorLoader* tra appState_->Shutdown(isProtected()); }); + // Self-update (Velopack self-release builds only): startup check plus the + // Settings-card button. Restart prompt mirrors Windows: "later" default. + if (AppUpdateManager::IsSelfRelease()) { + updateMgr_ = new AppUpdateManager(this); + connect(updateMgr_, &AppUpdateManager::updateDownloaded, this, + [this](QString version, bool) { + auto* box = new QMessageBox(QMessageBox::Information, + tr("Update available"), + tr("Version %1 has been downloaded and is ready to install.").arg(version), + QMessageBox::NoButton, this); + QPushButton* now = box->addButton(tr("Restart now"), QMessageBox::AcceptRole); + box->addButton(tr("Restart later"), QMessageBox::RejectRole); + box->setDefaultButton(qobject_cast(box->buttons().last())); + box->exec(); + if (box->clickedButton() == now) updateMgr_->ApplyAndRestart(); + box->deleteLater(); + }); + connect(updateMgr_, &AppUpdateManager::noUpdateFound, this, + [this](bool userInitiated) { + if (userInitiated) + QMessageBox::information(this, tr("Check for updates"), + tr("Agent Redactor is up to date.")); + }); + connect(updateMgr_, &AppUpdateManager::checkFailed, this, + [this](QString message, bool userInitiated) { + if (userInitiated) + QMessageBox::warning(this, tr("Check for updates"), + tr("Could not check for updates: %1").arg(message)); + }); + // The Velopack updater is already waiting for this process to exit; + // skip the quit confirmation and shut down immediately. + connect(updateMgr_, &AppUpdateManager::restartRequested, this, [this] { + quitting_ = true; + QApplication::quit(); + }); + QTimer::singleShot(0, this, [this] { updateMgr_->CheckForUpdates(false); }); + } + if (!trayOnly || !tray_->available()) { // Control-panel fallback: without a tray a hidden window would leave // the user with no UI at all, so --tray-only is ignored there. @@ -309,6 +348,13 @@ void MainWindow::buildUi() { startOnBootCheck_ = new QCheckBox(settingsCard); connect(startOnBootCheck_, &QCheckBox::toggled, this, &MainWindow::onStartOnBootToggled); settingsLayout->addWidget(startOnBootCheck_); + if (AppUpdateManager::IsSelfRelease()) { + checkUpdatesBtn_ = new QPushButton(settingsCard); + connect(checkUpdatesBtn_, &QPushButton::clicked, this, [this] { + updateMgr_->CheckForUpdates(true); + }); + settingsLayout->addWidget(checkUpdatesBtn_, 0, Qt::AlignLeft); + } cardsLayout->addWidget(settingsCard); cardsLayout->addStretch(); @@ -389,6 +435,7 @@ void MainWindow::retranslateUi() { findChild(QStringLiteral("openFolderBtn"))->setText(tr("Open folder")); findChild(QStringLiteral("clearLogsBtn"))->setText(tr("Clear logs")); startOnBootCheck_->setText(tr("Start on boot")); + if (checkUpdatesBtn_) checkUpdatesBtn_->setText(tr("Check for updates")); unlockBox_->setPlaceholderText(tr("Master password")); findChild(QStringLiteral("unlockBtn"))->setText(tr("Unlock")); if (auto* t = findChild(QStringLiteral("lockTitle"))) diff --git a/linux/gui/main_window.h b/linux/gui/main_window.h index 310d836..313ce0c 100644 --- a/linux/gui/main_window.h +++ b/linux/gui/main_window.h @@ -12,6 +12,7 @@ #include "engine_client.h" class AppState; +class AppUpdateManager; class TrayIcon; class TranslatorLoader; @@ -149,6 +150,10 @@ private slots: // Settings card QCheckBox* startOnBootCheck_ = nullptr; + QPushButton* checkUpdatesBtn_ = nullptr; + + // Self-update (Velopack); nullptr in non-self-release builds. + AppUpdateManager* updateMgr_ = nullptr; // Lock overlay widgets QWidget* lockOverlay_ = nullptr; diff --git a/linux/gui/update_manager.cpp b/linux/gui/update_manager.cpp new file mode 100644 index 0000000..26ccde2 --- /dev/null +++ b/linux/gui/update_manager.cpp @@ -0,0 +1,128 @@ +#include "update_manager.h" + +#include +#include + +#ifdef AR_SELFRELEASE + +#include + +#include +#include +#include +#include + +// The Cloudflare worker serves every file under this prefix from the R2 +// releases bucket. The host is shared with Windows; the channel segment for +// Linux x64 builds is "linux" (matches vpk pack -c in build-release.sh). +namespace { +constexpr const char* kUpdateFeedUrl = + "https://api.agentredactor.negativestarinnovators.com/updates/linux"; + +// Test hook (self-release builds only): AGENTREDACTOR_UPDATE_FEED overrides +// the update feed URL so the E2E tests can point at a local feed. Loopback +// only — honoring an arbitrary remote URL would let anyone who can launch +// the app with a custom environment steer updates to an untrusted server. +// Same contract as the Windows build. +std::string GetUpdateFeedUrl() { + if (const char* overrideUrl = std::getenv("AGENTREDACTOR_UPDATE_FEED"); + overrideUrl && *overrideUrl) { + std::string url = overrideUrl; + for (auto& c : url) c = static_cast(std::tolower(static_cast(c))); + if (url.rfind("http://127.0.0.1", 0) == 0 || url.rfind("http://localhost", 0) == 0) { + return overrideUrl; + } + qWarning("[UpdateManager] Ignoring AGENTREDACTOR_UPDATE_FEED (loopback URLs only): %s", + overrideUrl); + } + return kUpdateFeedUrl; +} + +// Test hook (self-release builds only): AGENTREDACTOR_UPDATE_AUTOAPPLY=1 +// skips the restart prompt and applies immediately. Unset in normal use. +bool AutoApplyEnabled() { + const char* value = std::getenv("AGENTREDACTOR_UPDATE_AUTOAPPLY"); + return value && std::string(value) == "1"; +} +} // namespace + +struct AppUpdateManager::Impl { + std::thread worker; + std::atomic busy{false}; + // Set on the worker after a successful download; consumed by + // ApplyAndRestart on the GUI thread. + std::optional pending; +}; + +AppUpdateManager::AppUpdateManager(QObject* parent) : QObject(parent), impl_(std::make_unique()) {} + +AppUpdateManager::~AppUpdateManager() { + if (impl_->worker.joinable()) impl_->worker.join(); +} + +bool AppUpdateManager::IsSelfRelease() { return true; } + +void AppUpdateManager::CheckForUpdates(bool userInitiated) { + bool expected = false; + if (!impl_->busy.compare_exchange_strong(expected, true)) return; + if (impl_->worker.joinable()) impl_->worker.join(); + + impl_->worker = std::thread([this, userInitiated] { + QString version, error; + try { + Velopack::UpdateManager manager(GetUpdateFeedUrl()); + auto update = manager.CheckForUpdates(); + if (update.has_value()) { + manager.DownloadUpdates(update.value()); + version = QString::fromStdString(update->TargetFullRelease.Version); + impl_->pending = std::move(update.value()); + } + } catch (const std::exception& e) { + error = QString::fromUtf8(e.what()); + } + impl_->busy = false; + QMetaObject::invokeMethod(this, "onWorkFinished", Qt::QueuedConnection, + Q_ARG(QString, version), Q_ARG(QString, error), + Q_ARG(bool, userInitiated)); + }); +} + +void AppUpdateManager::onWorkFinished(QString version, QString error, bool userInitiated) { + if (!error.isEmpty()) { + qWarning("[UpdateManager] update check failed: %s", qUtf8Printable(error)); + emit checkFailed(error, userInitiated); + } else if (version.isEmpty()) { + emit noUpdateFound(userInitiated); + } else if (AutoApplyEnabled()) { + qInfo("[UpdateManager] AGENTREDACTOR_UPDATE_AUTOAPPLY=1; applying without prompting"); + ApplyAndRestart(); + } else { + emit updateDownloaded(version, userInitiated); + } +} + +void AppUpdateManager::ApplyAndRestart() { + if (!impl_->pending.has_value()) return; + try { + Velopack::UpdateManager manager(GetUpdateFeedUrl()); + manager.WaitExitThenApplyUpdates(impl_->pending.value()); + } catch (const std::exception& e) { + qWarning("[UpdateManager] apply failed: %s", e.what()); + emit checkFailed(QString::fromUtf8(e.what()), true); + return; + } + emit restartRequested(); +} + +#else // !AR_SELFRELEASE — no-op stubs (mirrors the Windows Store/MSIX stubs) + +struct AppUpdateManager::Impl {}; + +AppUpdateManager::AppUpdateManager(QObject* parent) : QObject(parent), impl_(std::make_unique()) {} +AppUpdateManager::~AppUpdateManager() = default; +bool AppUpdateManager::IsSelfRelease() { return false; } +void AppUpdateManager::CheckForUpdates(bool) {} +void AppUpdateManager::onWorkFinished(QString, QString, bool) {} +void AppUpdateManager::ApplyAndRestart() {} + +#endif diff --git a/linux/gui/update_manager.h b/linux/gui/update_manager.h new file mode 100644 index 0000000..f8a95f8 --- /dev/null +++ b/linux/gui/update_manager.h @@ -0,0 +1,51 @@ +#pragma once + +// Linux self-update manager — Velopack C/C++ equivalent of +// windows/src/update_manager.cpp. Compiled to real code only in self-release +// builds (AR_SELFRELEASE, mirroring AGENTREDACTOR_SELFRELEASE); otherwise all +// entry points are no-ops and IsSelfRelease() is false, so packaged-by-other- +// means builds carry no update code. +// +// Threading: CheckForUpdates runs the blocking Velopack calls on a worker +// thread and reports back on the GUI thread via signals. The restart prompt +// and ApplyAndRestart run on the GUI thread. + +#include + +#include +#include + +class AppUpdateManager : public QObject { + Q_OBJECT +public: + explicit AppUpdateManager(QObject* parent = nullptr); + ~AppUpdateManager() override; + + // Compile-time: was this build produced with AR_SELFRELEASE=ON. + static bool IsSelfRelease(); + + // Async. Checks the feed, downloads the update when one exists, then + // emits updateDownloaded (caller prompts / applies) or noUpdateFound / + // checkFailed. No-op while a check is already running. + // userInitiated=true means the user pressed the button: surface errors + // and the "up to date" result; the startup check stays silent on those. + void CheckForUpdates(bool userInitiated); + + // GUI thread. Hands the downloaded update to the Velopack updater (which + // waits for this process to exit, applies, and restarts the app), then + // emits restartRequested — the caller must gracefully quit immediately. + void ApplyAndRestart(); + +signals: + void updateDownloaded(QString version, bool userInitiated); + void noUpdateFound(bool userInitiated); + void checkFailed(QString message, bool userInitiated); + void restartRequested(); + +private slots: + void onWorkFinished(QString version, QString error, bool userInitiated); + +private: + struct Impl; + std::unique_ptr impl_; +}; diff --git a/tests/linux/test_update_feed.py b/tests/linux/test_update_feed.py new file mode 100644 index 0000000..996519c --- /dev/null +++ b/tests/linux/test_update_feed.py @@ -0,0 +1,195 @@ +"""Linux self-update E2E (Velopack AppImage against a local feed). + +Mirrors tests/migration/test_selfrelease_upgrade.py on Windows: the packed +AppImage is launched with AGENTREDACTOR_UPDATE_FEED pointed at a loopback +http.server feed and AGENTREDACTOR_UPDATE_AUTOAPPLY=1, and the test asserts +the updater downloads the vNext package and swaps the AppImage in place. + +Skipped unless the release pack has been built (linux/build-release/velopack) +and vpk is available to pack the vNext feed. Unlike the regular smoke tests +this exercises the AR_SELFRELEASE build, not the dev-tree binary. +""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +import shutil +import subprocess +import sys +import time +from pathlib import Path + +import psutil +import pytest + +_tests_root = Path(__file__).resolve().parent.parent +for _p in (str(_tests_root), str(_tests_root / "gui")): + if _p not in sys.path: + sys.path.insert(0, _p) + +from config_factory import create_settings # noqa: E402 +from gui_process import _find_free_port, _kill_existing_agent_redactor # noqa: E402 + +pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="Linux update E2E") + +PROJECT_ROOT = _tests_root.parent +RELEASE_DIR = PROJECT_ROOT / "linux" / "build-release" +VELOPACK_OUT = RELEASE_DIR / "velopack" +APPDIR = RELEASE_DIR / "appdir" +APPIMAGE = VELOPACK_OUT / "AgentRedactor.AppImage" + +POLL_TIMEOUT_S = 180.0 +POLL_INTERVAL_S = 2.0 + + +def _vpk() -> str | None: + return shutil.which("vpk") or ( + str(Path.home() / ".dotnet" / "tools" / "vpk") + if (Path.home() / ".dotnet" / "tools" / "vpk").is_file() + else None + ) + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def _current_feed_version() -> str: + feed = json.loads((VELOPACK_OUT / "releases.linux.json").read_text(encoding="utf-8")) + return feed["Assets"][0]["Version"] + + +def _bump_patch(version: str) -> str: + m = re.match(r"^(\d+)\.(\d+)\.(\d+)$", version) + assert m, f"unexpected version format: {version}" + return f"{m.group(1)}.{m.group(2)}.{int(m.group(3)) + 1}" + + +def _gui_processes_for(path: Path) -> list[psutil.Process]: + out = [] + for p in psutil.process_iter(["name", "cmdline"]): + try: + if p.info["name"] == "agentredactor-gui" and str(path) in " ".join( + p.info["cmdline"] or [] + ): + out.append(p) + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + return out + + +@pytest.fixture() +def update_env(tmp_path: Path): + if not APPIMAGE.is_file(): + pytest.skip(f"AppImage not packed: {APPIMAGE} (run linux/build-release.sh)") + if not APPDIR.is_dir(): + pytest.skip(f"staged AppDir missing: {APPDIR} (run linux/build-release.sh)") + vpk = _vpk() + if not vpk: + pytest.skip("vpk (Velopack CLI) not installed") + _kill_existing_agent_redactor() + yield tmp_path, vpk + _kill_existing_agent_redactor() + for p in _gui_processes_for(tmp_path): + try: + p.kill() + except psutil.NoSuchProcess: + pass + + +def test_appimage_self_updates_against_local_feed(update_env) -> None: + tmp_path, vpk = update_env + + # 1. Pack a vNext feed from the same staged AppDir (the binary content is + # identical; Velopack compares the package versions in the manifest). + feed_dir = tmp_path / "feed" + feed_dir.mkdir() + next_version = _bump_patch(_current_feed_version()) + subprocess.run( + [ + vpk, "pack", + "--packId", "AgentRedactor", + "--packVersion", next_version, + "--packDir", str(APPDIR), + "--mainExe", "agentredactor-gui", + "--runtime", "linux-x64", + "--channel", "linux", + "--outputDir", str(feed_dir), + ], + check=True, capture_output=True, text=True, timeout=300, + ) + assert (feed_dir / "releases.linux.json").is_file() + + # 2. Seed an isolated config so the engine starts without needing the + # model download, plus an isolated HOME (first-run CLI symlink target). + config_dir = tmp_path / "config" + home_dir = tmp_path / "home" + home_dir.mkdir() + create_settings( + data_dir=config_dir, + upstream_url="http://127.0.0.1:9", # unreachable on purpose + api_key="sk-update-e2e", + proxy_port=_find_free_port(), + logging_enabled=False, + keywords=[], + regex_patterns=[], + ) + + # 3. Copy the shipped AppImage aside; the updater swaps this file. + work_dir = tmp_path / "work" + work_dir.mkdir() + app = work_dir / "AgentRedactor.AppImage" + shutil.copy2(APPIMAGE, app) + app.chmod(0o755) + before = _sha256(app) + + # 4. Serve the vNext feed on loopback (the override is loopback-only). + port = _find_free_port() + server = subprocess.Popen( + [sys.executable, "-m", "http.server", str(port), + "--bind", "127.0.0.1", "--directory", str(feed_dir)], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ) + + try: + # 5. Launch the AppImage with the feed override + auto-apply. + env = dict(os.environ) + env.update({ + "QT_QPA_PLATFORM": "offscreen", + "AGENTREDACTOR_UPDATE_FEED": f"http://127.0.0.1:{port}", + "AGENTREDACTOR_UPDATE_AUTOAPPLY": "1", + "AGENTREDACTOR_CONFIG_DIR": str(config_dir), + "HOME": str(home_dir), + "XDG_CONFIG_HOME": str(home_dir / ".config"), + "XDG_DATA_HOME": str(home_dir / ".local" / "share"), + }) + proc = subprocess.Popen( + [str(app)], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + # 6. Poll until the AppImage file changes (updater applied vNext and + # restarted the app; the relaunched instance sees no further update + # and just keeps running). + deadline = time.monotonic() + POLL_TIMEOUT_S + while time.monotonic() < deadline: + time.sleep(POLL_INTERVAL_S) + try: + if app.is_file() and _sha256(app) != before: + break + except OSError: + continue # mid-swap + else: + proc.kill() + raise AssertionError( + f"AppImage was not updated to v{next_version} within {POLL_TIMEOUT_S}s") + finally: + server.terminate() + for p in _gui_processes_for(tmp_path): + try: + p.terminate() + except psutil.NoSuchProcess: + pass + + assert _sha256(app) != before From f341ca9c9cad05d8b6272cd0e533e87bdeeb53be Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Wed, 19 Aug 2026 01:27:24 +0000 Subject: [PATCH 08/20] ci(linux): add build-linux workflow with gated R2 publish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ubuntu-24.04: build core+engine+GUI, run cli/migration/linux suites (separate pytest processes), pack the AppImage on every PR (artifact dry-run), publish channel 'linux' to R2 only on v* tags or manual dispatch with publish checked — mirroring release-selfrelease.yml gating and secrets. --- .github/workflows/build-linux.yml | 157 ++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 .github/workflows/build-linux.yml diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml new file mode 100644 index 0000000..1d25dd5 --- /dev/null +++ b/.github/workflows/build-linux.yml @@ -0,0 +1,157 @@ +name: Build Linux + +# Linux port CI: builds the core + engine + Qt GUI on Ubuntu, runs the +# cross-platform suites (cli, migration) plus the Linux-only suite, and packs +# the Velopack AppImage. Publishing is gated the same way as +# release-selfrelease.yml: only tag pushes (v*) or manual dispatches with +# publish checked upload to R2 (channel "linux"); PRs get a pack dry-run with +# the packages as artifacts. +on: + push: + tags: ['v*'] + branches: ['main'] + pull_request: + workflow_dispatch: + inputs: + publish: + description: 'Publish to R2 (agentredactor-releases bucket, prefix linux). Off = dry-run.' + required: false + type: boolean + default: false + +concurrency: + group: build-linux-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-24.04 + env: + ONNXRUNTIME_DIR: ${{ github.workspace }}/onnxruntime + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install build + test dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential cmake ninja-build pkg-config \ + libsecret-1-dev libcurl4-openssl-dev libssl-dev nlohmann-json3-dev \ + qt6-base-dev libgl1-mesa-dev fuse + + # onnxruntime has no apt package; official linux-x64 tarball, same as + # linux/README.md documents for local builds. + - name: Download onnxruntime + run: | + mkdir -p "$ONNXRUNTIME_DIR" + curl -sL https://github.com/microsoft/onnxruntime/releases/download/v1.29.0/onnxruntime-linux-x64-1.29.0.tgz \ + | tar xz -C "$ONNXRUNTIME_DIR" --strip-components=1 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install test dependencies + run: pip install -r tests/requirements.txt + + - name: Build (dev config, updater off) + run: | + cmake -S linux -B linux/build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DONNXRUNTIME_INCLUDE_DIR="$ONNXRUNTIME_DIR/include" \ + -DONNXRUNTIME_LIB="$ONNXRUNTIME_DIR/lib/libonnxruntime.so" + cmake --build linux/build + + # The three suites must each run in their own pytest process: tests/cli + # and tests/migration both ship a top-level conftest.py and a shared + # module name breaks a combined run. + - name: Run CLI tests + run: python -m pytest tests/cli -q + + - name: Run migration tests + run: python -m pytest tests/migration -q + + - name: Run Linux tests (offscreen GUI smoke) + run: python -m pytest tests/linux -q + + - name: Set up .NET (for vpk) + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + # Pinned to the Velopack library version vendored by + # linux/fetch-velopack.sh — keep the two in sync. + - name: Install vpk 1.2.0 (Velopack CLI) + run: dotnet tool install -g vpk --version 1.2.0 + + - name: Build and pack AppImage (build-release.sh) + env: + ONNXRUNTIME_INCLUDE_DIR: ${{ github.workspace }}/onnxruntime/include + ONNXRUNTIME_LIB: ${{ github.workspace }}/onnxruntime/lib/libonnxruntime.so + run: linux/build-release.sh + + - name: Verify Velopack output + run: | + dir=linux/build-release/velopack + test -f "$dir/releases.linux.json" || { echo "missing releases.linux.json"; exit 1; } + ls "$dir"/*-full.nupkg >/dev/null || { echo "no full nupkg"; exit 1; } + ls "$dir"/*.AppImage >/dev/null || { echo "no AppImage"; exit 1; } + echo "Velopack output OK:" && ls "$dir" + + - name: Upload Velopack output artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: AgentRedactor-velopack-linux + path: linux/build-release/velopack + retention-days: 30 + + # Tag pushes always publish; manual dispatches only when publish is checked. + # Same R2 secrets and bucket as the Windows self-release flow. + publish: + needs: build + runs-on: ubuntu-24.04 + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') || inputs.publish + steps: + - name: Download Velopack output + uses: actions/download-artifact@v4 + with: + name: AgentRedactor-velopack-linux + path: velopack-linux + + - name: Set up .NET (for vpk) + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Install vpk 1.2.0 (Velopack CLI) + run: dotnet tool install -g vpk --version 1.2.0 + + - name: Check R2 secrets are configured + run: | + missing="" + [ -z '${{ secrets.R2_ACCOUNT_ID }}' ] && missing="$missing R2_ACCOUNT_ID" + [ -z '${{ secrets.R2_ACCESS_KEY_ID }}' ] && missing="$missing R2_ACCESS_KEY_ID" + [ -z '${{ secrets.R2_SECRET_ACCESS_KEY }}' ] && missing="$missing R2_SECRET_ACCESS_KEY" + [ -n "$missing" ] && { echo "Missing repo secrets:$missing"; exit 1; } + + # Publishes the linux channel (releases.linux.json + nupkg + AppImage) + # to R2 so installed Linux instances can auto-update. vpk rejects + # --region together with --endpoint (custom endpoint implies it). + - name: Publish linux channel to R2 + run: vpk upload s3 --bucket agentredactor-releases --endpoint https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com --keyId ${{ secrets.R2_ACCESS_KEY_ID }} --secret ${{ secrets.R2_SECRET_ACCESS_KEY }} -c linux --prefix linux --outputDir velopack-linux + + # vpk uploads its own bookkeeping files no client ever downloads + # (assets.*.json upload manifest); keep the bucket to client files only. + - name: Remove non-client files from R2 + env: + AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + run: | + aws s3 rm "s3://agentredactor-releases/linux/" \ + --endpoint-url https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com \ + --region auto --recursive --exclude "*" --include "assets.*.json" --include "RELEASES*" From 07610590c2cc2712769f2b6f9b4961e02f9fb0fc Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Wed, 19 Aug 2026 02:27:18 +0000 Subject: [PATCH 09/20] feat(linux): add ARM64 support and fix CI model/Windows build issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ARM64 (mirrors the win-arm64 split): - fetch-velopack.sh also extracts the linux_arm64 velopack_libc .so; the GUI CMake picks x64/arm64 by CMAKE_SYSTEM_PROCESSOR. - build-release.sh derives vpk runtime/channel from the host arch: x64 keeps channel 'linux', aarch64 packs -r linux-arm64 -c linux-arm64 and uses the aarch64 Qt plugin dir. Updater feed URL is arch-aware via __aarch64__. - build-linux.yml gains an arm64 leg on ubuntu-24.04-arm (aarch64 onnxruntime tarball); publish uploads both channels and prunes both prefixes. - Feed worker allowlist gains the linux-arm64 channel. CI fixes surfaced by the first PR run: - platform_compat.h: include winsock2.h/ws2tcpip.h before windows.h on _WIN32 (core headers use SOCKET and no longer rely on the includer's pch ordering; fixes the Windows vcxproj build). - build-linux.yml: stage the NER model into ~/.local/share/agentredactor/models (companions from windows/models/, weights from the R2 models endpoint, cached) — the engine keeps proxy ports closed until weights exist, and the cli/linux suites spawn the real engine. - Run the AppImage self-update E2E in CI after packing. --- .github/workflows/build-linux.yml | 93 ++++++++++++++++++++++++------- cloudflare/src/routes/updates.js | 6 +- core/include/platform_compat.h | 5 ++ linux/README.md | 11 +++- linux/build-release.sh | 33 ++++++++--- linux/fetch-velopack.sh | 9 ++- linux/gui/CMakeLists.txt | 20 +++++-- linux/gui/update_manager.cpp | 11 +++- 8 files changed, 143 insertions(+), 45 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 1d25dd5..cf83f61 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -1,11 +1,12 @@ name: Build Linux -# Linux port CI: builds the core + engine + Qt GUI on Ubuntu, runs the +# Linux port CI: builds the core + engine + Qt GUI on Ubuntu (x64 and arm64, +# mirroring the win / win-arm64 split in release-selfrelease.yml), runs the # cross-platform suites (cli, migration) plus the Linux-only suite, and packs # the Velopack AppImage. Publishing is gated the same way as # release-selfrelease.yml: only tag pushes (v*) or manual dispatches with -# publish checked upload to R2 (channel "linux"); PRs get a pack dry-run with -# the packages as artifacts. +# publish checked upload to R2 (channels "linux" / "linux-arm64"); PRs get a +# pack dry-run with the packages as artifacts. on: push: tags: ['v*'] @@ -14,7 +15,7 @@ on: workflow_dispatch: inputs: publish: - description: 'Publish to R2 (agentredactor-releases bucket, prefix linux). Off = dry-run.' + description: 'Publish to R2 (agentredactor-releases bucket, prefixes linux/linux-arm64). Off = dry-run.' required: false type: boolean default: false @@ -28,7 +29,14 @@ permissions: jobs: build: - runs-on: ubuntu-24.04 + strategy: + # One arch failing must not cancel the other; publish only runs when + # both succeed anyway (same pattern as release-selfrelease.yml). + fail-fast: false + matrix: + arch: [x64, arm64] + # Native runners per arch, same pattern as the Windows ARM64 leg. + runs-on: ${{ matrix.arch == 'x64' && 'ubuntu-24.04' || 'ubuntu-24.04-arm' }} env: ONNXRUNTIME_DIR: ${{ github.workspace }}/onnxruntime steps: @@ -42,12 +50,13 @@ jobs: libsecret-1-dev libcurl4-openssl-dev libssl-dev nlohmann-json3-dev \ qt6-base-dev libgl1-mesa-dev fuse - # onnxruntime has no apt package; official linux-x64 tarball, same as + # onnxruntime has no apt package; official tarball per arch, same as # linux/README.md documents for local builds. - name: Download onnxruntime run: | mkdir -p "$ONNXRUNTIME_DIR" - curl -sL https://github.com/microsoft/onnxruntime/releases/download/v1.29.0/onnxruntime-linux-x64-1.29.0.tgz \ + rid="${{ matrix.arch == 'x64' && 'x64' || 'aarch64' }}" + curl -sL "https://github.com/microsoft/onnxruntime/releases/download/v1.29.0/onnxruntime-linux-${rid}-1.29.0.tgz" \ | tar xz -C "$ONNXRUNTIME_DIR" --strip-components=1 - name: Set up Python @@ -58,6 +67,30 @@ jobs: - name: Install test dependencies run: pip install -r tests/requirements.txt + # The engine refuses to serve (proxy ports stay closed) until the NER + # model weights exist, and the CLI/linux suites spawn the real engine — + # so the ~1.6 GB weights must be present. Companions come from the repo + # checkout (windows/models/, same files the Windows build ships); only + # the gitignored weights are downloaded, from the same R2 models + # endpoint the app itself uses. Cached across runs. + - name: Cache ONNX model weights + id: model-cache + uses: actions/cache@v4 + with: + path: ~/.local/share/agentredactor/models + key: onnx-model-ner-${{ runner.arch }}-v1 + + - name: Stage model files (cache miss only) + if: steps.model-cache.outputs.cache-hit != 'true' + run: | + dest=~/.local/share/agentredactor/models + mkdir -p "$dest/onnx" + cp windows/models/config.json windows/models/tokenizer.json \ + windows/models/viterbi_calibration.json "$dest/" + cp windows/models/onnx/model_quantized.onnx "$dest/onnx/" + curl -fL --retry 3 -o "$dest/onnx/model_quantized.onnx_data" \ + https://api.agentredactor.negativestarinnovators.com/models/model_quantized.onnx_data + - name: Build (dev config, updater off) run: | cmake -S linux -B linux/build -G Ninja \ @@ -88,6 +121,8 @@ jobs: - name: Install vpk 1.2.0 (Velopack CLI) run: dotnet tool install -g vpk --version 1.2.0 + # build-release.sh derives the vpk runtime/channel from the host arch + # (linux-x64/'linux', linux-arm64/'linux-arm64'). - name: Build and pack AppImage (build-release.sh) env: ONNXRUNTIME_INCLUDE_DIR: ${{ github.workspace }}/onnxruntime/include @@ -97,7 +132,8 @@ jobs: - name: Verify Velopack output run: | dir=linux/build-release/velopack - test -f "$dir/releases.linux.json" || { echo "missing releases.linux.json"; exit 1; } + channel="${{ matrix.arch == 'x64' && 'linux' || 'linux-arm64' }}" + test -f "$dir/releases.${channel}.json" || { echo "missing releases.${channel}.json"; exit 1; } ls "$dir"/*-full.nupkg >/dev/null || { echo "no full nupkg"; exit 1; } ls "$dir"/*.AppImage >/dev/null || { echo "no AppImage"; exit 1; } echo "Velopack output OK:" && ls "$dir" @@ -106,22 +142,32 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: AgentRedactor-velopack-linux + name: AgentRedactor-velopack-linux-${{ matrix.arch }} path: linux/build-release/velopack retention-days: 30 - # Tag pushes always publish; manual dispatches only when publish is checked. - # Same R2 secrets and bucket as the Windows self-release flow. + # Full self-update cycle against a loopback feed: packs a vNext release + # from the staged AppDir and asserts the shipped AppImage swaps itself. + # Runs after packing since it consumes the pack output; skips locally + # when vpk or the pack output is missing. + - name: Run update E2E (AppImage self-update) + run: python -m pytest tests/linux/test_update_feed.py -q + + # Publish is deliberately OUT of the matrix: both arch legs must pass first, + # then ONE job uploads both channels to R2 — same shape as + # release-selfrelease.yml. Tag pushes always publish; manual dispatches only + # when publish is checked. Same R2 secrets and bucket as the Windows flow. publish: needs: build runs-on: ubuntu-24.04 if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') || inputs.publish steps: - - name: Download Velopack output + - name: Download Velopack outputs (both arches) uses: actions/download-artifact@v4 with: - name: AgentRedactor-velopack-linux + pattern: AgentRedactor-velopack-linux-* path: velopack-linux + # Lands in velopack-linux/AgentRedactor-velopack-linux-x64 and ...-arm64. - name: Set up .NET (for vpk) uses: actions/setup-dotnet@v4 @@ -140,18 +186,25 @@ jobs: [ -n "$missing" ] && { echo "Missing repo secrets:$missing"; exit 1; } # Publishes the linux channel (releases.linux.json + nupkg + AppImage) - # to R2 so installed Linux instances can auto-update. vpk rejects + # to R2 so installed Linux x64 instances can auto-update. vpk rejects # --region together with --endpoint (custom endpoint implies it). - - name: Publish linux channel to R2 - run: vpk upload s3 --bucket agentredactor-releases --endpoint https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com --keyId ${{ secrets.R2_ACCESS_KEY_ID }} --secret ${{ secrets.R2_SECRET_ACCESS_KEY }} -c linux --prefix linux --outputDir velopack-linux + - name: Publish linux (x64) channel to R2 + run: vpk upload s3 --bucket agentredactor-releases --endpoint https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com --keyId ${{ secrets.R2_ACCESS_KEY_ID }} --secret ${{ secrets.R2_SECRET_ACCESS_KEY }} -c linux --prefix linux --outputDir velopack-linux/AgentRedactor-velopack-linux-x64 + + # Same for the linux-arm64 channel (prefix linux-arm64/). + - name: Publish linux-arm64 channel to R2 + run: vpk upload s3 --bucket agentredactor-releases --endpoint https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com --keyId ${{ secrets.R2_ACCESS_KEY_ID }} --secret ${{ secrets.R2_SECRET_ACCESS_KEY }} -c linux-arm64 --prefix linux-arm64 --outputDir velopack-linux/AgentRedactor-velopack-linux-arm64 # vpk uploads its own bookkeeping files no client ever downloads - # (assets.*.json upload manifest); keep the bucket to client files only. + # (assets.*.json upload manifest, legacy RELEASES file); keep the bucket + # to client files only. - name: Remove non-client files from R2 env: AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} run: | - aws s3 rm "s3://agentredactor-releases/linux/" \ - --endpoint-url https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com \ - --region auto --recursive --exclude "*" --include "assets.*.json" --include "RELEASES*" + for channel in linux linux-arm64; do + aws s3 rm "s3://agentredactor-releases/${channel}/" \ + --endpoint-url https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com \ + --region auto --recursive --exclude "*" --include "assets.*.json" --include "RELEASES*" + done diff --git a/cloudflare/src/routes/updates.js b/cloudflare/src/routes/updates.js index 43825c9..0c4beb3 100644 --- a/cloudflare/src/routes/updates.js +++ b/cloudflare/src/routes/updates.js @@ -1,9 +1,9 @@ -// Self-release update channels: x64 builds use the original 'win' channel, -// ARM64 builds use 'win-arm64', Linux x64 builds use 'linux'. Releases are +// Self-release update channels: x64 builds use the original 'win' / 'linux' +// channels, ARM64 builds use 'win-arm64' / 'linux-arm64'. Releases are // served exclusively from the RELEASES_BUCKET R2 binding at / // (R2 is the single host — no GitHub fallback). CI uploads each release via // `vpk upload s3`. -const CHANNEL_PATTERN = /^(win|win-arm64|linux)$/; +const CHANNEL_PATTERN = /^(win|win-arm64|linux|linux-arm64)$/; // Strict allowlist for files Velopack requests from the update feed: // releases..json, *.nupkg, *-Setup.exe, *-Portable.zip, *.AppImage diff --git a/core/include/platform_compat.h b/core/include/platform_compat.h index a791c68..c69657d 100644 --- a/core/include/platform_compat.h +++ b/core/include/platform_compat.h @@ -8,6 +8,11 @@ #ifdef _WIN32 +// winsock2.h must precede windows.h (otherwise the legacy winsock.h wins and +// SOCKET stays undeclared in core headers like http_server.h, which no longer +// rely on the includer's pch.h ordering). +#include +#include #include // POSIX name for the address-length type Winsock exposes as int. diff --git a/linux/README.md b/linux/README.md index 64d7d3f..8cca182 100644 --- a/linux/README.md +++ b/linux/README.md @@ -62,10 +62,15 @@ when none is running, and stops it on quit only when it spawned it. ## Release packaging and self-update (Velopack) Self-release builds package the app as a Velopack AppImage with an in-app -updater, mirroring the Windows self-release flow (channel `linux`, same R2 -bucket, same feed worker). Prereqs: the .NET SDK and the pinned Velopack CLI +updater, mirroring the Windows self-release flow (same R2 bucket, same feed +worker). Channels are arch-aware, mirroring `win` / `win-arm64`: x64 builds +use `linux`, ARM64 builds use `linux-arm64`. `build-release.sh` derives the +architecture from the host (`uname -m`); on aarch64 it packs +`-r linux-arm64 -c linux-arm64` and uses the aarch64 Qt plugin dir. Prereqs: +the .NET SDK and the pinned Velopack CLI (`dotnet tool install -g vpk --version 1.2.0` — keep it in sync with -`linux/fetch-velopack.sh`). +`linux/fetch-velopack.sh`). On aarch64, point `ONNXRUNTIME_INCLUDE_DIR` / +`ONNXRUNTIME_LIB` at the linux-aarch64 onnxruntime tarball instead. ```bash linux/build-release.sh # Release build (-DAR_SELFRELEASE=ON) + AppDir + vpk pack diff --git a/linux/build-release.sh b/linux/build-release.sh index fe1a78f..7e9b9d6 100755 --- a/linux/build-release.sh +++ b/linux/build-release.sh @@ -4,13 +4,15 @@ # Usage: # linux/build-release.sh # build + pack into linux/build-release/velopack/ # -# Produces the Velopack linux channel artifacts (AppImage, zsync, nupkg, -# releases.linux.json). Upload to R2 with (same secrets as the Windows flow): +# Produces the Velopack channel artifacts (AppImage, nupkg, feed JSON) for the +# host architecture: channel 'linux' on x64, 'linux-arm64' on aarch64 (mirrors +# the win / win-arm64 split). Upload to R2 with (same secrets as the Windows +# flow): # # vpk upload s3 --bucket agentredactor-releases \ # --endpoint https://$R2_ACCOUNT_ID.r2.cloudflarestorage.com \ # --keyId "$R2_ACCESS_KEY_ID" --secret "$R2_SECRET_ACCESS_KEY" \ -# --prefix linux -c linux --outputDir linux/build-release/velopack +# --prefix -c --outputDir linux/build-release/velopack # # Prereqs: build deps from linux/README.md, dotnet SDK + `vpk` 1.2.0 # (dotnet tool install -g vpk --version 1.2.0), onnxruntime tarball. @@ -24,7 +26,22 @@ VERSION="$(tr -d '[:space:]' < "${ROOT}/../windows/version.txt")" ONNX_INCLUDE="${ONNXRUNTIME_INCLUDE_DIR:-$HOME/onnxruntime/include}" ONNX_LIB="${ONNXRUNTIME_LIB:-$HOME/onnxruntime/lib/libonnxruntime.so}" -VP_QT_PLUGIN_DIR="${QT6_PLUGIN_DIR:-/usr/lib/x86_64-linux-gnu/qt6/plugins}" +# Arch-aware: mirrors the Windows win / win-arm64 channel split. The x64 +# channel keeps the original 'linux' name; ARM64 uses 'linux-arm64'. +ARCH="$(uname -m)" +if [ "${ARCH}" = "aarch64" ]; then + VP_ARCH="arm64" + VPK_RID="linux-arm64" + CHANNEL="linux-arm64" + LIB_DIR="/usr/lib/aarch64-linux-gnu" +else + VP_ARCH="x64" + VPK_RID="linux-x64" + CHANNEL="linux" + LIB_DIR="/usr/lib/x86_64-linux-gnu" +fi + +VP_QT_PLUGIN_DIR="${QT6_PLUGIN_DIR:-${LIB_DIR}/qt6/plugins}" echo "==> Fetching Velopack lib" bash "${ROOT}/fetch-velopack.sh" @@ -44,8 +61,8 @@ mkdir -p "${STAGE}/plugins" cp "${BUILD}/gui/agentredactor-gui" "${STAGE}/" cp "${BUILD}/engine/agentredactor" "${STAGE}/" # DT_NEEDED records the lib-prefixed name (see gui/CMakeLists.txt). -cp "${ROOT}/third_party/velopack/lib/velopack_libc_linux_x64_gnu.so" \ - "${STAGE}/libvelopack_libc_linux_x64_gnu.so" +cp "${ROOT}/third_party/velopack/lib/velopack_libc_linux_${VP_ARCH}_gnu.so" \ + "${STAGE}/libvelopack_libc_linux_${VP_ARCH}_gnu.so" cp "${ONNX_LIB}" "${STAGE}/" # Bundle the shared libraries the two binaries resolve to, minus the @@ -92,8 +109,8 @@ vpk pack \ --packVersion "${VERSION}" \ --packDir "${STAGE}" \ --mainExe agentredactor-gui \ - --runtime linux-x64 \ - --channel linux \ + --runtime "${VPK_RID}" \ + --channel "${CHANNEL}" \ --packTitle "Agent Redactor" \ --packAuthors "Negative Star Innovators" \ --icon "${ROOT}/gui/assets/app.png" \ diff --git a/linux/fetch-velopack.sh b/linux/fetch-velopack.sh index 09b407e..736fad8 100755 --- a/linux/fetch-velopack.sh +++ b/linux/fetch-velopack.sh @@ -10,7 +10,9 @@ URL="https://github.com/velopack/velopack/releases/download/${VP_VERSION}/velopa ROOT="$(cd "$(dirname "$0")" && pwd)" DEST="${ROOT}/third_party/velopack" -if [ -f "${DEST}/include/Velopack.hpp" ] && [ -f "${DEST}/lib/velopack_libc_linux_x64_gnu.so" ]; then +if [ -f "${DEST}/include/Velopack.hpp" ] \ + && [ -f "${DEST}/lib/velopack_libc_linux_x64_gnu.so" ] \ + && [ -f "${DEST}/lib/velopack_libc_linux_arm64_gnu.so" ]; then echo "velopack_libc ${VP_VERSION} already present in ${DEST}" exit 0 fi @@ -25,11 +27,12 @@ echo "${VP_SHA256} ${TMP}/velopack_libc.zip" | sha256sum -c - rm -rf "${DEST}" mkdir -p "${DEST}" -# Only the Linux x64 pieces this project links/ships; other platforms are -# fetched from the same zip if ever needed. +# Only the Linux pieces this project links/ships (x64 + arm64); other +# platforms are fetched from the same zip if ever needed. unzip -q "${TMP}/velopack_libc.zip" \ 'include/*' \ 'lib/velopack_libc_linux_x64_gnu.so' \ + 'lib/velopack_libc_linux_arm64_gnu.so' \ -d "${DEST}" echo "velopack_libc ${VP_VERSION} extracted to ${DEST}" diff --git a/linux/gui/CMakeLists.txt b/linux/gui/CMakeLists.txt index 0b09b44..456c0a1 100644 --- a/linux/gui/CMakeLists.txt +++ b/linux/gui/CMakeLists.txt @@ -34,7 +34,15 @@ target_link_libraries(agentredactor-gui PRIVATE if (AR_SELFRELEASE) set(VP_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../third_party/velopack) - if (NOT EXISTS ${VP_ROOT}/lib/velopack_libc_linux_x64_gnu.so) + # Arch-aware lib selection: linux_x64 / linux_arm64 (vpk pack gets the + # matching -r linux-x64/linux-arm64 in build-release.sh). + if (CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") + set(VP_ARCH arm64) + else() + set(VP_ARCH x64) + endif() + set(VP_SO ${VP_ROOT}/lib/velopack_libc_linux_${VP_ARCH}_gnu.so) + if (NOT EXISTS ${VP_SO}) message(FATAL_ERROR "AR_SELFRELEASE=ON but Velopack lib is missing; run linux/fetch-velopack.sh") endif() target_compile_definitions(agentredactor-gui PRIVATE AR_SELFRELEASE) @@ -44,14 +52,14 @@ if (AR_SELFRELEASE) # $ORIGIN then resolves both in the build tree and the staged AppDir. set(VP_LINK_DIR ${CMAKE_BINARY_DIR}/velopack-link) file(MAKE_DIRECTORY ${VP_LINK_DIR}) - file(CREATE_LINK ${VP_ROOT}/lib/velopack_libc_linux_x64_gnu.so - ${VP_LINK_DIR}/libvelopack_libc_linux_x64_gnu.so SYMBOLIC) + file(CREATE_LINK ${VP_SO} + ${VP_LINK_DIR}/libvelopack_libc_linux_${VP_ARCH}_gnu.so SYMBOLIC) target_link_directories(agentredactor-gui PRIVATE ${VP_LINK_DIR}) - target_link_libraries(agentredactor-gui PRIVATE velopack_libc_linux_x64_gnu) + target_link_libraries(agentredactor-gui PRIVATE velopack_libc_linux_${VP_ARCH}_gnu) # Ship the Velopack lib next to the GUI binary (AppDir staging) and find # it there at runtime. - install(FILES ${VP_ROOT}/lib/velopack_libc_linux_x64_gnu.so - DESTINATION . RENAME libvelopack_libc_linux_x64_gnu.so) + install(FILES ${VP_SO} + DESTINATION . RENAME libvelopack_libc_linux_${VP_ARCH}_gnu.so) set_target_properties(agentredactor-gui PROPERTIES BUILD_RPATH "$ORIGIN;${VP_LINK_DIR}" INSTALL_RPATH "$ORIGIN") diff --git a/linux/gui/update_manager.cpp b/linux/gui/update_manager.cpp index 26ccde2..85a5932 100644 --- a/linux/gui/update_manager.cpp +++ b/linux/gui/update_manager.cpp @@ -13,11 +13,18 @@ #include // The Cloudflare worker serves every file under this prefix from the R2 -// releases bucket. The host is shared with Windows; the channel segment for -// Linux x64 builds is "linux" (matches vpk pack -c in build-release.sh). +// releases bucket. The host is shared with Windows; the channel segment is +// arch-aware (matches vpk pack -c in build-release.sh): x64 builds use the +// original "linux" channel, ARM64 builds use "linux-arm64" — the same split +// as the Windows "win" / "win-arm64" channels. namespace { +#if defined(__aarch64__) +constexpr const char* kUpdateFeedUrl = + "https://api.agentredactor.negativestarinnovators.com/updates/linux-arm64"; +#else constexpr const char* kUpdateFeedUrl = "https://api.agentredactor.negativestarinnovators.com/updates/linux"; +#endif // Test hook (self-release builds only): AGENTREDACTOR_UPDATE_FEED overrides // the update feed URL so the E2E tests can point at a local feed. Loopback From a05ed311077af48edb7d5e2cd2a7d26c3e842fa9 Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Wed, 19 Aug 2026 06:32:24 +0000 Subject: [PATCH 10/20] fix(windows): resolve winsock v1/v2 clash in GUI build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit platform_compat.h now includes winsock2.h on _WIN32 (needed since core headers like http_server.h use SOCKET without relying on the includer). windows/include/system_tray.h included bare before any core header, compiling winsock v1 first and clashing with winsock2 in the non-pch batch — apply the same _WINSOCKAPI_ guard pch.h uses so v1 stays out and winsock2 (from platform_compat.h) provides SOCKET. --- windows/include/system_tray.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/windows/include/system_tray.h b/windows/include/system_tray.h index eaed435..4baa636 100644 --- a/windows/include/system_tray.h +++ b/windows/include/system_tray.h @@ -5,6 +5,10 @@ #include #include #include +// Block winsock v1 (same _WINSOCKAPI_ idiom as pch.h): core headers pull in +// platform_compat.h, which includes winsock2.h — if v1 gets compiled first +// here, the two socket APIs clash in this TU. +#define _WINSOCKAPI_ #include #include From 75071e56c44a57e25de2e471f868349a3721feda Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Wed, 19 Aug 2026 06:58:25 +0000 Subject: [PATCH 11/20] fix(core): restore winhttp.h include on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit proxy_engine.h used to include directly; the port swapped it for platform_compat.h, which didn't pull winhttp — breaking the Windows build of proxy_engine.cpp. platform_compat.h now includes winhttp.h on _WIN32. --- core/include/platform_compat.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/include/platform_compat.h b/core/include/platform_compat.h index c69657d..d5b72b0 100644 --- a/core/include/platform_compat.h +++ b/core/include/platform_compat.h @@ -10,10 +10,13 @@ // winsock2.h must precede windows.h (otherwise the legacy winsock.h wins and // SOCKET stays undeclared in core headers like http_server.h, which no longer -// rely on the includer's pch.h ordering). +// rely on the includer's pch.h ordering). winhttp.h is pulled in too: the +// core's WinHTTP upstream client (proxy_engine) used to get it from +// proxy_engine.h directly. #include #include #include +#include // POSIX name for the address-length type Winsock exposes as int. typedef int ar_socklen_t; From f2fafd7c7ae03e38b8c60f653bf97c3186cc9bf1 Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Wed, 19 Aug 2026 07:13:31 +0000 Subject: [PATCH 12/20] fix(core): include platform_compat.h in log_manager.h for IsLoggingEnabled macro windows.h (winevent.h) maps IsLoggingEnabled to IsLoggingEnabledW under UNICODE. On main the header self-included windows.h so declaration, definition and all call sites were mangled consistently; the port dropped that include, leaving the class declaration clean while log_manager.cpp's definition (after utils.h pulls windows.h) was renamed. Restoring the platform include in the header makes the macro visible at the declaration again, matching every other TU. --- core/include/log_manager.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/include/log_manager.h b/core/include/log_manager.h index 48c76cb..57ab268 100644 --- a/core/include/log_manager.h +++ b/core/include/log_manager.h @@ -4,6 +4,10 @@ #include #include #include +// On Windows this must come before the class: windows.h (via winevent.h) +// defines IsLoggingEnabled -> IsLoggingEnabledW under UNICODE, and the +// declaration, definition and every call site must all see the macro. +#include "platform_compat.h" namespace AgentRedactor { From 6ae28e24eadd51647e4b8624ebd5fb50287b5df0 Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Wed, 19 Aug 2026 10:07:39 +0000 Subject: [PATCH 13/20] =?UTF-8?q?feat(linux-gui):=20full=20i18n=20?= =?UTF-8?q?=E2=80=94=20live=20language=20switching=20and=2052=20catalogs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tray Language submenu and Settings language combo, both applying live via the settings poll + QEvent::LanguageChange (no restart, unlike Windows); RTL layout flip included. CLI 'set app-language' already worked engine-side and now retranslates the running GUI too. - retranslateUi now covers the menu bar, form labels, PII grid and the dynamic regex/keyword rows; the tray menu retranslates via TrayIcon. - English wording aligned to the Windows resw values where semantics matched (updater, model download, validation, remove-profile/quit/clear dialogs, PII_Type_* labels) so those translations are reused for free. - i18n/sync_ts.py scans tr() sources and fills agentredactor_.ts from windows/Strings//Resources.resw (normalizing '&' accelerators and {0} <-> %1 placeholders): 61/100 strings per language reused. TrayMenu_StartOnBoot is excluded (de/fr/pt translations say 'with Windows'). - i18n/bootstrap_translations.py machine-translates the 39 Linux-only strings (typed master password flow, tray/quit wording, UI labels) with Google Translate, same bootstrap convention as Windows; placeholders are verified to survive. Native review still needed. - CMake compiles the .ts catalogs into :/i18n with lrelease (qt6-l10n-tools; English-only fallback with a warning when absent). - New tests: every CLI-supported language has a catalog; live language switching (de/ar/zh-CN/en) while the GUI runs. --- .github/workflows/build-linux.yml | 2 +- .gitignore | 3 + linux/README.md | 31 +- linux/gui/CMakeLists.txt | 47 +++ linux/gui/i18n/agentredactor_af.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_ar.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_az_Latn.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_bg.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_cs.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_da.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_de.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_el.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_es.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_et.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_fi.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_fil.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_fr.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_ha_Latn.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_he.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_hi.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_hr.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_hu.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_hy.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_id.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_ig_NG.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_is.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_it.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_ja.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_ka.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_kk.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_ko.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_lb.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_lt.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_lv.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_ms.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_mt.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_nb.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_nl.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_pl.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_pt.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_ro.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_ru.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_sk.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_sl.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_sq.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_sr_Latn.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_sv.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_sw.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_ta.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_th.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_tr.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_uk.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_ur.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_vi.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_zh_CN.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/agentredactor_zh_TW.ts | 415 +++++++++++++++++++++++ linux/gui/i18n/bootstrap_translations.py | 161 +++++++++ linux/gui/i18n/sync_ts.py | 217 ++++++++++++ linux/gui/main_window.cpp | 175 +++++++--- linux/gui/main_window.h | 20 +- linux/gui/resources.qrc | 5 +- linux/gui/translator_loader.cpp | 7 +- linux/gui/tray_icon.cpp | 53 ++- linux/gui/tray_icon.h | 24 +- tests/linux/test_gui_smoke.py | 54 +++ 65 files changed, 22307 insertions(+), 72 deletions(-) create mode 100644 linux/gui/i18n/agentredactor_af.ts create mode 100644 linux/gui/i18n/agentredactor_ar.ts create mode 100644 linux/gui/i18n/agentredactor_az_Latn.ts create mode 100644 linux/gui/i18n/agentredactor_bg.ts create mode 100644 linux/gui/i18n/agentredactor_cs.ts create mode 100644 linux/gui/i18n/agentredactor_da.ts create mode 100644 linux/gui/i18n/agentredactor_de.ts create mode 100644 linux/gui/i18n/agentredactor_el.ts create mode 100644 linux/gui/i18n/agentredactor_es.ts create mode 100644 linux/gui/i18n/agentredactor_et.ts create mode 100644 linux/gui/i18n/agentredactor_fi.ts create mode 100644 linux/gui/i18n/agentredactor_fil.ts create mode 100644 linux/gui/i18n/agentredactor_fr.ts create mode 100644 linux/gui/i18n/agentredactor_ha_Latn.ts create mode 100644 linux/gui/i18n/agentredactor_he.ts create mode 100644 linux/gui/i18n/agentredactor_hi.ts create mode 100644 linux/gui/i18n/agentredactor_hr.ts create mode 100644 linux/gui/i18n/agentredactor_hu.ts create mode 100644 linux/gui/i18n/agentredactor_hy.ts create mode 100644 linux/gui/i18n/agentredactor_id.ts create mode 100644 linux/gui/i18n/agentredactor_ig_NG.ts create mode 100644 linux/gui/i18n/agentredactor_is.ts create mode 100644 linux/gui/i18n/agentredactor_it.ts create mode 100644 linux/gui/i18n/agentredactor_ja.ts create mode 100644 linux/gui/i18n/agentredactor_ka.ts create mode 100644 linux/gui/i18n/agentredactor_kk.ts create mode 100644 linux/gui/i18n/agentredactor_ko.ts create mode 100644 linux/gui/i18n/agentredactor_lb.ts create mode 100644 linux/gui/i18n/agentredactor_lt.ts create mode 100644 linux/gui/i18n/agentredactor_lv.ts create mode 100644 linux/gui/i18n/agentredactor_ms.ts create mode 100644 linux/gui/i18n/agentredactor_mt.ts create mode 100644 linux/gui/i18n/agentredactor_nb.ts create mode 100644 linux/gui/i18n/agentredactor_nl.ts create mode 100644 linux/gui/i18n/agentredactor_pl.ts create mode 100644 linux/gui/i18n/agentredactor_pt.ts create mode 100644 linux/gui/i18n/agentredactor_ro.ts create mode 100644 linux/gui/i18n/agentredactor_ru.ts create mode 100644 linux/gui/i18n/agentredactor_sk.ts create mode 100644 linux/gui/i18n/agentredactor_sl.ts create mode 100644 linux/gui/i18n/agentredactor_sq.ts create mode 100644 linux/gui/i18n/agentredactor_sr_Latn.ts create mode 100644 linux/gui/i18n/agentredactor_sv.ts create mode 100644 linux/gui/i18n/agentredactor_sw.ts create mode 100644 linux/gui/i18n/agentredactor_ta.ts create mode 100644 linux/gui/i18n/agentredactor_th.ts create mode 100644 linux/gui/i18n/agentredactor_tr.ts create mode 100644 linux/gui/i18n/agentredactor_uk.ts create mode 100644 linux/gui/i18n/agentredactor_ur.ts create mode 100644 linux/gui/i18n/agentredactor_vi.ts create mode 100644 linux/gui/i18n/agentredactor_zh_CN.ts create mode 100644 linux/gui/i18n/agentredactor_zh_TW.ts create mode 100644 linux/gui/i18n/bootstrap_translations.py create mode 100644 linux/gui/i18n/sync_ts.py diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index cf83f61..16b1836 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -48,7 +48,7 @@ jobs: sudo apt-get update sudo apt-get install -y build-essential cmake ninja-build pkg-config \ libsecret-1-dev libcurl4-openssl-dev libssl-dev nlohmann-json3-dev \ - qt6-base-dev libgl1-mesa-dev fuse + qt6-base-dev qt6-l10n-tools libgl1-mesa-dev fuse # onnxruntime has no apt package; official tarball per arch, same as # linux/README.md documents for local builds. diff --git a/.gitignore b/.gitignore index 6557643..8c17b59 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ windows/store-listing/ # Internal planning docs (not for public release) docs/linux-port-plan.md + +# Translation/CI tooling venv (lrelease, deep-translator) +.transvenv/ diff --git a/linux/README.md b/linux/README.md index 8cca182..4ba945d 100644 --- a/linux/README.md +++ b/linux/README.md @@ -6,7 +6,7 @@ on Linux: ```bash sudo apt install -y build-essential cmake ninja-build pkg-config \ libsecret-1-dev libcurl4-openssl-dev libssl-dev nlohmann-json3-dev \ - qt6-base-dev libgl1-mesa-dev \ + qt6-base-dev qt6-l10n-tools libgl1-mesa-dev \ python3-pytest python3-pytest-asyncio python3-aiohttp python3-psutil # onnxruntime is not packaged in apt; use the official linux-x64 tarball @@ -39,10 +39,11 @@ python -m pytest linux -q ## GUI, tray and autostart -`build/gui/agentredactor-gui` is the desktop app (Qt6 Widgets, English-only -UI structured for later `.ts` translations). It spawns the engine -(`agentredactor`, found next to it or in the sibling `engine/` build dir) -when none is running, and stops it on quit only when it spawned it. +`build/gui/agentredactor-gui` is the desktop app (Qt6 Widgets, translated +into every language Windows supports — see "Translations" below). It spawns +the engine (`agentredactor`, found next to it or in the sibling `engine/` +build dir) when none is running, and stops it on quit only when it spawned +it. - Tray: `QSystemTrayIcon` (StatusNotifierItem). On desktops without a tray (plain Wayland GNOME without the AppIndicator extension) the app runs as a @@ -52,6 +53,9 @@ when none is running, and stops it on quit only when it spawned it. - Autostart: the "Start on boot" toggle writes/removes `$XDG_CONFIG_HOME/autostart/agentredactor.desktop` (the GUI reconciles the file with the persisted setting on startup, so CLI changes apply too). +- Language: switchable from the tray Language submenu, the Settings card + combo, or the CLI (`agentredactor set app-language `); applies live + with no restart (Qt retranslation, RTL included). - Headless/boot-time startup without a desktop session: install the systemd user unit from `linux/systemd/agentredactor.service` (instructions in the file's header comment). @@ -59,6 +63,23 @@ when none is running, and stops it on quit only when it spawned it. app (not your OS login password). The GUI shows a lock overlay with a password field when the session is locked. +## Translations + +The GUI supports the same languages as Windows (`SUPPORTED_LANGUAGES` in +`core/include/constants.h` is the shared source of truth). Catalogs live in +`linux/gui/i18n/agentredactor_.ts` and are compiled into `:/i18n` +with `lrelease` (package `qt6-l10n-tools`; without it the build is +English-only with a CMake warning). Two scripts maintain them: + +- `i18n/sync_ts.py` — scans the GUI sources for `tr()` strings and fills + each catalog from the matching `windows/Strings//Resources.resw` + translations (normalizing `&` accelerators and `{0}` ↔ `%1` placeholders). + Idempotent; re-run it after changing any GUI string. +- `i18n/bootstrap_translations.py` — machine-translates the Linux-only + strings (typed-password flow etc.) with Google Translate, like the Windows + bootstrap (`windows/generate_new_languages.py`); review by native speakers + is still needed. + ## Release packaging and self-update (Velopack) Self-release builds package the app as a Velopack AppImage with an in-app diff --git a/linux/gui/CMakeLists.txt b/linux/gui/CMakeLists.txt index 456c0a1..a6d53f3 100644 --- a/linux/gui/CMakeLists.txt +++ b/linux/gui/CMakeLists.txt @@ -5,6 +5,52 @@ find_package(Qt6 REQUIRED COMPONENTS Widgets) find_package(CURL REQUIRED) +# Translation catalogs: i18n/agentredactor_.ts (synced from the +# Windows resw catalogs by i18n/sync_ts.py) are compiled to .qm with +# lrelease and embedded under :/i18n. When lrelease is unavailable the GUI +# builds English-only with a warning. +file(GLOB AR_TS_FILES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/i18n/agentredactor_*.ts) +set(AR_QRC_EXTRAS "") +if (AR_TS_FILES) + find_package(Qt6 COMPONENTS LinguistTools QUIET) + if (NOT Qt6_LRELEASE_EXECUTABLE) + find_program(Qt6_LRELEASE_EXECUTABLE NAMES lrelease lrelease-qt6 pyside6-lrelease + HINTS /usr/lib/qt6/bin) + endif() + if (Qt6_LRELEASE_EXECUTABLE) + set(AR_QM_FILES "") + foreach(TS_FILE ${AR_TS_FILES}) + get_filename_component(TS_NAME ${TS_FILE} NAME_WE) + set(QM_FILE ${CMAKE_CURRENT_BINARY_DIR}/i18n/${TS_NAME}.qm) + add_custom_command( + OUTPUT ${QM_FILE} + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/i18n + COMMAND ${Qt6_LRELEASE_EXECUTABLE} ${TS_FILE} -qm ${QM_FILE} + DEPENDS ${TS_FILE} + VERBATIM) + list(APPEND AR_QM_FILES ${QM_FILE}) + endforeach() + # Generated qrc mapping each .qm to :/i18n/.qm. + set(AR_I18N_QRC ${CMAKE_CURRENT_BINARY_DIR}/i18n.qrc) + file(WRITE ${AR_I18N_QRC} "\n \n") + foreach(QM_FILE ${AR_QM_FILES}) + get_filename_component(QM_NAME ${QM_FILE} NAME) + file(APPEND ${AR_I18N_QRC} + " ${QM_FILE}\n") + endforeach() + file(APPEND ${AR_I18N_QRC} " \n\n") + set(AR_QRC_EXTRAS ${AR_I18N_QRC} ${AR_QM_FILES}) + # Ordering: the .qm files must exist before AUTORCC compiles the + # generated qrc (same-target custom commands are not ordered). + add_custom_target(agentredactor-i18n ALL DEPENDS ${AR_QM_FILES}) + set_source_files_properties(${AR_I18N_QRC} PROPERTIES + OBJECT_DEPENDS "${AR_QM_FILES}") + else() + message(WARNING "lrelease not found — building without translations (English only)") + endif() +endif() + set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) @@ -19,6 +65,7 @@ add_executable(agentredactor-gui update_manager.cpp ../engine/control_api_client.cpp resources.qrc + ${AR_QRC_EXTRAS} ) target_include_directories(agentredactor-gui PRIVATE diff --git a/linux/gui/i18n/agentredactor_af.ts b/linux/gui/i18n/agentredactor_af.ts new file mode 100644 index 0000000..d64053e --- /dev/null +++ b/linux/gui/i18n/agentredactor_af.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Opdatering gereed om te installeer + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 is afgelaai. Herbegin nou om die opdatering toe te pas. + + + Restart now + Herbegin nou + + + Later + Later + + + Check for updates + Soek vir opdaterings + + + You're up to date. + Jy is op datum. + + + Couldn't check for updates. Try again later. + Kon nie vir opdaterings soek nie. Probeer later weer. + + + &File + &Lêer + + + &Quit + Maak toe + + + Profile + Profiel + + + Detection + Opsporing + + + Regex Patterns + Regex-patrone + + + Keywords + Sleutelwoorde + + + Password + Wagwoord + + + Statistics + Statistiek + + + Session Redactions + Sessieredigerings + + + Logs + Loglêers + + + Settings + Instellings + + + Name: + Naam: + + + Port: + Poort: + + + Forward To + Stuur deur na + + + API Key + API-sleutel + + + Use AI model: + Gebruik AI-model: + + + Confidence threshold: + Vertrouensdrempel: + + + Add + Voeg by + + + Remove + Verwyder + + + Show API key + Wys API-sleutel + + + Copy proxy URL + Kopieer proxy-URL + + + Save + Stoor + + + Use AI model for PII detection + Gebruik KI-model vir PII-opsporing + + + Case sensitive + Hooflettergevoelig + + + Require master password + Vereis hoofwagwoord + + + Clear statistics + Duidelike statistieke + + + Clear + Maak skoon + + + Enable logging + Aktiveer aanteken + + + Show sensitive information in logs + Wys sensitiewe inligting in logs + + + Open log file + Open loglêer + + + Open folder + Open vouer + + + Delete all logs + Vee alle loglêers uit + + + Start on Boot + Begin op Boot + + + Language + Taal + + + System default + Stelselverstek + + + Master Password + Meesterwagwoord + + + Unlock + Ontsluit + + + Agent Redactor is locked + Agent Redactor is gesluit + + + Account number + Rekeningnommer + + + Address + Adres + + + Date + Datum + + + Email + E-pos + + + Person + Persoon + + + Phone + Telefoon + + + URL + URL + + + Secret + Geheim + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Versoeke: %1 PII: %2 Regex: %3 Sleutelwoorde: %4 + + + Engine is not running — retrying… + Enjin loop nie – probeer weer … + + + Delete + Vee uit + + + Validation Error + Valideringsfout + + + Invalid regex syntax. + Ongeldige regex-sintaksis. + + + Case: Yes + Geval: Ja + + + Case: No + Geval: Nee + + + Port must be between 1024 and 65535. + Poort moet tussen 1024 en 65535 wees. + + + Port %1 is already used by profile '%2'. + Poort %1 word reeds deur profiel '%2' gebruik. + + + Forward To URL must start with http:// or https://. + Stuur-deur-na URL moet met http:// of https:// begin. + + + Confidence threshold must be between 0.0 and 1.0. + Vertrouensdrempel moet tussen 0.0 en 1.0 wees. + + + Security Warning + Sekuriteitswaarskuwing + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Jy gebruik 'n HTTP (ongekripteerde) opstroom-URL. Jou API-sleutel sal as plat teks oor die netwerk gestuur word. + + + Error + Fout + + + The engine rejected the profile. Check the engine log for details. + Die enjin het die profiel verwerp. Gaan die enjinlogboek na vir besonderhede. + + + Profile %1 + Profiel %1 + + + The engine rejected the new profile. + Die enjin het die nuwe profiel verwerp. + + + Remove Profile + Verwyder profiel + + + Are you sure? This operation is permanent. + Is jy seker? Hierdie bewerking is permanent. + + + Proxy URL copied to clipboard + Proxy-URL is na knipbord gekopieer + + + Wrong password. + Verkeerde wagwoord. + + + Show sensitive information + Wys sensitiewe inligting + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Sensitiewe logboek skryf rou, ongeredigeerde waardes (insluitend API-sleutels) na die logboek. Aktiveer dit net tydens ontfouting. + + + Enable logging first. + Aktiveer eers aanteken. + + + Delete all logs? + Vee alle loglêers uit? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Dit sal die huidige loglêer en alle geargiveerde sessieloglêers permanent uitvee. Dit kan nie ongedaan gemaak word nie. + + + Downloading AI model + Laai KI-model af + + + Retry + Probeer weer + + + The PII detection model is downloading (%1%). + Die PII-bespeuringsmodel laai tans af (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Die aflaai van die model het misluk. Gaan jou internetverbinding na en probeer weer. PII-opsporing is nie beskikbaar totdat die aflaai voltooi is nie. + + + Are you sure you want to quit? + Is jy seker jy wil toemaak? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + As jy toemaak, sal Agent Redactor nie meer API-verkeer monitor en redigeer nie. + + + Quit Agent Redactor? The engine keeps running in the background. + Verlaat Agent Redactor? Die enjin bly in die agtergrond loop. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Open Agent Redactor + + + Start on Boot + Begin op Boot + + + Language + Taal + + + Quit + Maak toe + + + + PasswordEnableDialog + + Enable password protection + Aktiveer wagwoordbeskerming + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Kies 'n hoofwagwoord vir Agent Redactor. Dit beskerm jou gestoorde API-sleutels op hierdie masjien en is nie verwant aan jou aanmeldwagwoord nie. + + + New password: + Nuwe wagwoord: + + + Confirm password: + Bevestig wagwoord: + + + Password must not be empty. + Wagwoord moet nie leeg wees nie. + + + Passwords do not match. + Wagwoorde stem nie ooreen nie. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Ontsluit Agent Redactor + + + Enter your master password to unlock. + Voer jou hoofwagwoord in om te ontsluit. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_ar.ts b/linux/gui/i18n/agentredactor_ar.ts new file mode 100644 index 0000000..39b3e86 --- /dev/null +++ b/linux/gui/i18n/agentredactor_ar.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + التحديث جاهز للتثبيت + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + تم تنزيل Agent Redactor %1. أعد التشغيل الآن لتطبيق التحديث. + + + Restart now + إعادة التشغيل الآن + + + Later + لاحقًا + + + Check for updates + التحقق من التحديثات + + + You're up to date. + لديك أحدث إصدار. + + + Couldn't check for updates. Try again later. + تعذّر التحقق من التحديثات. حاول مرة أخرى لاحقًا. + + + &File + &ملف + + + &Quit + يترك + + + Profile + حساب تعريفي + + + Detection + كشف + + + Regex Patterns + أنماط ريكس + + + Keywords + الكلمات الرئيسية + + + Password + كلمة المرور + + + Statistics + إحصائيات + + + Session Redactions + تنقيح الجلسة + + + Logs + سجلات + + + Settings + إعدادات + + + Name: + اسم: + + + Port: + ميناء: + + + Forward To + إلى الأمام إلى + + + API Key + مفتاح واجهة برمجة التطبيقات + + + Use AI model: + استخدم نموذج الذكاء الاصطناعي: + + + Confidence threshold: + عتبة الثقة: + + + Add + يضيف + + + Remove + يزيل + + + Show API key + إظهار مفتاح API + + + Copy proxy URL + انسخ عنوان URL للوكيل + + + Save + يحفظ + + + Use AI model for PII detection + استخدم نموذج الذكاء الاصطناعي لاكتشاف معلومات تحديد الهوية الشخصية (PII). + + + Case sensitive + حساسية الموضوع + + + Require master password + تتطلب كلمة المرور الرئيسية + + + Clear statistics + إحصائيات واضحة + + + Clear + واضح + + + Enable logging + تمكين التسجيل + + + Show sensitive information in logs + إظهار المعلومات الحساسة في السجلات + + + Open log file + فتح ملف السجل + + + Open folder + افتح المجلد + + + Delete all logs + حذف كافة السجلات + + + Start on Boot + البدء في التمهيد + + + Language + لغة + + + System default + الافتراضي للنظام + + + Master Password + كلمة المرور الرئيسية + + + Unlock + فتح + + + Agent Redactor is locked + تم تأمين وكيل Redactor + + + Account number + رقم الحساب + + + Address + عنوان + + + Date + تاريخ + + + Email + بريد إلكتروني + + + Person + شخص + + + Phone + هاتف + + + URL + URL + + + Secret + سر + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + الطلبات: %1 معلومات تحديد الهوية الشخصية: %2 التعبير العادي: %3 الكلمات الأساسية: %4 + + + Engine is not running — retrying… + المحرك لا يعمل — جارٍ إعادة المحاولة... + + + Delete + يمسح + + + Validation Error + خطأ في التحقق + + + Invalid regex syntax. + بناء جملة التعبير العادي غير صالح. + + + Case: Yes + الحالة: نعم + + + Case: No + الحالة: لا + + + Port must be between 1024 and 65535. + يجب أن يكون المنفذ بين 1024 و65535. + + + Port %1 is already used by profile '%2'. + المنفذ %1 مستخدم بالفعل بواسطة ملف التعريف '%2'. + + + Forward To URL must start with http:// or https://. + يجب أن يبدأ عنوان URL لإعادة التوجيه بـ http:// أو https://. + + + Confidence threshold must be between 0.0 and 1.0. + يجب أن تتراوح عتبة الثقة بين 0.0 و1.0. + + + Security Warning + تحذير أمني + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + أنت تستخدم عنوان URL الرئيسي لـ HTTP (غير مشفر). سيتم إرسال مفتاح API الخاص بك بنص عادي عبر الشبكة. + + + Error + خطأ + + + The engine rejected the profile. Check the engine log for details. + رفض المحرك الملف الشخصي. تحقق من سجل المحرك للحصول على التفاصيل. + + + Profile %1 + ملف التعريف %1 + + + The engine rejected the new profile. + رفض المحرك الملف الشخصي الجديد. + + + Remove Profile + إزالة الملف الشخصي + + + Are you sure? This operation is permanent. + هل أنت متأكد؟ هذه العملية دائمة. + + + Proxy URL copied to clipboard + تم نسخ عنوان URL للوكيل إلى الحافظة + + + Wrong password. + كلمة مرور خاطئة. + + + Show sensitive information + إظهار المعلومات الحساسة + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + يقوم التسجيل الحساس بكتابة قيم أولية غير منقحة (بما في ذلك مفاتيح واجهة برمجة التطبيقات) في السجل. تمكينه فقط أثناء التصحيح. + + + Enable logging first. + تمكين التسجيل أولا. + + + Delete all logs? + هل تريد حذف كافة السجلات؟ + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + سيؤدي هذا إلى حذف ملف السجل الحالي وجميع سجلات الجلسة المؤرشفة نهائيًا. لا يمكن التراجع عن هذا. + + + Downloading AI model + جارٍ تنزيل نموذج الذكاء الاصطناعي + + + Retry + إعادة المحاولة + + + The PII detection model is downloading (%1%). + يتم الآن تنزيل نموذج الكشف عن معلومات تحديد الهوية الشخصية (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + فشل تنزيل النموذج. تحقق من اتصالك بالإنترنت، ثم أعد المحاولة. الكشف عن PII غير متاح حتى يكتمل التنزيل. + + + Are you sure you want to quit? + هل أنت متأكد أنك تريد الإقلاع عن التدخين؟ + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + إذا قمت بالخروج، فلن يقوم Agent Redactor بمراقبة حركة مرور واجهة برمجة التطبيقات (API) وتنقيحها بعد ذلك. + + + Quit Agent Redactor? The engine keeps running in the background. + هل تريد إنهاء وكيل Redactor؟ يستمر المحرك في العمل في الخلفية. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + فتح وكيل المحرر + + + Start on Boot + البدء في التمهيد + + + Language + لغة + + + Quit + يترك + + + + PasswordEnableDialog + + Enable password protection + تمكين الحماية بكلمة المرور + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + اختر كلمة مرور رئيسية لـ Agent Redactor. إنه يحمي مفاتيح API المخزنة على هذا الجهاز ولا علاقة له بكلمة مرور تسجيل الدخول الخاصة بك. + + + New password: + كلمة المرور الجديدة: + + + Confirm password: + تأكيد كلمة المرور: + + + Password must not be empty. + يجب ألا تكون كلمة المرور فارغة. + + + Passwords do not match. + كلمات المرور غير متطابقة. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + فتح وكيل Redactor + + + Enter your master password to unlock. + أدخل كلمة المرور الرئيسية لفتح القفل. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_az_Latn.ts b/linux/gui/i18n/agentredactor_az_Latn.ts new file mode 100644 index 0000000..fd6214d --- /dev/null +++ b/linux/gui/i18n/agentredactor_az_Latn.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Yeniləmə quraşdırılmağa hazırdır + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 yükləndi. Yeniləməni tətbiq etmək üçün indi yenidən başladın. + + + Restart now + İndi yenidən başlat + + + Later + Daha sonra + + + Check for updates + Yeniləmələri yoxla + + + You're up to date. + Ən son versiyadan istifadə edirsiniz. + + + Couldn't check for updates. Try again later. + Yeniləmələri yoxlamaq mümkün olmadı. Daha sonra yenidən cəhd edin. + + + &File + &Fayl + + + &Quit + Çıxın + + + Profile + Profil + + + Detection + Aşkarlama + + + Regex Patterns + Regex Nümunələri + + + Keywords + Açar sözlər + + + Password + parol + + + Statistics + Statistika + + + Session Redactions + Sessiya redaktələri + + + Logs + Qeydlər + + + Settings + Parametrlər + + + Name: + Adı: + + + Port: + Liman: + + + Forward To + İrəli + + + API Key + API Açarı + + + Use AI model: + AI modelindən istifadə edin: + + + Confidence threshold: + Etibar həddi: + + + Add + əlavə et + + + Remove + Sil + + + Show API key + API açarını göstərin + + + Copy proxy URL + Proksi URL-ni kopyalayın + + + Save + Saxla + + + Use AI model for PII detection + PII aşkarlanması üçün AI modelindən istifadə edin + + + Case sensitive + Hərflərə həssasdır + + + Require master password + Əsas parol tələb edin + + + Clear statistics + Statistikanı təmizləyin + + + Clear + Təmiz + + + Enable logging + Girişi aktivləşdirin + + + Show sensitive information in logs + Jurnallarda həssas məlumatları göstərin + + + Open log file + Günlük faylını açın + + + Open folder + Qovluğu açın + + + Delete all logs + Bütün qeydləri silin + + + Start on Boot + Boot-da başlayın + + + Language + Dil + + + System default + Sistem standartı + + + Master Password + Master Parol + + + Unlock + Kilidi aç + + + Agent Redactor is locked + Agent Redaktor kilidlənib + + + Account number + Hesab nömrəsi + + + Address + Ünvan + + + Date + Tarix + + + Email + E-poçt + + + Person + şəxs + + + Phone + Telefon + + + URL + URL + + + Secret + Gizli + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Sorğular: %1 PII: %2 Regex: %3 Açar sözlər: %4 + + + Engine is not running — retrying… + Mühərrik işləmir — yenidən cəhd edilir... + + + Delete + Sil + + + Validation Error + Doğrulama Xətası + + + Invalid regex syntax. + Yanlış regex sintaksisi. + + + Case: Yes + Dava: Bəli + + + Case: No + Dava: Xeyr + + + Port must be between 1024 and 65535. + Port 1024 və 65535 arasında olmalıdır. + + + Port %1 is already used by profile '%2'. + %1 portu artıq "%2" profili tərəfindən istifadə olunur. + + + Forward To URL must start with http:// or https://. + URL-yə Yönləndirin http:// və ya https:// ilə başlamalıdır. + + + Confidence threshold must be between 0.0 and 1.0. + Etibar həddi 0,0 ilə 1,0 arasında olmalıdır. + + + Security Warning + Təhlükəsizlik Xəbərdarlığı + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Siz HTTP (şifrələnməmiş) yuxarı URL-dən istifadə edirsiniz. API açarınız şəbəkə üzərindən açıq mətnlə göndəriləcək. + + + Error + Xəta + + + The engine rejected the profile. Check the engine log for details. + Mühərrik profili rədd etdi. Təfərrüatlar üçün mühərrik jurnalını yoxlayın. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + Mühərrik yeni profili rədd etdi. + + + Remove Profile + Profili silin + + + Are you sure? This operation is permanent. + əminsən? Bu əməliyyat daimidir. + + + Proxy URL copied to clipboard + Proksi URL mübadilə buferinə kopyalandı + + + Wrong password. + Səhv parol. + + + Show sensitive information + Həssas məlumatları göstərin + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Həssas giriş jurnala xam, redaktə edilməmiş dəyərləri (APİ açarları daxil olmaqla) yazır. Yalnız sazlama zamanı onu aktivləşdirin. + + + Enable logging first. + Əvvəlcə girişi aktivləşdirin. + + + Delete all logs? + Bütün qeydlər silinsin? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Bu, cari jurnal faylını və bütün arxivləşdirilmiş sessiya qeydlərini həmişəlik siləcək. Bu geri qaytarıla bilməz. + + + Downloading AI model + AI modeli yüklənir + + + Retry + Yenidən cəhd et + + + The PII detection model is downloading (%1%). + PII aşkarlama modeli endirilir (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Modelin yüklənməsi uğursuz oldu. İnternet bağlantınızı yoxlayın, sonra yenidən cəhd edin. Yükləmə tamamlanana qədər PII aşkarlanması mövcud deyil. + + + Are you sure you want to quit? + Çıxmaq istədiyinizə əminsiniz? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Əgər çıxsanız, Agent Redactor artıq API trafikinə nəzarət etməyəcək və redaktə etməyəcək. + + + Quit Agent Redactor? The engine keeps running in the background. + Agent Redaktordan çıxın? Mühərrik arxa planda işləməyə davam edir. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Agent Redaktoru açın + + + Start on Boot + Boot-da başlayın + + + Language + Dil + + + Quit + Çıxın + + + + PasswordEnableDialog + + Enable password protection + Parol qorunmasını aktivləşdirin + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Agent Redactor üçün əsas parol seçin. O, bu maşında saxlanılan API açarlarınızı qoruyur və giriş parolunuzla əlaqəsi yoxdur. + + + New password: + Yeni parol: + + + Confirm password: + Şifrəni təsdiqləyin: + + + Password must not be empty. + Parol boş olmamalıdır. + + + Passwords do not match. + Parollar uyğun gəlmir. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Agent Redaktorun kilidini açın + + + Enter your master password to unlock. + Kilidi açmaq üçün əsas parolunuzu daxil edin. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_bg.ts b/linux/gui/i18n/agentredactor_bg.ts new file mode 100644 index 0000000..430e81d --- /dev/null +++ b/linux/gui/i18n/agentredactor_bg.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Актуализацията е готова за инсталиране + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 е изтеглен. Рестартирайте сега, за да приложите актуализацията. + + + Restart now + Рестартиране сега + + + Later + По-късно + + + Check for updates + Проверка за актуализации + + + You're up to date. + Имате най-новата версия. + + + Couldn't check for updates. Try again later. + Не можа да се провери за актуализации. Опитайте отново по-късно. + + + &File + &Файл + + + &Quit + Изход + + + Profile + Профил + + + Detection + Откриване + + + Regex Patterns + Regex шаблони + + + Keywords + Ключови думи + + + Password + Парола + + + Statistics + Статистика + + + Session Redactions + Редакции на сесията + + + Logs + Логове + + + Settings + Настройки + + + Name: + Име: + + + Port: + Порт: + + + Forward To + Препращане към + + + API Key + API ключ + + + Use AI model: + Използвайте AI модел: + + + Confidence threshold: + Праг на доверие: + + + Add + Добавяне + + + Remove + Премахване + + + Show API key + Показване на API ключ + + + Copy proxy URL + Копиране на прокси URL + + + Save + Запазване + + + Use AI model for PII detection + Използвайте AI модел за откриване на PII + + + Case sensitive + Чувствителност към регистъра + + + Require master password + Изискване на главна парола + + + Clear statistics + Ясна статистика + + + Clear + Изчистване + + + Enable logging + Активиране на регистриране + + + Show sensitive information in logs + Показване на поверителна информация в регистрационни файлове + + + Open log file + Отваряне на лог файла + + + Open folder + Отваряне на папката + + + Delete all logs + Изтриване на всички логове + + + Start on Boot + Стартирайте при зареждане + + + Language + Език + + + System default + Системна по подразбиране + + + Master Password + Главна парола + + + Unlock + Отключване + + + Agent Redactor is locked + Agent Redactor е заключен + + + Account number + Номер на сметка + + + Address + Адрес + + + Date + Дата + + + Email + Имейл + + + Person + Лице + + + Phone + Телефон + + + URL + URL + + + Secret + Тайна + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Заявки: %1 PII: %2 Regex: %3 Ключови думи: %4 + + + Engine is not running — retrying… + Двигателят не работи — опитвам отново... + + + Delete + Изтриване + + + Validation Error + Грешка при валидиране + + + Invalid regex syntax. + Невалиден regex синтаксис. + + + Case: Yes + Случай: Да + + + Case: No + Случай: Не + + + Port must be between 1024 and 65535. + Портът трябва да бъде между 1024 и 65535. + + + Port %1 is already used by profile '%2'. + Порт %1 вече се използва от профил '%2'. + + + Forward To URL must start with http:// or https://. + URL за препращане трябва да започва с http:// или https://. + + + Confidence threshold must be between 0.0 and 1.0. + Прагът на увереност трябва да бъде между 0,0 и 1,0. + + + Security Warning + Предупреждение за сигурност + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Използвате HTTP upstream URL (некриптиран). Вашият API ключ ще бъде изпратен като обикновен текст през мрежата. + + + Error + Грешка + + + The engine rejected the profile. Check the engine log for details. + Двигателят отхвърли профила. Проверете дневника на двигателя за подробности. + + + Profile %1 + Профил %1 + + + The engine rejected the new profile. + Двигателят отхвърли новия профил. + + + Remove Profile + Премахване на профил + + + Are you sure? This operation is permanent. + Сигурни ли сте? Тази операция е необратима. + + + Proxy URL copied to clipboard + URL адресът на прокси сървъра е копиран в клипборда + + + Wrong password. + Грешна парола. + + + Show sensitive information + Показване на чувствителна информация + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Чувствителното регистриране записва необработени, нередактирани стойности (включително API ключове) в регистрационния файл. Активирайте го само по време на отстраняване на грешки. + + + Enable logging first. + Първо активирайте регистриране. + + + Delete all logs? + Изтриване на всички логове? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Това ще изтрие перманентно текущия лог файл и всички архивирани логове от сесии. Не може да бъде отменено. + + + Downloading AI model + Изтегляне на AI модел + + + Retry + Опитай отново + + + The PII detection model is downloading (%1%). + Моделът за откриване на PII се изтегля (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Изтеглянето на модела не успя. Проверете интернет връзката си и опитайте отново. Откриването на PII не е налично, докато изтеглянето не приключи. + + + Are you sure you want to quit? + Сигурни ли сте, че искате да излезете? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Ако излезете, Agent Redactor повече няма да следи и редактира API трафика. + + + Quit Agent Redactor? The engine keeps running in the background. + Излизане от Agent Redactor? Двигателят продължава да работи на заден план. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Отваряне на Agent Redactor + + + Start on Boot + Стартирайте при зареждане + + + Language + Език + + + Quit + Изход + + + + PasswordEnableDialog + + Enable password protection + Активирайте защитата с парола + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Изберете главна парола за Agent Redactor. Той защитава вашите съхранени API ключове на тази машина и не е свързан с вашата парола за вход. + + + New password: + Нова парола: + + + Confirm password: + Потвърдете паролата: + + + Password must not be empty. + Паролата не трябва да е празна. + + + Passwords do not match. + Паролите не съвпадат. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Отключете Agent Editor + + + Enter your master password to unlock. + Въведете вашата главна парола, за да отключите. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_cs.ts b/linux/gui/i18n/agentredactor_cs.ts new file mode 100644 index 0000000..d97c108 --- /dev/null +++ b/linux/gui/i18n/agentredactor_cs.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Aktualizace připravena k instalaci + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 byl stažen. Restartujte nyní a aktualizaci použijte. + + + Restart now + Restartovat nyní + + + Later + Později + + + Check for updates + Zkontrolovat aktualizace + + + You're up to date. + Máte nejnovější verzi. + + + Couldn't check for updates. Try again later. + Nepodařilo se zkontrolovat aktualizace. Zkuste to znovu později. + + + &File + &Soubor + + + &Quit + Ukončit + + + Profile + Profil + + + Detection + Detekce + + + Regex Patterns + Regex vzory + + + Keywords + Klíčová slova + + + Password + Heslo + + + Statistics + Statistiky + + + Session Redactions + Redigace relace + + + Logs + Logy + + + Settings + Nastavení + + + Name: + Jméno: + + + Port: + Přístav: + + + Forward To + Přeposlat na + + + API Key + API klíč + + + Use AI model: + Použít model AI: + + + Confidence threshold: + Práh spolehlivosti: + + + Add + Přidat + + + Remove + Odebrat + + + Show API key + Zobrazit klíč API + + + Copy proxy URL + Zkopírujte adresu URL proxy + + + Save + Uložit + + + Use AI model for PII detection + Použijte model AI pro detekci PII + + + Case sensitive + Rozlišovat velikost písmen + + + Require master password + Vyžadovat hlavní heslo + + + Clear statistics + Vymazat statistiky + + + Clear + Vymazat + + + Enable logging + Povolit protokolování + + + Show sensitive information in logs + Zobrazovat citlivé informace v protokolech + + + Open log file + Otevřít soubor logu + + + Open folder + Otevřít složku + + + Delete all logs + Smazat všechny logy + + + Start on Boot + Začněte při spuštění + + + Language + Jazyk + + + System default + Výchozí nastavení systému + + + Master Password + Hlavní heslo + + + Unlock + Odemknout + + + Agent Redactor is locked + Agent Redactor je uzamčen + + + Account number + Číslo účtu + + + Address + Adresa + + + Date + Datum + + + Email + E-mail + + + Person + Osoba + + + Phone + Telefon + + + URL + URL + + + Secret + Tajemství + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Požadavky: %1 PII: %2 Regex: %3 Klíčová slova: %4 + + + Engine is not running — retrying… + Motor neběží – opakování… + + + Delete + Vymazat + + + Validation Error + Chyba ověření + + + Invalid regex syntax. + Neplatná regex syntaxe. + + + Case: Yes + Případ: Ano + + + Case: No + Případ: Ne + + + Port must be between 1024 and 65535. + Port musí být mezi 1024 a 65535. + + + Port %1 is already used by profile '%2'. + Port %1 již používá profil '%2'. + + + Forward To URL must start with http:// or https://. + URL pro přeposlání musí začínat http:// nebo https://. + + + Confidence threshold must be between 0.0 and 1.0. + Prah spolehlivosti musí být mezi 0,0 a 1,0. + + + Security Warning + Bezpečnostní varování + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Používáte HTTP upstream URL (nešifrované). Váš API klíč bude odeslán po síti jako prostý text. + + + Error + Chyba + + + The engine rejected the profile. Check the engine log for details. + Motor odmítl profil. Podrobnosti najdete v protokolu motoru. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + Motor odmítl nový profil. + + + Remove Profile + Odebrat profil + + + Are you sure? This operation is permanent. + Jste si jisti? Tato operace je trvalá. + + + Proxy URL copied to clipboard + Adresa URL proxy zkopírována do schránky + + + Wrong password. + Nesprávné heslo. + + + Show sensitive information + Ukažte citlivé informace + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Citlivé protokolování zapisuje do protokolu nezpracované, neredigované hodnoty (včetně klíčů API). Povolte jej pouze při ladění. + + + Enable logging first. + Nejprve povolte protokolování. + + + Delete all logs? + Smazat všechny logy? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Toto trvale smaže aktuální soubor logu a všechny archivované relační logy. Toto nelze vrátit zpět. + + + Downloading AI model + Stahování modelu AI + + + Retry + Zkusit znovu + + + The PII detection model is downloading (%1%). + Stahuje se model detekce PII (%1 %). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Stažení modelu se nezdařilo. Zkontrolujte připojení k internetu a zkuste to znovu. Detekce PII není k dispozici, dokud se stahování nedokončí. + + + Are you sure you want to quit? + Jste si jisti, že chcete ukončit? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Pokud ukončíte, Agent Redactor již nebude sledovat a redigovat API provoz. + + + Quit Agent Redactor? The engine keeps running in the background. + Ukončit Agent Redactor? Motor běží na pozadí. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Otevřít Agent Redactor + + + Start on Boot + Začněte při spuštění + + + Language + Jazyk + + + Quit + Ukončit + + + + PasswordEnableDialog + + Enable password protection + Povolit ochranu heslem + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Zvolte hlavní heslo pro Agent Redactor. Chrání vaše uložené klíče API na tomto počítači a nesouvisí s vaším přihlašovacím heslem. + + + New password: + Nové heslo: + + + Confirm password: + Potvrďte heslo: + + + Password must not be empty. + Heslo nesmí být prázdné. + + + Passwords do not match. + Hesla se neshodují. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Odemkněte Agent Redactor + + + Enter your master password to unlock. + Pro odemknutí zadejte své hlavní heslo. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_da.ts b/linux/gui/i18n/agentredactor_da.ts new file mode 100644 index 0000000..5808127 --- /dev/null +++ b/linux/gui/i18n/agentredactor_da.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Opdatering klar til installation + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 er blevet downloadet. Genstart nu for at anvende opdateringen. + + + Restart now + Genstart nu + + + Later + Senere + + + Check for updates + Søg efter opdateringer + + + You're up to date. + Du er opdateret. + + + Couldn't check for updates. Try again later. + Kunne ikke søge efter opdateringer. Prøv igen senere. + + + &File + &Fil + + + &Quit + Afslut + + + Profile + Profil + + + Detection + Opdagelse + + + Regex Patterns + Regex-mønstre + + + Keywords + Nøgleord + + + Password + Adgangskode + + + Statistics + Statistik + + + Session Redactions + Sessionredigeringer + + + Logs + Logfiler + + + Settings + Indstillinger + + + Name: + Navn: + + + Port: + Havn: + + + Forward To + Videresend til + + + API Key + API-nøgle + + + Use AI model: + Brug AI-model: + + + Confidence threshold: + Tillidsgrænse: + + + Add + Tilføj + + + Remove + Fjern + + + Show API key + Vis API-nøgle + + + Copy proxy URL + Kopiér proxy-URL + + + Save + Spare + + + Use AI model for PII detection + Brug AI-model til PII-detektion + + + Case sensitive + Forskel på store/små bogstaver + + + Require master password + Kræv hovedadgangskode + + + Clear statistics + Tydelig statistik + + + Clear + Ryd + + + Enable logging + Aktiver logning + + + Show sensitive information in logs + Vis følsomme oplysninger i logfiler + + + Open log file + Åbn logfil + + + Open folder + Åbn mappe + + + Delete all logs + Slet alle logfiler + + + Start on Boot + Start på Boot + + + Language + Sprog + + + System default + Systemstandard + + + Master Password + Hovedadgangskode + + + Unlock + Lås op + + + Agent Redactor is locked + Agent Redactor er låst + + + Account number + Kontonummer + + + Address + Adresse + + + Date + Dato + + + Email + E-mail + + + Person + Person + + + Phone + Telefon + + + URL + URL + + + Secret + Hemmelighed + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Anmodninger: %1 PII: %2 Regex: %3 Nøgleord: %4 + + + Engine is not running — retrying… + Motoren kører ikke - prøver igen... + + + Delete + Slet + + + Validation Error + Valideringsfejl + + + Invalid regex syntax. + Ugyldig regex-syntaks. + + + Case: Yes + Sag: Ja + + + Case: No + Sag: Nej + + + Port must be between 1024 and 65535. + Porten skal være mellem 1024 og 65535. + + + Port %1 is already used by profile '%2'. + Port %1 bruges allerede af profilen '%2'. + + + Forward To URL must start with http:// or https://. + Videresendelses-URL skal starte med http:// eller https://. + + + Confidence threshold must be between 0.0 and 1.0. + Konfidensgrænsen skal være mellem 0,0 og 1,0. + + + Security Warning + Sikkerhedsadvarsel + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Du bruger en HTTP-upstream-URL (ukrypteret). Din API-nøgle sendes i klartekst over netværket. + + + Error + Fejl + + + The engine rejected the profile. Check the engine log for details. + Motoren afviste profilen. Tjek motorloggen for detaljer. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + Motoren afviste den nye profil. + + + Remove Profile + Fjern profil + + + Are you sure? This operation is permanent. + Er du sikker? Denne handling kan ikke fortrydes. + + + Proxy URL copied to clipboard + Proxy-URL kopieret til udklipsholder + + + Wrong password. + Forkert adgangskode. + + + Show sensitive information + Vis følsomme oplysninger + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Følsom logning skriver rå, uredigerede værdier (inklusive API-nøgler) til loggen. Aktiver det kun under fejlretning. + + + Enable logging first. + Aktiver logning først. + + + Delete all logs? + Slet alle logfiler? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Dette sletter den aktuelle logfil og alle arkiverede sessionslogfiler permanent. Dette kan ikke fortrydes. + + + Downloading AI model + Downloader AI-model + + + Retry + Prøv igen + + + The PII detection model is downloading (%1%). + PII-detektionsmodellen downloades (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Download af modellen mislykkedes. Kontrollér din internetforbindelse, og prøv igen. PII-registrering er ikke tilgængelig, før downloaden er fuldført. + + + Are you sure you want to quit? + Er du sikker på, at du vil afslutte? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Hvis du afslutter, overvåger og redigerer Agent Redactor ikke længere API-trafik. + + + Quit Agent Redactor? The engine keeps running in the background. + Forlade Agent Redactor? Motoren bliver ved med at køre i baggrunden. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Åbn Agent Redactor + + + Start on Boot + Start på Boot + + + Language + Sprog + + + Quit + Afslut + + + + PasswordEnableDialog + + Enable password protection + Aktiver adgangskodebeskyttelse + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Vælg en hovedadgangskode til Agent Redactor. Det beskytter dine lagrede API-nøgler på denne maskine og er ikke relateret til din login-adgangskode. + + + New password: + Ny adgangskode: + + + Confirm password: + Bekræft adgangskode: + + + Password must not be empty. + Adgangskoden må ikke være tom. + + + Passwords do not match. + Adgangskoder stemmer ikke overens. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Lås Agent Redactor op + + + Enter your master password to unlock. + Indtast din hovedadgangskode for at låse op. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_de.ts b/linux/gui/i18n/agentredactor_de.ts new file mode 100644 index 0000000..93f2167 --- /dev/null +++ b/linux/gui/i18n/agentredactor_de.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Update bereit zur Installation + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 wurde heruntergeladen. Starten Sie jetzt neu, um das Update anzuwenden. + + + Restart now + Jetzt neu starten + + + Later + Später + + + Check for updates + Nach Updates suchen + + + You're up to date. + Sie sind auf dem neuesten Stand. + + + Couldn't check for updates. Try again later. + Es konnte nicht nach Updates gesucht werden. Versuchen Sie es später erneut. + + + &File + &Datei + + + &Quit + Beenden + + + Profile + Profil + + + Detection + Erkennung + + + Regex Patterns + Regex-Muster + + + Keywords + Schlüsselwörter + + + Password + Passwort + + + Statistics + Statistiken + + + Session Redactions + Sitzungsschwärzungen + + + Logs + Protokolle + + + Settings + Einstellungen + + + Name: + Name: + + + Port: + Hafen: + + + Forward To + Weiterleiten an + + + API Key + API-Schlüssel + + + Use AI model: + KI-Modell verwenden: + + + Confidence threshold: + Vertrauensschwelle: + + + Add + Hinzufügen + + + Remove + Entfernen + + + Show API key + API-Schlüssel anzeigen + + + Copy proxy URL + Proxy-URL kopieren + + + Save + Speichern + + + Use AI model for PII detection + Verwenden Sie ein KI-Modell zur PII-Erkennung + + + Case sensitive + Groß-/Kleinschreibung beachten + + + Require master password + Master-Passwort erforderlich + + + Clear statistics + Übersichtliche Statistiken + + + Clear + Zurücksetzen + + + Enable logging + Protokollierung aktivieren + + + Show sensitive information in logs + Vertrauliche Informationen in Protokollen anzeigen + + + Open log file + Protokolldatei öffnen + + + Open folder + Ordner öffnen + + + Delete all logs + Alle Protokolle löschen + + + Start on Boot + Beginnen Sie beim Booten + + + Language + Sprache + + + System default + Systemstandard + + + Master Password + Hauptpasswort + + + Unlock + Entsperren + + + Agent Redactor is locked + Agent Redactor ist gesperrt + + + Account number + Kontonummer + + + Address + Adresse + + + Date + Datum + + + Email + E-Mail + + + Person + Person + + + Phone + Telefon + + + URL + URL + + + Secret + Geheimnis + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Anfragen: %1 PII: %2 Regex: %3 Schlüsselwörter: %4 + + + Engine is not running — retrying… + Motor läuft nicht – erneuter Versuch… + + + Delete + Löschen + + + Validation Error + Validierungsfehler + + + Invalid regex syntax. + Ungültige Regex-Syntax. + + + Case: Yes + Fall: Ja + + + Case: No + Fall: Nein + + + Port must be between 1024 and 65535. + Port muss zwischen 1024 und 65535 liegen. + + + Port %1 is already used by profile '%2'. + Port %1 wird bereits vom Profil '%2' verwendet. + + + Forward To URL must start with http:// or https://. + Weiterleitungs-URL muss mit http:// oder https:// beginnen. + + + Confidence threshold must be between 0.0 and 1.0. + Konfidenzschwelle muss zwischen 0,0 und 1,0 liegen. + + + Security Warning + Sicherheitswarnung + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Sie verwenden eine HTTP- (unverschlüsselte) Upstream-URL. Ihr API-Schlüssel wird im Klartext über das Netzwerk gesendet. + + + Error + Fehler + + + The engine rejected the profile. Check the engine log for details. + Die Engine hat das Profil abgelehnt. Weitere Informationen finden Sie im Motorprotokoll. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + Die Engine hat das neue Profil abgelehnt. + + + Remove Profile + Profil entfernen + + + Are you sure? This operation is permanent. + Sind Sie sicher? Dieser Vorgang ist dauerhaft. + + + Proxy URL copied to clipboard + Proxy-URL in die Zwischenablage kopiert + + + Wrong password. + Falsches Passwort. + + + Show sensitive information + Sensible Informationen anzeigen + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Bei der sensiblen Protokollierung werden rohe, nicht redigierte Werte (einschließlich API-Schlüssel) in das Protokoll geschrieben. Aktivieren Sie es nur während des Debuggens. + + + Enable logging first. + Aktivieren Sie zuerst die Protokollierung. + + + Delete all logs? + Alle Protokolle löschen? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Dies löscht dauerhaft die aktuelle Protokolldatei und alle archivierten Sitzungsprotokolle. Dies kann nicht rückgängig gemacht werden. + + + Downloading AI model + KI-Modell wird heruntergeladen + + + Retry + Erneut versuchen + + + The PII detection model is downloading (%1%). + Das PII-Erkennungsmodell wird heruntergeladen (%1 %). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Das Herunterladen des Modells ist fehlgeschlagen. Überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut. Die PII-Erkennung ist erst verfügbar, wenn der Download abgeschlossen ist. + + + Are you sure you want to quit? + Sind Sie sicher, dass Sie beenden möchten? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Wenn Sie beenden, überwacht und schwärzt Agent Redactor keinen API-Datenverkehr mehr. + + + Quit Agent Redactor? The engine keeps running in the background. + Agent Redactor beenden? Der Motor läuft im Hintergrund weiter. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Agent Redactor öffnen + + + Start on Boot + Beginnen Sie beim Booten + + + Language + Sprache + + + Quit + Beenden + + + + PasswordEnableDialog + + Enable password protection + Passwortschutz aktivieren + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Wählen Sie ein Master-Passwort für Agent Redactor. Es schützt Ihre auf diesem Computer gespeicherten API-Schlüssel und hat nichts mit Ihrem Anmeldekennwort zu tun. + + + New password: + Neues Passwort: + + + Confirm password: + Passwort bestätigen: + + + Password must not be empty. + Das Passwort darf nicht leer sein. + + + Passwords do not match. + Passwörter stimmen nicht überein. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Schalten Sie Agent Redactor frei + + + Enter your master password to unlock. + Geben Sie zum Entsperren Ihr Master-Passwort ein. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_el.ts b/linux/gui/i18n/agentredactor_el.ts new file mode 100644 index 0000000..5591d86 --- /dev/null +++ b/linux/gui/i18n/agentredactor_el.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Η ενημέρωση είναι έτοιμη για εγκατάσταση + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Το Agent Redactor %1 έχει ληφθεί. Κάντε επανεκκίνηση τώρα για να εφαρμοστεί η ενημέρωση. + + + Restart now + Επανεκκίνηση τώρα + + + Later + Αργότερα + + + Check for updates + Έλεγχος για ενημερώσεις + + + You're up to date. + Έχετε την πιο πρόσφατη έκδοση. + + + Couldn't check for updates. Try again later. + Δεν ήταν δυνατός ο έλεγχος για ενημερώσεις. Δοκιμάστε ξανά αργότερα. + + + &File + &Αρχείο + + + &Quit + Έξοδος + + + Profile + Προφίλ + + + Detection + Ανίχνευση + + + Regex Patterns + Μοτίβα Regex + + + Keywords + Λέξεις-κλειδιά + + + Password + Κωδικός πρόσβασης + + + Statistics + Στατιστικά + + + Session Redactions + Αποκρύψεις συνεδρίας + + + Logs + Αρχεία καταγραφής + + + Settings + Ρυθμίσεις + + + Name: + Ονομα: + + + Port: + Λιμάνι: + + + Forward To + Προώθηση σε + + + API Key + Κλειδί API + + + Use AI model: + Χρησιμοποιήστε το μοντέλο AI: + + + Confidence threshold: + Όριο εμπιστοσύνης: + + + Add + Προσθήκη + + + Remove + Αφαίρεση + + + Show API key + Εμφάνιση κλειδιού API + + + Copy proxy URL + Αντιγραφή διεύθυνσης URL διακομιστή μεσολάβησης + + + Save + Εκτός + + + Use AI model for PII detection + Χρησιμοποιήστε μοντέλο AI για ανίχνευση PII + + + Case sensitive + Διάκριση πεζών-κεφαλαίων + + + Require master password + Απαιτείται κύριος κωδικός πρόσβασης + + + Clear statistics + Ξεκάθαρα στατιστικά + + + Clear + Εκκαθάριση + + + Enable logging + Ενεργοποίηση καταγραφής + + + Show sensitive information in logs + Εμφάνιση ευαίσθητων πληροφοριών στα αρχεία καταγραφής + + + Open log file + Άνοιγμα αρχείου καταγραφής + + + Open folder + Άνοιγμα φακέλου + + + Delete all logs + Διαγραφή όλων των αρχείων καταγραφής + + + Start on Boot + Ξεκινήστε από την εκκίνηση + + + Language + Γλώσσα + + + System default + Προεπιλογή συστήματος + + + Master Password + Κύριος κωδικός πρόσβασης + + + Unlock + Ξεκλείδωμα + + + Agent Redactor is locked + Το Agent Redactor είναι κλειδωμένο + + + Account number + Αριθμός λογαριασμού + + + Address + Διεύθυνση + + + Date + Ημερομηνία + + + Email + Email + + + Person + Άτομο + + + Phone + Τηλέφωνο + + + URL + URL + + + Secret + Μυστικό + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Αιτήματα: %1 PII: %2 Regex: %3 Λέξεις-κλειδιά: %4 + + + Engine is not running — retrying… + Ο κινητήρας δεν λειτουργεί — επανάληψη… + + + Delete + Διαγράφω + + + Validation Error + Σφάλμα επικύρωσης + + + Invalid regex syntax. + Μη έγκυρη σύνταξη regex. + + + Case: Yes + Υπόθεση: Ναι + + + Case: No + Υπόθεση: Όχι + + + Port must be between 1024 and 65535. + Η θύρα πρέπει να είναι μεταξύ 1024 και 65535. + + + Port %1 is already used by profile '%2'. + Η θύρα %1 χρησιμοποιείται ήδη από το προφίλ '%2'. + + + Forward To URL must start with http:// or https://. + Η URL προώθησης πρέπει να αρχίζει με http:// ή https://. + + + Confidence threshold must be between 0.0 and 1.0. + Το κατώφλι εμπιστοσύνης πρέπει να είναι μεταξύ 0,0 και 1,0. + + + Security Warning + Προειδοποίηση ασφαλείας + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Χρησιμοποιείτε HTTP upstream URL (μη κρυπτογραφημένο). Το κλειδί API σας θα σταλεί σε απλό κείμενο μέσω του δικτύου. + + + Error + Σφάλμα + + + The engine rejected the profile. Check the engine log for details. + Ο κινητήρας απέρριψε το προφίλ. Ελέγξτε το αρχείο καταγραφής του κινητήρα για λεπτομέρειες. + + + Profile %1 + Προφίλ %1 + + + The engine rejected the new profile. + Ο κινητήρας απέρριψε το νέο προφίλ. + + + Remove Profile + Αφαίρεση προφίλ + + + Are you sure? This operation is permanent. + Είστε σίγουροι; Αυτή η λειτουργία είναι μόνιμη. + + + Proxy URL copied to clipboard + Η διεύθυνση URL του διακομιστή μεσολάβησης αντιγράφηκε στο πρόχειρο + + + Wrong password. + Λάθος κωδικός πρόσβασης. + + + Show sensitive information + Εμφάνιση ευαίσθητων πληροφοριών + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Η ευαίσθητη καταγραφή εγγράφει ακατέργαστες, μη αναθεωρημένες τιμές (συμπεριλαμβανομένων των κλειδιών API) στο αρχείο καταγραφής. Ενεργοποιήστε το μόνο κατά τον εντοπισμό σφαλμάτων. + + + Enable logging first. + Ενεργοποιήστε πρώτα την καταγραφή. + + + Delete all logs? + Διαγραφή όλων των αρχείων καταγραφής; + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Αυτό θα διαγράψει μόνιμα το τρέχον αρχείο καταγραφής και όλα τα αρχειοθετημένα αρχεία συνεδριών. Δεν μπορεί να αναιρεθεί. + + + Downloading AI model + Λήψη μοντέλου AI + + + Retry + Επανάληψη + + + The PII detection model is downloading (%1%). + Γίνεται λήψη του μοντέλου ανίχνευσης PII (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Η λήψη του μοντέλου απέτυχε. Ελέγξτε τη σύνδεσή σας στο διαδίκτυο και δοκιμάστε ξανά. Ο εντοπισμός PII δεν είναι διαθέσιμος μέχρι να ολοκληρωθεί η λήψη. + + + Are you sure you want to quit? + Είστε σίγουροι ότι θέλετε να κλείσετε; + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Εάν κλείσετε, το Agent Redactor δεν θα παρακολουθεί και δεν θα αποκρύπτει πλέον την κίνηση API. + + + Quit Agent Redactor? The engine keeps running in the background. + Αποχώρηση από το Agent Redactor; Ο κινητήρας συνεχίζει να λειτουργεί στο παρασκήνιο. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Άνοιγμα Agent Redactor + + + Start on Boot + Ξεκινήστε από την εκκίνηση + + + Language + Γλώσσα + + + Quit + Έξοδος + + + + PasswordEnableDialog + + Enable password protection + Ενεργοποίηση προστασίας με κωδικό πρόσβασης + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Επιλέξτε έναν κύριο κωδικό πρόσβασης για το Agent Redactor. Προστατεύει τα αποθηκευμένα κλειδιά API σε αυτό το μηχάνημα και δεν σχετίζεται με τον κωδικό πρόσβασής σας. + + + New password: + Νέος κωδικός πρόσβασης: + + + Confirm password: + Επιβεβαίωση κωδικού πρόσβασης: + + + Password must not be empty. + Ο κωδικός πρόσβασης δεν πρέπει να είναι κενός. + + + Passwords do not match. + Οι κωδικοί πρόσβασης δεν ταιριάζουν. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Ξεκλειδώστε τον Agent Redactor + + + Enter your master password to unlock. + Εισαγάγετε τον κύριο κωδικό πρόσβασης για ξεκλείδωμα. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_es.ts b/linux/gui/i18n/agentredactor_es.ts new file mode 100644 index 0000000..51a9dfe --- /dev/null +++ b/linux/gui/i18n/agentredactor_es.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Actualización lista para instalarse + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Se ha descargado Agent Redactor %1. Reinicie ahora para aplicar la actualización. + + + Restart now + Reiniciar ahora + + + Later + Más tarde + + + Check for updates + Buscar actualizaciones + + + You're up to date. + Está al día. + + + Couldn't check for updates. Try again later. + No se pudieron buscar actualizaciones. Inténtelo de nuevo más tarde. + + + &File + &Archivo + + + &Quit + Salir + + + Profile + Perfil + + + Detection + Detección + + + Regex Patterns + Patrones de regex + + + Keywords + Palabras clave + + + Password + Contraseña + + + Statistics + Estadísticas + + + Session Redactions + Redacciones de la sesión + + + Logs + Registros + + + Settings + Configuración + + + Name: + Nombre: + + + Port: + Puerto: + + + Forward To + Reenviar a + + + API Key + Clave de API + + + Use AI model: + Utilice el modelo de IA: + + + Confidence threshold: + Umbral de confianza: + + + Add + Agregar + + + Remove + Eliminar + + + Show API key + Mostrar clave API + + + Copy proxy URL + Copiar URL del proxy + + + Save + Ahorrar + + + Use AI model for PII detection + Utilice el modelo de IA para la detección de PII + + + Case sensitive + Distinguir mayúsculas y minúsculas + + + Require master password + Requerir contraseña maestra + + + Clear statistics + Borrar estadísticas + + + Clear + Borrar + + + Enable logging + Habilitar el registro + + + Show sensitive information in logs + Mostrar información confidencial en registros + + + Open log file + Abrir archivo de registro + + + Open folder + Abrir carpeta + + + Delete all logs + Eliminar todos los registros + + + Start on Boot + Comenzar al arrancar + + + Language + Idioma + + + System default + Predeterminado del sistema + + + Master Password + Contraseña maestra + + + Unlock + Desbloquear + + + Agent Redactor is locked + El agente redactor está bloqueado + + + Account number + Número de cuenta + + + Address + Dirección + + + Date + Fecha + + + Email + Correo electrónico + + + Person + Persona + + + Phone + Teléfono + + + URL + URL + + + Secret + Secreto + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Solicitudes: %1 PII: %2 Regex: %3 Palabras clave: %4 + + + Engine is not running — retrying… + El motor no está funcionando. Volviendo a intentarlo... + + + Delete + Borrar + + + Validation Error + Error de validación + + + Invalid regex syntax. + Sintaxis de regex no válida. + + + Case: Yes + Caso: Sí + + + Case: No + Caso: No + + + Port must be between 1024 and 65535. + El puerto debe estar entre 1024 y 65535. + + + Port %1 is already used by profile '%2'. + El puerto %1 ya lo utiliza el perfil '%2'. + + + Forward To URL must start with http:// or https://. + La URL de reenvío debe comenzar con http:// o https://. + + + Confidence threshold must be between 0.0 and 1.0. + El umbral de confianza debe estar entre 0,0 y 1,0. + + + Security Warning + Advertencia de seguridad + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Está usando una URL upstream HTTP (sin cifrar). Su clave de API se enviará en texto plano por la red. + + + Error + Error + + + The engine rejected the profile. Check the engine log for details. + El motor rechazó el perfil. Consulte el registro del motor para obtener más detalles. + + + Profile %1 + Perfil %1 + + + The engine rejected the new profile. + El motor rechazó el nuevo perfil. + + + Remove Profile + Eliminar perfil + + + Are you sure? This operation is permanent. + ¿Está seguro? Esta operación es permanente. + + + Proxy URL copied to clipboard + URL de proxy copiada al portapapeles + + + Wrong password. + Contraseña incorrecta. + + + Show sensitive information + Mostrar información sensible + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + El registro confidencial escribe valores sin editar y no redactados (incluidas las claves API) en el registro. Habilítelo solo durante la depuración. + + + Enable logging first. + Habilite el registro primero. + + + Delete all logs? + ¿Eliminar todos los registros? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Esto eliminará permanentemente el archivo de registro actual y todos los registros de sesión archivados. No se puede deshacer. + + + Downloading AI model + Descargando modelo de IA + + + Retry + Reintentar + + + The PII detection model is downloading (%1%). + El modelo de detección de PII se está descargando (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Error al descargar el modelo. Compruebe su conexión a Internet e inténtelo de nuevo. La detección de PII no estará disponible hasta que finalice la descarga. + + + Are you sure you want to quit? + ¿Está seguro de que desea salir? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Si sale, Agent Redactor ya no supervisará ni redactará el tráfico de API. + + + Quit Agent Redactor? The engine keeps running in the background. + ¿Salir del Agente Redactor? El motor sigue funcionando en segundo plano. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Abrir Agent Redactor + + + Start on Boot + Comenzar al arrancar + + + Language + Idioma + + + Quit + Salir + + + + PasswordEnableDialog + + Enable password protection + Habilitar protección con contraseña + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Elija una contraseña maestra para el Agente Redactor. Protege sus claves API almacenadas en esta máquina y no está relacionada con su contraseña de inicio de sesión. + + + New password: + Nueva contraseña: + + + Confirm password: + Confirmar Contraseña: + + + Password must not be empty. + La contraseña no debe estar vacía. + + + Passwords do not match. + Las contraseñas no coinciden. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Desbloquear Agente Redactor + + + Enter your master password to unlock. + Ingrese su contraseña maestra para desbloquear. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_et.ts b/linux/gui/i18n/agentredactor_et.ts new file mode 100644 index 0000000..9e58191 --- /dev/null +++ b/linux/gui/i18n/agentredactor_et.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Värskendus on installimiseks valmis + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 on alla laaditud. Värskenduse rakendamiseks taaskäivitage kohe. + + + Restart now + Taaskäivita kohe + + + Later + Hiljem + + + Check for updates + Kontrolli värskendusi + + + You're up to date. + Teil on uusim versioon. + + + Couldn't check for updates. Try again later. + Värskendusi ei saanud kontrollida. Proovige hiljem uuesti. + + + &File + &Fail + + + &Quit + Välju + + + Profile + Profiil + + + Detection + Tuvastamine + + + Regex Patterns + Regex mustrid + + + Keywords + Märksõnad + + + Password + Parool + + + Statistics + Statistika + + + Session Redactions + Seansi redigeerimised + + + Logs + Logid + + + Settings + Seaded + + + Name: + Nimi: + + + Port: + Port: + + + Forward To + Edasta + + + API Key + API võti + + + Use AI model: + Kasutage AI mudelit: + + + Confidence threshold: + Usalduslävi: + + + Add + Lisa + + + Remove + Eemalda + + + Show API key + Kuva API võti + + + Copy proxy URL + Kopeeri puhverserveri URL + + + Save + Salvesta + + + Use AI model for PII detection + Kasutage PII tuvastamiseks tehisintellekti mudelit + + + Case sensitive + Tõstutundlik + + + Require master password + Nõua peaparooli + + + Clear statistics + Selge statistika + + + Clear + Tühjenda + + + Enable logging + Luba logimine + + + Show sensitive information in logs + Kuva logides tundlikku teavet + + + Open log file + Ava logifail + + + Open folder + Ava kaust + + + Delete all logs + Kustuta kõik logid + + + Start on Boot + Alustage alglaadimisest + + + Language + Keel + + + System default + Süsteemi vaikimisi + + + Master Password + Peaparool + + + Unlock + Ava lukk + + + Agent Redactor is locked + Agent Redactor on lukus + + + Account number + Kontonumber + + + Address + Aadress + + + Date + Kuupäev + + + Email + E-post + + + Person + Isik + + + Phone + Telefon + + + URL + URL + + + Secret + Saladus + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Taotlused: %1 PII: %2 Regex: %3 Märksõnad: %4 + + + Engine is not running — retrying… + Mootor ei tööta – proovitakse uuesti… + + + Delete + Kustuta + + + Validation Error + Valideerimisviga + + + Invalid regex syntax. + Sobimatu regex süntaks. + + + Case: Yes + Juhtum: Jah + + + Case: No + Juhtum: ei + + + Port must be between 1024 and 65535. + Port peab olema vahemikus 1024 kuni 65535. + + + Port %1 is already used by profile '%2'. + Port %1 on juba profiili '%2' kasutuses. + + + Forward To URL must start with http:// or https://. + Edastamise URL peab algama http:// või https://-ga. + + + Confidence threshold must be between 0.0 and 1.0. + Kindluse lävend peab olema vahemikus 0,0 kuni 1,0. + + + Security Warning + Turvahoiatus + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Kasutate HTTP upstream URL-i (krüpteerimata). Teie API võti saadetakse võrgu kaudu lihttekstina. + + + Error + Viga + + + The engine rejected the profile. Check the engine log for details. + Mootor lükkas profiili tagasi. Vaadake üksikasju mootori logist. + + + Profile %1 + Profiil %1 + + + The engine rejected the new profile. + Mootor lükkas uue profiili tagasi. + + + Remove Profile + Eemalda profiil + + + Are you sure? This operation is permanent. + Kas olete kindel? See toiming on püsiv. + + + Proxy URL copied to clipboard + Puhverserveri URL on lõikelauale kopeeritud + + + Wrong password. + Vale parool. + + + Show sensitive information + Näita tundlikku teavet + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Tundlik logimine kirjutab logisse töötlemata, redigeerimata väärtused (sh API võtmed). Lubage see ainult silumise ajal. + + + Enable logging first. + Esmalt lubage logimine. + + + Delete all logs? + Kustuta kõik logid? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + See kustutab jäädavalt praeguse logifaili ja kõik arhiveeritud seansilogid. Seda ei saa tagasi võtta. + + + Downloading AI model + AI-mudeli allalaadimine + + + Retry + Proovi uuesti + + + The PII detection model is downloading (%1%). + PII tuvastamise mudelit laaditakse alla (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Mudeli allalaadimine ebaõnnestus. Kontrollige oma internetiühendust ja proovige uuesti. PII tuvastamine pole saadaval enne allalaadimise lõppu. + + + Are you sure you want to quit? + Kas olete kindel, et soovite väljuda? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Kui väljute, ei jälgi ega redigeeri Agent Redactor enam API liiklust. + + + Quit Agent Redactor? The engine keeps running in the background. + Kas lahkuda Agent Redactorist? Mootor jätkab taustal töötamist. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Ava Agent Redactor + + + Start on Boot + Alustage alglaadimisest + + + Language + Keel + + + Quit + Välju + + + + PasswordEnableDialog + + Enable password protection + Luba paroolikaitse + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Valige Agent Redaktori põhiparool. See kaitseb teie selles masinas salvestatud API võtmeid ega ole seotud teie sisselogimisparooliga. + + + New password: + Uus parool: + + + Confirm password: + Kinnitage parool: + + + Password must not be empty. + Parool ei tohi olla tühi. + + + Passwords do not match. + Paroolid ei ühti. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Avage Agent Redactor + + + Enter your master password to unlock. + Avamiseks sisestage oma põhiparool. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_fi.ts b/linux/gui/i18n/agentredactor_fi.ts new file mode 100644 index 0000000..8645919 --- /dev/null +++ b/linux/gui/i18n/agentredactor_fi.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Päivitys valmis asennettavaksi + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 on ladattu. Käynnistä nyt uudelleen päivityksen käyttöön ottamiseksi. + + + Restart now + Käynnistä uudelleen nyt + + + Later + Myöhemmin + + + Check for updates + Tarkista päivitykset + + + You're up to date. + Sinulla on uusin versio. + + + Couldn't check for updates. Try again later. + Päivityksiä ei voitu tarkistaa. Yritä myöhemmin uudelleen. + + + &File + &Tiedosto + + + &Quit + Lopeta + + + Profile + Profiili + + + Detection + Havaitseminen + + + Regex Patterns + Regex-kuviot + + + Keywords + Avainsanat + + + Password + Salasana + + + Statistics + Tilastot + + + Session Redactions + Istunnon peitetyt + + + Logs + Lokit + + + Settings + Asetukset + + + Name: + Nimi: + + + Port: + Portti: + + + Forward To + Välitä kohteeseen + + + API Key + API-avain + + + Use AI model: + Käytä AI-mallia: + + + Confidence threshold: + Luottamusraja: + + + Add + Lisää + + + Remove + Poista + + + Show API key + Näytä API-avain + + + Copy proxy URL + Kopioi välityspalvelimen URL-osoite + + + Save + Tallentaa + + + Use AI model for PII detection + Käytä AI-mallia PII-tunnistukseen + + + Case sensitive + Kirjainkoko huomioidaan + + + Require master password + Vaadi pääsalasana + + + Clear statistics + Selkeät tilastot + + + Clear + Tyhjennä + + + Enable logging + Ota kirjaus käyttöön + + + Show sensitive information in logs + Näytä arkaluontoiset tiedot lokeissa + + + Open log file + Avaa lokitiedosto + + + Open folder + Avaa kansio + + + Delete all logs + Poista kaikki lokit + + + Start on Boot + Aloita Bootista + + + Language + Kieli + + + System default + Järjestelmän oletus + + + Master Password + Pääsalasana + + + Unlock + Avaa lukitus + + + Agent Redactor is locked + Agent Redactor on lukittu + + + Account number + Tilinumero + + + Address + Osoite + + + Date + Päivämäärä + + + Email + Sähköposti + + + Person + Henkilö + + + Phone + Puhelin + + + URL + URL + + + Secret + Salaisuus + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Pyynnöt: %1 PII: %2 Regex: %3 Avainsanat: %4 + + + Engine is not running — retrying… + Moottori ei käy – yritetään uudelleen… + + + Delete + Poistaa + + + Validation Error + Vahvistusvirhe + + + Invalid regex syntax. + Virheellinen regex-syntaksi. + + + Case: Yes + Tapaus: Kyllä + + + Case: No + Tapaus: Ei + + + Port must be between 1024 and 65535. + Portin on oltava välillä 1024–65535. + + + Port %1 is already used by profile '%2'. + Portti %1 on jo profiilin '%2' käytössä. + + + Forward To URL must start with http:// or https://. + Välitys-URL:n on alettava http:// tai https://. + + + Confidence threshold must be between 0.0 and 1.0. + Luottamusrajan on oltava välillä 0,0–1,0. + + + Security Warning + Turvallisuusvaroitus + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Käytät HTTP-ylävirta-URL-osoitetta (salaamaton). API-avaimesi lähetetään selkokielisenä verkon yli. + + + Error + Virhe + + + The engine rejected the profile. Check the engine log for details. + Moottori hylkäsi profiilin. Katso tarkemmat tiedot moottorin lokista. + + + Profile %1 + Profiili %1 + + + The engine rejected the new profile. + Moottori hylkäsi uuden profiilin. + + + Remove Profile + Poista profiili + + + Are you sure? This operation is permanent. + Oletko varma? Tämä toiminto on pysyvä. + + + Proxy URL copied to clipboard + Välityspalvelimen URL-osoite kopioitu leikepöydälle + + + Wrong password. + Väärä salasana. + + + Show sensitive information + Näytä arkaluontoiset tiedot + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Arkaluonteinen kirjaus kirjoittaa raaka-arvot, joita ei ole muokattu (mukaan lukien API-avaimet) lokiin. Ota se käyttöön vain virheenkorjauksen aikana. + + + Enable logging first. + Ota kirjaus käyttöön ensin. + + + Delete all logs? + Poista kaikki lokit? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Tämä poistaa nykyisen lokitiedoston ja kaikki arkistoidut istuntolokit pysyvästi. Tätä ei voi perua. + + + Downloading AI model + Ladataan tekoälymallia + + + Retry + Yritä uudelleen + + + The PII detection model is downloading (%1%). + PII-tunnistusmallia ladataan (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Mallin lataus epäonnistui. Tarkista internetyhteys ja yritä uudelleen. PII-tunnistus ei ole käytettävissä ennen kuin lataus on valmis. + + + Are you sure you want to quit? + Oletko varma, että haluat lopettaa? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Jos lopetat, Agent Redactor ei enää valvo ja peitä API-liikennettä. + + + Quit Agent Redactor? The engine keeps running in the background. + Lopettaako Agent Redactor? Moottori jatkaa toimintaansa taustalla. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Avaa Agent Redactor + + + Start on Boot + Aloita Bootista + + + Language + Kieli + + + Quit + Lopeta + + + + PasswordEnableDialog + + Enable password protection + Ota salasanasuojaus käyttöön + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Valitse Agent Redactorin pääsalasana. Se suojaa tälle koneelle tallennettuja API-avaimia, eikä se liity kirjautumissalasanasi. + + + New password: + Uusi salasana: + + + Confirm password: + Vahvista salasana: + + + Password must not be empty. + Salasana ei saa olla tyhjä. + + + Passwords do not match. + Salasanat eivät täsmää. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Avaa Agent Redactor + + + Enter your master password to unlock. + Avaa lukitus antamalla pääsalasanasi. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_fil.ts b/linux/gui/i18n/agentredactor_fil.ts new file mode 100644 index 0000000..80dddbe --- /dev/null +++ b/linux/gui/i18n/agentredactor_fil.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Handa nang i-install ang update + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Na-download na ang Agent Redactor %1. I-restart ngayon para mailapat ang update. + + + Restart now + I-restart ngayon + + + Later + Mamaya + + + Check for updates + Mag-check ng mga update + + + You're up to date. + Naka-update ka na. + + + Couldn't check for updates. Try again later. + Hindi nagawang mag-check ng mga update. Subukan ulit mamaya. + + + &File + &File + + + &Quit + Lumabas + + + Profile + Profile + + + Detection + Pagtuklas + + + Regex Patterns + Mga Regex Pattern + + + Keywords + Mga Keyword + + + Password + Password + + + Statistics + Estadistika + + + Session Redactions + Mga Session Redaction + + + Logs + Mga Log + + + Settings + Settings + + + Name: + Pangalan: + + + Port: + Port: + + + Forward To + I-forward Sa + + + API Key + API Key + + + Use AI model: + Gumamit ng modelo ng AI: + + + Confidence threshold: + Threshold ng kumpiyansa: + + + Add + Idagdag + + + Remove + Alisin + + + Show API key + Ipakita ang API key + + + Copy proxy URL + Kopyahin ang proxy URL + + + Save + I-save + + + Use AI model for PII detection + Gumamit ng AI model para sa PII detection + + + Case sensitive + Case sensitive + + + Require master password + Nangangailangan ng master password + + + Clear statistics + I-clear ang mga istatistika + + + Clear + I-clear + + + Enable logging + Paganahin ang pag-log + + + Show sensitive information in logs + Ipakita ang sensitibong impormasyon sa mga log + + + Open log file + Buksan ang log file + + + Open folder + Buksan ang folder + + + Delete all logs + Burahin lahat ng logs + + + Start on Boot + Magsimula sa Boot + + + Language + Wika + + + System default + System default + + + Master Password + Master Password + + + Unlock + I-unlock + + + Agent Redactor is locked + Naka-lock ang Agent Redactor + + + Account number + Account number + + + Address + Address + + + Date + Petsa + + + Email + Email + + + Person + Tao + + + Phone + Telepono + + + URL + URL + + + Secret + Sekreto + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Mga Kahilingan: %1 PII: %2 Regex: %3 Mga Keyword: %4 + + + Engine is not running — retrying… + Hindi tumatakbo ang makina — muling sinusubukan... + + + Delete + Tanggalin + + + Validation Error + Validation Error + + + Invalid regex syntax. + Di-wastong regex syntax. + + + Case: Yes + Kaso: Oo + + + Case: No + Kaso: Hindi + + + Port must be between 1024 and 65535. + Ang port ay dapat nasa pagitan ng 1024 at 65535. + + + Port %1 is already used by profile '%2'. + Ang port %1 ay ginagamit na ng profile '%2'. + + + Forward To URL must start with http:// or https://. + Ang Forward To URL ay dapat magsimula sa http:// o https://. + + + Confidence threshold must be between 0.0 and 1.0. + Ang confidence threshold ay dapat nasa pagitan ng 0.0 at 1.0. + + + Security Warning + Babala sa Seguridad + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Gumagamit ka ng HTTP (hindi naka-encrypt) na upstream URL. Ang iyong API key ay ipapadala nang plaintext sa network. + + + Error + Error + + + The engine rejected the profile. Check the engine log for details. + Tinanggihan ng makina ang profile. Suriin ang log ng engine para sa mga detalye. + + + Profile %1 + Profile %1 + + + The engine rejected the new profile. + Tinanggihan ng makina ang bagong profile. + + + Remove Profile + Alisin ang Profile + + + Are you sure? This operation is permanent. + Sigurado ka ba? Permanenteng operasyon ito. + + + Proxy URL copied to clipboard + Nakopya ang proxy URL sa clipboard + + + Wrong password. + Maling password. + + + Show sensitive information + Ipakita ang sensitibong impormasyon + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Ang sensitibong pag-log ay nagsusulat ng mga hilaw, hindi na-redact na mga halaga (kabilang ang mga API key) sa log. Paganahin lamang ito habang nagde-debug. + + + Enable logging first. + Paganahin muna ang pag-log. + + + Delete all logs? + Burahin lahat ng logs? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Permanente nitong buburahin ang kasalukuyang log file at lahat ng naka-archive na session logs. Hindi ito maibabalik. + + + Downloading AI model + Dina-download ang AI model + + + Retry + Subukan ulit + + + The PII detection model is downloading (%1%). + Nagda-download ang modelo ng PII detection (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Nabigo ang pag-download ng model. Suriin ang iyong koneksyon sa internet, pagkatapos ay subukan ulit. Hindi available ang PII detection hanggang matapos ang download. + + + Are you sure you want to quit? + Sigurado ka bang gusto mong lumabas? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Kung lalabas ka, hindi na mamomonitor at ire-redact ng Agent Redactor ang API traffic. + + + Quit Agent Redactor? The engine keeps running in the background. + Umalis sa Agent Redactor? Ang makina ay patuloy na tumatakbo sa background. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Buksan ang Agent Redactor + + + Start on Boot + Magsimula sa Boot + + + Language + Wika + + + Quit + Lumabas + + + + PasswordEnableDialog + + Enable password protection + Paganahin ang proteksyon ng password + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Pumili ng master password para sa Agent Redactor. Pinoprotektahan nito ang iyong mga nakaimbak na API key sa makinang ito at walang kaugnayan sa iyong password sa pag-log in. + + + New password: + Bagong password: + + + Confirm password: + Kumpirmahin ang password: + + + Password must not be empty. + Hindi dapat walang laman ang password. + + + Passwords do not match. + Hindi tugma ang mga password. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + I-unlock ang Agent Redactor + + + Enter your master password to unlock. + Ilagay ang iyong master password para i-unlock. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_fr.ts b/linux/gui/i18n/agentredactor_fr.ts new file mode 100644 index 0000000..380c868 --- /dev/null +++ b/linux/gui/i18n/agentredactor_fr.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Mise à jour prête à être installée + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 a été téléchargé. Redémarrez maintenant pour appliquer la mise à jour. + + + Restart now + Redémarrer maintenant + + + Later + Plus tard + + + Check for updates + Rechercher des mises à jour + + + You're up to date. + Vous êtes à jour. + + + Couldn't check for updates. Try again later. + Impossible de rechercher des mises à jour. Réessayez plus tard. + + + &File + &Déposer + + + &Quit + Quitter + + + Profile + Profil + + + Detection + Détection + + + Regex Patterns + Modèles regex + + + Keywords + Mots-clés + + + Password + Mot de passe + + + Statistics + Statistiques + + + Session Redactions + Masquages de la session + + + Logs + Journaux + + + Settings + Paramètres + + + Name: + Nom: + + + Port: + Port: + + + Forward To + Transmettre à + + + API Key + Clé API + + + Use AI model: + Utiliser le modèle IA : + + + Confidence threshold: + Seuil de confiance : + + + Add + Ajouter + + + Remove + Supprimer + + + Show API key + Afficher la clé API + + + Copy proxy URL + Copier l'URL du proxy + + + Save + Sauvegarder + + + Use AI model for PII detection + Utiliser le modèle d'IA pour la détection des informations personnelles + + + Case sensitive + Respecter la casse + + + Require master password + Exiger un mot de passe principal + + + Clear statistics + Des statistiques claires + + + Clear + Effacer + + + Enable logging + Activer la journalisation + + + Show sensitive information in logs + Afficher les informations sensibles dans les journaux + + + Open log file + Ouvrir le fichier journal + + + Open folder + Ouvrir le dossier + + + Delete all logs + Supprimer tous les journaux + + + Start on Boot + Démarrer au démarrage + + + Language + Langue + + + System default + Paramètre système + + + Master Password + Mot de passe maître + + + Unlock + Déverrouiller + + + Agent Redactor is locked + Agent Redactor est verrouillé + + + Account number + Numéro de compte + + + Address + Adresse + + + Date + Date + + + Email + E-mail + + + Person + Personne + + + Phone + Téléphone + + + URL + URL + + + Secret + Secret + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Requêtes : %1 PII : %2 Regex : %3 Mots clés : %4 + + + Engine is not running — retrying… + Le moteur ne tourne pas – réessayez… + + + Delete + Supprimer + + + Validation Error + Erreur de validation + + + Invalid regex syntax. + Syntaxe regex non valide. + + + Case: Yes + Cas : Oui + + + Case: No + Cas : Non + + + Port must be between 1024 and 65535. + Le port doit être compris entre 1024 et 65535. + + + Port %1 is already used by profile '%2'. + Le port %1 est déjà utilisé par le profil '%2'. + + + Forward To URL must start with http:// or https://. + L'URL de transmission doit commencer par http:// ou https://. + + + Confidence threshold must be between 0.0 and 1.0. + Le seuil de confiance doit être compris entre 0,0 et 1,0. + + + Security Warning + Avertissement de sécurité + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Vous utilisez une URL upstream HTTP (non chiffrée). Votre clé API sera envoyée en clair sur le réseau. + + + Error + Erreur + + + The engine rejected the profile. Check the engine log for details. + Le moteur a rejeté le profil. Consultez le journal du moteur pour plus de détails. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + Le moteur a rejeté le nouveau profil. + + + Remove Profile + Supprimer le profil + + + Are you sure? This operation is permanent. + Êtes-vous sûr ? Cette opération est irréversible. + + + Proxy URL copied to clipboard + URL du proxy copiée dans le presse-papiers + + + Wrong password. + Mauvais mot de passe. + + + Show sensitive information + Afficher les informations sensibles + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + La journalisation sensible écrit les valeurs brutes et non expurgées (y compris les clés API) dans le journal. Activez-le uniquement pendant le débogage. + + + Enable logging first. + Activez d'abord la journalisation. + + + Delete all logs? + Supprimer tous les journaux ? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Cela supprimera définitivement le fichier journal actuel et tous les journaux de session archivés. Cette action est irréversible. + + + Downloading AI model + Téléchargement du modèle d'IA + + + Retry + Réessayer + + + The PII detection model is downloading (%1%). + Le modèle de détection des informations personnelles est en cours de téléchargement (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Le téléchargement du modèle a échoué. Vérifiez votre connexion Internet, puis réessayez. La détection des PII est indisponible jusqu'à la fin du téléchargement. + + + Are you sure you want to quit? + Êtes-vous sûr de vouloir quitter ? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Si vous quittez, Agent Redactor ne surveillera et ne masquera plus le trafic API. + + + Quit Agent Redactor? The engine keeps running in the background. + Quitter Agent Redactor ? Le moteur continue de tourner en arrière-plan. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Ouvrir Agent Redactor + + + Start on Boot + Démarrer au démarrage + + + Language + Langue + + + Quit + Quitter + + + + PasswordEnableDialog + + Enable password protection + Activer la protection par mot de passe + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Choisissez un mot de passe principal pour Agent Redactor. Il protège vos clés API stockées sur cette machine et n'a aucun rapport avec votre mot de passe de connexion. + + + New password: + Nouveau mot de passe : + + + Confirm password: + Confirmez le mot de passe: + + + Password must not be empty. + Le mot de passe ne doit pas être vide. + + + Passwords do not match. + Les mots de passe ne correspondent pas. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Déverrouiller le rédacteur d'agent + + + Enter your master password to unlock. + Entrez votre mot de passe principal pour déverrouiller. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_ha_Latn.ts b/linux/gui/i18n/agentredactor_ha_Latn.ts new file mode 100644 index 0000000..bd5faf2 --- /dev/null +++ b/linux/gui/i18n/agentredactor_ha_Latn.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Sabuntawa yana shirye don saka shi + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + An sauke Agent Redactor %1. Sake kunnawa yanzu don amfani da sabuntawar. + + + Restart now + Sake kunnawa yanzu + + + Later + Daga baya + + + Check for updates + Duba sabuntawa + + + You're up to date. + Kana da sabon sigar. + + + Couldn't check for updates. Try again later. + Ba a iya duba sabuntawa ba. Sake gwadawa daga baya. + + + &File + &Fayil + + + &Quit + Dakata + + + Profile + Bayanan martaba + + + Detection + Ganewa + + + Regex Patterns + Tsarin Regex + + + Keywords + Mahimman kalmomi + + + Password + Kalmar wucewa + + + Statistics + Kididdiga + + + Session Redactions + Matsalolin Zama + + + Logs + Logs + + + Settings + Saituna + + + Name: + Suna: + + + Port: + Port: + + + Forward To + Gaba Zuwa + + + API Key + Maɓallin API + + + Use AI model: + Yi amfani da samfurin AI: + + + Confidence threshold: + Ƙofar amincewa: + + + Add + Ƙara + + + Remove + Cire + + + Show API key + Nuna maɓallin API + + + Copy proxy URL + Kwafi URL na wakili + + + Save + Ajiye + + + Use AI model for PII detection + Yi amfani da samfurin AI don gano PII + + + Case sensitive + Harka m + + + Require master password + Bukatar babban kalmar sirri + + + Clear statistics + Share kididdiga + + + Clear + Share + + + Enable logging + Kunna shiga + + + Show sensitive information in logs + Nuna mahimman bayanai a cikin rajistan ayyukan + + + Open log file + Buɗe fayil ɗin log + + + Open folder + Bude babban fayil + + + Delete all logs + Share duk rajistan ayyukan + + + Start on Boot + Fara a kan Boot + + + Language + Harshe + + + System default + Tsohuwar tsarin + + + Master Password + Babbar kalmar sirri + + + Unlock + Buɗe + + + Agent Redactor is locked + Agent Redactor yana kulle + + + Account number + Lambar akant + + + Address + Adireshi + + + Date + Kwanan wata + + + Email + Imel + + + Person + Mutum + + + Phone + Waya + + + URL + URL + + + Secret + Sirrin + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Buƙatun: %1 PII: %2 Regex: %3 Mahimman kalmomi: %4 + + + Engine is not running — retrying… + Injin baya aiki - sake gwadawa… + + + Delete + Share + + + Validation Error + Kuskuren Tabbatarwa + + + Invalid regex syntax. + Rubutun regex mara inganci. + + + Case: Yes + Case: E + + + Case: No + Harka: A'a + + + Port must be between 1024 and 65535. + Dole ne tashar jiragen ruwa ta kasance tsakanin 1024 da 65535. + + + Port %1 is already used by profile '%2'. + Port %1 an riga an yi amfani da shi ta hanyar bayanan martaba '%2'. + + + Forward To URL must start with http:// or https://. + Gaba zuwa URL dole ne a fara da http:// ko https://. + + + Confidence threshold must be between 0.0 and 1.0. + Ƙofar amincewa dole ne ya kasance tsakanin 0.0 da 1.0. + + + Security Warning + Gargadin Tsaro + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Kuna amfani da URL na sama (mara ɓoye) HTTP. Za'a aika maɓallin API ɗinku a bayyane akan hanyar sadarwar. + + + Error + Kuskure + + + The engine rejected the profile. Check the engine log for details. + Injin ya ƙi bayanin martabar. Bincika log ɗin injin don cikakkun bayanai. + + + Profile %1 + Bayanan martaba %1 + + + The engine rejected the new profile. + Injin ya ƙi sabon bayanin martaba. + + + Remove Profile + Cire Bayanan martaba + + + Are you sure? This operation is permanent. + Ka tabbata? Wannan aiki na dindindin ne. + + + Proxy URL copied to clipboard + An kwafi URL wakili zuwa allo + + + Wrong password. + Kalmar sirri mara daidai. + + + Show sensitive information + Nuna bayanai masu mahimmanci + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Shigar da hankali yana rubuta danye, ƙimar da ba a daidaita ba (ciki har da maɓallan API) zuwa log ɗin. Kunna shi kawai yayin da ake gyarawa. + + + Enable logging first. + Kunna shiga da farko. + + + Delete all logs? + Share duk rajistan ayyukan? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Wannan zai share fayil ɗin log ɗin na yanzu da duk bayanan zaman da aka adana har abada. Ba za a iya soke wannan ba. + + + Downloading AI model + Ana sauke samfurin AI + + + Retry + Sake gwadawa + + + The PII detection model is downloading (%1%). + Samfurin gano PII yana saukewa (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Sauke samfurin ya gaza. Duba haɗin intanet ɗinka, sannan sake gwadawa. Gano PII ba ya samuwa sai an gama saukewa. + + + Are you sure you want to quit? + Ka tabbata kana so ka daina? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Idan kun daina, Agent Redactor ba zai ƙara saka idanu da rage zirga-zirgar API ba. + + + Quit Agent Redactor? The engine keeps running in the background. + Bar Agent Redactor? Injin yana ci gaba da gudana a baya. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Buɗe Agent Redactor + + + Start on Boot + Fara a kan Boot + + + Language + Harshe + + + Quit + Dakata + + + + PasswordEnableDialog + + Enable password protection + Kunna kariyar kalmar sirri + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Zaɓi babban kalmar sirri don Agent Redactor. Yana kare maɓallan API ɗin ku da aka adana akan wannan na'ura kuma baya da alaƙa da kalmar wucewar shiga ku. + + + New password: + Sabuwar kalmar sirri: + + + Confirm password: + Tabbata kalmar shiga: + + + Password must not be empty. + Dole ne kalmar wucewa ta zama fanko. + + + Passwords do not match. + Kalmomin sirri ba su dace ba. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Buɗe Mai Redactor Agent + + + Enter your master password to unlock. + Shigar da babban kalmar sirri don buɗewa. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_he.ts b/linux/gui/i18n/agentredactor_he.ts new file mode 100644 index 0000000..12e7606 --- /dev/null +++ b/linux/gui/i18n/agentredactor_he.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + עדכון מוכן להתקנה + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 הורד. הפעל מחדש כעת כדי להחיל את העדכון. + + + Restart now + הפעל מחדש כעת + + + Later + מאוחר יותר + + + Check for updates + בדוק עדכונים + + + You're up to date. + יש לך את הגרסה העדכנית. + + + Couldn't check for updates. Try again later. + לא ניתן היה לבדוק עדכונים. נסה שוב מאוחר יותר. + + + &File + &קוֹבֶץ + + + &Quit + צא + + + Profile + פּרוֹפִיל + + + Detection + איתור + + + Regex Patterns + תבניות Regex + + + Keywords + מילות מפתח + + + Password + סיסמה + + + Statistics + סטטיסטיקה + + + Session Redactions + הסרות הפעלה + + + Logs + יומנים + + + Settings + הגדרות + + + Name: + שֵׁם: + + + Port: + נָמָל: + + + Forward To + העבר אל + + + API Key + מפתח API + + + Use AI model: + השתמש במודל AI: + + + Confidence threshold: + סף ביטחון: + + + Add + הוסף + + + Remove + הסר + + + Show API key + הצג מפתח API + + + Copy proxy URL + העתק את כתובת ה-proxy + + + Save + לְהַצִיל + + + Use AI model for PII detection + השתמש במודל AI לזיהוי PII + + + Case sensitive + רישיות תווים + + + Require master password + דרוש סיסמת אב + + + Clear statistics + סטטיסטיקה ברורה + + + Clear + נקה + + + Enable logging + אפשר רישום + + + Show sensitive information in logs + הצג מידע רגיש ביומנים + + + Open log file + פתח קובץ יומן + + + Open folder + פתח תיקייה + + + Delete all logs + מחק את כל היומנים + + + Start on Boot + התחל באתחול + + + Language + שפה + + + System default + ברירת מחדל של המערכת + + + Master Password + סיסמת מנהל + + + Unlock + בטל נעילה + + + Agent Redactor is locked + הסוכן Redactor נעול + + + Account number + מספר חשבון + + + Address + כתובת + + + Date + תאריך + + + Email + דוא"ל + + + Person + אדם + + + Phone + טלפון + + + URL + URL + + + Secret + סוד + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + בקשות: %1 PII: %2 Regex: %3 מילות מפתח: %4 + + + Engine is not running — retrying… + המנוע לא פועל - מנסה שוב... + + + Delete + לִמְחוֹק + + + Validation Error + שגיאת אימות + + + Invalid regex syntax. + תחביר regex לא חוקי. + + + Case: Yes + מקרה: כן + + + Case: No + מקרה: לא + + + Port must be between 1024 and 65535. + הפורט חייב להיות בין 1024 ל-65535. + + + Port %1 is already used by profile '%2'. + פורט %1 כבר נמצא בשימוש על ידי הפרופיל '%2'. + + + Forward To URL must start with http:// or https://. + כתובת ה-URL להעברה אליה חייבת להתחיל ב-http:// או https://. + + + Confidence threshold must be between 0.0 and 1.0. + סף הביטחון חייב להיות בין 0.0 ל-1.0. + + + Security Warning + אזהרת אבטחה + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + אתה משתמש ב-URL upstream של HTTP (לא מוצפן). מפתח ה-API שלך יישלח כטקסט פשוט דרך הרשת. + + + Error + שגיאה + + + The engine rejected the profile. Check the engine log for details. + המנוע דחה את הפרופיל. עיין ביומן המנוע לפרטים. + + + Profile %1 + פרופיל %1 + + + The engine rejected the new profile. + המנוע דחה את הפרופיל החדש. + + + Remove Profile + הסר פרופיל + + + Are you sure? This operation is permanent. + האם אתה בטוח? פעולה זו היא קבועה. + + + Proxy URL copied to clipboard + כתובת ה-proxy הועתקה ללוח + + + Wrong password. + סיסמה שגויה. + + + Show sensitive information + הצג מידע רגיש + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + רישום רגיש כותב ליומן ערכים גולמיים ללא שינוי (כולל מפתחות API). אפשר את זה רק בזמן ניפוי באגים. + + + Enable logging first. + הפעל תחילה רישום. + + + Delete all logs? + למחוק את כל היומנים? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + פעולה זו תמחק לצמיתות את קובץ היומן הנוכחי ואת כל יומני ההפעלה המאוחסנים. לא ניתן לבטל זאת. + + + Downloading AI model + מוריד מודל AI + + + Retry + נסה שוב + + + The PII detection model is downloading (%1%). + מודל זיהוי PII מוריד (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + הורדת המודל נכשלה. בדוק את חיבור האינטרנט ונסה שוב. זיהוי PII אינו זמין עד שההורדה תושלם. + + + Are you sure you want to quit? + האם אתה בטוח שברצונך לצאת? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + אם תצא, Agent Redactor לא ימשיך לפקח ולערוך את תעבורת ה-API. + + + Quit Agent Redactor? The engine keeps running in the background. + לעזוב את Agent Redactor? המנוע ממשיך לפעול ברקע. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + פתח את Agent Redactor + + + Start on Boot + התחל באתחול + + + Language + שפה + + + Quit + צא + + + + PasswordEnableDialog + + Enable password protection + הפעל הגנת סיסמה + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + בחר סיסמת אב עבור Agent Redactor. הוא מגן על מפתחות ה-API המאוחסנים במחשב זה ואינו קשור לסיסמת הכניסה שלך. + + + New password: + סיסמה חדשה: + + + Confirm password: + אשר סיסמה: + + + Password must not be empty. + אסור שהסיסמה תהיה ריקה. + + + Passwords do not match. + הסיסמאות אינן תואמות. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + פתח את הנעילה של Agent Redactor + + + Enter your master password to unlock. + הזן את סיסמת האב שלך כדי לבטל את הנעילה. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_hi.ts b/linux/gui/i18n/agentredactor_hi.ts new file mode 100644 index 0000000..11d77a8 --- /dev/null +++ b/linux/gui/i18n/agentredactor_hi.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + अपडेट इंस्टॉल करने के लिए तैयार + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 डाउनलोड हो गया है। अपडेट लागू करने के लिए अभी पुनरारंभ करें। + + + Restart now + अभी पुनरारंभ करें + + + Later + बाद में + + + Check for updates + अपडेट जांचें + + + You're up to date. + आपके पास नवीनतम संस्करण है। + + + Couldn't check for updates. Try again later. + अपडेट जांचे नहीं जा सके। बाद में पुनः प्रयास करें। + + + &File + &फ़ाइल + + + &Quit + बाहर निकलें + + + Profile + प्रोफ़ाइल + + + Detection + खोज + + + Regex Patterns + Regex पैटर्न + + + Keywords + कीवर्ड + + + Password + पासवर्ड + + + Statistics + आंकड़े + + + Session Redactions + सत्र रिडैक्शन + + + Logs + लॉग + + + Settings + सेटिंग्स + + + Name: + नाम: + + + Port: + पत्तन: + + + Forward To + फ़ॉरवर्ड करें + + + API Key + API कुंजी + + + Use AI model: + एआई मॉडल का प्रयोग करें: + + + Confidence threshold: + आत्मविश्वास की सीमा: + + + Add + जोड़ें + + + Remove + हटाएँ + + + Show API key + एपीआई कुंजी दिखाएँ + + + Copy proxy URL + प्रॉक्सी यूआरएल कॉपी करें + + + Save + बचाना + + + Use AI model for PII detection + पीआईआई का पता लगाने के लिए एआई मॉडल का उपयोग करें + + + Case sensitive + केस संवेदनशील + + + Require master password + मास्टर पासवर्ड की आवश्यकता है + + + Clear statistics + स्पष्ट आँकड़े + + + Clear + साफ़ करें + + + Enable logging + लॉगिंग सक्षम करें + + + Show sensitive information in logs + लॉग में संवेदनशील जानकारी दिखाएं + + + Open log file + लॉग फ़ाइल खोलें + + + Open folder + फ़ोल्डर खोलें + + + Delete all logs + सभी लॉग हटाएँ + + + Start on Boot + बूट पर प्रारंभ करें + + + Language + भाषा + + + System default + सिस्टम डिफ़ॉल्ट + + + Master Password + मास्टर पासवर्ड + + + Unlock + अनलॉक करें + + + Agent Redactor is locked + एजेंट रेडैक्टर लॉक है + + + Account number + खाता संख्या + + + Address + पता + + + Date + तारीख + + + Email + ईमेल + + + Person + व्यक्ति + + + Phone + फ़ोन + + + URL + URL + + + Secret + गुप्त + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + अनुरोध: %1 पीआईआई: %2 रेगेक्स: %3 कीवर्ड: %4 + + + Engine is not running — retrying… + इंजन नहीं चल रहा है - पुनः प्रयास किया जा रहा है... + + + Delete + मिटाना + + + Validation Error + मान्यता त्रुटि + + + Invalid regex syntax. + अमान्य regex सिंटैक्स। + + + Case: Yes + केस: हाँ + + + Case: No + केस: नहीं + + + Port must be between 1024 and 65535. + पोर्ट 1024 और 65535 के बीच होना चाहिए। + + + Port %1 is already used by profile '%2'. + पोर्ट %1 पहले से ही '%2' प्रोफ़ाइल द्वारा उपयोग में है। + + + Forward To URL must start with http:// or https://. + Forward To URL को http:// या https:// से शुरू होना चाहिए। + + + Confidence threshold must be between 0.0 and 1.0. + विश्वास सीमा 0.0 और 1.0 के बीच होनी चाहिए। + + + Security Warning + सुरक्षा चेतावनी + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + आप एक HTTP (अएन्क्रिप्टेड) अपस्ट्रीम URL का उपयोग कर रहे हैं। आपकी API कुंजी नेटवर्क पर प्लेनटेक्स्ट में भेजी जाएगी। + + + Error + त्रुटि + + + The engine rejected the profile. Check the engine log for details. + इंजन ने प्रोफ़ाइल को अस्वीकार कर दिया. विवरण के लिए इंजन लॉग की जाँच करें। + + + Profile %1 + प्रोफ़ाइल %1 + + + The engine rejected the new profile. + इंजन ने नई प्रोफ़ाइल को अस्वीकार कर दिया. + + + Remove Profile + प्रोफ़ाइल हटाएँ + + + Are you sure? This operation is permanent. + क्या आप निश्चित हैं? यह कार्रवाई स्थायी है। + + + Proxy URL copied to clipboard + प्रॉक्सी यूआरएल क्लिपबोर्ड पर कॉपी किया गया + + + Wrong password. + ग़लत पासवर्ड. + + + Show sensitive information + संवेदनशील जानकारी दिखाएं + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + संवेदनशील लॉगिंग लॉग में कच्चे, अप्रकाशित मान (एपीआई कुंजी सहित) लिखता है। इसे केवल डिबगिंग के दौरान ही सक्षम करें। + + + Enable logging first. + पहले लॉगिंग सक्षम करें. + + + Delete all logs? + सभी लॉग हटाएँ? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + यह वर्तमान लॉग फ़ाइल और सभी संग्रहीत सत्र लॉग को स्थायी रूप से हटा देगा। इसे पूर्ववत नहीं किया जा सकता। + + + Downloading AI model + AI मॉडल डाउनलोड हो रहा है + + + Retry + पुनः प्रयास करें + + + The PII detection model is downloading (%1%). + PII डिटेक्शन मॉडल डाउनलोड हो रहा है (%1%)। + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + मॉडल डाउनलोड विफल रहा। अपना इंटरनेट कनेक्शन जांचें, फिर पुनः प्रयास करें। डाउनलोड पूरा होने तक PII पहचान उपलब्ध नहीं है। + + + Are you sure you want to quit? + क्या आप निश्चित रूप से बाहर निकलना चाहते हैं? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + यदि आप बाहर निकलते हैं, तो Agent Redactor API ट्रैफ़िक की निगरानी और रिडैक्शन नहीं करेगा। + + + Quit Agent Redactor? The engine keeps running in the background. + एजेंट रेडैक्टर छोड़ें? इंजन पृष्ठभूमि में चलता रहता है. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Agent Redactor खोलें + + + Start on Boot + बूट पर प्रारंभ करें + + + Language + भाषा + + + Quit + बाहर निकलें + + + + PasswordEnableDialog + + Enable password protection + पासवर्ड सुरक्षा सक्षम करें + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + एजेंट रेडैक्टर के लिए एक मास्टर पासवर्ड चुनें। यह इस मशीन पर आपकी संग्रहीत एपीआई कुंजियों की सुरक्षा करता है और आपके लॉगिन पासवर्ड से असंबंधित है। + + + New password: + नया पासवर्ड: + + + Confirm password: + पासवर्ड की पुष्टि कीजिये: + + + Password must not be empty. + पासवर्ड खाली नहीं होना चाहिए. + + + Passwords do not match. + सांकेतिक शब्द मेल नहीं खाते। + + + + PasswordUnlockDialog + + Unlock Agent Redactor + एजेंट रेडैक्टर को अनलॉक करें + + + Enter your master password to unlock. + अनलॉक करने के लिए अपना मास्टर पासवर्ड दर्ज करें। + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_hr.ts b/linux/gui/i18n/agentredactor_hr.ts new file mode 100644 index 0000000..191b92c --- /dev/null +++ b/linux/gui/i18n/agentredactor_hr.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Ažuriranje spremno za instalaciju + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 je preuzet. Ponovno pokrenite sada da biste primijenili ažuriranje. + + + Restart now + Ponovno pokreni sada + + + Later + Kasnije + + + Check for updates + Provjeri ažuriranja + + + You're up to date. + Imate najnoviju verziju. + + + Couldn't check for updates. Try again later. + Nije moguće provjeriti ažuriranja. Pokušajte ponovno kasnije. + + + &File + &Datoteka + + + &Quit + Izađi + + + Profile + Profil + + + Detection + Otkrivanje + + + Regex Patterns + Regex uzorci + + + Keywords + Ključne riječi + + + Password + Lozinka + + + Statistics + Statistika + + + Session Redactions + Maskiranja u sesiji + + + Logs + Dnevnici + + + Settings + Postavke + + + Name: + Ime: + + + Port: + Luka: + + + Forward To + Proslijedi na + + + API Key + API ključ + + + Use AI model: + Koristi AI model: + + + Confidence threshold: + Prag pouzdanosti: + + + Add + Dodaj + + + Remove + Ukloni + + + Show API key + Prikaži API ključ + + + Copy proxy URL + Kopiraj proxy URL + + + Save + Uštedjeti + + + Use AI model for PII detection + Koristite AI model za otkrivanje PII + + + Case sensitive + Razlikuj velika i mala slova + + + Require master password + Zahtijevaj glavnu lozinku + + + Clear statistics + Jasna statistika + + + Clear + Očisti + + + Enable logging + Omogući bilježenje + + + Show sensitive information in logs + Prikaži osjetljive podatke u zapisima + + + Open log file + Otvori datoteku dnevnika + + + Open folder + Otvori mapu + + + Delete all logs + Izbriši sve dnevnike + + + Start on Boot + Pokrenite na Boot + + + Language + Jezik + + + System default + Zadano sustavom + + + Master Password + Glavna lozinka + + + Unlock + Otključaj + + + Agent Redactor is locked + Agent Redactor je zaključan + + + Account number + Broj računa + + + Address + Adresa + + + Date + Datum + + + Email + E-pošta + + + Person + Osoba + + + Phone + Telefon + + + URL + URL + + + Secret + Tajna + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Zahtjevi: %1 PII: %2 Regex: %3 Ključne riječi: %4 + + + Engine is not running — retrying… + Motor ne radi — ponovni pokušaj… + + + Delete + Izbrisati + + + Validation Error + Greška validacije + + + Invalid regex syntax. + Nevažeća regex sintaksa. + + + Case: Yes + Slučaj: Da + + + Case: No + Slučaj: br + + + Port must be between 1024 and 65535. + Port mora biti između 1024 i 65535. + + + Port %1 is already used by profile '%2'. + Port %1 već koristi profil '%2'. + + + Forward To URL must start with http:// or https://. + URL za prosljeđivanje mora počinjati s http:// ili https://. + + + Confidence threshold must be between 0.0 and 1.0. + Prag pouzdanosti mora biti između 0,0 i 1,0. + + + Security Warning + Sigurnosno upozorenje + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Koristite HTTP upstream URL (nekriptiran). Vaš API ključ bit će poslan kao običan tekst preko mreže. + + + Error + Greška + + + The engine rejected the profile. Check the engine log for details. + Motor je odbio profil. Provjerite zapisnik motora za detalje. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + Motor je odbio novi profil. + + + Remove Profile + Ukloni profil + + + Are you sure? This operation is permanent. + Jeste li sigurni? Ova operacija je trajna. + + + Proxy URL copied to clipboard + Proxy URL kopiran u međuspremnik + + + Wrong password. + Pogrešna lozinka. + + + Show sensitive information + Prikaži osjetljive informacije + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Osjetljivo bilježenje zapisuje sirove, neredigirane vrijednosti (uključujući API ključeve) u zapisnik. Omogućite ga samo tijekom otklanjanja pogrešaka. + + + Enable logging first. + Prvo omogućite bilježenje. + + + Delete all logs? + Izbrisati sve dnevnike? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Ovo će trajno izbrisati trenutnu datoteku dnevnika i sve arhivirane dnevnike sesija. Ne može se poništiti. + + + Downloading AI model + Preuzimanje AI modela + + + Retry + Pokušaj ponovno + + + The PII detection model is downloading (%1%). + Model otkrivanja PII se preuzima (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Preuzimanje modela nije uspjelo. Provjerite internetsku vezu i pokušajte ponovno. Otkrivanje PII-ja nije dostupno dok se preuzimanje ne dovrši. + + + Are you sure you want to quit? + Jeste li sigurni da želite izaći? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Ako izađete, Agent Redactor više neće nadzirati i maskirati API promet. + + + Quit Agent Redactor? The engine keeps running in the background. + Napustiti Agent Redactor? Motor nastavlja raditi u pozadini. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Otvori Agent Redactor + + + Start on Boot + Pokrenite na Boot + + + Language + Jezik + + + Quit + Izađi + + + + PasswordEnableDialog + + Enable password protection + Omogući zaštitu lozinkom + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Odaberite glavnu lozinku za Agent Redactor. Štiti vaše pohranjene API ključeve na ovom računalu i nije povezan s vašom lozinkom za prijavu. + + + New password: + Nova lozinka: + + + Confirm password: + Potvrdite lozinku: + + + Password must not be empty. + Lozinka ne smije biti prazna. + + + Passwords do not match. + Lozinke se ne podudaraju. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Otključajte Agent Editor + + + Enter your master password to unlock. + Unesite svoju glavnu lozinku za otključavanje. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_hu.ts b/linux/gui/i18n/agentredactor_hu.ts new file mode 100644 index 0000000..4bf8cee --- /dev/null +++ b/linux/gui/i18n/agentredactor_hu.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Frissítés készen áll a telepítésre + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Az Agent Redactor %1 letöltődött. Indítsa újra most a frissítés alkalmazásához. + + + Restart now + Újraindítás most + + + Later + Később + + + Check for updates + Frissítések keresése + + + You're up to date. + Az alkalmazás naprakész. + + + Couldn't check for updates. Try again later. + Nem sikerült frissítéseket keresni. Próbálja újra később. + + + &File + &Fájl + + + &Quit + Kilépés + + + Profile + Profil + + + Detection + Érzékelés + + + Regex Patterns + Regex minták + + + Keywords + Kulcsszavak + + + Password + Jelszó + + + Statistics + Statisztika + + + Session Redactions + Munkamenet-anonimizálások + + + Logs + Naplók + + + Settings + Beállítások + + + Name: + Név: + + + Port: + Kikötő: + + + Forward To + Továbbítás ide + + + API Key + API kulcs + + + Use AI model: + AI modell használata: + + + Confidence threshold: + Bizalmi küszöb: + + + Add + Hozzáadás + + + Remove + Eltávolítás + + + Show API key + API-kulcs megjelenítése + + + Copy proxy URL + Proxy URL másolása + + + Save + Megtakarítás + + + Use AI model for PII detection + Használjon mesterséges intelligencia modellt a személyazonosításra alkalmas adatok észleléséhez + + + Case sensitive + Kis- és nagybetűérzékeny + + + Require master password + Fő jelszó szükséges + + + Clear statistics + Tiszta statisztikák + + + Clear + Törlés + + + Enable logging + Naplózás engedélyezése + + + Show sensitive information in logs + Érzékeny információk megjelenítése a naplókban + + + Open log file + Naplófájl megnyitása + + + Open folder + Mappa megnyitása + + + Delete all logs + Összes napló törlése + + + Start on Boot + Indítsa el a Boot-on + + + Language + Nyelv + + + System default + Rendszer alapértelmezett + + + Master Password + Mesterjelszó + + + Unlock + Feloldás + + + Agent Redactor is locked + Az Agent Redactor zárolva van + + + Account number + Számlaszám + + + Address + Cím + + + Date + Dátum + + + Email + E-mail + + + Person + Személy + + + Phone + Telefon + + + URL + URL + + + Secret + Titok + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Kérések: %1 PII: %2 Regex: %3 Kulcsszavak: %4 + + + Engine is not running — retrying… + A motor nem jár – újrapróbálkozás… + + + Delete + Töröl + + + Validation Error + Érvényesítési hiba + + + Invalid regex syntax. + Érvénytelen regex szintaxis. + + + Case: Yes + Eset: Igen + + + Case: No + Eset: Nem + + + Port must be between 1024 and 65535. + A portnak 1024 és 65535 között kell lennie. + + + Port %1 is already used by profile '%2'. + A(z) %1 portot már használja a(z) '%2' profil. + + + Forward To URL must start with http:// or https://. + A továbbítási URL-nek http:// vagy https://-sal kell kezdődnie. + + + Confidence threshold must be between 0.0 and 1.0. + A bizalmi küszöbnek 0,0 és 1,0 között kell lennie. + + + Security Warning + Biztonsági figyelmeztetés + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + HTTP upstream URL-t használ (titkosítatlan). Az API kulcsa egyszerű szövegként kerül elküldésre a hálózaton. + + + Error + Hiba + + + The engine rejected the profile. Check the engine log for details. + A motor elutasította a profilt. A részletekért ellenőrizze a motornaplót. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + A motor elutasította az új profilt. + + + Remove Profile + Profil eltávolítása + + + Are you sure? This operation is permanent. + Biztos benne? Ez a művelet végleges. + + + Proxy URL copied to clipboard + Proxy URL a vágólapra másolva + + + Wrong password. + Hibás jelszó. + + + Show sensitive information + Érzékeny információk megjelenítése + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Az érzékeny naplózás nyers, módosítatlan értékeket (beleértve az API-kulcsokat) ír a naplóba. Csak hibakeresés közben engedélyezze. + + + Enable logging first. + Először engedélyezze a naplózást. + + + Delete all logs? + Törli az összes naplót? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Ez véglegesen törli az aktuális naplófájlt és az összes archivált munkamenet-naplót. Ez nem vonható vissza. + + + Downloading AI model + AI-modell letöltése + + + Retry + Újra + + + The PII detection model is downloading (%1%). + A személyazonosításra alkalmas azonosítási modell letöltése folyamatban van (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + A modell letöltése nem sikerült. Ellenőrizze az internetkapcsolatot, majd próbálja újra. A PII-észlelés nem érhető el a letöltés befejezéséig. + + + Are you sure you want to quit? + Biztosan ki szeretne lépni? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Ha kilép, az Agent Redactor többé nem figyeli és nem anonimizálja az API-forgalmat. + + + Quit Agent Redactor? The engine keeps running in the background. + Kilép az Agent Redactorból? A motor folyamatosan jár a háttérben. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Agent Redactor megnyitása + + + Start on Boot + Indítsa el a Boot-on + + + Language + Nyelv + + + Quit + Kilépés + + + + PasswordEnableDialog + + Enable password protection + Jelszavas védelem engedélyezése + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Válasszon fő jelszót az Agent Redactor számára. Ez védi a gépen tárolt API-kulcsokat, és nem kapcsolódik a bejelentkezési jelszavához. + + + New password: + Új jelszó: + + + Confirm password: + Jelszó megerősítése: + + + Password must not be empty. + A jelszó nem lehet üres. + + + Passwords do not match. + A jelszavak nem egyeznek. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Oldja fel az Agent Redactort + + + Enter your master password to unlock. + A feloldáshoz adja meg fő jelszavát. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_hy.ts b/linux/gui/i18n/agentredactor_hy.ts new file mode 100644 index 0000000..ea448e5 --- /dev/null +++ b/linux/gui/i18n/agentredactor_hy.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Թարմացումը պատրաստ է տեղադրման + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1-ը ներբեռնված է։ Վերամեկնարկեք հիմա՝ թարմացումը կիրառելու համար։ + + + Restart now + Վերամեկնարկել հիմա + + + Later + Ավելի ուշ + + + Check for updates + Ստուգել թարմացումները + + + You're up to date. + Դուք ունեք վերջին տարբերակը։ + + + Couldn't check for updates. Try again later. + Չհաջողվեց ստուգել թարմացումները։ Փորձեք ավելի ուշ։ + + + &File + &Ֆայլ + + + &Quit + Դուրս գալ + + + Profile + Անձնագիր + + + Detection + Հայտնաբերում + + + Regex Patterns + Regex նմուշներ + + + Keywords + Բանալի բառեր + + + Password + Գաղտնաբառ + + + Statistics + Վիճակագրություն + + + Session Redactions + Նստաշրջանի ջնջումներ + + + Logs + Մատյաններ + + + Settings + Կարգավորումներ + + + Name: + Անունը: + + + Port: + Նավահանգիստ: + + + Forward To + Փոխանցել + + + API Key + API բանալի + + + Use AI model: + Օգտագործեք AI մոդելը. + + + Confidence threshold: + Վստահության շեմ. + + + Add + Ավելացնել + + + Remove + Հեռացնել + + + Show API key + Ցույց տալ API ստեղնը + + + Copy proxy URL + Պատճենել վստահված անձի URL-ը + + + Save + Պահպանել + + + Use AI model for PII detection + Օգտագործեք AI մոդելը PII հայտնաբերման համար + + + Case sensitive + Հաշվի առնել ռեգիստրը + + + Require master password + Պահանջվում է հիմնական գաղտնաբառ + + + Clear statistics + Մաքրել վիճակագրությունը + + + Clear + Մաքրել + + + Enable logging + Միացնել գրանցումը + + + Show sensitive information in logs + Ցուցադրել զգայուն տեղեկատվությունը գրանցամատյաններում + + + Open log file + Բացել մատյանի ֆայլը + + + Open folder + Բացել պանակը + + + Delete all logs + Ջնջել բոլոր մատյանները + + + Start on Boot + Սկսեք Boot-ից + + + Language + Լեզու + + + System default + Համակարգային լռելյայն + + + Master Password + Հիմնական գաղտնաբառ + + + Unlock + Արգելափակել + + + Agent Redactor is locked + Agent Redactor-ը կողպված է + + + Account number + Հաշվի համարը + + + Address + Հասցե + + + Date + Ամսաթիվ + + + Email + Էլ. փոստ + + + Person + Անձ + + + Phone + Հեռախոս + + + URL + URL + + + Secret + Գաղտնիք + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Հարցումներ՝ %1 PII՝ %2 Ռեգեքս՝ %3 Հիմնաբառեր՝ %4 + + + Engine is not running — retrying… + Շարժիչը չի աշխատում. նորից փորձում է… + + + Delete + Ջնջել + + + Validation Error + Վավերացման սխալ + + + Invalid regex syntax. + Անվավեր regex շարահյուսություն: + + + Case: Yes + Դեպք: Այո + + + Case: No + Դեպք՝ ոչ + + + Port must be between 1024 and 65535. + Պորտը պետք է լինի 1024-65535 միջակայքում: + + + Port %1 is already used by profile '%2'. + %1 պորտն արդեն օգտագործվում է '%2' պրոֆիլի կողմից: + + + Forward To URL must start with http:// or https://. + Փոխանցման URL-ը պետք է սկսվի http:// կամ https://-ով: + + + Confidence threshold must be between 0.0 and 1.0. + Վստահության շեմը պետք է լինի 0,0-1,0 միջակայքում: + + + Security Warning + Անվտանգության զգուշացում + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Դուք օգտագործում եք HTTP upstream URL (գաղտնագրված չէ): Ձեր API բանալին ցանցով կուղարկվի որպես պարզ տեքստ: + + + Error + Սխալ + + + The engine rejected the profile. Check the engine log for details. + Շարժիչը մերժեց պրոֆիլը: Մանրամասների համար ստուգեք շարժիչի գրանցամատյանը: + + + Profile %1 + Պրոֆիլ %1 + + + The engine rejected the new profile. + Շարժիչը մերժեց նոր պրոֆիլը: + + + Remove Profile + Հեռացնել պրոֆիլը + + + Are you sure? This operation is permanent. + Համոզված եք? Այս գործողությունը անդառնալի է: + + + Proxy URL copied to clipboard + Վստահված անձի URL-ը պատճենվեց սեղմատախտակին + + + Wrong password. + Սխալ գաղտնաբառը: + + + Show sensitive information + Ցույց տալ զգայուն տեղեկատվությունը + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Զգայուն գրանցումը գրանցամատյանում գրում է չմշակված, չվերադարձված արժեքներ (ներառյալ API ստեղները): Միացրեք այն միայն վրիպազերծման ժամանակ: + + + Enable logging first. + Նախ միացրեք գրանցումը: + + + Delete all logs? + Ջնջել բոլոր մատյանները? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Այն մշտապես կջնջի ընթացիկ մատյանի ֆայլը և բոլոր արխիվացված նստաշրջանների մատյանները: Այն հնարավոր չէ հետարկել: + + + Downloading AI model + AI մոդելի ներբեռնում + + + Retry + Կրկնել + + + The PII detection model is downloading (%1%). + PII հայտնաբերման մոդելը ներբեռնվում է (%1%): + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Մոդելի ներբեռնումը ձախողվեց։ Ստուգեք ձեր ինտերնետ կապը և փորձեք կրկին։ PII-ի հայտնաբերումը հասանելի չէ մինչև ներբեռնման ավարտը։ + + + Are you sure you want to quit? + Համոզված եք, որ ցանկանում եք դուրս գալ? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Եթե դուրս գաք, Agent Redactor-ը այլևս չի հսկի և չի ջնջի API տրաֆիկը: + + + Quit Agent Redactor? The engine keeps running in the background. + Հրաժարվե՞լ Agent Redactor-ից: Շարժիչը շարունակում է աշխատել հետին պլանում: + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Բացել Agent Redactor-ը + + + Start on Boot + Սկսեք Boot-ից + + + Language + Լեզու + + + Quit + Դուրս գալ + + + + PasswordEnableDialog + + Enable password protection + Միացնել գաղտնաբառի պաշտպանությունը + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Ընտրեք հիմնական գաղտնաբառ Agent Redactor-ի համար: Այն պաշտպանում է ձեր պահված API ստեղները այս մեքենայի վրա և կապված չէ ձեր մուտքի գաղտնաբառի հետ: + + + New password: + Նոր գաղտնաբառ. + + + Confirm password: + Հաստատեք գաղտնաբառը. + + + Password must not be empty. + Գաղտնաբառը չպետք է դատարկ լինի: + + + Passwords do not match. + Գաղտնաբառերը չեն համընկնում: + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Բացել Agent Redactor-ը + + + Enter your master password to unlock. + Մուտքագրեք ձեր հիմնական գաղտնաբառը՝ ապակողպելու համար: + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_id.ts b/linux/gui/i18n/agentredactor_id.ts new file mode 100644 index 0000000..ce7cd81 --- /dev/null +++ b/linux/gui/i18n/agentredactor_id.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Pembaruan siap dipasang + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 telah diunduh. Mulai ulang sekarang untuk menerapkan pembaruan. + + + Restart now + Mulai ulang sekarang + + + Later + Nanti + + + Check for updates + Periksa pembaruan + + + You're up to date. + Anda sudah menggunakan versi terbaru. + + + Couldn't check for updates. Try again later. + Tidak dapat memeriksa pembaruan. Coba lagi nanti. + + + &File + &Mengajukan + + + &Quit + Keluar + + + Profile + Profil + + + Detection + Deteksi + + + Regex Patterns + Pola Regex + + + Keywords + Kata Kunci + + + Password + Kata sandi + + + Statistics + Statistik + + + Session Redactions + Penyuntingan Sesi + + + Logs + Log + + + Settings + Pengaturan + + + Name: + Nama: + + + Port: + Pelabuhan: + + + Forward To + Teruskan Ke + + + API Key + Kunci API + + + Use AI model: + Gunakan model AI: + + + Confidence threshold: + Ambang batas keyakinan: + + + Add + Tambah + + + Remove + Hapus + + + Show API key + Tampilkan kunci API + + + Copy proxy URL + Salin URL proksi + + + Save + Menyimpan + + + Use AI model for PII detection + Gunakan model AI untuk deteksi PII + + + Case sensitive + Sensitif huruf besar/kecil + + + Require master password + Memerlukan kata sandi utama + + + Clear statistics + Hapus statistik + + + Clear + Bersihkan + + + Enable logging + Aktifkan pencatatan + + + Show sensitive information in logs + Tampilkan informasi sensitif di log + + + Open log file + Buka file log + + + Open folder + Buka folder + + + Delete all logs + Hapus semua log + + + Start on Boot + Mulai saat Boot + + + Language + Bahasa + + + System default + Default sistem + + + Master Password + Kata Sandi Utama + + + Unlock + Buka Kunci + + + Agent Redactor is locked + Agen Redaktur terkunci + + + Account number + Nomor rekening + + + Address + Alamat + + + Date + Tanggal + + + Email + Email + + + Person + Orang + + + Phone + Telepon + + + URL + URL + + + Secret + Rahasia + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Permintaan: %1 PII: %2 Regex: %3 Kata kunci: %4 + + + Engine is not running — retrying… + Mesin tidak hidup — mencoba lagi… + + + Delete + Menghapus + + + Validation Error + Kesalahan Validasi + + + Invalid regex syntax. + Sintaks regex tidak valid. + + + Case: Yes + Kasus: Ya + + + Case: No + Kasus: Tidak + + + Port must be between 1024 and 65535. + Port harus antara 1024 dan 65535. + + + Port %1 is already used by profile '%2'. + Port %1 sudah digunakan oleh profil '%2'. + + + Forward To URL must start with http:// or https://. + URL Teruskan Ke harus dimulai dengan http:// atau https://. + + + Confidence threshold must be between 0.0 and 1.0. + Ambang kepercayaan harus antara 0.0 dan 1.0. + + + Security Warning + Peringatan Keamanan + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Anda menggunakan URL upstream HTTP (tidak terenkripsi). Kunci API Anda akan dikirim dalam teks biasa melalui jaringan. + + + Error + Kesalahan + + + The engine rejected the profile. Check the engine log for details. + Mesin menolak profil tersebut. Periksa log mesin untuk detailnya. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + Mesin menolak profil baru. + + + Remove Profile + Hapus Profil + + + Are you sure? This operation is permanent. + Anda yakin? Operasi ini permanen. + + + Proxy URL copied to clipboard + URL proxy disalin ke papan klip + + + Wrong password. + Kata sandi salah. + + + Show sensitive information + Tampilkan informasi sensitif + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Pencatatan log sensitif menulis nilai mentah yang belum disunting (termasuk kunci API) ke log. Hanya aktifkan saat melakukan debug. + + + Enable logging first. + Aktifkan logging terlebih dahulu. + + + Delete all logs? + Hapus semua log? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Tindakan ini akan menghapus file log saat ini dan semua log sesi yang diarsipkan secara permanen. Tidak dapat dibatalkan. + + + Downloading AI model + Mengunduh model AI + + + Retry + Coba lagi + + + The PII detection model is downloading (%1%). + Model deteksi PII sedang diunduh (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Pengunduhan model gagal. Periksa koneksi internet Anda, lalu coba lagi. Deteksi PII tidak tersedia hingga pengunduhan selesai. + + + Are you sure you want to quit? + Anda yakin ingin keluar? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Jika Anda keluar, Agent Redactor tidak akan lagi memantau dan menyunting lalu lintas API. + + + Quit Agent Redactor? The engine keeps running in the background. + Keluar dari Agen Redaktur? Mesin terus berjalan di latar belakang. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Buka Agent Redactor + + + Start on Boot + Mulai saat Boot + + + Language + Bahasa + + + Quit + Keluar + + + + PasswordEnableDialog + + Enable password protection + Aktifkan perlindungan kata sandi + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Pilih kata sandi utama untuk Agen Redaktur. Ini melindungi kunci API Anda yang tersimpan di mesin ini dan tidak terkait dengan kata sandi login Anda. + + + New password: + Kata sandi baru: + + + Confirm password: + Konfirmasi kata sandi: + + + Password must not be empty. + Kata sandi wajib diisi. + + + Passwords do not match. + Kata sandi tidak cocok. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Buka kunci Agen Redaktur + + + Enter your master password to unlock. + Masukkan kata sandi utama Anda untuk membuka kunci. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_ig_NG.ts b/linux/gui/i18n/agentredactor_ig_NG.ts new file mode 100644 index 0000000..d9145c6 --- /dev/null +++ b/linux/gui/i18n/agentredactor_ig_NG.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Mmelite dị njikere ịwụnye + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + E budatala Agent Redactor %1. Malitegharịa ugbu a iji tinye mmelite ahụ. + + + Restart now + Malitegharịa ugbu a + + + Later + Mgbe e mesịrị + + + Check for updates + Lelee mmelite + + + You're up to date. + I nwere ụdị kacha ọhụrụ. + + + Couldn't check for updates. Try again later. + Enweghị ike ịlele mmelite. Gbalịa ọzọ mgbe e mesịrị. + + + &File + &Faịlụ + + + &Quit + Pụọ + + + Profile + Profaịlụ + + + Detection + Nchọpụta + + + Regex Patterns + Regex Patterns + + + Keywords + Keywords + + + Password + Password + + + Statistics + Statistics + + + Session Redactions + Mmebi emebi nke Session + + + Logs + Logs + + + Settings + Ntọala + + + Name: + Aha: + + + Port: + Port: + + + Forward To + Zigara + + + API Key + Akụkọ API + + + Use AI model: + Jiri ụdị AI: + + + Confidence threshold: + Oke ntụkwasị obi: + + + Add + Tinye + + + Remove + Wepụ + + + Show API key + Gosi igodo API + + + Copy proxy URL + Detuo URL proxy + + + Save + Chekwa + + + Use AI model for PII detection + Jiri ụdị AI maka nchọpụta PII + + + Case sensitive + Case sensitive + + + Require master password + Chọrọ paswọọdụ nna ukwu + + + Clear statistics + Kpochapụ ọnụ ọgụgụ + + + Clear + Hichapụ + + + Enable logging + Kwado ịde osisi + + + Show sensitive information in logs + Gosi ozi nwere mmetụta na ndekọ + + + Open log file + Mepe log file + + + Open folder + Mepe folder + + + Delete all logs + Hichapụ logs niile + + + Start on Boot + Bido na buut + + + Language + Asụsụ + + + System default + Nke ndabere nke usoro + + + Master Password + Master Password + + + Unlock + Mepee + + + Agent Redactor is locked + Akpọchiri onye ọrụ Redactor + + + Account number + Nọmba akaụntụ + + + Address + Adreesị + + + Date + Ụbọchị + + + Email + Email + + + Person + Mmadụ + + + Phone + Telefonu + + + URL + URL + + + Secret + Nzuzo + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Arịrịọ: %1 PII: %2 Regex: %3 Okwu: %4 + + + Engine is not running — retrying… + Igwe anaghị arụ ọrụ - na-anwale… + + + Delete + Hichapụ + + + Validation Error + Validation Error + + + Invalid regex syntax. + Regex syntax ezighị ezi. + + + Case: Yes + Ikpe: Ee + + + Case: No + Ikpe: Mba + + + Port must be between 1024 and 65535. + Port kwesịghị ịdị n'etiti 1024 na 65535. + + + Port %1 is already used by profile '%2'. + Port %1 ọ na-arụ ọrụ site na profaịlụ '%2'. + + + Forward To URL must start with http:// or https://. + Forward To URL kwesịghị ịbụ na mmalite http:// ma ọ bụ https://. + + + Confidence threshold must be between 0.0 and 1.0. + Threshold nke nkwenye kwesịghị ịdị n'etiti 0.0 na 1.0. + + + Security Warning + Ịdọ aka ná ntị nchebe + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Ị na-eji HTTP (agaghị akọtara) upstream URL. Akụkọ API gị ga-eziga dị ka plain text n'ụzọ netwọk. + + + Error + Mmehie + + + The engine rejected the profile. Check the engine log for details. + Injin ahụ jụrụ profaịlụ. Lelee ndekọ engine maka nkọwa. + + + Profile %1 + Profaịlụ %1 + + + The engine rejected the new profile. + Injin ahụ jụrụ profaịlụ ọhụrụ ahụ. + + + Remove Profile + Wepụ Profaịlụ + + + Are you sure? This operation is permanent. + Ị doro anya? Ọrụ a adịghị agbanwe agbanwe. + + + Proxy URL copied to clipboard + Eṅomiri URL proxy na klipbọọdụ + + + Wrong password. + Okwuntughe na-ezighi ezi. + + + Show sensitive information + Gosi ozi nwere mmetụta + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Ndekọ osisi nwere mmetụta na-ede ụkpụrụ arụghị arụ ọrụ (gụnyere igodo API) na ndekọ ahụ. Naanị mee ya ka ị na-emegharị ya. + + + Enable logging first. + Kwado ịde osisi na mbụ. + + + Delete all logs? + Hichapụ logs niile? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Nke a ga-ebibi log file ugbu a na logs niile e chekwara nke ọma. Enweghị ike ịmegharị ya. + + + Downloading AI model + Na-ebudata nlereanya AI + + + Retry + Gbalịa ọzọ + + + The PII detection model is downloading (%1%). + Ụdị nchọpụta PII na-ebudata (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Mbudata nlereanya ahụ dara ada. Lelee njikọ ịntanetị gị, wee gbalịa ọzọ. Nchọpụta PII adịghị ruo mgbe mbudata gasịrị. + + + Are you sure you want to quit? + Ị doro anya na ị chọrọ ịpụ? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Ị pụrụ, Agent Redactor agaghị azọ akwa ma ọ bụ emebi emebi API traffic. + + + Quit Agent Redactor? The engine keeps running in the background. + Kwụsị Agent Redactor? Igwe ahụ na-aga n'ihu n'azụ. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Mepe Agent Redactor + + + Start on Boot + Bido na buut + + + Language + Asụsụ + + + Quit + Pụọ + + + + PasswordEnableDialog + + Enable password protection + Kwado nchedo okwuntughe + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Họrọ paswọọdụ nna ukwu maka Agent Redactor. Ọ na-echekwa igodo API echekwara na igwe a enweghị njikọ na paswọọdụ nbanye gị. + + + New password: + Okwuntughe Ọhụrụ: + + + Confirm password: + Kwenye Na Okwuntughe: + + + Password must not be empty. + Okwuntughe agaghị abụ ihe efu. + + + Passwords do not match. + Okwuntughe adabaghị. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Mepee onye nrụpụta ihe + + + Enter your master password to unlock. + Tinye paswọọdụ nna ukwu gị ka imeghe. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_is.ts b/linux/gui/i18n/agentredactor_is.ts new file mode 100644 index 0000000..db750a5 --- /dev/null +++ b/linux/gui/i18n/agentredactor_is.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Uppfærsla tilbúin til uppsetningar + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 hefur verið sótt. Endurræstu núna til að virkja uppfærsluna. + + + Restart now + Endurræsa núna + + + Later + Seinna + + + Check for updates + Athuga uppfærslur + + + You're up to date. + Þú ert með nýjustu útgáfuna. + + + Couldn't check for updates. Try again later. + Ekki tókst að athuga uppfærslur. Reyndu aftur síðar. + + + &File + &Skrá + + + &Quit + Hætta + + + Profile + Prófíll + + + Detection + Uppgötvun + + + Regex Patterns + Regex mynstur + + + Keywords + Lykilorð + + + Password + Lykilorð + + + Statistics + Tölfræði + + + Session Redactions + Afskráningar lotu + + + Logs + Skrár + + + Settings + Stillingar + + + Name: + Nafn: + + + Port: + Höfn: + + + Forward To + Áframsenda til + + + API Key + API lykill + + + Use AI model: + Notaðu gervigreind líkan: + + + Confidence threshold: + Sjálfstraustsþröskuldur: + + + Add + Bæta við + + + Remove + Fjarlægja + + + Show API key + Sýna API lykil + + + Copy proxy URL + Afritaðu proxy-slóð + + + Save + Vista + + + Use AI model for PII detection + Notaðu gervigreind líkan fyrir PII uppgötvun + + + Case sensitive + Mismunandi á hástöfum/lágstöfum + + + Require master password + Krefjast aðallykilorðs + + + Clear statistics + Skýr tölfræði + + + Clear + Hreinsa + + + Enable logging + Virkja skráningu + + + Show sensitive information in logs + Sýna viðkvæmar upplýsingar í annálum + + + Open log file + Opna skráarskrá + + + Open folder + Opna möppu + + + Delete all logs + Eyða öllum skrám + + + Start on Boot + Byrjaðu á Boot + + + Language + Tungumál + + + System default + Sjálfgefið af kerfi + + + Master Password + Aðallykilorð + + + Unlock + Aflæsa + + + Agent Redactor is locked + Agent Redactor er læstur + + + Account number + Reikningsnúmer + + + Address + Heimilisfang + + + Date + Dagsetning + + + Email + Tölvupóstur + + + Person + Einstaklingur + + + Phone + Sími + + + URL + URL + + + Secret + Leyndarmál + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Beiðnir: %1 PII: %2 Regex: %3 Leitarorð: %4 + + + Engine is not running — retrying… + Vél er ekki í gangi — reynir aftur... + + + Delete + Eyða + + + Validation Error + Staðfestingarvilla + + + Invalid regex syntax. + Ógild regex setningafræði. + + + Case: Yes + Mál: Já + + + Case: No + Mál: Nei + + + Port must be between 1024 and 65535. + Port verður að vera á milli 1024 og 65535. + + + Port %1 is already used by profile '%2'. + Port %1 er þegar í notkun af sniðinu '%2'. + + + Forward To URL must start with http:// or https://. + Áframsendingar URL verður að byrja á http:// eða https://. + + + Confidence threshold must be between 0.0 and 1.0. + Traustmörk verða að vera á milli 0,0 og 1,0. + + + Security Warning + Öryggisviðvörun + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Þú ert að nota HTTP upstream URL (ódulkóðað). API lykillinn þinn verður sendur sem hreinn texti yfir netið. + + + Error + Villa + + + The engine rejected the profile. Check the engine log for details. + Vélin hafnaði prófílnum. Athugaðu vélarskrána til að fá nánari upplýsingar. + + + Profile %1 + Prófíll %1 + + + The engine rejected the new profile. + Vélin hafnaði nýja prófílnum. + + + Remove Profile + Fjarlægja prófíl + + + Are you sure? This operation is permanent. + Ertu viss? Þessi aðgerð er óafturkræf. + + + Proxy URL copied to clipboard + Umboðsslóð afrituð á klemmuspjald + + + Wrong password. + Rangt lykilorð. + + + Show sensitive information + Sýndu viðkvæmar upplýsingar + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Viðkvæm skógarhögg skrifar hrá, óútfærð gildi (þar á meðal API lykla) í annálinn. Virkjaðu það aðeins við villuleit. + + + Enable logging first. + Virkjaðu skráningu fyrst. + + + Delete all logs? + Eyða öllum skrám? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Þetta eyðir fyrirvaralaust núverandi skráarskrá og öllum vistuðu lotuskrám. Ekki er hægt að afturkalla þetta. + + + Downloading AI model + Sækir gervigreindarlíkan + + + Retry + Reyna aftur + + + The PII detection model is downloading (%1%). + PII uppgötvun líkanið er að hlaða niður (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Niðurhal líkansins mistókst. Athugaðu nettenginguna og reyndu aftur. PII-greining er ekki í boði fyrr en niðurhalið lýkur. + + + Are you sure you want to quit? + Ertu viss um að þú viljir hætta? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Ef þú hættir mun Agent Redactor ekki lengur fylgjast með og afskrá API umferð. + + + Quit Agent Redactor? The engine keeps running in the background. + Hætta í Agent Redactor? Vélin heldur áfram að keyra í bakgrunni. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Opna Agent Redactor + + + Start on Boot + Byrjaðu á Boot + + + Language + Tungumál + + + Quit + Hætta + + + + PasswordEnableDialog + + Enable password protection + Virkjaðu lykilorðsvörn + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Veldu aðallykilorð fyrir Agent Redactor. Það verndar geymda API lykla þína á þessari vél og er ótengt innskráningarlykilorðinu þínu. + + + New password: + Nýtt lykilorð: + + + Confirm password: + Staðfestu lykilorð: + + + Password must not be empty. + Lykilorð má ekki vera tómt. + + + Passwords do not match. + Lykilorð passa ekki saman. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Opnaðu Agent Redactor + + + Enter your master password to unlock. + Sláðu inn aðal lykilorðið þitt til að opna. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_it.ts b/linux/gui/i18n/agentredactor_it.ts new file mode 100644 index 0000000..f18ac4a --- /dev/null +++ b/linux/gui/i18n/agentredactor_it.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Aggiornamento pronto per l'installazione + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 è stato scaricato. Riavvia ora per applicare l'aggiornamento. + + + Restart now + Riavvia ora + + + Later + Più tardi + + + Check for updates + Cerca aggiornamenti + + + You're up to date. + L'app è aggiornata. + + + Couldn't check for updates. Try again later. + Impossibile cercare aggiornamenti. Riprova più tardi. + + + &File + &File + + + &Quit + Esci + + + Profile + Profilo + + + Detection + Rilevamento + + + Regex Patterns + Pattern regex + + + Keywords + Parole chiave + + + Password + Password + + + Statistics + Statistiche + + + Session Redactions + Redazioni sessione + + + Logs + Log + + + Settings + Impostazioni + + + Name: + Nome: + + + Port: + Porta: + + + Forward To + Inoltra a + + + API Key + Chiave API + + + Use AI model: + Utilizza il modello AI: + + + Confidence threshold: + Soglia di confidenza: + + + Add + Aggiungi + + + Remove + Rimuovi + + + Show API key + Mostra la chiave API + + + Copy proxy URL + Copia l'URL proxy + + + Save + Salva + + + Use AI model for PII detection + Utilizza il modello AI per il rilevamento delle PII + + + Case sensitive + Maiuscole/minuscole sensibili + + + Require master password + Richiedi la password principale + + + Clear statistics + Statistiche chiare + + + Clear + Cancella + + + Enable logging + Abilita la registrazione + + + Show sensitive information in logs + Mostra informazioni sensibili nei log + + + Open log file + Apri file di log + + + Open folder + Apri cartella + + + Delete all logs + Elimina tutti i log + + + Start on Boot + Inizia all'avvio + + + Language + Lingua + + + System default + Predefinito di sistema + + + Master Password + Password principale + + + Unlock + Sblocca + + + Agent Redactor is locked + L'Agent Redactor è bloccato + + + Account number + Numero di conto + + + Address + Indirizzo + + + Date + Data + + + Email + E-mail + + + Person + Persona + + + Phone + Telefono + + + URL + URL + + + Secret + Segreto + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Richieste: %1 PII: %2 Regex: %3 Parole chiave: %4 + + + Engine is not running — retrying… + Il motore non funziona: nuovo tentativo... + + + Delete + Eliminare + + + Validation Error + Errore di convalida + + + Invalid regex syntax. + Sintassi regex non valida. + + + Case: Yes + Caso: sì + + + Case: No + Caso: no + + + Port must be between 1024 and 65535. + La porta deve essere compresa tra 1024 e 65535. + + + Port %1 is already used by profile '%2'. + La porta %1 è già utilizzata dal profilo '%2'. + + + Forward To URL must start with http:// or https://. + L'URL di inoltro deve iniziare con http:// o https://. + + + Confidence threshold must be between 0.0 and 1.0. + La soglia di confidenza deve essere compresa tra 0,0 e 1,0. + + + Security Warning + Avviso di sicurezza + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Stai utilizzando un URL upstream HTTP (non crittografato). La tua chiave API verrà inviata in chiaro sulla rete. + + + Error + Errore + + + The engine rejected the profile. Check the engine log for details. + Il motore ha rifiutato il profilo. Controlla il registro del motore per i dettagli. + + + Profile %1 + Profilo %1 + + + The engine rejected the new profile. + Il motore ha rifiutato il nuovo profilo. + + + Remove Profile + Rimuovi profilo + + + Are you sure? This operation is permanent. + Sei sicuro? Questa operazione è irreversibile. + + + Proxy URL copied to clipboard + URL proxy copiato negli appunti + + + Wrong password. + Password errata. + + + Show sensitive information + Mostra informazioni sensibili + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + La registrazione sensibile scrive valori grezzi e non oscurati (incluse le chiavi API) nel log. Abilitalo solo durante il debug. + + + Enable logging first. + Abilita prima la registrazione. + + + Delete all logs? + Eliminare tutti i log? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Questo eliminerà definitivamente il file di log attuale e tutti i log di sessione archiviati. Non può essere annullato. + + + Downloading AI model + Download del modello di IA in corso + + + Retry + Riprova + + + The PII detection model is downloading (%1%). + È in corso il download del modello di rilevamento PII (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Download del modello non riuscito. Controlla la connessione a Internet e riprova. Il rilevamento delle PII non è disponibile fino al completamento del download. + + + Are you sure you want to quit? + Sei sicuro di voler uscire? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Se esci, Agent Redactor non monitorerà e non redigerà più il traffico API. + + + Quit Agent Redactor? The engine keeps running in the background. + Uscire da Agent Redactor? Il motore continua a funzionare in background. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Apri Agent Redactor + + + Start on Boot + Inizia all'avvio + + + Language + Lingua + + + Quit + Esci + + + + PasswordEnableDialog + + Enable password protection + Abilita la protezione tramite password + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Scegli una password principale per Agent Redactor. Protegge le chiavi API archiviate su questa macchina e non è correlata alla password di accesso. + + + New password: + Nuova password: + + + Confirm password: + Conferma password: + + + Password must not be empty. + La password non deve essere vuota. + + + Passwords do not match. + Le password non corrispondono. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Sblocca Agent Redactor + + + Enter your master password to unlock. + Inserisci la tua password principale per sbloccare. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_ja.ts b/linux/gui/i18n/agentredactor_ja.ts new file mode 100644 index 0000000..6eec133 --- /dev/null +++ b/linux/gui/i18n/agentredactor_ja.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + 更新をインストールできます + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 がダウンロードされました。今すぐ再起動して更新を適用してください。 + + + Restart now + 今すぐ再起動 + + + Later + 後で + + + Check for updates + 更新を確認 + + + You're up to date. + 最新の状態です。 + + + Couldn't check for updates. Try again later. + 更新を確認できませんでした。後でもう一度お試しください。 + + + &File + &ファイル + + + &Quit + 終了 + + + Profile + プロフィール + + + Detection + 検出 + + + Regex Patterns + 正規表現パターン + + + Keywords + キーワード + + + Password + パスワード + + + Statistics + 統計 + + + Session Redactions + セッション編集 + + + Logs + ログ + + + Settings + 設定 + + + Name: + 名前: + + + Port: + ポート: + + + Forward To + 転送先 + + + API Key + API キー + + + Use AI model: + AI モデルを使用します。 + + + Confidence threshold: + 信頼度のしきい値: + + + Add + 追加 + + + Remove + 削除 + + + Show API key + APIキーを表示 + + + Copy proxy URL + プロキシ URL をコピーする + + + Save + 保存 + + + Use AI model for PII detection + PII 検出に AI モデルを使用する + + + Case sensitive + 大文字小文字を区別 + + + Require master password + マスターパスワードを要求する + + + Clear statistics + 統計をクリアする + + + Clear + クリア + + + Enable logging + ロギングを有効にする + + + Show sensitive information in logs + 機密情報をログに表示する + + + Open log file + ログファイルを開く + + + Open folder + フォルダーを開く + + + Delete all logs + すべてのログを削除 + + + Start on Boot + ブート時に開始 + + + Language + 言語 + + + System default + システム既定 + + + Master Password + マスター パスワード + + + Unlock + ロック解除 + + + Agent Redactor is locked + エージェント リダクターはロックされています + + + Account number + 口座番号 + + + Address + 住所 + + + Date + 日付 + + + Email + メール + + + Person + 人物 + + + Phone + 電話 + + + URL + URL + + + Secret + 秘密 + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + リクエスト: %1 PII: %2 正規表現: %3 キーワード: %4 + + + Engine is not running — retrying… + エンジンが動作していません - 再試行しています… + + + Delete + 消去 + + + Validation Error + 検証エラー + + + Invalid regex syntax. + 正規表現の構文が無効です。 + + + Case: Yes + ケース: はい + + + Case: No + ケース: いいえ + + + Port must be between 1024 and 65535. + ポートは 1024 から 65535 の間である必要があります。 + + + Port %1 is already used by profile '%2'. + ポート %1 はプロファイル「%2」によって使用されています。 + + + Forward To URL must start with http:// or https://. + 転送先 URL は http:// または https:// で始まる必要があります。 + + + Confidence threshold must be between 0.0 and 1.0. + 信頼度しきい値は 0.0 から 1.0 の間である必要があります。 + + + Security Warning + セキュリティ警告 + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + HTTP(暗号化されていない)アップストリーム URL を使用しています。API キーはネットワークを介して平文で送信されます。 + + + Error + エラー + + + The engine rejected the profile. Check the engine log for details. + エンジンがプロファイルを拒否しました。詳細については、エンジン ログを確認してください。 + + + Profile %1 + プロファイル %1 + + + The engine rejected the new profile. + エンジンは新しいプロファイルを拒否しました。 + + + Remove Profile + プロファイルを削除 + + + Are you sure? This operation is permanent. + よろしいですか?この操作は元に戻せません。 + + + Proxy URL copied to clipboard + プロキシ URL がクリップボードにコピーされました + + + Wrong password. + パスワードが間違っています。 + + + Show sensitive information + 機密情報を表示する + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + 機密ログでは、編集されていない生の値 (API キーを含む) がログに書き込まれます。デバッグ中にのみ有効にしてください。 + + + Enable logging first. + まずログを有効にします。 + + + Delete all logs? + すべてのログを削除しますか? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + 現在のログファイルとすべてのアーカイブ済みセッション ログが完全に削除されます。この操作は元に戻せません。 + + + Downloading AI model + AI モデルをダウンロードしています + + + Retry + 再試行 + + + The PII detection model is downloading (%1%). + PII 検出モデルをダウンロード中です (%1%)。 + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + モデルのダウンロードに失敗しました。インターネット接続を確認してから再試行してください。ダウンロードが完了するまで PII 検出は利用できません。 + + + Are you sure you want to quit? + 終了してもよろしいですか? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + 終了すると、Agent Redactor は API トラフィックの監視と編集を行わなくなります。 + + + Quit Agent Redactor? The engine keeps running in the background. + エージェント・リダクターを辞めますか?エンジンはバックグラウンドで動作し続けます。 + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Agent Redactor を開く + + + Start on Boot + ブート時に開始 + + + Language + 言語 + + + Quit + 終了 + + + + PasswordEnableDialog + + Enable password protection + パスワード保護を有効にする + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Agent Redactor のマスター パスワードを選択します。これは、このマシンに保存されている API キーを保護し、ログイン パスワードとは無関係です。 + + + New password: + 新しいパスワード: + + + Confirm password: + パスワードを認証する: + + + Password must not be empty. + パスワードを空にすることはできません。 + + + Passwords do not match. + パスワードが一致しません。 + + + + PasswordUnlockDialog + + Unlock Agent Redactor + エージェント リダクターのロックを解除する + + + Enter your master password to unlock. + マスターパスワードを入力してロックを解除します。 + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_ka.ts b/linux/gui/i18n/agentredactor_ka.ts new file mode 100644 index 0000000..0d5dcfd --- /dev/null +++ b/linux/gui/i18n/agentredactor_ka.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + განახლება მზადაა დასაყენებლად + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 ჩამოტვირთულია. გადატვირთეთ ახლა განახლების გამოსაყენებლად. + + + Restart now + გადატვირთვა ახლა + + + Later + მოგვიანებით + + + Check for updates + განახლებების შემოწმება + + + You're up to date. + თქვენ გაქვთ უახლესი ვერსია. + + + Couldn't check for updates. Try again later. + განახლებების შემოწმება ვერ მოხერხდა. სცადეთ მოგვიანებით. + + + &File + &ფაილი + + + &Quit + გამოსვლა + + + Profile + პროფილი + + + Detection + გამოვლენა + + + Regex Patterns + Regex ნიმუშები + + + Keywords + საკვანძო სიტყვები + + + Password + პაროლი + + + Statistics + სტატისტიკა + + + Session Redactions + სეანსის წაშლები + + + Logs + ჟურნალები + + + Settings + პარამეტრები + + + Name: + სახელი: + + + Port: + პორტი: + + + Forward To + გადამისამართება + + + API Key + API გასაღები + + + Use AI model: + გამოიყენეთ AI მოდელი: + + + Confidence threshold: + ნდობის ზღვარი: + + + Add + დამატება + + + Remove + წაშლა + + + Show API key + API გასაღების ჩვენება + + + Copy proxy URL + დააკოპირეთ პროქსის URL + + + Save + შენახვა + + + Use AI model for PII detection + გამოიყენეთ AI მოდელი PII გამოვლენისთვის + + + Case sensitive + რეგისტრის გათვალისწინება + + + Require master password + მოითხოვეთ ძირითადი პაროლი + + + Clear statistics + სტატისტიკის გასუფთავება + + + Clear + გასუფთავება + + + Enable logging + ჩართეთ ჟურნალი + + + Show sensitive information in logs + სენსიტიური ინფორმაციის ჩვენება ჟურნალებში + + + Open log file + ჟურნალის ფაილის გახსნა + + + Open folder + საქაღალდის გახსნა + + + Delete all logs + ყველა ჟურნალის წაშლა + + + Start on Boot + დაწყება ჩატვირთვით + + + Language + ენა + + + System default + სისტემის ნაგულისხმევი + + + Master Password + მთავარი პაროლი + + + Unlock + გახსნა + + + Agent Redactor is locked + აგენტი რედაქტორი ჩაკეტილია + + + Account number + ანგარიშის ნომერი + + + Address + მისამართი + + + Date + თარიღი + + + Email + ელ. ფოსტა + + + Person + პირი + + + Phone + ტელეფონი + + + URL + URL + + + Secret + საიდუმლო + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + მოთხოვნები: %1 PII: %2 რეგექსი: %3 საკვანძო სიტყვები: %4 + + + Engine is not running — retrying… + ძრავა არ მუშაობს — ხელახლა ცდა… + + + Delete + წაშლა + + + Validation Error + დადასტურების შეცდომა + + + Invalid regex syntax. + არასწორი regex სინტაქსი. + + + Case: Yes + საქმე: დიახ + + + Case: No + საქმე: არა + + + Port must be between 1024 and 65535. + პორტი უნდა იყოს 1024-65535 დიაპაზონში. + + + Port %1 is already used by profile '%2'. + პორტი %1 უკვე გამოიყენება პროფილის მიერ '%2'. + + + Forward To URL must start with http:// or https://. + გადამისამართების URL უნდა იწყებოდეს http:// ან https://-ით. + + + Confidence threshold must be between 0.0 and 1.0. + სანდოობის ზღვარი უნდა იყოს 0,0-1,0 დიაპაზონში. + + + Security Warning + უსაფრთხოების გაფრთხილება + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + თქვენ იყენებთ HTTP upstream URL-ს (დაშიფრული არ არის). თქვენი API გასაღები ქსელში უბრალო ტექსტის სახით გაიგზავნება. + + + Error + შეცდომა + + + The engine rejected the profile. Check the engine log for details. + ძრავმა უარყო პროფილი. შეამოწმეთ ძრავის ჟურნალი დეტალებისთვის. + + + Profile %1 + პროფილი %1 + + + The engine rejected the new profile. + ძრავმა უარყო ახალი პროფილი. + + + Remove Profile + პროფილის წაშლა + + + Are you sure? This operation is permanent. + დარწმუნებული ხართ? ეს ოპერაცია შეუქცევადია. + + + Proxy URL copied to clipboard + პროქსის URL კოპირებულია ბუფერში + + + Wrong password. + არასწორი პაროლი. + + + Show sensitive information + სენსიტიური ინფორმაციის ჩვენება + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + სენსიტიური ჟურნალი წერს დაუმუშავებელ, არარედაქტირებულ მნიშვნელობებს (API კლავიშების ჩათვლით) ჟურნალში. ჩართეთ ის მხოლოდ გამართვისას. + + + Enable logging first. + ჯერ ჩართეთ ჟურნალი. + + + Delete all logs? + ყველა ჟურნალის წაშლა? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + ეს მიმდინარე ჟურნალის ფაილს და ყველა არქივირებულ სეანსის ჟურნალს სამუდამოდ წაშლის. ეს ვერ მოხერხდება. + + + Downloading AI model + AI მოდელის ჩამოტვირთვა + + + Retry + ხელახლა ცდა + + + The PII detection model is downloading (%1%). + PII გამოვლენის მოდელი იტვირთება (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + მოდელის ჩამოტვირთვა ვერ მოხერხდა. შეამოწმეთ ინტერნეტ კავშირი და სცადეთ ხელახლა. PII-ის ამოცნობა მიუწვდომელია ჩამოტვირთვის დასრულებამდე. + + + Are you sure you want to quit? + დარწმუნებული ხართ, რომ გსურთ გამოსვლა? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + თუ გამოხვალთ, Agent Redactor აღარ დააკვირდება და არ წაშლის API ტრაფიკს. + + + Quit Agent Redactor? The engine keeps running in the background. + დატოვოთ აგენტი რედაქტორი? ძრავა აგრძელებს მუშაობას ფონზე. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Agent Redactor-ის გახსნა + + + Start on Boot + დაწყება ჩატვირთვით + + + Language + ენა + + + Quit + გამოსვლა + + + + PasswordEnableDialog + + Enable password protection + პაროლის დაცვის ჩართვა + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + აირჩიეთ მთავარი პაროლი Agent Redactor-ისთვის. ის იცავს თქვენს შენახულ API გასაღებებს ამ მოწყობილობაზე და არ არის დაკავშირებული თქვენს შესვლის პაროლთან. + + + New password: + ახალი პაროლი: + + + Confirm password: + პაროლის დადასტურება: + + + Password must not be empty. + პაროლი არ უნდა იყოს ცარიელი. + + + Passwords do not match. + პაროლები არ ემთხვევა. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + განბლოკეთ აგენტი რედაქტორი + + + Enter your master password to unlock. + შეიყვანეთ თქვენი ძირითადი პაროლი განბლოკვისთვის. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_kk.ts b/linux/gui/i18n/agentredactor_kk.ts new file mode 100644 index 0000000..9983502 --- /dev/null +++ b/linux/gui/i18n/agentredactor_kk.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Жаңарту орнатуға дайын + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 жүктелді. Жаңартуды қолдану үшін қазір қайта іске қосыңыз. + + + Restart now + Қазір қайта іске қосу + + + Later + Кейінірек + + + Check for updates + Жаңартуларды тексеру + + + You're up to date. + Сізде соңғы нұсқа бар. + + + Couldn't check for updates. Try again later. + Жаңартуларды тексеру мүмкін болмады. Кейінірек қайталап көріңіз. + + + &File + &Файл + + + &Quit + Шығу + + + Profile + Профиль + + + Detection + Анықтау + + + Regex Patterns + Regex үлгілері + + + Keywords + Негізгі сөздер + + + Password + Құпия сөз + + + Statistics + Статистика + + + Session Redactions + Сеанс түзетулері + + + Logs + Журналдар + + + Settings + Параметрлер + + + Name: + Аты: + + + Port: + Порт: + + + Forward To + Алға жіберу + + + API Key + API кілті + + + Use AI model: + AI үлгісін қолданыңыз: + + + Confidence threshold: + Сенімділік шегі: + + + Add + қосу + + + Remove + Жою + + + Show API key + API кілтін көрсету + + + Copy proxy URL + Прокси URL мекенжайын көшіріңіз + + + Save + Сақтау + + + Use AI model for PII detection + PII анықтау үшін AI үлгісін пайдаланыңыз + + + Case sensitive + Регистрге сезімтал + + + Require master password + Негізгі құпия сөзді талап ету + + + Clear statistics + Таза статистика + + + Clear + Таза + + + Enable logging + Тіркеуді қосу + + + Show sensitive information in logs + Журналдарда құпия ақпаратты көрсету + + + Open log file + Журнал файлын ашыңыз + + + Open folder + Қалтаны ашу + + + Delete all logs + Барлық журналдарды жойыңыз + + + Start on Boot + Жүктеуде бастаңыз + + + Language + Тіл + + + System default + Жүйе әдепкі + + + Master Password + Негізгі құпия сөз + + + Unlock + Құлыпты ашу + + + Agent Redactor is locked + Agent Redactor құлыпталған + + + Account number + Есептік жазба нөмірі + + + Address + Мекенжай + + + Date + Күн + + + Email + Электрондық пошта + + + Person + Адам + + + Phone + Телефон + + + URL + URL + + + Secret + Құпия + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Сұраныс: %1 PII: %2 Регекс: %3 Түйін сөздер: %4 + + + Engine is not running — retrying… + Қозғалтқыш жұмыс істемейді — әрекет қайталануда… + + + Delete + Жою + + + Validation Error + Тексеру қатесі + + + Invalid regex syntax. + Жарамсыз регекс синтаксисі. + + + Case: Yes + Іс: Иә + + + Case: No + Іс: Жоқ + + + Port must be between 1024 and 65535. + Порт 1024 пен 65535 арасында болуы керек. + + + Port %1 is already used by profile '%2'. + %1 порты "%2" профилінде бұрыннан пайдаланылады. + + + Forward To URL must start with http:// or https://. + URL мекенжайына бағыттау http:// немесе https:// арқылы басталуы керек. + + + Confidence threshold must be between 0.0 and 1.0. + Сенімділік шегі 0,0 және 1,0 арасында болуы керек. + + + Security Warning + Қауіпсіздік туралы ескерту + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Сіз HTTP (шифрланбаған) жоғары ағын URL мекенжайын пайдаланып жатырсыз. Сіздің API кілтіңіз желі арқылы ашық мәтінде жіберіледі. + + + Error + Қате + + + The engine rejected the profile. Check the engine log for details. + Қозғалтқыш профильді қабылдамады. Мәліметтер алу үшін қозғалтқыш журналын тексеріңіз. + + + Profile %1 + Профиль %1 + + + The engine rejected the new profile. + Қозғалтқыш жаңа профильді қабылдамады. + + + Remove Profile + Профильді жою + + + Are you sure? This operation is permanent. + Сіз сенімдісіз бе? Бұл операция тұрақты. + + + Proxy URL copied to clipboard + Прокси URL алмасу буферіне көшірілді + + + Wrong password. + Құпия сөз қате. + + + Show sensitive information + Құпия ақпаратты көрсету + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Сезімтал журнал журналға өңделмеген, өңделмеген мәндерді (соның ішінде API кілттерін) жазады. Оны түзету кезінде ғана қосыңыз. + + + Enable logging first. + Алдымен тіркеуді қосыңыз. + + + Delete all logs? + Барлық журналдар жойылсын ба? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Бұл ағымдағы журнал файлын және барлық мұрағатталған сеанс журналдарын біржола жояды. Бұл әрекетті қайтару мүмкін емес. + + + Downloading AI model + AI моделі жүктелуде + + + Retry + Қайталау + + + The PII detection model is downloading (%1%). + PII анықтау үлгісі жүктелуде (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Модельді жүктеу сәтсіз аяқталды. Интернет байланысын тексеріп, қайталап көріңіз. Жүктеу аяқталғанға дейін PII анықтау қолжетімсіз. + + + Are you sure you want to quit? + Шығыңыз келетініне сенімдісіз бе? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Шығарсаңыз, Agent Redactor API трафигін бақылап, өңдемейді. + + + Quit Agent Redactor? The engine keeps running in the background. + Agent Redactor-дан шығу керек пе? Қозғалтқыш фондық режимде жұмыс істей береді. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Агент редакторын ашыңыз + + + Start on Boot + Жүктеуде бастаңыз + + + Language + Тіл + + + Quit + Шығу + + + + PasswordEnableDialog + + Enable password protection + Құпия сөзбен қорғауды қосыңыз + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Agent Redactor үшін негізгі құпия сөзді таңдаңыз. Ол осы құрылғыда сақталған API кілттеріңізді қорғайды және кіру құпия сөзіңізге қатысы жоқ. + + + New password: + Жаңа құпия сөз: + + + Confirm password: + Құпия сөзді Растау: + + + Password must not be empty. + Құпия сөз бос болмауы керек. + + + Passwords do not match. + Құпия сөздер сәйкес келмейді. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Агент редакторының құлпын ашыңыз + + + Enter your master password to unlock. + Құлыпты ашу үшін негізгі құпия сөзіңізді енгізіңіз. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_ko.ts b/linux/gui/i18n/agentredactor_ko.ts new file mode 100644 index 0000000..90709a4 --- /dev/null +++ b/linux/gui/i18n/agentredactor_ko.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + 업데이트 설치 준비 완료 + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1이(가) 다운로드되었습니다. 지금 다시 시작하여 업데이트를 적용하세요. + + + Restart now + 지금 다시 시작 + + + Later + 나중에 + + + Check for updates + 업데이트 확인 + + + You're up to date. + 최신 버전입니다. + + + Couldn't check for updates. Try again later. + 업데이트를 확인할 수 없습니다. 나중에 다시 시도하세요. + + + &File + &파일 + + + &Quit + 종료 + + + Profile + 윤곽 + + + Detection + 발각 + + + Regex Patterns + 정규식 패턴 + + + Keywords + 키워드 + + + Password + 암호 + + + Statistics + 통계 + + + Session Redactions + 세션 삭제 + + + Logs + 로그 + + + Settings + 설정 + + + Name: + 이름: + + + Port: + 포트: + + + Forward To + 전달 대상 + + + API Key + API 키 + + + Use AI model: + AI 모델 사용: + + + Confidence threshold: + 신뢰도 임계값: + + + Add + 추가 + + + Remove + 제거 + + + Show API key + API 키 표시 + + + Copy proxy URL + 프록시 URL 복사 + + + Save + 구하다 + + + Use AI model for PII detection + PII 감지를 위해 AI 모델 사용 + + + Case sensitive + 대소문자 구분 + + + Require master password + 마스터 비밀번호 필요 + + + Clear statistics + 통계 지우기 + + + Clear + 지우기 + + + Enable logging + 로깅 활성화 + + + Show sensitive information in logs + 로그에 민감한 정보 표시 + + + Open log file + 로그 파일 열기 + + + Open folder + 폴더 열기 + + + Delete all logs + 모든 로그 삭제 + + + Start on Boot + 부팅 시 시작 + + + Language + 언어 + + + System default + 시스템 기본값 + + + Master Password + 마스터 암호 + + + Unlock + 잠금 해제 + + + Agent Redactor is locked + 에이전트 편집자가 잠겨 있습니다. + + + Account number + 계좌 번호 + + + Address + 주소 + + + Date + 날짜 + + + Email + 이메일 + + + Person + 사람 + + + Phone + 전화 + + + URL + URL + + + Secret + 비밀 + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + 요청: %1 PII: %2 정규식: %3 키워드: %4 + + + Engine is not running — retrying… + 엔진이 실행되고 있지 않습니다. 다시 시도하는 중입니다… + + + Delete + 삭제 + + + Validation Error + 유효성 검사 오류 + + + Invalid regex syntax. + 정규식 구문이 잘못되었습니다. + + + Case: Yes + 케이스: 예 + + + Case: No + 케이스: 아니오 + + + Port must be between 1024 and 65535. + 포트는 1024에서 65535 사이여야 합니다. + + + Port %1 is already used by profile '%2'. + 포트 %1은(는) '%2' 프로필에서 이미 사용 중입니다. + + + Forward To URL must start with http:// or https://. + 전달 대상 URL은 http:// 또는 https://로 시작해야 합니다. + + + Confidence threshold must be between 0.0 and 1.0. + 신뢰도 임계값은 0.0에서 1.0 사이여야 합니다. + + + Security Warning + 보안 경고 + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + HTTP(암호화되지 않은) 업스트림 URL을 사용하고 있습니다. API 키가 네트워크를 통해 일반 텍스트로 전송됩니다. + + + Error + 오류 + + + The engine rejected the profile. Check the engine log for details. + 엔진이 프로필을 거부했습니다. 자세한 내용은 엔진 로그를 확인하세요. + + + Profile %1 + 프로필%1 + + + The engine rejected the new profile. + 엔진이 새 프로필을 거부했습니다. + + + Remove Profile + 프로필 제거 + + + Are you sure? This operation is permanent. + 계속하시겠습니까? 이 작업은 되돌릴 수 없습니다. + + + Proxy URL copied to clipboard + 프록시 URL이 클립보드에 복사되었습니다. + + + Wrong password. + 비밀번호가 잘못되었습니다. + + + Show sensitive information + 민감한 정보 표시 + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + 민감한 로깅은 수정되지 않은 원시 값(API 키 포함)을 로그에 기록합니다. 디버깅하는 동안에만 활성화하십시오. + + + Enable logging first. + 먼저 로깅을 활성화하세요. + + + Delete all logs? + 모든 로그를 삭제하시겠습니까? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + 현재 로그 파일과 모든 보관된 세션 로그가 영구적으로 삭제됩니다. 이 작업은 취소할 수 없습니다. + + + Downloading AI model + AI 모델 다운로드 중 + + + Retry + 다시 시도 + + + The PII detection model is downloading (%1%). + PII 탐지 모델을 다운로드 중입니다(%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + 모델 다운로드에 실패했습니다. 인터넷 연결을 확인한 후 다시 시도하세요. 다운로드가 완료될 때까지 PII 감지를 사용할 수 없습니다. + + + Are you sure you want to quit? + 종료하시겠습니까? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + 종료하면 Agent Redactor가 API 트래픽을 더 이상 모니터링하고 삭제하지 않습니다. + + + Quit Agent Redactor? The engine keeps running in the background. + Agent Redactor를 종료하시겠습니까? 엔진은 백그라운드에서 계속 실행됩니다. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Agent Redactor 열기 + + + Start on Boot + 부팅 시 시작 + + + Language + 언어 + + + Quit + 종료 + + + + PasswordEnableDialog + + Enable password protection + 비밀번호 보호 활성화 + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Agent Redactor의 마스터 비밀번호를 선택하세요. 이는 이 시스템에 저장된 API 키를 보호하며 로그인 비밀번호와 관련이 없습니다. + + + New password: + 새 비밀번호: + + + Confirm password: + 비밀번호 확인: + + + Password must not be empty. + 비밀번호는 비워둘 수 없습니다. + + + Passwords do not match. + 비밀번호가 일치하지 않습니다. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + 에이전트 편집자 잠금 해제 + + + Enter your master password to unlock. + 잠금을 해제하려면 마스터 비밀번호를 입력하세요. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_lb.ts b/linux/gui/i18n/agentredactor_lb.ts new file mode 100644 index 0000000..d5c9329 --- /dev/null +++ b/linux/gui/i18n/agentredactor_lb.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Update prett fir d'Installatioun + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 gouf erofgelueden. Start elo nei fir den Update unzewenden. + + + Restart now + Elo nei starten + + + Later + Méi spéit + + + Check for updates + No Updates sichen + + + You're up to date. + Dir sidd um neiste Stand. + + + Couldn't check for updates. Try again later. + Et konnt net no Updates gesicht ginn. Probéiert et méi spéit nach eng Kéier. + + + &File + & Datei + + + &Quit + Zoumaachen + + + Profile + Profil + + + Detection + Detektioun + + + Regex Patterns + Regex-Muster + + + Keywords + Schlësselwierder + + + Password + Passwuert + + + Statistics + Statistiken + + + Session Redactions + Sitzungsschwäerzungen + + + Logs + Logfichieren + + + Settings + Astellungen + + + Name: + Numm: + + + Port: + Port: + + + Forward To + Viruleeden un + + + API Key + API-Schlëssel + + + Use AI model: + Benotzt AI Modell: + + + Confidence threshold: + Vertrauensgrenz: + + + Add + Dobäisetzen + + + Remove + Ewechhuelen + + + Show API key + Show API Schlëssel + + + Copy proxy URL + Proxy URL kopéieren + + + Save + Spueren + + + Use AI model for PII detection + Benotzt AI Modell fir PII Detektioun + + + Case sensitive + Grouss-/Klengschreiwung berécksiichtegen + + + Require master password + Verlaangt Meeschtesch Passwuert + + + Clear statistics + Kloer Statistiken + + + Clear + Eidel + + + Enable logging + Logbuch aktivéieren + + + Show sensitive information in logs + Weist sensibel Informatioun a Logbicher + + + Open log file + Logfichier opmaachen + + + Open folder + Dossier opmaachen + + + Delete all logs + All Logfichieren läschen + + + Start on Boot + Start op Boot + + + Language + Sprooch + + + System default + Systemstandard + + + Master Password + Haaptpasswuert + + + Unlock + Entspären + + + Agent Redactor is locked + Agent Redactor ass gespaart + + + Account number + Kontonummer + + + Address + Adress + + + Date + Datum + + + Email + E-Mail + + + Person + Persoun + + + Phone + Telefon + + + URL + URL + + + Secret + Geheimnis + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Ufroen: %1 PII: %2 Regex: %3 Schlësselwieder: %4 + + + Engine is not running — retrying… + De Motor leeft net - probéiert nach eng Kéier ... + + + Delete + Läschen + + + Validation Error + Validatiounsfehler + + + Invalid regex syntax. + Ongülteg Regex-Syntax. + + + Case: Yes + Fall: Jo + + + Case: No + Fall: Nee + + + Port must be between 1024 and 65535. + Port muss tëschent 1024 an 65535 leien. + + + Port %1 is already used by profile '%2'. + Port %1 gëtt scho vum Profil '%2' benotzt. + + + Forward To URL must start with http:// or https://. + Weiderleedungs-URL muss mat http:// oder https:// ugoen. + + + Confidence threshold must be between 0.0 and 1.0. + Konfidenzschwelle muss tëschent 0,0 an 1,0 leien. + + + Security Warning + Sécherheetswarnung + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Dir benotzt eng HTTP-upstream-URL (net verschlësselt). Äre API-Schlëssel gëtt als Klartext iwwer d'Netz geschéckt. + + + Error + Fehler + + + The engine rejected the profile. Check the engine log for details. + De Motor huet de Profil refuséiert. Kuckt de Motorlog fir Detailer. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + De Motor huet den neie Profil refuséiert. + + + Remove Profile + Profil ewechhuelen + + + Are you sure? This operation is permanent. + Sidd Dir sécher? Dës Operatioun kann net réckgängeg gemaach ginn. + + + Proxy URL copied to clipboard + Proxy URL kopéiert op Clipboard + + + Wrong password. + Falsch Passwuert. + + + Show sensitive information + Weist sensibel Informatiounen + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Sensibel Logging schreift rau, onredaktéiert Wäerter (abegraff API Schlësselen) an de Logbuch. Aktivéiert et nëmmen beim Debugging. + + + Enable logging first. + Aktivéiert als éischt de Logbicher. + + + Delete all logs? + All Logfichieren läschen? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Dëst läscht de aktuelle Logfichier an all archivéiert Sitzungslogfichieren definitiv. Kann net réckgängeg gemaach ginn. + + + Downloading AI model + AI-Modell gëtt erofgelueden + + + Retry + Nach eng Kéier probéieren + + + The PII detection model is downloading (%1%). + De PII Detektiounsmodell gëtt erofgelueden (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Den Download vum Modell ass feelgeschloe. Iwwerpréift är Internetverbindung a probéiert et nach eng Kéier. D'PII-Erkennung ass net verfügbar, bis den Download ofgeschloss ass. + + + Are you sure you want to quit? + Sidd Dir sécher, datt Dir zoumaache wëllt? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Wann Dir zoumaacht, iwwerwaacht a schwäerzt Agent Redactor kee API-Traffic méi. + + + Quit Agent Redactor? The engine keeps running in the background. + Agent Redactor ophalen? De Motor leeft weider am Hannergrond. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Agent Redactor opmaachen + + + Start on Boot + Start op Boot + + + Language + Sprooch + + + Quit + Zoumaachen + + + + PasswordEnableDialog + + Enable password protection + Passwuertschutz aktivéieren + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Wielt e Master Passwuert fir Agent Redactor. Et schützt Är gespäichert API Schlësselen op dëser Maschinn an ass net mat Ärem Login Passwuert verbonnen. + + + New password: + Neit Passwuert: + + + Confirm password: + Confirméieren Passwuert: + + + Password must not be empty. + Passwuert däerf net eidel sinn. + + + Passwords do not match. + Passwierder passen net. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Spär Agent Redactor + + + Enter your master password to unlock. + Gitt Äert Master-Passwuert fir ze spären. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_lt.ts b/linux/gui/i18n/agentredactor_lt.ts new file mode 100644 index 0000000..fd3f73d --- /dev/null +++ b/linux/gui/i18n/agentredactor_lt.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Naujinys paruoštas diegti + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 atsisiųstas. Paleiskite iš naujo dabar, kad pritaikytumėte naujinį. + + + Restart now + Paleisti iš naujo dabar + + + Later + Vėliau + + + Check for updates + Tikrinti naujinimus + + + You're up to date. + Turite naujausią versiją. + + + Couldn't check for updates. Try again later. + Nepavyko patikrinti naujinimų. Bandykite dar kartą vėliau. + + + &File + &Failas + + + &Quit + Išeiti + + + Profile + Profilis + + + Detection + Aptikimas + + + Regex Patterns + Regex šablonai + + + Keywords + Raktiniai žodžiai + + + Password + Slaptažodis + + + Statistics + Statistika + + + Session Redactions + Sesijos redagavimai + + + Logs + Žurnalai + + + Settings + Nustatymai + + + Name: + Vardas: + + + Port: + Uostas: + + + Forward To + Persiųsti į + + + API Key + API raktas + + + Use AI model: + Naudokite AI modelį: + + + Confidence threshold: + Pasitikėjimo slenkstis: + + + Add + Pridėti + + + Remove + Pašalinti + + + Show API key + Rodyti API raktą + + + Copy proxy URL + Nukopijuokite tarpinio serverio URL + + + Save + Išsaugoti + + + Use AI model for PII detection + Naudokite AI modelį AII aptikimui + + + Case sensitive + Skirti didžiąsias ir mažąsias raides + + + Require master password + Reikalauti pagrindinio slaptažodžio + + + Clear statistics + Aiški statistika + + + Clear + Išvalyti + + + Enable logging + Įjungti registravimą + + + Show sensitive information in logs + Rodyti slaptą informaciją žurnaluose + + + Open log file + Atidaryti žurnalo failą + + + Open folder + Atidaryti aplanką + + + Delete all logs + Ištrinti visus žurnalus + + + Start on Boot + Pradėkite nuo įkrovos + + + Language + Kalba + + + System default + Sistemos numatytoji + + + Master Password + Pagrindinis slaptažodis + + + Unlock + Atrakinti + + + Agent Redactor is locked + Agento redaktorius užrakintas + + + Account number + Sąskaitos numeris + + + Address + Adresas + + + Date + Data + + + Email + El. paštas + + + Person + Asmuo + + + Phone + Telefonas + + + URL + URL + + + Secret + Paslaptis + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Užklausos: %1 PII: %2 Regex: %3 Raktiniai žodžiai: %4 + + + Engine is not running — retrying… + Variklis neveikia – bandoma iš naujo… + + + Delete + Ištrinti + + + Validation Error + Patvirtinimo klaida + + + Invalid regex syntax. + Neteisinga regex sintaksė. + + + Case: Yes + Byla: Taip + + + Case: No + Byla: Ne + + + Port must be between 1024 and 65535. + Prievadas turi būti nuo 1024 iki 65535. + + + Port %1 is already used by profile '%2'. + Jungtis %1 jau naudojama profilio '%2'. + + + Forward To URL must start with http:// or https://. + Persiuntimo URL turi prasidėti http:// arba https://. + + + Confidence threshold must be between 0.0 and 1.0. + Pasitikėjimo riba turi būti nuo 0,0 iki 1,0. + + + Security Warning + Saugos įspėjimas + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Naudojate HTTP upstream URL (nešifruotą). Jūsų API raktas bus išsiųstas atviru tekstu per tinklą. + + + Error + Klaida + + + The engine rejected the profile. Check the engine log for details. + Variklis atmetė profilį. Norėdami gauti daugiau informacijos, patikrinkite variklio žurnalą. + + + Profile %1 + Profilis %1 + + + The engine rejected the new profile. + Variklis atmetė naują profilį. + + + Remove Profile + Pašalinti profilį + + + Are you sure? This operation is permanent. + Ar tikrai? Ši operacija yra neatšaukiama. + + + Proxy URL copied to clipboard + Tarpinio serverio URL nukopijuotas į mainų sritį + + + Wrong password. + Neteisingas slaptažodis. + + + Show sensitive information + Rodyti neskelbtiną informaciją + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Jautrus registravimas įrašo neapdorotas, neredaguotas reikšmes (įskaitant API raktus) į žurnalą. Įjunkite jį tik derinimo metu. + + + Enable logging first. + Pirmiausia įjunkite registravimą. + + + Delete all logs? + Ištrinti visus žurnalus? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Tai visam laikui ištrins dabartinį žurnalo failą ir visus archyvuotus sesijų žurnalus. To nebus galima atšaukti. + + + Downloading AI model + Atsisiunčiamas AI modelis + + + Retry + Bandyti dar kartą + + + The PII detection model is downloading (%1%). + AII aptikimo modelis atsisiunčiamas (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Nepavyko atsisiųsti modelio. Patikrinkite interneto ryšį ir bandykite dar kartą. PII aptikimas nepasiekiamas, kol atsisiuntimas nebus baigtas. + + + Are you sure you want to quit? + Ar tikrai norite išeiti? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Jei išeisite, Agent Redactor nebe stebės ir redaguos API srauto. + + + Quit Agent Redactor? The engine keeps running in the background. + Išeiti iš „Agent Redactor“? Variklis toliau dirba fone. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Atidaryti Agent Redactor + + + Start on Boot + Pradėkite nuo įkrovos + + + Language + Kalba + + + Quit + Išeiti + + + + PasswordEnableDialog + + Enable password protection + Įjungti apsaugą slaptažodžiu + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Pasirinkite pagrindinį „Agent Redactor“ slaptažodį. Jis apsaugo šiame įrenginyje saugomus API raktus ir nesusijęs su prisijungimo slaptažodžiu. + + + New password: + Naujas slaptažodis: + + + Confirm password: + Patvirtinkite slaptažodį: + + + Password must not be empty. + Slaptažodis neturi būti tuščias. + + + Passwords do not match. + Slaptažodžiai nesutampa. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Atrakinkite agento redaktorių + + + Enter your master password to unlock. + Norėdami atrakinti, įveskite pagrindinį slaptažodį. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_lv.ts b/linux/gui/i18n/agentredactor_lv.ts new file mode 100644 index 0000000..13d6a3e --- /dev/null +++ b/linux/gui/i18n/agentredactor_lv.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Atjauninājums gatavs instalēšanai + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 ir lejupielādēts. Restartējiet tagad, lai lietotu atjauninājumu. + + + Restart now + Restartēt tagad + + + Later + Vēlāk + + + Check for updates + Pārbaudīt atjauninājumus + + + You're up to date. + Jums ir jaunākā versija. + + + Couldn't check for updates. Try again later. + Neizdevās pārbaudīt atjauninājumus. Mēģiniet vēlāk vēlreiz. + + + &File + &Fails + + + &Quit + Iziet + + + Profile + Profils + + + Detection + Atklāšana + + + Regex Patterns + Regex paraugi + + + Keywords + Atslēgvārdi + + + Password + Parole + + + Statistics + Statistika + + + Session Redactions + Sesijas rediģējumi + + + Logs + Žurnāli + + + Settings + Iestatījumi + + + Name: + Vārds: + + + Port: + Ports: + + + Forward To + Pārsūtīt uz + + + API Key + API atslēga + + + Use AI model: + Izmantojiet AI modeli: + + + Confidence threshold: + Pārliecības slieksnis: + + + Add + Pievienot + + + Remove + Noņemt + + + Show API key + Rādīt API atslēgu + + + Copy proxy URL + Kopēt starpniekservera URL + + + Save + Saglabāt + + + Use AI model for PII detection + Izmantojiet AI modeli PII noteikšanai + + + Case sensitive + Reģistrjutīgs + + + Require master password + Pieprasīt galveno paroli + + + Clear statistics + Skaidra statistika + + + Clear + Notīrīt + + + Enable logging + Iespējot reģistrēšanu + + + Show sensitive information in logs + Rādīt sensitīvu informāciju žurnālos + + + Open log file + Atvērt žurnāla failu + + + Open folder + Atvērt mapi + + + Delete all logs + Dzēst visus žurnālus + + + Start on Boot + Sāciet ar sāknēšanu + + + Language + Valoda + + + System default + Sistēmas noklusējums + + + Master Password + Galvenā parole + + + Unlock + Atbloķēt + + + Agent Redactor is locked + Aģenta redaktors ir bloķēts + + + Account number + Konta numurs + + + Address + Adrese + + + Date + Datums + + + Email + E-pasts + + + Person + Persona + + + Phone + Tālrunis + + + URL + URL + + + Secret + Noslēpums + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Pieprasījumi: %1 PII: %2 Regex: %3 Atslēgvārdi: %4 + + + Engine is not running — retrying… + Dzinējs nedarbojas — notiek atkārtots mēģinājums… + + + Delete + Dzēst + + + Validation Error + Validācijas kļūda + + + Invalid regex syntax. + Nederīga regex sintakse. + + + Case: Yes + Lieta: Jā + + + Case: No + Lieta: Nē + + + Port must be between 1024 and 65535. + Portam jābūt no 1024 līdz 65535. + + + Port %1 is already used by profile '%2'. + Ports %1 jau izmanto profils '%2'. + + + Forward To URL must start with http:// or https://. + Pārsūtīšanas URL jāsākas ar http:// vai https://. + + + Confidence threshold must be between 0.0 and 1.0. + Pārliecības slieksnim jābūt no 0,0 līdz 1,0. + + + Security Warning + Drošības brīdinājums + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Jūs izmantojat HTTP upstream URL (nešifrētu). Jūsu API atslēga tiks nosūtīta kā vienkāršs teksts pa tīklu. + + + Error + Kļūda + + + The engine rejected the profile. Check the engine log for details. + Dzinējs profilu noraidīja. Sīkāku informāciju skatiet dzinēja žurnālā. + + + Profile %1 + Profils %1 + + + The engine rejected the new profile. + Dzinējs noraidīja jauno profilu. + + + Remove Profile + Noņemt profilu + + + Are you sure? This operation is permanent. + Vai esat pārliecināts? Šī darbība ir neatgriezeniska. + + + Proxy URL copied to clipboard + Starpniekservera URL ir kopēts starpliktuvē + + + Wrong password. + Nepareiza parole. + + + Show sensitive information + Rādīt sensitīvu informāciju + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Sensitīvā reģistrēšana žurnālā ieraksta neapstrādātas, nerediģētas vērtības (tostarp API atslēgas). Iespējojiet to tikai atkļūdošanas laikā. + + + Enable logging first. + Vispirms iespējojiet reģistrēšanu. + + + Delete all logs? + Dzēst visus žurnālus? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Tas neatgriezeniski izdzēsīs pašreizējo žurnāla failu un visus arhivētos sesiju žurnālus. To nevar atsaukt. + + + Downloading AI model + Lejupielādē AI modeli + + + Retry + Mēģināt vēlreiz + + + The PII detection model is downloading (%1%). + Notiek PII noteikšanas modeļa lejupielāde (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Modeļa lejupielāde neizdevās. Pārbaudiet interneta savienojumu un mēģiniet vēlreiz. PII noteikšana nav pieejama, līdz lejupielāde ir pabeigta. + + + Are you sure you want to quit? + Vai esat pārliecināts, ka vēlaties iziet? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Ja iziesiet, Agent Redactor vairs neuzraudzīs un nerediģēs API datplūsmu. + + + Quit Agent Redactor? The engine keeps running in the background. + Vai pamest Agent Redactor? Dzinējs turpina darboties fonā. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Atvērt Agent Redactor + + + Start on Boot + Sāciet ar sāknēšanu + + + Language + Valoda + + + Quit + Iziet + + + + PasswordEnableDialog + + Enable password protection + Iespējot paroles aizsardzību + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Izvēlieties Agent Redactor galveno paroli. Tas aizsargā jūsu saglabātās API atslēgas šajā iekārtā un nav saistīts ar jūsu pieteikšanās paroli. + + + New password: + Jauna parole: + + + Confirm password: + Apstipriniet paroli: + + + Password must not be empty. + Paroles lauks nedrīkst būt tukšs. + + + Passwords do not match. + Paroles nesakrīt. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Atbloķējiet aģentu redaktoru + + + Enter your master password to unlock. + Ievadiet savu galveno paroli, lai atbloķētu. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_ms.ts b/linux/gui/i18n/agentredactor_ms.ts new file mode 100644 index 0000000..a0a0bdc --- /dev/null +++ b/linux/gui/i18n/agentredactor_ms.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Kemas kini sedia untuk dipasang + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 telah dimuat turun. Mulakan semula sekarang untuk menggunakan kemas kini. + + + Restart now + Mulakan semula sekarang + + + Later + Kemudian + + + Check for updates + Semak kemas kini + + + You're up to date. + Anda menggunakan versi terkini. + + + Couldn't check for updates. Try again later. + Tidak dapat menyemak kemas kini. Cuba lagi kemudian. + + + &File + &Fail + + + &Quit + Berhenti + + + Profile + Profil + + + Detection + Pengesanan + + + Regex Patterns + Corak Regex + + + Keywords + Kata kunci + + + Password + Kata laluan + + + Statistics + Perangkaan + + + Session Redactions + Penyuntingan Sesi + + + Logs + Log + + + Settings + tetapan + + + Name: + nama: + + + Port: + Pelabuhan: + + + Forward To + Maju Kepada + + + API Key + Kunci API + + + Use AI model: + Gunakan model AI: + + + Confidence threshold: + Ambang keyakinan: + + + Add + Tambah + + + Remove + Alih keluar + + + Show API key + Tunjukkan kunci API + + + Copy proxy URL + Salin URL proksi + + + Save + Jimat + + + Use AI model for PII detection + Gunakan model AI untuk pengesanan PII + + + Case sensitive + Kes sensitif + + + Require master password + Memerlukan kata laluan induk + + + Clear statistics + Statistik yang jelas + + + Clear + Jelas + + + Enable logging + Dayakan pengelogan + + + Show sensitive information in logs + Tunjukkan maklumat sensitif dalam log + + + Open log file + Buka fail log + + + Open folder + Buka folder + + + Delete all logs + Padam semua log + + + Start on Boot + Mulakan pada Boot + + + Language + Bahasa + + + System default + Sistem lalai + + + Master Password + Kata Laluan Utama + + + Unlock + Buka kunci + + + Agent Redactor is locked + Agent Redactor dikunci + + + Account number + Nombor akaun + + + Address + Alamat + + + Date + tarikh + + + Email + E-mel + + + Person + Orang + + + Phone + telefon + + + URL + URL + + + Secret + Rahsia + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Permintaan: %1 PII: %2 Regex: %3 Kata kunci: %4 + + + Engine is not running — retrying… + Enjin tidak berfungsi — mencuba semula… + + + Delete + Padam + + + Validation Error + Ralat Pengesahan + + + Invalid regex syntax. + Sintaks regex tidak sah. + + + Case: Yes + Kes: Ya + + + Case: No + Kes: Tidak + + + Port must be between 1024 and 65535. + Pelabuhan mestilah antara 1024 dan 65535. + + + Port %1 is already used by profile '%2'. + Port %1 sudah digunakan oleh profil '%2'. + + + Forward To URL must start with http:// or https://. + Majukan Ke URL mesti bermula dengan http:// atau https://. + + + Confidence threshold must be between 0.0 and 1.0. + Ambang keyakinan mestilah antara 0.0 dan 1.0. + + + Security Warning + Amaran Keselamatan + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Anda menggunakan URL huluan HTTP (tidak disulitkan). Kunci API anda akan dihantar dalam teks biasa melalui rangkaian. + + + Error + ralat + + + The engine rejected the profile. Check the engine log for details. + Enjin menolak profil. Semak log enjin untuk butiran. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + Enjin menolak profil baharu. + + + Remove Profile + Alih Keluar Profil + + + Are you sure? This operation is permanent. + Adakah anda pasti? Operasi ini kekal. + + + Proxy URL copied to clipboard + URL proksi disalin ke papan keratan + + + Wrong password. + Kata laluan salah. + + + Show sensitive information + Tunjukkan maklumat sensitif + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Pengelogan sensitif menulis nilai mentah yang tidak disunting (termasuk kunci API) pada log. Hanya dayakannya semasa menyahpepijat. + + + Enable logging first. + Dayakan pengelogan dahulu. + + + Delete all logs? + Padamkan semua log? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Ini akan memadamkan fail log semasa dan semua log sesi yang diarkibkan secara kekal. Ini tidak boleh dibuat asal. + + + Downloading AI model + Memuat turun model AI + + + Retry + Cuba lagi + + + The PII detection model is downloading (%1%). + Model pengesanan PII sedang dimuat turun (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Muat turun model gagal. Semak sambungan internet anda, kemudian cuba lagi. Pengesanan PII tidak tersedia sehingga muat turun selesai. + + + Are you sure you want to quit? + Adakah anda pasti mahu berhenti? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Jika anda berhenti, Agent Redactor tidak lagi akan memantau dan menyunting trafik API. + + + Quit Agent Redactor? The engine keeps running in the background. + Keluar dari Agen Redactor? Enjin terus berjalan di latar belakang. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Buka Agen Redaktor + + + Start on Boot + Mulakan pada Boot + + + Language + Bahasa + + + Quit + Berhenti + + + + PasswordEnableDialog + + Enable password protection + Dayakan perlindungan kata laluan + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Pilih kata laluan induk untuk Agent Redactor. Ia melindungi kunci API anda yang disimpan pada mesin ini dan tidak berkaitan dengan kata laluan log masuk anda. + + + New password: + Kata laluan baharu: + + + Confirm password: + Sahkan kata laluan: + + + Password must not be empty. + Kata laluan tidak boleh kosong. + + + Passwords do not match. + Kata laluan tidak sepadan. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Buka Kunci Redaktor Agen + + + Enter your master password to unlock. + Masukkan kata laluan induk anda untuk membuka kunci. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_mt.ts b/linux/gui/i18n/agentredactor_mt.ts new file mode 100644 index 0000000..31d1492 --- /dev/null +++ b/linux/gui/i18n/agentredactor_mt.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Aġġornament lest biex jinstalla + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 ġie ddawnlowdjat. Erġa' niedi issa biex jiġi applikat l-aġġornament. + + + Restart now + Erġa' niedi issa + + + Later + Aktar tard + + + Check for updates + Iċċekkja għal aġġornamenti + + + You're up to date. + Għandek l-aħħar verżjoni. + + + Couldn't check for updates. Try again later. + Ma setax jiġi ċċekkjat għal aġġornamenti. Erġa' pprova aktar tard. + + + &File + &Fajl + + + &Quit + Oħroġ + + + Profile + Profil + + + Detection + Sejbien + + + Regex Patterns + Mudelli Regex + + + Keywords + Kelmiet Muftieħa + + + Password + Password + + + Statistics + Statistika + + + Session Redactions + Redazzjonijiet tas-Sessjoni + + + Logs + Logs + + + Settings + Settings + + + Name: + Isem: + + + Port: + Port: + + + Forward To + Wassal Lejn + + + API Key + Ċavetta API + + + Use AI model: + Uża mudell AI: + + + Confidence threshold: + Limitu ta' kunfidenza: + + + Add + Żid + + + Remove + Neħħi + + + Show API key + Uri ċ-ċavetta API + + + Copy proxy URL + Ikkopja l-URL tal-prokura + + + Save + Ħlief + + + Use AI model for PII detection + Uża mudell AI għall-iskoperta tal-PII + + + Case sensitive + Sensittiv għall-każ + + + Require master password + Jeħtieġ il-password prinċipali + + + Clear statistics + Statistika ċara + + + Clear + Ħassar + + + Enable logging + Ippermetti l-illoggjar + + + Show sensitive information in logs + Uri informazzjoni sensittiva fir-zkuk + + + Open log file + Iftaħ il-log file + + + Open folder + Iftaħ il-folder + + + Delete all logs + Ħassar il-logs kollha + + + Start on Boot + Ibda fuq Boot + + + Language + Lingwa + + + System default + Default tas-sistema + + + Master Password + Password Prinċipali + + + Unlock + Iftaħ + + + Agent Redactor is locked + L-aġent Redactor huwa msakkar + + + Account number + Numru tal-kont + + + Address + Indirizz + + + Date + Data + + + Email + Posta elettronika + + + Person + Persuna + + + Phone + Telefon + + + URL + URL + + + Secret + Sigriet + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Talbiet: %1 PII: %2 Regex: %3 Kliem ewlieni: %4 + + + Engine is not running — retrying… + Il-magna mhix qed taħdem — qed nipprova mill-ġdid... + + + Delete + Ħassar + + + Validation Error + Żball ta' Validazzjoni + + + Invalid regex syntax. + Sintassi regex invalida. + + + Case: Yes + Każ: Iva + + + Case: No + Każ: Le + + + Port must be between 1024 and 65535. + Il-port irid ikun bejn 1024 u 65535. + + + Port %1 is already used by profile '%2'. + Il-port %1 diġà qed jintuża mill-profil '%2'. + + + Forward To URL must start with http:// or https://. + L-URL Forward To irid jibda b'http:// jew https://. + + + Confidence threshold must be between 0.0 and 1.0. + Il-limitu tal-konfidenza irid ikun bejn 0.0 u 1.0. + + + Security Warning + Twissija ta' Sigurtà + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Qed tuża URL upstream HTTP (mhux inkriptat). Iċ-ċavetta API tiegħek se tintbagħat bħala test plain fuq in-netwerk. + + + Error + Żball + + + The engine rejected the profile. Check the engine log for details. + Il-magna rrifjutat il-profil. Iċċekkja l-ġurnal tal-magna għad-dettalji. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + Il-magna rrifjutat il-profil il-ġdid. + + + Remove Profile + Neħħi l-Profil + + + Are you sure? This operation is permanent. + Żgur? Din l-operazzjoni hija permanenti. + + + Proxy URL copied to clipboard + URL tal-prokura kkupjat fil-clipboard + + + Wrong password. + Password ħażina. + + + Show sensitive information + Uri informazzjoni sensittiva + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + L-illoggjar sensittiv jikteb valuri mhux maħduma, mhux redacted (inklużi ċwievet API) fil-log. Ippermettiha biss waqt id-debugging. + + + Enable logging first. + Ippermetti l-illoggjar l-ewwel. + + + Delete all logs? + Ħassar il-logs kollha? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Dan se jħassar il-log file attwali u l-logs tas-sessjoni arkivjati kollha b'mod permanenti. Ma jistax jitneħħa. + + + Downloading AI model + Qed jitniżżel il-mudell AI + + + Retry + Erġa' pprova + + + The PII detection model is downloading (%1%). + Il-mudell ta' skoperta tal-PII qed tniżżel (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Id-dawnload tal-mudell falla. Iċċekkja l-konnessjoni tal-internet tiegħek, imbagħad erġa' pprova. Is-sejbien ta' PII mhuwiex disponibbli sakemm id-dawnload jitlesta. + + + Are you sure you want to quit? + Żgur li trid toħroġ? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Jekk toħroġ, Agent Redactor m'għadux jimmonitorja u jirredatta t-traffiku API. + + + Quit Agent Redactor? The engine keeps running in the background. + Nieqaf aġent Redactor? Il-magna tibqa' taħdem fl-isfond. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Iftaħ Agent Redactor + + + Start on Boot + Ibda fuq Boot + + + Language + Lingwa + + + Quit + Oħroġ + + + + PasswordEnableDialog + + Enable password protection + Ippermetti l-protezzjoni bil-password + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Agħżel password prinċipali għal Agent Redactor. Jipproteġi ċ-ċwievet API maħżuna tiegħek fuq din il-magna u mhux relatat mal-password tal-login tiegħek. + + + New password: + Password ġdida: + + + Confirm password: + Ikkonferma l-password: + + + Password must not be empty. + Il-password m'għandux ikun vojt. + + + Passwords do not match. + Il-passwords ma jaqblux. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Nisfrutta l-aġent Redactor + + + Enter your master password to unlock. + Daħħal il-password prinċipali tiegħek biex tiftaħ. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_nb.ts b/linux/gui/i18n/agentredactor_nb.ts new file mode 100644 index 0000000..6c02a1e --- /dev/null +++ b/linux/gui/i18n/agentredactor_nb.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Oppdatering klar til å installeres + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 er lastet ned. Start på nytt nå for å bruke oppdateringen. + + + Restart now + Start på nytt nå + + + Later + Senere + + + Check for updates + Se etter oppdateringer + + + You're up to date. + Du har den nyeste versjonen. + + + Couldn't check for updates. Try again later. + Kunne ikke se etter oppdateringer. Prøv igjen senere. + + + &File + &Fil + + + &Quit + Avslutt + + + Profile + Profil + + + Detection + Oppdagelse + + + Regex Patterns + Regex-mønstre + + + Keywords + Nøkkelord + + + Password + Passord + + + Statistics + Statistikk + + + Session Redactions + Øktredigeringer + + + Logs + Logger + + + Settings + Innstillinger + + + Name: + Navn: + + + Port: + Havn: + + + Forward To + Videresend til + + + API Key + API-nøkkel + + + Use AI model: + Bruk AI-modell: + + + Confidence threshold: + Konfidensgrense: + + + Add + Legg til + + + Remove + Fjern + + + Show API key + Vis API-nøkkel + + + Copy proxy URL + Kopier proxy-URL + + + Save + Spare + + + Use AI model for PII detection + Bruk AI-modell for PII-deteksjon + + + Case sensitive + Skill store/små bokstaver + + + Require master password + Krev hovedpassord + + + Clear statistics + Tydelig statistikk + + + Clear + Tøm + + + Enable logging + Aktiver logging + + + Show sensitive information in logs + Vis sensitiv informasjon i logger + + + Open log file + Åpne loggfil + + + Open folder + Åpne mappe + + + Delete all logs + Slett alle logger + + + Start on Boot + Start ved oppstart + + + Language + Språk + + + System default + Systemstandard + + + Master Password + Hovedpassord + + + Unlock + Lås opp + + + Agent Redactor is locked + Agent Redactor er låst + + + Account number + Kontonummer + + + Address + Adresse + + + Date + Dato + + + Email + E-post + + + Person + Person + + + Phone + Telefon + + + URL + URL + + + Secret + Hemmelighet + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Forespørsler: %1 PII: %2 Regex: %3 Nøkkelord: %4 + + + Engine is not running — retrying… + Motoren går ikke – prøver på nytt... + + + Delete + Slett + + + Validation Error + Valideringsfeil + + + Invalid regex syntax. + Ugyldig regex-syntaks. + + + Case: Yes + Sak: Ja + + + Case: No + Sak: Nei + + + Port must be between 1024 and 65535. + Porten må være mellom 1024 og 65535. + + + Port %1 is already used by profile '%2'. + Port %1 er allerede i bruk av profilen '%2'. + + + Forward To URL must start with http:// or https://. + Videresendings-URL må starte med http:// eller https://. + + + Confidence threshold must be between 0.0 and 1.0. + Konfidensgrensen må være mellom 0,0 og 1,0. + + + Security Warning + Sikkerhetsadvarsel + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Du bruker en HTTP-upstream-URL (ukryptert). API-nøkkelen din sendes i klartekst over nettverket. + + + Error + Feil + + + The engine rejected the profile. Check the engine log for details. + Motoren avviste profilen. Sjekk motorloggen for detaljer. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + Motoren avviste den nye profilen. + + + Remove Profile + Fjern profil + + + Are you sure? This operation is permanent. + Er du sikker? Denne operasjonen kan ikke angres. + + + Proxy URL copied to clipboard + Proxy URL kopiert til utklippstavlen + + + Wrong password. + Feil passord. + + + Show sensitive information + Vis sensitiv informasjon + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Sensitiv logging skriver rå, uredigerte verdier (inkludert API-nøkler) til loggen. Aktiver det bare under feilsøking. + + + Enable logging first. + Aktiver logging først. + + + Delete all logs? + Slett alle logger? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Dette sletter gjeldende loggfil og alle arkiverte øktlogger permanent. Dette kan ikke angres. + + + Downloading AI model + Laster ned AI-modell + + + Retry + Prøv på nytt + + + The PII detection model is downloading (%1%). + PII-deteksjonsmodellen lastes ned (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Nedlastingen av modellen mislyktes. Kontroller Internett-tilkoblingen din, og prøv på nytt. PII-gjenkjenning er ikke tilgjengelig før nedlastingen er fullført. + + + Are you sure you want to quit? + Er du sikker på at du vil avslutte? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Hvis du avslutter, vil Agent Redactor ikke lenger overvåke og redigere API-trafikk. + + + Quit Agent Redactor? The engine keeps running in the background. + Vil du avslutte Agent Redactor? Motoren fortsetter å gå i bakgrunnen. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Åpne Agent Redactor + + + Start on Boot + Start ved oppstart + + + Language + Språk + + + Quit + Avslutt + + + + PasswordEnableDialog + + Enable password protection + Aktiver passordbeskyttelse + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Velg et hovedpassord for Agent Redactor. Den beskytter dine lagrede API-nøkler på denne maskinen og er ikke relatert til påloggingspassordet ditt. + + + New password: + Nytt passord: + + + Confirm password: + Bekreft passord: + + + Password must not be empty. + Passordet må ikke være tomt. + + + Passwords do not match. + Passord stemmer ikke. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Lås opp Agent Redactor + + + Enter your master password to unlock. + Skriv inn hovedpassordet ditt for å låse opp. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_nl.ts b/linux/gui/i18n/agentredactor_nl.ts new file mode 100644 index 0000000..67784aa --- /dev/null +++ b/linux/gui/i18n/agentredactor_nl.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Update gereed om te installeren + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 is gedownload. Start nu opnieuw op om de update toe te passen. + + + Restart now + Nu opnieuw opstarten + + + Later + Later + + + Check for updates + Controleren op updates + + + You're up to date. + Je bent up-to-date. + + + Couldn't check for updates. Try again later. + Kan niet controleren op updates. Probeer het later opnieuw. + + + &File + &Bestand + + + &Quit + Afsluiten + + + Profile + Profiel + + + Detection + Detectie + + + Regex Patterns + Regex-patronen + + + Keywords + Trefwoorden + + + Password + Wachtwoord + + + Statistics + Statistieken + + + Session Redactions + Sessieredacties + + + Logs + Logboeken + + + Settings + Instellingen + + + Name: + Naam: + + + Port: + Haven: + + + Forward To + Doorsturen naar + + + API Key + API-sleutel + + + Use AI model: + Gebruik AI-model: + + + Confidence threshold: + Vertrouwensdrempel: + + + Add + Toevoegen + + + Remove + Verwijderen + + + Show API key + Toon API-sleutel + + + Copy proxy URL + Kopieer de proxy-URL + + + Save + Redden + + + Use AI model for PII detection + Gebruik het AI-model voor PII-detectie + + + Case sensitive + Hoofdlettergevoelig + + + Require master password + Hoofdwachtwoord vereisen + + + Clear statistics + Duidelijke statistieken + + + Clear + Wissen + + + Enable logging + Logboekregistratie inschakelen + + + Show sensitive information in logs + Toon gevoelige informatie in logboeken + + + Open log file + Logbestand openen + + + Open folder + Map openen + + + Delete all logs + Alle logboeken verwijderen + + + Start on Boot + Begin bij het opstarten + + + Language + Taal + + + System default + Systeemstandaard + + + Master Password + Hoofdwachtwoord + + + Unlock + Ontgrendelen + + + Agent Redactor is locked + Agent Redactor is vergrendeld + + + Account number + Rekeningnummer + + + Address + Adres + + + Date + Datum + + + Email + E-mail + + + Person + Persoon + + + Phone + Telefoon + + + URL + URL + + + Secret + Geheim + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Verzoeken: %1 PII: %2 Regex: %3 Trefwoorden: %4 + + + Engine is not running — retrying… + De motor draait niet. Opnieuw proberen... + + + Delete + Verwijderen + + + Validation Error + Validatiefout + + + Invalid regex syntax. + Ongeldige regex-syntaxis. + + + Case: Yes + Geval: Ja + + + Case: No + Geval: Nee + + + Port must be between 1024 and 65535. + Poort moet tussen 1024 en 65535 liggen. + + + Port %1 is already used by profile '%2'. + Poort %1 wordt al gebruikt door profiel '%2'. + + + Forward To URL must start with http:// or https://. + Doorstuur-URL moet beginnen met http:// of https://. + + + Confidence threshold must be between 0.0 and 1.0. + Drempelwaarde voor betrouwbaarheid moet tussen 0,0 en 1,0 liggen. + + + Security Warning + Beveiligingswaarschuwing + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + U gebruikt een HTTP-upstream-URL (ongeëncrypteerd). Uw API-sleutel wordt als platte tekst over het netwerk verzonden. + + + Error + Fout + + + The engine rejected the profile. Check the engine log for details. + De motor heeft het profiel afgewezen. Controleer het motorlogboek voor meer informatie. + + + Profile %1 + Profiel %1 + + + The engine rejected the new profile. + De motor heeft het nieuwe profiel afgewezen. + + + Remove Profile + Profiel verwijderen + + + Are you sure? This operation is permanent. + Weet u het zeker? Deze bewerking is permanent. + + + Proxy URL copied to clipboard + Proxy-URL gekopieerd naar klembord + + + Wrong password. + Verkeerd wachtwoord. + + + Show sensitive information + Toon gevoelige informatie + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Gevoelige logboekregistratie schrijft onbewerkte, niet-geredigeerde waarden (inclusief API-sleutels) naar het logboek. Schakel het alleen in tijdens het debuggen. + + + Enable logging first. + Schakel eerst logboekregistratie in. + + + Delete all logs? + Alle logboeken verwijderen? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Dit verwijdert het huidige logbestand en alle gearchiveerde sessielogboeken permanent. Dit kan niet ongedaan worden gemaakt. + + + Downloading AI model + AI-model downloaden + + + Retry + Opnieuw proberen + + + The PII detection model is downloading (%1%). + Het PII-detectiemodel wordt gedownload (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Het downloaden van het model is mislukt. Controleer je internetverbinding en probeer het opnieuw. PII-detectie is niet beschikbaar totdat de download is voltooid. + + + Are you sure you want to quit? + Weet u zeker dat u wilt afsluiten? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Als u afsluit, controleert en redigeert Agent Redactor geen API-verkeer meer. + + + Quit Agent Redactor? The engine keeps running in the background. + Agent Redactor afsluiten? De motor blijft op de achtergrond draaien. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Agent Redactor openen + + + Start on Boot + Begin bij het opstarten + + + Language + Taal + + + Quit + Afsluiten + + + + PasswordEnableDialog + + Enable password protection + Schakel wachtwoordbeveiliging in + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Kies een hoofdwachtwoord voor Agent Redactor. Het beschermt uw opgeslagen API-sleutels op deze machine en is niet gerelateerd aan uw inlogwachtwoord. + + + New password: + Nieuw wachtwoord: + + + Confirm password: + Wachtwoord bevestigen: + + + Password must not be empty. + Wachtwoord mag niet leeg zijn. + + + Passwords do not match. + Wachtwoorden komen niet overeen. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Ontgrendel Agent Redactor + + + Enter your master password to unlock. + Voer uw hoofdwachtwoord in om te ontgrendelen. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_pl.ts b/linux/gui/i18n/agentredactor_pl.ts new file mode 100644 index 0000000..ed238d8 --- /dev/null +++ b/linux/gui/i18n/agentredactor_pl.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Aktualizacja gotowa do zainstalowania + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 został pobrany. Uruchom ponownie teraz, aby zastosować aktualizację. + + + Restart now + Uruchom ponownie teraz + + + Later + Później + + + Check for updates + Sprawdź aktualizacje + + + You're up to date. + Masz najnowszą wersję. + + + Couldn't check for updates. Try again later. + Nie można sprawdzić aktualizacji. Spróbuj ponownie później. + + + &File + &Plik + + + &Quit + Zakończ + + + Profile + Profil + + + Detection + Wykrywanie + + + Regex Patterns + Wzorce Regex + + + Keywords + Słowa kluczowe + + + Password + Hasło + + + Statistics + Statystyki + + + Session Redactions + Redakcje sesji + + + Logs + Dzienniki + + + Settings + Ustawienia + + + Name: + Nazwa: + + + Port: + Port: + + + Forward To + Przekaż do + + + API Key + Klucz API + + + Use AI model: + Użyj modelu AI: + + + Confidence threshold: + Próg ufności: + + + Add + Dodaj + + + Remove + Usuń + + + Show API key + Pokaż klucz API + + + Copy proxy URL + Skopiuj adres URL serwera proxy + + + Save + Ratować + + + Use AI model for PII detection + Użyj modelu AI do wykrywania informacji umożliwiających identyfikację + + + Case sensitive + Rozróżniaj wielkość liter + + + Require master password + Wymagaj hasła głównego + + + Clear statistics + Wyczyść statystyki + + + Clear + Wyczyść + + + Enable logging + Włącz rejestrowanie + + + Show sensitive information in logs + Pokaż wrażliwe informacje w logach + + + Open log file + Otwórz plik dziennika + + + Open folder + Otwórz folder + + + Delete all logs + Usuń wszystkie dzienniki + + + Start on Boot + Zacznij od rozruchu + + + Language + Język + + + System default + Domyślny systemowy + + + Master Password + Hasło główne + + + Unlock + Odblokuj + + + Agent Redactor is locked + Agent Redaktor jest zablokowany + + + Account number + Numer konta + + + Address + Adres + + + Date + Data + + + Email + E-mail + + + Person + Osoba + + + Phone + Telefon + + + URL + URL + + + Secret + Sekret + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Żądania: %1 Identyfikator identyfikacyjny: %2 Regex: %3 Słowa kluczowe: %4 + + + Engine is not running — retrying… + Silnik nie działa — ponawianie próby… + + + Delete + Usuwać + + + Validation Error + Błąd walidacji + + + Invalid regex syntax. + Nieprawidłowa składnia regex. + + + Case: Yes + Sprawa: Tak + + + Case: No + Sprawa: Nie + + + Port must be between 1024 and 65535. + Port musi być z zakresu od 1024 do 65535. + + + Port %1 is already used by profile '%2'. + Port %1 jest już używany przez profil '%2'. + + + Forward To URL must start with http:// or https://. + URL przekazywania musi zaczynać się od http:// lub https://. + + + Confidence threshold must be between 0.0 and 1.0. + Próg pewności musi być z zakresu od 0,0 do 1,0. + + + Security Warning + Ostrzeżenie bezpieczeństwa + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Używasz HTTP upstream URL (nieszyfrowany). Twój klucz API zostanie wysłany jako zwykły tekst przez sieć. + + + Error + Błąd + + + The engine rejected the profile. Check the engine log for details. + Silnik odrzucił profil. Sprawdź dziennik silnika, aby uzyskać szczegółowe informacje. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + Silnik odrzucił nowy profil. + + + Remove Profile + Usuń profil + + + Are you sure? This operation is permanent. + Czy na pewno? Ta operacja jest nieodwracalna. + + + Proxy URL copied to clipboard + Adres URL proxy skopiowany do schowka + + + Wrong password. + Błędne hasło. + + + Show sensitive information + Pokaż poufne informacje + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Rejestrowanie wrażliwe zapisuje surowe, niezredagowane wartości (w tym klucze API) w dzienniku. Włącz tę opcję tylko podczas debugowania. + + + Enable logging first. + Najpierw włącz rejestrowanie. + + + Delete all logs? + Usunąć wszystkie dzienniki? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + To trwale usunie bieżący plik dziennika i wszystkie zarchiwizowane dzienniki sesji. Tej operacji nie można cofnąć. + + + Downloading AI model + Pobieranie modelu AI + + + Retry + Spróbuj ponownie + + + The PII detection model is downloading (%1%). + Trwa pobieranie modelu wykrywania danych osobowych (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Pobieranie modelu nie powiodło się. Sprawdź połączenie internetowe i spróbuj ponownie. Wykrywanie PII jest niedostępne do zakończenia pobierania. + + + Are you sure you want to quit? + Czy na pewno chcesz zakończyć? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Jeśli zakończysz, Agent Redactor nie będzie już monitorował i redagował ruchu API. + + + Quit Agent Redactor? The engine keeps running in the background. + Opuścić Agenta Redaktora? Silnik cały czas pracuje w tle. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Otwórz Agent Redactor + + + Start on Boot + Zacznij od rozruchu + + + Language + Język + + + Quit + Zakończ + + + + PasswordEnableDialog + + Enable password protection + Włącz ochronę hasłem + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Wybierz hasło główne dla Agent Redactor. Chroni Twoje klucze API przechowywane na tym komputerze i nie jest powiązany z Twoim hasłem logowania. + + + New password: + Nowe hasło: + + + Confirm password: + Potwierdź hasło: + + + Password must not be empty. + Hasło nie może być puste. + + + Passwords do not match. + Hasła nie pasują. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Odblokuj Redaktora Agenta + + + Enter your master password to unlock. + Wprowadź swoje hasło główne, aby odblokować. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_pt.ts b/linux/gui/i18n/agentredactor_pt.ts new file mode 100644 index 0000000..c6d89be --- /dev/null +++ b/linux/gui/i18n/agentredactor_pt.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Atualização pronta a instalar + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + O Agent Redactor %1 foi transferido. Reinicie agora para aplicar a atualização. + + + Restart now + Reiniciar agora + + + Later + Mais tarde + + + Check for updates + Procurar atualizações + + + You're up to date. + Está atualizado. + + + Couldn't check for updates. Try again later. + Não foi possível procurar atualizações. Tente novamente mais tarde. + + + &File + &Arquivo + + + &Quit + Sair + + + Profile + Perfil + + + Detection + Detecção + + + Regex Patterns + Padrões de regex + + + Keywords + Palavras-chave + + + Password + Palavra-passe + + + Statistics + Estatísticas + + + Session Redactions + Redações da sessão + + + Logs + Registos + + + Settings + Definições + + + Name: + Nome: + + + Port: + Porta: + + + Forward To + Encaminhar para + + + API Key + Chave de API + + + Use AI model: + Use o modelo de IA: + + + Confidence threshold: + Limite de confiança: + + + Add + Adicionar + + + Remove + Remover + + + Show API key + Mostrar chave de API + + + Copy proxy URL + Copiar URL do proxy + + + Save + Salvar + + + Use AI model for PII detection + Use o modelo de IA para detecção de PII + + + Case sensitive + Diferenciar maiúsculas de minúsculas + + + Require master password + Exigir senha mestra + + + Clear statistics + Limpar estatísticas + + + Clear + Limpar + + + Enable logging + Habilitar registro + + + Show sensitive information in logs + Mostrar informações confidenciais em registros + + + Open log file + Abrir ficheiro de registo + + + Open folder + Abrir pasta + + + Delete all logs + Eliminar todos os registos + + + Start on Boot + Comece na inicialização + + + Language + Idioma + + + System default + Predefinição do sistema + + + Master Password + Palavra-passe mestra + + + Unlock + Desbloquear + + + Agent Redactor is locked + O Agente Redator está bloqueado + + + Account number + Número de conta + + + Address + Morada + + + Date + Data + + + Email + E-mail + + + Person + Pessoa + + + Phone + Telefone + + + URL + URL + + + Secret + Segredo + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Solicitações: %1 PII: %2 Regex: %3 Palavras-chave: %4 + + + Engine is not running — retrying… + O mecanismo não está funcionando – tentando novamente… + + + Delete + Excluir + + + Validation Error + Erro de validação + + + Invalid regex syntax. + Sintaxe de regex inválida. + + + Case: Yes + Caso: Sim + + + Case: No + Caso: Não + + + Port must be between 1024 and 65535. + A porta deve estar entre 1024 e 65535. + + + Port %1 is already used by profile '%2'. + A porta %1 já está a ser utilizada pelo perfil '%2'. + + + Forward To URL must start with http:// or https://. + O URL de encaminhamento deve começar por http:// ou https://. + + + Confidence threshold must be between 0.0 and 1.0. + O limiar de confiança deve estar entre 0,0 e 1,0. + + + Security Warning + Aviso de segurança + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Está a utilizar um URL upstream HTTP (não encriptado). A sua chave de API será enviada em texto simples pela rede. + + + Error + Erro + + + The engine rejected the profile. Check the engine log for details. + O mecanismo rejeitou o perfil. Verifique o log do mecanismo para obter detalhes. + + + Profile %1 + Perfil %1 + + + The engine rejected the new profile. + O mecanismo rejeitou o novo perfil. + + + Remove Profile + Remover perfil + + + Are you sure? This operation is permanent. + Tem a certeza? Esta operação é permanente. + + + Proxy URL copied to clipboard + URL do proxy copiado para a área de transferência + + + Wrong password. + Senha errada. + + + Show sensitive information + Mostrar informações confidenciais + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + O log confidencial grava valores brutos e não editados (incluindo chaves de API) no log. Habilite-o apenas durante a depuração. + + + Enable logging first. + Ative o registro primeiro. + + + Delete all logs? + Eliminar todos os registos? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Isto irá eliminar permanentemente o ficheiro de registo atual e todos os registos de sessão arquivados. Não pode ser anulado. + + + Downloading AI model + A transferir o modelo de IA + + + Retry + Tentar novamente + + + The PII detection model is downloading (%1%). + O modelo de detecção de PII está sendo baixado (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + A transferência do modelo falhou. Verifique a sua ligação à Internet e tente novamente. A deteção de PII não está disponível até a transferência concluir. + + + Are you sure you want to quit? + Tem a certeza de que pretende sair? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Se sair, o Agent Redactor deixará de monitorizar e redigir o tráfego de API. + + + Quit Agent Redactor? The engine keeps running in the background. + Sair do Agente Redactor? O mecanismo continua funcionando em segundo plano. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Abrir Agent Redactor + + + Start on Boot + Comece na inicialização + + + Language + Idioma + + + Quit + Sair + + + + PasswordEnableDialog + + Enable password protection + Ativar proteção por senha + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Escolha uma senha mestra para o Agent Redactor. Ele protege suas chaves de API armazenadas nesta máquina e não está relacionado à sua senha de login. + + + New password: + Nova Senha: + + + Confirm password: + Confirme sua senha: + + + Password must not be empty. + A senha não deve estar vazia. + + + Passwords do not match. + As senhas não coincidem. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Desbloquear Agente Redator + + + Enter your master password to unlock. + Digite sua senha mestra para desbloquear. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_ro.ts b/linux/gui/i18n/agentredactor_ro.ts new file mode 100644 index 0000000..614bcbe --- /dev/null +++ b/linux/gui/i18n/agentredactor_ro.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Actualizare gata de instalare + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 a fost descărcat. Reporniți acum pentru a aplica actualizarea. + + + Restart now + Repornire acum + + + Later + Mai târziu + + + Check for updates + Verificare actualizări + + + You're up to date. + Aveți cea mai recentă versiune. + + + Couldn't check for updates. Try again later. + Nu s-au putut verifica actualizările. Încercați din nou mai târziu. + + + &File + &Fişier + + + &Quit + Ieșire + + + Profile + Profil + + + Detection + Detectare + + + Regex Patterns + Modele Regex + + + Keywords + Cuvinte cheie + + + Password + Parolă + + + Statistics + Statistici + + + Session Redactions + Redactări sesiune + + + Logs + Jurnale + + + Settings + Setări + + + Name: + Nume: + + + Port: + Port: + + + Forward To + Redirecționează către + + + API Key + Cheie API + + + Use AI model: + Utilizați modelul AI: + + + Confidence threshold: + Pragul de încredere: + + + Add + Adaugă + + + Remove + Elimină + + + Show API key + Afișați cheia API + + + Copy proxy URL + Copiați adresa URL proxy + + + Save + Salva + + + Use AI model for PII detection + Utilizați modelul AI pentru detectarea PII + + + Case sensitive + Sensibil la majuscule/minuscule + + + Require master password + Solicitați parola principală + + + Clear statistics + Statistici clare + + + Clear + Șterge + + + Enable logging + Activați înregistrarea în jurnal + + + Show sensitive information in logs + Afișați informații sensibile în jurnale + + + Open log file + Deschide fișierul jurnal + + + Open folder + Deschide folderul + + + Delete all logs + Șterge toate jurnalele + + + Start on Boot + Începeți la Boot + + + Language + Limbă + + + System default + Implicit sistem + + + Master Password + Parolă principală + + + Unlock + Deblochează + + + Agent Redactor is locked + Agent Redactor este blocat + + + Account number + Număr cont + + + Address + Adresă + + + Date + Dată + + + Email + E-mail + + + Person + Persoană + + + Phone + Telefon + + + URL + URL + + + Secret + Secret + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Solicitări: %1 PII: %2 Regex: %3 Cuvinte cheie: %4 + + + Engine is not running — retrying… + Motorul nu funcționează — se reîncercă... + + + Delete + Şterge + + + Validation Error + Eroare de validare + + + Invalid regex syntax. + Sintaxă regex invalidă. + + + Case: Yes + Caz: Da + + + Case: No + Caz: Nu + + + Port must be between 1024 and 65535. + Portul trebuie să fie între 1024 și 65535. + + + Port %1 is already used by profile '%2'. + Portul %1 este deja utilizat de profilul '%2'. + + + Forward To URL must start with http:// or https://. + URL-ul de redirecționare trebuie să înceapă cu http:// sau https://. + + + Confidence threshold must be between 0.0 and 1.0. + Pragul de încredere trebuie să fie între 0,0 și 1,0. + + + Security Warning + Avertisment de securitate + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Utilizați un URL upstream HTTP (necriptat). Cheia dvs. API va fi trimisă în text clar prin rețea. + + + Error + Eroare + + + The engine rejected the profile. Check the engine log for details. + Motorul a respins profilul. Verificați jurnalul motorului pentru detalii. + + + Profile %1 + Profilul %1 + + + The engine rejected the new profile. + Motorul a respins noul profil. + + + Remove Profile + Elimină profilul + + + Are you sure? This operation is permanent. + Sunteți sigur? Această operațiune este permanentă. + + + Proxy URL copied to clipboard + Adresa URL proxy a fost copiată în clipboard + + + Wrong password. + Parolă greșită. + + + Show sensitive information + Afișați informații sensibile + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Jurnalul sensibil scrie valori brute, neredatate (inclusiv cheile API) în jurnal. Activați-l numai în timpul depanării. + + + Enable logging first. + Activați mai întâi înregistrarea. + + + Delete all logs? + Ștergeți toate jurnalele? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Aceasta va șterge permanent fișierul jurnal curent și toate jurnalele de sesiune arhivate. Nu poate fi anulată. + + + Downloading AI model + Se descarcă modelul AI + + + Retry + Reîncercare + + + The PII detection model is downloading (%1%). + Se descarcă modelul de detectare a PII (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Descărcarea modelului a eșuat. Verificați conexiunea la internet, apoi reîncercați. Detectarea PII nu este disponibilă până la finalizarea descărcării. + + + Are you sure you want to quit? + Sunteți sigur că doriți să ieșiți? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Dacă ieșiți, Agent Redactor nu va mai monitoriza și redacta traficul API. + + + Quit Agent Redactor? The engine keeps running in the background. + Părăsiți Agent Redactor? Motorul continuă să funcționeze în fundal. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Deschide Agent Redactor + + + Start on Boot + Începeți la Boot + + + Language + Limbă + + + Quit + Ieșire + + + + PasswordEnableDialog + + Enable password protection + Activați protecția prin parolă + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Alegeți o parolă principală pentru Agent Redactor. Acesta vă protejează cheile API stocate pe această mașină și nu are legătură cu parola dvs. de conectare. + + + New password: + Parolă Nouă: + + + Confirm password: + Confirmați parola: + + + Password must not be empty. + Parola nu trebuie să fie goală. + + + Passwords do not match. + Parolele nu se potrivesc. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Deblocați Agent Redactor + + + Enter your master password to unlock. + Introduceți parola principală pentru a debloca. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_ru.ts b/linux/gui/i18n/agentredactor_ru.ts new file mode 100644 index 0000000..2bd82f9 --- /dev/null +++ b/linux/gui/i18n/agentredactor_ru.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Обновление готово к установке + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 скачан. Перезапустите сейчас, чтобы применить обновление. + + + Restart now + Перезапустить сейчас + + + Later + Позже + + + Check for updates + Проверить обновления + + + You're up to date. + У вас последняя версия. + + + Couldn't check for updates. Try again later. + Не удалось проверить обновления. Повторите попытку позже. + + + &File + &Файл + + + &Quit + Выйти + + + Profile + Профиль + + + Detection + Обнаружение + + + Regex Patterns + Шаблоны регулярных выражений + + + Keywords + Ключевые слова + + + Password + Пароль + + + Statistics + Статистика + + + Session Redactions + Редактирования сеанса + + + Logs + Журналы + + + Settings + Настройки + + + Name: + Имя: + + + Port: + Порт: + + + Forward To + Пересылать на + + + API Key + Ключ API + + + Use AI model: + Использовать модель AI: + + + Confidence threshold: + Порог уверенности: + + + Add + Добавить + + + Remove + Удалить + + + Show API key + Показать ключ API + + + Copy proxy URL + Скопировать URL-адрес прокси-сервера + + + Save + Сохранять + + + Use AI model for PII detection + Использовать модель искусственного интеллекта для обнаружения личных данных + + + Case sensitive + Учитывать регистр + + + Require master password + Требовать мастер-пароль + + + Clear statistics + Очистить статистику + + + Clear + Очистить + + + Enable logging + Включить ведение журнала + + + Show sensitive information in logs + Показывать конфиденциальную информацию в журналах + + + Open log file + Открыть файл журнала + + + Open folder + Открыть папку + + + Delete all logs + Удалить все журналы + + + Start on Boot + Начать при загрузке + + + Language + Язык + + + System default + Системный по умолчанию + + + Master Password + Мастер-пароль + + + Unlock + Разблокировать + + + Agent Redactor is locked + Редактор агента заблокирован + + + Account number + Номер счета + + + Address + Адрес + + + Date + Дата + + + Email + Электронная почта + + + Person + Человек + + + Phone + Телефон + + + URL + URL + + + Secret + Секрет + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Запросы: %1 Персональные данные: %2 Регулярное выражение: %3 Ключевые слова: %4 + + + Engine is not running — retrying… + Двигатель не работает — повторная попытка… + + + Delete + Удалить + + + Validation Error + Ошибка проверки + + + Invalid regex syntax. + Неверный синтаксис регулярного выражения. + + + Case: Yes + Корпус: Да + + + Case: No + Корпус: Нет + + + Port must be between 1024 and 65535. + Порт должен быть в диапазоне от 1024 до 65535. + + + Port %1 is already used by profile '%2'. + Порт %1 уже используется профилем '%2'. + + + Forward To URL must start with http:// or https://. + URL для пересылки должен начинаться с http:// или https://. + + + Confidence threshold must be between 0.0 and 1.0. + Порог уверенности должен быть в диапазоне от 0,0 до 1,0. + + + Security Warning + Предупреждение безопасности + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Вы используете HTTP-URL вышестоящего сервера (не зашифрованный). Ваш ключ API будет отправлен в виде открытого текста по сети. + + + Error + Ошибка + + + The engine rejected the profile. Check the engine log for details. + Двигатель отклонил профиль. Подробности смотрите в журнале двигателя. + + + Profile %1 + Профиль %1 + + + The engine rejected the new profile. + Двигатель отверг новый профиль. + + + Remove Profile + Удалить профиль + + + Are you sure? This operation is permanent. + Вы уверены? Эта операция необратима. + + + Proxy URL copied to clipboard + URL-адрес прокси-сервера скопирован в буфер обмена. + + + Wrong password. + Неправильный пароль. + + + Show sensitive information + Показать конфиденциальную информацию + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + При конфиденциальном журналировании в журнал записываются необработанные, неотредактированные значения (включая ключи API). Включайте его только во время отладки. + + + Enable logging first. + Сначала включите ведение журнала. + + + Delete all logs? + Удалить все журналы? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Будут безвозвратно удалены текущий файл журнала и все архивированные журналы сеансов. Это нельзя отменить. + + + Downloading AI model + Скачивание модели ИИ + + + Retry + Повторить + + + The PII detection model is downloading (%1%). + Модель обнаружения личных данных загружается (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Не удалось скачать модель. Проверьте подключение к Интернету и повторите попытку. Обнаружение PII будет недоступно до завершения скачивания. + + + Are you sure you want to quit? + Вы уверены, что хотите выйти? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Если вы выйдете, Agent Redactor перестанет контролировать и редактировать API-трафик. + + + Quit Agent Redactor? The engine keeps running in the background. + Выйти из Agent Redactor? Двигатель продолжает работать в фоновом режиме. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Открыть Agent Redactor + + + Start on Boot + Начать при загрузке + + + Language + Язык + + + Quit + Выйти + + + + PasswordEnableDialog + + Enable password protection + Включить защиту паролем + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Выберите главный пароль для Agent Redactor. Он защищает ваши сохраненные ключи API на этом компьютере и не связан с вашим паролем для входа. + + + New password: + Новый пароль: + + + Confirm password: + Подтвердите пароль: + + + Password must not be empty. + Пароль не должен быть пустым. + + + Passwords do not match. + Пароли не совпадают. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Разблокировать Редактор Агента + + + Enter your master password to unlock. + Введите свой мастер-пароль для разблокировки. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_sk.ts b/linux/gui/i18n/agentredactor_sk.ts new file mode 100644 index 0000000..83f74b3 --- /dev/null +++ b/linux/gui/i18n/agentredactor_sk.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Aktualizácia pripravená na inštaláciu + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 bol stiahnutý. Reštartujte teraz a aktualizáciu použite. + + + Restart now + Reštartovať teraz + + + Later + Neskôr + + + Check for updates + Skontrolovať aktualizácie + + + You're up to date. + Máte najnovšiu verziu. + + + Couldn't check for updates. Try again later. + Nepodarilo sa skontrolovať aktualizácie. Skúste to znova neskôr. + + + &File + &Súbor + + + &Quit + Ukončiť + + + Profile + Profil + + + Detection + Detekcia + + + Regex Patterns + Regex vzory + + + Keywords + Kľúčové slová + + + Password + Heslo + + + Statistics + Štatistiky + + + Session Redactions + Redigovania relácie + + + Logs + Logy + + + Settings + Nastavenia + + + Name: + meno: + + + Port: + Port: + + + Forward To + Preposlať na + + + API Key + API kľúč + + + Use AI model: + Použiť model AI: + + + Confidence threshold: + Prah spoľahlivosti: + + + Add + Pridať + + + Remove + Odstrániť + + + Show API key + Zobraziť kľúč API + + + Copy proxy URL + Skopírujte adresu URL servera proxy + + + Save + Uložiť + + + Use AI model for PII detection + Na detekciu PII použite model AI + + + Case sensitive + Rozlišovať veľkosť písmen + + + Require master password + Vyžadovať hlavné heslo + + + Clear statistics + Prehľadné štatistiky + + + Clear + Vymazať + + + Enable logging + Povoliť protokolovanie + + + Show sensitive information in logs + Zobraziť citlivé informácie v denníkoch + + + Open log file + Otvoriť súbor logu + + + Open folder + Otvoriť priečinok + + + Delete all logs + Odstrániť všetky logy + + + Start on Boot + Začnite pri zavádzaní + + + Language + Jazyk + + + System default + Predvolené systémom + + + Master Password + Hlavné heslo + + + Unlock + Odomknúť + + + Agent Redactor is locked + Agent Redactor je uzamknutý + + + Account number + Číslo účtu + + + Address + Adresa + + + Date + Dátum + + + Email + E-mail + + + Person + Osoba + + + Phone + Telefón + + + URL + URL + + + Secret + Tajomstvo + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Žiadosti: %1 PII: %2 Regulárny výraz: %3 Kľúčové slová: %4 + + + Engine is not running — retrying… + Motor nebeží – pokus sa opakuje... + + + Delete + Odstrániť + + + Validation Error + Chyba overenia + + + Invalid regex syntax. + Neplatná regex syntax. + + + Case: Yes + Prípad: Áno + + + Case: No + Prípad: Nie + + + Port must be between 1024 and 65535. + Port musí byť medzi 1024 a 65535. + + + Port %1 is already used by profile '%2'. + Port %1 už používa profil '%2'. + + + Forward To URL must start with http:// or https://. + URL na preposlanie musí začínať http:// alebo https://. + + + Confidence threshold must be between 0.0 and 1.0. + Prah istoty musí byť medzi 0,0 a 1,0. + + + Security Warning + Bezpečnostné upozornenie + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Používate HTTP upstream URL (nešifrované). Váš API kľúč bude odoslaný cez sieť ako obyčajný text. + + + Error + Chyba + + + The engine rejected the profile. Check the engine log for details. + Motor odmietol profil. Podrobnosti nájdete v denníku motora. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + Motor odmietol nový profil. + + + Remove Profile + Odstrániť profil + + + Are you sure? This operation is permanent. + Ste si istí? Táto operácia je trvalá. + + + Proxy URL copied to clipboard + Adresa URL proxy servera bola skopírovaná do schránky + + + Wrong password. + Nesprávne heslo. + + + Show sensitive information + Ukážte citlivé informácie + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Citlivé protokolovanie zapisuje nespracované, nezreagované hodnoty (vrátane kľúčov API) do protokolu. Povoľte ho iba počas ladenia. + + + Enable logging first. + Najprv povoľte protokolovanie. + + + Delete all logs? + Odstrániť všetky logy? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Toto natrvalo odstráni aktuálny súbor logu a všetky archivované relačné logy. Toto nemožno vrátiť späť. + + + Downloading AI model + Sťahovanie modelu AI + + + Retry + Skúsiť znova + + + The PII detection model is downloading (%1%). + Prebieha sťahovanie modelu detekcie PII (%1 %). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Stiahnutie modelu zlyhalo. Skontrolujte pripojenie na internet a skúste to znova. Detekcia PII nie je k dispozícii, kým sa sťahovanie nedokončí. + + + Are you sure you want to quit? + Ste si istí, že chcete skončiť? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Ak skončíte, Agent Redactor už nebude sledovať a redigovať API prevádzku. + + + Quit Agent Redactor? The engine keeps running in the background. + Chcete ukončiť aplikáciu Agent Redactor? Motor beží na pozadí. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Otvoriť Agent Redactor + + + Start on Boot + Začnite pri zavádzaní + + + Language + Jazyk + + + Quit + Ukončiť + + + + PasswordEnableDialog + + Enable password protection + Povoliť ochranu heslom + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Zvoľte hlavné heslo pre Agent Redactor. Chráni vaše uložené kľúče API na tomto počítači a nesúvisí s vaším prihlasovacím heslom. + + + New password: + Nové heslo: + + + Confirm password: + Potvrďte heslo: + + + Password must not be empty. + Heslo nesmie byť prázdne. + + + Passwords do not match. + Heslá sa nezhodujú. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Odomknite Agent Redactor + + + Enter your master password to unlock. + Na odomknutie zadajte svoje hlavné heslo. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_sl.ts b/linux/gui/i18n/agentredactor_sl.ts new file mode 100644 index 0000000..c837dcd --- /dev/null +++ b/linux/gui/i18n/agentredactor_sl.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Posodobitev je pripravljena za namestitev + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 je bil prenesen. Zdaj znova zaženite, da uporabite posodobitev. + + + Restart now + Znova zaženi zdaj + + + Later + Pozneje + + + Check for updates + Preveri posodobitve + + + You're up to date. + Imate najnovejšo različico. + + + Couldn't check for updates. Try again later. + Posodobitev ni bilo mogoče preveriti. Poskusite znova pozneje. + + + &File + &Datoteka + + + &Quit + Izhod + + + Profile + Profil + + + Detection + Odkrivanje + + + Regex Patterns + Regex vzorci + + + Keywords + Ključne besede + + + Password + Geslo + + + Statistics + Statistika + + + Session Redactions + Urejanja seje + + + Logs + Dnevniki + + + Settings + Nastavitve + + + Name: + ime: + + + Port: + vrata: + + + Forward To + Posreduj na + + + API Key + API ključ + + + Use AI model: + Uporabite model AI: + + + Confidence threshold: + Prag zaupanja: + + + Add + Dodaj + + + Remove + Odstrani + + + Show API key + Prikaži ključ API + + + Copy proxy URL + Kopiraj URL posrednika + + + Save + Shrani + + + Use AI model for PII detection + Uporabite model AI za odkrivanje PII + + + Case sensitive + Razlikovanje velikih/malih črk + + + Require master password + Zahtevaj glavno geslo + + + Clear statistics + Čista statistika + + + Clear + Počisti + + + Enable logging + Omogoči beleženje + + + Show sensitive information in logs + Prikaži občutljive podatke v dnevnikih + + + Open log file + Odpri dnevniško datoteko + + + Open folder + Odpri mapo + + + Delete all logs + Izbriši vse dnevnike + + + Start on Boot + Začnite pri zagonu + + + Language + Jezik + + + System default + Sistemsko privzeto + + + Master Password + Glavno geslo + + + Unlock + Odkleni + + + Agent Redactor is locked + Agent Redactor je zaklenjen + + + Account number + Številka računa + + + Address + Naslov + + + Date + Datum + + + Email + E-pošta + + + Person + Oseba + + + Phone + Telefon + + + URL + URL + + + Secret + Skrivnost + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Zahteve: %1 PII: %2 Regex: %3 Ključne besede: %4 + + + Engine is not running — retrying… + Motor ne deluje - ponovni poskus ... + + + Delete + Izbriši + + + Validation Error + Napaka pri preverjanju + + + Invalid regex syntax. + Neveljavna regex sintaksa. + + + Case: Yes + Primer: Da + + + Case: No + Zadeva: št + + + Port must be between 1024 and 65535. + Vrata morajo biti med 1024 in 65535. + + + Port %1 is already used by profile '%2'. + Vrata %1 že uporablja profil '%2'. + + + Forward To URL must start with http:// or https://. + URL za posredovanje se mora začeti s http:// ali https://. + + + Confidence threshold must be between 0.0 and 1.0. + Prag zaupanja mora biti med 0,0 in 1,0. + + + Security Warning + Varnostno opozorilo + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Uporabljate HTTP upstream URL (nešifriran). Vaš API ključ bo poslan v čistem besedilu preko omrežja. + + + Error + Napaka + + + The engine rejected the profile. Check the engine log for details. + Motor je zavrnil profil. Za podrobnosti preverite dnevnik motorja. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + Motor je zavrnil nov profil. + + + Remove Profile + Odstrani profil + + + Are you sure? This operation is permanent. + Ste prepričani? Ta operacija je trajna. + + + Proxy URL copied to clipboard + URL posrednika je kopiran v odložišče + + + Wrong password. + Napačno geslo. + + + Show sensitive information + Prikaži občutljive podatke + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Občutljivo beleženje v dnevnik zapiše neobdelane vrednosti (vključno s ključi API-ja). Omogočite ga samo med odpravljanjem napak. + + + Enable logging first. + Najprej omogočite beleženje. + + + Delete all logs? + Izbriši vse dnevnike? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + To bo trajno izbrisalo trenutno dnevniško datoteko in vse arhivirane dnevnike sej. Tega ni mogoče razveljaviti. + + + Downloading AI model + Prenašanje modela UI + + + Retry + Poskusi znova + + + The PII detection model is downloading (%1%). + Model zaznavanja PII se prenaša (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Prenos modela ni uspel. Preverite internetno povezavo in poskusite znova. Zaznavanje PII ni na voljo, dokler prenos ni končan. + + + Are you sure you want to quit? + Ste prepričani, da želite izstopiti? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Če izstopite, Agent Redactor ne bo več nadzoroval in urejal API prometa. + + + Quit Agent Redactor? The engine keeps running in the background. + Zapustiti Agent Redactor? Motor teče v ozadju. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Odpri Agent Redactor + + + Start on Boot + Začnite pri zagonu + + + Language + Jezik + + + Quit + Izhod + + + + PasswordEnableDialog + + Enable password protection + Omogoči zaščito z geslom + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Izberite glavno geslo za Agent Redactor. Ščiti vaše shranjene ključe API na tem računalniku in ni povezan z vašim geslom za prijavo. + + + New password: + Novo geslo: + + + Confirm password: + Potrdite geslo: + + + Password must not be empty. + Geslo ne sme biti prazno. + + + Passwords do not match. + Gesli se ne ujemata. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Odkleni urejevalnik agentov + + + Enter your master password to unlock. + Vnesite glavno geslo za odklepanje. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_sq.ts b/linux/gui/i18n/agentredactor_sq.ts new file mode 100644 index 0000000..d6cc2ec --- /dev/null +++ b/linux/gui/i18n/agentredactor_sq.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Përditësimi është gati për instalim + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 u shkarkua. Riniseni tani për të zbatuar përditësimin. + + + Restart now + Rinis tani + + + Later + Më vonë + + + Check for updates + Kontrollo për përditësime + + + You're up to date. + Keni versionin më të fundit. + + + Couldn't check for updates. Try again later. + Nuk u kontrolluan dot përditësimet. Provoni sërish më vonë. + + + &File + &Skedar + + + &Quit + Dil + + + Profile + Profili + + + Detection + Zbulimi + + + Regex Patterns + Modelet Regex + + + Keywords + Fjalët kyçe + + + Password + Fjalëkalimi + + + Statistics + Statistikat + + + Session Redactions + Redaktimet e seancës + + + Logs + Regjistrat + + + Settings + Cilësimet + + + Name: + Emri: + + + Port: + Porti: + + + Forward To + Përcillo në + + + API Key + Çelësi API + + + Use AI model: + Përdorni modelin e AI: + + + Confidence threshold: + Pragu i besimit: + + + Add + Shto + + + Remove + Hiq + + + Show API key + Shfaq çelësin API + + + Copy proxy URL + Kopjo URL-në e përfaqësuesit + + + Save + Ruaj + + + Use AI model for PII detection + Përdorni modelin e AI për zbulimin e PII + + + Case sensitive + Ndjeshëm ndaj madhësisë së shkronjave + + + Require master password + Kërkoni fjalëkalimin kryesor + + + Clear statistics + Statistikat e qarta + + + Clear + Pastro + + + Enable logging + Aktivizo regjistrimin + + + Show sensitive information in logs + Shfaq informacione të ndjeshme në regjistra + + + Open log file + Hap skedarin e regjistrave + + + Open folder + Hap dosjen + + + Delete all logs + Fshi të gjitha regjistrat + + + Start on Boot + Filloni në Boot + + + Language + Gjuha + + + System default + Parazgjedhja e sistemit + + + Master Password + Fjalëkalimi kryesor + + + Unlock + Shkyç + + + Agent Redactor is locked + Agjenti Redaktori është i kyçur + + + Account number + Numri i llogarisë + + + Address + Adresa + + + Date + Data + + + Email + Email + + + Person + Person + + + Phone + Telefon + + + URL + URL + + + Secret + Sekret + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Kërkesat: %1 PII: %2 Regex: %3 Fjalë kyçe: %4 + + + Engine is not running — retrying… + Motori nuk funksionon - po riprovohet… + + + Delete + Fshije + + + Validation Error + Gabim validimi + + + Invalid regex syntax. + Sintaksë regex e pavlefshme. + + + Case: Yes + Rasti: Po + + + Case: No + Rasti: Jo + + + Port must be between 1024 and 65535. + Porti duhet të jetë midis 1024 dhe 65535. + + + Port %1 is already used by profile '%2'. + Porti %1 është tashmë në përdorim nga profili '%2'. + + + Forward To URL must start with http:// or https://. + URL-ja e përcjelljes duhet të fillojë me http:// ose https://. + + + Confidence threshold must be between 0.0 and 1.0. + Pragu i besueshmërisë duhet të jetë midis 0,0 dhe 1,0. + + + Security Warning + Paralajmërim sigurie + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Po përdorni një URL upstream HTTP (të pakriptuar). Çelësi juaj API do të dërgohet si tekst i thjeshtë nëpër rrjet. + + + Error + Gabim + + + The engine rejected the profile. Check the engine log for details. + Motori refuzoi profilin. Kontrolloni regjistrin e motorit për detaje. + + + Profile %1 + Profili %1 + + + The engine rejected the new profile. + Motori refuzoi profilin e ri. + + + Remove Profile + Hiq profilin + + + Are you sure? This operation is permanent. + Jeni i sigurt? Kjo veprim është e përhershme. + + + Proxy URL copied to clipboard + URL-ja e përfaqësuesit u kopjua në kujtesën e fragmenteve + + + Wrong password. + Fjalëkalim i gabuar. + + + Show sensitive information + Trego informacione të ndjeshme + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Regjistrimi i ndjeshëm shkruan vlera të papërpunuara, të pa redaktuara (përfshirë çelësat API) në regjistër. Aktivizoni atë vetëm gjatë korrigjimit. + + + Enable logging first. + Aktivizo fillimisht regjistrimin. + + + Delete all logs? + Fshi të gjitha regjistrat? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Kjo do të fshijë përgjithmonë skedarin aktual të regjistrave dhe të gjitha regjistrat e arkivuara të seancave. Nuk mund të zhbëhet. + + + Downloading AI model + Duke shkarkuar modelin e IA-së + + + Retry + Provo sërish + + + The PII detection model is downloading (%1%). + Modeli i zbulimit të PII po shkarkohet (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Shkarkimi i modelit dështoi. Kontrolloni lidhjen tuaj të internetit dhe provoni sërish. Zbulimi i PII nuk është i disponueshëm derisa shkarkimi të përfundojë. + + + Are you sure you want to quit? + Jeni i sigurt që doni të dilni? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Nëse dilni, Agent Redactor nuk do të monitorojë dhe redaktojë më trafikun API. + + + Quit Agent Redactor? The engine keeps running in the background. + Të largohesh nga redaktori i agjentit? Motori vazhdon të funksionojë në sfond. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Hap Agent Redactor + + + Start on Boot + Filloni në Boot + + + Language + Gjuha + + + Quit + Dil + + + + PasswordEnableDialog + + Enable password protection + Aktivizo mbrojtjen me fjalëkalim + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Zgjidhni një fjalëkalim kryesor për Agent Redactor. Ai mbron çelësat tuaj të ruajtur API në këtë pajisje dhe nuk ka lidhje me fjalëkalimin tuaj të hyrjes. + + + New password: + Fjalëkalimi i ri: + + + Confirm password: + Konfirmo fjalëkalimin: + + + Password must not be empty. + Fjalëkalimi nuk duhet të jetë bosh. + + + Passwords do not match. + Fjalëkalimet nuk përputhen. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Zhbllokoni Redaktorin e Agjentit + + + Enter your master password to unlock. + Futni fjalëkalimin tuaj kryesor për ta zhbllokuar. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_sr_Latn.ts b/linux/gui/i18n/agentredactor_sr_Latn.ts new file mode 100644 index 0000000..3084614 --- /dev/null +++ b/linux/gui/i18n/agentredactor_sr_Latn.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Ažuriranje je spremno za instalaciju + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 je preuzet. Restartujte sada da biste primenili ažuriranje. + + + Restart now + Restartuj sada + + + Later + Kasnije + + + Check for updates + Proveri ažuriranja + + + You're up to date. + Imate najnoviju verziju. + + + Couldn't check for updates. Try again later. + Nije moguće proveriti ažuriranja. Pokušajte ponovo kasnije. + + + &File + &Datoteka + + + &Quit + Izađi + + + Profile + Profil + + + Detection + Detection + + + Regex Patterns + Regex obrasci + + + Keywords + Ključne reči + + + Password + Lozinka + + + Statistics + Statistika + + + Session Redactions + Maskiranja u sesiji + + + Logs + Dnevnici + + + Settings + Podešavanja + + + Name: + ime: + + + Port: + Port: + + + Forward To + Prosledi na + + + API Key + API ključ + + + Use AI model: + Koristite AI model: + + + Confidence threshold: + Prag poverenja: + + + Add + Dodaj + + + Remove + Ukloni + + + Show API key + Prikaži API ključ + + + Copy proxy URL + Kopiraj URL proksija + + + Save + Sačuvaj + + + Use AI model for PII detection + Koristite AI model za otkrivanje PII + + + Case sensitive + Razlikuj velika i mala slova + + + Require master password + Zahtevaj glavnu lozinku + + + Clear statistics + Jasna statistika + + + Clear + Očisti + + + Enable logging + Omogući evidentiranje + + + Show sensitive information in logs + Prikaži osetljive informacije u evidenciji + + + Open log file + Otvori datoteku dnevnika + + + Open folder + Otvori fasciklu + + + Delete all logs + Obriši sve dnevnike + + + Start on Boot + Počnite pri pokretanju + + + Language + Jezik + + + System default + Sistemsko podrazumevano + + + Master Password + Glavna lozinka + + + Unlock + Otključaj + + + Agent Redactor is locked + Agent Redactor je zaključan + + + Account number + Broj računa + + + Address + Adresa + + + Date + Datum + + + Email + E-pošta + + + Person + Osoba + + + Phone + Telefon + + + URL + URL + + + Secret + Tajna + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Zahtevi: %1 PII: %2 Redovni izraz: %3 Ključne reči: %4 + + + Engine is not running — retrying… + Motor ne radi — pokušavam ponovo… + + + Delete + Izbriši + + + Validation Error + Greška validacije + + + Invalid regex syntax. + Nevažeća regex sintaksa. + + + Case: Yes + Slučaj: Da + + + Case: No + Slučaj: Ne + + + Port must be between 1024 and 65535. + Port mora biti između 1024 i 65535. + + + Port %1 is already used by profile '%2'. + Port %1 već koristi profil '%2'. + + + Forward To URL must start with http:// or https://. + URL za prosleđivanje mora počinjati sa http:// ili https://. + + + Confidence threshold must be between 0.0 and 1.0. + Prag poverenja mora biti između 0,0 i 1,0. + + + Security Warning + Bezbednosno upozorenje + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Koristite HTTP upstream URL (nekriptovan). Vaš API ključ biće poslat kao običan tekst preko mreže. + + + Error + Greška + + + The engine rejected the profile. Check the engine log for details. + Motor je odbio profil. Proverite dnevnik motora za detalje. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + Motor je odbio novi profil. + + + Remove Profile + Ukloni profil + + + Are you sure? This operation is permanent. + Da li ste sigurni? Ova operacija je trajna. + + + Proxy URL copied to clipboard + URL proksija je kopiran u međuspremnik + + + Wrong password. + Pogrešna lozinka. + + + Show sensitive information + Prikaži osetljive informacije + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Osetljivo evidentiranje upisuje neobrađene, nepromenjene vrednosti (uključujući API ključeve) u evidenciju. Omogućite ga samo tokom otklanjanja grešaka. + + + Enable logging first. + Prvo omogućite evidentiranje. + + + Delete all logs? + Obrisati sve dnevnike? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Ovo će trajno obrisati trenutnu datoteku dnevnika i sve arhivirane dnevnike sesija. Ne može se poništiti. + + + Downloading AI model + Preuzimanje AI modela + + + Retry + Pokušaj ponovo + + + The PII detection model is downloading (%1%). + Model otkrivanja PII se preuzima (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Preuzimanje modela nije uspelo. Proverite internet konekciju, a zatim pokušajte ponovo. Otkrivanje PII podataka nije dostupno dok se preuzimanje ne završi. + + + Are you sure you want to quit? + Da li ste sigurni da želite da izađete? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Ako izađete, Agent Redactor više neće nadzirati i maskirati API saobraćaj. + + + Quit Agent Redactor? The engine keeps running in the background. + Napustiti Agent Redactor? Motor nastavlja da radi u pozadini. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Otvori Agent Redactor + + + Start on Boot + Počnite pri pokretanju + + + Language + Jezik + + + Quit + Izađi + + + + PasswordEnableDialog + + Enable password protection + Omogućite zaštitu lozinkom + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Izaberite glavnu lozinku za Agent Redactor. On štiti vaše sačuvane API ključeve na ovoj mašini i nije povezan sa vašom lozinkom za prijavu. + + + New password: + Nova lozinka: + + + Confirm password: + Potvrdite lozinku: + + + Password must not be empty. + Lozinka ne sme biti prazna. + + + Passwords do not match. + Lozinke se ne poklapaju. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Otključajte Agent Redactor + + + Enter your master password to unlock. + Unesite svoju glavnu lozinku za otključavanje. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_sv.ts b/linux/gui/i18n/agentredactor_sv.ts new file mode 100644 index 0000000..9ee5ab6 --- /dev/null +++ b/linux/gui/i18n/agentredactor_sv.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Uppdatering redo att installeras + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 har laddats ned. Starta om nu för att tillämpa uppdateringen. + + + Restart now + Starta om nu + + + Later + Senare + + + Check for updates + Sök efter uppdateringar + + + You're up to date. + Du har den senaste versionen. + + + Couldn't check for updates. Try again later. + Det gick inte att söka efter uppdateringar. Försök igen senare. + + + &File + &Fil + + + &Quit + Avsluta + + + Profile + Profil + + + Detection + Upptäckt + + + Regex Patterns + Regex-mönster + + + Keywords + Nyckelord + + + Password + Lösenord + + + Statistics + Statistik + + + Session Redactions + Sessionsredigeringar + + + Logs + Loggar + + + Settings + Inställningar + + + Name: + Namn: + + + Port: + Hamn: + + + Forward To + Vidarebefordra till + + + API Key + API-nyckel + + + Use AI model: + Använd AI-modell: + + + Confidence threshold: + Förtroendetröskel: + + + Add + Lägg till + + + Remove + Ta bort + + + Show API key + Visa API-nyckel + + + Copy proxy URL + Kopiera proxy-URL + + + Save + Spara + + + Use AI model for PII detection + Använd AI-modell för PII-detektion + + + Case sensitive + Skiftlägeskänslig + + + Require master password + Kräv huvudlösenord + + + Clear statistics + Tydlig statistik + + + Clear + Rensa + + + Enable logging + Aktivera loggning + + + Show sensitive information in logs + Visa känslig information i loggar + + + Open log file + Öppna loggfil + + + Open folder + Öppna mapp + + + Delete all logs + Ta bort alla loggar + + + Start on Boot + Börja på Boot + + + Language + Språk + + + System default + Systemstandard + + + Master Password + Huvudlösenord + + + Unlock + Lås upp + + + Agent Redactor is locked + Agent Redactor är låst + + + Account number + Kontonummer + + + Address + Adress + + + Date + Datum + + + Email + E-post + + + Person + Person + + + Phone + Telefon + + + URL + URL + + + Secret + Hemlighet + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Förfrågningar: %1 PII: %2 Regex: %3 Nyckelord: %4 + + + Engine is not running — retrying… + Motorn går inte – försöker igen... + + + Delete + Radera + + + Validation Error + Valideringsfel + + + Invalid regex syntax. + Ogiltig regex-syntax. + + + Case: Yes + Fall: Ja + + + Case: No + Fall: Nej + + + Port must be between 1024 and 65535. + Porten måste vara mellan 1024 och 65535. + + + Port %1 is already used by profile '%2'. + Port %1 används redan av profilen '%2'. + + + Forward To URL must start with http:// or https://. + Vidarebefordrings-URL måste börja med http:// eller https://. + + + Confidence threshold must be between 0.0 and 1.0. + Konfidensgränsen måste vara mellan 0,0 och 1,0. + + + Security Warning + Säkerhetsvarning + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Du använder en HTTP-upstream-URL (okrypterad). Din API-nyckel skickas i klartext över nätverket. + + + Error + Fel + + + The engine rejected the profile. Check the engine log for details. + Motorn avvisade profilen. Kontrollera motorloggen för detaljer. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + Motorn avvisade den nya profilen. + + + Remove Profile + Ta bort profil + + + Are you sure? This operation is permanent. + Är du säker? Denna åtgärd kan inte ångras. + + + Proxy URL copied to clipboard + Proxy-URL kopierad till urklipp + + + Wrong password. + Fel lösenord. + + + Show sensitive information + Visa känslig information + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Känslig loggning skriver råa, oredigerade värden (inklusive API-nycklar) till loggen. Aktivera det bara under felsökning. + + + Enable logging first. + Aktivera loggning först. + + + Delete all logs? + Ta bort alla loggar? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Detta tar permanent bort den aktuella loggfilen och alla arkiverade sessionsloggar. Detta kan inte ångras. + + + Downloading AI model + Laddar ned AI-modell + + + Retry + Försök igen + + + The PII detection model is downloading (%1%). + PII-detekteringsmodellen laddas ned (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Nedladdningen av modellen misslyckades. Kontrollera din internetanslutning och försök igen. PII-identifiering är inte tillgänglig förrän nedladdningen är klar. + + + Are you sure you want to quit? + Är du säker på att du vill avsluta? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Om du avslutar kommer Agent Redactor inte längre att övervaka och redigera API-trafik. + + + Quit Agent Redactor? The engine keeps running in the background. + Avsluta Agent Redactor? Motorn fortsätter att gå i bakgrunden. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Öppna Agent Redactor + + + Start on Boot + Börja på Boot + + + Language + Språk + + + Quit + Avsluta + + + + PasswordEnableDialog + + Enable password protection + Aktivera lösenordsskydd + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Välj ett huvudlösenord för Agent Redactor. Det skyddar dina lagrade API-nycklar på den här maskinen och är inte relaterat till ditt inloggningslösenord. + + + New password: + Nytt lösenord: + + + Confirm password: + Bekräfta lösenord: + + + Password must not be empty. + Lösenordet får inte vara tomt. + + + Passwords do not match. + Lösenord stämmer inte överens. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Lås upp Agent Redactor + + + Enter your master password to unlock. + Ange ditt huvudlösenord för att låsa upp. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_sw.ts b/linux/gui/i18n/agentredactor_sw.ts new file mode 100644 index 0000000..ada2964 --- /dev/null +++ b/linux/gui/i18n/agentredactor_sw.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Sasisho tayari kusakinishwa + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 limepakuliwa. Anzisha upya sasa ili kutumia sasisho. + + + Restart now + Anzisha upya sasa + + + Later + Baadaye + + + Check for updates + Angalia sasisho + + + You're up to date. + Una toleo jipya zaidi. + + + Couldn't check for updates. Try again later. + Haikuweza kuangalia sasisho. Jaribu tena baadaye. + + + &File + &Faili + + + &Quit + Toka + + + Profile + Wasifu + + + Detection + Ugunduzi + + + Regex Patterns + Mistarifa ya Regex + + + Keywords + Maneno Muhimu + + + Password + Nenosiri + + + Statistics + Takwimu + + + Session Redactions + Kufutwa kwa Kipindi + + + Logs + Kumbukumbu + + + Settings + Mipangilio + + + Name: + Jina: + + + Port: + Bandari: + + + Forward To + Sambaza Kwa + + + API Key + Ufunguo wa API + + + Use AI model: + Tumia muundo wa AI: + + + Confidence threshold: + Kiwango cha Kujiamini: + + + Add + Ongeza + + + Remove + Ondoa + + + Show API key + Onyesha ufunguo wa API + + + Copy proxy URL + Nakili URL ya seva mbadala + + + Save + Hifadhi + + + Use AI model for PII detection + Tumia muundo wa AI kwa utambuzi wa PII + + + Case sensitive + Kutofautisha herufi kubwa/ndogo + + + Require master password + Inahitaji nenosiri kuu + + + Clear statistics + Takwimu wazi + + + Clear + Futa + + + Enable logging + Washa kumbukumbu + + + Show sensitive information in logs + Onyesha taarifa nyeti kwenye kumbukumbu + + + Open log file + Fungua faili la kumbukumbu + + + Open folder + Fungua folda + + + Delete all logs + Futa kumbukumbu zote + + + Start on Boot + Anza kwenye Boot + + + Language + Lugha + + + System default + Chaguo-msingi cha mfumo + + + Master Password + Nenosiri Kuu + + + Unlock + Fungua + + + Agent Redactor is locked + Wakala Redactor imefungwa + + + Account number + Nambari ya akaunti + + + Address + Anwani + + + Date + Tarehe + + + Email + Barua pepe + + + Person + Mtu + + + Phone + Simu + + + URL + URL + + + Secret + Siri + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Maombi: %1 PII: %2 Regex: %3 Maneno Muhimu: %4 + + + Engine is not running — retrying… + Injini haifanyi kazi - inajaribu tena... + + + Delete + Futa + + + Validation Error + Hitilafu ya Uhalalishaji + + + Invalid regex syntax. + Mtiririko wa regex usiosahihi. + + + Case: Yes + Kesi: Ndiyo + + + Case: No + Kesi: Hapana + + + Port must be between 1024 and 65535. + Bandari lazima iwe kati ya 1024 na 65535. + + + Port %1 is already used by profile '%2'. + Bandari %1 tayari inatumika na wasifu '%2'. + + + Forward To URL must start with http:// or https://. + URL ya Sambaza Kwa lazima ianze na http:// au https://. + + + Confidence threshold must be between 0.0 and 1.0. + Kizingiti cha ujasiri lazima kiwe kati ya 0.0 na 1.0. + + + Security Warning + Onyo la Usalama + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Unatumia URL ya upstream ya HTTP (isiyosimbwa). Ufunguo wako wa API utatumwa kama maandishi ghafi kupitia mtandao. + + + Error + Hitilafu + + + The engine rejected the profile. Check the engine log for details. + Injini ilikataa wasifu. Angalia logi ya injini kwa maelezo. + + + Profile %1 + Wasifu %1 + + + The engine rejected the new profile. + Injini ilikataa wasifu mpya. + + + Remove Profile + Ondoa Wasifu + + + Are you sure? This operation is permanent. + Una uhakika? Uendeshaji huu ni wa kudumu. + + + Proxy URL copied to clipboard + URL ya seva mbadala imenakiliwa kwenye ubao wa kunakili + + + Wrong password. + Nenosiri si sahihi. + + + Show sensitive information + Onyesha taarifa nyeti + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Uwekaji kumbukumbu nyeti huandika thamani ghafi, ambazo hazijarekebishwa (pamoja na vitufe vya API) kwenye kumbukumbu. Iwashe tu wakati unatatua. + + + Enable logging first. + Washa kumbukumbu kwanza. + + + Delete all logs? + Futa kumbukumbu zote? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Hii itafuta faili la kumbukumbu la sasa na kumbukumbu zote za vikao vilivyohifadhiwa kwa kudumu. Haiwezi kufutwa. + + + Downloading AI model + Inapakua modeli ya AI + + + Retry + Jaribu tena + + + The PII detection model is downloading (%1%). + Muundo wa utambuzi wa PII unapakuliwa (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Upakuaji wa modeli umeshindwa. Angalia muunganisho wako wa intaneti, kisha jaribu tena. Utambuzi wa PII haupatikani hadi upakuaji ukamilike. + + + Are you sure you want to quit? + Una uhakika unataka kutoka? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Ukitoka, Agent Redactor haitaendelea kufuatilia wala kufuta trafiki ya API. + + + Quit Agent Redactor? The engine keeps running in the background. + Je, ungependa kuachana na Kirekebishaji cha Wakala? Injini inaendelea kufanya kazi kwa nyuma. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Fungua Agent Redactor + + + Start on Boot + Anza kwenye Boot + + + Language + Lugha + + + Quit + Toka + + + + PasswordEnableDialog + + Enable password protection + Washa ulinzi wa nenosiri + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Chagua nenosiri kuu la Redactor ya Wakala. Hulinda funguo zako za API zilizohifadhiwa kwenye mashine hii na haihusiani na nenosiri lako la kuingia. + + + New password: + Nenosiri jipya: + + + Confirm password: + Thibitisha nenosiri: + + + Password must not be empty. + Nenosiri lazima lisiwe tupu. + + + Passwords do not match. + Manenosiri hayalingani. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Fungua Redactor ya Wakala + + + Enter your master password to unlock. + Weka nenosiri lako kuu ili kufungua. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_ta.ts b/linux/gui/i18n/agentredactor_ta.ts new file mode 100644 index 0000000..93b241e --- /dev/null +++ b/linux/gui/i18n/agentredactor_ta.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + புதுப்பிப்பு நிறுவ தயார் + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 பதிவிறக்கப்பட்டது. புதுப்பிப்பைப் பயன்படுத்த இப்போது மீண்டும் தொடங்கவும். + + + Restart now + இப்போது மீண்டும் தொடங்கு + + + Later + பின்னர் + + + Check for updates + புதுப்பிப்புகளைச் சரிபார் + + + You're up to date. + நீங்கள் புதுப்பித்த நிலையில் உள்ளீர்கள். + + + Couldn't check for updates. Try again later. + புதுப்பிப்புகளைச் சரிபார்க்க முடியவில்லை. பின்னர் மீண்டும் முயற்சிக்கவும். + + + &File + &கோப்பு + + + &Quit + வெளியேறு + + + Profile + சுயவிவரம் + + + Detection + கண்டறிதல் + + + Regex Patterns + Regex முறைகள் + + + Keywords + முக்கியசொற்கள் + + + Password + கடவுச்சொல் + + + Statistics + புள்ளிவிவரங்கள் + + + Session Redactions + அமர்வு மறைப்புகள் + + + Logs + பதிவுகள் + + + Settings + அமைப்புகள் + + + Name: + பெயர்: + + + Port: + துறைமுகம்: + + + Forward To + முன்னோக்கி அனுப்பு + + + API Key + API விசை + + + Use AI model: + AI மாதிரியைப் பயன்படுத்தவும்: + + + Confidence threshold: + நம்பிக்கை வரம்பு: + + + Add + சேர் + + + Remove + அகற்று + + + Show API key + API விசையைக் காட்டு + + + Copy proxy URL + ப்ராக்ஸி URL ஐ நகலெடுக்கவும் + + + Save + சேமிக்கவும் + + + Use AI model for PII detection + PII கண்டறிதலுக்கு AI மாதிரியைப் பயன்படுத்தவும் + + + Case sensitive + எழுத்துப்பிரிவு உணர்திறன் + + + Require master password + முதன்மை கடவுச்சொல் தேவை + + + Clear statistics + தெளிவான புள்ளிவிவரங்கள் + + + Clear + அழி + + + Enable logging + பதிவு செய்வதை இயக்கு + + + Show sensitive information in logs + பதிவுகளில் முக்கியமான தகவலைக் காட்டு + + + Open log file + பதிவு கோப்பைத் திற + + + Open folder + கோப்புறையைத் திற + + + Delete all logs + அனைத்து பதிவுகளையும் நீக்கு + + + Start on Boot + துவக்கத்தில் தொடங்கவும் + + + Language + மொழி + + + System default + கணினி இயல்புநிலை + + + Master Password + முதன்மை கடவுச்சொல் + + + Unlock + பூட்டைத் திற + + + Agent Redactor is locked + ஏஜென்ட் ரெடாக்டர் பூட்டப்பட்டுள்ளது + + + Account number + கணக்கு எண் + + + Address + முகவரி + + + Date + தேதி + + + Email + மின்னஞ்சல் + + + Person + நபர் + + + Phone + தொலைபேசி + + + URL + URL + + + Secret + ரகசியம் + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + கோரிக்கைகள்: %1 PII:%2 Regex:%3 முக்கிய வார்த்தைகள்:%4 + + + Engine is not running — retrying… + எஞ்சின் இயங்கவில்லை - மீண்டும் முயற்சிக்கிறது... + + + Delete + நீக்கு + + + Validation Error + சரிபார்ப்புப் பிழை + + + Invalid regex syntax. + தவறான regex தொடரமைப்பு. + + + Case: Yes + வழக்கு: ஆம் + + + Case: No + வழக்கு: இல்லை + + + Port must be between 1024 and 65535. + போர்ட் 1024 மற்றும் 65535 க்கு இடையில் இருக்க வேண்டும். + + + Port %1 is already used by profile '%2'. + போர்ட் %1 ஏற்கனவே '%2' சுயவிவரத்தால் பயன்படுத்தப்படுகிறது. + + + Forward To URL must start with http:// or https://. + Forward To URL http:// அல்லது https:// என தொடங்க வேண்டும். + + + Confidence threshold must be between 0.0 and 1.0. + நம்பகத்தன்மை வரம்பு 0.0 மற்றும் 1.0 க்கு இடையில் இருக்க வேண்டும். + + + Security Warning + பாதுகாப்பு எச்சரிக்கை + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + நீங்கள் HTTP (மறைக்கப்படாத) அப்ஸ்ட்ரீம் URL ஐப் பயன்படுத்துகிறீர்கள். உங்கள் API விசை பிளேன் டெக்ஸ்ட் மூலம் பிணையத்தில் அனுப்பப்படும். + + + Error + பிழை + + + The engine rejected the profile. Check the engine log for details. + இயந்திரம் சுயவிவரத்தை நிராகரித்தது. விவரங்களுக்கு என்ஜின் பதிவைச் சரிபார்க்கவும். + + + Profile %1 + சுயவிவரம்%1 + + + The engine rejected the new profile. + இயந்திரம் புதிய சுயவிவரத்தை நிராகரித்தது. + + + Remove Profile + சுயவிவரத்தை அகற்று + + + Are you sure? This operation is permanent. + நீங்கள் உறுதியாக இருக்கிறீர்களா? இந்த செயல் நிரந்தரமானது. + + + Proxy URL copied to clipboard + ப்ராக்ஸி URL கிளிப்போர்டுக்கு நகலெடுக்கப்பட்டது + + + Wrong password. + தவறான கடவுச்சொல். + + + Show sensitive information + முக்கியமான தகவலைக் காட்டு + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + சென்சிடிவ் லாக்கிங் மூல, திருத்தப்படாத மதிப்புகளை (API விசைகள் உட்பட) பதிவில் எழுதுகிறது. பிழைத்திருத்தத்தின் போது மட்டுமே அதை இயக்கவும். + + + Enable logging first. + முதலில் உள்நுழைவை இயக்கவும். + + + Delete all logs? + அனைத்து பதிவுகளையும் நீக்கவா? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + இது தற்போதைய பதிவு கோப்பு மற்றும் அனைத்து காப்பகப்படுத்தப்பட்ட அமர்வு பதிவுகளையும் நிரந்தரமாக நீக்கும். இதை செயல்தவிர்க்க முடியாது. + + + Downloading AI model + AI மாதிரியைப் பதிவிறக்குகிறது + + + Retry + மீண்டும் முயற்சி + + + The PII detection model is downloading (%1%). + PII கண்டறிதல் மாதிரி பதிவிறக்கம் செய்யப்படுகிறது (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + மாதிரி பதிவிறக்கம் தோல்வியடைந்தது. உங்கள் இணைய இணைப்பைச் சரிபார்த்து மீண்டும் முயற்சிக்கவும். பதிவிறக்கம் முடியும் வரை PII கண்டறிதல் கிடைக்காது. + + + Are you sure you want to quit? + நீங்கள் வெளியேற விரும்புகிறீர்களா? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + நீங்கள் வெளியேறினால், Agent Redactor API போக்குவரத்தை கண்காணித்து மறைக்க மாட்டாது. + + + Quit Agent Redactor? The engine keeps running in the background. + ஏஜென்ட் ரெடாக்டரை விட்டு வெளியேறவா? எஞ்சின் பின்னணியில் இயங்கிக் கொண்டே இருக்கும். + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Agent Redactor ஐத் திற + + + Start on Boot + துவக்கத்தில் தொடங்கவும் + + + Language + மொழி + + + Quit + வெளியேறு + + + + PasswordEnableDialog + + Enable password protection + கடவுச்சொல் பாதுகாப்பை இயக்கவும் + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + முகவர் ரெடாக்டருக்கான முதன்மை கடவுச்சொல்லைத் தேர்வு செய்யவும். இது இந்த கணினியில் உங்கள் சேமிக்கப்பட்ட API விசைகளைப் பாதுகாக்கிறது மற்றும் உங்கள் உள்நுழைவு கடவுச்சொல்லுடன் தொடர்பில்லாதது. + + + New password: + புதிய கடவுச்சொல்: + + + Confirm password: + கடவுச்சொல்லை உறுதிப்படுத்தவும்: + + + Password must not be empty. + கடவுச்சொல் காலியாக இருக்கக்கூடாது. + + + Passwords do not match. + கடவுச்சொற்கள் பொருந்தவில்லை. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + ஏஜென்ட் ரெடாக்டரைத் திறக்கவும் + + + Enter your master password to unlock. + திறக்க உங்கள் முதன்மை கடவுச்சொல்லை உள்ளிடவும். + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_th.ts b/linux/gui/i18n/agentredactor_th.ts new file mode 100644 index 0000000..b1abee3 --- /dev/null +++ b/linux/gui/i18n/agentredactor_th.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + การอัปเดตพร้อมติดตั้ง + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + ดาวน์โหลด Agent Redactor %1 แล้ว รีสตาร์ทตอนนี้เพื่อใช้การอัปเดต + + + Restart now + รีสตาร์ทตอนนี้ + + + Later + ภายหลัง + + + Check for updates + ตรวจสอบการอัปเดต + + + You're up to date. + คุณใช้เวอร์ชันล่าสุดอยู่แล้ว + + + Couldn't check for updates. Try again later. + ไม่สามารถตรวจสอบการอัปเดตได้ ลองอีกครั้งในภายหลัง + + + &File + &ไฟล์ + + + &Quit + ออก + + + Profile + ประวัติโดยย่อ + + + Detection + การตรวจจับ + + + Regex Patterns + รูปแบบ Regex + + + Keywords + คำสำคัญ + + + Password + รหัสผ่าน + + + Statistics + สถิติ + + + Session Redactions + การลดความละเอียดในเซสชัน + + + Logs + บันทึก + + + Settings + การตั้งค่า + + + Name: + ชื่อ: + + + Port: + ท่าเรือ: + + + Forward To + ส่งต่อไปยัง + + + API Key + คีย์ API + + + Use AI model: + ใช้โมเดล AI: + + + Confidence threshold: + เกณฑ์ความเชื่อมั่น: + + + Add + เพิ่ม + + + Remove + ลบ + + + Show API key + แสดงคีย์ API + + + Copy proxy URL + คัดลอก URL พร็อกซี + + + Save + บันทึก + + + Use AI model for PII detection + ใช้โมเดล AI สำหรับการตรวจจับ PII + + + Case sensitive + แยกแยะตัวพิมพ์เล็ก/ใหญ่ + + + Require master password + ต้องใช้รหัสผ่านหลัก + + + Clear statistics + สถิติที่ชัดเจน + + + Clear + ล้าง + + + Enable logging + เปิดใช้งานการบันทึก + + + Show sensitive information in logs + แสดงข้อมูลที่ละเอียดอ่อนในบันทึก + + + Open log file + เปิดไฟล์บันทึก + + + Open folder + เปิดโฟลเดอร์ + + + Delete all logs + ลบบันทึกทั้งหมด + + + Start on Boot + เริ่มที่บูท + + + Language + ภาษา + + + System default + ค่าเริ่มต้นของระบบ + + + Master Password + รหัสผ่านหลัก + + + Unlock + ปลดล็อก + + + Agent Redactor is locked + Agent Redactor ถูกล็อค + + + Account number + หมายเลขบัญชี + + + Address + ที่อยู่ + + + Date + วันที่ + + + Email + อีเมล + + + Person + บุคคล + + + Phone + โทรศัพท์ + + + URL + URL + + + Secret + ความลับ + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + คำขอ: %1 PII: %2 Regex: %3 คำสำคัญ: %4 + + + Engine is not running — retrying… + เครื่องยนต์ไม่ทำงาน — กำลังลองอีกครั้ง... + + + Delete + ลบ + + + Validation Error + ข้อผิดพลาดในการตรวจสอบ + + + Invalid regex syntax. + รูปแบบ Regex ไม่ถูกต้อง + + + Case: Yes + กรณี: ใช่ + + + Case: No + กรณี: ไม่ใช่ + + + Port must be between 1024 and 65535. + พอร์ตต้องอยู่ระหว่าง 1024 และ 65535 + + + Port %1 is already used by profile '%2'. + พอร์ต %1 ถูกใช้โดยโปรไฟล์ '%2' แล้ว + + + Forward To URL must start with http:// or https://. + URL ส่งต่อไปยังต้องขึ้นต้นด้วย http:// หรือ https:// + + + Confidence threshold must be between 0.0 and 1.0. + เกณฑ์ความเชื่อมั่นต้องอยู่ระหว่าง 0.0 และ 1.0 + + + Security Warning + คำเตือนด้านความปลอดภัย + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + คุณกำลังใช้ URL อัปสตรีม HTTP (ไม่ได้เข้ารหัส) คีย์ API ของคุณจะถูกส่งเป็นข้อความธรรมดาผ่านเครือข่าย + + + Error + ข้อผิดพลาด + + + The engine rejected the profile. Check the engine log for details. + เครื่องยนต์ปฏิเสธโปรไฟล์ ตรวจสอบบันทึกเครื่องยนต์เพื่อดูรายละเอียด + + + Profile %1 + โปรไฟล์ %1 + + + The engine rejected the new profile. + เครื่องยนต์ปฏิเสธโปรไฟล์ใหม่ + + + Remove Profile + ลบโปรไฟล์ + + + Are you sure? This operation is permanent. + คุณแน่ใจหรือไม่? การดำเนินการนี้ไม่สามารถย้อนกลับได้ + + + Proxy URL copied to clipboard + คัดลอก URL พร็อกซีไปยังคลิปบอร์ดแล้ว + + + Wrong password. + รหัสผ่านผิด + + + Show sensitive information + แสดงข้อมูลที่ละเอียดอ่อน + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + การบันทึกที่ละเอียดอ่อนจะเขียนค่าดิบที่ยังไม่ได้แก้ไข (รวมถึงคีย์ API) ลงในบันทึก เปิดใช้งานเฉพาะในขณะที่ทำการดีบักเท่านั้น + + + Enable logging first. + เปิดใช้งานการบันทึกก่อน + + + Delete all logs? + ลบบันทึกทั้งหมด? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + การดำเนินการนี้จะลบไฟล์บันทึกปัจจุบันและบันทึกเซสชันที่เก็บถาวรทั้งหมดอย่างถาวร ไม่สามารถย้อนกลับได้ + + + Downloading AI model + กำลังดาวน์โหลดโมเดล AI + + + Retry + ลองอีกครั้ง + + + The PII detection model is downloading (%1%). + กำลังดาวน์โหลดโมเดลการตรวจจับ PII (%1%) + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + การดาวน์โหลดโมเดลล้มเหลว ตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณ แล้วลองอีกครั้ง การตรวจจับ PII จะไม่พร้อมใช้งานจนกว่าการดาวน์โหลดจะเสร็จสมบูรณ์ + + + Are you sure you want to quit? + คุณแน่ใจหรือไม่ว่าต้องการออก? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + หากคุณออก Agent Redactor จะไม่ตรวจสอบและลดความละเอียดของการรับส่งข้อมูล API อีกต่อไป + + + Quit Agent Redactor? The engine keeps running in the background. + ออกจาก Agent Redactor หรือไม่ เครื่องยนต์ยังคงทำงานอยู่เบื้องหลัง + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + เปิด Agent Redactor + + + Start on Boot + เริ่มที่บูท + + + Language + ภาษา + + + Quit + ออก + + + + PasswordEnableDialog + + Enable password protection + เปิดใช้งานการป้องกันด้วยรหัสผ่าน + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + เลือกรหัสผ่านหลักสำหรับ Agent Redactor จะปกป้องคีย์ API ที่จัดเก็บไว้ในเครื่องนี้และไม่เกี่ยวข้องกับรหัสผ่านเข้าสู่ระบบของคุณ + + + New password: + รหัสผ่านใหม่: + + + Confirm password: + ยืนยันรหัสผ่าน: + + + Password must not be empty. + รหัสผ่านต้องไม่เว้นว่าง + + + Passwords do not match. + รหัสผ่านไม่ตรงกัน + + + + PasswordUnlockDialog + + Unlock Agent Redactor + ปลดล็อค Agent Redactor + + + Enter your master password to unlock. + ป้อนรหัสผ่านหลักของคุณเพื่อปลดล็อค + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_tr.ts b/linux/gui/i18n/agentredactor_tr.ts new file mode 100644 index 0000000..adc9187 --- /dev/null +++ b/linux/gui/i18n/agentredactor_tr.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Güncelleştirme yüklenmeye hazır + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 indirildi. Güncelleştirmeyi uygulamak için şimdi yeniden başlatın. + + + Restart now + Şimdi yeniden başlat + + + Later + Daha sonra + + + Check for updates + Güncelleştirmeleri denetle + + + You're up to date. + Güncel durumdasınız. + + + Couldn't check for updates. Try again later. + Güncelleştirmeler denetlenemedi. Daha sonra yeniden deneyin. + + + &File + &Dosya + + + &Quit + Çık + + + Profile + Profil + + + Detection + Algılama + + + Regex Patterns + Regex Desenleri + + + Keywords + Anahtar Kelimeler + + + Password + Parola + + + Statistics + İstatistikler + + + Session Redactions + Oturum Düzenlemeleri + + + Logs + Günlükler + + + Settings + Ayarlar + + + Name: + İsim: + + + Port: + Liman: + + + Forward To + Yönlendir + + + API Key + API Anahtarı + + + Use AI model: + Yapay zeka modelini kullanın: + + + Confidence threshold: + Güven eşiği: + + + Add + Ekle + + + Remove + Kaldır + + + Show API key + API anahtarını göster + + + Copy proxy URL + Proxy URL'sini kopyala + + + Save + Kaydetmek + + + Use AI model for PII detection + Kimlik bilgileri tespiti için AI modelini kullanın + + + Case sensitive + Büyük/küçük harf duyarlı + + + Require master password + Ana şifre gerektir + + + Clear statistics + İstatistikleri temizle + + + Clear + Temizle + + + Enable logging + Günlüğe kaydetmeyi etkinleştir + + + Show sensitive information in logs + Günlüklerde hassas bilgileri göster + + + Open log file + Günlük dosyasını aç + + + Open folder + Klasörü aç + + + Delete all logs + Tüm günlükleri sil + + + Start on Boot + Önyüklemede Başlat + + + Language + Dil + + + System default + Sistem varsayılanı + + + Master Password + Ana Parola + + + Unlock + Kilidi Aç + + + Agent Redactor is locked + Ajan Redaktörü kilitli + + + Account number + Hesap numarası + + + Address + Adres + + + Date + Tarih + + + Email + E-posta + + + Person + Kişi + + + Phone + Telefon + + + URL + URL + + + Secret + Gizli + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + İstekler: %1 PII: %2 Regex: %3 Anahtar Kelimeler: %4 + + + Engine is not running — retrying… + Motor çalışmıyor — yeniden deneniyor… + + + Delete + Silmek + + + Validation Error + Doğrulama Hatası + + + Invalid regex syntax. + Geçersiz regex sözdizimi. + + + Case: Yes + Durum: Evet + + + Case: No + Durum: Hayır + + + Port must be between 1024 and 65535. + Port 1024 ile 65535 arasında olmalıdır. + + + Port %1 is already used by profile '%2'. + Port %1 zaten '%2' profili tarafından kullanılıyor. + + + Forward To URL must start with http:// or https://. + Yönlendirme URL'si http:// veya https:// ile başlamalıdır. + + + Confidence threshold must be between 0.0 and 1.0. + Güven eşiği 0.0 ile 1.0 arasında olmalıdır. + + + Security Warning + Güvenlik Uyarısı + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + HTTP (şifrelenmemiş) upstream URL kullanıyorsunuz. API anahtarınız ağ üzerinden düz metin olarak gönderilecek. + + + Error + Hata + + + The engine rejected the profile. Check the engine log for details. + Motor profili reddetti. Ayrıntılar için motor günlüğünü kontrol edin. + + + Profile %1 + Profil %1 + + + The engine rejected the new profile. + Motor yeni profili reddetti. + + + Remove Profile + Profili Kaldır + + + Are you sure? This operation is permanent. + Emin misiniz? Bu işlem kalıcıdır. + + + Proxy URL copied to clipboard + Proxy URL'si panoya kopyalandı + + + Wrong password. + Yanlış şifre. + + + Show sensitive information + Hassas bilgileri göster + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Hassas günlük kaydı, günlüğe ham, düzenlenmemiş değerleri (API anahtarları dahil) yazar. Yalnızca hata ayıklama sırasında etkinleştirin. + + + Enable logging first. + Önce günlüğe kaydetmeyi etkinleştirin. + + + Delete all logs? + Tüm günlükler silinsin mi? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Bu işlem mevcut günlük dosyasını ve tüm arşivlenmiş oturum günlüklerini kalıcı olarak silecektir. Bu işlem geri alınamaz. + + + Downloading AI model + AI modeli indiriliyor + + + Retry + Yeniden dene + + + The PII detection model is downloading (%1%). + PII tespit modeli indiriliyor (%1). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Model indirilemedi. İnternet bağlantınızı kontrol edip yeniden deneyin. İndirme tamamlanana kadar PII algılama kullanılamaz. + + + Are you sure you want to quit? + Çıkmak istediğinizden emin misiniz? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Çıkarsanız Agent Redactor artık API trafiğini izlemeyecek ve düzenlemeyecektir. + + + Quit Agent Redactor? The engine keeps running in the background. + Agent Redactor'dan çıkılsın mı? Motor arka planda çalışmaya devam ediyor. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Agent Redactor'ı Aç + + + Start on Boot + Önyüklemede Başlat + + + Language + Dil + + + Quit + Çık + + + + PasswordEnableDialog + + Enable password protection + Parola korumasını etkinleştir + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Agent Redactor için bir ana parola seçin. Bu makinede saklanan API anahtarlarınızı korur ve oturum açma parolanızla ilgisi yoktur. + + + New password: + Yeni Şifre: + + + Confirm password: + Şifreyi onaylayın: + + + Password must not be empty. + Şifre boş olmamalıdır. + + + Passwords do not match. + Şifreler eşleşmiyor. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Agent Redactor'ın kilidini açın + + + Enter your master password to unlock. + Kilidi açmak için ana şifrenizi girin. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_uk.ts b/linux/gui/i18n/agentredactor_uk.ts new file mode 100644 index 0000000..59df728 --- /dev/null +++ b/linux/gui/i18n/agentredactor_uk.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Оновлення готове до інсталяції + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 завантажено. Перезапустіть зараз, щоб застосувати оновлення. + + + Restart now + Перезапустити зараз + + + Later + Пізніше + + + Check for updates + Перевірити оновлення + + + You're up to date. + У вас найновіша версія. + + + Couldn't check for updates. Try again later. + Не вдалося перевірити оновлення. Спробуйте пізніше. + + + &File + &Файл + + + &Quit + Вийти + + + Profile + Профіль + + + Detection + виявлення + + + Regex Patterns + Шаблони регулярних виразів + + + Keywords + Ключові слова + + + Password + Пароль + + + Statistics + Статистика + + + Session Redactions + Редагування сеансу + + + Logs + Журнали + + + Settings + Налаштування + + + Name: + Ім'я: + + + Port: + Порт: + + + Forward To + Пересилати на + + + API Key + Ключ API + + + Use AI model: + Використовуйте модель ШІ: + + + Confidence threshold: + Поріг впевненості: + + + Add + Додати + + + Remove + Видалити + + + Show API key + Показати ключ API + + + Copy proxy URL + Копіювати URL проксі + + + Save + зберегти + + + Use AI model for PII detection + Використовуйте модель ШІ для виявлення ідентифікаційної інформації + + + Case sensitive + Враховувати регістр + + + Require master password + Вимагати головний пароль + + + Clear statistics + Чітка статистика + + + Clear + Очистити + + + Enable logging + Увімкнути журналювання + + + Show sensitive information in logs + Показувати конфіденційну інформацію в журналах + + + Open log file + Відкрити файл журналу + + + Open folder + Відкрити папку + + + Delete all logs + Видалити всі журнали + + + Start on Boot + Почніть із завантаження + + + Language + Мова + + + System default + Системна за замовчуванням + + + Master Password + Головний пароль + + + Unlock + Розблокувати + + + Agent Redactor is locked + Редактор агента заблоковано + + + Account number + Номер рахунку + + + Address + Адреса + + + Date + Дата + + + Email + Електронна пошта + + + Person + Особа + + + Phone + Телефон + + + URL + URL + + + Secret + Секрет + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Запити: %1 ідентифікаційна інформація: %2 Регулярний вираз: %3 Ключові слова: %4 + + + Engine is not running — retrying… + Двигун не працює — повторна спроба… + + + Delete + Видалити + + + Validation Error + Помилка перевірки + + + Invalid regex syntax. + Неправильний синтаксис регулярного виразу. + + + Case: Yes + Справа: Так + + + Case: No + Справа: немає + + + Port must be between 1024 and 65535. + Порт має бути в діапазоні від 1024 до 65535. + + + Port %1 is already used by profile '%2'. + Порт %1 уже використовується профілем '%2'. + + + Forward To URL must start with http:// or https://. + URL для пересилання має починатися з http:// або https://. + + + Confidence threshold must be between 0.0 and 1.0. + Поріг впевненості має бути в діапазоні від 0,0 до 1,0. + + + Security Warning + Попередження безпеки + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Ви використовуєте HTTP URL вищого рівня (незашифрований). Ваш ключ API буде надіслано відкритим текстом через мережу. + + + Error + Помилка + + + The engine rejected the profile. Check the engine log for details. + Двигун відхилив профіль. Подробиці перевірте в журналі двигуна. + + + Profile %1 + Профіль %1 + + + The engine rejected the new profile. + Двигун відхилив новий профіль. + + + Remove Profile + Видалити профіль + + + Are you sure? This operation is permanent. + Ви впевнені? Ця операція є незворотною. + + + Proxy URL copied to clipboard + URL-адресу проксі-сервера скопійовано в буфер обміну + + + Wrong password. + Неправильний пароль. + + + Show sensitive information + Показати конфіденційну інформацію + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Конфіденційне журналювання записує невідредаговані значення (включно з ключами API) до журналу. Увімкніть його лише під час налагодження. + + + Enable logging first. + Спочатку увімкніть журналювання. + + + Delete all logs? + Видалити всі журнали? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Це назавжди видалить поточний файл журналу та всі архівовані журнали сеансів. Це не можна скасувати. + + + Downloading AI model + Завантаження моделі ШІ + + + Retry + Повторити + + + The PII detection model is downloading (%1%). + Модель виявлення ідентифікаційної інформації завантажується (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Не вдалося завантажити модель. Перевірте підключення до Інтернету та повторіть спробу. Виявлення PII недоступне, доки завантаження не завершиться. + + + Are you sure you want to quit? + Ви впевнені, що хочете вийти? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Якщо ви вийдете, Agent Redactor більше не контролюватиме та не редагуватиме API-трафік. + + + Quit Agent Redactor? The engine keeps running in the background. + Вийти з Agent Redactor? Двигун продовжує працювати у фоновому режимі. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Відкрити Agent Redactor + + + Start on Boot + Почніть із завантаження + + + Language + Мова + + + Quit + Вийти + + + + PasswordEnableDialog + + Enable password protection + Увімкнути захист паролем + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Виберіть головний пароль для Agent Redactor. Він захищає ключі API, які зберігаються на цій машині, і не пов’язаний з вашим паролем для входу. + + + New password: + Новий пароль: + + + Confirm password: + Підтвердьте пароль: + + + Password must not be empty. + Пароль не повинен бути порожнім. + + + Passwords do not match. + Паролі не збігаються. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Розблокувати редактор агентів + + + Enter your master password to unlock. + Введіть головний пароль, щоб розблокувати. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_ur.ts b/linux/gui/i18n/agentredactor_ur.ts new file mode 100644 index 0000000..7af7068 --- /dev/null +++ b/linux/gui/i18n/agentredactor_ur.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + اپڈیٹ انسٹال کے لیے تیار ہے + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 ڈاؤن لوڈ ہو گیا ہے۔ اپڈیٹ لاگو کرنے کے لیے ابھی دوبارہ شروع کریں۔ + + + Restart now + ابھی دوبارہ شروع کریں + + + Later + بعد میں + + + Check for updates + اپڈیٹس چیک کریں + + + You're up to date. + آپ کے پاس تازہ ترین ورژن ہے۔ + + + Couldn't check for updates. Try again later. + اپڈیٹس چیک نہیں ہو سکیں۔ بعد میں دوبارہ کوشش کریں۔ + + + &File + &فائل + + + &Quit + بند کریں + + + Profile + پروفائل + + + Detection + پتہ لگانا + + + Regex Patterns + Regex پیٹرن + + + Keywords + مطلوبہ الفاظ + + + Password + پاس ورڈ + + + Statistics + اعداد و شمار + + + Session Redactions + سیشن حذفیاں + + + Logs + لاگز + + + Settings + ترتیبات + + + Name: + نام: + + + Port: + پورٹ: + + + Forward To + آگے بھیجیں + + + API Key + API کلید + + + Use AI model: + AI ماڈل استعمال کریں: + + + Confidence threshold: + اعتماد کی حد: + + + Add + شامل کریں + + + Remove + ہٹائیں + + + Show API key + API کلید دکھائیں۔ + + + Copy proxy URL + پراکسی یو آر ایل کاپی کریں۔ + + + Save + محفوظ کریں۔ + + + Use AI model for PII detection + PII کا پتہ لگانے کے لیے AI ماڈل استعمال کریں۔ + + + Case sensitive + کیس حساس + + + Require master password + ماسٹر پاس ورڈ کی ضرورت ہے۔ + + + Clear statistics + اعدادوشمار صاف کریں۔ + + + Clear + صاف کریں + + + Enable logging + لاگنگ کو فعال کریں۔ + + + Show sensitive information in logs + نوشتہ جات میں حساس معلومات دکھائیں۔ + + + Open log file + لاگ فائل کھولیں + + + Open folder + فولڈر کھولیں + + + Delete all logs + تمام لاگز حذف کریں + + + Start on Boot + بوٹ پر شروع کریں۔ + + + Language + زبان + + + System default + سسٹم طے شدہ + + + Master Password + ماسٹر پاس ورڈ + + + Unlock + غیر مقفل کریں + + + Agent Redactor is locked + ایجنٹ ریڈیکٹر مقفل ہے۔ + + + Account number + اکاؤنٹ نمبر + + + Address + پتہ + + + Date + تاریخ + + + Email + ای میل + + + Person + شخص + + + Phone + فون + + + URL + URL + + + Secret + راز + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + درخواستیں: %1 PII: %2 Regex: %3 مطلوبہ الفاظ: %4 + + + Engine is not running — retrying… + انجن نہیں چل رہا ہے — دوبارہ کوشش کر رہا ہے… + + + Delete + حذف کریں۔ + + + Validation Error + توثیقی خرابی + + + Invalid regex syntax. + غلط regex سنٹیکس۔ + + + Case: Yes + کیس: ہاں + + + Case: No + کیس: نہیں۔ + + + Port must be between 1024 and 65535. + پورٹ 1024 اور 65535 کے درمیان ہونا چاہیے۔ + + + Port %1 is already used by profile '%2'. + پورٹ %1 پہلے ہی '%2' پروفائل استعمال کر رہی ہے۔ + + + Forward To URL must start with http:// or https://. + Forward To URL کو http:// یا https:// سے شروع ہونا چاہیے۔ + + + Confidence threshold must be between 0.0 and 1.0. + اعتماد کی حد 0.0 اور 1.0 کے درمیان ہونی چاہیے۔ + + + Security Warning + سیکیورٹی انتباہ + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + آپ HTTP (غیر مرموز) اپ اسٹریم URL استعمال کر رہے ہیں۔ آپ کی API کلید نیٹ ورک پر plain text میں بھیجی جائے گی۔ + + + Error + خرابی + + + The engine rejected the profile. Check the engine log for details. + انجن نے پروفائل کو مسترد کر دیا۔ تفصیلات کے لیے انجن لاگ چیک کریں۔ + + + Profile %1 + پروفائل %1 + + + The engine rejected the new profile. + انجن نے نئے پروفائل کو مسترد کر دیا۔ + + + Remove Profile + پروفائل ہٹائیں + + + Are you sure? This operation is permanent. + کیا آپ یقینی ہیں؟ یہ عمل مستقل ہے۔ + + + Proxy URL copied to clipboard + پراکسی یو آر ایل کلپ بورڈ پر کاپی ہو گیا۔ + + + Wrong password. + غلط پاس ورڈ۔ + + + Show sensitive information + حساس معلومات دکھائیں۔ + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + حساس لاگنگ لاگ میں خام، غیر ترمیم شدہ اقدار (بشمول API کیز) لکھتی ہے۔ صرف ڈیبگ کرتے وقت اسے فعال کریں۔ + + + Enable logging first. + پہلے لاگنگ کو فعال کریں۔ + + + Delete all logs? + تمام لاگز حذف کریں؟ + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + یہ موجودہ لاگ فائل اور تمام محفوظ کردہ سیشن لاگز کو مستقل طور پر حذف کر دے گا۔ اسے کالعدم نہیں کیا جا سکتا۔ + + + Downloading AI model + AI ماڈل ڈاؤن لوڈ ہو رہا ہے + + + Retry + دوبارہ کوشش کریں + + + The PII detection model is downloading (%1%). + PII کا پتہ لگانے والا ماڈل ڈاؤن لوڈ ہو رہا ہے (%1%)۔ + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + ماڈل کی ڈاؤن لوڈنگ ناکام رہی۔ اپنا انٹرنیٹ کنیکشن چیک کریں، پھر دوبارہ کوشش کریں۔ ڈاؤن لوڈ مکمل ہونے تک PII کی شناخت دستیاب نہیں ہے۔ + + + Are you sure you want to quit? + کیا آپ واقعی بند کرنا چاہتے ہیں؟ + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + اگر آپ بند کریں گے تو Agent Redactor API ٹریفک کی نگرانی اور حذف نہیں کرے گا۔ + + + Quit Agent Redactor? The engine keeps running in the background. + ایجنٹ ریڈیکٹر چھوڑیں؟ انجن پس منظر میں چلتا رہتا ہے۔ + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Agent Redactor کھولیں + + + Start on Boot + بوٹ پر شروع کریں۔ + + + Language + زبان + + + Quit + بند کریں + + + + PasswordEnableDialog + + Enable password protection + پاس ورڈ کے تحفظ کو فعال کریں۔ + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + ایجنٹ ریڈیکٹر کے لیے ماسٹر پاس ورڈ کا انتخاب کریں۔ یہ اس مشین پر آپ کی ذخیرہ شدہ API کیز کی حفاظت کرتا ہے اور آپ کے لاگ ان پاس ورڈ سے غیر متعلق ہے۔ + + + New password: + نیا پاس ورڈ: + + + Confirm password: + پاس ورڈ کی تصدیق کریں: + + + Password must not be empty. + پاس ورڈ خالی نہیں ہونا چاہیے۔ + + + Passwords do not match. + پاس ورڈز مماثل نہیں ہیں۔ + + + + PasswordUnlockDialog + + Unlock Agent Redactor + ایجنٹ ریڈیکٹر کو غیر مقفل کریں۔ + + + Enter your master password to unlock. + غیر مقفل کرنے کے لیے اپنا ماسٹر پاس ورڈ درج کریں۔ + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_vi.ts b/linux/gui/i18n/agentredactor_vi.ts new file mode 100644 index 0000000..27526b3 --- /dev/null +++ b/linux/gui/i18n/agentredactor_vi.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + Bản cập nhật sẵn sàng cài đặt + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 đã được tải xuống. Khởi động lại ngay để áp dụng bản cập nhật. + + + Restart now + Khởi động lại ngay + + + Later + Để sau + + + Check for updates + Kiểm tra cập nhật + + + You're up to date. + Bạn đang dùng phiên bản mới nhất. + + + Couldn't check for updates. Try again later. + Không thể kiểm tra cập nhật. Thử lại sau. + + + &File + &Tài liệu + + + &Quit + Thoát + + + Profile + Hồ sơ + + + Detection + Phát hiện + + + Regex Patterns + Mẫu Regex + + + Keywords + Từ khóa + + + Password + Mật khẩu + + + Statistics + Thống kê + + + Session Redactions + Xóa trong phiên + + + Logs + Nhật ký + + + Settings + Cài đặt + + + Name: + Tên: + + + Port: + Cảng: + + + Forward To + Chuyển tiếp đến + + + API Key + Khóa API + + + Use AI model: + Sử dụng mô hình AI: + + + Confidence threshold: + Ngưỡng tin cậy: + + + Add + Thêm + + + Remove + Xóa + + + Show API key + Hiển thị khóa API + + + Copy proxy URL + Sao chép URL proxy + + + Save + Cứu + + + Use AI model for PII detection + Sử dụng mô hình AI để phát hiện PII + + + Case sensitive + Phân biệt chữ hoa/chữ thường + + + Require master password + Yêu cầu mật khẩu chính + + + Clear statistics + Xóa số liệu thống kê + + + Clear + Xóa + + + Enable logging + Bật ghi nhật ký + + + Show sensitive information in logs + Hiển thị thông tin nhạy cảm trong nhật ký + + + Open log file + Mở tệp nhật ký + + + Open folder + Mở thư mục + + + Delete all logs + Xóa tất cả nhật ký + + + Start on Boot + Bắt đầu khi khởi động + + + Language + Ngôn ngữ + + + System default + Mặc định hệ thống + + + Master Password + Mật khẩu chính + + + Unlock + Mở khóa + + + Agent Redactor is locked + Agent Redactor bị khóa + + + Account number + Số tài khoản + + + Address + Địa chỉ + + + Date + Ngày + + + Email + Email + + + Person + Ngườ + + + Phone + Điện thoại + + + URL + URL + + + Secret + Bí mật + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + Yêu cầu: %1 PII: %2 Regex: %3 Từ khóa: %4 + + + Engine is not running — retrying… + Động cơ không chạy — đang thử lại… + + + Delete + Xóa bỏ + + + Validation Error + Lỗi xác thực + + + Invalid regex syntax. + Cú pháp regex không hợp lệ. + + + Case: Yes + Trường hợp: Có + + + Case: No + Trường hợp: Không + + + Port must be between 1024 and 65535. + Cổng phải nằm trong khoảng từ 1024 đến 65535. + + + Port %1 is already used by profile '%2'. + Cổng %1 đã được hồ sơ '%2' sử dụng. + + + Forward To URL must start with http:// or https://. + URL chuyển tiếp đến phải bắt đầu bằng http:// hoặc https://. + + + Confidence threshold must be between 0.0 and 1.0. + Ngưỡng tin cậy phải nằm trong khoảng từ 0.0 đến 1.0. + + + Security Warning + Cảnh báo bảo mật + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + Bạn đang sử dụng URL upstream HTTP (không mã hóa). Khóa API của bạn sẽ được gửi dưới dạng văn bản thô qua mạng. + + + Error + Lỗi + + + The engine rejected the profile. Check the engine log for details. + Động cơ đã từ chối hồ sơ. Kiểm tra nhật ký động cơ để biết chi tiết. + + + Profile %1 + Hồ sơ %1 + + + The engine rejected the new profile. + Động cơ đã từ chối hồ sơ mới. + + + Remove Profile + Xóa hồ sơ + + + Are you sure? This operation is permanent. + Bạn có chắc không? Thao tác này không thể hoàn tác. + + + Proxy URL copied to clipboard + Đã sao chép URL proxy vào bảng nhớ tạm + + + Wrong password. + Mật khẩu sai. + + + Show sensitive information + Hiển thị thông tin nhạy cảm + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + Ghi nhật ký nhạy cảm ghi các giá trị thô, chưa được xử lý lại (bao gồm cả khóa API) vào nhật ký. Chỉ kích hoạt nó trong khi gỡ lỗi. + + + Enable logging first. + Cho phép đăng nhập đầu tiên. + + + Delete all logs? + Xóa tất cả nhật ký? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + Thao tác này sẽ xóa vĩnh viễn tệp nhật ký hiện tại và tất cả nhật ký phiên đã lưu trữ. Không thể hoàn tác. + + + Downloading AI model + Đang tải xuống mô hình AI + + + Retry + Thử lại + + + The PII detection model is downloading (%1%). + Mô hình phát hiện PII đang tải xuống (%1%). + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + Tải xuống mô hình không thành công. Kiểm tra kết nối internet của bạn, rồi thử lại. Tính năng phát hiện PII không khả dụng cho đến khi tải xuống hoàn tất. + + + Are you sure you want to quit? + Bạn có chắc muốn thoát? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + Nếu thoát, Agent Redactor sẽ không còn giám sát và xóa lưu lượng API. + + + Quit Agent Redactor? The engine keeps running in the background. + Thoát khỏi Agent Redactor? Động cơ tiếp tục chạy ở chế độ nền. + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + Mở Agent Redactor + + + Start on Boot + Bắt đầu khi khởi động + + + Language + Ngôn ngữ + + + Quit + Thoát + + + + PasswordEnableDialog + + Enable password protection + Kích hoạt bảo vệ mật khẩu + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + Chọn mật khẩu chính cho Agent Redactor. Nó bảo vệ các khóa API được lưu trữ của bạn trên máy này và không liên quan đến mật khẩu đăng nhập của bạn. + + + New password: + Mật khẩu mới: + + + Confirm password: + Xác nhận mật khẩu: + + + Password must not be empty. + Mật khẩu không được để trống. + + + Passwords do not match. + Mật khẩu không khớp. + + + + PasswordUnlockDialog + + Unlock Agent Redactor + Mở khóa Agent Redactor + + + Enter your master password to unlock. + Nhập mật khẩu chính của bạn để mở khóa. + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_zh_CN.ts b/linux/gui/i18n/agentredactor_zh_CN.ts new file mode 100644 index 0000000..f75e06d --- /dev/null +++ b/linux/gui/i18n/agentredactor_zh_CN.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + 更新已准备好安装 + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 已下载。立即重启以应用更新。 + + + Restart now + 立即重启 + + + Later + 稍后 + + + Check for updates + 检查更新 + + + You're up to date. + 你已是最新版本。 + + + Couldn't check for updates. Try again later. + 无法检查更新。请稍后重试。 + + + &File + &文件 + + + &Quit + 退出 + + + Profile + 轮廓 + + + Detection + 检测 + + + Regex Patterns + 正则表达式模式 + + + Keywords + 关键词 + + + Password + 密码 + + + Statistics + 统计 + + + Session Redactions + 会话脱敏 + + + Logs + 日志 + + + Settings + 设置 + + + Name: + 姓名: + + + Port: + 港口: + + + Forward To + 转发至 + + + API Key + API 密钥 + + + Use AI model: + 使用AI模型: + + + Confidence threshold: + 置信阈值: + + + Add + 添加 + + + Remove + 移除 + + + Show API key + 显示 API 密钥 + + + Copy proxy URL + 复制代理网址 + + + Save + 节省 + + + Use AI model for PII detection + 使用AI模型进行PII检测 + + + Case sensitive + 区分大小写 + + + Require master password + 需要主密码 + + + Clear statistics + 清晰的统计数据 + + + Clear + 清除 + + + Enable logging + 启用日志记录 + + + Show sensitive information in logs + 在日志中显示敏感信息 + + + Open log file + 打开日志文件 + + + Open folder + 打开文件夹 + + + Delete all logs + 删除所有日志 + + + Start on Boot + 开机启动 + + + Language + 语言 + + + System default + 系统默认 + + + Master Password + 主密码 + + + Unlock + 解锁 + + + Agent Redactor is locked + 代理编辑器已锁定 + + + Account number + 账号 + + + Address + 地址 + + + Date + 日期 + + + Email + 电子邮件 + + + Person + 人物 + + + Phone + 电话 + + + URL + URL + + + Secret + 机密 + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + 请求:%1 PII:%2 正则表达式:%3 关键字:%4 + + + Engine is not running — retrying… + 引擎未运行 — 正在重试... + + + Delete + 删除 + + + Validation Error + 验证错误 + + + Invalid regex syntax. + 正则表达式语法无效。 + + + Case: Yes + 案例:是 + + + Case: No + 案例:无 + + + Port must be between 1024 and 65535. + 端口必须介于 1024 与 65535 之间。 + + + Port %1 is already used by profile '%2'. + 端口 %1 已被配置文件“%2”使用。 + + + Forward To URL must start with http:// or https://. + 转发 URL 必须以 http:// 或 https:// 开头。 + + + Confidence threshold must be between 0.0 and 1.0. + 置信度阈值必须介于 0.0 与 1.0 之间。 + + + Security Warning + 安全警告 + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + 您正在使用 HTTP(未加密)上游 URL。您的 API 密钥将以明文形式通过网络发送。 + + + Error + 错误 + + + The engine rejected the profile. Check the engine log for details. + 引擎拒绝了该配置文件。检查引擎日志以了解详细信息。 + + + Profile %1 + 配置文件%1 + + + The engine rejected the new profile. + 引擎拒绝了新的配置文件。 + + + Remove Profile + 移除配置文件 + + + Are you sure? This operation is permanent. + 是否确定?此操作不可撤销。 + + + Proxy URL copied to clipboard + 代理 URL 已复制到剪贴板 + + + Wrong password. + 密码错误。 + + + Show sensitive information + 显示敏感信息 + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + 敏感日志记录将原始的、未编辑的值(包括 API 密钥)写入日志。仅在调试时启用它。 + + + Enable logging first. + 首先启用日志记录。 + + + Delete all logs? + 删除所有日志? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + 这将永久删除当前日志文件和所有已归档的会话日志。无法撤销。 + + + Downloading AI model + 正在下载 AI 模型 + + + Retry + 重试 + + + The PII detection model is downloading (%1%). + 正在下载 PII 检测模型 (%1%)。 + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + 模型下载失败。请检查网络连接,然后重试。下载完成前,PII 检测不可用。 + + + Are you sure you want to quit? + 是否确定要退出? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + 如果退出,Agent Redactor 将不再监控和脱敏 API 流量。 + + + Quit Agent Redactor? The engine keeps running in the background. + 退出 Agent Redactor?引擎在后台持续运行。 + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + 打开 Agent Redactor + + + Start on Boot + 开机启动 + + + Language + 语言 + + + Quit + 退出 + + + + PasswordEnableDialog + + Enable password protection + 启用密码保护 + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + 选择 Agent Redactor 的主密码。它保护您在本机上存储的 API 密钥,与您的登录密码无关。 + + + New password: + 新密码: + + + Confirm password: + 确认密码: + + + Password must not be empty. + 密码不能为空。 + + + Passwords do not match. + 密码不匹配。 + + + + PasswordUnlockDialog + + Unlock Agent Redactor + 解锁代理编辑器 + + + Enter your master password to unlock. + 输入您的主密码进行解锁。 + + + \ No newline at end of file diff --git a/linux/gui/i18n/agentredactor_zh_TW.ts b/linux/gui/i18n/agentredactor_zh_TW.ts new file mode 100644 index 0000000..d1d3701 --- /dev/null +++ b/linux/gui/i18n/agentredactor_zh_TW.ts @@ -0,0 +1,415 @@ + + + + MainWindow + + Agent Redactor + Agent Redactor + + + Update ready to install + 更新已準備好安裝 + + + Agent Redactor %1 has been downloaded. Restart now to apply the update. + Agent Redactor %1 已下載。立即重新啟動以套用更新。 + + + Restart now + 立即重新啟動 + + + Later + 稍後 + + + Check for updates + 檢查更新 + + + You're up to date. + 您已是最新版本。 + + + Couldn't check for updates. Try again later. + 無法檢查更新。請稍後重試。 + + + &File + &文件 + + + &Quit + 結束 + + + Profile + 輪廓 + + + Detection + 偵測 + + + Regex Patterns + 規則運算式模式 + + + Keywords + 關鍵字 + + + Password + 密碼 + + + Statistics + 統計資料 + + + Session Redactions + 工作階段去識別化 + + + Logs + 記錄 + + + Settings + 設定 + + + Name: + 姓名: + + + Port: + 港口: + + + Forward To + 轉送至 + + + API Key + API 金鑰 + + + Use AI model: + 使用AI模型: + + + Confidence threshold: + 信賴閾值: + + + Add + 新增 + + + Remove + 移除 + + + Show API key + 顯示 API 金鑰 + + + Copy proxy URL + 複製代理網址 + + + Save + 節省 + + + Use AI model for PII detection + 使用AI模型進行PII檢測 + + + Case sensitive + 區分大小寫 + + + Require master password + 需要主密碼 + + + Clear statistics + 清晰的統計數據 + + + Clear + 清除 + + + Enable logging + 啟用日誌記錄 + + + Show sensitive information in logs + 在日誌中顯示敏感資訊 + + + Open log file + 開啟記錄檔 + + + Open folder + 開啟資料夾 + + + Delete all logs + 刪除所有記錄 + + + Start on Boot + 開機啟動 + + + Language + 語言 + + + System default + 系統預設 + + + Master Password + 主密碼 + + + Unlock + 解除鎖定 + + + Agent Redactor is locked + 代理編輯器已鎖定 + + + Account number + 帳號 + + + Address + 地址 + + + Date + 日期 + + + Email + 電子郵件 + + + Person + 人物 + + + Phone + 電話 + + + URL + URL + + + Secret + 機密 + + + Requests: %1 PII: %2 Regex: %3 Keywords: %4 + 請求:%1 PII:%2 正規表示式:%3 關鍵字:%4 + + + Engine is not running — retrying… + 引擎未運作 — 正在重試... + + + Delete + 刪除 + + + Validation Error + 驗證錯誤 + + + Invalid regex syntax. + 規則運算式語法無效。 + + + Case: Yes + 案例:是 + + + Case: No + 案例:無 + + + Port must be between 1024 and 65535. + 連接埠必須介於 1024 與 65535 之間。 + + + Port %1 is already used by profile '%2'. + 連接埠 %1 已被設定檔「%2」使用。 + + + Forward To URL must start with http:// or https://. + 轉送 URL 必須以 http:// 或 https:// 開頭。 + + + Confidence threshold must be between 0.0 and 1.0. + 信賴度閾值必須介於 0.0 與 1.0 之間。 + + + Security Warning + 安全性警告 + + + You are using an HTTP (unencrypted) upstream URL. Your API key will be sent in plaintext over the network. + 您正在使用 HTTP(未加密)上游 URL。您的 API 金鑰將以明文形式透過網路傳送。 + + + Error + 錯誤 + + + The engine rejected the profile. Check the engine log for details. + 引擎拒絕了該設定檔。檢查引擎日誌以了解詳細資訊。 + + + Profile %1 + 設定檔%1 + + + The engine rejected the new profile. + 引擎拒絕了新的設定檔。 + + + Remove Profile + 移除設定檔 + + + Are you sure? This operation is permanent. + 是否確定?此操作無法復原。 + + + Proxy URL copied to clipboard + 代理 URL 已複製到剪貼簿 + + + Wrong password. + 密碼錯誤。 + + + Show sensitive information + 顯示敏感訊息 + + + Sensitive logging writes raw, unredacted values (including API keys) to the log. Only enable it while debugging. + 敏感日志记录将原始的、未编辑的值(包括 API 密钥)写入日志。僅在調試時啟用它。 + + + Enable logging first. + 首先啟用日誌記錄。 + + + Delete all logs? + 刪除所有記錄? + + + This will permanently delete the current log file and all archived session logs. This cannot be undone. + 這將永久刪除目前記錄檔和所有已封存的作業記錄。無法復原。 + + + Downloading AI model + 正在下載 AI 模型 + + + Retry + 重試 + + + The PII detection model is downloading (%1%). + 正在下載 PII 檢測模型 (%1%)。 + + + The model download failed. Check your internet connection, then retry. PII detection is unavailable until the download completes. + 模型下載失敗。請檢查網路連線,然後重試。下載完成前,PII 偵測無法使用。 + + + Are you sure you want to quit? + 是否確定要結束? + + + If you quit, Agent Redactor will no longer monitor and redact API traffic. + 如果結束,Agent Redactor 將不再監控和去識別化 API 流量。 + + + Quit Agent Redactor? The engine keeps running in the background. + 退出 Agent Redactor?引擎在後台持續運轉。 + + + + TrayIcon + + Agent Redactor + Agent Redactor + + + Open Agent Redactor + 開啟 Agent Redactor + + + Start on Boot + 開機啟動 + + + Language + 語言 + + + Quit + 結束 + + + + PasswordEnableDialog + + Enable password protection + 啟用密碼保護 + + + Choose a master password for Agent Redactor. It protects your stored API keys on this machine and is unrelated to your login password. + 選擇 Agent Redactor 的主密碼。它保護您在本機上儲存的 API 金鑰,與您的登入密碼無關。 + + + New password: + 新密碼: + + + Confirm password: + 確認密碼: + + + Password must not be empty. + 密碼不能為空。 + + + Passwords do not match. + 密碼不符。 + + + + PasswordUnlockDialog + + Unlock Agent Redactor + 解鎖代理編輯器 + + + Enter your master password to unlock. + 輸入您的主密碼進行解鎖。 + + + \ No newline at end of file diff --git a/linux/gui/i18n/bootstrap_translations.py b/linux/gui/i18n/bootstrap_translations.py new file mode 100644 index 0000000..ca822f3 --- /dev/null +++ b/linux/gui/i18n/bootstrap_translations.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""Bootstrap the unfinished Linux GUI translations with Google Translate. + +Fills every type="unfinished" entry in linux/gui/i18n/agentredactor_*.ts — +these are strings the Windows resw catalogs never had (the typed master +password flow, the Linux tray/quit wording, a few UI labels). Mirrors the +Windows bootstrap convention (windows/generate_new_languages.py): machine +translation now, native review later. + +Requires the repo venv (see sync_ts.py header): + python3 -m venv --without-pip .transvenv && .transvenv/bin/python get-pip.py + .transvenv/bin/pip install deep-translator + +Run from the repository root: + .transvenv/bin/python linux/gui/i18n/bootstrap_translations.py [tag ...] + +With no arguments all languages are processed; pass resw tags (e.g. "de fr") +to limit the run. Placeholders (%1, %2) are verified to survive translation; +entries where they do not are left unfinished for manual review. +""" + +import re +import sys +import time +import xml.etree.ElementTree as ET +from pathlib import Path + +REPO = Path(__file__).resolve().parents[3] +I18N_DIR = REPO / "linux" / "gui" / "i18n" +STRINGS_DIR = REPO / "windows" / "Strings" + +try: + from deep_translator import GoogleTranslator +except ImportError as e: # pragma: no cover + raise ImportError( + "deep-translator is required. Run:\n" + " .transvenv/bin/pip install deep-translator" + ) from e + +# resw tag -> Google Translate code (only the ones that differ). +GOOGLE_CODE = { + "zh-CN": "zh-CN", + "zh-TW": "zh-TW", + "fil": "tl", + "nb": "no", + "he": "iw", + "az-Latn": "az", + "ha-Latn": "ha", + "ig-NG": "ig", + "sr-Latn": "sr", # Google returns Cyrillic; transliterated below +} + +# Serbian Cyrillic -> Latin (1:1 digraph-aware mapping). +SR_CYR_TO_LAT = { + "а": "a", "б": "b", "в": "v", "г": "g", "д": "d", "ђ": "đ", "е": "e", + "ж": "ž", "з": "z", "и": "i", "ј": "j", "к": "k", "л": "l", "љ": "lj", + "м": "m", "н": "n", "њ": "nj", "о": "o", "п": "p", "р": "r", "с": "s", + "т": "t", "ћ": "ć", "у": "u", "ф": "f", "х": "h", "ц": "c", "ч": "č", + "џ": "dž", "ш": "š", + "А": "A", "Б": "B", "В": "V", "Г": "G", "Д": "D", "Ђ": "Đ", "Е": "E", + "Ж": "Ž", "З": "Z", "И": "I", "Ј": "J", "К": "K", "Л": "L", "Љ": "Lj", + "М": "M", "Н": "N", "Њ": "Nj", "О": "O", "П": "P", "Р": "R", "С": "S", + "Т": "T", "Ћ": "Ć", "У": "U", "Ф": "F", "Х": "H", "Ц": "C", "Ч": "Č", + "Џ": "Dž", "Ш": "Š", +} + + +def sr_latinize(text: str) -> str: + return "".join(SR_CYR_TO_LAT.get(ch, ch) for ch in text) + + +PLACEHOLDER_RE = re.compile(r"%\d+") + + +def placeholders_ok(source: str, translated: str) -> bool: + return sorted(PLACEHOLDER_RE.findall(source)) == sorted(PLACEHOLDER_RE.findall(translated)) + + +def main() -> int: + only = set(sys.argv[1:]) + tags = sorted(p.name for p in STRINGS_DIR.iterdir() + if (p / "Resources.resw").exists() and p.name != "en") + if only: + unknown = only - set(tags) + if unknown: + print(f"unknown tags: {', '.join(sorted(unknown))}") + return 2 + tags = [t for t in tags if t in only] + + total_filled = total_failed = 0 + for tag in tags: + ts_path = I18N_DIR / f"agentredactor_{tag.replace('-', '_')}.ts" + if not ts_path.exists(): + print(f"{tag}: no .ts file (run sync_ts.py first) — skipped") + continue + tree = ET.parse(ts_path) + root = tree.getroot() + + # Collect unfinished entries in order. + pending = [] # (translation_element, source_text) + for msg in root.iter("message"): + tr = msg.find("translation") + if tr is not None and tr.get("type") == "unfinished": + pending.append((tr, msg.findtext("source") or "")) + if not pending: + print(f"{tag}: nothing to do") + continue + + target = GOOGLE_CODE.get(tag, tag) + translator = GoogleTranslator(source="en", target=target) + print(f"{tag}: translating {len(pending)} strings -> {target}", flush=True) + + filled = failed = 0 + chunk = 20 + for i in range(0, len(pending), chunk): + batch = pending[i:i + chunk] + sources = [s for _, s in batch] + retries = 3 + results = None + while retries > 0: + try: + results = translator.translate_batch(sources) + if len(results) != len(sources): + raise RuntimeError(f"batch length mismatch {len(results)} vs {len(sources)}") + break + except Exception as e: # network hiccup / rate limit + print(f" retry {4 - retries}/3: {e}", flush=True) + retries -= 1 + time.sleep(5) + if results is None: + results = sources # leave as English below (still flagged) + + for (tr, source), translated in zip(batch, results): + if tag == "sr-Latn": + translated = sr_latinize(translated) + if not translated or translated == source and retries == 0 and results is sources: + continue # untouched unfinished (translation failed) + if not placeholders_ok(source, translated): + print(f" PLACEHOLDER LOST: {source!r} -> {translated!r} (left unfinished)") + failed += 1 + continue + tr.text = translated + # Machine-translated: mark finished so lrelease ships it; + # native review happens on the .ts files (same convention as + # the Windows bootstrap). + del tr.attrib["type"] + filled += 1 + time.sleep(0.5) # be polite to the endpoint + + ET.indent(tree) + tree.write(ts_path, encoding="utf-8", xml_declaration=True) + print(f" {tag}: {filled} filled, {failed} placeholder failures") + total_filled += filled + total_failed += failed + + print(f"\ntotal: {total_filled} translated, {total_failed} placeholder failures") + return 0 if total_failed == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/linux/gui/i18n/sync_ts.py b/linux/gui/i18n/sync_ts.py new file mode 100644 index 0000000..03286bf --- /dev/null +++ b/linux/gui/i18n/sync_ts.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +"""Sync Qt .ts catalogs for the Linux GUI from the Windows .resw catalogs. + +Scans linux/gui/*.cpp for tr("...") sources (one Qt context per class), +matches each source against the English windows/Strings/en/Resources.resw +values, and writes linux/gui/i18n/agentredactor_.ts for every +Windows language folder, filling translations from that language's resw. +Sources with no Windows counterpart are left type="unfinished" — fill them +with bootstrap_translations.py (machine translation) and native review. + +Matching normalizes accelerator markers ('&') and placeholders (%1 <-> {0}). +Existing translations in the .ts files are preserved across runs, so this +script is idempotent and safe to re-run after source strings change. + +Run from the repository root: + python3 linux/gui/i18n/sync_ts.py +""" + +import re +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +REPO = Path(__file__).resolve().parents[3] +GUI_SRC = REPO / "linux" / "gui" +I18N_DIR = GUI_SRC / "i18n" +STRINGS_DIR = REPO / "windows" / "Strings" + +# tr() source files and the Qt context (class) they belong to. Files listed +# here map wholly to one context except password_dialog.cpp, whose two +# classes are split by tracking the enclosing ClassName:: qualifier. +FILE_CONTEXT = { + "main_window.cpp": "MainWindow", + "tray_icon.cpp": "TrayIcon", + "password_dialog.cpp": None, # split by qualifier +} + + +def unescape_cpp(s: str) -> str: + return (s.replace('\\"', '"').replace("\\n", "\n").replace("\\t", "\t") + .replace("\\\\", "\\")) + + +def extract_tr_calls(path: Path): + """Yield (offset, source_text) for every tr("...") call, concatenating + adjacent string literals.""" + text = path.read_text(encoding="utf-8") + results = [] + for m in re.finditer(r"\btr\(\s*", text): + i = m.end() + depth = 1 + parts = [] + while i < len(text) and depth > 0: + ch = text[i] + if ch == "(": + depth += 1 + i += 1 + elif ch == ")": + depth -= 1 + i += 1 + elif ch == '"': + j = i + 1 + buf = [] + while j < len(text) and text[j] != '"': + if text[j] == "\\": + buf.append(text[j:j + 2]) + j += 2 + else: + buf.append(text[j]) + j += 1 + parts.append("".join(buf)) + i = j + 1 + else: + i += 1 + if parts: + results.append((m.start(), unescape_cpp("".join(parts)))) + return results + + +def collect_inventory(): + """Return {context: [source, ...]} preserving first-seen order.""" + inventory = {} + for fname, context in FILE_CONTEXT.items(): + path = GUI_SRC / fname + text = path.read_text(encoding="utf-8") + for offset, source in extract_tr_calls(path): + ctx = context + if ctx is None: + # Last ClassName:: qualifier before this call names the class. + qualifiers = re.findall(r"(\w+)::\w+\s*\(", text[:offset]) + ctx = qualifiers[-1] if qualifiers else "PasswordEnableDialog" + inventory.setdefault(ctx, []) + if source not in inventory[ctx]: + inventory[ctx].append(source) + return inventory + + +PLACEHOLDER_RE = re.compile(r"%\d+|\{\d+\}") + + +def normalize(s: str) -> str: + s = s.replace("&", "") + s = PLACEHOLDER_RE.sub("#", s) + return s.strip() + + +def convert_placeholders(s: str) -> str: + """Windows {0}/{1} -> Qt %1/%2 (only plain {digit} tokens).""" + return re.sub(r"\{(\d+)\}", lambda m: f"%{int(m.group(1)) + 1}", s) + + +def parse_resw(path: Path): + tree = ET.parse(path) + out = {} + for d in tree.getroot().iter("data"): + v = d.find("value") + if v is not None and v.text is not None: + out[d.get("name")] = v.text.strip() + return out + + +def qt_locale(tag: str) -> str: + """BCP-47 tag -> Qt locale/file suffix (zh-CN -> zh_CN).""" + return tag.replace("-", "_") + + +def load_existing_translations(ts_path: Path): + """{(context, source): (translation, finished)} from an existing .ts.""" + if not ts_path.exists(): + return {} + tree = ET.parse(ts_path) + out = {} + for ctx in tree.getroot().iter("context"): + name = ctx.findtext("name") + for msg in ctx.iter("message"): + src = msg.findtext("source") or "" + tr = msg.find("translation") + text = tr.text or "" + finished = tr.get("type") != "unfinished" + if text: + out[(name, src)] = (text, finished) + return out + + +# Sources whose Windows translations embed OS-specific wording and therefore +# must NOT be reused (they get machine-translated fresh instead). +# TrayMenu_StartOnBoot is "Mit Windows starten" / "Démarrer avec Windows" / +# "Iniciar com o Windows" in de/fr/pt — wrong on Linux. +EXCLUDE_REUSE = {"Start on Boot"} + + +def main(): + inventory = collect_inventory() + total = sum(len(v) for v in inventory.values()) + print(f"scanned {total} unique source strings in {len(inventory)} contexts") + + en = parse_resw(STRINGS_DIR / "en" / "Resources.resw") + # Normalized English value -> resw key (first key wins). + en_by_norm = {} + for key, value in en.items(): + en_by_norm.setdefault(normalize(value), key) + + tags = sorted(p.name for p in STRINGS_DIR.iterdir() + if (p / "Resources.resw").exists() and p.name != "en") + print(f"{len(tags)} language catalogs: {', '.join(tags)}") + + I18N_DIR.mkdir(exist_ok=True) + summary = {} + for tag in tags: + resw = parse_resw(STRINGS_DIR / tag / "Resources.resw") + ts_path = I18N_DIR / f"agentredactor_{qt_locale(tag)}.ts" + existing = load_existing_translations(ts_path) + + ts = ET.Element("TS", version="2.1", language=qt_locale(tag)) + used = reused = kept = gaps = 0 + for context, sources in inventory.items(): + ctx_el = ET.SubElement(ts, "context") + ET.SubElement(ctx_el, "name").text = context + for source in sources: + msg = ET.SubElement(ctx_el, "message") + ET.SubElement(msg, "source").text = source + key = None if source in EXCLUDE_REUSE else en_by_norm.get(normalize(source)) + translation = None + finished = True + if key is not None and key in resw: + translation = convert_placeholders(resw[key]) + reused += 1 + elif ((context, source) in existing + and source not in EXCLUDE_REUSE): + # Previously bootstrapped/reviewed translation survives. + translation, finished = existing[(context, source)] + kept += 1 + else: + gaps += 1 + tr_el = ET.SubElement(msg, "translation") + if translation: + tr_el.text = translation + if not finished: + tr_el.set("type", "unfinished") + else: + tr_el.set("type", "unfinished") + + ET.indent(ts) + ET.ElementTree(ts).write(ts_path, encoding="utf-8", xml_declaration=True) + summary[tag] = (reused, kept, gaps) + used += reused + + print(f"\nper-language: resw-reused / preserved / gaps") + for tag, (r, k, g) in summary.items(): + print(f" {tag:8} {r:3} / {k:3} / {g:3}") + print(f"\nwrote {len(summary)} .ts files to {I18N_DIR.relative_to(REPO)}") + print("next: python3 linux/gui/i18n/bootstrap_translations.py (fills the gaps)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/linux/gui/main_window.cpp b/linux/gui/main_window.cpp index 9480a7d..c1544ca 100644 --- a/linux/gui/main_window.cpp +++ b/linux/gui/main_window.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -66,14 +67,15 @@ MainWindow::MainWindow(AppState* appState, TrayIcon* tray, TranslatorLoader* tra buildUi(); - auto* fileMenu = menuBar()->addMenu(tr("&File")); - auto* quitAction = fileMenu->addAction(tr("&Quit")); - connect(quitAction, &QAction::triggered, this, &MainWindow::onQuitRequested); - connect(appState_, &AppState::statusUpdated, this, &MainWindow::onStatusUpdated); connect(appState_, &AppState::settingsChanged, this, &MainWindow::onSettingsChanged); connect(appState_, &AppState::modelDownloadChanged, this, &MainWindow::onModelDownloadChanged); connect(appState_, &AppState::connectionLost, this, &MainWindow::onConnectionLost); + connect(tray_, &TrayIcon::languageChangeRequested, this, [this](const QString& tag) { + // The settings poll picks up the change and retranslates everything + // live (no restart, unlike Windows). + appState_->client().PutSetting(L"appLanguage", tag.toStdString()); + }); // 10-minute inactivity re-lock, reset by any key/mouse activity (mirrors // the Windows message-filter timer). @@ -106,11 +108,11 @@ MainWindow::MainWindow(AppState* appState, TrayIcon* tray, TranslatorLoader* tra connect(updateMgr_, &AppUpdateManager::updateDownloaded, this, [this](QString version, bool) { auto* box = new QMessageBox(QMessageBox::Information, - tr("Update available"), - tr("Version %1 has been downloaded and is ready to install.").arg(version), + tr("Update ready to install"), + tr("Agent Redactor %1 has been downloaded. Restart now to apply the update.").arg(version), QMessageBox::NoButton, this); QPushButton* now = box->addButton(tr("Restart now"), QMessageBox::AcceptRole); - box->addButton(tr("Restart later"), QMessageBox::RejectRole); + box->addButton(tr("Later"), QMessageBox::RejectRole); box->setDefaultButton(qobject_cast(box->buttons().last())); box->exec(); if (box->clickedButton() == now) updateMgr_->ApplyAndRestart(); @@ -120,13 +122,13 @@ MainWindow::MainWindow(AppState* appState, TrayIcon* tray, TranslatorLoader* tra [this](bool userInitiated) { if (userInitiated) QMessageBox::information(this, tr("Check for updates"), - tr("Agent Redactor is up to date.")); + tr("You're up to date.")); }); connect(updateMgr_, &AppUpdateManager::checkFailed, this, - [this](QString message, bool userInitiated) { + [this](QString, bool userInitiated) { if (userInitiated) QMessageBox::warning(this, tr("Check for updates"), - tr("Could not check for updates: %1").arg(message)); + tr("Couldn't check for updates. Try again later.")); }); // The Velopack updater is already waiting for this process to exit; // skip the quit confirmation and shut down immediately. @@ -190,15 +192,19 @@ void MainWindow::buildUi() { auto* profileCard = makeCard(QString(), profileLayout, cards); // title set in retranslateUi profileCard->setObjectName(QStringLiteral("profileCard")); auto* profileForm = new QFormLayout; + aliasLabel_ = new QLabel(profileCard); + portLabel_ = new QLabel(profileCard); + urlLabel_ = new QLabel(profileCard); + apiKeyLabel_ = new QLabel(profileCard); aliasBox_ = new QLineEdit(profileCard); portBox_ = new QLineEdit(profileCard); urlBox_ = new QLineEdit(profileCard); apiKeyBox_ = new QLineEdit(profileCard); apiKeyBox_->setEchoMode(QLineEdit::Password); - profileForm->addRow(QStringLiteral("Name:"), aliasBox_); - profileForm->addRow(QStringLiteral("Port:"), portBox_); - profileForm->addRow(QStringLiteral("Upstream URL:"), urlBox_); - profileForm->addRow(QStringLiteral("API key:"), apiKeyBox_); + profileForm->addRow(aliasLabel_, aliasBox_); + profileForm->addRow(portLabel_, portBox_); + profileForm->addRow(urlLabel_, urlBox_); + profileForm->addRow(apiKeyLabel_, apiKeyBox_); profileLayout->addLayout(profileForm); connect(aliasBox_, &QLineEdit::textEdited, this, markDirty); connect(portBox_, &QLineEdit::textEdited, this, markDirty); @@ -224,10 +230,12 @@ void MainWindow::buildUi() { auto* detectionCard = makeCard(QString(), detectionLayout, cards); detectionCard->setObjectName(QStringLiteral("detectionCard")); auto* detectionForm = new QFormLayout; + useAiLabel_ = new QLabel(detectionCard); + confidenceLabel_ = new QLabel(detectionCard); useAiCheck_ = new QCheckBox(detectionCard); confidenceBox_ = new QLineEdit(detectionCard); - detectionForm->addRow(QStringLiteral("Use AI model:"), useAiCheck_); - detectionForm->addRow(QStringLiteral("Confidence threshold:"), confidenceBox_); + detectionForm->addRow(useAiLabel_, useAiCheck_); + detectionForm->addRow(confidenceLabel_, confidenceBox_); detectionLayout->addLayout(detectionForm); connect(useAiCheck_, &QCheckBox::toggled, this, markDirty); connect(confidenceBox_, &QLineEdit::textEdited, this, markDirty); @@ -348,6 +356,22 @@ void MainWindow::buildUi() { startOnBootCheck_ = new QCheckBox(settingsCard); connect(startOnBootCheck_, &QCheckBox::toggled, this, &MainWindow::onStartOnBootToggled); settingsLayout->addWidget(startOnBootCheck_); + + // Language selector (Windows: Settings page combo + tray submenu). Qt + // retranslates live, so no restart is needed here — the settings poll + // re-applies the tag and retranslateUi rebuilds every string. + auto* langRow = new QHBoxLayout; + languageLabel_ = new QLabel(settingsCard); + languageCombo_ = new QComboBox(settingsCard); + languageCombo_->addItem(QString(), QString()); // "System default" (retranslateUi) + for (const auto& lang : SUPPORTED_LANGUAGES) { + languageCombo_->addItem(QString::fromStdWString(lang.nativeName), + QString::fromStdWString(lang.tag)); + } + connect(languageCombo_, &QComboBox::activated, this, &MainWindow::onLanguageSelected); + langRow->addWidget(languageLabel_); + langRow->addWidget(languageCombo_, 1); + settingsLayout->addLayout(langRow); if (AppUpdateManager::IsSelfRelease()) { checkUpdatesBtn_ = new QPushButton(settingsCard); connect(checkUpdatesBtn_, &QPushButton::clicked, this, [this] { @@ -402,21 +426,35 @@ void MainWindow::buildUi() { setCentralWidget(central); centralStack_->setCurrentIndex(0); + // Menu bar (titles set in retranslateUi). + fileMenu_ = menuBar()->addMenu(QString()); + quitMenuAction_ = fileMenu_->addAction(QString()); + connect(quitMenuAction_, &QAction::triggered, this, &MainWindow::onQuitRequested); + retranslateUi(); } void MainWindow::retranslateUi() { setWindowTitle(tr("Agent Redactor")); + fileMenu_->setTitle(tr("&File")); + quitMenuAction_->setText(tr("&Quit")); findChild(QStringLiteral("profileCard"))->setTitle(tr("Profile")); findChild(QStringLiteral("detectionCard"))->setTitle(tr("Detection")); - findChild(QStringLiteral("regexCard"))->setTitle(tr("Regex patterns")); + findChild(QStringLiteral("regexCard"))->setTitle(tr("Regex Patterns")); findChild(QStringLiteral("keywordsCard"))->setTitle(tr("Keywords")); findChild(QStringLiteral("passwordCard"))->setTitle(tr("Password")); findChild(QStringLiteral("statsCard"))->setTitle(tr("Statistics")); - findChild(QStringLiteral("matchesCard"))->setTitle(tr("Session redactions")); + findChild(QStringLiteral("matchesCard"))->setTitle(tr("Session Redactions")); findChild(QStringLiteral("logsCard"))->setTitle(tr("Logs")); findChild(QStringLiteral("settingsCard"))->setTitle(tr("Settings")); + aliasLabel_->setText(tr("Name:")); + portLabel_->setText(tr("Port:")); + urlLabel_->setText(tr("Forward To")); + apiKeyLabel_->setText(tr("API Key")); + useAiLabel_->setText(tr("Use AI model:")); + confidenceLabel_->setText(tr("Confidence threshold:")); + addProfileBtn_->setText(tr("Add")); removeProfileBtn_->setText(tr("Remove")); showKeyCheck_->setText(tr("Show API key")); @@ -431,20 +469,34 @@ void MainWindow::retranslateUi() { findChild(QStringLiteral("clearMatchesBtn"))->setText(tr("Clear")); loggingCheck_->setText(tr("Enable logging")); showSensitiveCheck_->setText(tr("Show sensitive information in logs")); - findChild(QStringLiteral("openLogBtn"))->setText(tr("Open log")); + findChild(QStringLiteral("openLogBtn"))->setText(tr("Open log file")); findChild(QStringLiteral("openFolderBtn"))->setText(tr("Open folder")); - findChild(QStringLiteral("clearLogsBtn"))->setText(tr("Clear logs")); - startOnBootCheck_->setText(tr("Start on boot")); + findChild(QStringLiteral("clearLogsBtn"))->setText(tr("Delete all logs")); + startOnBootCheck_->setText(tr("Start on Boot")); + languageLabel_->setText(tr("Language")); + languageCombo_->setItemText(0, tr("System default")); if (checkUpdatesBtn_) checkUpdatesBtn_->setText(tr("Check for updates")); - unlockBox_->setPlaceholderText(tr("Master password")); + unlockBox_->setPlaceholderText(tr("Master Password")); findChild(QStringLiteral("unlockBtn"))->setText(tr("Unlock")); if (auto* t = findChild(QStringLiteral("lockTitle"))) t->setText(tr("Agent Redactor is locked")); + + // PII grid labels are translated too (Windows PII_Type_* strings). + for (auto& [type, check] : piiChecks_) check->setText(piiTypeLabel(type)); } QString MainWindow::piiTypeLabel(const std::wstring& type) { - // English display labels for the PII grid (Windows: PII_Type_ resw - // keys). Underscores become spaces, first letter capitalized. + // English sources match the Windows PII_Type_ resw values, so the + // existing per-language catalogs translate these for free. + if (type == L"account_number") return tr("Account number"); + if (type == L"private_address") return tr("Address"); + if (type == L"private_date") return tr("Date"); + if (type == L"private_email") return tr("Email"); + if (type == L"private_person") return tr("Person"); + if (type == L"private_phone") return tr("Phone"); + if (type == L"private_url") return tr("URL"); + if (type == L"secret") return tr("Secret"); + // Unknown future type: raw type name, humanized (English fallback). QString s = QString::fromStdWString(type); s.replace(QLatin1Char('_'), QLatin1Char(' ')); if (!s.isEmpty()) s[0] = s[0].toUpper(); @@ -516,6 +568,17 @@ void MainWindow::onSettingsChanged() { translator_->applyLanguage( QString::fromStdString(settings.value("appLanguage", std::string()))); + // Language controls: the engine reports the effective tag (empty/system + // is already resolved to the OS locale), so an unrecognized tag means + // "System default" — nothing is checked/selected beyond index 0. + const QString langTag = QString::fromStdString(settings.value("appLanguage", std::string())); + { + QSignalBlocker b(languageCombo_); + const int idx = languageCombo_->findData(langTag); + languageCombo_->setCurrentIndex(idx < 0 ? 0 : idx); + } + tray_->setCurrentLanguage(langTag); + { QSignalBlocker b(requirePasswordCheck_); requirePasswordCheck_->setChecked(isProtected()); @@ -641,8 +704,8 @@ void MainWindow::loadProfileIntoForm(int index) { try { std::regex re(Utils::WideToUtf8(normalized), std::regex_constants::ECMAScript); } catch (const std::regex_error&) { - QMessageBox::warning(this, tr("Invalid regex"), - tr("The pattern is not a valid regular expression.")); + QMessageBox::warning(this, tr("Validation Error"), + tr("Invalid regex syntax.")); reloadProfiles(true); return; } @@ -772,7 +835,9 @@ bool MainWindow::validateForm(QString& error, bool& httpWarning) { for (size_t i = 0; i < profiles_.size(); ++i) { if (static_cast(i) == profileList_->currentRow()) continue; if (profiles_[i].value("port", 0) == port) { - error = tr("Another profile already uses this port."); + error = tr("Port %1 is already used by profile '%2'.") + .arg(port) + .arg(QString::fromStdString(profiles_[i].value("alias", std::string()))); return false; } } @@ -782,7 +847,7 @@ bool MainWindow::validateForm(QString& error, bool& httpWarning) { if (url.isEmpty() || !(url.startsWith(QLatin1String("http://")) || url.startsWith(QLatin1String("https://"))) || parsed.host().isEmpty()) { - error = tr("Upstream URL must start with http:// or https:// and have a host."); + error = tr("Forward To URL must start with http:// or https://."); return false; } if (url.startsWith(QLatin1String("http://")) && @@ -793,7 +858,7 @@ bool MainWindow::validateForm(QString& error, bool& httpWarning) { const double confidence = confidenceBox_->text().toDouble(&ok); if (!ok || confidence < 0.0 || confidence > 1.0) { - error = tr("Confidence threshold must be between 0 and 1."); + error = tr("Confidence threshold must be between 0.0 and 1.0."); return false; } return true; @@ -806,14 +871,14 @@ void MainWindow::onSaveProfile() { QString error; bool httpWarning = false; if (!validateForm(error, httpWarning)) { - QMessageBox::warning(this, tr("Invalid profile"), error); + QMessageBox::warning(this, tr("Validation Error"), error); reloadProfiles(true); return; } if (httpWarning) { - const auto answer = QMessageBox::warning(this, tr("Plain HTTP upstream"), - tr("The upstream URL uses plain HTTP to a non-local host. Redacted requests " - "will be readable on the network. Save anyway?"), + const auto answer = QMessageBox::warning(this, tr("Security Warning"), + tr("You are using an HTTP (unencrypted) upstream URL. Your API key will be " + "sent in plaintext over the network."), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (answer != QMessageBox::Yes) { reloadProfiles(true); @@ -823,7 +888,7 @@ void MainWindow::onSaveProfile() { const json updated = gatherProfileFromForm(); if (!appState_->client().PutProfile(w(selectedProfileId()), updated)) { - QMessageBox::warning(this, tr("Save failed"), + QMessageBox::warning(this, tr("Error"), tr("The engine rejected the profile. Check the engine log for details.")); return; } @@ -858,7 +923,7 @@ void MainWindow::onAddProfile() { std::wstring id; if (!appState_->client().PostProfile(profile, id)) { - QMessageBox::warning(this, tr("Add profile failed"), + QMessageBox::warning(this, tr("Error"), tr("The engine rejected the new profile.")); return; } @@ -876,10 +941,8 @@ void MainWindow::onAddProfile() { void MainWindow::onRemoveProfile() { if (profiles_.size() <= 1) return; // the last profile cannot be deleted - const QString alias = profileList_->currentItem() - ? profileList_->currentItem()->text() : QString(); - const auto answer = QMessageBox::question(this, tr("Remove profile"), - tr("Remove profile \"%1\"?").arg(alias)); + const auto answer = QMessageBox::question(this, tr("Remove Profile"), + tr("Are you sure? This operation is permanent.")); if (answer != QMessageBox::Yes) return; if (appState_->client().DeleteProfile(w(selectedProfileId()))) { @@ -912,8 +975,8 @@ void MainWindow::onAddRegex() { try { std::regex re(Utils::WideToUtf8(normalized), std::regex_constants::ECMAScript); } catch (const std::regex_error&) { - QMessageBox::warning(this, tr("Invalid regex"), - tr("The pattern is not a valid regular expression.")); + QMessageBox::warning(this, tr("Validation Error"), + tr("Invalid regex syntax.")); return; } @@ -1082,8 +1145,9 @@ void MainWindow::onOpenLogFolder() { } void MainWindow::onClearLogs() { - const auto answer = QMessageBox::question(this, tr("Clear logs"), - tr("Delete the log files? This cannot be undone.")); + const auto answer = QMessageBox::question(this, tr("Delete all logs?"), + tr("This will permanently delete the current log file and all archived session " + "logs. This cannot be undone.")); if (answer != QMessageBox::Yes) return; // Same files the Windows GUI deletes directly on disk. @@ -1112,6 +1176,14 @@ void MainWindow::onStartOnBootToggled(bool checked) { } } +void MainWindow::onLanguageSelected(int index) { + if (loading_) return; + // Empty data = "System default"; the engine resolves it from the OS + // locale. The poll then re-applies the language live to every widget. + const QString tag = languageCombo_->itemData(index).toString(); + appState_->client().PutSetting(L"appLanguage", tag.toStdString()); +} + // --------------------------------------------------------------------------- // Model download dialog (blocking, non-dismissible) // --------------------------------------------------------------------------- @@ -1145,7 +1217,7 @@ void MainWindow::updateModelDownloadDialog() { layout->addWidget(modelProgress_); layout->addWidget(modelRetryBtn_, 0, Qt::AlignLeft); } - modelDialog_->setWindowTitle(tr("Downloading language model")); + modelDialog_->setWindowTitle(tr("Downloading AI model")); modelRetryBtn_->setText(tr("Retry")); const int percent = status.value("modelDownloadPercent", 0); @@ -1153,7 +1225,8 @@ void MainWindow::updateModelDownloadDialog() { modelStatusLabel_->setText(tr("The PII detection model is downloading (%1%).").arg(percent)); modelRetryBtn_->setEnabled(failed); if (failed) { - modelStatusLabel_->setText(tr("The model download failed. Check your connection and retry.")); + modelStatusLabel_->setText(tr("The model download failed. Check your internet " + "connection, then retry. PII detection is unavailable until the download completes.")); } else if (!inProgress) { // Not started yet — kick it off. appState_->client().DownloadModel(); @@ -1199,9 +1272,9 @@ void MainWindow::openWindow() { } void MainWindow::onQuitRequested() { - const auto answer = QMessageBox::question(this, tr("Quit Agent Redactor"), + const auto answer = QMessageBox::question(this, tr("Are you sure you want to quit?"), tray_->available() || appState_->engineSpawned() - ? tr("Quit Agent Redactor? The proxy will stop.") + ? tr("If you quit, Agent Redactor will no longer monitor and redact API traffic.") : tr("Quit Agent Redactor? The engine keeps running in the background.")); if (answer != QMessageBox::Yes) return; @@ -1212,6 +1285,12 @@ void MainWindow::onQuitRequested() { } void MainWindow::changeEvent(QEvent* event) { - if (event->type() == QEvent::LanguageChange) retranslateUi(); + if (event->type() == QEvent::LanguageChange) { + retranslateUi(); + // Persistent/transient strings outside retranslateUi: the tray menu + // (built once) and the regex/keyword rows (rebuilt on profile load). + tray_->retranslate(); + if (!dirty_ && !profiles_.empty()) reloadProfiles(true); + } QMainWindow::changeEvent(event); } diff --git a/linux/gui/main_window.h b/linux/gui/main_window.h index 313ce0c..e5a1049 100644 --- a/linux/gui/main_window.h +++ b/linux/gui/main_window.h @@ -16,12 +16,15 @@ class AppUpdateManager; class TrayIcon; class TranslatorLoader; +class QAction; class QCheckBox; class QCloseEvent; +class QComboBox; class QDialog; class QLabel; class QLineEdit; class QListWidget; +class QMenu; class QProgressBar; class QPushButton; class QStackedLayout; @@ -75,6 +78,8 @@ private slots: void onOpenLogFolder(); void onClearLogs(); + void onLanguageSelected(int index); + private: // Loading/saving void reloadProfiles(bool keepSelection); @@ -99,13 +104,18 @@ private slots: void retranslateUi(); void setCardsEnabled(bool enabled); - // PII type display label (English; PII_Type_ keys in Windows resw). + // PII type display label (translated; English source matches the + // PII_Type_ values in the Windows resw catalogs). static QString piiTypeLabel(const std::wstring& type); AppState* appState_ = nullptr; TrayIcon* tray_ = nullptr; TranslatorLoader* translator_ = nullptr; + // Menu bar (kept for retranslateUi; built once in buildUi) + QMenu* fileMenu_ = nullptr; + QAction* quitMenuAction_ = nullptr; + // Central stack: page 0 = content, page 1 = lock overlay. QStackedLayout* centralStack_ = nullptr; @@ -115,6 +125,10 @@ private slots: QPushButton* removeProfileBtn_ = nullptr; // Profile card + QLabel* aliasLabel_ = nullptr; + QLabel* portLabel_ = nullptr; + QLabel* urlLabel_ = nullptr; + QLabel* apiKeyLabel_ = nullptr; QLineEdit* aliasBox_ = nullptr; QLineEdit* portBox_ = nullptr; QLineEdit* urlBox_ = nullptr; @@ -124,6 +138,8 @@ private slots: QPushButton* saveBtn_ = nullptr; // Detection card + QLabel* useAiLabel_ = nullptr; + QLabel* confidenceLabel_ = nullptr; QCheckBox* useAiCheck_ = nullptr; QLineEdit* confidenceBox_ = nullptr; std::vector> piiChecks_; @@ -150,6 +166,8 @@ private slots: // Settings card QCheckBox* startOnBootCheck_ = nullptr; + QLabel* languageLabel_ = nullptr; + QComboBox* languageCombo_ = nullptr; QPushButton* checkUpdatesBtn_ = nullptr; // Self-update (Velopack); nullptr in non-self-release builds. diff --git a/linux/gui/resources.qrc b/linux/gui/resources.qrc index cd4240b..db9aed5 100644 --- a/linux/gui/resources.qrc +++ b/linux/gui/resources.qrc @@ -2,6 +2,7 @@ assets/app.png - + diff --git a/linux/gui/translator_loader.cpp b/linux/gui/translator_loader.cpp index dd7c8e6..420ccba 100644 --- a/linux/gui/translator_loader.cpp +++ b/linux/gui/translator_loader.cpp @@ -29,8 +29,11 @@ void TranslatorLoader::applyLanguage(const QString& tag) { } if (effective != QLatin1String("en")) { - // Translation catalogs (when they exist) are embedded under :/i18n. - if (translator_.load(QStringLiteral(":/i18n/agentredactor_") + effective)) { + // Translation catalogs are embedded under :/i18n, named with Qt + // locale suffixes (zh_CN, sr_Latn) rather than BCP-47 dashes. + QString fileTag = effective; + fileTag.replace(QLatin1Char('-'), QLatin1Char('_')); + if (translator_.load(QStringLiteral(":/i18n/agentredactor_") + fileTag)) { app_.installTranslator(&translator_); } } diff --git a/linux/gui/tray_icon.cpp b/linux/gui/tray_icon.cpp index 7a0e7d6..4000c98 100644 --- a/linux/gui/tray_icon.cpp +++ b/linux/gui/tray_icon.cpp @@ -1,33 +1,53 @@ #include "tray_icon.h" #include +#include #include #include +#include "constants.h" // SUPPORTED_LANGUAGES + TrayIcon::TrayIcon(QObject* parent) : QObject(parent) { if (!QSystemTrayIcon::isSystemTrayAvailable()) return; tray_ = new QSystemTrayIcon(QIcon(QStringLiteral(":/app.png")), this); - tray_->setToolTip(tr("Agent Redactor")); - auto* menu = new QMenu(); - auto* openAction = menu->addAction(tr("Open")); - connect(openAction, &QAction::triggered, this, &TrayIcon::openRequested); + menu_ = new QMenu(); + openAction_ = menu_->addAction(QString()); + connect(openAction_, &QAction::triggered, this, &TrayIcon::openRequested); - startOnBootAction_ = menu->addAction(tr("Start on boot")); + startOnBootAction_ = menu_->addAction(QString()); startOnBootAction_->setCheckable(true); connect(startOnBootAction_, &QAction::toggled, this, &TrayIcon::startOnBootToggled); - menu->addSeparator(); - auto* quitAction = menu->addAction(tr("Quit")); - connect(quitAction, &QAction::triggered, this, &TrayIcon::quitRequested); + // Language submenu: one checkable entry per supported language, labels in + // the language itself (nativeName needs no translation). Live-switch — + // the engine persists the tag and the settings poll retranslates the UI. + languageMenu_ = menu_->addMenu(QString()); + languageGroup_ = new QActionGroup(languageMenu_); + languageGroup_->setExclusive(true); + for (const auto& lang : AgentRedactor::SUPPORTED_LANGUAGES) { + const QString tag = QString::fromStdWString(lang.tag); + QAction* action = languageMenu_->addAction(QString::fromStdWString(lang.nativeName)); + action->setCheckable(true); + action->setData(tag); + languageGroup_->addAction(action); + connect(action, &QAction::triggered, this, + [this, tag] { emit languageChangeRequested(tag); }); + } + + menu_->addSeparator(); + quitAction_ = menu_->addAction(QString()); + connect(quitAction_, &QAction::triggered, this, &TrayIcon::quitRequested); - tray_->setContextMenu(menu); + tray_->setContextMenu(menu_); connect(tray_, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::Trigger) emit openRequested(); }); + + retranslate(); } bool TrayIcon::available() const { return tray_ != nullptr; } @@ -39,3 +59,18 @@ void TrayIcon::showIcon() { void TrayIcon::setStartOnBoot(bool enabled) { if (startOnBootAction_) startOnBootAction_->setChecked(enabled); } + +void TrayIcon::setCurrentLanguage(const QString& tag) { + if (!languageMenu_) return; + for (QAction* action : languageMenu_->actions()) + action->setChecked(action->data().toString() == tag); +} + +void TrayIcon::retranslate() { + if (!tray_) return; + tray_->setToolTip(tr("Agent Redactor")); + openAction_->setText(tr("Open Agent Redactor")); + startOnBootAction_->setText(tr("Start on Boot")); + languageMenu_->setTitle(tr("Language")); + quitAction_->setText(tr("Quit")); +} diff --git a/linux/gui/tray_icon.h b/linux/gui/tray_icon.h index 4717f1a..7549027 100644 --- a/linux/gui/tray_icon.h +++ b/linux/gui/tray_icon.h @@ -2,15 +2,18 @@ // System tray for the Linux GUI (QSystemTrayIcon / StatusNotifierItem). // Mirrors the Windows tray: left-click opens the window; menu has Open, -// Start on boot (checkable) and Quit with a confirmation dialog. When no -// system tray is available (plain Wayland GNOME without the AppIndicator -// extension), the app runs as a control-panel window instead — see -// MainWindow's close handling. +// Start on boot (checkable), a Language submenu (built from the shared +// SUPPORTED_LANGUAGES list, checkmark on the active one) and Quit with a +// confirmation dialog. Unlike Windows the language applies live — no +// restart. When no system tray is available (plain Wayland GNOME without +// the AppIndicator extension), the app runs as a control-panel window +// instead — see MainWindow's close handling. #include #include class QAction; +class QActionGroup; class QMenu; class TrayIcon : public QObject { @@ -22,12 +25,25 @@ class TrayIcon : public QObject { void showIcon(); void setStartOnBoot(bool enabled); + // BCP-47 tag from settings; empty/unknown = no checkmark (system default). + void setCurrentLanguage(const QString& tag); + // Re-apply tr() to the persistent menu strings after a language change + // (the menu is built once, so it does not see QEvent::LanguageChange). + void retranslate(); + signals: void openRequested(); void startOnBootToggled(bool enabled); + // Empty tag = system default is not offered here (window Settings only). + void languageChangeRequested(const QString& tag); void quitRequested(); private: QSystemTrayIcon* tray_ = nullptr; + QMenu* menu_ = nullptr; + QAction* openAction_ = nullptr; QAction* startOnBootAction_ = nullptr; + QMenu* languageMenu_ = nullptr; + QActionGroup* languageGroup_ = nullptr; + QAction* quitAction_ = nullptr; }; diff --git a/tests/linux/test_gui_smoke.py b/tests/linux/test_gui_smoke.py index bb1cb2b..92dacd9 100644 --- a/tests/linux/test_gui_smoke.py +++ b/tests/linux/test_gui_smoke.py @@ -36,6 +36,16 @@ or PROJECT_ROOT / "linux" / "build" / "gui" / "agentredactor-gui" ) ENGINE_BIN = PROJECT_ROOT / "linux" / "build" / "engine" / "agentredactor" +I18N_DIR = PROJECT_ROOT / "linux" / "gui" / "i18n" + + +def _cli(config_dir: Path, *args: str) -> subprocess.CompletedProcess: + env = dict(os.environ) + env["AGENTREDACTOR_CONFIG_DIR"] = str(config_dir) + return subprocess.run( + [str(ENGINE_BIN), *args], env=env, + stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=30, + ) def _engine_processes() -> list[psutil.Process]: @@ -243,3 +253,47 @@ def test_autostart_file_reconciled_with_setting(gui_env) -> None: time.sleep(0.2) gui.stop() assert not desktop_file.exists() + + +def test_all_supported_languages_have_catalogs(gui_env) -> None: + """Every language the CLI/engine supports must have a Qt catalog, so the + GUI can never offer a language it cannot display.""" + config_dir, _, proxy_port = gui_env + _start_engine(config_dir, proxy_port) + + r = _cli(config_dir, "languages") + assert r.returncode == 0, r.stdout + r.stderr + tags = [line.strip() for line in r.stdout.splitlines() if line.strip()] + assert len(tags) > 50 + + missing = [ + tag for tag in tags + if tag != "en" # English is the source language; no catalog needed + and not (I18N_DIR / f"agentredactor_{tag.replace('-', '_')}.ts").is_file() + ] + assert not missing, f"languages without a translation catalog: {missing}" + + +def test_gui_applies_language_setting_live(gui_env) -> None: + """Switching app-language via the CLI while the GUI runs is picked up by + the settings poll and retranslates without a restart (and without a + crash — offscreen we cannot assert pixels, but the whole load/retranslate + path executes, including the RTL layout flip).""" + config_dir, xdg_home, proxy_port = gui_env + _start_engine(config_dir, proxy_port) + + gui = GuiProcess(config_dir, xdg_home) + gui.start() + try: + time.sleep(3) + assert gui.process.poll() is None + + for tag in ("de", "ar", "zh-CN", "en"): + r = _cli(config_dir, "set", "app-language", tag) + assert r.returncode == 0, r.stdout + r.stderr + time.sleep(2.5) # at least one settings poll + LanguageChange + assert gui.process.poll() is None, f"GUI died switching to {tag}" + settings = _control_api(config_dir, "/settings") + assert settings.get("appLanguage") == tag + finally: + gui.stop() From 5896e6ed7418446b64d340eed88c570763db7ace Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Thu, 20 Aug 2026 00:15:04 +0000 Subject: [PATCH 14/20] fix(linux): stop update E2E from poisoning real AppImages via shared Velopack state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Velopack keeps downloaded packages in a machine-wide /var/tmp/velopack/ dir that the test's HOME/XDG isolation does not cover. The vNext package staged by the self-update E2E survived the test, and the next real AppImage launch applied it in place — silently reverting the user's freshly packed AppImage to the older test build. The fixture now purges that state dir before and after the test. Also: skip CLI symlink creation under AppImage runs (applicationDirPath is an ephemeral /tmp/.mount_* there, so ~/.local/bin/agentredactor dangled after exit) and clean up such dangling links. --- linux/gui/main.cpp | 22 +++++++++++++++++++--- tests/linux/test_update_feed.py | 14 ++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/linux/gui/main.cpp b/linux/gui/main.cpp index 0556c99..e5fc2b6 100644 --- a/linux/gui/main.cpp +++ b/linux/gui/main.cpp @@ -3,6 +3,7 @@ // window / tray. All backend logic lives in the engine process. #include +#include #include #include @@ -40,15 +41,30 @@ void onSignal(int sig) { // Terminal discoverability: expose the bundled engine/CLI binary as // ~/.local/bin/agentredactor. Best-effort and idempotent; only done in the // installed layout (engine next to the GUI binary), never in the dev tree. +// Skipped under an AppImage: applicationDirPath is then an ephemeral +// /tmp/.mount_* and the symlink would dangle as soon as the app exits. void EnsureCliSymlink() { namespace fs = std::filesystem; - const fs::path engine = - fs::path(QCoreApplication::applicationDirPath().toStdString()) / "agentredactor"; - if (!fs::exists(engine)) return; const fs::path binDir = fs::path(QStandardPaths::writableLocation(QStandardPaths::HomeLocation).toStdString()) / ".local" / "bin"; const fs::path link = binDir / "agentredactor"; + + const bool appImageRun = std::getenv("APPIMAGE") != nullptr || + QCoreApplication::applicationDirPath().startsWith(QLatin1String("/tmp/.mount_")); + if (appImageRun) { + // Clean up a dangling link left by an earlier AppImage run. + std::error_code ec; + if (fs::is_symlink(link, ec) && + fs::read_symlink(link, ec).string().rfind("/tmp/.mount_", 0) == 0) { + fs::remove(link, ec); + } + return; + } + + const fs::path engine = + fs::path(QCoreApplication::applicationDirPath().toStdString()) / "agentredactor"; + if (!fs::exists(engine)) return; std::error_code ec; if (fs::exists(link, ec) || fs::is_symlink(link, ec)) { if (fs::read_symlink(link, ec) == engine) return; // already correct diff --git a/tests/linux/test_update_feed.py b/tests/linux/test_update_feed.py index 996519c..63cc091 100644 --- a/tests/linux/test_update_feed.py +++ b/tests/linux/test_update_feed.py @@ -8,6 +8,12 @@ Skipped unless the release pack has been built (linux/build-release/velopack) and vpk is available to pack the vNext feed. Unlike the regular smoke tests this exercises the AR_SELFRELEASE build, not the dev-tree binary. + +Note: Velopack keeps downloaded packages in a fixed machine-wide state dir +(/var/tmp/velopack/) that does NOT follow the test's isolated +HOME/XDG. The fixture purges it before and after — a leftover vNext package +there would otherwise be applied to ANY AgentRedactor AppImage started later +(including a freshly packed one), silently reverting it to the test build. """ from __future__ import annotations @@ -41,6 +47,9 @@ APPDIR = RELEASE_DIR / "appdir" APPIMAGE = VELOPACK_OUT / "AgentRedactor.AppImage" +# Velopack's machine-wide package cache/staging for this packId. +VELOPACK_STATE = Path("/var/tmp/velopack/AgentRedactor") + POLL_TIMEOUT_S = 180.0 POLL_INTERVAL_S = 2.0 @@ -91,6 +100,8 @@ def update_env(tmp_path: Path): if not vpk: pytest.skip("vpk (Velopack CLI) not installed") _kill_existing_agent_redactor() + # No leftover staged packages from a previous run (theirs or ours). + shutil.rmtree(VELOPACK_STATE, ignore_errors=True) yield tmp_path, vpk _kill_existing_agent_redactor() for p in _gui_processes_for(tmp_path): @@ -98,6 +109,9 @@ def update_env(tmp_path: Path): p.kill() except psutil.NoSuchProcess: pass + # Never leave the test's vNext package in the machine-wide Velopack + # state dir: the next real AppImage launch would apply it in place. + shutil.rmtree(VELOPACK_STATE, ignore_errors=True) def test_appimage_self_updates_against_local_feed(update_env) -> None: From 40672fc65185665eff5ab3a4fbeecc23ae59b97e Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Thu, 20 Aug 2026 01:58:55 +0000 Subject: [PATCH 15/20] fix(linux): ship model companions in AppImage; apply language changes immediately - build-release.sh stages the small model files (tokenizer/config/ calibration/model graph) next to the binaries, mirroring the Windows self-release split (build.ps1 /XF *.onnx_data). Without them EnsureModelFiles could never start the first-run weight download, so AppImage installs on clean machines stayed stuck with no detector. - GUI applies a newly selected language (Settings combo and tray menu) immediately instead of waiting for the settings-poll round-trip. --- linux/README.md | 7 +++++++ linux/build-release.sh | 11 +++++++++++ linux/gui/main_window.cpp | 10 +++++++--- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/linux/README.md b/linux/README.md index 4ba945d..5b53279 100644 --- a/linux/README.md +++ b/linux/README.md @@ -116,6 +116,13 @@ stops and respawns it. Qt is bundled dynamically linked inside the AppImage with `LGPL-Qt-notice.txt`. First run symlinks the CLI to `~/.local/bin/agentredactor`. +Model files follow the Windows self-release split: the small companions +(`config.json`, `tokenizer.json`, `viterbi_calibration.json`, +`onnx/model_quantized.onnx` from `windows/models/`) ship inside the package +next to the binaries, while the ~1.6 GB `onnx/model_quantized.onnx_data` +weights download on first run from the R2 endpoint into +`~/.local/share/agentredactor/models/` (see `core/src/model_downloader.cpp`). + Test hooks (self-release builds only, same contract as Windows): `AGENTREDACTOR_UPDATE_FEED` overrides the feed URL (loopback http only) and `AGENTREDACTOR_UPDATE_AUTOAPPLY=1` skips the restart prompt. diff --git a/linux/build-release.sh b/linux/build-release.sh index 7e9b9d6..8d8b0bd 100755 --- a/linux/build-release.sh +++ b/linux/build-release.sh @@ -65,6 +65,17 @@ cp "${ROOT}/third_party/velopack/lib/velopack_libc_linux_${VP_ARCH}_gnu.so" \ "${STAGE}/libvelopack_libc_linux_${VP_ARCH}_gnu.so" cp "${ONNX_LIB}" "${STAGE}/" +# Model companions ship in the package next to the exe (same split as the +# Windows self-release channel, build.ps1 /XF *.onnx_data): the 1.6 GB weights +# download on first run from the R2 endpoint, but the tokenizer / config / +# calibration / model graph must be present or EnsureModelFiles cannot start +# (core/src/model_downloader.cpp kCompanionFiles). +MODELS_SRC="${ROOT}/../windows/models" +mkdir -p "${STAGE}/models/onnx" +cp "${MODELS_SRC}/config.json" "${MODELS_SRC}/tokenizer.json" \ + "${MODELS_SRC}/viterbi_calibration.json" "${STAGE}/models/" +cp "${MODELS_SRC}/onnx/model_quantized.onnx" "${STAGE}/models/onnx/" + # Bundle the shared libraries the two binaries resolve to, minus the # AppImage-standard system set that must come from the host. EXCLUDE='^(linux-vdso|ld-linux|libc|libm|libdl|librt|libpthread|libresolv|libnsl|libutil|libz|libGL|libEGL|libX11|libxcb|libXau|libXdmcp|libdrm|libgbm|libwayland-|libxkbcommon|libfontconfig|libfreetype|libexpat|libdbus-1|libsystemd|libglib-2.0|libgobject-2.0|libgio-2.0)\.so' diff --git a/linux/gui/main_window.cpp b/linux/gui/main_window.cpp index c1544ca..f0678e8 100644 --- a/linux/gui/main_window.cpp +++ b/linux/gui/main_window.cpp @@ -72,8 +72,10 @@ MainWindow::MainWindow(AppState* appState, TrayIcon* tray, TranslatorLoader* tra connect(appState_, &AppState::modelDownloadChanged, this, &MainWindow::onModelDownloadChanged); connect(appState_, &AppState::connectionLost, this, &MainWindow::onConnectionLost); connect(tray_, &TrayIcon::languageChangeRequested, this, [this](const QString& tag) { - // The settings poll picks up the change and retranslates everything - // live (no restart, unlike Windows). + // Apply immediately so the UI switches without waiting for the + // settings-poll round-trip; the poll later reconciles the persisted + // tag (no restart, unlike Windows). + translator_->applyLanguage(tag); appState_->client().PutSetting(L"appLanguage", tag.toStdString()); }); @@ -1179,8 +1181,10 @@ void MainWindow::onStartOnBootToggled(bool checked) { void MainWindow::onLanguageSelected(int index) { if (loading_) return; // Empty data = "System default"; the engine resolves it from the OS - // locale. The poll then re-applies the language live to every widget. + // locale. Apply immediately so the UI switches without waiting for the + // settings-poll round-trip; the poll later re-applies the persisted tag. const QString tag = languageCombo_->itemData(index).toString(); + translator_->applyLanguage(tag); appState_->client().PutSetting(L"appLanguage", tag.toStdString()); } From 0765d9311e3f525bd2e113dbc13936045edacd86 Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Thu, 20 Aug 2026 03:03:20 +0000 Subject: [PATCH 16/20] fix(core): atomic cross-process appends for agent_redactor.log on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The engine and the UI both append to the shared log. The CRT append stream seeks to EOF and then writes, so racing processes overwrite each other's lines — seen in the Self-Release E2E as a mangled '[UpdateManager] Up to date' line (only '.5, latest 1.1.5)' survived), which the updater health check polls for. Open the log with FILE_APPEND_DATA only (no FILE_WRITE_DATA) so every WriteFile lands atomically at EOF regardless of the file pointer, and write UTF-8 (previously the CRT 'C' locale mangled non-ASCII to '?', matching the Linux side which already writes UTF-8 under O_APPEND). --- core/src/utils.cpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/core/src/utils.cpp b/core/src/utils.cpp index d4a6a66..f9e302c 100644 --- a/core/src/utils.cpp +++ b/core/src/utils.cpp @@ -123,14 +123,32 @@ static void WriteLogLine(const std::wstring& message) { if (g_logFilePath.empty()) InitializeLogging(); std::lock_guard lock(g_logMutex); try { + auto now = std::chrono::system_clock::now(); + auto time = std::chrono::system_clock::to_time_t(now); + std::wstring timeStr = FormatLocalizedDateTime(time); +#ifdef _WIN32 + // The engine and the UI both append to this file. A CRT append stream + // seeks to EOF and then writes, so two processes racing that pattern + // overwrite each other's lines (seen in CI as a mangled + // "[UpdateManager] Up to date" line). A handle opened with + // FILE_APPEND_DATA only (no FILE_WRITE_DATA) ignores the file pointer + // and appends every WriteFile atomically at EOF. + HANDLE h = CreateFileW(g_logFilePath.c_str(), FILE_APPEND_DATA, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (h != INVALID_HANDLE_VALUE) { + const std::string utf8 = WideToUtf8(L"[" + timeStr + L"] " + message + L"\r\n"); + DWORD written = 0; + WriteFile(h, utf8.data(), static_cast(utf8.size()), &written, nullptr); + CloseHandle(h); + } +#else std::wofstream logFile(g_logFilePath, std::ios::app); if (logFile) { - auto now = std::chrono::system_clock::now(); - auto time = std::chrono::system_clock::to_time_t(now); - std::wstring timeStr = FormatLocalizedDateTime(time); logFile << L"[" << timeStr << L"] " << message << std::endl; logFile.flush(); } +#endif } catch (...) {} OutputDebugStringW((message + L"\n").c_str()); } From acd66a82b4a4da4a982a0cb464a5e4ae9a287177 Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Thu, 20 Aug 2026 05:00:05 +0000 Subject: [PATCH 17/20] feat(linux): expose the CLI as 'agentredactor' on PATH for AppImage users Running the AppImage once now drops a two-line wrapper at ~/.local/bin/agentredactor that re-launches the AppImage file with --cli; the GUI binary (before Velopack/Qt startup) execs the bundled dual-mode engine/CLI binary with the remaining args. A symlink cannot reach inside the ephemeral /tmp/.mount_* AppImage mount, which is why the shim was previously skipped entirely for AppImage runs. The wrapper is rewritten on every launch, so moving the AppImage self-heals on the next run. Installed layouts keep the direct symlink to the binary. The update E2E now asserts the shim is created and drives the full chain (wrapper -> AppImage -> --cli -> usage output) end-to-end. --- linux/README.md | 8 ++- linux/gui/main.cpp | 90 +++++++++++++++++++++++++++++---- tests/linux/test_update_feed.py | 21 ++++++++ 3 files changed, 108 insertions(+), 11 deletions(-) diff --git a/linux/README.md b/linux/README.md index 5b53279..bf1bd5d 100644 --- a/linux/README.md +++ b/linux/README.md @@ -113,8 +113,12 @@ Settings (self-release builds only); when an update is downloaded the app offers "Restart now / later", applies via Velopack, and restarts. The engine binary ships inside the AppImage next to the GUI; on version mismatch the GUI stops and respawns it. Qt is bundled dynamically linked inside the AppImage -with `LGPL-Qt-notice.txt`. First run symlinks the CLI to -`~/.local/bin/agentredactor`. +with `LGPL-Qt-notice.txt`. First run exposes the CLI as +`~/.local/bin/agentredactor`: a plain symlink in installed layouts, and under +an AppImage a two-line wrapper that re-runs the AppImage file with `--cli` +(the GUI binary then execs the bundled dual-mode engine/CLI binary; a symlink +cannot reach inside the ephemeral mount). The wrapper is rewritten on every +launch, so moving the AppImage self-heals on the next run. Model files follow the Windows self-release split: the small companions (`config.json`, `tokenizer.json`, `viterbi_calibration.json`, diff --git a/linux/gui/main.cpp b/linux/gui/main.cpp index e5fc2b6..1def1e0 100644 --- a/linux/gui/main.cpp +++ b/linux/gui/main.cpp @@ -2,9 +2,15 @@ // engine is running (spawning it detached when not), and shows the main // window / tray. All backend logic lives in the engine process. +#include #include +#include #include +#include #include +#include +#include +#include #include #include @@ -38,27 +44,90 @@ void onSignal(int sig) { } } -// Terminal discoverability: expose the bundled engine/CLI binary as -// ~/.local/bin/agentredactor. Best-effort and idempotent; only done in the -// installed layout (engine next to the GUI binary), never in the dev tree. -// Skipped under an AppImage: applicationDirPath is then an ephemeral -// /tmp/.mount_* and the symlink would dangle as soon as the app exits. -void EnsureCliSymlink() { +// CLI pass-through: " --cli " re-execs the sibling dual-mode +// engine/CLI binary with the remaining args. This is how the +// ~/.local/bin/agentredactor wrapper reaches the CLI inside an AppImage: the +// wrapper re-launches the AppImage file with --cli, the AppImage runtime +// mounts and starts this binary, and we hand off to the real CLI. Runs before +// Velopack/Qt startup so CLI calls stay fast and never parse GUI flags. +int ForwardToCli(int argc, char* argv[]) { + namespace fs = std::filesystem; + std::error_code ec; + const fs::path self = fs::read_symlink("/proc/self/exe", ec); + const fs::path cliBin = ec ? fs::path() : self.parent_path() / "agentredactor"; + + std::vector args; + args.push_back(cliBin.string()); + for (int i = 2; i < argc; ++i) args.emplace_back(argv[i]); + std::vector cargv; + for (auto& a : args) cargv.push_back(a.data()); + cargv.push_back(nullptr); + + execv(cliBin.c_str(), cargv.data()); + std::fprintf(stderr, "agentredactor: could not launch the bundled CLI (%s)\n", + std::strerror(errno)); + return 1; +} + +// Shell-quote a path for embedding in a double-quoted wrapper script. +std::string ShellQuoteDouble(const std::string& s) { + std::string out; + for (char c : s) { + if (c == '"' || c == '\\' || c == '$' || c == '`') out += '\\'; + out += c; + } + return out; +} + +// Terminal discoverability: expose the CLI as ~/.local/bin/agentredactor. +// Best-effort and idempotent. +// - Installed/dev layout (engine binary next to the GUI): plain symlink to +// the dual-mode binary. +// - AppImage: the CLI binary lives inside the ephemeral /tmp/.mount_* so a +// symlink cannot reach it; instead drop a two-line wrapper that re-runs +// the AppImage file ($APPIMAGE is the stable path) with --cli. Rewritten +// on every launch so moving the AppImage self-heals on the next run. +void EnsureCliShim() { namespace fs = std::filesystem; const fs::path binDir = fs::path(QStandardPaths::writableLocation(QStandardPaths::HomeLocation).toStdString()) / ".local" / "bin"; const fs::path link = binDir / "agentredactor"; - const bool appImageRun = std::getenv("APPIMAGE") != nullptr || + const char* appImageEnv = std::getenv("APPIMAGE"); + const bool appImageRun = (appImageEnv && *appImageEnv) || QCoreApplication::applicationDirPath().startsWith(QLatin1String("/tmp/.mount_")); if (appImageRun) { - // Clean up a dangling link left by an earlier AppImage run. + // Clean up a dangling symlink left by an earlier AppImage run. std::error_code ec; if (fs::is_symlink(link, ec) && fs::read_symlink(link, ec).string().rfind("/tmp/.mount_", 0) == 0) { fs::remove(link, ec); } + if (!appImageEnv || !*appImageEnv) return; // extract-and-run: no stable path + + std::error_code ec2; + fs::create_directories(binDir, ec2); + const std::string script = "#!/bin/sh\nexec \"" + + ShellQuoteDouble(appImageEnv) + "\" --cli \"$@\"\n"; + bool upToDate = false; + { + std::ifstream in(link, std::ios::binary); + if (in) upToDate = std::string(std::istreambuf_iterator(in), + std::istreambuf_iterator()) == script; + } + if (!upToDate) { + // Not atomic, but a torn half-written wrapper just fails its next + // exec and is rewritten on the following app launch. + std::ofstream out(link, std::ios::binary | std::ios::trunc); + out << script; + out.close(); + fs::permissions(link, fs::perms::owner_all | fs::perms::group_read | + fs::perms::group_exec | fs::perms::others_read | fs::perms::others_exec, + fs::perm_options::replace, ec2); + if (ec2) qWarning("[main] could not write CLI wrapper %s: %s", + link.c_str(), ec2.message().c_str()); + } return; } @@ -79,6 +148,9 @@ void EnsureCliSymlink() { } // namespace int main(int argc, char* argv[]) { + if (argc > 1 && std::strcmp(argv[1], "--cli") == 0) { + return ForwardToCli(argc, argv); + } #ifdef AR_SELFRELEASE // Velopack startup logic: handles post-update restart/apply hooks and may // exit or restart the process. Must run before anything else. @@ -111,7 +183,7 @@ int main(int argc, char* argv[]) { const bool trayOnly = QApplication::arguments().contains(QLatin1String("--tray-only")); - EnsureCliSymlink(); + EnsureCliShim(); TranslatorLoader translator(app); diff --git a/tests/linux/test_update_feed.py b/tests/linux/test_update_feed.py index 63cc091..7096487 100644 --- a/tests/linux/test_update_feed.py +++ b/tests/linux/test_update_feed.py @@ -207,3 +207,24 @@ def test_appimage_self_updates_against_local_feed(update_env) -> None: pass assert _sha256(app) != before + + # 7. First-run CLI shim: launching the AppImage drops a wrapper at + # ~/.local/bin/agentredactor that re-invokes the AppImage with --cli. + # Drive the full chain end-to-end: wrapper -> AppImage runtime -> GUI + # binary -> exec of the bundled dual-mode CLI binary. + shim = home_dir / ".local" / "bin" / "agentredactor" + assert shim.is_file(), f"CLI shim missing at {shim}" + assert os.access(shim, os.X_OK) + assert str(app) in shim.read_text(encoding="utf-8") + cli_env = dict(os.environ) + cli_env.update({ + "AGENTREDACTOR_CONFIG_DIR": str(config_dir), + "HOME": str(home_dir), + "XDG_CONFIG_HOME": str(home_dir / ".config"), + "XDG_DATA_HOME": str(home_dir / ".local" / "share"), + }) + cli = subprocess.run( + [str(shim), "help"], env=cli_env, + capture_output=True, text=True, timeout=60) + assert cli.returncode == 0, f"CLI via shim failed: {cli.stdout} {cli.stderr}" + assert "profiles" in cli.stdout and "keywords" in cli.stdout From 3f74d9a7dfda82bc19bdc657f8ec5e2c31277248 Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Thu, 20 Aug 2026 06:56:35 +0000 Subject: [PATCH 18/20] chore(scripts): add linux-clean-slate.sh to reset a test machine Stops app processes, unmounts stale AppImage mounts, and removes config, logs, downloaded model weights, the CLI shim, the autostart entry and Velopack staging so a machine can be returned to a first-run state. --- scripts/linux-clean-slate.sh | 60 ++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100755 scripts/linux-clean-slate.sh diff --git a/scripts/linux-clean-slate.sh b/scripts/linux-clean-slate.sh new file mode 100755 index 0000000..632fa04 --- /dev/null +++ b/scripts/linux-clean-slate.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Agent Redactor — return a Linux machine to a clean slate for testing. +# +# Removes every trace the app leaves behind: +# - running GUI/engine processes (including AppImage-mounted ones) +# - stale /tmp/.mount_AgentR* FUSE mounts from killed AppImage runs +# - config, logs, sessions, lock and control files (~/.config/agentredactor) +# - downloaded AI model files (~/.local/share/agentredactor, incl. the +# ~1.6 GB ONNX weights — the next run re-downloads them) +# - the ~/.local/bin/agentredactor CLI shim +# - the XDG autostart entry (~/.config/autostart/agentredactor.desktop) +# - Velopack update staging (/var/tmp/velopack/AgentRedactor) +# +# It does NOT delete any .AppImage file you downloaded — remove that yourself. +# +# Usage: +# scripts/linux-clean-slate.sh # asks for confirmation +# scripts/linux-clean-slate.sh --yes # no prompt +set -u + +XDG_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}" +XDG_DATA="${XDG_DATA_HOME:-$HOME/.local/share}" + +if [ "${1:-}" != "--yes" ]; then + echo "This removes ALL Agent Redactor state from this machine, including" + echo "settings, logs and the downloaded AI model (re-downloaded on next run)." + printf "Continue? [y/N] " + read -r answer + case "$answer" in y|Y|yes|YES) ;; *) echo "Aborted."; exit 1;; esac +fi + +echo "==> Stopping app processes" +pkill -x agentredactor 2>/dev/null # engine / CLI (any layout) +pkill -f '/agentredactor-gui' 2>/dev/null # GUI (comm name is truncated, so match the path) +sleep 1 + +echo "==> Unmounting stale AppImage mounts" +for m in /tmp/.mount_AgentR*; do + [ -e "$m" ] || continue + fusermount3 -u "$m" 2>/dev/null || fusermount -u "$m" 2>/dev/null + rmdir "$m" 2>/dev/null || echo " still busy (a running app?): $m" +done + +remove() { + if [ -e "$1" ] || [ -L "$1" ]; then + rm -rf -- "$1" && echo " removed $1" + else + echo " absent $1" + fi +} + +echo "==> Removing app state" +remove "$XDG_CONFIG/agentredactor" # settings, logs, sessions, control.json +remove "$XDG_DATA/agentredactor" # downloaded AI model files +remove "$HOME/.local/bin/agentredactor" # CLI shim (symlink or wrapper) +remove "$XDG_CONFIG/autostart/agentredactor.desktop" # start-on-boot entry +remove "/var/tmp/velopack/AgentRedactor" # Velopack update staging +remove "/tmp/velopack_AgentRedactor.log" # Velopack log + +echo "==> Done. Verify with: ls $XDG_CONFIG $XDG_DATA | grep -i redactor (expect nothing)" From e300e0d8e7b1330c9a43f423dca3a93a113a451f Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Thu, 20 Aug 2026 06:56:35 +0000 Subject: [PATCH 19/20] fix(linux): fit window to work area; seed a default profile on first run - The window hard-coded 1000x900; on a 1280x800 screen that opens taller than the display and, since Wayland apps cannot reposition themselves, the title bar can be lost off the top edge for good. Clamp the initial size to the available work area (content already scrolls). - Mirror the Windows GUI (HomePage::LoadProfileList): when the engine reports zero profiles, seed one named 'Default' on the first free port from 8080 instead of landing the user on an empty form. The alias rides the existing Windows resw translations (sync_ts.py reuse, 52 catalogs). --- linux/gui/i18n/agentredactor_af.ts | 8 +++-- linux/gui/i18n/agentredactor_ar.ts | 8 +++-- linux/gui/i18n/agentredactor_az_Latn.ts | 8 +++-- linux/gui/i18n/agentredactor_bg.ts | 8 +++-- linux/gui/i18n/agentredactor_cs.ts | 8 +++-- linux/gui/i18n/agentredactor_da.ts | 8 +++-- linux/gui/i18n/agentredactor_de.ts | 8 +++-- linux/gui/i18n/agentredactor_el.ts | 8 +++-- linux/gui/i18n/agentredactor_es.ts | 8 +++-- linux/gui/i18n/agentredactor_et.ts | 8 +++-- linux/gui/i18n/agentredactor_fi.ts | 8 +++-- linux/gui/i18n/agentredactor_fil.ts | 8 +++-- linux/gui/i18n/agentredactor_fr.ts | 8 +++-- linux/gui/i18n/agentredactor_ha_Latn.ts | 8 +++-- linux/gui/i18n/agentredactor_he.ts | 8 +++-- linux/gui/i18n/agentredactor_hi.ts | 8 +++-- linux/gui/i18n/agentredactor_hr.ts | 8 +++-- linux/gui/i18n/agentredactor_hu.ts | 8 +++-- linux/gui/i18n/agentredactor_hy.ts | 8 +++-- linux/gui/i18n/agentredactor_id.ts | 8 +++-- linux/gui/i18n/agentredactor_ig_NG.ts | 8 +++-- linux/gui/i18n/agentredactor_is.ts | 8 +++-- linux/gui/i18n/agentredactor_it.ts | 8 +++-- linux/gui/i18n/agentredactor_ja.ts | 8 +++-- linux/gui/i18n/agentredactor_ka.ts | 8 +++-- linux/gui/i18n/agentredactor_kk.ts | 8 +++-- linux/gui/i18n/agentredactor_ko.ts | 8 +++-- linux/gui/i18n/agentredactor_lb.ts | 8 +++-- linux/gui/i18n/agentredactor_lt.ts | 8 +++-- linux/gui/i18n/agentredactor_lv.ts | 8 +++-- linux/gui/i18n/agentredactor_ms.ts | 8 +++-- linux/gui/i18n/agentredactor_mt.ts | 8 +++-- linux/gui/i18n/agentredactor_nb.ts | 8 +++-- linux/gui/i18n/agentredactor_nl.ts | 8 +++-- linux/gui/i18n/agentredactor_pl.ts | 8 +++-- linux/gui/i18n/agentredactor_pt.ts | 8 +++-- linux/gui/i18n/agentredactor_ro.ts | 8 +++-- linux/gui/i18n/agentredactor_ru.ts | 8 +++-- linux/gui/i18n/agentredactor_sk.ts | 8 +++-- linux/gui/i18n/agentredactor_sl.ts | 8 +++-- linux/gui/i18n/agentredactor_sq.ts | 8 +++-- linux/gui/i18n/agentredactor_sr_Latn.ts | 8 +++-- linux/gui/i18n/agentredactor_sv.ts | 8 +++-- linux/gui/i18n/agentredactor_sw.ts | 8 +++-- linux/gui/i18n/agentredactor_ta.ts | 8 +++-- linux/gui/i18n/agentredactor_th.ts | 8 +++-- linux/gui/i18n/agentredactor_tr.ts | 8 +++-- linux/gui/i18n/agentredactor_uk.ts | 8 +++-- linux/gui/i18n/agentredactor_ur.ts | 8 +++-- linux/gui/i18n/agentredactor_vi.ts | 8 +++-- linux/gui/i18n/agentredactor_zh_CN.ts | 8 +++-- linux/gui/i18n/agentredactor_zh_TW.ts | 8 +++-- linux/gui/main_window.cpp | 41 ++++++++++++++++++++++++- 53 files changed, 352 insertions(+), 105 deletions(-) diff --git a/linux/gui/i18n/agentredactor_af.ts b/linux/gui/i18n/agentredactor_af.ts index d64053e..ec3de18 100644 --- a/linux/gui/i18n/agentredactor_af.ts +++ b/linux/gui/i18n/agentredactor_af.ts @@ -164,7 +164,7 @@ Start on Boot - Begin op Boot + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Enjin loop nie – probeer weer … + + Default + Verstek + Delete Vee uit @@ -363,7 +367,7 @@ Start on Boot - Begin op Boot + Language diff --git a/linux/gui/i18n/agentredactor_ar.ts b/linux/gui/i18n/agentredactor_ar.ts index 39b3e86..81cd401 100644 --- a/linux/gui/i18n/agentredactor_ar.ts +++ b/linux/gui/i18n/agentredactor_ar.ts @@ -164,7 +164,7 @@ Start on Boot - البدء في التمهيد + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… المحرك لا يعمل — جارٍ إعادة المحاولة... + + Default + تقصير + Delete يمسح @@ -363,7 +367,7 @@ Start on Boot - البدء في التمهيد + Language diff --git a/linux/gui/i18n/agentredactor_az_Latn.ts b/linux/gui/i18n/agentredactor_az_Latn.ts index fd6214d..98dfd13 100644 --- a/linux/gui/i18n/agentredactor_az_Latn.ts +++ b/linux/gui/i18n/agentredactor_az_Latn.ts @@ -164,7 +164,7 @@ Start on Boot - Boot-da başlayın + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Mühərrik işləmir — yenidən cəhd edilir... + + Default + Defolt + Delete Sil @@ -363,7 +367,7 @@ Start on Boot - Boot-da başlayın + Language diff --git a/linux/gui/i18n/agentredactor_bg.ts b/linux/gui/i18n/agentredactor_bg.ts index 430e81d..b075f79 100644 --- a/linux/gui/i18n/agentredactor_bg.ts +++ b/linux/gui/i18n/agentredactor_bg.ts @@ -164,7 +164,7 @@ Start on Boot - Стартирайте при зареждане + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Двигателят не работи — опитвам отново... + + Default + По подразбиране + Delete Изтриване @@ -363,7 +367,7 @@ Start on Boot - Стартирайте при зареждане + Language diff --git a/linux/gui/i18n/agentredactor_cs.ts b/linux/gui/i18n/agentredactor_cs.ts index d97c108..4d5e4e5 100644 --- a/linux/gui/i18n/agentredactor_cs.ts +++ b/linux/gui/i18n/agentredactor_cs.ts @@ -164,7 +164,7 @@ Start on Boot - Začněte při spuštění + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Motor neběží – opakování… + + Default + Výchozí + Delete Vymazat @@ -363,7 +367,7 @@ Start on Boot - Začněte při spuštění + Language diff --git a/linux/gui/i18n/agentredactor_da.ts b/linux/gui/i18n/agentredactor_da.ts index 5808127..e468ad2 100644 --- a/linux/gui/i18n/agentredactor_da.ts +++ b/linux/gui/i18n/agentredactor_da.ts @@ -164,7 +164,7 @@ Start on Boot - Start på Boot + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Motoren kører ikke - prøver igen... + + Default + Standard + Delete Slet @@ -363,7 +367,7 @@ Start on Boot - Start på Boot + Language diff --git a/linux/gui/i18n/agentredactor_de.ts b/linux/gui/i18n/agentredactor_de.ts index 93f2167..d296fba 100644 --- a/linux/gui/i18n/agentredactor_de.ts +++ b/linux/gui/i18n/agentredactor_de.ts @@ -164,7 +164,7 @@ Start on Boot - Beginnen Sie beim Booten + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Motor läuft nicht – erneuter Versuch… + + Default + Standard + Delete Löschen @@ -363,7 +367,7 @@ Start on Boot - Beginnen Sie beim Booten + Language diff --git a/linux/gui/i18n/agentredactor_el.ts b/linux/gui/i18n/agentredactor_el.ts index 5591d86..b2e6c9c 100644 --- a/linux/gui/i18n/agentredactor_el.ts +++ b/linux/gui/i18n/agentredactor_el.ts @@ -164,7 +164,7 @@ Start on Boot - Ξεκινήστε από την εκκίνηση + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Ο κινητήρας δεν λειτουργεί — επανάληψη… + + Default + Προεπιλογή + Delete Διαγράφω @@ -363,7 +367,7 @@ Start on Boot - Ξεκινήστε από την εκκίνηση + Language diff --git a/linux/gui/i18n/agentredactor_es.ts b/linux/gui/i18n/agentredactor_es.ts index 51a9dfe..9110a5f 100644 --- a/linux/gui/i18n/agentredactor_es.ts +++ b/linux/gui/i18n/agentredactor_es.ts @@ -164,7 +164,7 @@ Start on Boot - Comenzar al arrancar + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… El motor no está funcionando. Volviendo a intentarlo... + + Default + Predeterminado + Delete Borrar @@ -363,7 +367,7 @@ Start on Boot - Comenzar al arrancar + Language diff --git a/linux/gui/i18n/agentredactor_et.ts b/linux/gui/i18n/agentredactor_et.ts index 9e58191..f35dd86 100644 --- a/linux/gui/i18n/agentredactor_et.ts +++ b/linux/gui/i18n/agentredactor_et.ts @@ -164,7 +164,7 @@ Start on Boot - Alustage alglaadimisest + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Mootor ei tööta – proovitakse uuesti… + + Default + Vaikimisi + Delete Kustuta @@ -363,7 +367,7 @@ Start on Boot - Alustage alglaadimisest + Language diff --git a/linux/gui/i18n/agentredactor_fi.ts b/linux/gui/i18n/agentredactor_fi.ts index 8645919..359d0ff 100644 --- a/linux/gui/i18n/agentredactor_fi.ts +++ b/linux/gui/i18n/agentredactor_fi.ts @@ -164,7 +164,7 @@ Start on Boot - Aloita Bootista + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Moottori ei käy – yritetään uudelleen… + + Default + Oletus + Delete Poistaa @@ -363,7 +367,7 @@ Start on Boot - Aloita Bootista + Language diff --git a/linux/gui/i18n/agentredactor_fil.ts b/linux/gui/i18n/agentredactor_fil.ts index 80dddbe..a51c78f 100644 --- a/linux/gui/i18n/agentredactor_fil.ts +++ b/linux/gui/i18n/agentredactor_fil.ts @@ -164,7 +164,7 @@ Start on Boot - Magsimula sa Boot + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Hindi tumatakbo ang makina — muling sinusubukan... + + Default + Default + Delete Tanggalin @@ -363,7 +367,7 @@ Start on Boot - Magsimula sa Boot + Language diff --git a/linux/gui/i18n/agentredactor_fr.ts b/linux/gui/i18n/agentredactor_fr.ts index 380c868..35986a3 100644 --- a/linux/gui/i18n/agentredactor_fr.ts +++ b/linux/gui/i18n/agentredactor_fr.ts @@ -164,7 +164,7 @@ Start on Boot - Démarrer au démarrage + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Le moteur ne tourne pas – réessayez… + + Default + Par défaut + Delete Supprimer @@ -363,7 +367,7 @@ Start on Boot - Démarrer au démarrage + Language diff --git a/linux/gui/i18n/agentredactor_ha_Latn.ts b/linux/gui/i18n/agentredactor_ha_Latn.ts index bd5faf2..552ae09 100644 --- a/linux/gui/i18n/agentredactor_ha_Latn.ts +++ b/linux/gui/i18n/agentredactor_ha_Latn.ts @@ -164,7 +164,7 @@ Start on Boot - Fara a kan Boot + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Injin baya aiki - sake gwadawa… + + Default + Default + Delete Share @@ -363,7 +367,7 @@ Start on Boot - Fara a kan Boot + Language diff --git a/linux/gui/i18n/agentredactor_he.ts b/linux/gui/i18n/agentredactor_he.ts index 12e7606..4fde779 100644 --- a/linux/gui/i18n/agentredactor_he.ts +++ b/linux/gui/i18n/agentredactor_he.ts @@ -164,7 +164,7 @@ Start on Boot - התחל באתחול + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… המנוע לא פועל - מנסה שוב... + + Default + ברירת מחדל + Delete לִמְחוֹק @@ -363,7 +367,7 @@ Start on Boot - התחל באתחול + Language diff --git a/linux/gui/i18n/agentredactor_hi.ts b/linux/gui/i18n/agentredactor_hi.ts index 11d77a8..2341541 100644 --- a/linux/gui/i18n/agentredactor_hi.ts +++ b/linux/gui/i18n/agentredactor_hi.ts @@ -164,7 +164,7 @@ Start on Boot - बूट पर प्रारंभ करें + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… इंजन नहीं चल रहा है - पुनः प्रयास किया जा रहा है... + + Default + डिफ़ॉल्ट + Delete मिटाना @@ -363,7 +367,7 @@ Start on Boot - बूट पर प्रारंभ करें + Language diff --git a/linux/gui/i18n/agentredactor_hr.ts b/linux/gui/i18n/agentredactor_hr.ts index 191b92c..fc99168 100644 --- a/linux/gui/i18n/agentredactor_hr.ts +++ b/linux/gui/i18n/agentredactor_hr.ts @@ -164,7 +164,7 @@ Start on Boot - Pokrenite na Boot + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Motor ne radi — ponovni pokušaj… + + Default + Zadano + Delete Izbrisati @@ -363,7 +367,7 @@ Start on Boot - Pokrenite na Boot + Language diff --git a/linux/gui/i18n/agentredactor_hu.ts b/linux/gui/i18n/agentredactor_hu.ts index 4bf8cee..dc7de58 100644 --- a/linux/gui/i18n/agentredactor_hu.ts +++ b/linux/gui/i18n/agentredactor_hu.ts @@ -164,7 +164,7 @@ Start on Boot - Indítsa el a Boot-on + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… A motor nem jár – újrapróbálkozás… + + Default + Alapértelmezett + Delete Töröl @@ -363,7 +367,7 @@ Start on Boot - Indítsa el a Boot-on + Language diff --git a/linux/gui/i18n/agentredactor_hy.ts b/linux/gui/i18n/agentredactor_hy.ts index ea448e5..3100f15 100644 --- a/linux/gui/i18n/agentredactor_hy.ts +++ b/linux/gui/i18n/agentredactor_hy.ts @@ -164,7 +164,7 @@ Start on Boot - Սկսեք Boot-ից + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Շարժիչը չի աշխատում. նորից փորձում է… + + Default + Լռելյայն + Delete Ջնջել @@ -363,7 +367,7 @@ Start on Boot - Սկսեք Boot-ից + Language diff --git a/linux/gui/i18n/agentredactor_id.ts b/linux/gui/i18n/agentredactor_id.ts index ce7cd81..5d1638e 100644 --- a/linux/gui/i18n/agentredactor_id.ts +++ b/linux/gui/i18n/agentredactor_id.ts @@ -164,7 +164,7 @@ Start on Boot - Mulai saat Boot + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Mesin tidak hidup — mencoba lagi… + + Default + Default + Delete Menghapus @@ -363,7 +367,7 @@ Start on Boot - Mulai saat Boot + Language diff --git a/linux/gui/i18n/agentredactor_ig_NG.ts b/linux/gui/i18n/agentredactor_ig_NG.ts index d9145c6..f2680d1 100644 --- a/linux/gui/i18n/agentredactor_ig_NG.ts +++ b/linux/gui/i18n/agentredactor_ig_NG.ts @@ -164,7 +164,7 @@ Start on Boot - Bido na buut + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Igwe anaghị arụ ọrụ - na-anwale… + + Default + Ndabere + Delete Hichapụ @@ -363,7 +367,7 @@ Start on Boot - Bido na buut + Language diff --git a/linux/gui/i18n/agentredactor_is.ts b/linux/gui/i18n/agentredactor_is.ts index db750a5..0181d0b 100644 --- a/linux/gui/i18n/agentredactor_is.ts +++ b/linux/gui/i18n/agentredactor_is.ts @@ -164,7 +164,7 @@ Start on Boot - Byrjaðu á Boot + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Vél er ekki í gangi — reynir aftur... + + Default + Sjálfgefið + Delete Eyða @@ -363,7 +367,7 @@ Start on Boot - Byrjaðu á Boot + Language diff --git a/linux/gui/i18n/agentredactor_it.ts b/linux/gui/i18n/agentredactor_it.ts index f18ac4a..afd8d2d 100644 --- a/linux/gui/i18n/agentredactor_it.ts +++ b/linux/gui/i18n/agentredactor_it.ts @@ -164,7 +164,7 @@ Start on Boot - Inizia all'avvio + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Il motore non funziona: nuovo tentativo... + + Default + Predefinito + Delete Eliminare @@ -363,7 +367,7 @@ Start on Boot - Inizia all'avvio + Language diff --git a/linux/gui/i18n/agentredactor_ja.ts b/linux/gui/i18n/agentredactor_ja.ts index 6eec133..1efeb38 100644 --- a/linux/gui/i18n/agentredactor_ja.ts +++ b/linux/gui/i18n/agentredactor_ja.ts @@ -164,7 +164,7 @@ Start on Boot - ブート時に開始 + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… エンジンが動作していません - 再試行しています… + + Default + 既定 + Delete 消去 @@ -363,7 +367,7 @@ Start on Boot - ブート時に開始 + Language diff --git a/linux/gui/i18n/agentredactor_ka.ts b/linux/gui/i18n/agentredactor_ka.ts index 0d5dcfd..6e23b9a 100644 --- a/linux/gui/i18n/agentredactor_ka.ts +++ b/linux/gui/i18n/agentredactor_ka.ts @@ -164,7 +164,7 @@ Start on Boot - დაწყება ჩატვირთვით + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… ძრავა არ მუშაობს — ხელახლა ცდა… + + Default + ნაგულისხმევი + Delete წაშლა @@ -363,7 +367,7 @@ Start on Boot - დაწყება ჩატვირთვით + Language diff --git a/linux/gui/i18n/agentredactor_kk.ts b/linux/gui/i18n/agentredactor_kk.ts index 9983502..76dfde3 100644 --- a/linux/gui/i18n/agentredactor_kk.ts +++ b/linux/gui/i18n/agentredactor_kk.ts @@ -164,7 +164,7 @@ Start on Boot - Жүктеуде бастаңыз + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Қозғалтқыш жұмыс істемейді — әрекет қайталануда… + + Default + Әдепкі + Delete Жою @@ -363,7 +367,7 @@ Start on Boot - Жүктеуде бастаңыз + Language diff --git a/linux/gui/i18n/agentredactor_ko.ts b/linux/gui/i18n/agentredactor_ko.ts index 90709a4..f96b54b 100644 --- a/linux/gui/i18n/agentredactor_ko.ts +++ b/linux/gui/i18n/agentredactor_ko.ts @@ -164,7 +164,7 @@ Start on Boot - 부팅 시 시작 + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… 엔진이 실행되고 있지 않습니다. 다시 시도하는 중입니다… + + Default + 기본값 + Delete 삭제 @@ -363,7 +367,7 @@ Start on Boot - 부팅 시 시작 + Language diff --git a/linux/gui/i18n/agentredactor_lb.ts b/linux/gui/i18n/agentredactor_lb.ts index d5c9329..ba55ade 100644 --- a/linux/gui/i18n/agentredactor_lb.ts +++ b/linux/gui/i18n/agentredactor_lb.ts @@ -164,7 +164,7 @@ Start on Boot - Start op Boot + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… De Motor leeft net - probéiert nach eng Kéier ... + + Default + Standard + Delete Läschen @@ -363,7 +367,7 @@ Start on Boot - Start op Boot + Language diff --git a/linux/gui/i18n/agentredactor_lt.ts b/linux/gui/i18n/agentredactor_lt.ts index fd3f73d..c9db143 100644 --- a/linux/gui/i18n/agentredactor_lt.ts +++ b/linux/gui/i18n/agentredactor_lt.ts @@ -164,7 +164,7 @@ Start on Boot - Pradėkite nuo įkrovos + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Variklis neveikia – bandoma iš naujo… + + Default + Numatytasis + Delete Ištrinti @@ -363,7 +367,7 @@ Start on Boot - Pradėkite nuo įkrovos + Language diff --git a/linux/gui/i18n/agentredactor_lv.ts b/linux/gui/i18n/agentredactor_lv.ts index 13d6a3e..f6bd5d3 100644 --- a/linux/gui/i18n/agentredactor_lv.ts +++ b/linux/gui/i18n/agentredactor_lv.ts @@ -164,7 +164,7 @@ Start on Boot - Sāciet ar sāknēšanu + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Dzinējs nedarbojas — notiek atkārtots mēģinājums… + + Default + Noklusējums + Delete Dzēst @@ -363,7 +367,7 @@ Start on Boot - Sāciet ar sāknēšanu + Language diff --git a/linux/gui/i18n/agentredactor_ms.ts b/linux/gui/i18n/agentredactor_ms.ts index a0a0bdc..0954f71 100644 --- a/linux/gui/i18n/agentredactor_ms.ts +++ b/linux/gui/i18n/agentredactor_ms.ts @@ -164,7 +164,7 @@ Start on Boot - Mulakan pada Boot + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Enjin tidak berfungsi — mencuba semula… + + Default + Lalai + Delete Padam @@ -363,7 +367,7 @@ Start on Boot - Mulakan pada Boot + Language diff --git a/linux/gui/i18n/agentredactor_mt.ts b/linux/gui/i18n/agentredactor_mt.ts index 31d1492..1f003e0 100644 --- a/linux/gui/i18n/agentredactor_mt.ts +++ b/linux/gui/i18n/agentredactor_mt.ts @@ -164,7 +164,7 @@ Start on Boot - Ibda fuq Boot + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Il-magna mhix qed taħdem — qed nipprova mill-ġdid... + + Default + Default + Delete Ħassar @@ -363,7 +367,7 @@ Start on Boot - Ibda fuq Boot + Language diff --git a/linux/gui/i18n/agentredactor_nb.ts b/linux/gui/i18n/agentredactor_nb.ts index 6c02a1e..c5a8d14 100644 --- a/linux/gui/i18n/agentredactor_nb.ts +++ b/linux/gui/i18n/agentredactor_nb.ts @@ -164,7 +164,7 @@ Start on Boot - Start ved oppstart + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Motoren går ikke – prøver på nytt... + + Default + Standard + Delete Slett @@ -363,7 +367,7 @@ Start on Boot - Start ved oppstart + Language diff --git a/linux/gui/i18n/agentredactor_nl.ts b/linux/gui/i18n/agentredactor_nl.ts index 67784aa..5a82d7a 100644 --- a/linux/gui/i18n/agentredactor_nl.ts +++ b/linux/gui/i18n/agentredactor_nl.ts @@ -164,7 +164,7 @@ Start on Boot - Begin bij het opstarten + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… De motor draait niet. Opnieuw proberen... + + Default + Standaard + Delete Verwijderen @@ -363,7 +367,7 @@ Start on Boot - Begin bij het opstarten + Language diff --git a/linux/gui/i18n/agentredactor_pl.ts b/linux/gui/i18n/agentredactor_pl.ts index ed238d8..7434385 100644 --- a/linux/gui/i18n/agentredactor_pl.ts +++ b/linux/gui/i18n/agentredactor_pl.ts @@ -164,7 +164,7 @@ Start on Boot - Zacznij od rozruchu + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Silnik nie działa — ponawianie próby… + + Default + Domyślny + Delete Usuwać @@ -363,7 +367,7 @@ Start on Boot - Zacznij od rozruchu + Language diff --git a/linux/gui/i18n/agentredactor_pt.ts b/linux/gui/i18n/agentredactor_pt.ts index c6d89be..f9bdb8d 100644 --- a/linux/gui/i18n/agentredactor_pt.ts +++ b/linux/gui/i18n/agentredactor_pt.ts @@ -164,7 +164,7 @@ Start on Boot - Comece na inicialização + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… O mecanismo não está funcionando – tentando novamente… + + Default + Predefinido + Delete Excluir @@ -363,7 +367,7 @@ Start on Boot - Comece na inicialização + Language diff --git a/linux/gui/i18n/agentredactor_ro.ts b/linux/gui/i18n/agentredactor_ro.ts index 614bcbe..8cb3f54 100644 --- a/linux/gui/i18n/agentredactor_ro.ts +++ b/linux/gui/i18n/agentredactor_ro.ts @@ -164,7 +164,7 @@ Start on Boot - Începeți la Boot + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Motorul nu funcționează — se reîncercă... + + Default + Implicit + Delete Şterge @@ -363,7 +367,7 @@ Start on Boot - Începeți la Boot + Language diff --git a/linux/gui/i18n/agentredactor_ru.ts b/linux/gui/i18n/agentredactor_ru.ts index 2bd82f9..68a2379 100644 --- a/linux/gui/i18n/agentredactor_ru.ts +++ b/linux/gui/i18n/agentredactor_ru.ts @@ -164,7 +164,7 @@ Start on Boot - Начать при загрузке + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Двигатель не работает — повторная попытка… + + Default + По умолчанию + Delete Удалить @@ -363,7 +367,7 @@ Start on Boot - Начать при загрузке + Language diff --git a/linux/gui/i18n/agentredactor_sk.ts b/linux/gui/i18n/agentredactor_sk.ts index 83f74b3..7af4d3d 100644 --- a/linux/gui/i18n/agentredactor_sk.ts +++ b/linux/gui/i18n/agentredactor_sk.ts @@ -164,7 +164,7 @@ Start on Boot - Začnite pri zavádzaní + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Motor nebeží – pokus sa opakuje... + + Default + Predvolené + Delete Odstrániť @@ -363,7 +367,7 @@ Start on Boot - Začnite pri zavádzaní + Language diff --git a/linux/gui/i18n/agentredactor_sl.ts b/linux/gui/i18n/agentredactor_sl.ts index c837dcd..18766dc 100644 --- a/linux/gui/i18n/agentredactor_sl.ts +++ b/linux/gui/i18n/agentredactor_sl.ts @@ -164,7 +164,7 @@ Start on Boot - Začnite pri zagonu + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Motor ne deluje - ponovni poskus ... + + Default + Privzeto + Delete Izbriši @@ -363,7 +367,7 @@ Start on Boot - Začnite pri zagonu + Language diff --git a/linux/gui/i18n/agentredactor_sq.ts b/linux/gui/i18n/agentredactor_sq.ts index d6cc2ec..1f4d530 100644 --- a/linux/gui/i18n/agentredactor_sq.ts +++ b/linux/gui/i18n/agentredactor_sq.ts @@ -164,7 +164,7 @@ Start on Boot - Filloni në Boot + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Motori nuk funksionon - po riprovohet… + + Default + Parazgjedhur + Delete Fshije @@ -363,7 +367,7 @@ Start on Boot - Filloni në Boot + Language diff --git a/linux/gui/i18n/agentredactor_sr_Latn.ts b/linux/gui/i18n/agentredactor_sr_Latn.ts index 3084614..2e25834 100644 --- a/linux/gui/i18n/agentredactor_sr_Latn.ts +++ b/linux/gui/i18n/agentredactor_sr_Latn.ts @@ -164,7 +164,7 @@ Start on Boot - Počnite pri pokretanju + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Motor ne radi — pokušavam ponovo… + + Default + Podrazumevano + Delete Izbriši @@ -363,7 +367,7 @@ Start on Boot - Počnite pri pokretanju + Language diff --git a/linux/gui/i18n/agentredactor_sv.ts b/linux/gui/i18n/agentredactor_sv.ts index 9ee5ab6..1619904 100644 --- a/linux/gui/i18n/agentredactor_sv.ts +++ b/linux/gui/i18n/agentredactor_sv.ts @@ -164,7 +164,7 @@ Start on Boot - Börja på Boot + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Motorn går inte – försöker igen... + + Default + Standard + Delete Radera @@ -363,7 +367,7 @@ Start on Boot - Börja på Boot + Language diff --git a/linux/gui/i18n/agentredactor_sw.ts b/linux/gui/i18n/agentredactor_sw.ts index ada2964..d5c0854 100644 --- a/linux/gui/i18n/agentredactor_sw.ts +++ b/linux/gui/i18n/agentredactor_sw.ts @@ -164,7 +164,7 @@ Start on Boot - Anza kwenye Boot + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Injini haifanyi kazi - inajaribu tena... + + Default + Chaguo-msingi + Delete Futa @@ -363,7 +367,7 @@ Start on Boot - Anza kwenye Boot + Language diff --git a/linux/gui/i18n/agentredactor_ta.ts b/linux/gui/i18n/agentredactor_ta.ts index 93b241e..7e627c6 100644 --- a/linux/gui/i18n/agentredactor_ta.ts +++ b/linux/gui/i18n/agentredactor_ta.ts @@ -164,7 +164,7 @@ Start on Boot - துவக்கத்தில் தொடங்கவும் + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… எஞ்சின் இயங்கவில்லை - மீண்டும் முயற்சிக்கிறது... + + Default + இயல்புநிலை + Delete நீக்கு @@ -363,7 +367,7 @@ Start on Boot - துவக்கத்தில் தொடங்கவும் + Language diff --git a/linux/gui/i18n/agentredactor_th.ts b/linux/gui/i18n/agentredactor_th.ts index b1abee3..5782134 100644 --- a/linux/gui/i18n/agentredactor_th.ts +++ b/linux/gui/i18n/agentredactor_th.ts @@ -164,7 +164,7 @@ Start on Boot - เริ่มที่บูท + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… เครื่องยนต์ไม่ทำงาน — กำลังลองอีกครั้ง... + + Default + ค่าเริ่มต้น + Delete ลบ @@ -363,7 +367,7 @@ Start on Boot - เริ่มที่บูท + Language diff --git a/linux/gui/i18n/agentredactor_tr.ts b/linux/gui/i18n/agentredactor_tr.ts index adc9187..7cf680d 100644 --- a/linux/gui/i18n/agentredactor_tr.ts +++ b/linux/gui/i18n/agentredactor_tr.ts @@ -164,7 +164,7 @@ Start on Boot - Önyüklemede Başlat + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Motor çalışmıyor — yeniden deneniyor… + + Default + Varsayılan + Delete Silmek @@ -363,7 +367,7 @@ Start on Boot - Önyüklemede Başlat + Language diff --git a/linux/gui/i18n/agentredactor_uk.ts b/linux/gui/i18n/agentredactor_uk.ts index 59df728..9bae183 100644 --- a/linux/gui/i18n/agentredactor_uk.ts +++ b/linux/gui/i18n/agentredactor_uk.ts @@ -164,7 +164,7 @@ Start on Boot - Почніть із завантаження + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Двигун не працює — повторна спроба… + + Default + За замовчуванням + Delete Видалити @@ -363,7 +367,7 @@ Start on Boot - Почніть із завантаження + Language diff --git a/linux/gui/i18n/agentredactor_ur.ts b/linux/gui/i18n/agentredactor_ur.ts index 7af7068..a49b4c8 100644 --- a/linux/gui/i18n/agentredactor_ur.ts +++ b/linux/gui/i18n/agentredactor_ur.ts @@ -164,7 +164,7 @@ Start on Boot - بوٹ پر شروع کریں۔ + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… انجن نہیں چل رہا ہے — دوبارہ کوشش کر رہا ہے… + + Default + طے شدہ + Delete حذف کریں۔ @@ -363,7 +367,7 @@ Start on Boot - بوٹ پر شروع کریں۔ + Language diff --git a/linux/gui/i18n/agentredactor_vi.ts b/linux/gui/i18n/agentredactor_vi.ts index 27526b3..8e7e9d6 100644 --- a/linux/gui/i18n/agentredactor_vi.ts +++ b/linux/gui/i18n/agentredactor_vi.ts @@ -164,7 +164,7 @@ Start on Boot - Bắt đầu khi khởi động + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… Động cơ không chạy — đang thử lại… + + Default + Mặc định + Delete Xóa bỏ @@ -363,7 +367,7 @@ Start on Boot - Bắt đầu khi khởi động + Language diff --git a/linux/gui/i18n/agentredactor_zh_CN.ts b/linux/gui/i18n/agentredactor_zh_CN.ts index f75e06d..1605b42 100644 --- a/linux/gui/i18n/agentredactor_zh_CN.ts +++ b/linux/gui/i18n/agentredactor_zh_CN.ts @@ -164,7 +164,7 @@ Start on Boot - 开机启动 + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… 引擎未运行 — 正在重试... + + Default + 默认 + Delete 删除 @@ -363,7 +367,7 @@ Start on Boot - 开机启动 + Language diff --git a/linux/gui/i18n/agentredactor_zh_TW.ts b/linux/gui/i18n/agentredactor_zh_TW.ts index d1d3701..37efd83 100644 --- a/linux/gui/i18n/agentredactor_zh_TW.ts +++ b/linux/gui/i18n/agentredactor_zh_TW.ts @@ -164,7 +164,7 @@ Start on Boot - 開機啟動 + Language @@ -226,6 +226,10 @@ Engine is not running — retrying… 引擎未運作 — 正在重試... + + Default + 預設 + Delete 刪除 @@ -363,7 +367,7 @@ Start on Boot - 開機啟動 + Language diff --git a/linux/gui/main_window.cpp b/linux/gui/main_window.cpp index f0678e8..ed96ece 100644 --- a/linux/gui/main_window.cpp +++ b/linux/gui/main_window.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -63,7 +65,14 @@ MainWindow::MainWindow(AppState* appState, TrayIcon* tray, TranslatorLoader* tra : QMainWindow(parent), appState_(appState), tray_(tray), translator_(translator) { setWindowTitle(tr("Agent Redactor")); setWindowIcon(QIcon(QStringLiteral(":/app.png"))); - resize(1000, 900); + // Never open larger than the available work area: on Wayland an app + // cannot reposition its own window, so one that opens taller than the + // screen (900px on a 1280x800 display) loses its title bar off the top + // edge with no way to drag it back down. + const QRect avail = QGuiApplication::primaryScreen() + ? QGuiApplication::primaryScreen()->availableGeometry() + : QRect(0, 0, 1000, 900); + resize(std::min(1000, avail.width()), std::min(900, avail.height())); buildUi(); @@ -612,6 +621,36 @@ void MainWindow::reloadProfiles(bool keepSelection) { json profiles; if (!appState_->client().GetProfiles(profiles) || !profiles.is_array()) return; + if (profiles.empty()) { + // Mirror the Windows GUI (HomePage::LoadProfileList): seed a default + // profile on first run so the user never lands on an empty form. + const int port = FindAvailablePort(8080, {}); + json profile = { + {"alias", tr("Default").toStdString()}, + {"upstream_url", ""}, + {"api_key", ""}, + {"port", port > 0 ? port : 8080}, + {"use_openai_model", true}, + {"protocol_mode", "none"}, + {"enabled_pii_types", json::array()}, + {"pii_confidence_threshold", 0.9}, + {"regex_patterns", json::array()}, + {"keywords", json::array()}, + {"stats", {{"total_requests", 0}, {"total_pii_detected", 0}, + {"total_regex_matches", 0}, {"total_keyword_matches", 0}, + {"pii_type_breakdown", json::object()}}}, + {"enabled", true}, + }; + for (const auto& t : DEFAULT_PII_TYPES) + profile["enabled_pii_types"].push_back(Utils::WideToUtf8(t)); + std::wstring id; + if (appState_->client().PostProfile(profile, id)) { + appState_->client().RestartListeners(); + reloadProfiles(keepSelection); + } + return; + } + const QString previousId = keepSelection ? selectedProfileId() : QString(); profiles_ = profiles; From 51145b7742f4c5bfa594ade0870b7324c1548c1c Mon Sep 17 00:00:00 2001 From: Negative Star Innovators Date: Thu, 20 Aug 2026 08:16:25 +0000 Subject: [PATCH 20/20] fix(linux): bundle Qt wayland-decoration-client plugins in the AppImage GNOME does not provide server-side decorations; without the bundled adwaita/bradient decoration plugins Qt logs 'No decoration plugins available' and the window runs with no title bar at all (no minimize/ maximize/close). --- linux/build-release.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/linux/build-release.sh b/linux/build-release.sh index 8d8b0bd..d3cb1aa 100755 --- a/linux/build-release.sh +++ b/linux/build-release.sh @@ -87,7 +87,10 @@ for bin in "${STAGE}/agentredactor-gui" "${STAGE}/agentredactor"; do done # Qt plugins the GUI actually uses; qt.conf points Qt at the bundled copy. -for group in platforms platformthemes wayland-shell-integration xcbglintegrations imageformats iconengines tls; do +# wayland-decoration-client provides the client-side title bar (min/max/ +# close buttons) on compositors without server-side decorations (GNOME) — +# without it Qt runs with "no decorations" and the window has no title bar. +for group in platforms platformthemes wayland-shell-integration wayland-decoration-client xcbglintegrations imageformats iconengines tls; do if [ -d "${VP_QT_PLUGIN_DIR}/${group}" ]; then cp -r "${VP_QT_PLUGIN_DIR}/${group}" "${STAGE}/plugins/" fi