From b5baec64951e12fb2b2d91f10de4a59890082a82 Mon Sep 17 00:00:00 2001 From: CraftBot Date: Sun, 30 Aug 2026 12:32:15 +0900 Subject: [PATCH 01/53] feature:Localization update --- app/config.py | 24 + app/i18n/__init__.py | 30 + app/i18n/ui_messages.en.json | 78 ++ app/i18n/ui_messages.es.json | 78 ++ app/i18n/ui_messages.id.json | 78 ++ app/i18n/ui_messages.ja.json | 78 ++ app/i18n/ui_messages.ko.json | 78 ++ app/i18n/ui_messages.zh-CN.json | 78 ++ app/i18n/ui_messages.zh-TW.json | 78 ++ app/onboarding/interfaces/steps.py | 96 ++- app/ui_layer/adapters/browser_adapter.py | 64 +- .../browser/frontend/package-lock.json | 74 ++ app/ui_layer/browser/frontend/package.json | 2 + app/ui_layer/browser/frontend/src/App.tsx | 4 +- .../frontend/src/components/Chat/Chat.tsx | 93 +-- .../src/components/Chat/QuestionBox.tsx | 20 +- .../components/activity/ActivityBlocks.tsx | 11 +- .../components/activity/mascotFormatters.ts | 157 ++-- .../frontend/src/components/layout/Layout.tsx | 4 +- .../frontend/src/components/layout/NavBar.tsx | 101 +-- .../frontend/src/components/layout/TopBar.tsx | 6 +- .../src/components/ui/AttachmentDisplay.tsx | 8 +- .../components/ui/AttachmentPreviewModal.tsx | 23 +- .../src/components/ui/ConfirmModal.tsx | 10 +- .../src/components/ui/CreateCustomWizard.tsx | 147 ++-- .../src/components/ui/CreateLivingUIModal.tsx | 71 +- .../src/components/ui/ImportProfileModal.tsx | 64 +- .../frontend/src/components/ui/Modal.tsx | 4 +- .../src/components/ui/PlaybookModal.tsx | 44 +- .../frontend/src/components/ui/ResetModal.tsx | 81 ++- .../src/components/ui/SkillCreatorModal.tsx | 51 +- .../ui/SlashCommandAutocomplete.tsx | 6 +- .../frontend/src/constants/errorCategories.ts | 37 +- .../src/contexts/WebSocketContext.tsx | 5 +- .../src/contexts/WorkspaceContext.tsx | 17 +- .../frontend/src/hooks/useConfirmModal.ts | 9 +- .../src/hooks/useDerivedAgentStatus.ts | 19 +- .../frontend/src/hooks/useSkillCreator.ts | 29 +- .../browser/frontend/src/i18n/config.ts | 76 ++ .../browser/frontend/src/i18n/format.ts | 52 ++ .../browser/frontend/src/i18n/i18next.d.ts | 39 + .../browser/frontend/src/i18n/languages.ts | 57 ++ .../frontend/src/locales/en/activity.json | 165 +++++ .../browser/frontend/src/locales/en/chat.json | 82 +++ .../frontend/src/locales/en/common.json | 89 +++ .../frontend/src/locales/en/components.json | 186 +++++ .../frontend/src/locales/en/dashboard.json | 231 ++++++ .../frontend/src/locales/en/errors.json | 2 + .../frontend/src/locales/en/livingui.json | 76 ++ .../frontend/src/locales/en/memory.json | 113 +++ .../browser/frontend/src/locales/en/nav.json | 111 +++ .../frontend/src/locales/en/onboarding.json | 90 +++ .../frontend/src/locales/en/settings.json | 684 ++++++++++++++++++ .../browser/frontend/src/locales/en/tour.json | 65 ++ .../frontend/src/locales/en/workspace.json | 74 ++ .../frontend/src/locales/es/activity.json | 165 +++++ .../browser/frontend/src/locales/es/chat.json | 82 +++ .../frontend/src/locales/es/common.json | 89 +++ .../frontend/src/locales/es/components.json | 186 +++++ .../frontend/src/locales/es/dashboard.json | 231 ++++++ .../frontend/src/locales/es/errors.json | 2 + .../frontend/src/locales/es/livingui.json | 76 ++ .../frontend/src/locales/es/memory.json | 113 +++ .../browser/frontend/src/locales/es/nav.json | 111 +++ .../frontend/src/locales/es/onboarding.json | 90 +++ .../frontend/src/locales/es/settings.json | 684 ++++++++++++++++++ .../browser/frontend/src/locales/es/tour.json | 65 ++ .../frontend/src/locales/es/workspace.json | 74 ++ .../frontend/src/locales/id/activity.json | 165 +++++ .../browser/frontend/src/locales/id/chat.json | 82 +++ .../frontend/src/locales/id/common.json | 89 +++ .../frontend/src/locales/id/components.json | 186 +++++ .../frontend/src/locales/id/dashboard.json | 231 ++++++ .../frontend/src/locales/id/errors.json | 1 + .../frontend/src/locales/id/livingui.json | 76 ++ .../frontend/src/locales/id/memory.json | 113 +++ .../browser/frontend/src/locales/id/nav.json | 111 +++ .../frontend/src/locales/id/onboarding.json | 90 +++ .../frontend/src/locales/id/settings.json | 684 ++++++++++++++++++ .../browser/frontend/src/locales/id/tour.json | 65 ++ .../frontend/src/locales/id/workspace.json | 74 ++ .../frontend/src/locales/ja/activity.json | 165 +++++ .../browser/frontend/src/locales/ja/chat.json | 82 +++ .../frontend/src/locales/ja/common.json | 89 +++ .../frontend/src/locales/ja/components.json | 186 +++++ .../frontend/src/locales/ja/dashboard.json | 231 ++++++ .../frontend/src/locales/ja/errors.json | 2 + .../frontend/src/locales/ja/livingui.json | 76 ++ .../frontend/src/locales/ja/memory.json | 113 +++ .../browser/frontend/src/locales/ja/nav.json | 111 +++ .../frontend/src/locales/ja/onboarding.json | 90 +++ .../frontend/src/locales/ja/settings.json | 684 ++++++++++++++++++ .../browser/frontend/src/locales/ja/tour.json | 65 ++ .../frontend/src/locales/ja/workspace.json | 74 ++ .../frontend/src/locales/ko/activity.json | 165 +++++ .../browser/frontend/src/locales/ko/chat.json | 82 +++ .../frontend/src/locales/ko/common.json | 89 +++ .../frontend/src/locales/ko/components.json | 186 +++++ .../frontend/src/locales/ko/dashboard.json | 231 ++++++ .../frontend/src/locales/ko/errors.json | 1 + .../frontend/src/locales/ko/livingui.json | 76 ++ .../frontend/src/locales/ko/memory.json | 113 +++ .../browser/frontend/src/locales/ko/nav.json | 111 +++ .../frontend/src/locales/ko/onboarding.json | 90 +++ .../frontend/src/locales/ko/settings.json | 684 ++++++++++++++++++ .../browser/frontend/src/locales/ko/tour.json | 65 ++ .../frontend/src/locales/ko/workspace.json | 74 ++ .../frontend/src/locales/zh-CN/activity.json | 165 +++++ .../frontend/src/locales/zh-CN/chat.json | 82 +++ .../frontend/src/locales/zh-CN/common.json | 89 +++ .../src/locales/zh-CN/components.json | 186 +++++ .../frontend/src/locales/zh-CN/dashboard.json | 231 ++++++ .../frontend/src/locales/zh-CN/errors.json | 1 + .../frontend/src/locales/zh-CN/livingui.json | 76 ++ .../frontend/src/locales/zh-CN/memory.json | 113 +++ .../frontend/src/locales/zh-CN/nav.json | 111 +++ .../src/locales/zh-CN/onboarding.json | 90 +++ .../frontend/src/locales/zh-CN/settings.json | 684 ++++++++++++++++++ .../frontend/src/locales/zh-CN/tour.json | 65 ++ .../frontend/src/locales/zh-CN/workspace.json | 74 ++ .../frontend/src/locales/zh-TW/activity.json | 165 +++++ .../frontend/src/locales/zh-TW/chat.json | 82 +++ .../frontend/src/locales/zh-TW/common.json | 89 +++ .../src/locales/zh-TW/components.json | 186 +++++ .../frontend/src/locales/zh-TW/dashboard.json | 231 ++++++ .../frontend/src/locales/zh-TW/errors.json | 1 + .../frontend/src/locales/zh-TW/livingui.json | 76 ++ .../frontend/src/locales/zh-TW/memory.json | 113 +++ .../frontend/src/locales/zh-TW/nav.json | 111 +++ .../src/locales/zh-TW/onboarding.json | 90 +++ .../frontend/src/locales/zh-TW/settings.json | 684 ++++++++++++++++++ .../frontend/src/locales/zh-TW/tour.json | 65 ++ .../frontend/src/locales/zh-TW/workspace.json | 74 ++ app/ui_layer/browser/frontend/src/main.tsx | 1 + .../frontend/src/pages/Chat/ChatMessage.tsx | 15 +- .../src/pages/Chat/TypingIndicator.tsx | 6 +- .../pages/Dashboard/layout/AddWidgetModal.tsx | 10 +- .../pages/Dashboard/layout/DashboardGrid.tsx | 6 +- .../Dashboard/layout/DashboardHeader.tsx | 30 +- .../Dashboard/layout/LayoutNameModal.tsx | 10 +- .../pages/Dashboard/layout/WidgetChrome.tsx | 4 +- .../Dashboard/layout/WidgetErrorBoundary.tsx | 29 +- .../pages/Dashboard/layout/defaultLayout.ts | 3 +- .../Dashboard/layout/useDashboardLayouts.ts | 3 +- .../Dashboard/widgets/CraftBotIntroWidget.tsx | 190 ++--- .../Dashboard/widgets/IntegrationsWidget.tsx | 19 +- .../Dashboard/widgets/LivingUIWidget.tsx | 17 +- .../Dashboard/widgets/McpServersWidget.tsx | 19 +- .../Dashboard/widgets/ModelInfoWidget.tsx | 14 +- .../widgets/RecentActivityWidget.tsx | 16 +- .../pages/Dashboard/widgets/SkillsWidget.tsx | 19 +- .../widgets/SystemResourcesWidget.tsx | 14 +- .../Dashboard/widgets/TaskStatsWidget.tsx | 21 +- .../Dashboard/widgets/TokenUsageWidget.tsx | 17 +- .../Dashboard/widgets/UsagePatternsWidget.tsx | 23 +- .../src/pages/Dashboard/widgets/registry.ts | 48 +- .../src/pages/Dashboard/widgets/shared.tsx | 56 +- .../src/pages/Dashboard/widgets/types.ts | 6 +- .../src/pages/LivingUI/ConstructionDock.tsx | 16 +- .../src/pages/LivingUI/LivingUIPage.tsx | 63 +- .../src/pages/LivingUI/LivingUIThemeModal.tsx | 22 +- .../frontend/src/pages/LivingUI/iframePool.ts | 4 +- .../src/pages/LivingUI/themeCatalog.tsx | 39 +- .../frontend/src/pages/Memory/MemoryPage.tsx | 185 ++--- .../src/pages/Onboarding/OnboardingPage.tsx | 156 ++-- .../frontend/src/pages/Screen/ScreenPage.tsx | 24 +- .../src/pages/Settings/GeneralSettings.tsx | 283 +++++--- .../pages/Settings/IntegrationsSettings.tsx | 210 +++--- .../src/pages/Settings/LivingUISettings.tsx | 190 ++--- .../src/pages/Settings/MCPSettings.tsx | 85 +-- .../src/pages/Settings/MemorySettings.tsx | 89 ++- .../src/pages/Settings/ModelSettings.tsx | 217 +++--- .../pages/Settings/OpenRouterModelPicker.tsx | 68 +- .../src/pages/Settings/ProactiveSettings.tsx | 202 +++--- .../pages/Settings/SettingsPage.module.css | 15 + .../src/pages/Settings/SettingsPage.tsx | 4 +- .../src/pages/Settings/SkillsSettings.tsx | 133 ++-- .../frontend/src/pages/Settings/types.ts | 10 +- .../src/pages/Workspace/WorkspacePage.tsx | 180 +++-- .../frontend/src/store/slices/agentSlice.ts | 5 +- .../src/store/slices/livingUiSettingsSlice.ts | 5 +- .../src/store/slices/localLlmSlice.ts | 7 +- .../src/store/slices/modelSettingsSlice.ts | 5 +- .../src/store/slices/onboardingSlice.ts | 9 +- .../src/store/slices/workspaceSlice.ts | 3 +- .../browser/frontend/src/tour/controller.ts | 27 +- .../browser/frontend/src/tour/tours/core.ts | 57 +- .../browser/frontend/src/tour/types.ts | 10 +- app/ui_layer/browser/frontend/tsconfig.json | 2 +- app/ui_layer/commands/builtin/update.py | 27 +- app/ui_layer/settings/general_settings.py | 6 + app/updater.py | 6 +- packaging/CraftBotAgent.spec | 14 +- 193 files changed, 16937 insertions(+), 1890 deletions(-) create mode 100644 app/i18n/ui_messages.en.json create mode 100644 app/i18n/ui_messages.es.json create mode 100644 app/i18n/ui_messages.id.json create mode 100644 app/i18n/ui_messages.ja.json create mode 100644 app/i18n/ui_messages.ko.json create mode 100644 app/i18n/ui_messages.zh-CN.json create mode 100644 app/i18n/ui_messages.zh-TW.json create mode 100644 app/ui_layer/browser/frontend/src/i18n/config.ts create mode 100644 app/ui_layer/browser/frontend/src/i18n/format.ts create mode 100644 app/ui_layer/browser/frontend/src/i18n/i18next.d.ts create mode 100644 app/ui_layer/browser/frontend/src/i18n/languages.ts create mode 100644 app/ui_layer/browser/frontend/src/locales/en/activity.json create mode 100644 app/ui_layer/browser/frontend/src/locales/en/chat.json create mode 100644 app/ui_layer/browser/frontend/src/locales/en/common.json create mode 100644 app/ui_layer/browser/frontend/src/locales/en/components.json create mode 100644 app/ui_layer/browser/frontend/src/locales/en/dashboard.json create mode 100644 app/ui_layer/browser/frontend/src/locales/en/errors.json create mode 100644 app/ui_layer/browser/frontend/src/locales/en/livingui.json create mode 100644 app/ui_layer/browser/frontend/src/locales/en/memory.json create mode 100644 app/ui_layer/browser/frontend/src/locales/en/nav.json create mode 100644 app/ui_layer/browser/frontend/src/locales/en/onboarding.json create mode 100644 app/ui_layer/browser/frontend/src/locales/en/settings.json create mode 100644 app/ui_layer/browser/frontend/src/locales/en/tour.json create mode 100644 app/ui_layer/browser/frontend/src/locales/en/workspace.json create mode 100644 app/ui_layer/browser/frontend/src/locales/es/activity.json create mode 100644 app/ui_layer/browser/frontend/src/locales/es/chat.json create mode 100644 app/ui_layer/browser/frontend/src/locales/es/common.json create mode 100644 app/ui_layer/browser/frontend/src/locales/es/components.json create mode 100644 app/ui_layer/browser/frontend/src/locales/es/dashboard.json create mode 100644 app/ui_layer/browser/frontend/src/locales/es/errors.json create mode 100644 app/ui_layer/browser/frontend/src/locales/es/livingui.json create mode 100644 app/ui_layer/browser/frontend/src/locales/es/memory.json create mode 100644 app/ui_layer/browser/frontend/src/locales/es/nav.json create mode 100644 app/ui_layer/browser/frontend/src/locales/es/onboarding.json create mode 100644 app/ui_layer/browser/frontend/src/locales/es/settings.json create mode 100644 app/ui_layer/browser/frontend/src/locales/es/tour.json create mode 100644 app/ui_layer/browser/frontend/src/locales/es/workspace.json create mode 100644 app/ui_layer/browser/frontend/src/locales/id/activity.json create mode 100644 app/ui_layer/browser/frontend/src/locales/id/chat.json create mode 100644 app/ui_layer/browser/frontend/src/locales/id/common.json create mode 100644 app/ui_layer/browser/frontend/src/locales/id/components.json create mode 100644 app/ui_layer/browser/frontend/src/locales/id/dashboard.json create mode 100644 app/ui_layer/browser/frontend/src/locales/id/errors.json create mode 100644 app/ui_layer/browser/frontend/src/locales/id/livingui.json create mode 100644 app/ui_layer/browser/frontend/src/locales/id/memory.json create mode 100644 app/ui_layer/browser/frontend/src/locales/id/nav.json create mode 100644 app/ui_layer/browser/frontend/src/locales/id/onboarding.json create mode 100644 app/ui_layer/browser/frontend/src/locales/id/settings.json create mode 100644 app/ui_layer/browser/frontend/src/locales/id/tour.json create mode 100644 app/ui_layer/browser/frontend/src/locales/id/workspace.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ja/activity.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ja/chat.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ja/common.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ja/components.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ja/dashboard.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ja/errors.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ja/livingui.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ja/memory.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ja/nav.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ja/onboarding.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ja/settings.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ja/tour.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ja/workspace.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ko/activity.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ko/chat.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ko/common.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ko/components.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ko/dashboard.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ko/errors.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ko/livingui.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ko/memory.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ko/nav.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ko/onboarding.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ko/settings.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ko/tour.json create mode 100644 app/ui_layer/browser/frontend/src/locales/ko/workspace.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-CN/activity.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-CN/chat.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-CN/common.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-CN/components.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-CN/dashboard.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-CN/errors.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-CN/livingui.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-CN/memory.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-CN/nav.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-CN/onboarding.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-CN/settings.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-CN/tour.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-CN/workspace.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-TW/activity.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-TW/chat.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-TW/common.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-TW/components.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-TW/dashboard.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-TW/errors.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-TW/livingui.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-TW/memory.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-TW/nav.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-TW/onboarding.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-TW/settings.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-TW/tour.json create mode 100644 app/ui_layer/browser/frontend/src/locales/zh-TW/workspace.json diff --git a/app/config.py b/app/config.py index 2876364c..884247e6 100644 --- a/app/config.py +++ b/app/config.py @@ -542,6 +542,30 @@ def get_os_language() -> str: return settings.get("general", {}).get("os_language", "en") +def get_ui_language() -> str: + """Get the browser UI language from settings. + + This governs the language of the CraftBot web interface chrome and is + chosen by the user in Settings. It is intentionally distinct from + ``os_language`` (which governs the language the agent converses in). Before + the user has picked one, the initial value is derived from ``os_language``; + the frontend normalizes the raw code to the nearest supported UI language. + + Returns: + Language code (e.g. "en", "ja", "zh-CN") for the interface. + """ + settings = get_settings() + general = settings.get("general", {}) + return general.get("ui_language") or general.get("os_language", "en") + + +def set_ui_language(lang_code: str) -> None: + """Persist the browser UI language to settings.json ``general.ui_language``.""" + settings = get_settings() + settings.setdefault("general", {})["ui_language"] = lang_code + save_settings(settings) + + def detect_and_save_os_language() -> str: """Detect OS language and save to settings. Called on first launch only. diff --git a/app/i18n/__init__.py b/app/i18n/__init__.py index fac368dd..c9892961 100644 --- a/app/i18n/__init__.py +++ b/app/i18n/__init__.py @@ -88,6 +88,36 @@ def t(key: str, **kwargs: str) -> str: return template.format_map(kwargs) +# ── Browser-UI message catalog ──────────────────────────────────────────────── +# User-facing strings the backend emits to the browser interface (validation +# errors, success toasts, update/progress text). These are localized in the +# user's chosen *UI* language (settings.json general.ui_language), which is +# distinct from os_language. Files: app/i18n/ui_messages..json. + +_ui_catalog_cache: dict[str, dict[str, str]] = {} + + +def _load_ui_catalog(lang: str) -> dict[str, str]: + if lang not in _ui_catalog_cache: + path = _I18N_DIR / f"ui_messages.{lang}.json" + _ui_catalog_cache[lang] = ( + json.loads(path.read_text(encoding="utf-8")) if path.exists() else {} + ) + return _ui_catalog_cache[lang] + + +def tui(key: str, **kwargs: str) -> str: + """Render a browser-UI message *key* in the user's UI language. + + Resolves in order: UI locale → "en" → key itself (never raises). + """ + from app.config import get_ui_language + + lang = get_ui_language() + template = _load_ui_catalog(lang).get(key) or _load_ui_catalog("en").get(key, key) + return template.format_map(kwargs) + + # ── Public classifier ───────────────────────────────────────────────────────── diff --git a/app/i18n/ui_messages.en.json b/app/i18n/ui_messages.en.json new file mode 100644 index 00000000..ef0454b9 --- /dev/null +++ b/app/i18n/ui_messages.en.json @@ -0,0 +1,78 @@ +{ + "onboarding_step_required": "This step is required and cannot be skipped", + "onboarding_at_first_step": "Already at the first step", + + "model_no_model_specified": "No model specified", + "model_test_failed": "Test failed", + + "livingui_name_desc_required": "Name and description are required", + "livingui_project_created": "Project created, starting development...", + "livingui_dev_run_failed": "Failed to start development run", + "livingui_launch_failed": "Failed to launch Living UI server", + "livingui_no_zip": "No ZIP file uploaded", + "livingui_no_bundle": "No bundle file uploaded", + "livingui_app_id_name_required": "App ID and name are required", + "livingui_installing_marketplace": "Installing from marketplace...", + + "memory_disabled": "Memory is disabled. Enable memory mode first.", + "memory_no_unprocessed_events": "No unprocessed events to process.", + "memory_processing_queued": "Memory processing run queued", + + "schedule_not_found": "Schedule not found", + + "integration_unknown": "Unknown integration: {integration_id}", + "integration_unknown_generic": "Unknown integration", + "integration_oauth_cancelled": "OAuth cancelled", + "integration_connection_cancelled": "Connection cancelled", + "integration_add_account_cancelled": "Add account cancelled", + + "playbook_catalogue_not_found": "Playbook catalogue not found.", + + "update_checking": "Checking for updates...", + "update_check_failed": "Failed to check for updates: {error}", + "update_off_branch": "On branch '{branch}' (v{current}). Updates only apply on the {update_branch} branch; switch to it to update.", + "update_up_to_date": "CraftBot is up to date (v{current}).", + "update_available": "Update available: v{current} → v{latest}", + "update_available_starting": "Update available: v{current} → v{latest}. Starting update...", + "update_launching": "Launching updater in a new window (pulling {branch})...", + "update_shutting_down": "Shutting down, the updater will relaunch CraftBot shortly.", + "update_failed": "Update failed: {error}", + + "onboarding_intro_title": "Nice to meet you, I'm CraftBot!", + "onboarding_intro_desc": "Nice to meet you, I am CraftBot! I am here to help you with work or life. Now before we begin, there are some baseline settings we need to configure.", + "onboarding_provider_title": "Select LLM Provider", + "onboarding_provider_desc": "Choose which AI provider to use for the agent.", + "onboarding_provider_desc_openai": "GPT models", + "onboarding_provider_desc_gemini": "Gemini models", + "onboarding_provider_desc_byteplus": "Kimi models", + "onboarding_provider_desc_anthropic": "Claude models", + "onboarding_provider_desc_deepseek": "DeepSeek models", + "onboarding_provider_desc_minimax": "MiniMax models", + "onboarding_provider_desc_moonshot": "Moonshot models", + "onboarding_provider_desc_grok": "Grok models", + "onboarding_provider_desc_glm": "GLM models", + "onboarding_provider_desc_fugu": "Fugu models", + "onboarding_provider_desc_remote": "Self-hosted models", + "onboarding_provider_invalid": "Invalid provider. Choose from: {providers}", + "onboarding_apikey_title_ollama": "Connect Ollama", + "onboarding_apikey_title_openrouter": "Enter {display} API Key", + "onboarding_apikey_title_default": "Enter API Key", + "onboarding_apikey_desc_ollama": "Connect to your local Ollama instance.\nIf Ollama isn't installed yet, we'll help you set it up.", + "onboarding_apikey_desc_openrouter": "Enter your {display} API key. If your region doesn't have direct access, you can use OpenRouter as a fallback instead.", + "onboarding_apikey_desc_default": "Enter your API key for the selected provider.", + "onboarding_validate_invalid_url": "Please enter a valid URL (e.g. http://localhost:11434)", + "onboarding_validate_api_key_required": "API key is required", + "onboarding_validate_api_key_too_short": "API key seems too short", + "onboarding_agent_name_title": "Name your agent", + "onboarding_agent_name_desc": "Give your agent a name.", + "onboarding_field_agent_name_label": "Agent Name", + "onboarding_field_agent_name_placeholder": "Enter a name", + "onboarding_validate_agent_name_too_long": "Agent name must be 20 characters or fewer", + "onboarding_validate_agent_name_invalid": "Invalid agent name submission", + "onboarding_user_profile_title": "Your Name", + "onboarding_user_profile_desc": "What should your agent call you?", + "onboarding_field_user_name_label": "Your Name", + "onboarding_field_user_name_placeholder": "Your name", + "onboarding_validate_expected_dict": "Expected a dictionary of form values", + "onboarding_validate_user_name_too_long": "Name must be 20 characters or fewer" +} diff --git a/app/i18n/ui_messages.es.json b/app/i18n/ui_messages.es.json new file mode 100644 index 00000000..857e2279 --- /dev/null +++ b/app/i18n/ui_messages.es.json @@ -0,0 +1,78 @@ +{ + "onboarding_step_required": "Este paso es obligatorio y no se puede omitir", + "onboarding_at_first_step": "Ya estás en el primer paso", + + "model_no_model_specified": "No se ha especificado ningún modelo", + "model_test_failed": "La prueba falló", + + "livingui_name_desc_required": "El nombre y la descripción son obligatorios", + "livingui_project_created": "Proyecto creado, iniciando el desarrollo...", + "livingui_dev_run_failed": "Error al iniciar la ejecución de desarrollo", + "livingui_launch_failed": "Error al iniciar el servidor de Living UI", + "livingui_no_zip": "No se ha subido ningún archivo ZIP", + "livingui_no_bundle": "No se ha subido ningún archivo de paquete", + "livingui_app_id_name_required": "El ID y el nombre de la app son obligatorios", + "livingui_installing_marketplace": "Instalando desde el marketplace...", + + "memory_disabled": "La memoria está desactivada. Activa primero el modo de memoria.", + "memory_no_unprocessed_events": "No hay eventos sin procesar que procesar.", + "memory_processing_queued": "Ejecución de procesamiento de memoria en cola", + + "schedule_not_found": "Programación no encontrada", + + "integration_unknown": "Integración desconocida: {integration_id}", + "integration_unknown_generic": "Integración desconocida", + "integration_oauth_cancelled": "OAuth cancelado", + "integration_connection_cancelled": "Conexión cancelada", + "integration_add_account_cancelled": "Adición de cuenta cancelada", + + "playbook_catalogue_not_found": "Catálogo de guías no encontrado.", + + "update_checking": "Buscando actualizaciones...", + "update_check_failed": "Error al buscar actualizaciones: {error}", + "update_off_branch": "En la rama '{branch}' (v{current}). Las actualizaciones solo se aplican en la rama {update_branch}; cambia a ella para actualizar.", + "update_up_to_date": "CraftBot está actualizado (v{current}).", + "update_available": "Actualización disponible: v{current} → v{latest}", + "update_available_starting": "Actualización disponible: v{current} → v{latest}. Iniciando la actualización...", + "update_launching": "Iniciando el actualizador en una nueva ventana (descargando {branch})...", + "update_shutting_down": "Cerrando; el actualizador volverá a abrir CraftBot en breve.", + "update_failed": "Error al actualizar: {error}", + + "onboarding_intro_title": "Encantado de conocerte, ¡soy CraftBot!", + "onboarding_intro_desc": "Encantado de conocerte, ¡soy CraftBot! Estoy aquí para ayudarte con el trabajo o la vida. Ahora, antes de empezar, hay algunos ajustes básicos que debemos configurar.", + "onboarding_provider_title": "Selecciona el proveedor de LLM", + "onboarding_provider_desc": "Elige qué proveedor de IA usar para el agente.", + "onboarding_provider_desc_openai": "Modelos GPT", + "onboarding_provider_desc_gemini": "Modelos Gemini", + "onboarding_provider_desc_byteplus": "Modelos Kimi", + "onboarding_provider_desc_anthropic": "Modelos Claude", + "onboarding_provider_desc_deepseek": "Modelos DeepSeek", + "onboarding_provider_desc_minimax": "Modelos MiniMax", + "onboarding_provider_desc_moonshot": "Modelos Moonshot", + "onboarding_provider_desc_grok": "Modelos Grok", + "onboarding_provider_desc_glm": "Modelos GLM", + "onboarding_provider_desc_fugu": "Modelos Fugu", + "onboarding_provider_desc_remote": "Modelos autoalojados", + "onboarding_provider_invalid": "Proveedor no válido. Elige entre: {providers}", + "onboarding_apikey_title_ollama": "Conectar Ollama", + "onboarding_apikey_title_openrouter": "Introduce la clave de API de {display}", + "onboarding_apikey_title_default": "Introduce la clave de API", + "onboarding_apikey_desc_ollama": "Conéctate a tu instancia local de Ollama.\nSi Ollama aún no está instalado, te ayudaremos a configurarlo.", + "onboarding_apikey_desc_openrouter": "Introduce tu clave de API de {display}. Si tu región no tiene acceso directo, puedes usar OpenRouter como alternativa.", + "onboarding_apikey_desc_default": "Introduce tu clave de API para el proveedor seleccionado.", + "onboarding_validate_invalid_url": "Introduce una URL válida (p. ej. http://localhost:11434)", + "onboarding_validate_api_key_required": "La clave de API es obligatoria", + "onboarding_validate_api_key_too_short": "La clave de API parece demasiado corta", + "onboarding_agent_name_title": "Nombra a tu agente", + "onboarding_agent_name_desc": "Dale un nombre a tu agente.", + "onboarding_field_agent_name_label": "Nombre del agente", + "onboarding_field_agent_name_placeholder": "Introduce un nombre", + "onboarding_validate_agent_name_too_long": "El nombre del agente debe tener 20 caracteres o menos", + "onboarding_validate_agent_name_invalid": "Envío de nombre de agente no válido", + "onboarding_user_profile_title": "Tu nombre", + "onboarding_user_profile_desc": "¿Cómo debería llamarte tu agente?", + "onboarding_field_user_name_label": "Tu nombre", + "onboarding_field_user_name_placeholder": "Tu nombre", + "onboarding_validate_expected_dict": "Se esperaba un diccionario de valores del formulario", + "onboarding_validate_user_name_too_long": "El nombre debe tener 20 caracteres o menos" +} diff --git a/app/i18n/ui_messages.id.json b/app/i18n/ui_messages.id.json new file mode 100644 index 00000000..7503777c --- /dev/null +++ b/app/i18n/ui_messages.id.json @@ -0,0 +1,78 @@ +{ + "onboarding_step_required": "Langkah ini wajib dan tidak dapat dilewati", + "onboarding_at_first_step": "Sudah berada di langkah pertama", + + "model_no_model_specified": "Tidak ada model yang ditentukan", + "model_test_failed": "Pengujian gagal", + + "livingui_name_desc_required": "Nama dan deskripsi wajib diisi", + "livingui_project_created": "Proyek dibuat, memulai pengembangan...", + "livingui_dev_run_failed": "Gagal memulai proses pengembangan", + "livingui_launch_failed": "Gagal meluncurkan server Living UI", + "livingui_no_zip": "Tidak ada berkas ZIP yang diunggah", + "livingui_no_bundle": "Tidak ada berkas bundel yang diunggah", + "livingui_app_id_name_required": "ID dan nama aplikasi wajib diisi", + "livingui_installing_marketplace": "Menginstal dari pasar...", + + "memory_disabled": "Memori dinonaktifkan. Aktifkan mode memori terlebih dahulu.", + "memory_no_unprocessed_events": "Tidak ada peristiwa yang belum diproses untuk diproses.", + "memory_processing_queued": "Proses pemrosesan memori telah diantrekan", + + "schedule_not_found": "Jadwal tidak ditemukan", + + "integration_unknown": "Integrasi tidak dikenal: {integration_id}", + "integration_unknown_generic": "Integrasi tidak dikenal", + "integration_oauth_cancelled": "OAuth dibatalkan", + "integration_connection_cancelled": "Koneksi dibatalkan", + "integration_add_account_cancelled": "Penambahan akun dibatalkan", + + "playbook_catalogue_not_found": "Katalog playbook tidak ditemukan.", + + "update_checking": "Memeriksa pembaruan...", + "update_check_failed": "Gagal memeriksa pembaruan: {error}", + "update_off_branch": "Di branch '{branch}' (v{current}). Pembaruan hanya berlaku pada branch {update_branch}; beralihlah ke branch tersebut untuk memperbarui.", + "update_up_to_date": "CraftBot sudah versi terbaru (v{current}).", + "update_available": "Pembaruan tersedia: v{current} → v{latest}", + "update_available_starting": "Pembaruan tersedia: v{current} → v{latest}. Memulai pembaruan...", + "update_launching": "Meluncurkan pembaru di jendela baru (menarik {branch})...", + "update_shutting_down": "Mematikan, pembaru akan segera meluncurkan ulang CraftBot.", + "update_failed": "Pembaruan gagal: {error}", + + "onboarding_intro_title": "Senang bertemu dengan Anda, saya CraftBot!", + "onboarding_intro_desc": "Senang bertemu dengan Anda, saya CraftBot! Saya di sini untuk membantu Anda dalam pekerjaan maupun kehidupan. Nah, sebelum kita mulai, ada beberapa pengaturan dasar yang perlu kita konfigurasikan.", + "onboarding_provider_title": "Pilih Penyedia LLM", + "onboarding_provider_desc": "Pilih penyedia AI mana yang akan digunakan untuk agen.", + "onboarding_provider_desc_openai": "Model GPT", + "onboarding_provider_desc_gemini": "Model Gemini", + "onboarding_provider_desc_byteplus": "Model Kimi", + "onboarding_provider_desc_anthropic": "Model Claude", + "onboarding_provider_desc_deepseek": "Model DeepSeek", + "onboarding_provider_desc_minimax": "Model MiniMax", + "onboarding_provider_desc_moonshot": "Model Moonshot", + "onboarding_provider_desc_grok": "Model Grok", + "onboarding_provider_desc_glm": "Model GLM", + "onboarding_provider_desc_fugu": "Model Fugu", + "onboarding_provider_desc_remote": "Model yang di-host sendiri", + "onboarding_provider_invalid": "Penyedia tidak valid. Pilih dari: {providers}", + "onboarding_apikey_title_ollama": "Sambungkan Ollama", + "onboarding_apikey_title_openrouter": "Masukkan Kunci API {display}", + "onboarding_apikey_title_default": "Masukkan Kunci API", + "onboarding_apikey_desc_ollama": "Sambungkan ke instance Ollama lokal Anda.\nJika Ollama belum terinstal, kami akan membantu Anda menyiapkannya.", + "onboarding_apikey_desc_openrouter": "Masukkan kunci API {display} Anda. Jika wilayah Anda tidak memiliki akses langsung, Anda dapat menggunakan OpenRouter sebagai cadangan.", + "onboarding_apikey_desc_default": "Masukkan kunci API Anda untuk penyedia yang dipilih.", + "onboarding_validate_invalid_url": "Silakan masukkan URL yang valid (mis. http://localhost:11434)", + "onboarding_validate_api_key_required": "Kunci API wajib diisi", + "onboarding_validate_api_key_too_short": "Kunci API tampaknya terlalu pendek", + "onboarding_agent_name_title": "Beri nama agen Anda", + "onboarding_agent_name_desc": "Beri nama untuk agen Anda.", + "onboarding_field_agent_name_label": "Nama Agen", + "onboarding_field_agent_name_placeholder": "Masukkan nama", + "onboarding_validate_agent_name_too_long": "Nama agen harus 20 karakter atau kurang", + "onboarding_validate_agent_name_invalid": "Pengiriman nama agen tidak valid", + "onboarding_user_profile_title": "Nama Anda", + "onboarding_user_profile_desc": "Agen Anda harus memanggil Anda apa?", + "onboarding_field_user_name_label": "Nama Anda", + "onboarding_field_user_name_placeholder": "Nama Anda", + "onboarding_validate_expected_dict": "Diharapkan berupa kamus nilai formulir", + "onboarding_validate_user_name_too_long": "Nama harus 20 karakter atau kurang" +} diff --git a/app/i18n/ui_messages.ja.json b/app/i18n/ui_messages.ja.json new file mode 100644 index 00000000..291da406 --- /dev/null +++ b/app/i18n/ui_messages.ja.json @@ -0,0 +1,78 @@ +{ + "onboarding_step_required": "このステップは必須のためスキップできません", + "onboarding_at_first_step": "既に最初のステップです", + + "model_no_model_specified": "モデルが指定されていません", + "model_test_failed": "テストに失敗しました", + + "livingui_name_desc_required": "名前と説明は必須です", + "livingui_project_created": "プロジェクトを作成しました。開発を開始しています...", + "livingui_dev_run_failed": "開発実行の開始に失敗しました", + "livingui_launch_failed": "Living UI サーバーの起動に失敗しました", + "livingui_no_zip": "ZIP ファイルがアップロードされていません", + "livingui_no_bundle": "バンドルファイルがアップロードされていません", + "livingui_app_id_name_required": "アプリ ID と名前は必須です", + "livingui_installing_marketplace": "マーケットプレイスからインストール中...", + + "memory_disabled": "メモリーが無効です。まずメモリーモードを有効にしてください。", + "memory_no_unprocessed_events": "処理する未処理のイベントがありません。", + "memory_processing_queued": "メモリー処理の実行をキューに追加しました", + + "schedule_not_found": "スケジュールが見つかりません", + + "integration_unknown": "不明な連携: {integration_id}", + "integration_unknown_generic": "不明な連携", + "integration_oauth_cancelled": "OAuth をキャンセルしました", + "integration_connection_cancelled": "接続をキャンセルしました", + "integration_add_account_cancelled": "アカウントの追加をキャンセルしました", + + "playbook_catalogue_not_found": "プレイブックのカタログが見つかりません。", + + "update_checking": "更新を確認中...", + "update_check_failed": "更新の確認に失敗しました: {error}", + "update_off_branch": "ブランチ '{branch}' (v{current}) にいます。更新は {update_branch} ブランチでのみ適用されます。更新するには切り替えてください。", + "update_up_to_date": "CraftBot は最新です (v{current})。", + "update_available": "更新が利用可能です: v{current} → v{latest}", + "update_available_starting": "更新が利用可能です: v{current} → v{latest}。更新を開始しています...", + "update_launching": "新しいウィンドウでアップデーターを起動中({branch} を取得中)...", + "update_shutting_down": "シャットダウン中です。まもなくアップデーターが CraftBot を再起動します。", + "update_failed": "更新に失敗しました: {error}", + + "onboarding_intro_title": "はじめまして、CraftBot です!", + "onboarding_intro_desc": "はじめまして、CraftBot です!仕事でも生活でも、お手伝いします。さて、始める前にいくつか基本設定が必要です。", + "onboarding_provider_title": "LLM プロバイダーを選択", + "onboarding_provider_desc": "エージェントに使用する AI プロバイダーを選択してください。", + "onboarding_provider_desc_openai": "GPT モデル", + "onboarding_provider_desc_gemini": "Gemini モデル", + "onboarding_provider_desc_byteplus": "Kimi モデル", + "onboarding_provider_desc_anthropic": "Claude モデル", + "onboarding_provider_desc_deepseek": "DeepSeek モデル", + "onboarding_provider_desc_minimax": "MiniMax モデル", + "onboarding_provider_desc_moonshot": "Moonshot モデル", + "onboarding_provider_desc_grok": "Grok モデル", + "onboarding_provider_desc_glm": "GLM モデル", + "onboarding_provider_desc_fugu": "Fugu モデル", + "onboarding_provider_desc_remote": "セルフホストのモデル", + "onboarding_provider_invalid": "プロバイダーが無効です。次から選択してください: {providers}", + "onboarding_apikey_title_ollama": "Ollama に接続", + "onboarding_apikey_title_openrouter": "{display} の API キーを入力", + "onboarding_apikey_title_default": "API キーを入力", + "onboarding_apikey_desc_ollama": "ローカルの Ollama インスタンスに接続します。\nOllama がまだインストールされていない場合は、セットアップをお手伝いします。", + "onboarding_apikey_desc_openrouter": "{display} の API キーを入力してください。お住まいの地域で直接アクセスできない場合は、代わりに OpenRouter をフォールバックとして使用できます。", + "onboarding_apikey_desc_default": "選択したプロバイダーの API キーを入力してください。", + "onboarding_validate_invalid_url": "有効な URL を入力してください(例: http://localhost:11434)", + "onboarding_validate_api_key_required": "API キーは必須です", + "onboarding_validate_api_key_too_short": "API キーが短すぎるようです", + "onboarding_agent_name_title": "エージェントに名前を付ける", + "onboarding_agent_name_desc": "エージェントに名前を付けてください。", + "onboarding_field_agent_name_label": "エージェント名", + "onboarding_field_agent_name_placeholder": "名前を入力", + "onboarding_validate_agent_name_too_long": "エージェント名は 20 文字以内にしてください", + "onboarding_validate_agent_name_invalid": "エージェント名の入力が無効です", + "onboarding_user_profile_title": "あなたの名前", + "onboarding_user_profile_desc": "エージェントはあなたを何と呼べばよいですか?", + "onboarding_field_user_name_label": "あなたの名前", + "onboarding_field_user_name_placeholder": "あなたの名前", + "onboarding_validate_expected_dict": "フォーム値の辞書が必要です", + "onboarding_validate_user_name_too_long": "名前は 20 文字以内にしてください" +} diff --git a/app/i18n/ui_messages.ko.json b/app/i18n/ui_messages.ko.json new file mode 100644 index 00000000..59cff817 --- /dev/null +++ b/app/i18n/ui_messages.ko.json @@ -0,0 +1,78 @@ +{ + "onboarding_step_required": "이 단계는 필수이며 건너뛸 수 없습니다", + "onboarding_at_first_step": "이미 첫 단계입니다", + + "model_no_model_specified": "지정된 모델이 없습니다", + "model_test_failed": "테스트 실패", + + "livingui_name_desc_required": "이름과 설명은 필수입니다", + "livingui_project_created": "프로젝트가 생성되었습니다. 개발을 시작하는 중...", + "livingui_dev_run_failed": "개발 실행을 시작하지 못했습니다", + "livingui_launch_failed": "Living UI 서버를 실행하지 못했습니다", + "livingui_no_zip": "업로드된 ZIP 파일이 없습니다", + "livingui_no_bundle": "업로드된 번들 파일이 없습니다", + "livingui_app_id_name_required": "앱 ID와 이름은 필수입니다", + "livingui_installing_marketplace": "마켓플레이스에서 설치하는 중...", + + "memory_disabled": "메모리가 비활성화되어 있습니다. 먼저 메모리 모드를 활성화하세요.", + "memory_no_unprocessed_events": "처리할 미처리 이벤트가 없습니다.", + "memory_processing_queued": "메모리 처리 실행이 대기열에 추가되었습니다", + + "schedule_not_found": "일정을 찾을 수 없습니다", + + "integration_unknown": "알 수 없는 연동: {integration_id}", + "integration_unknown_generic": "알 수 없는 연동", + "integration_oauth_cancelled": "OAuth가 취소되었습니다", + "integration_connection_cancelled": "연결이 취소되었습니다", + "integration_add_account_cancelled": "계정 추가가 취소되었습니다", + + "playbook_catalogue_not_found": "플레이북 카탈로그를 찾을 수 없습니다.", + + "update_checking": "업데이트를 확인하는 중...", + "update_check_failed": "업데이트 확인 실패: {error}", + "update_off_branch": "'{branch}' 브랜치에 있습니다 (v{current}). 업데이트는 {update_branch} 브랜치에서만 적용됩니다. 업데이트하려면 해당 브랜치로 전환하세요.", + "update_up_to_date": "CraftBot이 최신 상태입니다 (v{current}).", + "update_available": "업데이트 가능: v{current} → v{latest}", + "update_available_starting": "업데이트 가능: v{current} → v{latest}. 업데이트를 시작하는 중...", + "update_launching": "새 창에서 업데이터를 실행하는 중 ({branch} 가져오는 중)...", + "update_shutting_down": "종료하는 중입니다. 업데이터가 곧 CraftBot을 다시 실행합니다.", + "update_failed": "업데이트 실패: {error}", + + "onboarding_intro_title": "만나서 반가워요, 저는 CraftBot이에요!", + "onboarding_intro_desc": "만나서 반가워요, 저는 CraftBot이에요! 일이든 삶이든 당신을 돕기 위해 왔어요. 자, 시작하기 전에 구성해야 할 몇 가지 기본 설정이 있어요.", + "onboarding_provider_title": "LLM 공급자 선택", + "onboarding_provider_desc": "에이전트에 사용할 AI 공급자를 선택하세요.", + "onboarding_provider_desc_openai": "GPT 모델", + "onboarding_provider_desc_gemini": "Gemini 모델", + "onboarding_provider_desc_byteplus": "Kimi 모델", + "onboarding_provider_desc_anthropic": "Claude 모델", + "onboarding_provider_desc_deepseek": "DeepSeek 모델", + "onboarding_provider_desc_minimax": "MiniMax 모델", + "onboarding_provider_desc_moonshot": "Moonshot 모델", + "onboarding_provider_desc_grok": "Grok 모델", + "onboarding_provider_desc_glm": "GLM 모델", + "onboarding_provider_desc_fugu": "Fugu 모델", + "onboarding_provider_desc_remote": "셀프 호스팅 모델", + "onboarding_provider_invalid": "잘못된 공급자입니다. 다음 중에서 선택하세요: {providers}", + "onboarding_apikey_title_ollama": "Ollama 연결", + "onboarding_apikey_title_openrouter": "{display} API 키 입력", + "onboarding_apikey_title_default": "API 키 입력", + "onboarding_apikey_desc_ollama": "로컬 Ollama 인스턴스에 연결합니다.\nOllama가 아직 설치되지 않았다면 설정을 도와드립니다.", + "onboarding_apikey_desc_openrouter": "{display} API 키를 입력하세요. 해당 지역에서 직접 접근할 수 없다면 대신 OpenRouter를 대체 수단으로 사용할 수 있습니다.", + "onboarding_apikey_desc_default": "선택한 공급자의 API 키를 입력하세요.", + "onboarding_validate_invalid_url": "유효한 URL을 입력하세요 (예: http://localhost:11434)", + "onboarding_validate_api_key_required": "API 키는 필수입니다", + "onboarding_validate_api_key_too_short": "API 키가 너무 짧은 것 같습니다", + "onboarding_agent_name_title": "에이전트 이름 짓기", + "onboarding_agent_name_desc": "에이전트에 이름을 지어 주세요.", + "onboarding_field_agent_name_label": "에이전트 이름", + "onboarding_field_agent_name_placeholder": "이름을 입력하세요", + "onboarding_validate_agent_name_too_long": "에이전트 이름은 20자 이하여야 합니다", + "onboarding_validate_agent_name_invalid": "잘못된 에이전트 이름 제출입니다", + "onboarding_user_profile_title": "당신의 이름", + "onboarding_user_profile_desc": "에이전트가 당신을 어떻게 부르면 될까요?", + "onboarding_field_user_name_label": "당신의 이름", + "onboarding_field_user_name_placeholder": "이름", + "onboarding_validate_expected_dict": "폼 값의 딕셔너리가 필요합니다", + "onboarding_validate_user_name_too_long": "이름은 20자 이하여야 합니다" +} diff --git a/app/i18n/ui_messages.zh-CN.json b/app/i18n/ui_messages.zh-CN.json new file mode 100644 index 00000000..f2f2bd0d --- /dev/null +++ b/app/i18n/ui_messages.zh-CN.json @@ -0,0 +1,78 @@ +{ + "onboarding_step_required": "此步骤为必填项,无法跳过", + "onboarding_at_first_step": "已处于第一步", + + "model_no_model_specified": "未指定模型", + "model_test_failed": "测试失败", + + "livingui_name_desc_required": "名称和描述为必填项", + "livingui_project_created": "项目已创建,正在开始开发…", + "livingui_dev_run_failed": "启动开发运行失败", + "livingui_launch_failed": "启动 Living UI 服务器失败", + "livingui_no_zip": "未上传 ZIP 文件", + "livingui_no_bundle": "未上传捆绑包文件", + "livingui_app_id_name_required": "应用 ID 和名称为必填项", + "livingui_installing_marketplace": "正在从市场安装…", + + "memory_disabled": "记忆已禁用。请先启用记忆模式。", + "memory_no_unprocessed_events": "没有可处理的未处理事件。", + "memory_processing_queued": "记忆处理运行已加入队列", + + "schedule_not_found": "未找到计划", + + "integration_unknown": "未知集成:{integration_id}", + "integration_unknown_generic": "未知集成", + "integration_oauth_cancelled": "OAuth 已取消", + "integration_connection_cancelled": "连接已取消", + "integration_add_account_cancelled": "添加账户已取消", + + "playbook_catalogue_not_found": "未找到剧本目录。", + + "update_checking": "正在检查更新…", + "update_check_failed": "检查更新失败:{error}", + "update_off_branch": "当前处于分支“{branch}”(v{current})。更新仅在 {update_branch} 分支上生效;请切换到该分支以更新。", + "update_up_to_date": "CraftBot 已是最新版本(v{current})。", + "update_available": "有可用更新:v{current} → v{latest}", + "update_available_starting": "有可用更新:v{current} → v{latest}。正在开始更新…", + "update_launching": "正在新窗口中启动更新程序(正在拉取 {branch})…", + "update_shutting_down": "正在关闭,更新程序稍后将重新启动 CraftBot。", + "update_failed": "更新失败:{error}", + + "onboarding_intro_title": "很高兴认识你,我是 CraftBot!", + "onboarding_intro_desc": "很高兴认识你,我是 CraftBot!我在这里帮你处理工作和生活。那么在开始之前,我们需要先配置一些基础设置。", + "onboarding_provider_title": "选择 LLM 供应商", + "onboarding_provider_desc": "选择智能体使用的 AI 供应商。", + "onboarding_provider_desc_openai": "GPT 模型", + "onboarding_provider_desc_gemini": "Gemini 模型", + "onboarding_provider_desc_byteplus": "Kimi 模型", + "onboarding_provider_desc_anthropic": "Claude 模型", + "onboarding_provider_desc_deepseek": "DeepSeek 模型", + "onboarding_provider_desc_minimax": "MiniMax 模型", + "onboarding_provider_desc_moonshot": "Moonshot 模型", + "onboarding_provider_desc_grok": "Grok 模型", + "onboarding_provider_desc_glm": "GLM 模型", + "onboarding_provider_desc_fugu": "Fugu 模型", + "onboarding_provider_desc_remote": "自托管模型", + "onboarding_provider_invalid": "供应商无效。请从以下选项中选择:{providers}", + "onboarding_apikey_title_ollama": "连接 Ollama", + "onboarding_apikey_title_openrouter": "输入 {display} API 密钥", + "onboarding_apikey_title_default": "输入 API 密钥", + "onboarding_apikey_desc_ollama": "连接到你的本地 Ollama 实例。\n如果尚未安装 Ollama,我们会帮你设置好。", + "onboarding_apikey_desc_openrouter": "输入你的 {display} API 密钥。如果你所在地区无法直连访问,可改用 OpenRouter 作为后备方案。", + "onboarding_apikey_desc_default": "为所选供应商输入你的 API 密钥。", + "onboarding_validate_invalid_url": "请输入有效的 URL(例如 http://localhost:11434)", + "onboarding_validate_api_key_required": "API 密钥为必填项", + "onboarding_validate_api_key_too_short": "API 密钥似乎太短", + "onboarding_agent_name_title": "为你的智能体命名", + "onboarding_agent_name_desc": "给你的智能体起个名字。", + "onboarding_field_agent_name_label": "智能体名称", + "onboarding_field_agent_name_placeholder": "输入一个名称", + "onboarding_validate_agent_name_too_long": "智能体名称不得超过 20 个字符", + "onboarding_validate_agent_name_invalid": "智能体名称提交无效", + "onboarding_user_profile_title": "你的名字", + "onboarding_user_profile_desc": "你的智能体该怎么称呼你?", + "onboarding_field_user_name_label": "你的名字", + "onboarding_field_user_name_placeholder": "你的名字", + "onboarding_validate_expected_dict": "预期为一个表单值字典", + "onboarding_validate_user_name_too_long": "名字不得超过 20 个字符" +} diff --git a/app/i18n/ui_messages.zh-TW.json b/app/i18n/ui_messages.zh-TW.json new file mode 100644 index 00000000..b7629b46 --- /dev/null +++ b/app/i18n/ui_messages.zh-TW.json @@ -0,0 +1,78 @@ +{ + "onboarding_step_required": "此步驟為必填,無法略過", + "onboarding_at_first_step": "已在第一個步驟", + + "model_no_model_specified": "未指定模型", + "model_test_failed": "測試失敗", + + "livingui_name_desc_required": "必須填寫名稱與描述", + "livingui_project_created": "專案已建立,正在開始開發…", + "livingui_dev_run_failed": "無法開始開發執行", + "livingui_launch_failed": "無法啟動 Living UI 伺服器", + "livingui_no_zip": "未上傳 ZIP 檔", + "livingui_no_bundle": "未上傳套件檔", + "livingui_app_id_name_required": "必須填寫應用程式 ID 與名稱", + "livingui_installing_marketplace": "正在從市集安裝…", + + "memory_disabled": "記憶已停用。請先啟用記憶模式。", + "memory_no_unprocessed_events": "沒有可處理的未處理事件。", + "memory_processing_queued": "記憶處理執行已排入佇列", + + "schedule_not_found": "找不到排程", + + "integration_unknown": "未知的整合:{integration_id}", + "integration_unknown_generic": "未知的整合", + "integration_oauth_cancelled": "已取消 OAuth", + "integration_connection_cancelled": "已取消連接", + "integration_add_account_cancelled": "已取消新增帳號", + + "playbook_catalogue_not_found": "找不到劇本目錄。", + + "update_checking": "正在檢查更新…", + "update_check_failed": "檢查更新失敗:{error}", + "update_off_branch": "目前在「{branch}」分支上(v{current})。更新只會套用於 {update_branch} 分支;請切換至該分支以更新。", + "update_up_to_date": "CraftBot 已是最新版本(v{current})。", + "update_available": "有可用的更新:v{current} → v{latest}", + "update_available_starting": "有可用的更新:v{current} → v{latest}。正在開始更新…", + "update_launching": "正在於新視窗中啟動更新程式(正在提取 {branch})…", + "update_shutting_down": "正在關閉,更新程式很快將重新啟動 CraftBot。", + "update_failed": "更新失敗:{error}", + + "onboarding_intro_title": "很高興認識你,我是 CraftBot!", + "onboarding_intro_desc": "很高興認識你,我是 CraftBot!我在這裡協助你處理工作或生活。現在,在我們開始之前,有一些基本設定需要先完成。", + "onboarding_provider_title": "選取 LLM 供應商", + "onboarding_provider_desc": "選擇要用於代理的 AI 供應商。", + "onboarding_provider_desc_openai": "GPT 模型", + "onboarding_provider_desc_gemini": "Gemini 模型", + "onboarding_provider_desc_byteplus": "Kimi 模型", + "onboarding_provider_desc_anthropic": "Claude 模型", + "onboarding_provider_desc_deepseek": "DeepSeek 模型", + "onboarding_provider_desc_minimax": "MiniMax 模型", + "onboarding_provider_desc_moonshot": "Moonshot 模型", + "onboarding_provider_desc_grok": "Grok 模型", + "onboarding_provider_desc_glm": "GLM 模型", + "onboarding_provider_desc_fugu": "Fugu 模型", + "onboarding_provider_desc_remote": "自行架設的模型", + "onboarding_provider_invalid": "供應商無效。請從以下選擇:{providers}", + "onboarding_apikey_title_ollama": "連接 Ollama", + "onboarding_apikey_title_openrouter": "輸入 {display} API 金鑰", + "onboarding_apikey_title_default": "輸入 API 金鑰", + "onboarding_apikey_desc_ollama": "連接到你本機的 Ollama 執行個體。\n若尚未安裝 Ollama,我們會協助你完成設定。", + "onboarding_apikey_desc_openrouter": "輸入你的 {display} API 金鑰。若你所在地區無法直接存取,可改用 OpenRouter 作為備援。", + "onboarding_apikey_desc_default": "輸入所選供應商的 API 金鑰。", + "onboarding_validate_invalid_url": "請輸入有效的網址(例如 http://localhost:11434)", + "onboarding_validate_api_key_required": "API 金鑰為必填", + "onboarding_validate_api_key_too_short": "API 金鑰似乎太短", + "onboarding_agent_name_title": "為你的代理命名", + "onboarding_agent_name_desc": "為你的代理取個名字。", + "onboarding_field_agent_name_label": "代理名稱", + "onboarding_field_agent_name_placeholder": "輸入名稱", + "onboarding_validate_agent_name_too_long": "代理名稱不得超過 20 個字元", + "onboarding_validate_agent_name_invalid": "代理名稱送出內容無效", + "onboarding_user_profile_title": "你的名字", + "onboarding_user_profile_desc": "你的代理該如何稱呼你?", + "onboarding_field_user_name_label": "你的名字", + "onboarding_field_user_name_placeholder": "你的名字", + "onboarding_validate_expected_dict": "預期為表單值的字典", + "onboarding_validate_user_name_too_long": "名字不得超過 20 個字元" +} diff --git a/app/onboarding/interfaces/steps.py b/app/onboarding/interfaces/steps.py index 5d3c1243..16c25388 100644 --- a/app/onboarding/interfaces/steps.py +++ b/app/onboarding/interfaces/steps.py @@ -10,6 +10,8 @@ from dataclasses import dataclass, field from typing import Any, Dict, List, Optional, Protocol, runtime_checkable +from app.i18n import tui + @dataclass class StepOption: @@ -98,14 +100,16 @@ class IntroStep: """ name = "intro" - title = "Nice to meet you, I'm CraftBot!" - description = ( - "Nice to meet you, I am CraftBot! I am here to help you with work or " - "life. Now before we begin, there are some baseline settings we need " - "to configure." - ) required = True + @property + def title(self) -> str: + return tui("onboarding_intro_title") + + @property + def description(self) -> str: + return tui("onboarding_intro_desc") + def get_options(self) -> List[StepOption]: return [] @@ -120,10 +124,16 @@ class ProviderStep: """LLM provider selection step.""" name = "provider" - title = "Select LLM Provider" - description = "Choose which AI provider to use for the agent." required = True + @property + def title(self) -> str: + return tui("onboarding_provider_title") + + @property + def description(self) -> str: + return tui("onboarding_provider_desc") + # Provider options with their display names PROVIDERS = [ ("openai", "OpenAI", "GPT models"), @@ -144,7 +154,7 @@ def get_options(self) -> List[StepOption]: StepOption( value=provider_id, label=label, - description=desc, + description=tui(f"onboarding_provider_desc_{provider_id}"), default=(provider_id == "openai"), ) for provider_id, label, desc in self.PROVIDERS @@ -154,7 +164,9 @@ def validate(self, value: Any) -> tuple[bool, Optional[str]]: valid_providers = [p[0] for p in self.PROVIDERS] if value in valid_providers: return True, None - return False, f"Invalid provider. Choose from: {', '.join(valid_providers)}" + return False, tui( + "onboarding_provider_invalid", providers=", ".join(valid_providers) + ) def get_default(self) -> str: # Check settings.json for existing provider @@ -231,26 +243,20 @@ def _subscription_connected(self) -> bool: @property def title(self) -> str: if self.provider == "remote": - return "Connect Ollama" + return tui("onboarding_apikey_title_ollama") if self.provider in self.OPENROUTER_PROXIED: display = self.OPENROUTER_PROXIED_DISPLAY.get(self.provider, self.provider) - return f"Enter {display} API Key" - return "Enter API Key" + return tui("onboarding_apikey_title_openrouter", display=display) + return tui("onboarding_apikey_title_default") @property def description(self) -> str: if self.provider == "remote": - return ( - "Connect to your local Ollama instance.\n" - "If Ollama isn't installed yet, we'll help you set it up." - ) + return tui("onboarding_apikey_desc_ollama") if self.provider in self.OPENROUTER_PROXIED: display = self.OPENROUTER_PROXIED_DISPLAY.get(self.provider, self.provider) - return ( - f"Enter your {display} API key. If your region doesn't have direct access, " - f"you can use OpenRouter as a fallback instead." - ) - return "Enter your API key for the selected provider." + return tui("onboarding_apikey_desc_openrouter", display=display) + return tui("onboarding_apikey_desc_default") def get_options(self) -> List[StepOption]: # Free-form input, no options @@ -263,14 +269,14 @@ def validate(self, value: Any) -> tuple[bool, Optional[str]]: return True, None # Empty = use default URL v = value.strip() if not (v.startswith("http://") or v.startswith("https://")): - return False, "Please enter a valid URL (e.g. http://localhost:11434)" + return False, tui("onboarding_validate_invalid_url") return True, None # Proxied providers submit {api_key, via, or_model?} dict if self.provider in self.OPENROUTER_PROXIED and isinstance(value, dict): api_key = value.get("api_key", "") if not api_key or len(str(api_key).strip()) < 10: - return False, "API key is required" + return False, tui("onboarding_validate_api_key_required") return True, None # A connected subscription (ChatGPT Plus/Pro, SuperGrok) authorizes the @@ -281,10 +287,10 @@ def validate(self, value: Any) -> tuple[bool, Optional[str]]: return True, None if not value or not isinstance(value, str): - return False, "API key is required" + return False, tui("onboarding_validate_api_key_required") if len(value.strip()) < 10: - return False, "API key seems too short" + return False, tui("onboarding_validate_api_key_too_short") return True, None @@ -305,18 +311,24 @@ class AgentNameStep: """Agent name configuration step (name only, no avatar).""" name = "agent_name" - title = "Name your agent" - description = "Give your agent a name." required = False + @property + def title(self) -> str: + return tui("onboarding_agent_name_title") + + @property + def description(self) -> str: + return tui("onboarding_agent_name_desc") + def get_form_fields(self) -> List[FormField]: return [ FormField( name="agent_name", - label="Agent Name", + label=tui("onboarding_field_agent_name_label"), field_type="text", default="CraftBot", - placeholder="Enter a name", + placeholder=tui("onboarding_field_agent_name_placeholder"), ), ] @@ -327,14 +339,14 @@ def validate(self, value: Any) -> tuple[bool, Optional[str]]: # Accept legacy string submissions (plain text name) for backward compat. if isinstance(value, str): if len(value) > 20: - return False, "Agent name must be 20 characters or fewer" + return False, tui("onboarding_validate_agent_name_too_long") return True, None if isinstance(value, dict): agent_name = value.get("agent_name") if agent_name and len(str(agent_name)) > 20: - return False, "Agent name must be 20 characters or fewer" + return False, tui("onboarding_validate_agent_name_too_long") return True, None - return False, "Invalid agent name submission" + return False, tui("onboarding_validate_agent_name_invalid") def get_default(self) -> Dict[str, Any]: return {"agent_name": "CraftBot"} @@ -350,10 +362,16 @@ class UserProfileStep: """ name = "user_profile" - title = "Your Name" - description = "What should your agent call you?" required = False + @property + def title(self) -> str: + return tui("onboarding_user_profile_title") + + @property + def description(self) -> str: + return tui("onboarding_user_profile_desc") + @staticmethod def fetch_geolocation() -> str: """Fetch user's location from IP. Returns 'City, Country' or '' on failure.""" @@ -388,9 +406,9 @@ def get_form_fields(self) -> List[FormField]: return [ FormField( name="user_name", - label="Your Name", + label=tui("onboarding_field_user_name_label"), field_type="text", - placeholder="Your name", + placeholder=tui("onboarding_field_user_name_placeholder"), default="", ), ] @@ -424,10 +442,10 @@ def get_options(self) -> List[StepOption]: def validate(self, value: Any) -> tuple[bool, Optional[str]]: """Validate the form data dict. The name is optional.""" if not isinstance(value, dict): - return False, "Expected a dictionary of form values" + return False, tui("onboarding_validate_expected_dict") user_name = value.get("user_name") if user_name and len(str(user_name)) > 20: - return False, "Name must be 20 characters or fewer" + return False, tui("onboarding_validate_user_name_too_long") return True, None def get_default(self) -> Dict[str, Any]: diff --git a/app/ui_layer/adapters/browser_adapter.py b/app/ui_layer/adapters/browser_adapter.py index 8935e678..1e98d762 100644 --- a/app/ui_layer/adapters/browser_adapter.py +++ b/app/ui_layer/adapters/browser_adapter.py @@ -24,6 +24,7 @@ ) from agent_core.utils.logger import logger from app.config import AGENT_WORKSPACE_ROOT, APP_DATA_PATH +from app.i18n import tui from app.ui_layer.adapters.base import InterfaceAdapter from app.ui_layer.settings import ( # General settings @@ -1992,7 +1993,7 @@ async def progress(msg: str) -> None: await self._broadcast( { "type": "update_progress", - "data": {"message": f"Update failed: {e}"}, + "data": {"message": tui("update_failed", error=str(e))}, } ) @@ -2361,7 +2362,7 @@ async def _handle_onboarding_skip(self) -> None: "type": "onboarding_skip", "data": { "success": False, - "error": "This step is required and cannot be skipped", + "error": tui("onboarding_step_required"), }, } ) @@ -2450,7 +2451,7 @@ async def _handle_onboarding_back(self) -> None: "type": "onboarding_back", "data": { "success": False, - "error": "Already at the first step", + "error": tui("onboarding_at_first_step"), }, } ) @@ -2617,7 +2618,7 @@ async def _handle_local_llm_pull_model( await self._broadcast( { "type": "local_llm_pull_model", - "data": {"success": False, "error": "No model specified"}, + "data": {"success": False, "error": tui("model_no_model_specified")}, } ) return @@ -2717,7 +2718,7 @@ async def _handle_living_ui_wizard_finalize(self, data: Dict[str, Any]) -> None: name = str(config.get("name", "")).strip() description = str(config.get("description", "")).strip() if not name or not description: - raise ValueError("Name and description are required") + raise ValueError(tui("livingui_name_desc_required")) living_ui_dir = Path(self._living_ui_manager.living_ui_dir) image_notes = self._wizard_image_notes.pop(wizard_id, None) @@ -2856,7 +2857,7 @@ async def _handle_living_ui_wizard_finalize(self, data: Dict[str, Any]) -> None: "projectId": project.id, "phase": "initializing", "progress": 10, - "message": "Project created, starting development...", + "message": tui("livingui_project_created"), }, } ) @@ -2864,7 +2865,7 @@ async def _handle_living_ui_wizard_finalize(self, data: Dict[str, Any]) -> None: # Queue the build run in the project's dedicated session. session_id = await self._living_ui_manager.start_development_run(project.id) if not session_id: - raise RuntimeError("Failed to start development run") + raise RuntimeError(tui("livingui_dev_run_failed")) # CHAT-PATH tail: tell the session that ran living_ui_scaffold # which project resulted. Without this the origin agent's last @@ -2957,7 +2958,7 @@ async def _handle_living_ui_create(self, data: Dict[str, Any]) -> None: "type": "living_ui_error", "data": { "projectId": "", - "error": "Name and description are required", + "error": tui("livingui_name_desc_required"), }, } ) @@ -3047,7 +3048,7 @@ async def _handle_living_ui_create(self, data: Dict[str, Any]) -> None: "projectId": project.id, "phase": "initializing", "progress": 10, - "message": "Project created, starting development...", + "message": tui("livingui_project_created"), }, } ) @@ -3070,7 +3071,7 @@ async def _handle_living_ui_create(self, data: Dict[str, Any]) -> None: "type": "living_ui_error", "data": { "projectId": project.id, - "error": "Failed to start development run", + "error": tui("livingui_dev_run_failed"), }, } ) @@ -3417,7 +3418,9 @@ async def _living_ui_import_handler(self, request: "web.Request") -> "web.Respon zip_path = tmp.name if not zip_path: - return web.json_response({"error": "No ZIP file uploaded"}, status=400) + return web.json_response( + {"error": tui("livingui_no_zip")}, status=400 + ) return web.json_response( { @@ -3624,7 +3627,7 @@ async def _profile_inspect_handler(self, request: "web.Request") -> "web.Respons bundle_path = await self._stage_uploaded_bundle(request) if not bundle_path: return web.json_response( - {"error": "No bundle file uploaded"}, status=400 + {"error": tui("livingui_no_bundle")}, status=400 ) result = inspect_bundle(bundle_path) # Read bytes into memory and delete the temp file immediately so a @@ -3868,7 +3871,7 @@ async def broadcast_living_ui_ready( "type": "living_ui_error", "data": { "projectId": project_id, - "error": "Failed to launch Living UI server", + "error": tui("livingui_launch_failed"), }, } ) @@ -4172,6 +4175,7 @@ async def _handle_settings_get(self) -> None: settings = { "agentName": result.get("agent_name", "CraftBot"), "theme": "dark", # Theme is managed client-side + "language": result.get("language", "en"), "agentProfilePictureUrl": result.get( "agent_profile_picture_url", "/api/agent-profile-picture" ), @@ -4207,6 +4211,8 @@ async def _handle_settings_update(self, settings: Dict[str, Any]) -> None: update_data = {} if "agentName" in settings: update_data["agent_name"] = settings["agentName"] + if "language" in settings: + update_data["language"] = settings["language"] result = update_general_settings(update_data) @@ -5363,7 +5369,7 @@ async def _handle_memory_process_trigger(self) -> None: "type": "memory_process_trigger", "data": { "success": False, - "error": "Memory is disabled. Enable memory mode first.", + "error": tui("memory_disabled"), }, } ) @@ -5380,7 +5386,7 @@ async def _handle_memory_process_trigger(self) -> None: "type": "memory_process_trigger", "data": { "success": False, - "error": "No unprocessed events to process.", + "error": tui("memory_no_unprocessed_events"), }, } ) @@ -5403,7 +5409,7 @@ async def _handle_memory_process_trigger(self) -> None: "type": "memory_process_trigger", "data": { "success": True, - "message": "Memory processing run queued", + "message": tui("memory_processing_queued"), }, } ) @@ -5427,7 +5433,7 @@ async def _handle_memory_schedule_get(self) -> None: await self._broadcast( { "type": "memory_schedule_get", - "data": {"success": False, "error": "Schedule not found"}, + "data": {"success": False, "error": tui("schedule_not_found")}, } ) return @@ -5946,7 +5952,7 @@ async def _handle_model_connection_test( "type": "model_connection_test", "data": { "success": False, - "message": "Test failed", + "message": tui("model_test_failed"), "provider": provider, "error": str(e), }, @@ -7138,7 +7144,9 @@ async def _handle_integration_connect_token( try: system = self._system_for(integration_id) if system is None: - success, message = False, f"Unknown integration: {integration_id}" + success, message = False, tui( + "integration_unknown", integration_id=integration_id + ) else: from app.data.action.integrations._helpers import system_connect_token @@ -7195,7 +7203,9 @@ async def _run_oauth_flow(self, integration_id: str) -> None: try: system = self._system_for(integration_id) if system is None: - success, message = False, f"Unknown integration: {integration_id}" + success, message = False, tui( + "integration_unknown", integration_id=integration_id + ) else: success, message, _accounts = await system.add_account(integration_id) await self._broadcast( @@ -7222,7 +7232,7 @@ async def _run_oauth_flow(self, integration_id: str) -> None: "type": "integration_connect_result", "data": { "success": False, - "message": "OAuth cancelled", + "message": tui("integration_oauth_cancelled"), "id": integration_id, }, } @@ -7297,7 +7307,7 @@ async def _run_interactive_flow(self, integration_id: str) -> None: "type": "integration_connect_result", "data": { "success": False, - "message": "Connection cancelled", + "message": tui("integration_connection_cancelled"), "id": integration_id, }, } @@ -7526,7 +7536,7 @@ async def _run_accounts_add( "id": integration_id, "requestId": request_id, "ok": False, - "message": "Add account cancelled", + "message": tui("integration_add_account_cancelled"), }, self._current_accounts(integration_id), ), @@ -7663,7 +7673,7 @@ async def _handle_integration_get_config(self, integration_id: str) -> None: "data": { "id": integration_id, "success": False, - "error": "Unknown integration", + "error": tui("integration_unknown_generic"), }, } ) @@ -7873,7 +7883,7 @@ async def _handle_playbook_list(self) -> None: "type": "playbook_list", "data": { "success": False, - "error": "Playbook catalogue not found.", + "error": tui("playbook_catalogue_not_found"), "playbooks": [], }, } @@ -7971,7 +7981,7 @@ async def _handle_marketplace_install( "type": "living_ui_marketplace_install", "data": { "success": False, - "error": "App ID and name are required", + "error": tui("livingui_app_id_name_required"), "appId": app_id, }, } @@ -7994,7 +8004,7 @@ async def _handle_marketplace_install( "projectId": project_id, "phase": "initializing", "progress": 10, - "message": "Installing from marketplace...", + "message": tui("livingui_installing_marketplace"), }, } ) diff --git a/app/ui_layer/browser/frontend/package-lock.json b/app/ui_layer/browser/frontend/package-lock.json index 7831af07..1ba478c3 100644 --- a/app/ui_layer/browser/frontend/package-lock.json +++ b/app/ui_layer/browser/frontend/package-lock.json @@ -12,11 +12,13 @@ "@ridemountainpig/svgl-react": "^1.0.17", "@tanstack/react-virtual": "^3.13.23", "driver.js": "^1.8.0", + "i18next": "^23.16.8", "lucide-react": "^0.344.0", "prism-react-renderer": "^2.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-grid-layout": "^1.4.4", + "react-i18next": "^14.1.3", "react-markdown": "^9.0.1", "react-redux": "^9.3.0", "react-router-dom": "^6.22.0", @@ -291,6 +293,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -2808,6 +2819,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-parse-stringify": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.1.0.tgz", + "integrity": "sha512-E0oAXcELOtsXe+BmpJ2EZyedbldPpriV5vICzEuo6xjC/D1lDukOI7KrpfQGF2Qc4wWEy0nk3bFORS2K5ZAhFQ==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -2818,6 +2838,29 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/i18next": { + "version": "23.16.8", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz", + "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4426,6 +4469,28 @@ "react-dom": ">= 16.3.0" } }, + "node_modules/react-i18next": { + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.3.tgz", + "integrity": "sha512-wZnpfunU6UIAiJ+bxwOiTmBOAaB14ha97MjOEnLGac2RJ+h/maIYXZuTHlmyqQVX1UVHmU1YDTQ5vxLmwfXTjw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -5217,6 +5282,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/app/ui_layer/browser/frontend/package.json b/app/ui_layer/browser/frontend/package.json index bafb5a8f..c14323fa 100644 --- a/app/ui_layer/browser/frontend/package.json +++ b/app/ui_layer/browser/frontend/package.json @@ -14,11 +14,13 @@ "@ridemountainpig/svgl-react": "^1.0.17", "@tanstack/react-virtual": "^3.13.23", "driver.js": "^1.8.0", + "i18next": "^23.16.8", "lucide-react": "^0.344.0", "prism-react-renderer": "^2.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-grid-layout": "^1.4.4", + "react-i18next": "^14.1.3", "react-markdown": "^9.0.1", "react-redux": "^9.3.0", "react-router-dom": "^6.22.0", diff --git a/app/ui_layer/browser/frontend/src/App.tsx b/app/ui_layer/browser/frontend/src/App.tsx index 502e0e3b..08f44644 100644 --- a/app/ui_layer/browser/frontend/src/App.tsx +++ b/app/ui_layer/browser/frontend/src/App.tsx @@ -1,4 +1,5 @@ import { useEffect } from 'react' +import { useTranslation } from 'react-i18next' import { Routes, Route, Navigate, useParams } from 'react-router-dom' import { Layout } from './components/layout' import { ChatPage } from './pages/Chat' @@ -31,6 +32,7 @@ function SessionChatRoute() { } function App() { + const { t } = useTranslation(['nav', 'common']) const { initReceived, needsHardOnboarding } = useWebSocket() // Fade the main interface in once, right after the onboarding outro hands off @@ -82,7 +84,7 @@ function App() {

- Waking up CraftBot + {t('nav:app.wakingUp')}

diff --git a/app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx b/app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx index 271d7bcf..be3f1556 100644 --- a/app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx +++ b/app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx @@ -1,5 +1,8 @@ import React, { useState, useRef, useEffect, useLayoutEffect, KeyboardEvent, useCallback, ChangeEvent, useMemo } from 'react' +import { useTranslation, Trans } from 'react-i18next' import { useNavigate } from 'react-router-dom' +import i18n from '../../i18n/config' +import { formatDate } from '../../i18n/format' import { Send, Square, Paperclip, Plus, X, Loader2, File, AlertCircle, Mic, MicOff, ChevronDown, Sparkles, BookOpen, Reply } from 'lucide-react' import { useVirtualizer } from '@tanstack/react-virtual' import { useWebSocket } from '../../contexts/WebSocketContext' @@ -144,18 +147,19 @@ const formatDateDivider = (tsMs: number): string => { a.getMonth() === b.getMonth() && a.getDate() === b.getDate() - if (sameDay(date, now)) return 'Today' + if (sameDay(date, now)) return i18n.t('chat:dateDivider.today') const yesterday = new Date(now) yesterday.setDate(yesterday.getDate() - 1) - if (sameDay(date, yesterday)) return 'Yesterday' + if (sameDay(date, yesterday)) return i18n.t('chat:dateDivider.yesterday') if (date.getFullYear() === now.getFullYear()) { - return date.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' }) + return formatDate(date, { weekday: 'long', month: 'long', day: 'numeric' }) } - return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }) + return formatDate(date, { year: 'numeric', month: 'long', day: 'numeric' }) } export function Chat({ sessionId, placeholder }: ChatProps) { + const { t } = useTranslation(['chat', 'common']) const navigate = useNavigate() const { connected, @@ -431,13 +435,13 @@ export function Chat({ sessionId, placeholder }: ChatProps) { const totalSize = pendingAttachments.reduce((sum, att) => sum + att.size, 0) const count = pendingAttachments.length if (count > MAX_ATTACHMENT_COUNT) { - return { valid: false, error: `Maximum ${MAX_ATTACHMENT_COUNT} files allowed. You have ${count} files.` } + return { valid: false, error: t('chat:attachment.tooManyFilesDetailed', { max: MAX_ATTACHMENT_COUNT, have: count }) } } if (totalSize > MAX_TOTAL_SIZE_BYTES) { - return { valid: false, error: `Total size (${formatFileSize(totalSize)}) exceeds 200 MB limit.` } + return { valid: false, error: t('chat:attachment.totalSizeExceeds', { size: formatFileSize(totalSize) }) } } return { valid: true, error: null } - }, [pendingAttachments]) + }, [pendingAttachments, t]) // The live status row renders as one extra virtual row appended after // the timeline — but ONLY while the tail chunk is expanded (or no tail @@ -894,9 +898,9 @@ export function Chat({ sessionId, placeholder }: ChatProps) { enhanceTimeoutRef.current = setTimeout(() => { enhanceTimeoutRef.current = null setEnhancing(false) - showToast('error', 'AI enhance timed out — please try again.') + showToast('error', t('chat:toast.enhanceTimedOut')) }, ENHANCE_TIMEOUT_MS) - }, [input, enhancing, enhancePrompt, showToast]) + }, [input, enhancing, enhancePrompt, showToast, t]) useEffect(() => { return () => { @@ -944,7 +948,7 @@ export function Chat({ sessionId, placeholder }: ChatProps) { const SpeechRecognitionAPI = w.SpeechRecognition || w.webkitSpeechRecognition if (!SpeechRecognitionAPI) { - alert('Speech recognition is not supported in this browser.') + alert(t('chat:voice.notSupported')) return } @@ -974,7 +978,7 @@ export function Chat({ sessionId, placeholder }: ChatProps) { recognition.onerror = (event: any) => { setIsListening(false) if (event.error === 'not-allowed' || event.error === 'service-not-allowed') { - alert('Microphone access denied. Please allow microphone permission in your browser settings.') + alert(t('chat:voice.micDenied')) } } recognition.onend = () => setIsListening(false) @@ -983,7 +987,7 @@ export function Chat({ sessionId, placeholder }: ChatProps) { recognition.start() setIsListening(true) inputRef.current?.focus() - }, [isListening, micLang, dispatch, sessionId]) + }, [isListening, micLang, dispatch, sessionId, t]) // Stop mic if component unmounts while listening useEffect(() => { @@ -1024,7 +1028,7 @@ export function Chat({ sessionId, placeholder }: ChatProps) { ) } if (!connected) { - showToast('info', 'Reconnecting — your message will send when the connection is restored.') + showToast('info', t('chat:toast.reconnecting')) } // Sending always snaps the view back to the newest content. stickToBottomRef.current = true @@ -1137,14 +1141,14 @@ export function Chat({ sessionId, placeholder }: ChatProps) { setPendingAttachments(prev => prev.map(a => a === placeholder ? { ...a, uploadStatus: 'error' as const } : a )) - setAttachmentError(`Failed to upload "${file.name}".`) + setAttachmentError(t('chat:attachment.uploadFailed', { name: file.name })) } xhr.onerror = () => { setPendingAttachments(prev => prev.map(a => a === placeholder ? { ...a, uploadStatus: 'error' as const } : a )) - setAttachmentError(`Failed to upload "${file.name}": network error.`) + setAttachmentError(t('chat:attachment.uploadFailedNetwork', { name: file.name })) } const name = encodeURIComponent(file.name) @@ -1158,7 +1162,7 @@ export function Chat({ sessionId, placeholder }: ChatProps) { const totalFileCount = pendingAttachments.length + files.length if (totalFileCount > MAX_ATTACHMENT_COUNT) { - setAttachmentError(`Maximum ${MAX_ATTACHMENT_COUNT} files allowed.`) + setAttachmentError(t('chat:attachment.tooManyFiles', { max: MAX_ATTACHMENT_COUNT })) return } @@ -1167,11 +1171,11 @@ export function Chat({ sessionId, placeholder }: ChatProps) { // Validate sizes first before doing any I/O for (const file of files) { if (file.size > MAX_TOTAL_SIZE_BYTES) { - setAttachmentError(`File "${file.name}" (${formatFileSize(file.size)}) exceeds the 200 MB limit.`) + setAttachmentError(t('chat:attachment.fileExceedsLimit', { name: file.name, size: formatFileSize(file.size) })) return } if (newTotalSize + file.size > MAX_TOTAL_SIZE_BYTES) { - setAttachmentError(`Adding "${file.name}" would exceed the 200 MB total size limit.`) + setAttachmentError(t('chat:attachment.addWouldExceed', { name: file.name })) return } newTotalSize += file.size @@ -1191,7 +1195,7 @@ export function Chat({ sessionId, placeholder }: ChatProps) { content, }]) } catch { - setAttachmentError(`Failed to read file "${file.name}".`) + setAttachmentError(t('chat:attachment.readFailed', { name: file.name })) return } } else { @@ -1273,7 +1277,7 @@ export function Chat({ sessionId, placeholder }: ChatProps) { in-flow sibling it pushes the whole column down instead. */} {loadingOlderMessages && (
- Loading older messages... + {t('chat:history.loadingOlder')}
)} {rowCount === 0 ? null : ( @@ -1367,9 +1371,9 @@ export function Chat({ sessionId, placeholder }: ChatProps) { )} {showUnreadDivider && ( -
+
- New + {t('chat:unread.label')}
)} @@ -1417,8 +1421,8 @@ export function Chat({ sessionId, placeholder }: ChatProps) { type="button" className={styles.scrollToBottomBtn} onClick={scrollToBottom} - aria-label="Scroll to latest message" - title="Scroll to latest message" + aria-label={t('chat:scrollToLatest')} + title={t('chat:scrollToLatest')} > @@ -1453,8 +1457,15 @@ export function Chat({ sessionId, placeholder }: ChatProps) { {replyTarget && (
- Replying to: {replyTarget.displayName} -
@@ -1464,7 +1475,7 @@ export function Chat({ sessionId, placeholder }: ChatProps) {
{attachmentError || attachmentValidation.error} -
@@ -1477,7 +1488,7 @@ export function Chat({ sessionId, placeholder }: ChatProps) { -
@@ -1512,7 +1523,7 @@ export function Chat({ sessionId, placeholder }: ChatProps) {