diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cbc7d680805..781e4bddd59 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,7 +57,7 @@ jobs: printf '%s\n' "MODELS_DEV_API_JSON=${{ github.workspace }}/packages/opencode/test/tool/fixtures/models-api.json" >> "$GITHUB_ENV" - name: Seed opencode data - if: runner.os != 'Windows' + if: matrix.settings.name != 'windows' working-directory: packages/opencode run: bun script/seed-e2e.ts env: @@ -78,9 +78,9 @@ jobs: OPENCODE_E2E_MODEL: "opencode/gpt-5-nano" - name: Run opencode server - if: runner.os != 'Windows' + if: matrix.settings.name != 'windows' working-directory: packages/opencode - run: bun dev -- --print-logs --log-level WARN serve --port 4096 --hostname 0.0.0.0 & + run: bun dev -- --print-logs --log-level WARN serve --port 4096 --hostname 127.0.0.1 & env: MODELS_DEV_API_JSON: ${{ env.MODELS_DEV_API_JSON }} OPENCODE_DISABLE_MODELS_FETCH: "true" @@ -96,10 +96,10 @@ jobs: OPENCODE_CLIENT: "app" - name: Wait for opencode server - if: runner.os != 'Windows' + if: matrix.settings.name != 'windows' run: | - for i in {1..60}; do - curl -fsS "http://localhost:4096/global/health" > /dev/null && exit 0 + for i in {1..120}; do + curl -fsS "http://127.0.0.1:4096/global/health" > /dev/null && exit 0 sleep 1 done exit 1 @@ -120,9 +120,9 @@ jobs: XDG_CACHE_HOME: ${{ env.XDG_CACHE_HOME }} XDG_CONFIG_HOME: ${{ env.XDG_CONFIG_HOME }} XDG_STATE_HOME: ${{ env.XDG_STATE_HOME }} - PLAYWRIGHT_SERVER_HOST: "localhost" + PLAYWRIGHT_SERVER_HOST: "127.0.0.1" PLAYWRIGHT_SERVER_PORT: "4096" - VITE_OPENCODE_SERVER_HOST: "localhost" + VITE_OPENCODE_SERVER_HOST: "127.0.0.1" VITE_OPENCODE_SERVER_PORT: "4096" OPENCODE_CLIENT: "app" timeout-minutes: 30 diff --git a/.husky/pre-push b/.husky/pre-push index 2fd039d56dd..5d3cc53411b 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,9 +1,20 @@ #!/bin/sh +set -e # Check if bun version matches package.json -EXPECTED_VERSION=$(grep '"packageManager"' package.json | sed 's/.*"bun@\([^"]*\)".*/\1/') -CURRENT_VERSION=$(bun --version) -if [ "$CURRENT_VERSION" != "$EXPECTED_VERSION" ]; then - echo "Error: Bun version $CURRENT_VERSION does not match expected version $EXPECTED_VERSION from package.json" - exit 1 -fi +# keep in sync with packages/script/src/index.ts semver qualifier +bun -e ' +import { semver } from "bun"; +const pkg = await Bun.file("package.json").json(); +const expectedBunVersion = pkg.packageManager?.split("@")[1]; +if (!expectedBunVersion) { + throw new Error("packageManager field not found in root package.json"); +} +const expectedBunVersionRange = `^${expectedBunVersion}`; +if (!semver.satisfies(process.versions.bun, expectedBunVersionRange)) { + throw new Error(`This script requires bun@${expectedBunVersionRange}, but you are using bun@${process.versions.bun}`); +} +if (process.versions.bun !== expectedBunVersion) { + console.warn(`Warning: Bun version ${process.versions.bun} differs from expected ${expectedBunVersion}`); +} +' bun typecheck diff --git a/STATS.md b/STATS.md index a7af9bd251a..25c848ccb34 100644 --- a/STATS.md +++ b/STATS.md @@ -206,3 +206,4 @@ | 2026-01-18 | 4,627,623 (+238,065) | 1,839,171 (+33,856) | 6,466,794 (+271,921) | | 2026-01-19 | 4,861,108 (+233,485) | 1,863,112 (+23,941) | 6,724,220 (+257,426) | | 2026-01-20 | 5,128,999 (+267,891) | 1,903,665 (+40,553) | 7,032,664 (+308,444) | +| 2026-01-21 | 5,444,842 (+315,843) | 1,962,531 (+58,866) | 7,407,373 (+374,709) | diff --git a/bun.lock b/bun.lock index 893ff1d8e89..ce1cf116197 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.28-1", + "version": "1.1.30", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -33,6 +33,7 @@ "@solid-primitives/active-element": "2.1.3", "@solid-primitives/audio": "1.4.2", "@solid-primitives/event-bus": "1.1.2", + "@solid-primitives/i18n": "2.2.1", "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/scroll": "2.1.3", @@ -73,7 +74,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.28-1", + "version": "1.1.30", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -107,7 +108,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.28-1", + "version": "1.1.30", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -134,7 +135,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.28-1", + "version": "1.1.30", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -158,7 +159,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.28-1", + "version": "1.1.30", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -182,7 +183,7 @@ }, "packages/desktop": { "name": "@shuvcode/desktop", - "version": "1.1.28-1", + "version": "1.1.30", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -211,7 +212,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.28-1", + "version": "1.1.30", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -240,7 +241,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.28-1", + "version": "1.1.30", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -256,14 +257,14 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.28-1", + "version": "1.1.30", "bin": { "opencode": "./bin/opencode", }, "dependencies": { "@actions/core": "1.11.1", "@actions/github": "6.0.1", - "@agentclientprotocol/sdk": "0.5.1", + "@agentclientprotocol/sdk": "0.12.0", "@ai-sdk/amazon-bedrock": "3.0.73", "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/azure": "2.0.91", @@ -284,7 +285,7 @@ "@ai-sdk/vercel": "1.0.31", "@ai-sdk/xai": "2.0.51", "@clack/prompts": "1.0.0-alpha.1", - "@gitlab/gitlab-ai-provider": "3.1.2", + "@gitlab/gitlab-ai-provider": "3.1.3", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", "@modelcontextprotocol/sdk": "1.25.2", @@ -361,7 +362,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.28-1", + "version": "1.1.30", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -380,12 +381,11 @@ }, "devDependencies": { "@types/bun": "catalog:", - "@types/semver": "catalog:", }, }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.28-1", + "version": "1.1.30", "devDependencies": { "@hey-api/openapi-ts": "0.90.4", "@tsconfig/node22": "catalog:", @@ -396,7 +396,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.28-1", + "version": "1.1.30", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -409,7 +409,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.28-1", + "version": "1.1.30", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -450,7 +450,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.28-1", + "version": "1.1.30", "dependencies": { "zod": "catalog:", }, @@ -461,7 +461,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.28-1", + "version": "1.1.30", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -560,7 +560,7 @@ "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], - "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.5.1", "", { "dependencies": { "zod": "^3.0.0" } }, "sha512-9bq2TgjhLBSUSC5jE04MEe+Hqw8YePzKghhYZ9QcjOyonY3q2oJfX6GoSO83hURpEnsqEPIrex6VZN3+61fBJg=="], + "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.12.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-V8uH/KK1t7utqyJmTA7y7DzKu6+jKFIXM+ZVouz8E55j8Ej2RV42rEvPKn3/PpBJlliI5crcGk1qQhZ7VwaepA=="], "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.73", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EAAGJ/dfbAZaqIhK3w52hq6cftSLZwXdC6uHKh8Cls1T0N4MxS6ykDf54UyFO3bZWkQxR+Mdw1B3qireGOxtJQ=="], @@ -1060,7 +1060,7 @@ "@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="], - "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.2", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-p0NZhZJSavWDX9r/Px/mOK2YIC803GZa8iRzcg3f1C6S0qfea/HBTe4/NWvT2+2kWIwhCePGuI4FN2UFiUWXUg=="], + "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.3", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-ikumi4PZN/S0f+j/5rb5dBRtORyT41Pl/tj8vHhnpFtpYcxXsaNv2RvCKBVf2/PovvSz2pYMOcpujIU4MdGfyQ=="], "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], @@ -1780,6 +1780,8 @@ "@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="], + "@solid-primitives/i18n": ["@solid-primitives/i18n@2.2.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TnTnE2Ku11MGYZ1JzhJ8pYscwg1fr9MteoYxPwsfxWfh9Jp5K7RRJncJn9BhOHvNLwROjqOHZ46PT7sPHqbcXw=="], + "@solid-primitives/keyed": ["@solid-primitives/keyed@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-BgoEdqPw48URnI+L5sZIHdF4ua4Las1eWEBBPaoSFs42kkhnHue+rwCBPL2Z9ebOyQ75sUhUfOETdJfmv0D6Kg=="], "@solid-primitives/map": ["@solid-primitives/map@0.4.13", "", { "dependencies": { "@solid-primitives/trigger": "^1.1.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-B1zyFbsiTQvqPr+cuPCXO72sRuczG9Swncqk5P74NCGw1VE8qa/Ry9GlfI1e/VdeQYHjan+XkbE3rO2GW/qKew=="], @@ -2002,8 +2004,6 @@ "@types/scheduler": ["@types/scheduler@0.26.0", "", {}, "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA=="], - "@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], - "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], @@ -4248,8 +4248,6 @@ "@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], - "@agentclientprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="], "@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="], diff --git a/flake.lock b/flake.lock index 87f95fb3eb7..5ef276f0a08 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1768569498, - "narHash": "sha256-bB6Nt99Cj8Nu5nIUq0GLmpiErIT5KFshMQJGMZwgqUo=", + "lastModified": 1768302833, + "narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "be5afa0fcb31f0a96bf9ecba05a516c66fcd8114", + "rev": "61db79b0c6b838d9894923920b612048e1201926", "type": "github" }, "original": { diff --git a/infra/app.ts b/infra/app.ts index 1b2351ec8cd..bb627f51ec5 100644 --- a/infra/app.ts +++ b/infra/app.ts @@ -4,6 +4,10 @@ const GITHUB_APP_ID = new sst.Secret("GITHUB_APP_ID") const GITHUB_APP_PRIVATE_KEY = new sst.Secret("GITHUB_APP_PRIVATE_KEY") export const EMAILOCTOPUS_API_KEY = new sst.Secret("EMAILOCTOPUS_API_KEY") const ADMIN_SECRET = new sst.Secret("ADMIN_SECRET") +const DISCORD_SUPPORT_BOT_TOKEN = new sst.Secret("DISCORD_SUPPORT_BOT_TOKEN") +const DISCORD_SUPPORT_CHANNEL_ID = new sst.Secret("DISCORD_SUPPORT_CHANNEL_ID") +const FEISHU_APP_ID = new sst.Secret("FEISHU_APP_ID") +const FEISHU_APP_SECRET = new sst.Secret("FEISHU_APP_SECRET") const bucket = new sst.cloudflare.Bucket("Bucket") export const api = new sst.cloudflare.Worker("Api", { @@ -13,7 +17,16 @@ export const api = new sst.cloudflare.Worker("Api", { WEB_DOMAIN: domain, }, url: true, - link: [bucket, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, ADMIN_SECRET], + link: [ + bucket, + GITHUB_APP_ID, + GITHUB_APP_PRIVATE_KEY, + ADMIN_SECRET, + DISCORD_SUPPORT_BOT_TOKEN, + DISCORD_SUPPORT_CHANNEL_ID, + FEISHU_APP_ID, + FEISHU_APP_SECRET, + ], transform: { worker: (args) => { args.logpush = true diff --git a/nix/hashes.json b/nix/hashes.json index b945a90f3df..c94a408f0b8 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-xVA4r7Qugw0TSx5wiTI5al93FI4D5LlvQo2ab3cUlmE=", - "aarch64-linux": "sha256-EV0U/mXlrnEyCryL9rLlOZvMn6U0+BSgPhTIudVeqTo=", - "aarch64-darwin": "sha256-zQvdRyNEHrpJsQMj8PZH0Ub21EREmDetVaJ0yBCgDlE=", - "x86_64-darwin": "sha256-Tt5k5KBnrsNVIqPET7OFzClerjdR68XYstyCj3KpvdI=" + "x86_64-linux": "sha256-sH6zUk9G4vC6btPZIR9aiSHX0F4aGyUZB7fKbpDUcpE=", + "aarch64-linux": "sha256-CVpdXnFns34hmGwwlRrrI6Uk6B/jZUxfnH4HC2NanEo=", + "aarch64-darwin": "sha256-khP27Iiq+FAZRlzUy7rGXc2MviZjirFH1ShRyd7q1bY=", + "x86_64-darwin": "sha256-nE2p62Tld64sQVMq7j0YNT5Zwjqp22H997+K8xfi1ag=" } } diff --git a/packages/app/AGENTS.md b/packages/app/AGENTS.md index bba2b4ebac4..aefbb05d5aa 100644 --- a/packages/app/AGENTS.md +++ b/packages/app/AGENTS.md @@ -2,6 +2,14 @@ - NEVER try to restart the app, or the server process, EVER. +## Local Dev + +- `opencode dev web` proxies `https://app.opencode.ai`, so local UI/CSS changes will not show there. +- For local UI changes, run the backend and app dev servers separately. +- Backend (from `packages/opencode`): `bun run --conditions=browser ./src/index.ts serve --port 4096` +- App (from `packages/app`): `bun dev -- --port 4444` +- Open `http://localhost:4444` to verify UI changes (it targets the backend at `http://localhost:4096`). + ## SolidJS - Always prefer `createStore` over multiple `createSignal` calls diff --git a/packages/app/package.json b/packages/app/package.json index 62236df25b8..960d3579027 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.28-1", + "version": "1.1.30", "description": "", "type": "module", "exports": { @@ -43,6 +43,7 @@ "@shikijs/transformers": "3.9.2", "@solid-primitives/active-element": "2.1.3", "@solid-primitives/audio": "1.4.2", + "@solid-primitives/i18n": "2.2.1", "@solid-primitives/event-bus": "1.1.2", "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", diff --git a/packages/app/public/apple-touch-icon-v2.png b/packages/app/public/apple-touch-icon-v2.png deleted file mode 120000 index c0d4353db47..00000000000 --- a/packages/app/public/apple-touch-icon-v2.png +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/favicon/apple-touch-icon-v2.png \ No newline at end of file diff --git a/packages/app/public/apple-touch-icon-v3.png b/packages/app/public/apple-touch-icon-v3.png new file mode 120000 index 00000000000..a6f48a689db --- /dev/null +++ b/packages/app/public/apple-touch-icon-v3.png @@ -0,0 +1 @@ +../../ui/src/assets/favicon/apple-touch-icon-v3.png \ No newline at end of file diff --git a/packages/app/public/favicon-96x96-v2.png b/packages/app/public/favicon-96x96-v2.png deleted file mode 120000 index b3129f6bf91..00000000000 --- a/packages/app/public/favicon-96x96-v2.png +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/favicon/favicon-96x96-v2.png \ No newline at end of file diff --git a/packages/app/public/favicon-96x96-v3.png b/packages/app/public/favicon-96x96-v3.png new file mode 120000 index 00000000000..5d21163ce86 --- /dev/null +++ b/packages/app/public/favicon-96x96-v3.png @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-96x96-v3.png \ No newline at end of file diff --git a/packages/app/public/favicon-v2.ico b/packages/app/public/favicon-v2.ico deleted file mode 120000 index d8527270af6..00000000000 --- a/packages/app/public/favicon-v2.ico +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/favicon/favicon-v2.ico \ No newline at end of file diff --git a/packages/app/public/favicon-v2.svg b/packages/app/public/favicon-v2.svg deleted file mode 120000 index 2600394ceae..00000000000 --- a/packages/app/public/favicon-v2.svg +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/favicon/favicon-v2.svg \ No newline at end of file diff --git a/packages/app/public/favicon-v3.ico b/packages/app/public/favicon-v3.ico new file mode 120000 index 00000000000..b3da91f3c45 --- /dev/null +++ b/packages/app/public/favicon-v3.ico @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-v3.ico \ No newline at end of file diff --git a/packages/app/public/favicon-v3.svg b/packages/app/public/favicon-v3.svg new file mode 120000 index 00000000000..fc95f68af4a --- /dev/null +++ b/packages/app/public/favicon-v3.svg @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-v3.svg \ No newline at end of file diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 2fb06f6c0d3..22ccf570d17 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -6,6 +6,7 @@ import { Font } from "@opencode-ai/ui/font" import { MarkedProvider } from "@opencode-ai/ui/context/marked" import { DiffComponentProvider } from "@opencode-ai/ui/context/diff" import { CodeComponentProvider } from "@opencode-ai/ui/context/code" +import { I18nProvider } from "@opencode-ai/ui/context" import { Diff } from "@opencode-ai/ui/diff" import { Code } from "@opencode-ai/ui/code" import { ThemeProvider } from "@opencode-ai/ui/theme" @@ -21,6 +22,7 @@ import { FileProvider } from "@/context/file" import { NotificationProvider } from "@/context/notification" import { DialogProvider } from "@opencode-ai/ui/context/dialog" import { CommandProvider } from "@/context/command" +import { LanguageProvider, useLanguage } from "@/context/language" import { Logo } from "@opencode-ai/ui/logo" import Layout from "@/pages/layout" import DirectoryLayout from "@/pages/directory-layout" @@ -32,6 +34,11 @@ const Home = lazy(() => import("@/pages/home")) const Session = lazy(() => import("@/pages/session")) const Loading = () =>
+function UiI18nBridge(props: ParentProps) { + const language = useLanguage() + return {props.children} +} + declare global { interface Window { __SHUVCODE__?: { updaterEnabled?: boolean; port?: number; serverReady?: boolean } @@ -73,15 +80,19 @@ export function AppBaseProviders(props: ParentProps) { - }> - - - - {props.children} - - - - + + + }> + + + + {props.children} + + + + + + ) diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 789a5d3b748..4ec4c8daa25 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -14,6 +14,7 @@ import { iife } from "@opencode-ai/util/iife" import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js" import { createStore, produce } from "solid-js/store" import { Link } from "@/components/link" +import { useLanguage } from "@/context/language" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" import { usePlatform } from "@/context/platform" @@ -25,13 +26,14 @@ export function DialogConnectProvider(props: { provider: string }) { const globalSync = useGlobalSync() const globalSDK = useGlobalSDK() const platform = usePlatform() + const language = useLanguage() const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!) const methods = createMemo( () => globalSync.data.provider_auth[props.provider] ?? [ { type: "api", - label: "API key", + label: language.t("provider.connect.method.apiKey"), }, ], ) @@ -44,6 +46,12 @@ export function DialogConnectProvider(props: { provider: string }) { const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined)) + const methodLabel = (value?: { type?: string; label?: string }) => { + if (!value) return "" + if (value.type === "api") return language.t("provider.connect.method.apiKey") + return value.label ?? "" + } + async function selectMethod(index: number) { const method = methods()[index] setStore( @@ -112,8 +120,8 @@ export function DialogConnectProvider(props: { provider: string }) { showToast({ variant: "success", icon: "circle-check", - title: `${provider().name} connected`, - description: `${provider().name} models are now available to use.`, + title: language.t("provider.connect.toast.connected.title", { provider: provider().name }), + description: language.t("provider.connect.toast.connected.description", { provider: provider().name }), }) } @@ -142,16 +150,18 @@ export function DialogConnectProvider(props: { provider: string }) {
- Login with Claude Pro/Max + {language.t("provider.connect.title.anthropicProMax")} - Connect {provider().name} + {language.t("provider.connect.title", { provider: provider().name })}
-
Select login method for {provider().name}.
+
+ {language.t("provider.connect.selectMethod", { provider: provider().name })} +
{ @@ -169,7 +179,7 @@ export function DialogConnectProvider(props: { provider: string }) {
- {i.label} + {methodLabel(i)}
)}
@@ -179,7 +189,7 @@ export function DialogConnectProvider(props: { provider: string }) {
- Authorization in progress... + {language.t("provider.connect.status.inProgress")}
@@ -187,7 +197,7 @@ export function DialogConnectProvider(props: { provider: string }) {
- Authorization failed: {store.error} + {language.t("provider.connect.status.failed", { error: store.error ?? "" })}
@@ -206,7 +216,7 @@ export function DialogConnectProvider(props: { provider: string }) { const apiKey = formData.get("apiKey") as string if (!apiKey?.trim()) { - setFormStore("error", "API key is required") + setFormStore("error", language.t("provider.connect.apiKey.required")) return } @@ -227,25 +237,23 @@ export function DialogConnectProvider(props: { provider: string }) {
- OpenCode Zen gives you access to a curated set of reliable optimized models for coding - agents. + {language.t("provider.connect.opencodeZen.line1")}
- With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more. + {language.t("provider.connect.opencodeZen.line2")}
- Visit{" "} + {language.t("provider.connect.opencodeZen.visit.prefix")} - opencode.ai/zen - {" "} - to collect your API key. + {language.t("provider.connect.opencodeZen.visit.link")} + + {language.t("provider.connect.opencodeZen.visit.suffix")}
- Enter your {provider().name} API key to connect your account and use {provider().name} models - in OpenCode. + {language.t("provider.connect.apiKey.description", { provider: provider().name })}
@@ -253,8 +261,8 @@ export function DialogConnectProvider(props: { provider: string }) {
@@ -292,35 +300,44 @@ export function DialogConnectProvider(props: { provider: string }) { const code = formData.get("code") as string if (!code?.trim()) { - setFormStore("error", "Authorization code is required") + setFormStore("error", language.t("provider.connect.oauth.code.required")) return } setFormStore("error", undefined) - const { error } = await globalSDK.client.provider.oauth.callback({ - providerID: props.provider, - method: store.methodIndex, - code, - }) - if (!error) { + const result = await globalSDK.client.provider.oauth + .callback({ + providerID: props.provider, + method: store.methodIndex, + code, + }) + .then((value) => + value.error ? { ok: false as const, error: value.error } : { ok: true as const }, + ) + .catch((error) => ({ ok: false as const, error })) + if (result.ok) { await complete() return } - setFormStore("error", "Invalid authorization code") + const message = result.error instanceof Error ? result.error.message : String(result.error) + setFormStore("error", message || language.t("provider.connect.oauth.code.invalid")) } return (
- Visit this link to collect your authorization - code to connect your account and use {provider().name} models in OpenCode. + {language.t("provider.connect.oauth.code.visit.prefix")} + + {language.t("provider.connect.oauth.code.visit.link")} + + {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
@@ -346,13 +363,19 @@ export function DialogConnectProvider(props: { provider: string }) { }) onMount(async () => { - const result = await globalSDK.client.provider.oauth.callback({ - providerID: props.provider, - method: store.methodIndex, - }) - if (result.error) { - // TODO: show error - dialog.close() + const result = await globalSDK.client.provider.oauth + .callback({ + providerID: props.provider, + method: store.methodIndex, + }) + .then((value) => + value.error ? { ok: false as const, error: value.error } : { ok: true as const }, + ) + .catch((error) => ({ ok: false as const, error })) + if (!result.ok) { + const message = result.error instanceof Error ? result.error.message : String(result.error) + setStore("state", "error") + setStore("error", message) return } await complete() @@ -361,13 +384,22 @@ export function DialogConnectProvider(props: { provider: string }) { return (
- Visit this link and enter the code below to - connect your account and use {provider().name} models in OpenCode. + {language.t("provider.connect.oauth.auto.visit.prefix")} + + {language.t("provider.connect.oauth.auto.visit.link")} + + {language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })}
- +
- Waiting for authorization... + {language.t("provider.connect.status.waiting")}
) diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 2e414a43702..acf146ef52d 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -9,12 +9,14 @@ import { useGlobalSDK } from "@/context/global-sdk" import { type LocalProject, getAvatarColors } from "@/context/layout" import { getFilename } from "@opencode-ai/util/path" import { Avatar } from "@opencode-ai/ui/avatar" +import { useLanguage } from "@/context/language" const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const export function DialogEditProject(props: { project: LocalProject }) { const dialog = useDialog() const globalSDK = useGlobalSDK() + const language = useLanguage() const folderName = createMemo(() => getFilename(props.project.worktree)) const defaultName = createMemo(() => props.project.name || folderName()) @@ -73,6 +75,7 @@ export function DialogEditProject(props: { project: LocalProject }) { const name = store.name.trim() === folderName() ? "" : store.name.trim() await globalSDK.client.project.update({ projectID: props.project.id, + directory: props.project.worktree, name, icon: { color: store.color, override: store.iconUrl }, }) @@ -81,20 +84,20 @@ export function DialogEditProject(props: { project: LocalProject }) { } return ( - +
setStore("name", v)} />
- +
setIconHover(true)} onMouseLeave={() => setIconHover(false)}>
} > - Project icon + {language.t("dialog.project.edit.icon.alt")}
- Recommended size 128x128px + {language.t("dialog.project.edit.icon.hint")} + {language.t("dialog.project.edit.icon.recommended")}
- +
{(color) => ( @@ -209,10 +217,10 @@ export function DialogEditProject(props: { project: LocalProject }) {
diff --git a/packages/app/src/components/dialog-fork.tsx b/packages/app/src/components/dialog-fork.tsx index 472a1994f13..17782f5ab8d 100644 --- a/packages/app/src/components/dialog-fork.tsx +++ b/packages/app/src/components/dialog-fork.tsx @@ -9,6 +9,7 @@ import { List } from "@opencode-ai/ui/list" import { extractPromptFromParts } from "@/utils/prompt" import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client" import { base64Encode } from "@opencode-ai/util/encode" +import { useLanguage } from "@/context/language" interface ForkableMessage { id: string @@ -27,6 +28,7 @@ export const DialogFork: Component = () => { const sdk = useSDK() const prompt = usePrompt() const dialog = useDialog() + const language = useLanguage() const messages = createMemo((): ForkableMessage[] => { const sessionID = params.id @@ -59,7 +61,10 @@ export const DialogFork: Component = () => { if (!sessionID) return const parts = sync.data.part[item.id] ?? [] - const restored = extractPromptFromParts(parts, { directory: sdk.directory }) + const restored = extractPromptFromParts(parts, { + directory: sdk.directory, + attachmentName: language.t("common.attachment"), + }) dialog.close() @@ -73,11 +78,11 @@ export const DialogFork: Component = () => { } return ( - + x.id} items={messages} filterKeys={["text"]} diff --git a/packages/app/src/components/dialog-manage-models.tsx b/packages/app/src/components/dialog-manage-models.tsx index 66d12528891..1ecefa2cbbf 100644 --- a/packages/app/src/components/dialog-manage-models.tsx +++ b/packages/app/src/components/dialog-manage-models.tsx @@ -4,14 +4,16 @@ import { Switch } from "@opencode-ai/ui/switch" import type { Component } from "solid-js" import { useLocal } from "@/context/local" import { popularProviders } from "@/hooks/use-providers" +import { useLanguage } from "@/context/language" export const DialogManageModels: Component = () => { const local = useLocal() + const language = useLanguage() return ( - + `${x?.provider?.id}:${x?.id}`} items={local.model.list()} filterKeys={["provider.name", "name", "id"]} diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx new file mode 100644 index 00000000000..1ee2501de5f --- /dev/null +++ b/packages/app/src/components/dialog-select-directory.tsx @@ -0,0 +1,117 @@ +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Dialog } from "@opencode-ai/ui/dialog" +import { FileIcon } from "@opencode-ai/ui/file-icon" +import { List } from "@opencode-ai/ui/list" +import { getDirectory, getFilename } from "@opencode-ai/util/path" +import { createMemo } from "solid-js" +import { useGlobalSDK } from "@/context/global-sdk" +import { useGlobalSync } from "@/context/global-sync" +import { useLanguage } from "@/context/language" + +interface DialogSelectDirectoryProps { + title?: string + multiple?: boolean + onSelect: (result: string | string[] | null) => void +} + +export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { + const sync = useGlobalSync() + const sdk = useGlobalSDK() + const dialog = useDialog() + const language = useLanguage() + + const home = createMemo(() => sync.data.path.home) + const root = createMemo(() => sync.data.path.home || sync.data.path.directory) + + function join(base: string | undefined, rel: string) { + const b = (base ?? "").replace(/[\\/]+$/, "") + const r = rel.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "") + if (!b) return r + if (!r) return b + return b + "/" + r + } + + function display(rel: string) { + const full = join(root(), rel) + const h = home() + if (!h) return full + if (full === h) return "~" + if (full.startsWith(h + "/") || full.startsWith(h + "\\")) { + return "~" + full.slice(h.length) + } + return full + } + + function normalizeQuery(query: string) { + const h = home() + + if (!query) return query + if (query.startsWith("~/")) return query.slice(2) + + if (h) { + const lc = query.toLowerCase() + const hc = h.toLowerCase() + if (lc === hc || lc.startsWith(hc + "/") || lc.startsWith(hc + "\\")) { + return query.slice(h.length).replace(/^[\\/]+/, "") + } + } + + return query + } + + async function fetchDirs(query: string) { + const directory = root() + if (!directory) return [] as string[] + + const results = await sdk.client.find + .files({ directory, query, type: "directory", limit: 50 }) + .then((x) => x.data ?? []) + .catch(() => []) + + return results.map((x) => x.replace(/[\\/]+$/, "")) + } + + const directories = async (filter: string) => { + const query = normalizeQuery(filter.trim()) + return fetchDirs(query) + } + + function resolve(rel: string) { + const absolute = join(root(), rel) + props.onSelect(props.multiple ? [absolute] : absolute) + dialog.close() + } + + return ( + + x} + onSelect={(path) => { + if (!path) return + resolve(path) + }} + > + {(rel) => { + const path = display(rel) + return ( +
+
+ +
+ + {getDirectory(path)} + + {getFilename(path)} +
+
+
+ ) + }} +
+
+ ) +} diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 2e28c4d2edf..7c3113a544e 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -9,6 +9,7 @@ import { createMemo, createSignal, onCleanup, Show } from "solid-js" import { formatKeybind, useCommand, type CommandOption } from "@/context/command" import { useLayout } from "@/context/layout" import { useFile } from "@/context/file" +import { useLanguage } from "@/context/language" type EntryType = "command" | "file" @@ -18,13 +19,14 @@ type Entry = { title: string description?: string keybind?: string - category: "Commands" | "Files" + category: string option?: CommandOption path?: string } export function DialogSelectFile() { const command = useCommand() + const language = useLanguage() const layout = useLayout() const file = useFile() const dialog = useDialog() @@ -56,7 +58,7 @@ export function DialogSelectFile() { title: option.title, description: option.description, keybind: option.keybind, - category: "Commands", + category: language.t("palette.group.commands"), option, }) @@ -64,7 +66,7 @@ export function DialogSelectFile() { id: "file:" + path, type: "file", title: path, - category: "Files", + category: language.t("palette.group.files"), path, }) @@ -143,8 +145,14 @@ export function DialogSelectFile() { return ( item.id} filterKeys={["title", "description", "category"]} diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx index c29cd827e3b..8eb08878912 100644 --- a/packages/app/src/components/dialog-select-mcp.tsx +++ b/packages/app/src/components/dialog-select-mcp.tsx @@ -4,10 +4,12 @@ import { useSDK } from "@/context/sdk" import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { Switch } from "@opencode-ai/ui/switch" +import { useLanguage } from "@/context/language" export const DialogSelectMcp: Component = () => { const sync = useSync() const sdk = useSDK() + const language = useLanguage() const [loading, setLoading] = createSignal(null) const items = createMemo(() => @@ -34,10 +36,13 @@ export const DialogSelectMcp: Component = () => { const totalCount = createMemo(() => items().length) return ( - + x?.name ?? ""} items={items} filterKeys={["name", "status"]} @@ -60,19 +65,19 @@ export const DialogSelectMcp: Component = () => {
{i.name} - connected + {language.t("mcp.status.connected")} - failed + {language.t("mcp.status.failed")} - needs auth + {language.t("mcp.status.needs_auth")} - disabled + {language.t("mcp.status.disabled")} - ... + {language.t("common.loading.ellipsis")}
diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index 24ec8092deb..1e7c9f52a9a 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid.tsx @@ -5,16 +5,20 @@ import type { IconName } from "@opencode-ai/ui/icons/provider" import { List, type ListRef } from "@opencode-ai/ui/list" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Tag } from "@opencode-ai/ui/tag" +import { Tooltip } from "@opencode-ai/ui/tooltip" import { type Component, onCleanup, onMount, Show } from "solid-js" import { useLocal } from "@/context/local" import { popularProviders, useProviders } from "@/hooks/use-providers" import { DialogConnectProvider } from "./dialog-connect-provider" import { DialogSelectProvider } from "./dialog-select-provider" +import { ModelTooltip } from "./model-tooltip" +import { useLanguage } from "@/context/language" export const DialogSelectModelUnpaid: Component = () => { const local = useLocal() const dialog = useDialog() const providers = useProviders() + const language = useLanguage() let listRef: ListRef | undefined const handleKey = (e: KeyboardEvent) => { @@ -30,14 +34,30 @@ export const DialogSelectModelUnpaid: Component = () => { }) return ( - +
-
Free models provided by OpenCode
+
{language.t("dialog.model.unpaid.freeModels.title")}
(listRef = ref)} items={local.model.list} current={local.model.current()} key={(x) => `${x.provider.id}:${x.id}`} + itemWrapper={(item, node) => ( + + } + > + {node} + + )} onSelect={(x) => { local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true, @@ -48,9 +68,9 @@ export const DialogSelectModelUnpaid: Component = () => { {(i) => (
{i.name} - Free + {language.t("model.tag.free")} - Latest + {language.t("model.tag.latest")}
)} @@ -61,7 +81,7 @@ export const DialogSelectModelUnpaid: Component = () => {
-
Add more models from popular providers
+
{language.t("dialog.model.unpaid.addMore.title")}
{ {i.name} - Recommended + {language.t("dialog.provider.tag.recommended")} -
Connect with Claude Pro/Max or API key
+
{language.t("dialog.provider.anthropic.note")}
)} @@ -99,7 +119,7 @@ export const DialogSelectModelUnpaid: Component = () => { dialog.show(() => ) }} > - View all providers + {language.t("dialog.provider.viewAll")}
diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index d54f9369af1..4fd8af1b368 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -4,18 +4,24 @@ import { useLocal } from "@/context/local" import { useDialog } from "@opencode-ai/ui/context/dialog" import { popularProviders } from "@/hooks/use-providers" import { Button } from "@opencode-ai/ui/button" +import { IconButton } from "@opencode-ai/ui/icon-button" import { Tag } from "@opencode-ai/ui/tag" import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" +import { Tooltip } from "@opencode-ai/ui/tooltip" import { DialogSelectProvider } from "./dialog-select-provider" import { DialogManageModels } from "./dialog-manage-models" +import { ModelTooltip } from "./model-tooltip" +import { useLanguage } from "@/context/language" const ModelList: Component<{ provider?: string class?: string onSelect: () => void + action?: JSX.Element }> = (props) => { const local = useLocal() + const language = useLanguage() const models = createMemo(() => local.model @@ -27,8 +33,8 @@ const ModelList: Component<{ return ( `${x.provider.id}:${x.id}`} items={models} current={local.model.current()} @@ -36,14 +42,28 @@ const ModelList: Component<{ sortBy={(a, b) => a.name.localeCompare(b.name)} groupBy={(x) => x.provider.name} sortGroupsBy={(a, b) => { - if (a.category === "Recent" && b.category !== "Recent") return -1 - if (b.category === "Recent" && a.category !== "Recent") return 1 const aProvider = a.items[0].provider.id const bProvider = b.items[0].provider.id if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1 if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1 return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider) }} + itemWrapper={(item, node) => ( + + } + > + {node} + + )} onSelect={(x) => { local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true, @@ -55,10 +75,10 @@ const ModelList: Component<{
{i.name} - Free + {language.t("model.tag.free")} - Latest + {language.t("model.tag.latest")}
)} @@ -71,14 +91,36 @@ export const ModelSelectorPopover: Component<{ children: JSX.Element }> = (props) => { const [open, setOpen] = createSignal(false) + const dialog = useDialog() + + const handleManage = () => { + setOpen(false) + dialog.show(() => ) + } + const language = useLanguage() return ( {props.children} - Select model - setOpen(false)} class="p-1" /> + {language.t("dialog.model.select.title")} + setOpen(false)} + class="p-1" + action={ + + } + /> @@ -87,10 +129,11 @@ export const ModelSelectorPopover: Component<{ export const DialogSelectModel: Component<{ provider?: string }> = (props) => { const dialog = useDialog() + const language = useLanguage() return ( = (props) => { tabIndex={-1} onClick={() => dialog.show(() => )} > - Connect provider + {language.t("command.provider.connect")} } > @@ -108,7 +151,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => { class="ml-3 mt-5 mb-6 text-text-base self-start" onClick={() => dialog.show(() => )} > - Manage models + {language.t("dialog.model.manage")} ) diff --git a/packages/app/src/components/dialog-select-provider.tsx b/packages/app/src/components/dialog-select-provider.tsx index 76a3dd661ee..517c6b3d1e1 100644 --- a/packages/app/src/components/dialog-select-provider.tsx +++ b/packages/app/src/components/dialog-select-provider.tsx @@ -7,28 +7,38 @@ import { Tag } from "@opencode-ai/ui/tag" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { IconName } from "@opencode-ai/ui/icons/provider" import { DialogConnectProvider } from "./dialog-connect-provider" +import { useLanguage } from "@/context/language" export const DialogSelectProvider: Component = () => { const dialog = useDialog() const providers = useProviders() + const language = useLanguage() + + const popularGroup = () => language.t("dialog.provider.group.popular") + const otherGroup = () => language.t("dialog.provider.group.other") return ( - + x?.id} - items={providers.all} + items={() => { + language.locale() + return providers.all() + }} filterKeys={["id", "name"]} - groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")} + groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())} sortBy={(a, b) => { if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) return a.name.localeCompare(b.name) }} sortGroupsBy={(a, b) => { - if (a.category === "Popular" && b.category !== "Popular") return -1 - if (b.category === "Popular" && a.category !== "Popular") return 1 + const popular = popularGroup() + if (a.category === popular && b.category !== popular) return -1 + if (b.category === popular && a.category !== popular) return 1 return 0 }} onSelect={(x) => { @@ -41,10 +51,10 @@ export const DialogSelectProvider: Component = () => { {i.name} - Recommended + {language.t("dialog.provider.tag.recommended")} -
Connect with Claude Pro/Max or API key
+
{language.t("dialog.provider.anthropic.note")}
)} diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index 90f37212888..bb0ad5b437b 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -10,6 +10,7 @@ import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/serv import { usePlatform } from "@/context/platform" import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" import { useNavigate } from "@solidjs/router" +import { useLanguage } from "@/context/language" type ServerStatus = { healthy: boolean; version?: string } @@ -30,6 +31,7 @@ export function DialogSelectServer() { const dialog = useDialog() const server = useServer() const platform = usePlatform() + const language = useLanguage() const [store, setStore] = createStore({ url: "", adding: false, @@ -109,7 +111,7 @@ export function DialogSelectServer() { setStore("adding", false) if (!result.healthy) { - setStore("error", "Could not connect to server") + setStore("error", language.t("dialog.server.add.error")) return } @@ -122,11 +124,11 @@ export function DialogSelectServer() { } return ( - +
x} current={current()} @@ -168,16 +170,16 @@ export function DialogSelectServer() {
-

Add a server

+

{language.t("dialog.server.add.title")}

{ setStore("url", v) @@ -188,7 +190,7 @@ export function DialogSelectServer() { />
@@ -197,10 +199,8 @@ export function DialogSelectServer() {
-

Default server

-

- Connect to this server on app launch instead of starting a local server. Requires restart. -

+

{language.t("dialog.server.default.title")}

+

{language.t("dialog.server.default.description")}

No server selected} + fallback={ + {language.t("dialog.server.default.none")} + } > } @@ -234,7 +236,7 @@ export function DialogSelectServer() { defaultUrlActions.refetch() }} > - Clear + {language.t("dialog.server.default.clear")}
diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx index 5ef89b8bfc7..1e9575cb23e 100644 --- a/packages/app/src/components/dialog-settings.tsx +++ b/packages/app/src/components/dialog-settings.tsx @@ -2,6 +2,7 @@ import { Component } from "solid-js" import { Dialog } from "@opencode-ai/ui/dialog" import { Tabs } from "@opencode-ai/ui/tabs" import { Icon } from "@opencode-ai/ui/icon" +import { useLanguage } from "@/context/language" import { SettingsGeneral } from "./settings-general" import { SettingsKeybinds } from "./settings-keybinds" import { SettingsPermissions } from "./settings-permissions" @@ -12,6 +13,8 @@ import { SettingsCommands } from "./settings-commands" import { SettingsMcp } from "./settings-mcp" export const DialogSettings: Component = () => { + const language = useLanguage() + return ( @@ -26,15 +29,15 @@ export const DialogSettings: Component = () => { "padding-bottom": "12px", }} > - Desktop + {language.t("settings.section.desktop")}
- General + {language.t("settings.tab.general")} - Shortcuts + {language.t("settings.tab.shortcuts")}
diff --git a/packages/app/src/components/font-picker.tsx b/packages/app/src/components/font-picker.tsx deleted file mode 100644 index 213ce097fe8..00000000000 --- a/packages/app/src/components/font-picker.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { createMemo, createSignal, onMount, Show } from "solid-js" -import { Button } from "@opencode-ai/ui/button" -import { Icon } from "@opencode-ai/ui/icon" -import { Tooltip } from "@opencode-ai/ui/tooltip" -import { Dialog } from "@opencode-ai/ui/dialog" -import { List } from "@opencode-ai/ui/list" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { FONTS, getFontById, type FontDefinition } from "@/fonts/font-definitions" -import { useLayout } from "@/context/layout" -import { applyFontWithLoad, ensureFontLoaded, applyFont } from "@/fonts/apply-font" - -function DialogSelectFont(props: { originalFont: string }) { - const layout = useLayout() - const dialog = useDialog() - const [previewFont, setPreviewFont] = createSignal(props.originalFont) - const currentFont = createMemo(() => getFontById(previewFont()) ?? FONTS[0]) - - async function handleSelect(font: FontDefinition | undefined) { - if (!font) return - - const loaded = await ensureFontLoaded(font) - if (!loaded) return - - layout.font.set(font.id) - applyFont(font.id) - dialog.close() - } - - async function handleActiveChange(font: FontDefinition | undefined) { - if (!font) return - - const loaded = await ensureFontLoaded(font) - if (!loaded) return - - setPreviewFont(font.id) - applyFont(font.id) - } - - return ( - - f.id} - items={() => [...FONTS]} - current={currentFont()} - filterKeys={["name", "family"]} - onSelect={handleSelect} - onMove={handleActiveChange} - > - {(font: FontDefinition) => ( -
- {font.name} -
- )} -
-
- ) -} - -export function FontPicker(props: { class?: string; mobile?: boolean }) { - const layout = useLayout() - const dialog = useDialog() - const currentFont = createMemo(() => getFontById(layout.font.current()) ?? FONTS[0]) - - onMount(() => applyFontWithLoad(currentFont())) - - function openDialog() { - const originalFont = currentFont().id - dialog.show( - () => , - () => applyFont(originalFont), - ) - } - - return ( - - - - } - > - - - ) -} diff --git a/packages/app/src/components/header.tsx b/packages/app/src/components/header.tsx index 55fd5ff5ca3..9e85ec01a40 100644 --- a/packages/app/src/components/header.tsx +++ b/packages/app/src/components/header.tsx @@ -16,8 +16,6 @@ import { A, useParams } from "@solidjs/router" import { createMemo, createResource, Show } from "solid-js" import { IconButton } from "@opencode-ai/ui/icon-button" import { iife } from "@opencode-ai/util/iife" -import { ThemePicker } from "@/components/theme-picker" -import { FontPicker } from "@/components/font-picker" export function Header(props: { navigateToProject: (directory: string) => void @@ -57,12 +55,7 @@ export function Header(props: {
0 && params.dir} - fallback={ - - } + fallback={
- {/* Theme and Font first - desktop only */} - {/* Review toggle - requires session */} + +type ModelInfo = { + id: string + name: string + provider: { + name: string + } + capabilities?: { + reasoning: boolean + input: InputMap + } + modalities?: { + input: Array + } + reasoning?: boolean + limit: { + context: number + } +} + +export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?: boolean }> = (props) => { + const language = useLanguage() + const sourceName = (model: ModelInfo) => { + const value = `${model.id} ${model.name}`.toLowerCase() + + if (/claude|anthropic/.test(value)) return language.t("model.provider.anthropic") + if (/gpt|o[1-4]|codex|openai/.test(value)) return language.t("model.provider.openai") + if (/gemini|palm|bard|google/.test(value)) return language.t("model.provider.google") + if (/grok|xai/.test(value)) return language.t("model.provider.xai") + if (/llama|meta/.test(value)) return language.t("model.provider.meta") + + return model.provider.name + } + const inputLabel = (value: string) => { + if (value === "text") return language.t("model.input.text") + if (value === "image") return language.t("model.input.image") + if (value === "audio") return language.t("model.input.audio") + if (value === "video") return language.t("model.input.video") + if (value === "pdf") return language.t("model.input.pdf") + return value + } + const title = () => { + const tags: Array = [] + if (props.latest) tags.push(language.t("model.tag.latest")) + if (props.free) tags.push(language.t("model.tag.free")) + const suffix = tags.length ? ` (${tags.join(", ")})` : "" + return `${sourceName(props.model)} ${props.model.name}${suffix}` + } + const inputs = () => { + if (props.model.capabilities) { + const input = props.model.capabilities.input + const order: Array = ["text", "image", "audio", "video", "pdf"] + const entries = order.filter((key) => input[key]).map((key) => inputLabel(key)) + return entries.length ? entries.join(", ") : undefined + } + const raw = props.model.modalities?.input + if (!raw) return + const entries = raw.map((value) => inputLabel(value)) + return entries.length ? entries.join(", ") : undefined + } + const reasoning = () => { + if (props.model.capabilities) + return props.model.capabilities.reasoning + ? language.t("model.tooltip.reasoning.allowed") + : language.t("model.tooltip.reasoning.none") + return props.model.reasoning + ? language.t("model.tooltip.reasoning.allowed") + : language.t("model.tooltip.reasoning.none") + } + const context = () => language.t("model.tooltip.context", { limit: props.model.limit.context.toLocaleString() }) + + return ( +
+
{title()}
+ + {(value) => ( +
+ {language.t("model.tooltip.allows", { inputs: value() })} +
+ )} +
+
{reasoning()}
+
{context()}
+
+ ) +} diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 09ccbbe0da3..af391163f53 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -49,6 +49,7 @@ import { Persist, persisted } from "@/utils/persist" import { Identifier } from "@/utils/id" import { SessionContextUsage } from "@/components/session-context-usage" import { usePermission } from "@/context/permission" +import { useLanguage } from "@/context/language" import { useGlobalSync } from "@/context/global-sync" import { usePlatform } from "@/context/platform" import { createOpencodeClient, type Message, type Part } from "@opencode-ai/sdk/v2/client" @@ -66,33 +67,33 @@ interface PromptInputProps { onNewSessionWorktreeReset?: () => void } -const PLACEHOLDERS = [ - "Fix a TODO in the codebase", - "What is the tech stack of this project?", - "Fix broken tests", - "Explain how authentication works", - "Find and fix security vulnerabilities", - "Add unit tests for the user service", - "Refactor this function to be more readable", - "What does this error mean?", - "Help me debug this issue", - "Generate API documentation", - "Optimize database queries", - "Add input validation", - "Create a new component for...", - "How do I deploy this project?", - "Review my code for best practices", - "Add error handling to this function", - "Explain this regex pattern", - "Convert this to TypeScript", - "Add logging throughout the codebase", - "What dependencies are outdated?", - "Help me write a migration script", - "Implement caching for this endpoint", - "Add pagination to this list", - "Create a CLI command for...", - "How do environment variables work here?", -] +const EXAMPLES = [ + "prompt.example.1", + "prompt.example.2", + "prompt.example.3", + "prompt.example.4", + "prompt.example.5", + "prompt.example.6", + "prompt.example.7", + "prompt.example.8", + "prompt.example.9", + "prompt.example.10", + "prompt.example.11", + "prompt.example.12", + "prompt.example.13", + "prompt.example.14", + "prompt.example.15", + "prompt.example.16", + "prompt.example.17", + "prompt.example.18", + "prompt.example.19", + "prompt.example.20", + "prompt.example.21", + "prompt.example.22", + "prompt.example.23", + "prompt.example.24", + "prompt.example.25", +] as const interface SlashCommand { id: string @@ -118,6 +119,7 @@ export const PromptInput: Component = (props) => { const providers = useProviders() const command = useCommand() const permission = usePermission() + const language = useLanguage() let editorRef!: HTMLDivElement let fileInputRef!: HTMLInputElement let scrollRef!: HTMLDivElement @@ -184,7 +186,7 @@ export const PromptInput: Component = (props) => { popover: null, historyIndex: -1, savedPrompt: null, - placeholder: Math.floor(Math.random() * PLACEHOLDERS.length), + placeholder: Math.floor(Math.random() * EXAMPLES.length), dragging: false, mode: "normal", applyingHistory: false, @@ -257,7 +259,7 @@ export const PromptInput: Component = (props) => { params.id if (params.id) return const interval = setInterval(() => { - setStore("placeholder", (prev) => (prev + 1) % PLACEHOLDERS.length) + setStore("placeholder", (prev) => (prev + 1) % EXAMPLES.length) }, 6500) onCleanup(() => clearInterval(interval)) }) @@ -312,8 +314,8 @@ export const PromptInput: Component = (props) => { if (fileItems.length > 0) { showToast({ - title: "Unsupported paste", - description: "Only images or PDFs can be pasted here.", + title: language.t("prompt.toast.pasteUnsupported.title"), + description: language.t("prompt.toast.pasteUnsupported.description"), }) return } @@ -559,6 +561,25 @@ export const PromptInput: Component = (props) => { }) }) + const selectPopoverActive = () => { + if (store.popover === "at") { + const items = atFlat() + if (items.length === 0) return + const active = atActive() + const item = items.find((entry) => atKey(entry) === active) ?? items[0] + handleAtSelect(item) + return + } + + if (store.popover === "slash") { + const items = slashFlat() + if (items.length === 0) return + const active = slashActive() + const item = items.find((entry) => entry.id === active) ?? items[0] + handleSlashSelect(item) + } + } + createEffect( on( () => prompt.current(), @@ -919,14 +940,24 @@ export const PromptInput: Component = (props) => { return } - if (store.popover && (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter")) { - if (store.popover === "at") { - atOnKeyDown(event) - } else { - slashOnKeyDown(event) + if (store.popover) { + if (event.key === "Tab") { + selectPopoverActive() + event.preventDefault() + return + } + if (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter") { + if (store.popover === "at") { + atOnKeyDown(event) + event.preventDefault() + return + } + if (store.popover === "slash") { + slashOnKeyDown(event) + } + event.preventDefault() + return } - event.preventDefault() - return } const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey @@ -1008,8 +1039,8 @@ export const PromptInput: Component = (props) => { const currentAgent = local.agent.current() if (!currentModel || !currentAgent) { showToast({ - title: "Select an agent and model", - description: "Choose an agent and model before sending a prompt.", + title: language.t("prompt.toast.modelAgentRequired.title"), + description: language.t("prompt.toast.modelAgentRequired.description"), }) return } @@ -1020,7 +1051,7 @@ export const PromptInput: Component = (props) => { if (data?.message) return data.message } if (err instanceof Error) return err.message - return "Request failed" + return language.t("common.requestFailed") } addToHistory(currentPrompt, mode) @@ -1041,7 +1072,7 @@ export const PromptInput: Component = (props) => { .then((x) => x.data) .catch((err) => { showToast({ - title: "Failed to create worktree", + title: language.t("prompt.toast.worktreeCreateFailed.title"), description: errorMessage(err), }) return undefined @@ -1049,8 +1080,8 @@ export const PromptInput: Component = (props) => { if (!createdWorktree?.directory) { showToast({ - title: "Failed to create worktree", - description: "Request failed", + title: language.t("prompt.toast.worktreeCreateFailed.title"), + description: language.t("common.requestFailed"), }) return } @@ -1081,7 +1112,7 @@ export const PromptInput: Component = (props) => { .then((x) => x.data ?? undefined) .catch((err) => { showToast({ - title: "Failed to create session", + title: language.t("prompt.toast.sessionCreateFailed.title"), description: errorMessage(err), }) return undefined @@ -1125,7 +1156,7 @@ export const PromptInput: Component = (props) => { }) .catch((err) => { showToast({ - title: "Failed to send shell command", + title: language.t("prompt.toast.shellSendFailed.title"), description: errorMessage(err), }) restoreInput() @@ -1157,7 +1188,7 @@ export const PromptInput: Component = (props) => { }) .catch((err) => { showToast({ - title: "Failed to send command", + title: language.t("prompt.toast.commandSendFailed.title"), description: errorMessage(err), }) restoreInput() @@ -1328,7 +1359,7 @@ export const PromptInput: Component = (props) => { }) .catch((err) => { showToast({ - title: "Failed to send prompt", + title: language.t("prompt.toast.promptSendFailed.title"), description: errorMessage(err), }) removeOptimisticMessage() @@ -1352,7 +1383,7 @@ export const PromptInput: Component = (props) => { 0} - fallback={
No matching results
} + fallback={
{language.t("prompt.popover.emptyResults")}
} > {(item) => ( @@ -1398,7 +1429,7 @@ export const PromptInput: Component = (props) => { 0} - fallback={
No matching commands
} + fallback={
{language.t("prompt.popover.emptyCommands")}
} > {(cmd) => ( @@ -1420,7 +1451,7 @@ export const PromptInput: Component = (props) => {
- custom + {language.t("prompt.slash.badge.custom")} @@ -1449,7 +1480,7 @@ export const PromptInput: Component = (props) => {
- Drop images or PDFs here + {language.t("prompt.dropzone.label")}
@@ -1462,7 +1493,7 @@ export const PromptInput: Component = (props) => {
{getDirectory(path())} {getFilename(path())} - active + {language.t("prompt.context.active")}
= (props) => { onClick={() => prompt.context.addActive()} > - Include active file + {language.t("prompt.context.includeActiveFile")} @@ -1574,8 +1605,8 @@ export const PromptInput: Component = (props) => {
{store.mode === "shell" - ? "Enter shell command..." - : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`} + ? language.t("prompt.placeholder.shell") + : language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })}
@@ -1585,12 +1616,16 @@ export const PromptInput: Component = (props) => {
- Shell - esc to exit + {language.t("prompt.mode.shell")} + {language.t("prompt.mode.shell.exit")}
- + setTitle(e.currentTarget.value)} + onBlur={save} + onKeyDown={keydown} + onMouseDown={(e) => e.stopPropagation()} + class="bg-transparent border-none outline-none text-sm min-w-0 flex-1" + /> +
+
+ + + + + + {language.t("common.rename")} + + + + {language.t("common.close")} + + + +
) diff --git a/packages/app/src/components/settings-agents.tsx b/packages/app/src/components/settings-agents.tsx index 892be152b32..e68f1e59c53 100644 --- a/packages/app/src/components/settings-agents.tsx +++ b/packages/app/src/components/settings-agents.tsx @@ -1,11 +1,14 @@ import { Component } from "solid-js" +import { useLanguage } from "@/context/language" export const SettingsAgents: Component = () => { + const language = useLanguage() + return (
-

Agents

-

Agent settings will be configurable here.

+

{language.t("settings.agents.title")}

+

{language.t("settings.agents.description")}

) diff --git a/packages/app/src/components/settings-commands.tsx b/packages/app/src/components/settings-commands.tsx index e98c0eeb032..cf796d0aa7a 100644 --- a/packages/app/src/components/settings-commands.tsx +++ b/packages/app/src/components/settings-commands.tsx @@ -1,11 +1,14 @@ import { Component } from "solid-js" +import { useLanguage } from "@/context/language" export const SettingsCommands: Component = () => { + const language = useLanguage() + return (
-

Commands

-

Command settings will be configurable here.

+

{language.t("settings.commands.title")}

+

{language.t("settings.commands.description")}

) diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index e8749cbdeaf..69d180f9d6e 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -2,36 +2,46 @@ import { Component, createMemo, type JSX } from "solid-js" import { Select } from "@opencode-ai/ui/select" import { Switch } from "@opencode-ai/ui/switch" import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme" +import { useLanguage } from "@/context/language" import { useSettings, monoFontFamily } from "@/context/settings" import { playSound, SOUND_OPTIONS } from "@/utils/sound" export const SettingsGeneral: Component = () => { const theme = useTheme() + const language = useLanguage() const settings = useSettings() const themeOptions = createMemo(() => Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })), ) - const colorSchemeOptions: { value: ColorScheme; label: string }[] = [ - { value: "system", label: "System setting" }, - { value: "light", label: "Light" }, - { value: "dark", label: "Dark" }, - ] + const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [ + { value: "system", label: language.t("theme.scheme.system") }, + { value: "light", label: language.t("theme.scheme.light") }, + { value: "dark", label: language.t("theme.scheme.dark") }, + ]) + + const languageOptions = createMemo(() => + language.locales.map((locale) => ({ + value: locale, + label: language.label(locale), + })), + ) const fontOptions = [ - { value: "ibm-plex-mono", label: "IBM Plex Mono" }, - { value: "cascadia-code", label: "Cascadia Code" }, - { value: "fira-code", label: "Fira Code" }, - { value: "hack", label: "Hack" }, - { value: "inconsolata", label: "Inconsolata" }, - { value: "intel-one-mono", label: "Intel One Mono" }, - { value: "jetbrains-mono", label: "JetBrains Mono" }, - { value: "meslo-lgs", label: "Meslo LGS" }, - { value: "roboto-mono", label: "Roboto Mono" }, - { value: "source-code-pro", label: "Source Code Pro" }, - { value: "ubuntu-mono", label: "Ubuntu Mono" }, - ] + { value: "ibm-plex-mono", label: "font.option.ibmPlexMono" }, + { value: "cascadia-code", label: "font.option.cascadiaCode" }, + { value: "fira-code", label: "font.option.firaCode" }, + { value: "hack", label: "font.option.hack" }, + { value: "inconsolata", label: "font.option.inconsolata" }, + { value: "intel-one-mono", label: "font.option.intelOneMono" }, + { value: "jetbrains-mono", label: "font.option.jetbrainsMono" }, + { value: "meslo-lgs", label: "font.option.mesloLgs" }, + { value: "roboto-mono", label: "font.option.robotoMono" }, + { value: "source-code-pro", label: "font.option.sourceCodePro" }, + { value: "ubuntu-mono", label: "font.option.ubuntuMono" }, + ] as const + const fontOptionsList = [...fontOptions] const soundOptions = [...SOUND_OPTIONS] @@ -45,20 +55,39 @@ export const SettingsGeneral: Component = () => { }} >
-

General

+

{language.t("settings.tab.general")}

{/* Appearance Section */}
-

Appearance

+

{language.t("settings.general.section.appearance")}

- + + o.value === theme.colorScheme())} + options={colorSchemeOptions()} + current={colorSchemeOptions().find((o) => o.value === theme.colorScheme())} value={(o) => o.value} label={(o) => o.label} onSelect={(option) => option && theme.setColorScheme(option.value)} @@ -74,12 +103,12 @@ export const SettingsGeneral: Component = () => { - Customise how OpenCode is themed.{" "} + {language.t("settings.general.row.theme.description")}{" "} - Learn more + {language.t("common.learnMore")} } @@ -104,19 +133,26 @@ export const SettingsGeneral: Component = () => { /> - +
@@ -124,12 +160,12 @@ export const SettingsGeneral: Component = () => { {/* System notifications Section */}
-

System notifications

+

{language.t("settings.general.section.notifications")}

{ /> - + settings.notifications.setPermissions(checked)} /> - + settings.notifications.setErrors(checked)} @@ -155,15 +197,18 @@ export const SettingsGeneral: Component = () => { {/* Sound effects Section */}
-

Sound effects

+

{language.t("settings.general.section.sounds")}

- + o.id === settings.sounds.permissions())} value={(o) => o.id} - label={(o) => o.label} + label={(o) => language.t(o.label)} onHighlight={(option) => { if (!option) return playSound(option.src) @@ -200,12 +248,15 @@ export const SettingsGeneral: Component = () => { /> - + o.value === actionFor(item.id))} + options={actions()} + current={actions().find((o) => o.value === actionFor(item.id))} value={(o) => o.value} label={(o) => o.label} onSelect={(option) => option && setPermission(item.id, option.value)} diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index cf90b6c1332..7b6ca193924 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -1,11 +1,14 @@ import { Component } from "solid-js" +import { useLanguage } from "@/context/language" export const SettingsProviders: Component = () => { + const language = useLanguage() + return (
-

Providers

-

Provider settings will be configurable here.

+

{language.t("settings.providers.title")}

+

{language.t("settings.providers.description")}

) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index f19366b8ab9..6bedcfae2ce 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -10,6 +10,7 @@ export interface TerminalProps extends ComponentProps<"div"> { pty: LocalPTY onSubmit?: () => void onCleanup?: (pty: LocalPTY) => void + onConnect?: () => void onConnectError?: (error: unknown) => void } @@ -40,7 +41,7 @@ export const Terminal = (props: TerminalProps) => { const settings = useSettings() const theme = useTheme() let container!: HTMLDivElement - const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"]) + const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnect", "onConnectError"]) let ws: WebSocket | undefined let term: Term | undefined let ghostty: Ghostty @@ -241,7 +242,7 @@ export const Terminal = (props: TerminalProps) => { // console.log("Scroll position:", ydisp) // }) socket.addEventListener("open", () => { - console.log("WebSocket connected") + local.onConnect?.() sdk.client.pty .update({ ptyID: local.pty.id, @@ -256,15 +257,22 @@ export const Terminal = (props: TerminalProps) => { t.write(event.data) }) socket.addEventListener("error", (error) => { + if (disposed) return console.error("WebSocket error:", error) - props.onConnectError?.(error) + local.onConnectError?.(error) }) - socket.addEventListener("close", () => { - console.log("WebSocket disconnected") + socket.addEventListener("close", (event) => { + if (disposed) return + // Normal closure (code 1000) means PTY process exited - server event handles cleanup + // For other codes (network issues, server restart), trigger error handler + if (event.code !== 1000) { + local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`)) + } }) }) onCleanup(() => { + disposed = true if (handleResize) { window.removeEventListener("resize", handleResize) } @@ -293,6 +301,7 @@ export const Terminal = (props: TerminalProps) => { ref={container} data-component="terminal" data-prevent-autofocus + tabIndex={-1} style={{ "background-color": terminalColors().background }} classList={{ ...(local.classList ?? {}), diff --git a/packages/app/src/components/theme-picker.tsx b/packages/app/src/components/theme-picker.tsx deleted file mode 100644 index 9fe2989a0bb..00000000000 --- a/packages/app/src/components/theme-picker.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { createMemo, createSignal, onMount, Show } from "solid-js" -import { Button } from "@opencode-ai/ui/button" -import { Icon } from "@opencode-ai/ui/icon" -import { Tooltip } from "@opencode-ai/ui/tooltip" -import { Dialog } from "@opencode-ai/ui/dialog" -import { List } from "@opencode-ai/ui/list" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { useLayout } from "@/context/layout" -import { THEMES, getThemeById, applyTheme, type Theme } from "@/theme/apply-theme" - -export function DialogSelectTheme(props: { originalTheme: string }) { - const layout = useLayout() - const dialog = useDialog() - const [previewTheme, setPreviewTheme] = createSignal(props.originalTheme) - const currentTheme = createMemo(() => getThemeById(previewTheme())) - - function handleSelect(theme: Theme | undefined) { - if (!theme) return - layout.theme.set(theme.id) - applyTheme(theme.id) - dialog.close() - } - - function handleActiveChange(theme: Theme | undefined) { - if (!theme) return - setPreviewTheme(theme.id) - applyTheme(theme.id) - } - - return ( - - t.id} - items={() => [...THEMES]} - current={currentTheme()} - filterKeys={["name", "id"]} - onSelect={handleSelect} - onMove={handleActiveChange} - > - {(theme: Theme) => ( -
- {theme.name} -
- )} -
-
- ) -} - -export function ThemePicker(props: { class?: string; mobile?: boolean }) { - const layout = useLayout() - const dialog = useDialog() - const currentTheme = createMemo(() => getThemeById(layout.theme.current())) - - onMount(() => applyTheme(currentTheme().id)) - - function openDialog() { - const originalTheme = currentTheme().id - dialog.show( - () => , - () => applyTheme(layout.theme.current()), - ) - } - - return ( - - - - } - > - - - ) -} diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 272f8514497..6e8fe34c5ee 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -1,19 +1,25 @@ import { createEffect, createMemo, Show } from "solid-js" import { IconButton } from "@opencode-ai/ui/icon-button" +import { Icon } from "@opencode-ai/ui/icon" +import { Button } from "@opencode-ai/ui/button" import { TooltipKeybind } from "@opencode-ai/ui/tooltip" import { useTheme } from "@opencode-ai/ui/theme" +import { AsciiMark } from "@opencode-ai/ui/logo" import { useLayout } from "@/context/layout" import { usePlatform } from "@/context/platform" import { useCommand } from "@/context/command" +import { useLanguage } from "@/context/language" export function Titlebar() { const layout = useLayout() const platform = usePlatform() const command = useCommand() + const language = useLanguage() const theme = useTheme() const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos") + const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows") const reserve = createMemo( () => platform.platform === "desktop" && (platform.os === "windows" || platform.os === "linux"), ) @@ -71,43 +77,81 @@ export function Titlebar() { } return ( -
+
+ +
+ +
+
- +
- +
- + aria-label={language.t("command.sidebar.toggle")} + aria-expanded={layout.sidebar.opened()} + > +
+ +
+
-
+
-
- -
+
+ +
diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index 681dcb23550..39bd11a1bb0 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -2,6 +2,7 @@ import { createEffect, createMemo, createSignal, onCleanup, onMount, type Access import { createStore } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" import { Persist, persisted } from "@/utils/persist" @@ -154,6 +155,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex init: () => { const dialog = useDialog() const settings = useSettings() + const language = useLanguage() const [registrations, setRegistrations] = createSignal[]>([]) const [suspendCount, setSuspendCount] = createSignal(0) @@ -213,7 +215,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex ...suggested.map((x) => ({ ...x, id: SUGGESTED_PREFIX + x.id, - category: "Suggested", + category: language.t("command.category.suggested"), })), ...resolved, ] diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx index 2cc0d62de76..d7630509a1b 100644 --- a/packages/app/src/context/file.tsx +++ b/packages/app/src/context/file.tsx @@ -7,6 +7,7 @@ import { useParams } from "@solidjs/router" import { getFilename } from "@opencode-ai/util/path" import { useSDK } from "./sdk" import { useSync } from "./sync" +import { useLanguage } from "@/context/language" import { Persist, persisted } from "@/utils/persist" export type FileSelection = { @@ -186,6 +187,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ const sdk = useSDK() const sync = useSync() const params = useParams() + const language = useLanguage() const directory = createMemo(() => sync.data.path.directory) @@ -193,7 +195,20 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ const root = directory() const prefix = root.endsWith("/") ? root : root + "/" - let path = stripQueryAndHash(stripFileProtocol(input)) + let path = input + + // Only strip protocol and decode if it's a file URI + if (path.startsWith("file://")) { + const raw = stripQueryAndHash(stripFileProtocol(path)) + try { + // Attempt to treat as a standard URI + path = decodeURIComponent(raw) + } catch { + // Fallback for legacy paths that might contain invalid URI sequences (e.g. "100%") + // In this case, we treat the path as raw, but still strip the protocol + path = raw + } + } if (path.startsWith(prefix)) { path = path.slice(prefix.length) @@ -216,7 +231,8 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ function tab(input: string) { const path = normalize(input) - return `file://${path}` + const encoded = path.split("/").map(encodeURIComponent).join("/") + return `file://${encoded}` } function pathFromTab(tabValue: string) { @@ -323,7 +339,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ ) showToast({ variant: "error", - title: "Failed to load file", + title: language.t("toast.file.loadFailed.title"), description: e.message, }) }) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 82fc49ea76c..8c22f1801cf 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -45,6 +45,7 @@ import { showToast } from "@opencode-ai/ui/toast" import { getFilename } from "@opencode-ai/util/path" import { isHostedEnvironment } from "@/utils/hosted" import { usePlatform } from "./platform" +import { useLanguage } from "@/context/language" import { Persist, persisted } from "@/utils/persist" type State = { @@ -103,6 +104,7 @@ type ChildOptions = { function createGlobalSync() { const globalSDK = useGlobalSDK() const platform = usePlatform() + const language = useLanguage() const owner = getOwner() if (!owner) throw new Error("GlobalSync must be created within owner") const vcsCache = new Map() @@ -247,7 +249,7 @@ function createGlobalSync() { .catch((err) => { console.error("Failed to load sessions", err) const project = getFilename(directory) - showToast({ title: `Failed to load sessions for ${project}`, description: err.message }) + showToast({ title: language.t("toast.session.listFailed.title", { project }), description: err.message }) }) sessionLoads.set(directory, promise) @@ -700,10 +702,7 @@ function createGlobalSync() { return } // For non-hosted environments, show the error page - setGlobalStore( - "error", - new Error(`Could not connect to server. Is there a server running at \`${globalSDK.url}\`?`), - ) + setGlobalStore("error", new Error(language.t("error.globalSync.connectFailed", { url: globalSDK.url }))) setGlobalStore("connectionState", "error") return } diff --git a/packages/app/src/context/language.tsx b/packages/app/src/context/language.tsx new file mode 100644 index 00000000000..f7c2e277b67 --- /dev/null +++ b/packages/app/src/context/language.tsx @@ -0,0 +1,118 @@ +import * as i18n from "@solid-primitives/i18n" +import { createEffect, createMemo } from "solid-js" +import { createStore } from "solid-js/store" +import { createSimpleContext } from "@opencode-ai/ui/context" +import { Persist, persisted } from "@/utils/persist" +import { dict as en } from "@/i18n/en" +import { dict as zh } from "@/i18n/zh" +import { dict as ko } from "@/i18n/ko" +import { dict as de } from "@/i18n/de" +import { dict as es } from "@/i18n/es" +import { dict as fr } from "@/i18n/fr" +import { dict as da } from "@/i18n/da" +import { dict as ja } from "@/i18n/ja" +import { dict as uiEn } from "@opencode-ai/ui/i18n/en" +import { dict as uiZh } from "@opencode-ai/ui/i18n/zh" +import { dict as uiKo } from "@opencode-ai/ui/i18n/ko" +import { dict as uiDe } from "@opencode-ai/ui/i18n/de" +import { dict as uiEs } from "@opencode-ai/ui/i18n/es" +import { dict as uiFr } from "@opencode-ai/ui/i18n/fr" +import { dict as uiDa } from "@opencode-ai/ui/i18n/da" +import { dict as uiJa } from "@opencode-ai/ui/i18n/ja" + +export type Locale = "en" | "zh" | "ko" | "de" | "es" | "fr" | "da" | "ja" + +type RawDictionary = typeof en & typeof uiEn +type Dictionary = i18n.Flatten + +const LOCALES: readonly Locale[] = ["en", "zh", "ko", "de", "es", "fr", "da", "ja"] + +function detectLocale(): Locale { + if (typeof navigator !== "object") return "en" + + const languages = navigator.languages?.length ? navigator.languages : [navigator.language] + for (const language of languages) { + if (!language) continue + if (language.toLowerCase().startsWith("zh")) return "zh" + if (language.toLowerCase().startsWith("ko")) return "ko" + if (language.toLowerCase().startsWith("de")) return "de" + if (language.toLowerCase().startsWith("es")) return "es" + if (language.toLowerCase().startsWith("fr")) return "fr" + if (language.toLowerCase().startsWith("da")) return "da" + if (language.toLowerCase().startsWith("ja")) return "ja" + } + + return "en" +} + +export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({ + name: "Language", + init: () => { + const [store, setStore, _, ready] = persisted( + Persist.global("language", ["language.v1"]), + createStore({ + locale: detectLocale() as Locale, + }), + ) + + const locale = createMemo(() => { + if (store.locale === "zh") return "zh" + if (store.locale === "ko") return "ko" + if (store.locale === "de") return "de" + if (store.locale === "es") return "es" + if (store.locale === "fr") return "fr" + if (store.locale === "da") return "da" + if (store.locale === "ja") return "ja" + return "en" + }) + + createEffect(() => { + const current = locale() + if (store.locale === current) return + setStore("locale", current) + }) + + const base = i18n.flatten({ ...en, ...uiEn }) + const dict = createMemo(() => { + if (locale() === "en") return base + if (locale() === "zh") return { ...base, ...i18n.flatten({ ...zh, ...uiZh }) } + if (locale() === "de") return { ...base, ...i18n.flatten({ ...de, ...uiDe }) } + if (locale() === "es") return { ...base, ...i18n.flatten({ ...es, ...uiEs }) } + if (locale() === "fr") return { ...base, ...i18n.flatten({ ...fr, ...uiFr }) } + if (locale() === "da") return { ...base, ...i18n.flatten({ ...da, ...uiDa }) } + if (locale() === "ja") return { ...base, ...i18n.flatten({ ...ja, ...uiJa }) } + return { ...base, ...i18n.flatten({ ...ko, ...uiKo }) } + }) + + const t = i18n.translator(dict, i18n.resolveTemplate) + + const labelKey: Record = { + en: "language.en", + zh: "language.zh", + ko: "language.ko", + de: "language.de", + es: "language.es", + fr: "language.fr", + da: "language.da", + ja: "language.ja", + } + + const label = (value: Locale) => t(labelKey[value]) + + createEffect(() => { + if (typeof document !== "object") return + document.documentElement.lang = locale() + }) + + return { + ready, + locale, + locales: LOCALES, + label, + t, + setLocale(next: Locale) { + setStore("locale", next) + }, + } + }, +}) diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 2521efc8df1..561d3a09ab1 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -314,7 +314,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( used.add(color) setColors(project.worktree, color) if (!project.id) continue - void globalSdk.client.project.update({ projectID: project.id, icon: { color } }) + void globalSdk.client.project.update({ projectID: project.id, directory: project.worktree, icon: { color } }) } }) diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index f3441ded967..9b66401c927 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -10,6 +10,7 @@ import { useProviders } from "@/hooks/use-providers" import { DateTime } from "luxon" import { Persist, persisted } from "@/utils/persist" import { showToast } from "@opencode-ai/ui/toast" +import { useLanguage } from "@/context/language" export type LocalFile = FileNode & Partial<{ @@ -42,6 +43,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const sdk = useSDK() const sync = useSync() const providers = useProviders() + const language = useLanguage() function isModelValid(model: ModelKey) { const provider = providers.all().find((x) => x.id === model.providerID) @@ -409,7 +411,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ .catch((e) => { showToast({ variant: "error", - title: "Failed to load file", + title: language.t("toast.file.loadFailed.title"), description: e.message, }) }) diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx index 8b108851949..58e7fbf83aa 100644 --- a/packages/app/src/context/notification.tsx +++ b/packages/app/src/context/notification.tsx @@ -1,12 +1,14 @@ import { createStore } from "solid-js/store" import { createEffect, onCleanup } from "solid-js" +import { useParams } from "@solidjs/router" import { createSimpleContext } from "@opencode-ai/ui/context" import { useGlobalSDK } from "./global-sdk" import { useGlobalSync } from "./global-sync" import { usePlatform } from "@/context/platform" +import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" import { Binary } from "@opencode-ai/util/binary" -import { base64Encode } from "@opencode-ai/util/encode" +import { base64Decode, base64Encode } from "@opencode-ai/util/encode" import { EventSessionError } from "@opencode-ai/sdk/v2" import { Persist, persisted } from "@/utils/persist" import { playSound, soundSrc } from "@/utils/sound" @@ -43,10 +45,12 @@ function pruneNotifications(list: Notification[]) { export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({ name: "Notification", init: () => { + const params = useParams() const globalSDK = useGlobalSDK() const globalSync = useGlobalSync() const platform = usePlatform() const settings = useSettings() + const language = useLanguage() const [store, setStore, _, ready] = persisted( Persist.global("notification", ["notification.v1"]), @@ -71,10 +75,15 @@ export const { use: useNotification, provider: NotificationProvider } = createSi const unsub = globalSDK.event.listen((e) => { const directory = e.name const event = e.details - const base = { - directory, - time: Date.now(), - viewed: false, + const time = Date.now() + const activeDirectory = params.dir ? base64Decode(params.dir) : undefined + const activeSession = params.id + const viewed = (sessionID?: string) => { + if (!activeDirectory) return false + if (!activeSession) return false + if (!sessionID) return false + if (directory !== activeDirectory) return false + return sessionID === activeSession } switch (event.type) { case "session.idle": { @@ -87,16 +96,21 @@ export const { use: useNotification, provider: NotificationProvider } = createSi playSound(soundSrc(settings.sounds.agent())) append({ - ...base, + directory, + time, + viewed: viewed(sessionID), type: "turn-complete", session: sessionID, }) const href = `/${base64Encode(directory)}/session/${sessionID}` if (settings.notifications.agent()) { - void platform.notify("Response ready", session?.title ?? sessionID, href) + void platform.notify( + language.t("notification.session.responseReady.title"), + session?.title ?? sessionID, + href, + ) } - break } case "session.error": { @@ -110,18 +124,20 @@ export const { use: useNotification, provider: NotificationProvider } = createSi const error = "error" in event.properties ? event.properties.error : undefined append({ - ...base, + directory, + time, + viewed: viewed(sessionID), type: "error", session: sessionID ?? "global", error, }) - - const description = session?.title ?? (typeof error === "string" ? error : "An error occurred") + const description = + session?.title ?? + (typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription")) const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}` if (settings.notifications.errors()) { - void platform.notify("Session error", description, href) + void platform.notify(language.t("notification.session.error.title"), description, href) } - break } } diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index 5732114b46b..147c4f8f7ea 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -1,6 +1,6 @@ import { createStore, produce } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" -import { batch, createMemo, createRoot, onCleanup } from "solid-js" +import { batch, createEffect, createMemo, createRoot, onCleanup } from "solid-js" import { useParams } from "@solidjs/router" import { useSDK } from "./sdk" import { Persist, persisted } from "@/utils/persist" @@ -13,6 +13,7 @@ export type LocalPTY = { cols?: number buffer?: string scrollY?: number + error?: boolean } const WORKSPACE_KEY = "__workspace__" @@ -28,6 +29,14 @@ type TerminalCacheEntry = { function createTerminalSession(sdk: ReturnType, dir: string, session?: string) { const legacy = session ? [`${dir}/terminal/${session}.v1`, `${dir}/terminal.v1`] : [`${dir}/terminal.v1`] + const numberFromTitle = (title: string) => { + const match = title.match(/^Terminal (\d+)$/) + if (!match) return + const value = Number(match[1]) + if (!Number.isFinite(value) || value <= 0) return + return value + } + const [store, setStore, _, ready] = persisted( Persist.workspace(dir, "terminal", legacy), createStore<{ @@ -54,24 +63,36 @@ function createTerminalSession(sdk: ReturnType, dir: string, sess }) onCleanup(unsub) + const meta = { migrated: false } + + createEffect(() => { + if (!ready()) return + if (meta.migrated) return + meta.migrated = true + + setStore("all", (all) => { + const next = all.map((pty) => { + const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined + if (direct !== undefined) return pty + const parsed = numberFromTitle(pty.title) + if (parsed === undefined) return pty + return { ...pty, titleNumber: parsed } + }) + if (next.every((pty, index) => pty === all[index])) return all + return next + }) + }) + return { ready, all: createMemo(() => Object.values(store.all)), active: createMemo(() => store.active), new() { - const parse = (title: string) => { - const match = title.match(/^Terminal (\d+)$/) - if (!match) return - const value = Number(match[1]) - if (!Number.isFinite(value) || value <= 0) return - return value - } - const existingTitleNumbers = new Set( store.all.flatMap((pty) => { const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined if (direct !== undefined) return [direct] - const parsed = parse(pty.title) + const parsed = numberFromTitle(pty.title) if (parsed === undefined) return [] return [parsed] }), @@ -87,14 +108,15 @@ function createTerminalSession(sdk: ReturnType, dir: string, sess .then((pty) => { const id = pty.data?.id if (!id) return - setStore("all", [ - ...store.all, - { - id, - title: pty.data?.title ?? "Terminal", - titleNumber: nextNumber, - }, - ]) + const newTerminal = { + id, + title: pty.data?.title ?? "Terminal", + titleNumber: nextNumber, + } + setStore("all", (all) => { + const newAll = [...all, newTerminal] + return newAll + }) setStore("active", id) }) .catch((e) => { @@ -102,7 +124,10 @@ function createTerminalSession(sdk: ReturnType, dir: string, sess }) }, update(pty: Partial & { id: string }) { - setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x))) + const index = store.all.findIndex((x) => x.id === pty.id) + if (index !== -1) { + setStore("all", index, (existing) => ({ ...existing, ...pty })) + } sdk.client.pty .update({ ptyID: pty.id, @@ -137,18 +162,29 @@ function createTerminalSession(sdk: ReturnType, dir: string, sess open(id: string) { setStore("active", id) }, + next() { + const index = store.all.findIndex((x) => x.id === store.active) + if (index === -1) return + const nextIndex = (index + 1) % store.all.length + setStore("active", store.all[nextIndex]?.id) + }, + previous() { + const index = store.all.findIndex((x) => x.id === store.active) + if (index === -1) return + const prevIndex = index === 0 ? store.all.length - 1 : index - 1 + setStore("active", store.all[prevIndex]?.id) + }, async close(id: string) { batch(() => { - setStore( - "all", - store.all.filter((x) => x.id !== id), - ) + const filtered = store.all.filter((x) => x.id !== id) if (store.active === id) { const index = store.all.findIndex((f) => f.id === id) - const previous = store.all[Math.max(0, index - 1)] - setStore("active", previous?.id) + const next = index > 0 ? index - 1 : 0 + setStore("active", filtered[next]?.id) } + setStore("all", filtered) }) + await sdk.client.pty.remove({ ptyID: id }).catch((e) => { console.error("Failed to close terminal", e) }) @@ -224,6 +260,8 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont open: (id: string) => workspace().open(id), close: (id: string) => workspace().close(id), move: (id: string, to: number) => workspace().move(id, to), + next: () => workspace().next(), + previous: () => workspace().previous(), } }, }) diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index 8c4662926ad..df8547636b4 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -2,13 +2,25 @@ import { render } from "solid-js/web" import { AppBaseProviders, AppInterface } from "@/app" import { Platform, PlatformProvider } from "@/context/platform" +import { dict as en } from "@/i18n/en" +import { dict as zh } from "@/i18n/zh" import pkg from "../package.json" const root = document.getElementById("root") if (import.meta.env.DEV && !(root instanceof HTMLElement)) { - throw new Error( - "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?", - ) + const locale = (() => { + if (typeof navigator !== "object") return "en" as const + const languages = navigator.languages?.length ? navigator.languages : [navigator.language] + for (const language of languages) { + if (!language) continue + if (language.toLowerCase().startsWith("zh")) return "zh" as const + } + return "en" as const + })() + + const key = "error.dev.rootNotFound" as const + const message = locale === "zh" ? (zh[key] ?? en[key]) : en[key] + throw new Error(message) } const platform: Platform = { @@ -37,7 +49,7 @@ const platform: Platform = { .then(() => { const notification = new Notification(title, { body: description ?? "", - icon: "https://opencode.ai/favicon-96x96-v2.png", + icon: "https://opencode.ai/favicon-96x96-v3.png", }) notification.onclick = () => { window.focus() diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts new file mode 100644 index 00000000000..7d381a8209f --- /dev/null +++ b/packages/app/src/i18n/da.ts @@ -0,0 +1,557 @@ +export const dict = { + "command.category.suggested": "Foreslået", + "command.category.view": "Vis", + "command.category.project": "Projekt", + "command.category.provider": "Udbyder", + "command.category.server": "Server", + "command.category.session": "Session", + "command.category.theme": "Tema", + "command.category.language": "Sprog", + "command.category.file": "Fil", + "command.category.terminal": "Terminal", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Tilladelser", + "command.category.workspace": "Arbejdsområde", + + "theme.scheme.system": "System", + "theme.scheme.light": "Lys", + "theme.scheme.dark": "Mørk", + + "command.sidebar.toggle": "Skift sidebjælke", + "command.project.open": "Åbn projekt", + "command.provider.connect": "Tilslut udbyder", + "command.server.switch": "Skift server", + "command.session.previous": "Forrige session", + "command.session.next": "Næste session", + "command.session.archive": "Arkivér session", + + "command.palette": "Kommandopalette", + + "command.theme.cycle": "Skift tema", + "command.theme.set": "Brug tema: {{theme}}", + "command.theme.scheme.cycle": "Skift farveskema", + "command.theme.scheme.set": "Brug farveskema: {{scheme}}", + + "command.language.cycle": "Skift sprog", + "command.language.set": "Brug sprog: {{language}}", + + "command.session.new": "Ny session", + "command.file.open": "Åbn fil", + "command.file.open.description": "Søg i filer og kommandoer", + "command.terminal.toggle": "Skift terminal", + "command.review.toggle": "Skift gennemgang", + "command.terminal.new": "Ny terminal", + "command.terminal.new.description": "Opret en ny terminalfane", + "command.steps.toggle": "Skift trin", + "command.steps.toggle.description": "Vis eller skjul trin for den aktuelle besked", + "command.message.previous": "Forrige besked", + "command.message.previous.description": "Gå til den forrige brugerbesked", + "command.message.next": "Næste besked", + "command.message.next.description": "Gå til den næste brugerbesked", + "command.model.choose": "Vælg model", + "command.model.choose.description": "Vælg en anden model", + "command.mcp.toggle": "Skift MCP'er", + "command.mcp.toggle.description": "Skift MCP'er", + "command.agent.cycle": "Skift agent", + "command.agent.cycle.description": "Skift til næste agent", + "command.agent.cycle.reverse": "Skift agent baglæns", + "command.agent.cycle.reverse.description": "Skift til forrige agent", + "command.model.variant.cycle": "Skift tænkeindsats", + "command.model.variant.cycle.description": "Skift til næste indsatsniveau", + "command.permissions.autoaccept.enable": "Accepter ændringer automatisk", + "command.permissions.autoaccept.disable": "Stop automatisk accept af ændringer", + "command.session.undo": "Fortryd", + "command.session.undo.description": "Fortryd den sidste besked", + "command.session.redo": "Omgør", + "command.session.redo.description": "Omgør den sidste fortrudte besked", + "command.session.compact": "Komprimér session", + "command.session.compact.description": "Opsummer sessionen for at reducere kontekststørrelsen", + "command.session.fork": "Forgren fra besked", + "command.session.fork.description": "Opret en ny session fra en tidligere besked", + "command.session.share": "Del session", + "command.session.share.description": "Del denne session og kopier URL'en til udklipsholderen", + "command.session.unshare": "Stop deling af session", + "command.session.unshare.description": "Stop med at dele denne session", + + "palette.search.placeholder": "Søg i filer og kommandoer", + "palette.empty": "Ingen resultater fundet", + "palette.group.commands": "Kommandoer", + "palette.group.files": "Filer", + + "dialog.provider.search.placeholder": "Søg udbydere", + "dialog.provider.empty": "Ingen udbydere fundet", + "dialog.provider.group.popular": "Populære", + "dialog.provider.group.other": "Andre", + "dialog.provider.tag.recommended": "Anbefalet", + "dialog.provider.anthropic.note": "Forbind med Claude Pro/Max eller API-nøgle", + + "dialog.model.select.title": "Vælg model", + "dialog.model.search.placeholder": "Søg modeller", + "dialog.model.empty": "Ingen modeller fundet", + "dialog.model.manage": "Administrer modeller", + "dialog.model.manage.description": "Tilpas hvilke modeller der vises i modelvælgeren.", + + "dialog.model.unpaid.freeModels.title": "Gratis modeller leveret af OpenCode", + "dialog.model.unpaid.addMore.title": "Tilføj flere modeller fra populære udbydere", + + "dialog.provider.viewAll": "Vis alle udbydere", + + "provider.connect.title": "Forbind {{provider}}", + "provider.connect.title.anthropicProMax": "Log ind med Claude Pro/Max", + "provider.connect.selectMethod": "Vælg loginmetode for {{provider}}.", + "provider.connect.method.apiKey": "API-nøgle", + "provider.connect.status.inProgress": "Godkendelse i gang...", + "provider.connect.status.waiting": "Venter på godkendelse...", + "provider.connect.status.failed": "Godkendelse mislykkedes: {{error}}", + "provider.connect.apiKey.description": + "Indtast din {{provider}} API-nøgle for at forbinde din konto og bruge {{provider}} modeller i OpenCode.", + "provider.connect.apiKey.label": "{{provider}} API-nøgle", + "provider.connect.apiKey.placeholder": "API-nøgle", + "provider.connect.apiKey.required": "API-nøgle er påkrævet", + "provider.connect.opencodeZen.line1": + "OpenCode Zen giver dig adgang til et udvalg af pålidelige optimerede modeller til kodningsagenter.", + "provider.connect.opencodeZen.line2": + "Med en enkelt API-nøgle får du adgang til modeller som Claude, GPT, Gemini, GLM og flere.", + "provider.connect.opencodeZen.visit.prefix": "Besøg ", + "provider.connect.opencodeZen.visit.suffix": " for at hente din API-nøgle.", + "provider.connect.oauth.code.visit.prefix": "Besøg ", + "provider.connect.oauth.code.visit.link": "dette link", + "provider.connect.oauth.code.visit.suffix": + " for at hente din godkendelseskode for at forbinde din konto og bruge {{provider}} modeller i OpenCode.", + "provider.connect.oauth.code.label": "{{method}} godkendelseskode", + "provider.connect.oauth.code.placeholder": "Godkendelseskode", + "provider.connect.oauth.code.required": "Godkendelseskode er påkrævet", + "provider.connect.oauth.code.invalid": "Ugyldig godkendelseskode", + "provider.connect.oauth.auto.visit.prefix": "Besøg ", + "provider.connect.oauth.auto.visit.link": "dette link", + "provider.connect.oauth.auto.visit.suffix": + " og indtast koden nedenfor for at forbinde din konto og bruge {{provider}} modeller i OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Bekræftelseskode", + "provider.connect.toast.connected.title": "{{provider}} forbundet", + "provider.connect.toast.connected.description": "{{provider}} modeller er nu tilgængelige.", + + "model.tag.free": "Gratis", + "model.tag.latest": "Nyeste", + + "common.search.placeholder": "Søg", + "common.loading": "Indlæser", + "common.cancel": "Annuller", + "common.submit": "Indsend", + "common.save": "Gem", + "common.saving": "Gemmer...", + "common.default": "Standard", + "common.attachment": "vedhæftning", + + "prompt.placeholder.shell": "Indtast shell-kommando...", + "prompt.placeholder.normal": 'Spørg om hvad som helst... "{{example}}"', + "prompt.mode.shell": "Shell", + "prompt.mode.shell.exit": "esc for at afslutte", + + "prompt.example.1": "Ret en TODO i koden", + "prompt.example.2": "Hvad er teknologistakken for dette projekt?", + "prompt.example.3": "Ret ødelagte tests", + "prompt.example.4": "Forklar hvordan godkendelse fungerer", + "prompt.example.5": "Find og ret sikkerhedshuller", + "prompt.example.6": "Tilføj enhedstests for brugerservice", + "prompt.example.7": "Refaktorer denne funktion så den er mere læsbar", + "prompt.example.8": "Hvad betyder denne fejl?", + "prompt.example.9": "Hjælp mig med at debugge dette problem", + "prompt.example.10": "Generer API-dokumentation", + "prompt.example.11": "Optimer databaseforespørgsler", + "prompt.example.12": "Tilføj validering af input", + "prompt.example.13": "Opret en ny komponent til...", + "prompt.example.14": "Hvordan deployerer jeg dette projekt?", + "prompt.example.15": "Gennemgå min kode for bedste praksis", + "prompt.example.16": "Tilføj fejlhåndtering til denne funktion", + "prompt.example.17": "Forklar dette regex-mønster", + "prompt.example.18": "Konverter dette til TypeScript", + "prompt.example.19": "Tilføj logning i hele koden", + "prompt.example.20": "Hvilke afhængigheder er forældede?", + "prompt.example.21": "Hjælp mig med at skrive et migreringsscript", + "prompt.example.22": "Implementer caching for dette endpoint", + "prompt.example.23": "Tilføj sideinddeling til denne liste", + "prompt.example.24": "Opret en CLI-kommando til...", + "prompt.example.25": "Hvordan fungerer miljøvariabler her?", + + "prompt.popover.emptyResults": "Ingen matchende resultater", + "prompt.popover.emptyCommands": "Ingen matchende kommandoer", + "prompt.dropzone.label": "Slip billeder eller PDF'er her", + "prompt.slash.badge.custom": "brugerdefineret", + "prompt.context.active": "aktiv", + "prompt.context.includeActiveFile": "Inkluder aktiv fil", + "prompt.action.attachFile": "Vedhæft fil", + "prompt.action.send": "Send", + "prompt.action.stop": "Stop", + + "prompt.toast.pasteUnsupported.title": "Ikke understøttet indsæt", + "prompt.toast.pasteUnsupported.description": "Kun billeder eller PDF'er kan indsættes her.", + "prompt.toast.modelAgentRequired.title": "Vælg en agent og model", + "prompt.toast.modelAgentRequired.description": "Vælg en agent og model før du sender en forespørgsel.", + "prompt.toast.worktreeCreateFailed.title": "Kunne ikke oprette worktree", + "prompt.toast.sessionCreateFailed.title": "Kunne ikke oprette session", + "prompt.toast.shellSendFailed.title": "Kunne ikke sende shell-kommando", + "prompt.toast.commandSendFailed.title": "Kunne ikke sende kommando", + "prompt.toast.promptSendFailed.title": "Kunne ikke sende forespørgsel", + + "dialog.mcp.title": "MCP'er", + "dialog.mcp.description": "{{enabled}} af {{total}} aktiveret", + "dialog.mcp.empty": "Ingen MCP'er konfigureret", + + "mcp.status.connected": "forbundet", + "mcp.status.failed": "mislykkedes", + "mcp.status.needs_auth": "kræver godkendelse", + "mcp.status.disabled": "deaktiveret", + + "dialog.fork.empty": "Ingen beskeder at forgrene fra", + + "dialog.directory.search.placeholder": "Søg mapper", + "dialog.directory.empty": "Ingen mapper fundet", + + "dialog.server.title": "Servere", + "dialog.server.description": "Skift hvilken OpenCode-server denne app forbinder til.", + "dialog.server.search.placeholder": "Søg servere", + "dialog.server.empty": "Ingen servere endnu", + "dialog.server.add.title": "Tilføj en server", + "dialog.server.add.url": "Server URL", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Kunne ikke forbinde til server", + "dialog.server.add.checking": "Tjekker...", + "dialog.server.add.button": "Tilføj", + "dialog.server.default.title": "Standardserver", + "dialog.server.default.description": + "Forbind til denne server ved start af app i stedet for at starte en lokal server. Kræver genstart.", + "dialog.server.default.none": "Ingen server valgt", + "dialog.server.default.set": "Sæt nuværende server som standard", + "dialog.server.default.clear": "Ryd", + + "dialog.project.edit.title": "Rediger projekt", + "dialog.project.edit.name": "Navn", + "dialog.project.edit.icon": "Ikon", + "dialog.project.edit.icon.alt": "Projektikon", + "dialog.project.edit.icon.hint": "Klik eller træk et billede", + "dialog.project.edit.icon.recommended": "Anbefalet: 128x128px", + "dialog.project.edit.color": "Farve", + + "context.breakdown.title": "Kontekstfordeling", + "context.breakdown.note": + 'Omtrentlig fordeling af input-tokens. "Andre" inkluderer værktøjsdefinitioner og overhead.', + "context.breakdown.system": "System", + "context.breakdown.user": "Bruger", + "context.breakdown.assistant": "Assistent", + "context.breakdown.tool": "Værktøjskald", + "context.breakdown.other": "Andre", + + "context.systemPrompt.title": "Systemprompt", + "context.rawMessages.title": "Rå beskeder", + + "context.stats.session": "Session", + "context.stats.messages": "Beskeder", + "context.stats.provider": "Udbyder", + "context.stats.model": "Model", + "context.stats.limit": "Kontekstgrænse", + "context.stats.totalTokens": "Total Tokens", + "context.stats.usage": "Forbrug", + "context.stats.inputTokens": "Input Tokens", + "context.stats.outputTokens": "Output Tokens", + "context.stats.reasoningTokens": "Tænke Tokens", + "context.stats.cacheTokens": "Cache Tokens (læs/skriv)", + "context.stats.userMessages": "Brugerbeskeder", + "context.stats.assistantMessages": "Assistentbeskeder", + "context.stats.totalCost": "Samlede omkostninger", + "context.stats.sessionCreated": "Session oprettet", + "context.stats.lastActivity": "Seneste aktivitet", + + "context.usage.tokens": "Tokens", + "context.usage.usage": "Forbrug", + "context.usage.cost": "Omkostning", + "context.usage.clickToView": "Klik for at se kontekst", + + "language.en": "Engelsk", + "language.zh": "Kinesisk", + "language.ko": "Koreansk", + "language.de": "Tysk", + "language.es": "Spansk", + "language.fr": "Fransk", + "language.ja": "Japansk", + "language.da": "Dansk", + + "toast.language.title": "Sprog", + "toast.language.description": "Skiftede til {{language}}", + + "toast.theme.title": "Tema skiftet", + "toast.scheme.title": "Farveskema", + + "toast.permissions.autoaccept.on.title": "Accepterer ændringer automatisk", + "toast.permissions.autoaccept.on.description": "Redigerings- og skrivetilladelser vil automatisk blive godkendt", + "toast.permissions.autoaccept.off.title": "Stoppede automatisk accept af ændringer", + "toast.permissions.autoaccept.off.description": "Redigerings- og skrivetilladelser vil kræve godkendelse", + + "toast.model.none.title": "Ingen model valgt", + "toast.model.none.description": "Forbind en udbyder for at opsummere denne session", + + "toast.file.loadFailed.title": "Kunne ikke indlæse fil", + + "toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til udklipsholder", + "toast.session.share.success.title": "Session delt", + "toast.session.share.success.description": "Delings-URL kopieret til udklipsholder!", + "toast.session.share.failed.title": "Kunne ikke dele session", + "toast.session.share.failed.description": "Der opstod en fejl under deling af sessionen", + + "toast.session.unshare.success.title": "Deling af session stoppet", + "toast.session.unshare.success.description": "Deling af session blev stoppet!", + "toast.session.unshare.failed.title": "Kunne ikke stoppe deling af session", + "toast.session.unshare.failed.description": "Der opstod en fejl under stop af sessionsdeling", + + "toast.session.listFailed.title": "Kunne ikke indlæse sessioner for {{project}}", + + "toast.update.title": "Opdatering tilgængelig", + "toast.update.description": "En ny version af OpenCode ({{version}}) er nu tilgængelig til installation.", + "toast.update.action.installRestart": "Installer og genstart", + "toast.update.action.notYet": "Ikke endnu", + + "error.page.title": "Noget gik galt", + "error.page.description": "Der opstod en fejl under indlæsning af applikationen.", + "error.page.details.label": "Fejldetaljer", + "error.page.action.restart": "Genstart", + "error.page.action.checking": "Tjekker...", + "error.page.action.checkUpdates": "Tjek for opdateringer", + "error.page.action.updateTo": "Opdater til {{version}}", + "error.page.report.prefix": "Rapporter venligst denne fejl til OpenCode-teamet", + "error.page.report.discord": "på Discord", + "error.page.version": "Version: {{version}}", + + "error.dev.rootNotFound": + "Rodelement ikke fundet. Har du glemt at tilføje det til din index.html? Eller måske er id-attributten stavet forkert?", + + "error.globalSync.connectFailed": "Kunne ikke forbinde til server. Kører der en server på `{{url}}`?", + + "error.chain.unknown": "Ukendt fejl", + "error.chain.causedBy": "Forårsaget af:", + "error.chain.apiError": "API-fejl", + "error.chain.status": "Status: {{status}}", + "error.chain.retryable": "Kan forsøges igen: {{retryable}}", + "error.chain.responseBody": "Svarindhold:\n{{body}}", + "error.chain.didYouMean": "Mente du: {{suggestions}}", + "error.chain.modelNotFound": "Model ikke fundet: {{provider}}/{{model}}", + "error.chain.checkConfig": "Tjek dine konfigurations (opencode.json) udbyder/modelnavne", + "error.chain.mcpFailed": 'MCP-server "{{name}}" fejlede. Bemærk, OpenCode understøtter ikke MCP-godkendelse endnu.', + "error.chain.providerAuthFailed": "Udbydergodkendelse mislykkedes ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Kunne ikke initialisere udbyder "{{provider}}". Tjek legitimationsoplysninger og konfiguration.', + "error.chain.configJsonInvalid": "Konfigurationsfil på {{path}} er ikke gyldig JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Konfigurationsfil på {{path}} er ikke gyldig JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Mappe "{{dir}}" i {{path}} er ikke gyldig. Omdøb mappen til "{{suggestion}}" eller fjern den. Dette er en almindelig slåfejl.', + "error.chain.configFrontmatterError": "Kunne ikke parse frontmatter i {{path}}:\n{{message}}", + "error.chain.configInvalid": "Konfigurationsfil på {{path}} er ugyldig", + "error.chain.configInvalidWithMessage": "Konfigurationsfil på {{path}} er ugyldig: {{message}}", + + "notification.permission.title": "Tilladelse påkrævet", + "notification.permission.description": "{{sessionTitle}} i {{projectName}} kræver tilladelse", + "notification.question.title": "Spørgsmål", + "notification.question.description": "{{sessionTitle}} i {{projectName}} har et spørgsmål", + "notification.action.goToSession": "Gå til session", + + "notification.session.responseReady.title": "Svar klar", + "notification.session.error.title": "Sessionsfejl", + "notification.session.error.fallbackDescription": "Der opstod en fejl", + + "home.recentProjects": "Seneste projekter", + "home.empty.title": "Ingen seneste projekter", + "home.empty.description": "Kom i gang ved at åbne et lokalt projekt", + + "session.tab.session": "Session", + "session.tab.review": "Gennemgang", + "session.tab.context": "Kontekst", + "session.review.filesChanged": "{{count}} Filer ændret", + "session.review.loadingChanges": "Indlæser ændringer...", + "session.review.empty": "Ingen ændringer i denne session endnu", + "session.messages.renderEarlier": "Vis tidligere beskeder", + "session.messages.loadingEarlier": "Indlæser tidligere beskeder...", + "session.messages.loadEarlier": "Indlæs tidligere beskeder", + "session.messages.loading": "Indlæser beskeder...", + + "session.context.addToContext": "Tilføj {{selection}} til kontekst", + + "session.new.worktree.main": "Hovedgren", + "session.new.worktree.mainWithBranch": "Hovedgren ({{branch}})", + "session.new.worktree.create": "Opret nyt worktree", + "session.new.lastModified": "Sidst ændret", + + "session.header.search.placeholder": "Søg {{project}}", + + "session.share.popover.title": "Udgiv på nettet", + "session.share.popover.description.shared": + "Denne session er offentlig på nettet. Den er tilgængelig for alle med linket.", + "session.share.popover.description.unshared": + "Del session offentligt på nettet. Den vil være tilgængelig for alle med linket.", + "session.share.action.share": "Del", + "session.share.action.publish": "Udgiv", + "session.share.action.publishing": "Udgiver...", + "session.share.action.unpublish": "Afpublicer", + "session.share.action.unpublishing": "Afpublicerer...", + "session.share.action.view": "Vis", + "session.share.copy.copied": "Kopieret", + "session.share.copy.copyLink": "Kopier link", + + "lsp.tooltip.none": "Ingen LSP-servere", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "Indlæser prompt...", + "terminal.loading": "Indlæser terminal...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + + "common.closeTab": "Luk fane", + "common.dismiss": "Afvis", + "common.requestFailed": "Forespørgsel mislykkedes", + "common.moreOptions": "Flere muligheder", + "common.learnMore": "Lær mere", + "common.rename": "Omdøb", + "common.reset": "Nulstil", + "common.delete": "Slet", + "common.close": "Luk", + "common.edit": "Rediger", + "common.loadMore": "Indlæs flere", + + "sidebar.settings": "Indstillinger", + "sidebar.help": "Hjælp", + "sidebar.workspaces.enable": "Aktiver arbejdsområder", + "sidebar.workspaces.disable": "Deaktiver arbejdsområder", + "sidebar.gettingStarted.title": "Kom i gang", + "sidebar.gettingStarted.line1": "OpenCode inkluderer gratis modeller så du kan starte med det samme.", + "sidebar.gettingStarted.line2": "Forbind enhver udbyder for at bruge modeller, inkl. Claude, GPT, Gemini osv.", + "sidebar.project.recentSessions": "Seneste sessioner", + "sidebar.project.viewAllSessions": "Vis alle sessioner", + + "settings.section.desktop": "Desktop", + "settings.tab.general": "Generelt", + "settings.tab.shortcuts": "Genveje", + + "settings.general.section.appearance": "Udseende", + "settings.general.section.notifications": "Systemmeddelelser", + "settings.general.section.sounds": "Lydeffekter", + + "settings.general.row.language.title": "Sprog", + "settings.general.row.language.description": "Ændr visningssproget for OpenCode", + "settings.general.row.appearance.title": "Udseende", + "settings.general.row.appearance.description": "Tilpas hvordan OpenCode ser ud på din enhed", + "settings.general.row.theme.title": "Tema", + "settings.general.row.theme.description": "Tilpas hvordan OpenCode er temabestemt.", + "settings.general.row.font.title": "Skrifttype", + "settings.general.row.font.description": "Tilpas mono-skrifttypen brugt i kodeblokke", + + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Vis systemmeddelelse når agenten er færdig eller kræver opmærksomhed", + "settings.general.notifications.permissions.title": "Tilladelser", + "settings.general.notifications.permissions.description": "Vis systemmeddelelse når en tilladelse er påkrævet", + "settings.general.notifications.errors.title": "Fejl", + "settings.general.notifications.errors.description": "Vis systemmeddelelse når der opstår en fejl", + + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Afspil lyd når agenten er færdig eller kræver opmærksomhed", + "settings.general.sounds.permissions.title": "Tilladelser", + "settings.general.sounds.permissions.description": "Afspil lyd når en tilladelse er påkrævet", + "settings.general.sounds.errors.title": "Fejl", + "settings.general.sounds.errors.description": "Afspil lyd når der opstår en fejl", + + "settings.shortcuts.title": "Tastaturgenveje", + "settings.shortcuts.reset.button": "Nulstil til standard", + "settings.shortcuts.reset.toast.title": "Genveje nulstillet", + "settings.shortcuts.reset.toast.description": "Tastaturgenveje er blevet nulstillet til standard.", + "settings.shortcuts.conflict.title": "Genvej allerede i brug", + "settings.shortcuts.conflict.description": "{{keybind}} er allerede tildelt til {{titles}}.", + "settings.shortcuts.unassigned": "Ikke tildelt", + "settings.shortcuts.pressKeys": "Tryk på taster", + "settings.shortcuts.search.placeholder": "Søg genveje", + "settings.shortcuts.search.empty": "Ingen genveje fundet", + + "settings.shortcuts.group.general": "Generelt", + "settings.shortcuts.group.session": "Session", + "settings.shortcuts.group.navigation": "Navigation", + "settings.shortcuts.group.modelAndAgent": "Model og agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Prompt", + + "settings.providers.title": "Udbydere", + "settings.providers.description": "Udbyderindstillinger vil kunne konfigureres her.", + "settings.models.title": "Modeller", + "settings.models.description": "Modelindstillinger vil kunne konfigureres her.", + "settings.agents.title": "Agenter", + "settings.agents.description": "Agentindstillinger vil kunne konfigureres her.", + "settings.commands.title": "Kommandoer", + "settings.commands.description": "Kommandoindstillinger vil kunne konfigureres her.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP-indstillinger vil kunne konfigureres her.", + + "settings.permissions.title": "Tilladelser", + "settings.permissions.description": "Styr hvilke værktøjer serveren kan bruge som standard.", + "settings.permissions.section.tools": "Værktøjer", + "settings.permissions.toast.updateFailed.title": "Kunne ikke opdatere tilladelser", + + "settings.permissions.action.allow": "Tillad", + "settings.permissions.action.ask": "Spørg", + "settings.permissions.action.deny": "Afvis", + + "settings.permissions.tool.read.title": "Læs", + "settings.permissions.tool.read.description": "Læsning af en fil (matcher filstien)", + "settings.permissions.tool.edit.title": "Rediger", + "settings.permissions.tool.edit.description": + "Ændre filer, herunder redigeringer, skrivninger, patches og multi-redigeringer", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Match filer ved hjælp af glob-mønstre", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Søg i filindhold ved hjælp af regulære udtryk", + "settings.permissions.tool.list.title": "Liste", + "settings.permissions.tool.list.description": "List filer i en mappe", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Kør shell-kommandoer", + "settings.permissions.tool.task.title": "Opgave", + "settings.permissions.tool.task.description": "Start underagenter", + "settings.permissions.tool.skill.title": "Færdighed", + "settings.permissions.tool.skill.description": "Indlæs en færdighed efter navn", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Kør sprogserverforespørgsler", + "settings.permissions.tool.todoread.title": "Læs To-do", + "settings.permissions.tool.todoread.description": "Læs to-do listen", + "settings.permissions.tool.todowrite.title": "Skriv To-do", + "settings.permissions.tool.todowrite.description": "Opdater to-do listen", + "settings.permissions.tool.webfetch.title": "Webhentning", + "settings.permissions.tool.webfetch.description": "Hent indhold fra en URL", + "settings.permissions.tool.websearch.title": "Websøgning", + "settings.permissions.tool.websearch.description": "Søg på nettet", + "settings.permissions.tool.codesearch.title": "Kodesøgning", + "settings.permissions.tool.codesearch.description": "Søg kode på nettet", + "settings.permissions.tool.external_directory.title": "Ekstern mappe", + "settings.permissions.tool.external_directory.description": "Få adgang til filer uden for projektmappen", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Opdag gentagne værktøjskald med identisk input", + + "workspace.new": "Nyt arbejdsområde", + "workspace.type.local": "lokal", + "workspace.type.sandbox": "sandkasse", + "workspace.create.failed.title": "Kunne ikke oprette arbejdsområde", + "workspace.delete.failed.title": "Kunne ikke slette arbejdsområde", + "workspace.resetting.title": "Nulstiller arbejdsområde", + "workspace.resetting.description": "Dette kan tage et minut.", + "workspace.reset.failed.title": "Kunne ikke nulstille arbejdsområde", + "workspace.reset.success.title": "Arbejdsområde nulstillet", + "workspace.reset.success.description": "Arbejdsområdet matcher nu hovedgrenen.", + "workspace.status.checking": "Tjekker for uflettede ændringer...", + "workspace.status.error": "Kunne ikke bekræfte git-status.", + "workspace.status.clean": "Ingen uflettede ændringer fundet.", + "workspace.status.dirty": "Uflettede ændringer fundet i dette arbejdsområde.", + "workspace.delete.title": "Slet arbejdsområde", + "workspace.delete.confirm": 'Slet arbejdsområde "{{name}}"?', + "workspace.delete.button": "Slet arbejdsområde", + "workspace.reset.title": "Nulstil arbejdsområde", + "workspace.reset.confirm": 'Nulstil arbejdsområde "{{name}}"?', + "workspace.reset.button": "Nulstil arbejdsområde", + "workspace.reset.archived.none": "Ingen aktive sessioner vil blive arkiveret.", + "workspace.reset.archived.one": "1 session vil blive arkiveret.", + "workspace.reset.archived.many": "{{count}} sessioner vil blive arkiveret.", + "workspace.reset.note": "Dette vil nulstille arbejdsområdet til at matche hovedgrenen.", +} diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts new file mode 100644 index 00000000000..495308a6f78 --- /dev/null +++ b/packages/app/src/i18n/de.ts @@ -0,0 +1,566 @@ +import { dict as en } from "./en" + +type Keys = keyof typeof en + +export const dict = { + "command.category.suggested": "Vorgeschlagen", + "command.category.view": "Ansicht", + "command.category.project": "Projekt", + "command.category.provider": "Anbieter", + "command.category.server": "Server", + "command.category.session": "Sitzung", + "command.category.theme": "Thema", + "command.category.language": "Sprache", + "command.category.file": "Datei", + "command.category.terminal": "Terminal", + "command.category.model": "Modell", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Berechtigungen", + "command.category.workspace": "Arbeitsbereich", + + "theme.scheme.system": "System", + "theme.scheme.light": "Hell", + "theme.scheme.dark": "Dunkel", + + "command.sidebar.toggle": "Seitenleiste umschalten", + "command.project.open": "Projekt öffnen", + "command.provider.connect": "Anbieter verbinden", + "command.server.switch": "Server wechseln", + "command.session.previous": "Vorherige Sitzung", + "command.session.next": "Nächste Sitzung", + "command.session.archive": "Sitzung archivieren", + + "command.palette": "Befehlspalette", + + "command.theme.cycle": "Thema wechseln", + "command.theme.set": "Thema verwenden: {{theme}}", + "command.theme.scheme.cycle": "Farbschema wechseln", + "command.theme.scheme.set": "Farbschema verwenden: {{scheme}}", + + "command.language.cycle": "Sprache wechseln", + "command.language.set": "Sprache verwenden: {{language}}", + + "command.session.new": "Neue Sitzung", + "command.file.open": "Datei öffnen", + "command.file.open.description": "Dateien und Befehle durchsuchen", + "command.terminal.toggle": "Terminal umschalten", + "command.review.toggle": "Überprüfung umschalten", + "command.terminal.new": "Neues Terminal", + "command.terminal.new.description": "Neuen Terminal-Tab erstellen", + "command.steps.toggle": "Schritte umschalten", + "command.steps.toggle.description": "Schritte für die aktuelle Nachricht anzeigen oder ausblenden", + "command.message.previous": "Vorherige Nachricht", + "command.message.previous.description": "Zur vorherigen Benutzernachricht gehen", + "command.message.next": "Nächste Nachricht", + "command.message.next.description": "Zur nächsten Benutzernachricht gehen", + "command.model.choose": "Modell wählen", + "command.model.choose.description": "Ein anderes Modell auswählen", + "command.mcp.toggle": "MCPs umschalten", + "command.mcp.toggle.description": "MCPs umschalten", + "command.agent.cycle": "Agent wechseln", + "command.agent.cycle.description": "Zum nächsten Agenten wechseln", + "command.agent.cycle.reverse": "Agent rückwärts wechseln", + "command.agent.cycle.reverse.description": "Zum vorherigen Agenten wechseln", + "command.model.variant.cycle": "Denkaufwand wechseln", + "command.model.variant.cycle.description": "Zum nächsten Aufwandslevel wechseln", + "command.permissions.autoaccept.enable": "Änderungen automatisch akzeptieren", + "command.permissions.autoaccept.disable": "Automatische Annahme von Änderungen stoppen", + "command.session.undo": "Rückgängig", + "command.session.undo.description": "Letzte Nachricht rückgängig machen", + "command.session.redo": "Wiederherstellen", + "command.session.redo.description": "Letzte rückgängig gemachte Nachricht wiederherstellen", + "command.session.compact": "Sitzung komprimieren", + "command.session.compact.description": "Sitzung zusammenfassen, um die Kontextgröße zu reduzieren", + "command.session.fork": "Von Nachricht abzweigen", + "command.session.fork.description": "Neue Sitzung aus einer früheren Nachricht erstellen", + "command.session.share": "Sitzung teilen", + "command.session.share.description": "Diese Sitzung teilen und URL in die Zwischenablage kopieren", + "command.session.unshare": "Teilen der Sitzung aufheben", + "command.session.unshare.description": "Teilen dieser Sitzung beenden", + + "palette.search.placeholder": "Dateien und Befehle durchsuchen", + "palette.empty": "Keine Ergebnisse gefunden", + "palette.group.commands": "Befehle", + "palette.group.files": "Dateien", + + "dialog.provider.search.placeholder": "Anbieter durchsuchen", + "dialog.provider.empty": "Keine Anbieter gefunden", + "dialog.provider.group.popular": "Beliebt", + "dialog.provider.group.other": "Andere", + "dialog.provider.tag.recommended": "Empfohlen", + "dialog.provider.anthropic.note": "Mit Claude Pro/Max oder API-Schlüssel verbinden", + + "dialog.model.select.title": "Modell auswählen", + "dialog.model.search.placeholder": "Modelle durchsuchen", + "dialog.model.empty": "Keine Modellergebnisse", + "dialog.model.manage": "Modelle verwalten", + "dialog.model.manage.description": "Anpassen, welche Modelle in der Modellauswahl erscheinen.", + + "dialog.model.unpaid.freeModels.title": "Kostenlose Modelle von OpenCode", + "dialog.model.unpaid.addMore.title": "Weitere Modelle von beliebten Anbietern hinzufügen", + + "dialog.provider.viewAll": "Alle Anbieter anzeigen", + + "provider.connect.title": "{{provider}} verbinden", + "provider.connect.title.anthropicProMax": "Mit Claude Pro/Max anmelden", + "provider.connect.selectMethod": "Anmeldemethode für {{provider}} auswählen.", + "provider.connect.method.apiKey": "API-Schlüssel", + "provider.connect.status.inProgress": "Autorisierung läuft...", + "provider.connect.status.waiting": "Warten auf Autorisierung...", + "provider.connect.status.failed": "Autorisierung fehlgeschlagen: {{error}}", + "provider.connect.apiKey.description": + "Geben Sie Ihren {{provider}} API-Schlüssel ein, um Ihr Konto zu verbinden und {{provider}} Modelle in OpenCode zu nutzen.", + "provider.connect.apiKey.label": "{{provider}} API-Schlüssel", + "provider.connect.apiKey.placeholder": "API-Schlüssel", + "provider.connect.apiKey.required": "API-Schlüssel ist erforderlich", + "provider.connect.opencodeZen.line1": + "OpenCode Zen bietet Ihnen Zugriff auf eine kuratierte Auswahl zuverlässiger, optimierter Modelle für Coding-Agenten.", + "provider.connect.opencodeZen.line2": + "Mit einem einzigen API-Schlüssel erhalten Sie Zugriff auf Modelle wie Claude, GPT, Gemini, GLM und mehr.", + "provider.connect.opencodeZen.visit.prefix": "Besuchen Sie ", + "provider.connect.opencodeZen.visit.suffix": ", um Ihren API-Schlüssel zu erhalten.", + "provider.connect.oauth.code.visit.prefix": "Besuchen Sie ", + "provider.connect.oauth.code.visit.link": "diesen Link", + "provider.connect.oauth.code.visit.suffix": + ", um Ihren Autorisierungscode zu erhalten, Ihr Konto zu verbinden und {{provider}} Modelle in OpenCode zu nutzen.", + "provider.connect.oauth.code.label": "{{method}} Autorisierungscode", + "provider.connect.oauth.code.placeholder": "Autorisierungscode", + "provider.connect.oauth.code.required": "Autorisierungscode ist erforderlich", + "provider.connect.oauth.code.invalid": "Ungültiger Autorisierungscode", + "provider.connect.oauth.auto.visit.prefix": "Besuchen Sie ", + "provider.connect.oauth.auto.visit.link": "diesen Link", + "provider.connect.oauth.auto.visit.suffix": + " und geben Sie den untenstehenden Code ein, um Ihr Konto zu verbinden und {{provider}} Modelle in OpenCode zu nutzen.", + "provider.connect.oauth.auto.confirmationCode": "Bestätigungscode", + "provider.connect.toast.connected.title": "{{provider}} verbunden", + "provider.connect.toast.connected.description": "{{provider}} Modelle sind jetzt verfügbar.", + + "model.tag.free": "Kostenlos", + "model.tag.latest": "Neueste", + + "common.search.placeholder": "Suchen", + "common.loading": "Laden", + "common.cancel": "Abbrechen", + "common.submit": "Absenden", + "common.save": "Speichern", + "common.saving": "Speichert...", + "common.default": "Standard", + "common.attachment": "Anhang", + + "prompt.placeholder.shell": "Shell-Befehl eingeben...", + "prompt.placeholder.normal": 'Fragen Sie alles... "{{example}}"', + "prompt.mode.shell": "Shell", + "prompt.mode.shell.exit": "esc zum Verlassen", + + "prompt.example.1": "Ein TODO in der Codebasis beheben", + "prompt.example.2": "Was ist der Tech-Stack dieses Projekts?", + "prompt.example.3": "Fehlerhafte Tests beheben", + "prompt.example.4": "Erkläre, wie die Authentifizierung funktioniert", + "prompt.example.5": "Sicherheitslücken finden und beheben", + "prompt.example.6": "Unit-Tests für den Benutzerdienst hinzufügen", + "prompt.example.7": "Diese Funktion lesbarer gestalten", + "prompt.example.8": "Was bedeutet dieser Fehler?", + "prompt.example.9": "Hilf mir, dieses Problem zu debuggen", + "prompt.example.10": "API-Dokumentation generieren", + "prompt.example.11": "Datenbankabfragen optimieren", + "prompt.example.12": "Eingabevalidierung hinzufügen", + "prompt.example.13": "Neue Komponente erstellen für...", + "prompt.example.14": "Wie deploye ich dieses Projekt?", + "prompt.example.15": "Meinen Code auf Best Practices überprüfen", + "prompt.example.16": "Fehlerbehandlung zu dieser Funktion hinzufügen", + "prompt.example.17": "Erkläre dieses Regex-Muster", + "prompt.example.18": "Dies in TypeScript konvertieren", + "prompt.example.19": "Logging in der gesamten Codebasis hinzufügen", + "prompt.example.20": "Welche Abhängigkeiten sind veraltet?", + "prompt.example.21": "Hilf mir, ein Migrationsskript zu schreiben", + "prompt.example.22": "Caching für diesen Endpunkt implementieren", + "prompt.example.23": "Paginierung zu dieser Liste hinzufügen", + "prompt.example.24": "CLI-Befehl erstellen für...", + "prompt.example.25": "Wie funktionieren Umgebungsvariablen hier?", + + "prompt.popover.emptyResults": "Keine passenden Ergebnisse", + "prompt.popover.emptyCommands": "Keine passenden Befehle", + "prompt.dropzone.label": "Bilder oder PDFs hier ablegen", + "prompt.slash.badge.custom": "benutzerdefiniert", + "prompt.context.active": "aktiv", + "prompt.context.includeActiveFile": "Aktive Datei einbeziehen", + "prompt.action.attachFile": "Datei anhängen", + "prompt.action.send": "Senden", + "prompt.action.stop": "Stopp", + + "prompt.toast.pasteUnsupported.title": "Nicht unterstütztes Einfügen", + "prompt.toast.pasteUnsupported.description": "Hier können nur Bilder oder PDFs eingefügt werden.", + "prompt.toast.modelAgentRequired.title": "Wählen Sie einen Agenten und ein Modell", + "prompt.toast.modelAgentRequired.description": + "Wählen Sie einen Agenten und ein Modell, bevor Sie eine Eingabe senden.", + "prompt.toast.worktreeCreateFailed.title": "Worktree konnte nicht erstellt werden", + "prompt.toast.sessionCreateFailed.title": "Sitzung konnte nicht erstellt werden", + "prompt.toast.shellSendFailed.title": "Shell-Befehl konnte nicht gesendet werden", + "prompt.toast.commandSendFailed.title": "Befehl konnte nicht gesendet werden", + "prompt.toast.promptSendFailed.title": "Eingabe konnte nicht gesendet werden", + + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} von {{total}} aktiviert", + "dialog.mcp.empty": "Keine MCPs konfiguriert", + + "mcp.status.connected": "verbunden", + "mcp.status.failed": "fehlgeschlagen", + "mcp.status.needs_auth": "benötigt Authentifizierung", + "mcp.status.disabled": "deaktiviert", + + "dialog.fork.empty": "Keine Nachrichten zum Abzweigen vorhanden", + + "dialog.directory.search.placeholder": "Ordner durchsuchen", + "dialog.directory.empty": "Keine Ordner gefunden", + + "dialog.server.title": "Server", + "dialog.server.description": "Wechseln Sie den OpenCode-Server, mit dem sich diese App verbindet.", + "dialog.server.search.placeholder": "Server durchsuchen", + "dialog.server.empty": "Noch keine Server", + "dialog.server.add.title": "Server hinzufügen", + "dialog.server.add.url": "Server-URL", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Verbindung zum Server fehlgeschlagen", + "dialog.server.add.checking": "Prüfen...", + "dialog.server.add.button": "Hinzufügen", + "dialog.server.default.title": "Standardserver", + "dialog.server.default.description": + "Beim App-Start mit diesem Server verbinden, anstatt einen lokalen Server zu starten. Erfordert Neustart.", + "dialog.server.default.none": "Kein Server ausgewählt", + "dialog.server.default.set": "Aktuellen Server als Standard setzen", + "dialog.server.default.clear": "Löschen", + + "dialog.project.edit.title": "Projekt bearbeiten", + "dialog.project.edit.name": "Name", + "dialog.project.edit.icon": "Icon", + "dialog.project.edit.icon.alt": "Projekt-Icon", + "dialog.project.edit.icon.hint": "Klicken oder Bild ziehen", + "dialog.project.edit.icon.recommended": "Empfohlen: 128x128px", + "dialog.project.edit.color": "Farbe", + + "context.breakdown.title": "Kontext-Aufschlüsselung", + "context.breakdown.note": + 'Ungefähre Aufschlüsselung der Eingabe-Token. "Andere" beinhaltet Werkzeugdefinitionen und Overhead.', + "context.breakdown.system": "System", + "context.breakdown.user": "Benutzer", + "context.breakdown.assistant": "Assistent", + "context.breakdown.tool": "Werkzeugaufrufe", + "context.breakdown.other": "Andere", + + "context.systemPrompt.title": "System-Prompt", + "context.rawMessages.title": "Rohdaten der Nachrichten", + + "context.stats.session": "Sitzung", + "context.stats.messages": "Nachrichten", + "context.stats.provider": "Anbieter", + "context.stats.model": "Modell", + "context.stats.limit": "Kontextlimit", + "context.stats.totalTokens": "Gesamt-Token", + "context.stats.usage": "Nutzung", + "context.stats.inputTokens": "Eingabe-Token", + "context.stats.outputTokens": "Ausgabe-Token", + "context.stats.reasoningTokens": "Reasoning-Token", + "context.stats.cacheTokens": "Cache-Token (lesen/schreiben)", + "context.stats.userMessages": "Benutzernachrichten", + "context.stats.assistantMessages": "Assistentennachrichten", + "context.stats.totalCost": "Gesamtkosten", + "context.stats.sessionCreated": "Sitzung erstellt", + "context.stats.lastActivity": "Letzte Aktivität", + + "context.usage.tokens": "Token", + "context.usage.usage": "Nutzung", + "context.usage.cost": "Kosten", + "context.usage.clickToView": "Klicken, um Kontext anzuzeigen", + + "language.en": "Englisch", + "language.zh": "Chinesisch", + "language.ko": "Koreanisch", + "language.de": "Deutsch", + "language.es": "Spanisch", + "language.fr": "Französisch", + "language.ja": "Japanisch", + "language.da": "Dänisch", + + "toast.language.title": "Sprache", + "toast.language.description": "Zu {{language}} gewechselt", + + "toast.theme.title": "Thema gewechselt", + "toast.scheme.title": "Farbschema", + + "toast.permissions.autoaccept.on.title": "Änderungen werden automatisch akzeptiert", + "toast.permissions.autoaccept.on.description": "Bearbeitungs- und Schreibrechte werden automatisch genehmigt", + "toast.permissions.autoaccept.off.title": "Automatische Annahme von Änderungen gestoppt", + "toast.permissions.autoaccept.off.description": "Bearbeitungs- und Schreibrechte erfordern Genehmigung", + + "toast.model.none.title": "Kein Modell ausgewählt", + "toast.model.none.description": "Verbinden Sie einen Anbieter, um diese Sitzung zusammenzufassen", + + "toast.file.loadFailed.title": "Datei konnte nicht geladen werden", + + "toast.session.share.copyFailed.title": "URL konnte nicht in die Zwischenablage kopiert werden", + "toast.session.share.success.title": "Sitzung geteilt", + "toast.session.share.success.description": "Teilen-URL in die Zwischenablage kopiert!", + "toast.session.share.failed.title": "Sitzung konnte nicht geteilt werden", + "toast.session.share.failed.description": "Beim Teilen der Sitzung ist ein Fehler aufgetreten", + + "toast.session.unshare.success.title": "Teilen der Sitzung aufgehoben", + "toast.session.unshare.success.description": "Teilen der Sitzung erfolgreich aufgehoben!", + "toast.session.unshare.failed.title": "Aufheben des Teilens fehlgeschlagen", + "toast.session.unshare.failed.description": "Beim Aufheben des Teilens ist ein Fehler aufgetreten", + + "toast.session.listFailed.title": "Sitzungen für {{project}} konnten nicht geladen werden", + + "toast.update.title": "Update verfügbar", + "toast.update.description": "Eine neue Version von OpenCode ({{version}}) ist zur Installation verfügbar.", + "toast.update.action.installRestart": "Installieren und neu starten", + "toast.update.action.notYet": "Noch nicht", + + "error.page.title": "Etwas ist schiefgelaufen", + "error.page.description": "Beim Laden der Anwendung ist ein Fehler aufgetreten.", + "error.page.details.label": "Fehlerdetails", + "error.page.action.restart": "Neustart", + "error.page.action.checking": "Prüfen...", + "error.page.action.checkUpdates": "Nach Updates suchen", + "error.page.action.updateTo": "Auf {{version}} aktualisieren", + "error.page.report.prefix": "Bitte melden Sie diesen Fehler dem OpenCode-Team", + "error.page.report.discord": "auf Discord", + "error.page.version": "Version: {{version}}", + + "error.dev.rootNotFound": + "Wurzelelement nicht gefunden. Haben Sie vergessen, es in Ihre index.html aufzunehmen? Oder wurde das id-Attribut falsch geschrieben?", + + "error.globalSync.connectFailed": "Verbindung zum Server fehlgeschlagen. Läuft ein Server unter `{{url}}`?", + + "error.chain.unknown": "Unbekannter Fehler", + "error.chain.causedBy": "Verursacht durch:", + "error.chain.apiError": "API-Fehler", + "error.chain.status": "Status: {{status}}", + "error.chain.retryable": "Wiederholbar: {{retryable}}", + "error.chain.responseBody": "Antwort-Body:\n{{body}}", + "error.chain.didYouMean": "Meinten Sie: {{suggestions}}", + "error.chain.modelNotFound": "Modell nicht gefunden: {{provider}}/{{model}}", + "error.chain.checkConfig": "Überprüfen Sie Ihre Konfiguration (opencode.json) auf Anbieter-/Modellnamen", + "error.chain.mcpFailed": + 'MCP-Server "{{name}}" fehlgeschlagen. Hinweis: OpenCode unterstützt noch keine MCP-Authentifizierung.', + "error.chain.providerAuthFailed": "Anbieter-Authentifizierung fehlgeschlagen ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Anbieter "{{provider}}" konnte nicht initialisiert werden. Überprüfen Sie Anmeldeinformationen und Konfiguration.', + "error.chain.configJsonInvalid": "Konfigurationsdatei unter {{path}} ist kein gültiges JSON(C)", + "error.chain.configJsonInvalidWithMessage": + "Konfigurationsdatei unter {{path}} ist kein gültiges JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Verzeichnis "{{dir}}" in {{path}} ist ungültig. Benennen Sie das Verzeichnis in "{{suggestion}}" um oder entfernen Sie es. Dies ist ein häufiger Tippfehler.', + "error.chain.configFrontmatterError": "Frontmatter in {{path}} konnte nicht geparst werden:\n{{message}}", + "error.chain.configInvalid": "Konfigurationsdatei unter {{path}} ist ungültig", + "error.chain.configInvalidWithMessage": "Konfigurationsdatei unter {{path}} ist ungültig: {{message}}", + + "notification.permission.title": "Berechtigung erforderlich", + "notification.permission.description": "{{sessionTitle}} in {{projectName}} benötigt Berechtigung", + "notification.question.title": "Frage", + "notification.question.description": "{{sessionTitle}} in {{projectName}} hat eine Frage", + "notification.action.goToSession": "Zur Sitzung gehen", + + "notification.session.responseReady.title": "Antwort bereit", + "notification.session.error.title": "Sitzungsfehler", + "notification.session.error.fallbackDescription": "Ein Fehler ist aufgetreten", + + "home.recentProjects": "Letzte Projekte", + "home.empty.title": "Keine letzten Projekte", + "home.empty.description": "Starten Sie, indem Sie ein lokales Projekt öffnen", + + "session.tab.session": "Sitzung", + "session.tab.review": "Überprüfung", + "session.tab.context": "Kontext", + "session.review.filesChanged": "{{count}} Dateien geändert", + "session.review.loadingChanges": "Lade Änderungen...", + "session.review.empty": "Noch keine Änderungen in dieser Sitzung", + "session.messages.renderEarlier": "Frühere Nachrichten rendern", + "session.messages.loadingEarlier": "Lade frühere Nachrichten...", + "session.messages.loadEarlier": "Frühere Nachrichten laden", + "session.messages.loading": "Lade Nachrichten...", + + "session.context.addToContext": "{{selection}} zum Kontext hinzufügen", + + "session.new.worktree.main": "Haupt-Branch", + "session.new.worktree.mainWithBranch": "Haupt-Branch ({{branch}})", + "session.new.worktree.create": "Neuen Worktree erstellen", + "session.new.lastModified": "Zuletzt geändert", + + "session.header.search.placeholder": "{{project}} durchsuchen", + + "session.share.popover.title": "Im Web veröffentlichen", + "session.share.popover.description.shared": + "Diese Sitzung ist öffentlich im Web. Sie ist für jeden mit dem Link zugänglich.", + "session.share.popover.description.unshared": + "Sitzung öffentlich im Web teilen. Sie wird für jeden mit dem Link zugänglich sein.", + "session.share.action.share": "Teilen", + "session.share.action.publish": "Veröffentlichen", + "session.share.action.publishing": "Veröffentliche...", + "session.share.action.unpublish": "Veröffentlichung aufheben", + "session.share.action.unpublishing": "Hebe Veröffentlichung auf...", + "session.share.action.view": "Ansehen", + "session.share.copy.copied": "Kopiert", + "session.share.copy.copyLink": "Link kopieren", + + "lsp.tooltip.none": "Keine LSP-Server", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "Lade Prompt...", + "terminal.loading": "Lade Terminal...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + + "common.closeTab": "Tab schließen", + "common.dismiss": "Verwerfen", + "common.requestFailed": "Anfrage fehlgeschlagen", + "common.moreOptions": "Weitere Optionen", + "common.learnMore": "Mehr erfahren", + "common.rename": "Umbenennen", + "common.reset": "Zurücksetzen", + "common.delete": "Löschen", + "common.close": "Schließen", + "common.edit": "Bearbeiten", + "common.loadMore": "Mehr laden", + + "sidebar.settings": "Einstellungen", + "sidebar.help": "Hilfe", + "sidebar.workspaces.enable": "Arbeitsbereiche aktivieren", + "sidebar.workspaces.disable": "Arbeitsbereiche deaktivieren", + "sidebar.gettingStarted.title": "Erste Schritte", + "sidebar.gettingStarted.line1": "OpenCode enthält kostenlose Modelle, damit Sie sofort loslegen können.", + "sidebar.gettingStarted.line2": + "Verbinden Sie einen beliebigen Anbieter, um Modelle wie Claude, GPT, Gemini usw. zu nutzen.", + "sidebar.project.recentSessions": "Letzte Sitzungen", + "sidebar.project.viewAllSessions": "Alle Sitzungen anzeigen", + + "settings.section.desktop": "Desktop", + "settings.tab.general": "Allgemein", + "settings.tab.shortcuts": "Tastenkombinationen", + + "settings.general.section.appearance": "Erscheinungsbild", + "settings.general.section.notifications": "Systembenachrichtigungen", + "settings.general.section.sounds": "Soundeffekte", + + "settings.general.row.language.title": "Sprache", + "settings.general.row.language.description": "Die Anzeigesprache für OpenCode ändern", + "settings.general.row.appearance.title": "Erscheinungsbild", + "settings.general.row.appearance.description": "Anpassen, wie OpenCode auf Ihrem Gerät aussieht", + "settings.general.row.theme.title": "Thema", + "settings.general.row.theme.description": "Das Thema von OpenCode anpassen.", + "settings.general.row.font.title": "Schriftart", + "settings.general.row.font.description": "Die in Codeblöcken verwendete Monospace-Schriftart anpassen", + + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Systembenachrichtigung anzeigen, wenn der Agent fertig ist oder Aufmerksamkeit benötigt", + "settings.general.notifications.permissions.title": "Berechtigungen", + "settings.general.notifications.permissions.description": + "Systembenachrichtigung anzeigen, wenn eine Berechtigung erforderlich ist", + "settings.general.notifications.errors.title": "Fehler", + "settings.general.notifications.errors.description": "Systembenachrichtigung anzeigen, wenn ein Fehler auftritt", + + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Ton abspielen, wenn der Agent fertig ist oder Aufmerksamkeit benötigt", + "settings.general.sounds.permissions.title": "Berechtigungen", + "settings.general.sounds.permissions.description": "Ton abspielen, wenn eine Berechtigung erforderlich ist", + "settings.general.sounds.errors.title": "Fehler", + "settings.general.sounds.errors.description": "Ton abspielen, wenn ein Fehler auftritt", + + "settings.shortcuts.title": "Tastenkombinationen", + "settings.shortcuts.reset.button": "Auf Standard zurücksetzen", + "settings.shortcuts.reset.toast.title": "Tastenkombinationen zurückgesetzt", + "settings.shortcuts.reset.toast.description": "Die Tastenkombinationen wurden auf die Standardwerte zurückgesetzt.", + "settings.shortcuts.conflict.title": "Tastenkombination bereits in Verwendung", + "settings.shortcuts.conflict.description": "{{keybind}} ist bereits {{titles}} zugewiesen.", + "settings.shortcuts.unassigned": "Nicht zugewiesen", + "settings.shortcuts.pressKeys": "Tasten drücken", + "settings.shortcuts.search.placeholder": "Tastenkürzel suchen", + "settings.shortcuts.search.empty": "Keine Tastenkürzel gefunden", + + "settings.shortcuts.group.general": "Allgemein", + "settings.shortcuts.group.session": "Sitzung", + "settings.shortcuts.group.navigation": "Navigation", + "settings.shortcuts.group.modelAndAgent": "Modell und Agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Prompt", + + "settings.providers.title": "Anbieter", + "settings.providers.description": "Anbietereinstellungen können hier konfiguriert werden.", + "settings.models.title": "Modelle", + "settings.models.description": "Modelleinstellungen können hier konfiguriert werden.", + "settings.agents.title": "Agenten", + "settings.agents.description": "Agenteneinstellungen können hier konfiguriert werden.", + "settings.commands.title": "Befehle", + "settings.commands.description": "Befehlseinstellungen können hier konfiguriert werden.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP-Einstellungen können hier konfiguriert werden.", + + "settings.permissions.title": "Berechtigungen", + "settings.permissions.description": "Steuern Sie, welche Tools der Server standardmäßig verwenden darf.", + "settings.permissions.section.tools": "Tools", + "settings.permissions.toast.updateFailed.title": "Berechtigungen konnten nicht aktualisiert werden", + + "settings.permissions.action.allow": "Erlauben", + "settings.permissions.action.ask": "Fragen", + "settings.permissions.action.deny": "Verweigern", + + "settings.permissions.tool.read.title": "Lesen", + "settings.permissions.tool.read.description": "Lesen einer Datei (stimmt mit dem Dateipfad überein)", + "settings.permissions.tool.edit.title": "Bearbeiten", + "settings.permissions.tool.edit.description": + "Dateien ändern, einschließlich Bearbeitungen, Schreibvorgängen, Patches und Mehrfachbearbeitungen", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Dateien mithilfe von Glob-Mustern abgleichen", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Dateiinhalte mit regulären Ausdrücken durchsuchen", + "settings.permissions.tool.list.title": "Auflisten", + "settings.permissions.tool.list.description": "Dateien in einem Verzeichnis auflisten", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Shell-Befehle ausführen", + "settings.permissions.tool.task.title": "Aufgabe", + "settings.permissions.tool.task.description": "Unteragenten starten", + "settings.permissions.tool.skill.title": "Fähigkeit", + "settings.permissions.tool.skill.description": "Eine Fähigkeit nach Namen laden", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Language-Server-Abfragen ausführen", + "settings.permissions.tool.todoread.title": "Todo lesen", + "settings.permissions.tool.todoread.description": "Die Todo-Liste lesen", + "settings.permissions.tool.todowrite.title": "Todo schreiben", + "settings.permissions.tool.todowrite.description": "Die Todo-Liste aktualisieren", + "settings.permissions.tool.webfetch.title": "Web-Abruf", + "settings.permissions.tool.webfetch.description": "Inhalt von einer URL abrufen", + "settings.permissions.tool.websearch.title": "Web-Suche", + "settings.permissions.tool.websearch.description": "Das Web durchsuchen", + "settings.permissions.tool.codesearch.title": "Code-Suche", + "settings.permissions.tool.codesearch.description": "Code im Web durchsuchen", + "settings.permissions.tool.external_directory.title": "Externes Verzeichnis", + "settings.permissions.tool.external_directory.description": "Zugriff auf Dateien außerhalb des Projektverzeichnisses", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Wiederholte Tool-Aufrufe mit identischer Eingabe erkennen", + + "workspace.new": "Neuer Arbeitsbereich", + "workspace.type.local": "lokal", + "workspace.type.sandbox": "Sandbox", + "workspace.create.failed.title": "Arbeitsbereich konnte nicht erstellt werden", + "workspace.delete.failed.title": "Arbeitsbereich konnte nicht gelöscht werden", + "workspace.resetting.title": "Arbeitsbereich wird zurückgesetzt", + "workspace.resetting.description": "Dies kann eine Minute dauern.", + "workspace.reset.failed.title": "Arbeitsbereich konnte nicht zurückgesetzt werden", + "workspace.reset.success.title": "Arbeitsbereich zurückgesetzt", + "workspace.reset.success.description": "Der Arbeitsbereich entspricht jetzt dem Standard-Branch.", + "workspace.status.checking": "Suche nach nicht zusammengeführten Änderungen...", + "workspace.status.error": "Git-Status konnte nicht überprüft werden.", + "workspace.status.clean": "Keine nicht zusammengeführten Änderungen erkannt.", + "workspace.status.dirty": "Nicht zusammengeführte Änderungen in diesem Arbeitsbereich erkannt.", + "workspace.delete.title": "Arbeitsbereich löschen", + "workspace.delete.confirm": 'Arbeitsbereich "{{name}}" löschen?', + "workspace.delete.button": "Arbeitsbereich löschen", + "workspace.reset.title": "Arbeitsbereich zurücksetzen", + "workspace.reset.confirm": 'Arbeitsbereich "{{name}}" zurücksetzen?', + "workspace.reset.button": "Arbeitsbereich zurücksetzen", + "workspace.reset.archived.none": "Keine aktiven Sitzungen werden archiviert.", + "workspace.reset.archived.one": "1 Sitzung wird archiviert.", + "workspace.reset.archived.many": "{{count}} Sitzungen werden archiviert.", + "workspace.reset.note": "Dadurch wird der Arbeitsbereich auf den Standard-Branch zurückgesetzt.", +} satisfies Partial> diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts new file mode 100644 index 00000000000..36f90a4fee0 --- /dev/null +++ b/packages/app/src/i18n/en.ts @@ -0,0 +1,635 @@ +export const dict = { + "command.category.suggested": "Suggested", + "command.category.view": "View", + "command.category.project": "Project", + "command.category.provider": "Provider", + "command.category.server": "Server", + "command.category.session": "Session", + "command.category.theme": "Theme", + "command.category.language": "Language", + "command.category.file": "File", + "command.category.terminal": "Terminal", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Permissions", + "command.category.workspace": "Workspace", + "command.category.settings": "Settings", + + "theme.scheme.system": "System", + "theme.scheme.light": "Light", + "theme.scheme.dark": "Dark", + + "command.sidebar.toggle": "Toggle sidebar", + "command.project.open": "Open project", + "command.provider.connect": "Connect provider", + "command.server.switch": "Switch server", + "command.settings.open": "Open settings", + "command.session.previous": "Previous session", + "command.session.next": "Next session", + "command.session.archive": "Archive session", + + "command.palette": "Command palette", + + "command.theme.cycle": "Cycle theme", + "command.theme.set": "Use theme: {{theme}}", + "command.theme.scheme.cycle": "Cycle color scheme", + "command.theme.scheme.set": "Use color scheme: {{scheme}}", + + "command.language.cycle": "Cycle language", + "command.language.set": "Use language: {{language}}", + + "command.session.new": "New session", + "command.file.open": "Open file", + "command.file.open.description": "Search files and commands", + "command.terminal.toggle": "Toggle terminal", + "command.review.toggle": "Toggle review", + "command.terminal.new": "New terminal", + "command.terminal.new.description": "Create a new terminal tab", + "command.steps.toggle": "Toggle steps", + "command.steps.toggle.description": "Show or hide steps for the current message", + "command.message.previous": "Previous message", + "command.message.previous.description": "Go to the previous user message", + "command.message.next": "Next message", + "command.message.next.description": "Go to the next user message", + "command.model.choose": "Choose model", + "command.model.choose.description": "Select a different model", + "command.mcp.toggle": "Toggle MCPs", + "command.mcp.toggle.description": "Toggle MCPs", + "command.agent.cycle": "Cycle agent", + "command.agent.cycle.description": "Switch to the next agent", + "command.agent.cycle.reverse": "Cycle agent backwards", + "command.agent.cycle.reverse.description": "Switch to the previous agent", + "command.model.variant.cycle": "Cycle thinking effort", + "command.model.variant.cycle.description": "Switch to the next effort level", + "command.permissions.autoaccept.enable": "Auto-accept edits", + "command.permissions.autoaccept.disable": "Stop auto-accepting edits", + "command.session.undo": "Undo", + "command.session.undo.description": "Undo the last message", + "command.session.redo": "Redo", + "command.session.redo.description": "Redo the last undone message", + "command.session.compact": "Compact session", + "command.session.compact.description": "Summarize the session to reduce context size", + "command.session.fork": "Fork from message", + "command.session.fork.description": "Create a new session from a previous message", + "command.session.share": "Share session", + "command.session.share.description": "Share this session and copy the URL to clipboard", + "command.session.unshare": "Unshare session", + "command.session.unshare.description": "Stop sharing this session", + + "palette.search.placeholder": "Search files and commands", + "palette.empty": "No results found", + "palette.group.commands": "Commands", + "palette.group.files": "Files", + + "dialog.provider.search.placeholder": "Search providers", + "dialog.provider.empty": "No providers found", + "dialog.provider.group.popular": "Popular", + "dialog.provider.group.other": "Other", + "dialog.provider.tag.recommended": "Recommended", + "dialog.provider.anthropic.note": "Connect with Claude Pro/Max or API key", + + "dialog.model.select.title": "Select model", + "dialog.model.search.placeholder": "Search models", + "dialog.model.empty": "No model results", + "dialog.model.manage": "Manage models", + "dialog.model.manage.description": "Customize which models appear in the model selector.", + + "dialog.model.unpaid.freeModels.title": "Free models provided by OpenCode", + "dialog.model.unpaid.addMore.title": "Add more models from popular providers", + + "dialog.provider.viewAll": "View all providers", + + "provider.connect.title": "Connect {{provider}}", + "provider.connect.title.anthropicProMax": "Login with Claude Pro/Max", + "provider.connect.selectMethod": "Select login method for {{provider}}.", + "provider.connect.method.apiKey": "API key", + "provider.connect.status.inProgress": "Authorization in progress...", + "provider.connect.status.waiting": "Waiting for authorization...", + "provider.connect.status.failed": "Authorization failed: {{error}}", + "provider.connect.apiKey.description": + "Enter your {{provider}} API key to connect your account and use {{provider}} models in OpenCode.", + "provider.connect.apiKey.label": "{{provider}} API key", + "provider.connect.apiKey.placeholder": "API key", + "provider.connect.apiKey.required": "API key is required", + "provider.connect.opencodeZen.line1": + "OpenCode Zen gives you access to a curated set of reliable optimized models for coding agents.", + "provider.connect.opencodeZen.line2": + "With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more.", + "provider.connect.opencodeZen.visit.prefix": "Visit ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " to collect your API key.", + "provider.connect.oauth.code.visit.prefix": "Visit ", + "provider.connect.oauth.code.visit.link": "this link", + "provider.connect.oauth.code.visit.suffix": + " to collect your authorization code to connect your account and use {{provider}} models in OpenCode.", + "provider.connect.oauth.code.label": "{{method}} authorization code", + "provider.connect.oauth.code.placeholder": "Authorization code", + "provider.connect.oauth.code.required": "Authorization code is required", + "provider.connect.oauth.code.invalid": "Invalid authorization code", + "provider.connect.oauth.auto.visit.prefix": "Visit ", + "provider.connect.oauth.auto.visit.link": "this link", + "provider.connect.oauth.auto.visit.suffix": + " and enter the code below to connect your account and use {{provider}} models in OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Confirmation code", + "provider.connect.toast.connected.title": "{{provider}} connected", + "provider.connect.toast.connected.description": "{{provider}} models are now available to use.", + + "model.tag.free": "Free", + "model.tag.latest": "Latest", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "text", + "model.input.image": "image", + "model.input.audio": "audio", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Allows: {{inputs}}", + "model.tooltip.reasoning.allowed": "Allows reasoning", + "model.tooltip.reasoning.none": "No reasoning", + "model.tooltip.context": "Context limit {{limit}}", + + "common.search.placeholder": "Search", + "common.loading": "Loading", + "common.loading.ellipsis": "...", + "common.cancel": "Cancel", + "common.submit": "Submit", + "common.save": "Save", + "common.saving": "Saving...", + "common.default": "Default", + "common.attachment": "attachment", + + "prompt.placeholder.shell": "Enter shell command...", + "prompt.placeholder.normal": 'Ask anything... "{{example}}"', + "prompt.mode.shell": "Shell", + "prompt.mode.shell.exit": "esc to exit", + + "prompt.example.1": "Fix a TODO in the codebase", + "prompt.example.2": "What is the tech stack of this project?", + "prompt.example.3": "Fix broken tests", + "prompt.example.4": "Explain how authentication works", + "prompt.example.5": "Find and fix security vulnerabilities", + "prompt.example.6": "Add unit tests for the user service", + "prompt.example.7": "Refactor this function to be more readable", + "prompt.example.8": "What does this error mean?", + "prompt.example.9": "Help me debug this issue", + "prompt.example.10": "Generate API documentation", + "prompt.example.11": "Optimize database queries", + "prompt.example.12": "Add input validation", + "prompt.example.13": "Create a new component for...", + "prompt.example.14": "How do I deploy this project?", + "prompt.example.15": "Review my code for best practices", + "prompt.example.16": "Add error handling to this function", + "prompt.example.17": "Explain this regex pattern", + "prompt.example.18": "Convert this to TypeScript", + "prompt.example.19": "Add logging throughout the codebase", + "prompt.example.20": "What dependencies are outdated?", + "prompt.example.21": "Help me write a migration script", + "prompt.example.22": "Implement caching for this endpoint", + "prompt.example.23": "Add pagination to this list", + "prompt.example.24": "Create a CLI command for...", + "prompt.example.25": "How do environment variables work here?", + + "prompt.popover.emptyResults": "No matching results", + "prompt.popover.emptyCommands": "No matching commands", + "prompt.dropzone.label": "Drop images or PDFs here", + "prompt.slash.badge.custom": "custom", + "prompt.context.active": "active", + "prompt.context.includeActiveFile": "Include active file", + "prompt.action.attachFile": "Attach file", + "prompt.action.send": "Send", + "prompt.action.stop": "Stop", + + "prompt.toast.pasteUnsupported.title": "Unsupported paste", + "prompt.toast.pasteUnsupported.description": "Only images or PDFs can be pasted here.", + "prompt.toast.modelAgentRequired.title": "Select an agent and model", + "prompt.toast.modelAgentRequired.description": "Choose an agent and model before sending a prompt.", + "prompt.toast.worktreeCreateFailed.title": "Failed to create worktree", + "prompt.toast.sessionCreateFailed.title": "Failed to create session", + "prompt.toast.shellSendFailed.title": "Failed to send shell command", + "prompt.toast.commandSendFailed.title": "Failed to send command", + "prompt.toast.promptSendFailed.title": "Failed to send prompt", + + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} of {{total}} enabled", + "dialog.mcp.empty": "No MCPs configured", + + "mcp.status.connected": "connected", + "mcp.status.failed": "failed", + "mcp.status.needs_auth": "needs auth", + "mcp.status.disabled": "disabled", + + "dialog.fork.empty": "No messages to fork from", + + "dialog.directory.search.placeholder": "Search folders", + "dialog.directory.empty": "No folders found", + + "dialog.server.title": "Servers", + "dialog.server.description": "Switch which OpenCode server this app connects to.", + "dialog.server.search.placeholder": "Search servers", + "dialog.server.empty": "No servers yet", + "dialog.server.add.title": "Add a server", + "dialog.server.add.url": "Server URL", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Could not connect to server", + "dialog.server.add.checking": "Checking...", + "dialog.server.add.button": "Add", + "dialog.server.default.title": "Default server", + "dialog.server.default.description": + "Connect to this server on app launch instead of starting a local server. Requires restart.", + "dialog.server.default.none": "No server selected", + "dialog.server.default.set": "Set current server as default", + "dialog.server.default.clear": "Clear", + + "dialog.project.edit.title": "Edit project", + "dialog.project.edit.name": "Name", + "dialog.project.edit.icon": "Icon", + "dialog.project.edit.icon.alt": "Project icon", + "dialog.project.edit.icon.hint": "Click or drag an image", + "dialog.project.edit.icon.recommended": "Recommended: 128x128px", + "dialog.project.edit.color": "Color", + + "context.breakdown.title": "Context Breakdown", + "context.breakdown.note": 'Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.', + "context.breakdown.system": "System", + "context.breakdown.user": "User", + "context.breakdown.assistant": "Assistant", + "context.breakdown.tool": "Tool Calls", + "context.breakdown.other": "Other", + + "context.systemPrompt.title": "System Prompt", + "context.rawMessages.title": "Raw messages", + + "context.stats.session": "Session", + "context.stats.messages": "Messages", + "context.stats.provider": "Provider", + "context.stats.model": "Model", + "context.stats.limit": "Context Limit", + "context.stats.totalTokens": "Total Tokens", + "context.stats.usage": "Usage", + "context.stats.inputTokens": "Input Tokens", + "context.stats.outputTokens": "Output Tokens", + "context.stats.reasoningTokens": "Reasoning Tokens", + "context.stats.cacheTokens": "Cache Tokens (read/write)", + "context.stats.userMessages": "User Messages", + "context.stats.assistantMessages": "Assistant Messages", + "context.stats.totalCost": "Total Cost", + "context.stats.sessionCreated": "Session Created", + "context.stats.lastActivity": "Last Activity", + + "context.usage.tokens": "Tokens", + "context.usage.usage": "Usage", + "context.usage.cost": "Cost", + "context.usage.clickToView": "Click to view context", + + "language.en": "English", + "language.zh": "Chinese", + "language.ko": "Korean", + "language.de": "German", + "language.es": "Spanish", + "language.fr": "French", + "language.ja": "Japanese", + "language.da": "Danish", + + "toast.language.title": "Language", + "toast.language.description": "Switched to {{language}}", + + "toast.theme.title": "Theme switched", + "toast.scheme.title": "Color scheme", + + "toast.permissions.autoaccept.on.title": "Auto-accepting edits", + "toast.permissions.autoaccept.on.description": "Edit and write permissions will be automatically approved", + "toast.permissions.autoaccept.off.title": "Stopped auto-accepting edits", + "toast.permissions.autoaccept.off.description": "Edit and write permissions will require approval", + + "toast.model.none.title": "No model selected", + "toast.model.none.description": "Connect a provider to summarize this session", + + "toast.file.loadFailed.title": "Failed to load file", + + "toast.session.share.copyFailed.title": "Failed to copy URL to clipboard", + "toast.session.share.success.title": "Session shared", + "toast.session.share.success.description": "Share URL copied to clipboard!", + "toast.session.share.failed.title": "Failed to share session", + "toast.session.share.failed.description": "An error occurred while sharing the session", + + "toast.session.unshare.success.title": "Session unshared", + "toast.session.unshare.success.description": "Session unshared successfully!", + "toast.session.unshare.failed.title": "Failed to unshare session", + "toast.session.unshare.failed.description": "An error occurred while unsharing the session", + + "toast.session.listFailed.title": "Failed to load sessions for {{project}}", + + "toast.update.title": "Update available", + "toast.update.description": "A new version of OpenCode ({{version}}) is now available to install.", + "toast.update.action.installRestart": "Install and restart", + "toast.update.action.notYet": "Not yet", + + "error.page.title": "Something went wrong", + "error.page.description": "An error occurred while loading the application.", + "error.page.details.label": "Error Details", + "error.page.action.restart": "Restart", + "error.page.action.checking": "Checking...", + "error.page.action.checkUpdates": "Check for updates", + "error.page.action.updateTo": "Update to {{version}}", + "error.page.report.prefix": "Please report this error to the OpenCode team", + "error.page.report.discord": "on Discord", + "error.page.version": "Version: {{version}}", + + "error.dev.rootNotFound": + "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?", + + "error.globalSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?", + + "error.chain.unknown": "Unknown error", + "error.chain.causedBy": "Caused by:", + "error.chain.apiError": "API error", + "error.chain.status": "Status: {{status}}", + "error.chain.retryable": "Retryable: {{retryable}}", + "error.chain.responseBody": "Response body:\n{{body}}", + "error.chain.didYouMean": "Did you mean: {{suggestions}}", + "error.chain.modelNotFound": "Model not found: {{provider}}/{{model}}", + "error.chain.checkConfig": "Check your config (opencode.json) provider/model names", + "error.chain.mcpFailed": 'MCP server "{{name}}" failed. Note, OpenCode does not support MCP authentication yet.', + "error.chain.providerAuthFailed": "Provider authentication failed ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Failed to initialize provider "{{provider}}". Check credentials and configuration.', + "error.chain.configJsonInvalid": "Config file at {{path}} is not valid JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Config file at {{path}} is not valid JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Directory "{{dir}}" in {{path}} is not valid. Rename the directory to "{{suggestion}}" or remove it. This is a common typo.', + "error.chain.configFrontmatterError": "Failed to parse frontmatter in {{path}}:\n{{message}}", + "error.chain.configInvalid": "Config file at {{path}} is invalid", + "error.chain.configInvalidWithMessage": "Config file at {{path}} is invalid: {{message}}", + + "notification.permission.title": "Permission required", + "notification.permission.description": "{{sessionTitle}} in {{projectName}} needs permission", + "notification.question.title": "Question", + "notification.question.description": "{{sessionTitle}} in {{projectName}} has a question", + "notification.action.goToSession": "Go to session", + + "notification.session.responseReady.title": "Response ready", + "notification.session.error.title": "Session error", + "notification.session.error.fallbackDescription": "An error occurred", + + "home.recentProjects": "Recent projects", + "home.empty.title": "No recent projects", + "home.empty.description": "Get started by opening a local project", + + "session.tab.session": "Session", + "session.tab.review": "Review", + "session.tab.context": "Context", + "session.review.filesChanged": "{{count}} Files Changed", + "session.review.loadingChanges": "Loading changes...", + "session.review.empty": "No changes in this session yet", + "session.messages.renderEarlier": "Render earlier messages", + "session.messages.loadingEarlier": "Loading earlier messages...", + "session.messages.loadEarlier": "Load earlier messages", + "session.messages.loading": "Loading messages...", + "session.messages.jumpToLatest": "Jump to latest", + + "session.context.addToContext": "Add {{selection}} to context", + + "session.new.worktree.main": "Main branch", + "session.new.worktree.mainWithBranch": "Main branch ({{branch}})", + "session.new.worktree.create": "Create new worktree", + "session.new.lastModified": "Last modified", + + "session.header.search.placeholder": "Search {{project}}", + + "session.share.popover.title": "Publish on web", + "session.share.popover.description.shared": + "This session is public on the web. It is accessible to anyone with the link.", + "session.share.popover.description.unshared": + "Share session publicly on the web. It will be accessible to anyone with the link.", + "session.share.action.share": "Share", + "session.share.action.publish": "Publish", + "session.share.action.publishing": "Publishing...", + "session.share.action.unpublish": "Unpublish", + "session.share.action.unpublishing": "Unpublishing...", + "session.share.action.view": "View", + "session.share.copy.copied": "Copied", + "session.share.copy.copyLink": "Copy link", + + "lsp.tooltip.none": "No LSP servers", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "Loading prompt...", + "terminal.loading": "Loading terminal...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.connectionLost.title": "Connection Lost", + "terminal.connectionLost.description": + "The terminal connection was interrupted. This can happen when the server restarts.", + + "common.closeTab": "Close tab", + "common.dismiss": "Dismiss", + "common.requestFailed": "Request failed", + "common.moreOptions": "More options", + "common.learnMore": "Learn more", + "common.rename": "Rename", + "common.reset": "Reset", + "common.delete": "Delete", + "common.close": "Close", + "common.edit": "Edit", + "common.loadMore": "Load more", + "common.key.esc": "ESC", + + "sidebar.menu.toggle": "Toggle menu", + "sidebar.settings": "Settings", + "sidebar.help": "Help", + "sidebar.workspaces.enable": "Enable workspaces", + "sidebar.workspaces.disable": "Disable workspaces", + "sidebar.gettingStarted.title": "Getting started", + "sidebar.gettingStarted.line1": "OpenCode includes free models so you can start immediately.", + "sidebar.gettingStarted.line2": "Connect any provider to use models, inc. Claude, GPT, Gemini etc.", + "sidebar.project.recentSessions": "Recent sessions", + "sidebar.project.viewAllSessions": "View all sessions", + + "settings.section.desktop": "Desktop", + "settings.tab.general": "General", + "settings.tab.shortcuts": "Shortcuts", + + "settings.general.section.appearance": "Appearance", + "settings.general.section.notifications": "System notifications", + "settings.general.section.sounds": "Sound effects", + + "settings.general.row.language.title": "Language", + "settings.general.row.language.description": "Change the display language for OpenCode", + "settings.general.row.appearance.title": "Appearance", + "settings.general.row.appearance.description": "Customise how OpenCode looks on your device", + "settings.general.row.theme.title": "Theme", + "settings.general.row.theme.description": "Customise how OpenCode is themed.", + "settings.general.row.font.title": "Font", + "settings.general.row.font.description": "Customise the mono font used in code blocks", + "font.option.ibmPlexMono": "IBM Plex Mono", + "font.option.cascadiaCode": "Cascadia Code", + "font.option.firaCode": "Fira Code", + "font.option.hack": "Hack", + "font.option.inconsolata": "Inconsolata", + "font.option.intelOneMono": "Intel One Mono", + "font.option.jetbrainsMono": "JetBrains Mono", + "font.option.mesloLgs": "Meslo LGS", + "font.option.robotoMono": "Roboto Mono", + "font.option.sourceCodePro": "Source Code Pro", + "font.option.ubuntuMono": "Ubuntu Mono", + "sound.option.alert01": "Alert 01", + "sound.option.alert02": "Alert 02", + "sound.option.alert03": "Alert 03", + "sound.option.alert04": "Alert 04", + "sound.option.alert05": "Alert 05", + "sound.option.alert06": "Alert 06", + "sound.option.alert07": "Alert 07", + "sound.option.alert08": "Alert 08", + "sound.option.alert09": "Alert 09", + "sound.option.alert10": "Alert 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Show system notification when the agent is complete or needs attention", + "settings.general.notifications.permissions.title": "Permissions", + "settings.general.notifications.permissions.description": "Show system notification when a permission is required", + "settings.general.notifications.errors.title": "Errors", + "settings.general.notifications.errors.description": "Show system notification when an error occurs", + + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Play sound when the agent is complete or needs attention", + "settings.general.sounds.permissions.title": "Permissions", + "settings.general.sounds.permissions.description": "Play sound when a permission is required", + "settings.general.sounds.errors.title": "Errors", + "settings.general.sounds.errors.description": "Play sound when an error occurs", + + "settings.shortcuts.title": "Keyboard shortcuts", + "settings.shortcuts.reset.button": "Reset to defaults", + "settings.shortcuts.reset.toast.title": "Shortcuts reset", + "settings.shortcuts.reset.toast.description": "Keyboard shortcuts have been reset to defaults.", + "settings.shortcuts.conflict.title": "Shortcut already in use", + "settings.shortcuts.conflict.description": "{{keybind}} is already assigned to {{titles}}.", + "settings.shortcuts.unassigned": "Unassigned", + "settings.shortcuts.pressKeys": "Press keys", + "settings.shortcuts.search.placeholder": "Search shortcuts", + "settings.shortcuts.search.empty": "No shortcuts found", + + "settings.shortcuts.group.general": "General", + "settings.shortcuts.group.session": "Session", + "settings.shortcuts.group.navigation": "Navigation", + "settings.shortcuts.group.modelAndAgent": "Model and agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Prompt", + + "settings.providers.title": "Providers", + "settings.providers.description": "Provider settings will be configurable here.", + "settings.models.title": "Models", + "settings.models.description": "Model settings will be configurable here.", + "settings.agents.title": "Agents", + "settings.agents.description": "Agent settings will be configurable here.", + "settings.commands.title": "Commands", + "settings.commands.description": "Command settings will be configurable here.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP settings will be configurable here.", + + "settings.permissions.title": "Permissions", + "settings.permissions.description": "Control what tools the server can use by default.", + "settings.permissions.section.tools": "Tools", + "settings.permissions.toast.updateFailed.title": "Failed to update permissions", + + "settings.permissions.action.allow": "Allow", + "settings.permissions.action.ask": "Ask", + "settings.permissions.action.deny": "Deny", + + "settings.permissions.tool.read.title": "Read", + "settings.permissions.tool.read.description": "Reading a file (matches the file path)", + "settings.permissions.tool.edit.title": "Edit", + "settings.permissions.tool.edit.description": "Modify files, including edits, writes, patches, and multi-edits", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Match files using glob patterns", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Search file contents using regular expressions", + "settings.permissions.tool.list.title": "List", + "settings.permissions.tool.list.description": "List files within a directory", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Run shell commands", + "settings.permissions.tool.task.title": "Task", + "settings.permissions.tool.task.description": "Launch sub-agents", + "settings.permissions.tool.skill.title": "Skill", + "settings.permissions.tool.skill.description": "Load a skill by name", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Run language server queries", + "settings.permissions.tool.todoread.title": "Todo Read", + "settings.permissions.tool.todoread.description": "Read the todo list", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "Update the todo list", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "Fetch content from a URL", + "settings.permissions.tool.websearch.title": "Web Search", + "settings.permissions.tool.websearch.description": "Search the web", + "settings.permissions.tool.codesearch.title": "Code Search", + "settings.permissions.tool.codesearch.description": "Search code on the web", + "settings.permissions.tool.external_directory.title": "External Directory", + "settings.permissions.tool.external_directory.description": "Access files outside the project directory", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Detect repeated tool calls with identical input", + + "workspace.new": "New workspace", + "workspace.type.local": "local", + "workspace.type.sandbox": "sandbox", + "workspace.create.failed.title": "Failed to create workspace", + "workspace.delete.failed.title": "Failed to delete workspace", + "workspace.resetting.title": "Resetting workspace", + "workspace.resetting.description": "This may take a minute.", + "workspace.reset.failed.title": "Failed to reset workspace", + "workspace.reset.success.title": "Workspace reset", + "workspace.reset.success.description": "Workspace now matches the default branch.", + "workspace.status.checking": "Checking for unmerged changes...", + "workspace.status.error": "Unable to verify git status.", + "workspace.status.clean": "No unmerged changes detected.", + "workspace.status.dirty": "Unmerged changes detected in this workspace.", + "workspace.delete.title": "Delete workspace", + "workspace.delete.confirm": 'Delete workspace "{{name}}"?', + "workspace.delete.button": "Delete workspace", + "workspace.reset.title": "Reset workspace", + "workspace.reset.confirm": 'Reset workspace "{{name}}"?', + "workspace.reset.button": "Reset workspace", + "workspace.reset.archived.none": "No active sessions will be archived.", + "workspace.reset.archived.one": "1 session will be archived.", + "workspace.reset.archived.many": "{{count}} sessions will be archived.", + "workspace.reset.note": "This will reset the workspace to match the default branch.", +} diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts new file mode 100644 index 00000000000..bae4ad48035 --- /dev/null +++ b/packages/app/src/i18n/es.ts @@ -0,0 +1,560 @@ +export const dict = { + "command.category.suggested": "Sugerido", + "command.category.view": "Ver", + "command.category.project": "Proyecto", + "command.category.provider": "Proveedor", + "command.category.server": "Servidor", + "command.category.session": "Sesión", + "command.category.theme": "Tema", + "command.category.language": "Idioma", + "command.category.file": "Archivo", + "command.category.terminal": "Terminal", + "command.category.model": "Modelo", + "command.category.mcp": "MCP", + "command.category.agent": "Agente", + "command.category.permissions": "Permisos", + "command.category.workspace": "Espacio de trabajo", + + "theme.scheme.system": "Sistema", + "theme.scheme.light": "Claro", + "theme.scheme.dark": "Oscuro", + + "command.sidebar.toggle": "Alternar barra lateral", + "command.project.open": "Abrir proyecto", + "command.provider.connect": "Conectar proveedor", + "command.server.switch": "Cambiar servidor", + "command.session.previous": "Sesión anterior", + "command.session.next": "Siguiente sesión", + "command.session.archive": "Archivar sesión", + + "command.palette": "Paleta de comandos", + + "command.theme.cycle": "Alternar tema", + "command.theme.set": "Usar tema: {{theme}}", + "command.theme.scheme.cycle": "Alternar esquema de color", + "command.theme.scheme.set": "Usar esquema de color: {{scheme}}", + + "command.language.cycle": "Alternar idioma", + "command.language.set": "Usar idioma: {{language}}", + + "command.session.new": "Nueva sesión", + "command.file.open": "Abrir archivo", + "command.file.open.description": "Buscar archivos y comandos", + "command.terminal.toggle": "Alternar terminal", + "command.review.toggle": "Alternar revisión", + "command.terminal.new": "Nueva terminal", + "command.terminal.new.description": "Crear una nueva pestaña de terminal", + "command.steps.toggle": "Alternar pasos", + "command.steps.toggle.description": "Mostrar u ocultar pasos para el mensaje actual", + "command.message.previous": "Mensaje anterior", + "command.message.previous.description": "Ir al mensaje de usuario anterior", + "command.message.next": "Siguiente mensaje", + "command.message.next.description": "Ir al siguiente mensaje de usuario", + "command.model.choose": "Elegir modelo", + "command.model.choose.description": "Seleccionar un modelo diferente", + "command.mcp.toggle": "Alternar MCPs", + "command.mcp.toggle.description": "Alternar MCPs", + "command.agent.cycle": "Alternar agente", + "command.agent.cycle.description": "Cambiar al siguiente agente", + "command.agent.cycle.reverse": "Alternar agente hacia atrás", + "command.agent.cycle.reverse.description": "Cambiar al agente anterior", + "command.model.variant.cycle": "Alternar esfuerzo de pensamiento", + "command.model.variant.cycle.description": "Cambiar al siguiente nivel de esfuerzo", + "command.permissions.autoaccept.enable": "Aceptar ediciones automáticamente", + "command.permissions.autoaccept.disable": "Dejar de aceptar ediciones automáticamente", + "command.session.undo": "Deshacer", + "command.session.undo.description": "Deshacer el último mensaje", + "command.session.redo": "Rehacer", + "command.session.redo.description": "Rehacer el último mensaje deshecho", + "command.session.compact": "Compactar sesión", + "command.session.compact.description": "Resumir la sesión para reducir el tamaño del contexto", + "command.session.fork": "Bifurcar desde mensaje", + "command.session.fork.description": "Crear una nueva sesión desde un mensaje anterior", + "command.session.share": "Compartir sesión", + "command.session.share.description": "Compartir esta sesión y copiar la URL al portapapeles", + "command.session.unshare": "Dejar de compartir sesión", + "command.session.unshare.description": "Dejar de compartir esta sesión", + + "palette.search.placeholder": "Buscar archivos y comandos", + "palette.empty": "No se encontraron resultados", + "palette.group.commands": "Comandos", + "palette.group.files": "Archivos", + + "dialog.provider.search.placeholder": "Buscar proveedores", + "dialog.provider.empty": "No se encontraron proveedores", + "dialog.provider.group.popular": "Popular", + "dialog.provider.group.other": "Otro", + "dialog.provider.tag.recommended": "Recomendado", + "dialog.provider.anthropic.note": "Conectar con Claude Pro/Max o clave API", + + "dialog.model.select.title": "Seleccionar modelo", + "dialog.model.search.placeholder": "Buscar modelos", + "dialog.model.empty": "Sin resultados de modelos", + "dialog.model.manage": "Gestionar modelos", + "dialog.model.manage.description": "Personalizar qué modelos aparecen en el selector de modelos.", + + "dialog.model.unpaid.freeModels.title": "Modelos gratuitos proporcionados por OpenCode", + "dialog.model.unpaid.addMore.title": "Añadir más modelos de proveedores populares", + + "dialog.provider.viewAll": "Ver todos los proveedores", + + "provider.connect.title": "Conectar {{provider}}", + "provider.connect.title.anthropicProMax": "Iniciar sesión con Claude Pro/Max", + "provider.connect.selectMethod": "Seleccionar método de inicio de sesión para {{provider}}.", + "provider.connect.method.apiKey": "Clave API", + "provider.connect.status.inProgress": "Autorización en progreso...", + "provider.connect.status.waiting": "Esperando autorización...", + "provider.connect.status.failed": "Autorización fallida: {{error}}", + "provider.connect.apiKey.description": + "Introduce tu clave API de {{provider}} para conectar tu cuenta y usar modelos de {{provider}} en OpenCode.", + "provider.connect.apiKey.label": "Clave API de {{provider}}", + "provider.connect.apiKey.placeholder": "Clave API", + "provider.connect.apiKey.required": "La clave API es obligatoria", + "provider.connect.opencodeZen.line1": + "OpenCode Zen te da acceso a un conjunto curado de modelos fiables optimizados para agentes de programación.", + "provider.connect.opencodeZen.line2": + "Con una sola clave API obtendrás acceso a modelos como Claude, GPT, Gemini, GLM y más.", + "provider.connect.opencodeZen.visit.prefix": "Visita ", + "provider.connect.opencodeZen.visit.suffix": " para obtener tu clave API.", + "provider.connect.oauth.code.visit.prefix": "Visita ", + "provider.connect.oauth.code.visit.link": "este enlace", + "provider.connect.oauth.code.visit.suffix": + " para obtener tu código de autorización para conectar tu cuenta y usar modelos de {{provider}} en OpenCode.", + "provider.connect.oauth.code.label": "Código de autorización {{method}}", + "provider.connect.oauth.code.placeholder": "Código de autorización", + "provider.connect.oauth.code.required": "El código de autorización es obligatorio", + "provider.connect.oauth.code.invalid": "Código de autorización inválido", + "provider.connect.oauth.auto.visit.prefix": "Visita ", + "provider.connect.oauth.auto.visit.link": "este enlace", + "provider.connect.oauth.auto.visit.suffix": + " e introduce el código a continuación para conectar tu cuenta y usar modelos de {{provider}} en OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Código de confirmación", + "provider.connect.toast.connected.title": "{{provider}} conectado", + "provider.connect.toast.connected.description": "Los modelos de {{provider}} ahora están disponibles para usar.", + + "model.tag.free": "Gratis", + "model.tag.latest": "Último", + + "common.search.placeholder": "Buscar", + "common.loading": "Cargando", + "common.cancel": "Cancelar", + "common.submit": "Enviar", + "common.save": "Guardar", + "common.saving": "Guardando...", + "common.default": "Predeterminado", + "common.attachment": "adjunto", + + "prompt.placeholder.shell": "Introduce comando de shell...", + "prompt.placeholder.normal": 'Pregunta cualquier cosa... "{{example}}"', + "prompt.mode.shell": "Shell", + "prompt.mode.shell.exit": "esc para salir", + + "prompt.example.1": "Arreglar un TODO en el código", + "prompt.example.2": "¿Cuál es el stack tecnológico de este proyecto?", + "prompt.example.3": "Arreglar pruebas rotas", + "prompt.example.4": "Explicar cómo funciona la autenticación", + "prompt.example.5": "Encontrar y arreglar vulnerabilidades de seguridad", + "prompt.example.6": "Añadir pruebas unitarias para el servicio de usuario", + "prompt.example.7": "Refactorizar esta función para que sea más legible", + "prompt.example.8": "¿Qué significa este error?", + "prompt.example.9": "Ayúdame a depurar este problema", + "prompt.example.10": "Generar documentación de API", + "prompt.example.11": "Optimizar consultas a la base de datos", + "prompt.example.12": "Añadir validación de entrada", + "prompt.example.13": "Crear un nuevo componente para...", + "prompt.example.14": "¿Cómo despliego este proyecto?", + "prompt.example.15": "Revisar mi código para mejores prácticas", + "prompt.example.16": "Añadir manejo de errores a esta función", + "prompt.example.17": "Explicar este patrón de regex", + "prompt.example.18": "Convertir esto a TypeScript", + "prompt.example.19": "Añadir logging en todo el código", + "prompt.example.20": "¿Qué dependencias están desactualizadas?", + "prompt.example.21": "Ayúdame a escribir un script de migración", + "prompt.example.22": "Implementar caché para este endpoint", + "prompt.example.23": "Añadir paginación a esta lista", + "prompt.example.24": "Crear un comando CLI para...", + "prompt.example.25": "¿Cómo funcionan las variables de entorno aquí?", + + "prompt.popover.emptyResults": "Sin resultados coincidentes", + "prompt.popover.emptyCommands": "Sin comandos coincidentes", + "prompt.dropzone.label": "Suelta imágenes o PDFs aquí", + "prompt.slash.badge.custom": "personalizado", + "prompt.context.active": "activo", + "prompt.context.includeActiveFile": "Incluir archivo activo", + "prompt.action.attachFile": "Adjuntar archivo", + "prompt.action.send": "Enviar", + "prompt.action.stop": "Detener", + + "prompt.toast.pasteUnsupported.title": "Pegado no soportado", + "prompt.toast.pasteUnsupported.description": "Solo se pueden pegar imágenes o PDFs aquí.", + "prompt.toast.modelAgentRequired.title": "Selecciona un agente y modelo", + "prompt.toast.modelAgentRequired.description": "Elige un agente y modelo antes de enviar un prompt.", + "prompt.toast.worktreeCreateFailed.title": "Fallo al crear el árbol de trabajo", + "prompt.toast.sessionCreateFailed.title": "Fallo al crear la sesión", + "prompt.toast.shellSendFailed.title": "Fallo al enviar comando de shell", + "prompt.toast.commandSendFailed.title": "Fallo al enviar comando", + "prompt.toast.promptSendFailed.title": "Fallo al enviar prompt", + + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} de {{total}} habilitados", + "dialog.mcp.empty": "No hay MCPs configurados", + + "mcp.status.connected": "conectado", + "mcp.status.failed": "fallido", + "mcp.status.needs_auth": "necesita auth", + "mcp.status.disabled": "deshabilitado", + + "dialog.fork.empty": "No hay mensajes desde donde bifurcar", + + "dialog.directory.search.placeholder": "Buscar carpetas", + "dialog.directory.empty": "No se encontraron carpetas", + + "dialog.server.title": "Servidores", + "dialog.server.description": "Cambiar a qué servidor de OpenCode se conecta esta app.", + "dialog.server.search.placeholder": "Buscar servidores", + "dialog.server.empty": "No hay servidores aún", + "dialog.server.add.title": "Añadir un servidor", + "dialog.server.add.url": "URL del servidor", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "No se pudo conectar al servidor", + "dialog.server.add.checking": "Comprobando...", + "dialog.server.add.button": "Añadir", + "dialog.server.default.title": "Servidor predeterminado", + "dialog.server.default.description": + "Conectar a este servidor al iniciar la app en lugar de iniciar un servidor local. Requiere reinicio.", + "dialog.server.default.none": "Ningún servidor seleccionado", + "dialog.server.default.set": "Establecer servidor actual como predeterminado", + "dialog.server.default.clear": "Limpiar", + + "dialog.project.edit.title": "Editar proyecto", + "dialog.project.edit.name": "Nombre", + "dialog.project.edit.icon": "Icono", + "dialog.project.edit.icon.alt": "Icono del proyecto", + "dialog.project.edit.icon.hint": "Haz clic o arrastra una imagen", + "dialog.project.edit.icon.recommended": "Recomendado: 128x128px", + "dialog.project.edit.color": "Color", + + "context.breakdown.title": "Desglose de Contexto", + "context.breakdown.note": + 'Desglose aproximado de tokens de entrada. "Otro" incluye definiciones de herramientas y sobrecarga.', + "context.breakdown.system": "Sistema", + "context.breakdown.user": "Usuario", + "context.breakdown.assistant": "Asistente", + "context.breakdown.tool": "Llamadas a herramientas", + "context.breakdown.other": "Otro", + + "context.systemPrompt.title": "Prompt del Sistema", + "context.rawMessages.title": "Mensajes en bruto", + + "context.stats.session": "Sesión", + "context.stats.messages": "Mensajes", + "context.stats.provider": "Proveedor", + "context.stats.model": "Modelo", + "context.stats.limit": "Límite de Contexto", + "context.stats.totalTokens": "Tokens Totales", + "context.stats.usage": "Uso", + "context.stats.inputTokens": "Tokens de Entrada", + "context.stats.outputTokens": "Tokens de Salida", + "context.stats.reasoningTokens": "Tokens de Razonamiento", + "context.stats.cacheTokens": "Tokens de Caché (lectura/escritura)", + "context.stats.userMessages": "Mensajes de Usuario", + "context.stats.assistantMessages": "Mensajes de Asistente", + "context.stats.totalCost": "Costo Total", + "context.stats.sessionCreated": "Sesión Creada", + "context.stats.lastActivity": "Última Actividad", + + "context.usage.tokens": "Tokens", + "context.usage.usage": "Uso", + "context.usage.cost": "Costo", + "context.usage.clickToView": "Haz clic para ver contexto", + + "language.en": "Inglés", + "language.zh": "Chino", + "language.ko": "Coreano", + "language.de": "Alemán", + "language.es": "Español", + "language.fr": "Francés", + "language.ja": "Japonés", + "language.da": "Danés", + + "toast.language.title": "Idioma", + "toast.language.description": "Cambiado a {{language}}", + + "toast.theme.title": "Tema cambiado", + "toast.scheme.title": "Esquema de color", + + "toast.permissions.autoaccept.on.title": "Aceptando ediciones automáticamente", + "toast.permissions.autoaccept.on.description": "Los permisos de edición y escritura serán aprobados automáticamente", + "toast.permissions.autoaccept.off.title": "Se dejó de aceptar ediciones automáticamente", + "toast.permissions.autoaccept.off.description": "Los permisos de edición y escritura requerirán aprobación", + + "toast.model.none.title": "Ningún modelo seleccionado", + "toast.model.none.description": "Conecta un proveedor para resumir esta sesión", + + "toast.file.loadFailed.title": "Fallo al cargar archivo", + + "toast.session.share.copyFailed.title": "Fallo al copiar URL al portapapeles", + "toast.session.share.success.title": "Sesión compartida", + "toast.session.share.success.description": "¡URL compartida copiada al portapapeles!", + "toast.session.share.failed.title": "Fallo al compartir sesión", + "toast.session.share.failed.description": "Ocurrió un error al compartir la sesión", + + "toast.session.unshare.success.title": "Sesión dejó de compartirse", + "toast.session.unshare.success.description": "¡La sesión dejó de compartirse exitosamente!", + "toast.session.unshare.failed.title": "Fallo al dejar de compartir sesión", + "toast.session.unshare.failed.description": "Ocurrió un error al dejar de compartir la sesión", + + "toast.session.listFailed.title": "Fallo al cargar sesiones para {{project}}", + + "toast.update.title": "Actualización disponible", + "toast.update.description": "Una nueva versión de OpenCode ({{version}}) está disponible para instalar.", + "toast.update.action.installRestart": "Instalar y reiniciar", + "toast.update.action.notYet": "Todavía no", + + "error.page.title": "Algo salió mal", + "error.page.description": "Ocurrió un error al cargar la aplicación.", + "error.page.details.label": "Detalles del error", + "error.page.action.restart": "Reiniciar", + "error.page.action.checking": "Comprobando...", + "error.page.action.checkUpdates": "Buscar actualizaciones", + "error.page.action.updateTo": "Actualizar a {{version}}", + "error.page.report.prefix": "Por favor reporta este error al equipo de OpenCode", + "error.page.report.discord": "en Discord", + "error.page.version": "Versión: {{version}}", + + "error.dev.rootNotFound": + "Elemento raíz no encontrado. ¿Olvidaste añadirlo a tu index.html? ¿O tal vez el atributo id está mal escrito?", + + "error.globalSync.connectFailed": "No se pudo conectar al servidor. ¿Hay un servidor ejecutándose en `{{url}}`?", + + "error.chain.unknown": "Error desconocido", + "error.chain.causedBy": "Causado por:", + "error.chain.apiError": "Error de API", + "error.chain.status": "Estado: {{status}}", + "error.chain.retryable": "Reintentable: {{retryable}}", + "error.chain.responseBody": "Cuerpo de la respuesta:\n{{body}}", + "error.chain.didYouMean": "¿Quisiste decir: {{suggestions}}", + "error.chain.modelNotFound": "Modelo no encontrado: {{provider}}/{{model}}", + "error.chain.checkConfig": "Comprueba los nombres de proveedor/modelo en tu configuración (opencode.json)", + "error.chain.mcpFailed": 'El servidor MCP "{{name}}" falló. Nota, OpenCode no soporta autenticación MCP todavía.', + "error.chain.providerAuthFailed": "Autenticación de proveedor fallida ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Fallo al inicializar proveedor "{{provider}}". Comprueba credenciales y configuración.', + "error.chain.configJsonInvalid": "El archivo de configuración en {{path}} no es un JSON(C) válido", + "error.chain.configJsonInvalidWithMessage": + "El archivo de configuración en {{path}} no es un JSON(C) válido: {{message}}", + "error.chain.configDirectoryTypo": + 'El directorio "{{dir}}" en {{path}} no es válido. Renombra el directorio a "{{suggestion}}" o elimínalo. Esto es un error tipográfico común.', + "error.chain.configFrontmatterError": "Fallo al analizar frontmatter en {{path}}:\n{{message}}", + "error.chain.configInvalid": "El archivo de configuración en {{path}} es inválido", + "error.chain.configInvalidWithMessage": "El archivo de configuración en {{path}} es inválido: {{message}}", + + "notification.permission.title": "Permiso requerido", + "notification.permission.description": "{{sessionTitle}} en {{projectName}} necesita permiso", + "notification.question.title": "Pregunta", + "notification.question.description": "{{sessionTitle}} en {{projectName}} tiene una pregunta", + "notification.action.goToSession": "Ir a sesión", + + "notification.session.responseReady.title": "Respuesta lista", + "notification.session.error.title": "Error de sesión", + "notification.session.error.fallbackDescription": "Ocurrió un error", + + "home.recentProjects": "Proyectos recientes", + "home.empty.title": "Sin proyectos recientes", + "home.empty.description": "Empieza abriendo un proyecto local", + + "session.tab.session": "Sesión", + "session.tab.review": "Revisión", + "session.tab.context": "Contexto", + "session.review.filesChanged": "{{count}} Archivos Cambiados", + "session.review.loadingChanges": "Cargando cambios...", + "session.review.empty": "No hay cambios en esta sesión aún", + "session.messages.renderEarlier": "Renderizar mensajes anteriores", + "session.messages.loadingEarlier": "Cargando mensajes anteriores...", + "session.messages.loadEarlier": "Cargar mensajes anteriores", + "session.messages.loading": "Cargando mensajes...", + + "session.context.addToContext": "Añadir {{selection}} al contexto", + + "session.new.worktree.main": "Rama principal", + "session.new.worktree.mainWithBranch": "Rama principal ({{branch}})", + "session.new.worktree.create": "Crear nuevo árbol de trabajo", + "session.new.lastModified": "Última modificación", + + "session.header.search.placeholder": "Buscar {{project}}", + + "session.share.popover.title": "Publicar en web", + "session.share.popover.description.shared": + "Esta sesión es pública en la web. Es accesible para cualquiera con el enlace.", + "session.share.popover.description.unshared": + "Compartir sesión públicamente en la web. Será accesible para cualquiera con el enlace.", + "session.share.action.share": "Compartir", + "session.share.action.publish": "Publicar", + "session.share.action.publishing": "Publicando...", + "session.share.action.unpublish": "Despublicar", + "session.share.action.unpublishing": "Despublicando...", + "session.share.action.view": "Ver", + "session.share.copy.copied": "Copiado", + "session.share.copy.copyLink": "Copiar enlace", + + "lsp.tooltip.none": "Sin servidores LSP", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "Cargando prompt...", + "terminal.loading": "Cargando terminal...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + + "common.closeTab": "Cerrar pestaña", + "common.dismiss": "Descartar", + "common.requestFailed": "Solicitud fallida", + "common.moreOptions": "Más opciones", + "common.learnMore": "Saber más", + "common.rename": "Renombrar", + "common.reset": "Restablecer", + "common.delete": "Eliminar", + "common.close": "Cerrar", + "common.edit": "Editar", + "common.loadMore": "Cargar más", + + "sidebar.settings": "Ajustes", + "sidebar.help": "Ayuda", + "sidebar.workspaces.enable": "Habilitar espacios de trabajo", + "sidebar.workspaces.disable": "Deshabilitar espacios de trabajo", + "sidebar.gettingStarted.title": "Empezando", + "sidebar.gettingStarted.line1": "OpenCode incluye modelos gratuitos para que puedas empezar inmediatamente.", + "sidebar.gettingStarted.line2": "Conecta cualquier proveedor para usar modelos, inc. Claude, GPT, Gemini etc.", + "sidebar.project.recentSessions": "Sesiones recientes", + "sidebar.project.viewAllSessions": "Ver todas las sesiones", + + "settings.section.desktop": "Escritorio", + "settings.tab.general": "General", + "settings.tab.shortcuts": "Atajos", + + "settings.general.section.appearance": "Apariencia", + "settings.general.section.notifications": "Notificaciones del sistema", + "settings.general.section.sounds": "Efectos de sonido", + + "settings.general.row.language.title": "Idioma", + "settings.general.row.language.description": "Cambiar el idioma de visualización para OpenCode", + "settings.general.row.appearance.title": "Apariencia", + "settings.general.row.appearance.description": "Personaliza cómo se ve OpenCode en tu dispositivo", + "settings.general.row.theme.title": "Tema", + "settings.general.row.theme.description": "Personaliza el tema de OpenCode.", + "settings.general.row.font.title": "Fuente", + "settings.general.row.font.description": "Personaliza la fuente mono usada en bloques de código", + + "settings.general.notifications.agent.title": "Agente", + "settings.general.notifications.agent.description": + "Mostrar notificación del sistema cuando el agente termine o necesite atención", + "settings.general.notifications.permissions.title": "Permisos", + "settings.general.notifications.permissions.description": + "Mostrar notificación del sistema cuando se requiera un permiso", + "settings.general.notifications.errors.title": "Errores", + "settings.general.notifications.errors.description": "Mostrar notificación del sistema cuando ocurra un error", + + "settings.general.sounds.agent.title": "Agente", + "settings.general.sounds.agent.description": "Reproducir sonido cuando el agente termine o necesite atención", + "settings.general.sounds.permissions.title": "Permisos", + "settings.general.sounds.permissions.description": "Reproducir sonido cuando se requiera un permiso", + "settings.general.sounds.errors.title": "Errores", + "settings.general.sounds.errors.description": "Reproducir sonido cuando ocurra un error", + + "settings.shortcuts.title": "Atajos de teclado", + "settings.shortcuts.reset.button": "Restablecer a valores predeterminados", + "settings.shortcuts.reset.toast.title": "Atajos restablecidos", + "settings.shortcuts.reset.toast.description": + "Los atajos de teclado han sido restablecidos a los valores predeterminados.", + "settings.shortcuts.conflict.title": "Atajo ya en uso", + "settings.shortcuts.conflict.description": "{{keybind}} ya está asignado a {{titles}}.", + "settings.shortcuts.unassigned": "Sin asignar", + "settings.shortcuts.pressKeys": "Presiona teclas", + "settings.shortcuts.search.placeholder": "Buscar atajos", + "settings.shortcuts.search.empty": "No se encontraron atajos", + + "settings.shortcuts.group.general": "General", + "settings.shortcuts.group.session": "Sesión", + "settings.shortcuts.group.navigation": "Navegación", + "settings.shortcuts.group.modelAndAgent": "Modelo y agente", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Prompt", + + "settings.providers.title": "Proveedores", + "settings.providers.description": "La configuración de proveedores estará disponible aquí.", + "settings.models.title": "Modelos", + "settings.models.description": "La configuración de modelos estará disponible aquí.", + "settings.agents.title": "Agentes", + "settings.agents.description": "La configuración de agentes estará disponible aquí.", + "settings.commands.title": "Comandos", + "settings.commands.description": "La configuración de comandos estará disponible aquí.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "La configuración de MCP estará disponible aquí.", + + "settings.permissions.title": "Permisos", + "settings.permissions.description": "Controla qué herramientas puede usar el servidor por defecto.", + "settings.permissions.section.tools": "Herramientas", + "settings.permissions.toast.updateFailed.title": "Fallo al actualizar permisos", + + "settings.permissions.action.allow": "Permitir", + "settings.permissions.action.ask": "Preguntar", + "settings.permissions.action.deny": "Denegar", + + "settings.permissions.tool.read.title": "Leer", + "settings.permissions.tool.read.description": "Leer un archivo (coincide con la ruta del archivo)", + "settings.permissions.tool.edit.title": "Editar", + "settings.permissions.tool.edit.description": + "Modificar archivos, incluyendo ediciones, escrituras, parches y multi-ediciones", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Coincidir archivos usando patrones glob", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Buscar contenidos de archivo usando expresiones regulares", + "settings.permissions.tool.list.title": "Listar", + "settings.permissions.tool.list.description": "Listar archivos dentro de un directorio", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Ejecutar comandos de shell", + "settings.permissions.tool.task.title": "Tarea", + "settings.permissions.tool.task.description": "Lanzar sub-agentes", + "settings.permissions.tool.skill.title": "Habilidad", + "settings.permissions.tool.skill.description": "Cargar una habilidad por nombre", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Ejecutar consultas de servidor de lenguaje", + "settings.permissions.tool.todoread.title": "Leer Todo", + "settings.permissions.tool.todoread.description": "Leer la lista de tareas", + "settings.permissions.tool.todowrite.title": "Escribir Todo", + "settings.permissions.tool.todowrite.description": "Actualizar la lista de tareas", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "Obtener contenido de una URL", + "settings.permissions.tool.websearch.title": "Búsqueda Web", + "settings.permissions.tool.websearch.description": "Buscar en la web", + "settings.permissions.tool.codesearch.title": "Búsqueda de Código", + "settings.permissions.tool.codesearch.description": "Buscar código en la web", + "settings.permissions.tool.external_directory.title": "Directorio Externo", + "settings.permissions.tool.external_directory.description": "Acceder a archivos fuera del directorio del proyecto", + "settings.permissions.tool.doom_loop.title": "Bucle Infinito", + "settings.permissions.tool.doom_loop.description": "Detectar llamadas a herramientas repetidas con entrada idéntica", + + "workspace.new": "Nuevo espacio de trabajo", + "workspace.type.local": "local", + "workspace.type.sandbox": "sandbox", + "workspace.create.failed.title": "Fallo al crear espacio de trabajo", + "workspace.delete.failed.title": "Fallo al eliminar espacio de trabajo", + "workspace.resetting.title": "Restableciendo espacio de trabajo", + "workspace.resetting.description": "Esto puede tomar un minuto.", + "workspace.reset.failed.title": "Fallo al restablecer espacio de trabajo", + "workspace.reset.success.title": "Espacio de trabajo restablecido", + "workspace.reset.success.description": "El espacio de trabajo ahora coincide con la rama predeterminada.", + "workspace.status.checking": "Comprobando cambios no fusionados...", + "workspace.status.error": "No se pudo verificar el estado de git.", + "workspace.status.clean": "No se detectaron cambios no fusionados.", + "workspace.status.dirty": "Cambios no fusionados detectados en este espacio de trabajo.", + "workspace.delete.title": "Eliminar espacio de trabajo", + "workspace.delete.confirm": '¿Eliminar espacio de trabajo "{{name}}"?', + "workspace.delete.button": "Eliminar espacio de trabajo", + "workspace.reset.title": "Restablecer espacio de trabajo", + "workspace.reset.confirm": '¿Restablecer espacio de trabajo "{{name}}"?', + "workspace.reset.button": "Restablecer espacio de trabajo", + "workspace.reset.archived.none": "No se archivarán sesiones activas.", + "workspace.reset.archived.one": "1 sesión será archivada.", + "workspace.reset.archived.many": "{{count}} sesiones serán archivadas.", + "workspace.reset.note": "Esto restablecerá el espacio de trabajo para coincidir con la rama predeterminada.", +} diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts new file mode 100644 index 00000000000..c0014210001 --- /dev/null +++ b/packages/app/src/i18n/fr.ts @@ -0,0 +1,567 @@ +export const dict = { + "command.category.suggested": "Suggéré", + "command.category.view": "Affichage", + "command.category.project": "Projet", + "command.category.provider": "Fournisseur", + "command.category.server": "Serveur", + "command.category.session": "Session", + "command.category.theme": "Thème", + "command.category.language": "Langue", + "command.category.file": "Fichier", + "command.category.terminal": "Terminal", + "command.category.model": "Modèle", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Permissions", + "command.category.workspace": "Espace de travail", + + "theme.scheme.system": "Système", + "theme.scheme.light": "Clair", + "theme.scheme.dark": "Sombre", + + "command.sidebar.toggle": "Basculer la barre latérale", + "command.project.open": "Ouvrir un projet", + "command.provider.connect": "Connecter un fournisseur", + "command.server.switch": "Changer de serveur", + "command.session.previous": "Session précédente", + "command.session.next": "Session suivante", + "command.session.archive": "Archiver la session", + + "command.palette": "Palette de commandes", + + "command.theme.cycle": "Changer de thème", + "command.theme.set": "Utiliser le thème : {{theme}}", + "command.theme.scheme.cycle": "Changer de schéma de couleurs", + "command.theme.scheme.set": "Utiliser le schéma de couleurs : {{scheme}}", + + "command.language.cycle": "Changer de langue", + "command.language.set": "Utiliser la langue : {{language}}", + + "command.session.new": "Nouvelle session", + "command.file.open": "Ouvrir un fichier", + "command.file.open.description": "Rechercher des fichiers et des commandes", + "command.terminal.toggle": "Basculer le terminal", + "command.review.toggle": "Basculer la revue", + "command.terminal.new": "Nouveau terminal", + "command.terminal.new.description": "Créer un nouvel onglet de terminal", + "command.steps.toggle": "Basculer les étapes", + "command.steps.toggle.description": "Afficher ou masquer les étapes du message actuel", + "command.message.previous": "Message précédent", + "command.message.previous.description": "Aller au message utilisateur précédent", + "command.message.next": "Message suivant", + "command.message.next.description": "Aller au message utilisateur suivant", + "command.model.choose": "Choisir le modèle", + "command.model.choose.description": "Sélectionner un modèle différent", + "command.mcp.toggle": "Basculer MCP", + "command.mcp.toggle.description": "Basculer les MCPs", + "command.agent.cycle": "Changer d'agent", + "command.agent.cycle.description": "Passer à l'agent suivant", + "command.agent.cycle.reverse": "Changer d'agent (inverse)", + "command.agent.cycle.reverse.description": "Passer à l'agent précédent", + "command.model.variant.cycle": "Changer l'effort de réflexion", + "command.model.variant.cycle.description": "Passer au niveau d'effort suivant", + "command.permissions.autoaccept.enable": "Accepter automatiquement les modifications", + "command.permissions.autoaccept.disable": "Arrêter l'acceptation automatique des modifications", + "command.session.undo": "Annuler", + "command.session.undo.description": "Annuler le dernier message", + "command.session.redo": "Rétablir", + "command.session.redo.description": "Rétablir le dernier message annulé", + "command.session.compact": "Compacter la session", + "command.session.compact.description": "Résumer la session pour réduire la taille du contexte", + "command.session.fork": "Bifurquer à partir du message", + "command.session.fork.description": "Créer une nouvelle session à partir d'un message précédent", + "command.session.share": "Partager la session", + "command.session.share.description": "Partager cette session et copier l'URL dans le presse-papiers", + "command.session.unshare": "Ne plus partager la session", + "command.session.unshare.description": "Arrêter de partager cette session", + + "palette.search.placeholder": "Rechercher des fichiers et des commandes", + "palette.empty": "Aucun résultat trouvé", + "palette.group.commands": "Commandes", + "palette.group.files": "Fichiers", + + "dialog.provider.search.placeholder": "Rechercher des fournisseurs", + "dialog.provider.empty": "Aucun fournisseur trouvé", + "dialog.provider.group.popular": "Populaire", + "dialog.provider.group.other": "Autre", + "dialog.provider.tag.recommended": "Recommandé", + "dialog.provider.anthropic.note": "Connectez-vous avec Claude Pro/Max ou une clé API", + + "dialog.model.select.title": "Sélectionner un modèle", + "dialog.model.search.placeholder": "Rechercher des modèles", + "dialog.model.empty": "Aucun résultat de modèle", + "dialog.model.manage": "Gérer les modèles", + "dialog.model.manage.description": "Personnalisez les modèles qui apparaissent dans le sélecteur.", + + "dialog.model.unpaid.freeModels.title": "Modèles gratuits fournis par OpenCode", + "dialog.model.unpaid.addMore.title": "Ajouter plus de modèles de fournisseurs populaires", + + "dialog.provider.viewAll": "Voir tous les fournisseurs", + + "provider.connect.title": "Connecter {{provider}}", + "provider.connect.title.anthropicProMax": "Connexion avec Claude Pro/Max", + "provider.connect.selectMethod": "Sélectionnez la méthode de connexion pour {{provider}}.", + "provider.connect.method.apiKey": "Clé API", + "provider.connect.status.inProgress": "Autorisation en cours...", + "provider.connect.status.waiting": "En attente d'autorisation...", + "provider.connect.status.failed": "Échec de l'autorisation : {{error}}", + "provider.connect.apiKey.description": + "Entrez votre clé API {{provider}} pour connecter votre compte et utiliser les modèles {{provider}} dans OpenCode.", + "provider.connect.apiKey.label": "Clé API {{provider}}", + "provider.connect.apiKey.placeholder": "Clé API", + "provider.connect.apiKey.required": "La clé API est requise", + "provider.connect.opencodeZen.line1": + "OpenCode Zen vous donne accès à un ensemble sélectionné de modèles fiables et optimisés pour les agents de codage.", + "provider.connect.opencodeZen.line2": + "Avec une seule clé API, vous aurez accès à des modèles tels que Claude, GPT, Gemini, GLM et plus encore.", + "provider.connect.opencodeZen.visit.prefix": "Visitez ", + "provider.connect.opencodeZen.visit.suffix": " pour récupérer votre clé API.", + "provider.connect.oauth.code.visit.prefix": "Visitez ", + "provider.connect.oauth.code.visit.link": "ce lien", + "provider.connect.oauth.code.visit.suffix": + " pour récupérer votre code d'autorisation afin de connecter votre compte et utiliser les modèles {{provider}} dans OpenCode.", + "provider.connect.oauth.code.label": "Code d'autorisation {{method}}", + "provider.connect.oauth.code.placeholder": "Code d'autorisation", + "provider.connect.oauth.code.required": "Le code d'autorisation est requis", + "provider.connect.oauth.code.invalid": "Code d'autorisation invalide", + "provider.connect.oauth.auto.visit.prefix": "Visitez ", + "provider.connect.oauth.auto.visit.link": "ce lien", + "provider.connect.oauth.auto.visit.suffix": + " et entrez le code ci-dessous pour connecter votre compte et utiliser les modèles {{provider}} dans OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Code de confirmation", + "provider.connect.toast.connected.title": "{{provider}} connecté", + "provider.connect.toast.connected.description": "Les modèles {{provider}} sont maintenant disponibles.", + + "model.tag.free": "Gratuit", + "model.tag.latest": "Dernier", + + "common.search.placeholder": "Rechercher", + "common.loading": "Chargement", + "common.cancel": "Annuler", + "common.submit": "Soumettre", + "common.save": "Enregistrer", + "common.saving": "Enregistrement...", + "common.default": "Défaut", + "common.attachment": "pièce jointe", + + "prompt.placeholder.shell": "Entrez une commande shell...", + "prompt.placeholder.normal": 'Demandez n\'importe quoi... "{{example}}"', + "prompt.mode.shell": "Shell", + "prompt.mode.shell.exit": "esc pour quitter", + + "prompt.example.1": "Corriger un TODO dans la base de code", + "prompt.example.2": "Quelle est la pile technique de ce projet ?", + "prompt.example.3": "Réparer les tests échoués", + "prompt.example.4": "Expliquer comment fonctionne l'authentification", + "prompt.example.5": "Trouver et corriger les vulnérabilités de sécurité", + "prompt.example.6": "Ajouter des tests unitaires pour le service utilisateur", + "prompt.example.7": "Refactoriser cette fonction pour être plus lisible", + "prompt.example.8": "Que signifie cette erreur ?", + "prompt.example.9": "Aidez-moi à déboguer ce problème", + "prompt.example.10": "Générer la documentation de l'API", + "prompt.example.11": "Optimiser les requêtes de base de données", + "prompt.example.12": "Ajouter une validation d'entrée", + "prompt.example.13": "Créer un nouveau composant pour...", + "prompt.example.14": "Comment déployer ce projet ?", + "prompt.example.15": "Vérifier mon code pour les meilleures pratiques", + "prompt.example.16": "Ajouter la gestion des erreurs à cette fonction", + "prompt.example.17": "Expliquer ce modèle regex", + "prompt.example.18": "Convertir ceci en TypeScript", + "prompt.example.19": "Ajouter des logs dans toute la base de code", + "prompt.example.20": "Quelles dépendances sont obsolètes ?", + "prompt.example.21": "Aidez-moi à écrire un script de migration", + "prompt.example.22": "Implémenter la mise en cache pour ce point de terminaison", + "prompt.example.23": "Ajouter la pagination à cette liste", + "prompt.example.24": "Créer une commande CLI pour...", + "prompt.example.25": "Comment fonctionnent les variables d'environnement ici ?", + + "prompt.popover.emptyResults": "Aucun résultat correspondant", + "prompt.popover.emptyCommands": "Aucune commande correspondante", + "prompt.dropzone.label": "Déposez des images ou des PDF ici", + "prompt.slash.badge.custom": "personnalisé", + "prompt.context.active": "actif", + "prompt.context.includeActiveFile": "Inclure le fichier actif", + "prompt.action.attachFile": "Joindre un fichier", + "prompt.action.send": "Envoyer", + "prompt.action.stop": "Arrêter", + + "prompt.toast.pasteUnsupported.title": "Collage non supporté", + "prompt.toast.pasteUnsupported.description": "Seules les images ou les PDF peuvent être collés ici.", + "prompt.toast.modelAgentRequired.title": "Sélectionnez un agent et un modèle", + "prompt.toast.modelAgentRequired.description": "Choisissez un agent et un modèle avant d'envoyer un message.", + "prompt.toast.worktreeCreateFailed.title": "Échec de la création de l'arbre de travail", + "prompt.toast.sessionCreateFailed.title": "Échec de la création de la session", + "prompt.toast.shellSendFailed.title": "Échec de l'envoi de la commande shell", + "prompt.toast.commandSendFailed.title": "Échec de l'envoi de la commande", + "prompt.toast.promptSendFailed.title": "Échec de l'envoi du message", + + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} sur {{total}} activés", + "dialog.mcp.empty": "Aucun MCP configuré", + + "mcp.status.connected": "connecté", + "mcp.status.failed": "échoué", + "mcp.status.needs_auth": "nécessite auth", + "mcp.status.disabled": "désactivé", + + "dialog.fork.empty": "Aucun message à partir duquel bifurquer", + + "dialog.directory.search.placeholder": "Rechercher des dossiers", + "dialog.directory.empty": "Aucun dossier trouvé", + + "dialog.server.title": "Serveurs", + "dialog.server.description": "Changez le serveur OpenCode auquel cette application se connecte.", + "dialog.server.search.placeholder": "Rechercher des serveurs", + "dialog.server.empty": "Aucun serveur pour l'instant", + "dialog.server.add.title": "Ajouter un serveur", + "dialog.server.add.url": "URL du serveur", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Impossible de se connecter au serveur", + "dialog.server.add.checking": "Vérification...", + "dialog.server.add.button": "Ajouter", + "dialog.server.default.title": "Serveur par défaut", + "dialog.server.default.description": + "Se connecter à ce serveur au lancement de l'application au lieu de démarrer un serveur local. Nécessite un redémarrage.", + "dialog.server.default.none": "Aucun serveur sélectionné", + "dialog.server.default.set": "Définir le serveur actuel comme défaut", + "dialog.server.default.clear": "Effacer", + + "dialog.project.edit.title": "Modifier le projet", + "dialog.project.edit.name": "Nom", + "dialog.project.edit.icon": "Icône", + "dialog.project.edit.icon.alt": "Icône du projet", + "dialog.project.edit.icon.hint": "Cliquez ou faites glisser une image", + "dialog.project.edit.icon.recommended": "Recommandé : 128x128px", + "dialog.project.edit.color": "Couleur", + + "context.breakdown.title": "Répartition du contexte", + "context.breakdown.note": + "Répartition approximative des jetons d'entrée. \"Autre\" inclut les définitions d'outils et les frais généraux.", + "context.breakdown.system": "Système", + "context.breakdown.user": "Utilisateur", + "context.breakdown.assistant": "Assistant", + "context.breakdown.tool": "Appels d'outils", + "context.breakdown.other": "Autre", + + "context.systemPrompt.title": "Prompt système", + "context.rawMessages.title": "Messages bruts", + + "context.stats.session": "Session", + "context.stats.messages": "Messages", + "context.stats.provider": "Fournisseur", + "context.stats.model": "Modèle", + "context.stats.limit": "Limite de contexte", + "context.stats.totalTokens": "Total des jetons", + "context.stats.usage": "Utilisation", + "context.stats.inputTokens": "Jetons d'entrée", + "context.stats.outputTokens": "Jetons de sortie", + "context.stats.reasoningTokens": "Jetons de raisonnement", + "context.stats.cacheTokens": "Jetons de cache (lecture/écriture)", + "context.stats.userMessages": "Messages utilisateur", + "context.stats.assistantMessages": "Messages assistant", + "context.stats.totalCost": "Coût total", + "context.stats.sessionCreated": "Session créée", + "context.stats.lastActivity": "Dernière activité", + + "context.usage.tokens": "Jetons", + "context.usage.usage": "Utilisation", + "context.usage.cost": "Coût", + "context.usage.clickToView": "Cliquez pour voir le contexte", + + "language.en": "Anglais", + "language.zh": "Chinois", + "language.ko": "Coréen", + "language.de": "Allemand", + "language.es": "Espagnol", + "language.fr": "Français", + "language.ja": "Japonais", + "language.da": "Danois", + + "toast.language.title": "Langue", + "toast.language.description": "Passé à {{language}}", + + "toast.theme.title": "Thème changé", + "toast.scheme.title": "Schéma de couleurs", + + "toast.permissions.autoaccept.on.title": "Acceptation auto des modifications", + "toast.permissions.autoaccept.on.description": + "Les permissions de modification et d'écriture seront automatiquement approuvées", + "toast.permissions.autoaccept.off.title": "Arrêt acceptation auto des modifications", + "toast.permissions.autoaccept.off.description": + "Les permissions de modification et d'écriture nécessiteront une approbation", + + "toast.model.none.title": "Aucun modèle sélectionné", + "toast.model.none.description": "Connectez un fournisseur pour résumer cette session", + + "toast.file.loadFailed.title": "Échec du chargement du fichier", + + "toast.session.share.copyFailed.title": "Échec de la copie de l'URL dans le presse-papiers", + "toast.session.share.success.title": "Session partagée", + "toast.session.share.success.description": "URL de partage copiée dans le presse-papiers !", + "toast.session.share.failed.title": "Échec du partage de la session", + "toast.session.share.failed.description": "Une erreur s'est produite lors du partage de la session", + + "toast.session.unshare.success.title": "Session non partagée", + "toast.session.unshare.success.description": "Session non partagée avec succès !", + "toast.session.unshare.failed.title": "Échec de l'annulation du partage", + "toast.session.unshare.failed.description": "Une erreur s'est produite lors de l'annulation du partage de la session", + + "toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}", + + "toast.update.title": "Mise à jour disponible", + "toast.update.description": + "Une nouvelle version d'OpenCode ({{version}}) est maintenant disponible pour installation.", + "toast.update.action.installRestart": "Installer et redémarrer", + "toast.update.action.notYet": "Pas encore", + + "error.page.title": "Quelque chose s'est mal passé", + "error.page.description": "Une erreur s'est produite lors du chargement de l'application.", + "error.page.details.label": "Détails de l'erreur", + "error.page.action.restart": "Redémarrer", + "error.page.action.checking": "Vérification...", + "error.page.action.checkUpdates": "Vérifier les mises à jour", + "error.page.action.updateTo": "Mettre à jour vers {{version}}", + "error.page.report.prefix": "Veuillez signaler cette erreur à l'équipe OpenCode", + "error.page.report.discord": "sur Discord", + "error.page.version": "Version : {{version}}", + + "error.dev.rootNotFound": + "Élément racine introuvable. Avez-vous oublié de l'ajouter à votre index.html ? Ou peut-être que l'attribut id est mal orthographié ?", + + "error.globalSync.connectFailed": + "Impossible de se connecter au serveur. Y a-t-il un serveur en cours d'exécution à `{{url}}` ?", + + "error.chain.unknown": "Erreur inconnue", + "error.chain.causedBy": "Causé par :", + "error.chain.apiError": "Erreur API", + "error.chain.status": "Statut : {{status}}", + "error.chain.retryable": "Réessayable : {{retryable}}", + "error.chain.responseBody": "Corps de la réponse :\n{{body}}", + "error.chain.didYouMean": "Vouliez-vous dire : {{suggestions}}", + "error.chain.modelNotFound": "Modèle introuvable : {{provider}}/{{model}}", + "error.chain.checkConfig": "Vérifiez votre configuration (opencode.json) pour les noms de fournisseur/modèle", + "error.chain.mcpFailed": + "Le serveur MCP \"{{name}}\" a échoué. Notez qu'OpenCode ne supporte pas encore l'authentification MCP.", + "error.chain.providerAuthFailed": "Échec de l'authentification du fournisseur ({{provider}}) : {{message}}", + "error.chain.providerInitFailed": + 'Échec de l\'initialisation du fournisseur "{{provider}}". Vérifiez les identifiants et la configuration.', + "error.chain.configJsonInvalid": "Le fichier de configuration à {{path}} n'est pas un JSON(C) valide", + "error.chain.configJsonInvalidWithMessage": + "Le fichier de configuration à {{path}} n'est pas un JSON(C) valide : {{message}}", + "error.chain.configDirectoryTypo": + 'Le répertoire "{{dir}}" dans {{path}} n\'est pas valide. Renommez le répertoire en "{{suggestion}}" ou supprimez-le. C\'est une faute de frappe courante.', + "error.chain.configFrontmatterError": "Échec de l'analyse du frontmatter dans {{path}} :\n{{message}}", + "error.chain.configInvalid": "Le fichier de configuration à {{path}} est invalide", + "error.chain.configInvalidWithMessage": "Le fichier de configuration à {{path}} est invalide : {{message}}", + + "notification.permission.title": "Permission requise", + "notification.permission.description": "{{sessionTitle}} dans {{projectName}} a besoin d'une permission", + "notification.question.title": "Question", + "notification.question.description": "{{sessionTitle}} dans {{projectName}} a une question", + "notification.action.goToSession": "Aller à la session", + + "notification.session.responseReady.title": "Réponse prête", + "notification.session.error.title": "Erreur de session", + "notification.session.error.fallbackDescription": "Une erreur s'est produite", + + "home.recentProjects": "Projets récents", + "home.empty.title": "Aucun projet récent", + "home.empty.description": "Commencez par ouvrir un projet local", + + "session.tab.session": "Session", + "session.tab.review": "Revue", + "session.tab.context": "Contexte", + "session.review.filesChanged": "{{count}} fichiers modifiés", + "session.review.loadingChanges": "Chargement des modifications...", + "session.review.empty": "Aucune modification dans cette session pour l'instant", + "session.messages.renderEarlier": "Afficher les messages précédents", + "session.messages.loadingEarlier": "Chargement des messages précédents...", + "session.messages.loadEarlier": "Charger les messages précédents", + "session.messages.loading": "Chargement des messages...", + + "session.context.addToContext": "Ajouter {{selection}} au contexte", + + "session.new.worktree.main": "Branche principale", + "session.new.worktree.mainWithBranch": "Branche principale ({{branch}})", + "session.new.worktree.create": "Créer un nouvel arbre de travail", + "session.new.lastModified": "Dernière modification", + + "session.header.search.placeholder": "Rechercher {{project}}", + + "session.share.popover.title": "Publier sur le web", + "session.share.popover.description.shared": + "Cette session est publique sur le web. Elle est accessible à toute personne disposant du lien.", + "session.share.popover.description.unshared": + "Partager la session publiquement sur le web. Elle sera accessible à toute personne disposant du lien.", + "session.share.action.share": "Partager", + "session.share.action.publish": "Publier", + "session.share.action.publishing": "Publication...", + "session.share.action.unpublish": "Dépublier", + "session.share.action.unpublishing": "Dépublication...", + "session.share.action.view": "Voir", + "session.share.copy.copied": "Copié", + "session.share.copy.copyLink": "Copier le lien", + + "lsp.tooltip.none": "Aucun serveur LSP", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "Chargement du prompt...", + "terminal.loading": "Chargement du terminal...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + + "common.closeTab": "Fermer l'onglet", + "common.dismiss": "Ignorer", + "common.requestFailed": "La demande a échoué", + "common.moreOptions": "Plus d'options", + "common.learnMore": "En savoir plus", + "common.rename": "Renommer", + "common.reset": "Réinitialiser", + "common.delete": "Supprimer", + "common.close": "Fermer", + "common.edit": "Modifier", + "common.loadMore": "Charger plus", + + "sidebar.settings": "Paramètres", + "sidebar.help": "Aide", + "sidebar.workspaces.enable": "Activer les espaces de travail", + "sidebar.workspaces.disable": "Désactiver les espaces de travail", + "sidebar.gettingStarted.title": "Commencer", + "sidebar.gettingStarted.line1": + "OpenCode inclut des modèles gratuits pour que vous puissiez commencer immédiatement.", + "sidebar.gettingStarted.line2": + "Connectez n'importe quel fournisseur pour utiliser des modèles, y compris Claude, GPT, Gemini etc.", + "sidebar.project.recentSessions": "Sessions récentes", + "sidebar.project.viewAllSessions": "Voir toutes les sessions", + + "settings.section.desktop": "Bureau", + "settings.tab.general": "Général", + "settings.tab.shortcuts": "Raccourcis", + + "settings.general.section.appearance": "Apparence", + "settings.general.section.notifications": "Notifications système", + "settings.general.section.sounds": "Effets sonores", + + "settings.general.row.language.title": "Langue", + "settings.general.row.language.description": "Changer la langue d'affichage pour OpenCode", + "settings.general.row.appearance.title": "Apparence", + "settings.general.row.appearance.description": "Personnaliser l'apparence d'OpenCode sur votre appareil", + "settings.general.row.theme.title": "Thème", + "settings.general.row.theme.description": "Personnaliser le thème d'OpenCode.", + "settings.general.row.font.title": "Police", + "settings.general.row.font.description": "Personnaliser la police mono utilisée dans les blocs de code", + + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Afficher une notification système lorsque l'agent a terminé ou nécessite une attention", + "settings.general.notifications.permissions.title": "Permissions", + "settings.general.notifications.permissions.description": + "Afficher une notification système lorsqu'une permission est requise", + "settings.general.notifications.errors.title": "Erreurs", + "settings.general.notifications.errors.description": "Afficher une notification système lorsqu'une erreur se produit", + + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Jouer un son lorsque l'agent a terminé ou nécessite une attention", + "settings.general.sounds.permissions.title": "Permissions", + "settings.general.sounds.permissions.description": "Jouer un son lorsqu'une permission est requise", + "settings.general.sounds.errors.title": "Erreurs", + "settings.general.sounds.errors.description": "Jouer un son lorsqu'une erreur se produit", + + "settings.shortcuts.title": "Raccourcis clavier", + "settings.shortcuts.reset.button": "Rétablir les défauts", + "settings.shortcuts.reset.toast.title": "Raccourcis réinitialisés", + "settings.shortcuts.reset.toast.description": "Les raccourcis clavier ont été réinitialisés aux valeurs par défaut.", + "settings.shortcuts.conflict.title": "Raccourci déjà utilisé", + "settings.shortcuts.conflict.description": "{{keybind}} est déjà assigné à {{titles}}.", + "settings.shortcuts.unassigned": "Non assigné", + "settings.shortcuts.pressKeys": "Appuyez sur les touches", + "settings.shortcuts.search.placeholder": "Rechercher des raccourcis", + "settings.shortcuts.search.empty": "Aucun raccourci trouvé", + + "settings.shortcuts.group.general": "Général", + "settings.shortcuts.group.session": "Session", + "settings.shortcuts.group.navigation": "Navigation", + "settings.shortcuts.group.modelAndAgent": "Modèle et agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Prompt", + + "settings.providers.title": "Fournisseurs", + "settings.providers.description": "Les paramètres des fournisseurs seront configurables ici.", + "settings.models.title": "Modèles", + "settings.models.description": "Les paramètres des modèles seront configurables ici.", + "settings.agents.title": "Agents", + "settings.agents.description": "Les paramètres des agents seront configurables ici.", + "settings.commands.title": "Commandes", + "settings.commands.description": "Les paramètres des commandes seront configurables ici.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "Les paramètres MCP seront configurables ici.", + + "settings.permissions.title": "Permissions", + "settings.permissions.description": "Contrôlez les outils que le serveur peut utiliser par défaut.", + "settings.permissions.section.tools": "Outils", + "settings.permissions.toast.updateFailed.title": "Échec de la mise à jour des permissions", + + "settings.permissions.action.allow": "Autoriser", + "settings.permissions.action.ask": "Demander", + "settings.permissions.action.deny": "Refuser", + + "settings.permissions.tool.read.title": "Lire", + "settings.permissions.tool.read.description": "Lecture d'un fichier (correspond au chemin du fichier)", + "settings.permissions.tool.edit.title": "Modifier", + "settings.permissions.tool.edit.description": + "Modifier des fichiers, y compris les modifications, écritures, patchs et multi-modifications", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Correspondre aux fichiers utilisant des modèles glob", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": + "Rechercher dans le contenu des fichiers à l'aide d'expressions régulières", + "settings.permissions.tool.list.title": "Lister", + "settings.permissions.tool.list.description": "Lister les fichiers dans un répertoire", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Exécuter des commandes shell", + "settings.permissions.tool.task.title": "Tâche", + "settings.permissions.tool.task.description": "Lancer des sous-agents", + "settings.permissions.tool.skill.title": "Compétence", + "settings.permissions.tool.skill.description": "Charger une compétence par son nom", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Exécuter des requêtes de serveur de langage", + "settings.permissions.tool.todoread.title": "Lire Todo", + "settings.permissions.tool.todoread.description": "Lire la liste de tâches", + "settings.permissions.tool.todowrite.title": "Écrire Todo", + "settings.permissions.tool.todowrite.description": "Mettre à jour la liste de tâches", + "settings.permissions.tool.webfetch.title": "Récupération Web", + "settings.permissions.tool.webfetch.description": "Récupérer le contenu d'une URL", + "settings.permissions.tool.websearch.title": "Recherche Web", + "settings.permissions.tool.websearch.description": "Rechercher sur le web", + "settings.permissions.tool.codesearch.title": "Recherche de code", + "settings.permissions.tool.codesearch.description": "Rechercher du code sur le web", + "settings.permissions.tool.external_directory.title": "Répertoire externe", + "settings.permissions.tool.external_directory.description": "Accéder aux fichiers en dehors du répertoire du projet", + "settings.permissions.tool.doom_loop.title": "Boucle infernale", + "settings.permissions.tool.doom_loop.description": "Détecter les appels d'outils répétés avec une entrée identique", + + "workspace.new": "Nouvel espace de travail", + "workspace.type.local": "local", + "workspace.type.sandbox": "bac à sable", + "workspace.create.failed.title": "Échec de la création de l'espace de travail", + "workspace.delete.failed.title": "Échec de la suppression de l'espace de travail", + "workspace.resetting.title": "Réinitialisation de l'espace de travail", + "workspace.resetting.description": "Cela peut prendre une minute.", + "workspace.reset.failed.title": "Échec de la réinitialisation de l'espace de travail", + "workspace.reset.success.title": "Espace de travail réinitialisé", + "workspace.reset.success.description": "L'espace de travail correspond maintenant à la branche par défaut.", + "workspace.status.checking": "Vérification des modifications non fusionnées...", + "workspace.status.error": "Impossible de vérifier le statut git.", + "workspace.status.clean": "Aucune modification non fusionnée détectée.", + "workspace.status.dirty": "Modifications non fusionnées détectées dans cet espace de travail.", + "workspace.delete.title": "Supprimer l'espace de travail", + "workspace.delete.confirm": 'Supprimer l\'espace de travail "{{name}}" ?', + "workspace.delete.button": "Supprimer l'espace de travail", + "workspace.reset.title": "Réinitialiser l'espace de travail", + "workspace.reset.confirm": 'Réinitialiser l\'espace de travail "{{name}}" ?', + "workspace.reset.button": "Réinitialiser l'espace de travail", + "workspace.reset.archived.none": "Aucune session active ne sera archivée.", + "workspace.reset.archived.one": "1 session sera archivée.", + "workspace.reset.archived.many": "{{count}} sessions seront archivées.", + "workspace.reset.note": "Cela réinitialisera l'espace de travail pour correspondre à la branche par défaut.", +} diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts new file mode 100644 index 00000000000..c8448811a82 --- /dev/null +++ b/packages/app/src/i18n/ja.ts @@ -0,0 +1,554 @@ +export const dict = { + "command.category.suggested": "おすすめ", + "command.category.view": "表示", + "command.category.project": "プロジェクト", + "command.category.provider": "プロバイダー", + "command.category.server": "サーバー", + "command.category.session": "セッション", + "command.category.theme": "テーマ", + "command.category.language": "言語", + "command.category.file": "ファイル", + "command.category.terminal": "ターミナル", + "command.category.model": "モデル", + "command.category.mcp": "MCP", + "command.category.agent": "エージェント", + "command.category.permissions": "権限", + "command.category.workspace": "ワークスペース", + + "theme.scheme.system": "システム", + "theme.scheme.light": "ライト", + "theme.scheme.dark": "ダーク", + + "command.sidebar.toggle": "サイドバーの切り替え", + "command.project.open": "プロジェクトを開く", + "command.provider.connect": "プロバイダーに接続", + "command.server.switch": "サーバーの切り替え", + "command.session.previous": "前のセッション", + "command.session.next": "次のセッション", + "command.session.archive": "セッションをアーカイブ", + + "command.palette": "コマンドパレット", + + "command.theme.cycle": "テーマの切り替え", + "command.theme.set": "テーマを使用: {{theme}}", + "command.theme.scheme.cycle": "配色の切り替え", + "command.theme.scheme.set": "配色を使用: {{scheme}}", + + "command.language.cycle": "言語の切り替え", + "command.language.set": "言語を使用: {{language}}", + + "command.session.new": "新しいセッション", + "command.file.open": "ファイルを開く", + "command.file.open.description": "ファイルとコマンドを検索", + "command.terminal.toggle": "ターミナルの切り替え", + "command.review.toggle": "レビューの切り替え", + "command.terminal.new": "新しいターミナル", + "command.terminal.new.description": "新しいターミナルタブを作成", + "command.steps.toggle": "ステップの切り替え", + "command.steps.toggle.description": "現在のメッセージのステップを表示または非表示", + "command.message.previous": "前のメッセージ", + "command.message.previous.description": "前のユーザーメッセージに移動", + "command.message.next": "次のメッセージ", + "command.message.next.description": "次のユーザーメッセージに移動", + "command.model.choose": "モデルを選択", + "command.model.choose.description": "別のモデルを選択", + "command.mcp.toggle": "MCPの切り替え", + "command.mcp.toggle.description": "MCPを切り替える", + "command.agent.cycle": "エージェントの切り替え", + "command.agent.cycle.description": "次のエージェントに切り替え", + "command.agent.cycle.reverse": "エージェントを逆順に切り替え", + "command.agent.cycle.reverse.description": "前のエージェントに切り替え", + "command.model.variant.cycle": "思考レベルの切り替え", + "command.model.variant.cycle.description": "次の思考レベルに切り替え", + "command.permissions.autoaccept.enable": "編集を自動承認", + "command.permissions.autoaccept.disable": "編集の自動承認を停止", + "command.session.undo": "元に戻す", + "command.session.undo.description": "最後のメッセージを元に戻す", + "command.session.redo": "やり直す", + "command.session.redo.description": "元に戻したメッセージをやり直す", + "command.session.compact": "セッションを圧縮", + "command.session.compact.description": "セッションを要約してコンテキストサイズを削減", + "command.session.fork": "メッセージからフォーク", + "command.session.fork.description": "以前のメッセージから新しいセッションを作成", + "command.session.share": "セッションを共有", + "command.session.share.description": "このセッションを共有しURLをクリップボードにコピー", + "command.session.unshare": "セッションの共有を停止", + "command.session.unshare.description": "このセッションの共有を停止", + + "palette.search.placeholder": "ファイルとコマンドを検索", + "palette.empty": "結果が見つかりません", + "palette.group.commands": "コマンド", + "palette.group.files": "ファイル", + + "dialog.provider.search.placeholder": "プロバイダーを検索", + "dialog.provider.empty": "プロバイダーが見つかりません", + "dialog.provider.group.popular": "人気", + "dialog.provider.group.other": "その他", + "dialog.provider.tag.recommended": "推奨", + "dialog.provider.anthropic.note": "Claude Pro/MaxまたはAPIキーで接続", + + "dialog.model.select.title": "モデルを選択", + "dialog.model.search.placeholder": "モデルを検索", + "dialog.model.empty": "モデルが見つかりません", + "dialog.model.manage": "モデルを管理", + "dialog.model.manage.description": "モデルセレクターに表示するモデルをカスタマイズします。", + + "dialog.model.unpaid.freeModels.title": "OpenCodeが提供する無料モデル", + "dialog.model.unpaid.addMore.title": "人気のプロバイダーからモデルを追加", + + "dialog.provider.viewAll": "すべてのプロバイダーを表示", + + "provider.connect.title": "{{provider}}を接続", + "provider.connect.title.anthropicProMax": "Claude Pro/Maxでログイン", + "provider.connect.selectMethod": "{{provider}}のログイン方法を選択してください。", + "provider.connect.method.apiKey": "APIキー", + "provider.connect.status.inProgress": "認証中...", + "provider.connect.status.waiting": "認証を待機中...", + "provider.connect.status.failed": "認証に失敗しました: {{error}}", + "provider.connect.apiKey.description": + "{{provider}}のAPIキーを入力してアカウントを接続し、OpenCodeで{{provider}}モデルを使用します。", + "provider.connect.apiKey.label": "{{provider}} APIキー", + "provider.connect.apiKey.placeholder": "APIキー", + "provider.connect.apiKey.required": "APIキーが必要です", + "provider.connect.opencodeZen.line1": + "OpenCode Zenは、コーディングエージェント向けに最適化された信頼性の高いモデルへのアクセスを提供します。", + "provider.connect.opencodeZen.line2": "1つのAPIキーで、Claude、GPT、Gemini、GLMなどのモデルにアクセスできます。", + "provider.connect.opencodeZen.visit.prefix": " ", + "provider.connect.opencodeZen.visit.suffix": " にアクセスしてAPIキーを取得してください。", + "provider.connect.oauth.code.visit.prefix": " ", + "provider.connect.oauth.code.visit.link": "このリンク", + "provider.connect.oauth.code.visit.suffix": + " にアクセスして認証コードを取得し、アカウントを接続してOpenCodeで{{provider}}モデルを使用してください。", + "provider.connect.oauth.code.label": "{{method}} 認証コード", + "provider.connect.oauth.code.placeholder": "認証コード", + "provider.connect.oauth.code.required": "認証コードが必要です", + "provider.connect.oauth.code.invalid": "無効な認証コード", + "provider.connect.oauth.auto.visit.prefix": " ", + "provider.connect.oauth.auto.visit.link": "このリンク", + "provider.connect.oauth.auto.visit.suffix": + " にアクセスし、以下のコードを入力してアカウントを接続し、OpenCodeで{{provider}}モデルを使用してください。", + "provider.connect.oauth.auto.confirmationCode": "確認コード", + "provider.connect.toast.connected.title": "{{provider}}が接続されました", + "provider.connect.toast.connected.description": "{{provider}}モデルが使用可能になりました。", + + "model.tag.free": "無料", + "model.tag.latest": "最新", + + "common.search.placeholder": "検索", + "common.loading": "読み込み中", + "common.cancel": "キャンセル", + "common.submit": "送信", + "common.save": "保存", + "common.saving": "保存中...", + "common.default": "デフォルト", + "common.attachment": "添付ファイル", + + "prompt.placeholder.shell": "シェルコマンドを入力...", + "prompt.placeholder.normal": '何でも聞いてください... "{{example}}"', + "prompt.mode.shell": "Shell", + "prompt.mode.shell.exit": "escで終了", + + "prompt.example.1": "コードベースのTODOを修正", + "prompt.example.2": "このプロジェクトの技術スタックは何ですか?", + "prompt.example.3": "壊れたテストを修正", + "prompt.example.4": "認証の仕組みを説明して", + "prompt.example.5": "セキュリティの脆弱性を見つけて修正", + "prompt.example.6": "ユーザーサービスのユニットテストを追加", + "prompt.example.7": "この関数を読みやすくリファクタリング", + "prompt.example.8": "このエラーはどういう意味ですか?", + "prompt.example.9": "この問題のデバッグを手伝って", + "prompt.example.10": "APIドキュメントを生成", + "prompt.example.11": "データベースクエリを最適化", + "prompt.example.12": "入力バリデーションを追加", + "prompt.example.13": "〜の新しいコンポーネントを作成", + "prompt.example.14": "このプロジェクトをデプロイするには?", + "prompt.example.15": "ベストプラクティスの観点でコードをレビュー", + "prompt.example.16": "この関数にエラーハンドリングを追加", + "prompt.example.17": "この正規表現パターンを説明して", + "prompt.example.18": "これをTypeScriptに変換", + "prompt.example.19": "コードベース全体にログを追加", + "prompt.example.20": "古い依存関係はどれですか?", + "prompt.example.21": "マイグレーションスクリプトの作成を手伝って", + "prompt.example.22": "このエンドポイントにキャッシュを実装", + "prompt.example.23": "このリストにページネーションを追加", + "prompt.example.24": "〜のCLIコマンドを作成", + "prompt.example.25": "ここでは環境変数はどう機能しますか?", + + "prompt.popover.emptyResults": "一致する結果がありません", + "prompt.popover.emptyCommands": "一致するコマンドがありません", + "prompt.dropzone.label": "画像またはPDFをここにドロップ", + "prompt.slash.badge.custom": "カスタム", + "prompt.context.active": "アクティブ", + "prompt.context.includeActiveFile": "アクティブなファイルを含める", + "prompt.action.attachFile": "ファイルを添付", + "prompt.action.send": "送信", + "prompt.action.stop": "停止", + + "prompt.toast.pasteUnsupported.title": "サポートされていない貼り付け", + "prompt.toast.pasteUnsupported.description": "ここでは画像またはPDFのみ貼り付け可能です。", + "prompt.toast.modelAgentRequired.title": "エージェントとモデルを選択", + "prompt.toast.modelAgentRequired.description": "プロンプトを送信する前にエージェントとモデルを選択してください。", + "prompt.toast.worktreeCreateFailed.title": "ワークツリーの作成に失敗しました", + "prompt.toast.sessionCreateFailed.title": "セッションの作成に失敗しました", + "prompt.toast.shellSendFailed.title": "シェルコマンドの送信に失敗しました", + "prompt.toast.commandSendFailed.title": "コマンドの送信に失敗しました", + "prompt.toast.promptSendFailed.title": "プロンプトの送信に失敗しました", + + "dialog.mcp.title": "MCP", + "dialog.mcp.description": "{{total}}個中{{enabled}}個が有効", + "dialog.mcp.empty": "MCPが設定されていません", + + "mcp.status.connected": "接続済み", + "mcp.status.failed": "失敗", + "mcp.status.needs_auth": "認証が必要", + "mcp.status.disabled": "無効", + + "dialog.fork.empty": "フォーク元のメッセージがありません", + + "dialog.directory.search.placeholder": "フォルダを検索", + "dialog.directory.empty": "フォルダが見つかりません", + + "dialog.server.title": "サーバー", + "dialog.server.description": "このアプリが接続するOpenCodeサーバーを切り替えます。", + "dialog.server.search.placeholder": "サーバーを検索", + "dialog.server.empty": "サーバーはまだありません", + "dialog.server.add.title": "サーバーを追加", + "dialog.server.add.url": "サーバーURL", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "サーバーに接続できませんでした", + "dialog.server.add.checking": "確認中...", + "dialog.server.add.button": "追加", + "dialog.server.default.title": "デフォルトサーバー", + "dialog.server.default.description": + "ローカルサーバーを起動する代わりに、アプリ起動時にこのサーバーに接続します。再起動が必要です。", + "dialog.server.default.none": "サーバーが選択されていません", + "dialog.server.default.set": "現在のサーバーをデフォルトに設定", + "dialog.server.default.clear": "クリア", + + "dialog.project.edit.title": "プロジェクトを編集", + "dialog.project.edit.name": "名前", + "dialog.project.edit.icon": "アイコン", + "dialog.project.edit.icon.alt": "プロジェクトアイコン", + "dialog.project.edit.icon.hint": "クリックまたは画像をドラッグ", + "dialog.project.edit.icon.recommended": "推奨: 128x128px", + "dialog.project.edit.color": "色", + + "context.breakdown.title": "コンテキストの内訳", + "context.breakdown.note": '入力トークンのおおよその内訳です。"その他"にはツールの定義やオーバーヘッドが含まれます。', + "context.breakdown.system": "システム", + "context.breakdown.user": "ユーザー", + "context.breakdown.assistant": "アシスタント", + "context.breakdown.tool": "ツール呼び出し", + "context.breakdown.other": "その他", + + "context.systemPrompt.title": "システムプロンプト", + "context.rawMessages.title": "生のメッセージ", + + "context.stats.session": "セッション", + "context.stats.messages": "メッセージ", + "context.stats.provider": "プロバイダー", + "context.stats.model": "モデル", + "context.stats.limit": "コンテキスト制限", + "context.stats.totalTokens": "総トークン数", + "context.stats.usage": "使用量", + "context.stats.inputTokens": "入力トークン", + "context.stats.outputTokens": "出力トークン", + "context.stats.reasoningTokens": "推論トークン", + "context.stats.cacheTokens": "キャッシュトークン (読込/書込)", + "context.stats.userMessages": "ユーザーメッセージ", + "context.stats.assistantMessages": "アシスタントメッセージ", + "context.stats.totalCost": "総コスト", + "context.stats.sessionCreated": "セッション作成日時", + "context.stats.lastActivity": "最終アクティビティ", + + "context.usage.tokens": "トークン", + "context.usage.usage": "使用量", + "context.usage.cost": "コスト", + "context.usage.clickToView": "クリックしてコンテキストを表示", + + "language.en": "英語", + "language.zh": "中国語", + "language.ko": "韓国語", + "language.de": "ドイツ語", + "language.es": "スペイン語", + "language.fr": "フランス語", + "language.ja": "日本語", + "language.da": "デンマーク語", + + "toast.language.title": "言語", + "toast.language.description": "{{language}}に切り替えました", + + "toast.theme.title": "テーマが切り替わりました", + "toast.scheme.title": "配色", + + "toast.permissions.autoaccept.on.title": "編集を自動承認中", + "toast.permissions.autoaccept.on.description": "編集と書き込みの権限は自動的に承認されます", + "toast.permissions.autoaccept.off.title": "編集の自動承認を停止しました", + "toast.permissions.autoaccept.off.description": "編集と書き込みの権限には承認が必要です", + + "toast.model.none.title": "モデルが選択されていません", + "toast.model.none.description": "このセッションを要約するにはプロバイダーを接続してください", + + "toast.file.loadFailed.title": "ファイルの読み込みに失敗しました", + + "toast.session.share.copyFailed.title": "URLのコピーに失敗しました", + "toast.session.share.success.title": "セッションを共有しました", + "toast.session.share.success.description": "共有URLをクリップボードにコピーしました!", + "toast.session.share.failed.title": "セッションの共有に失敗しました", + "toast.session.share.failed.description": "セッションの共有中にエラーが発生しました", + + "toast.session.unshare.success.title": "セッションの共有を解除しました", + "toast.session.unshare.success.description": "セッションの共有解除に成功しました!", + "toast.session.unshare.failed.title": "セッションの共有解除に失敗しました", + "toast.session.unshare.failed.description": "セッションの共有解除中にエラーが発生しました", + + "toast.session.listFailed.title": "{{project}}のセッション読み込みに失敗しました", + + "toast.update.title": "アップデートが利用可能です", + "toast.update.description": "OpenCodeの新しいバージョン ({{version}}) がインストール可能です。", + "toast.update.action.installRestart": "インストールして再起動", + "toast.update.action.notYet": "今はしない", + + "error.page.title": "問題が発生しました", + "error.page.description": "アプリケーションの読み込み中にエラーが発生しました。", + "error.page.details.label": "エラー詳細", + "error.page.action.restart": "再起動", + "error.page.action.checking": "確認中...", + "error.page.action.checkUpdates": "アップデートを確認", + "error.page.action.updateTo": "{{version}}にアップデート", + "error.page.report.prefix": "このエラーをOpenCodeチームに報告してください: ", + "error.page.report.discord": "Discord", + "error.page.version": "バージョン: {{version}}", + + "error.dev.rootNotFound": + "ルート要素が見つかりません。index.htmlに追加するのを忘れていませんか?またはid属性のスペルが間違っていませんか?", + + "error.globalSync.connectFailed": "サーバーに接続できませんでした。`{{url}}`でサーバーが実行されていますか?", + + "error.chain.unknown": "不明なエラー", + "error.chain.causedBy": "原因:", + "error.chain.apiError": "APIエラー", + "error.chain.status": "ステータス: {{status}}", + "error.chain.retryable": "再試行可能: {{retryable}}", + "error.chain.responseBody": "レスポンス本文:\n{{body}}", + "error.chain.didYouMean": "もしかして: {{suggestions}}", + "error.chain.modelNotFound": "モデルが見つかりません: {{provider}}/{{model}}", + "error.chain.checkConfig": "config (opencode.json) のプロバイダー/モデル名を確認してください", + "error.chain.mcpFailed": 'MCPサーバー "{{name}}" が失敗しました。注意: OpenCodeはまだMCP認証をサポートしていません。', + "error.chain.providerAuthFailed": "プロバイダー認証に失敗しました ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'プロバイダー "{{provider}}" の初期化に失敗しました。認証情報と設定を確認してください。', + "error.chain.configJsonInvalid": "{{path}} の設定ファイルは有効なJSON(C)ではありません", + "error.chain.configJsonInvalidWithMessage": "{{path}} の設定ファイルは有効なJSON(C)ではありません: {{message}}", + "error.chain.configDirectoryTypo": + '{{path}} 内のディレクトリ "{{dir}}" は無効です。"{{suggestion}}" に名前を変更するか削除してください。これはよくあるタイプミスです。', + "error.chain.configFrontmatterError": "{{path}} のフロントマターの解析に失敗しました:\n{{message}}", + "error.chain.configInvalid": "{{path}} の設定ファイルが無効です", + "error.chain.configInvalidWithMessage": "{{path}} の設定ファイルが無効です: {{message}}", + + "notification.permission.title": "権限が必要です", + "notification.permission.description": "{{projectName}} の {{sessionTitle}} が権限を必要としています", + "notification.question.title": "質問", + "notification.question.description": "{{projectName}} の {{sessionTitle}} から質問があります", + "notification.action.goToSession": "セッションへ移動", + + "notification.session.responseReady.title": "応答の準備ができました", + "notification.session.error.title": "セッションエラー", + "notification.session.error.fallbackDescription": "エラーが発生しました", + + "home.recentProjects": "最近のプロジェクト", + "home.empty.title": "最近のプロジェクトはありません", + "home.empty.description": "ローカルプロジェクトを開いて始めましょう", + + "session.tab.session": "セッション", + "session.tab.review": "レビュー", + "session.tab.context": "コンテキスト", + "session.review.filesChanged": "{{count}} ファイル変更", + "session.review.loadingChanges": "変更を読み込み中...", + "session.review.empty": "このセッションでの変更はまだありません", + "session.messages.renderEarlier": "以前のメッセージを表示", + "session.messages.loadingEarlier": "以前のメッセージを読み込み中...", + "session.messages.loadEarlier": "以前のメッセージを読み込む", + "session.messages.loading": "メッセージを読み込み中...", + + "session.context.addToContext": "{{selection}}をコンテキストに追加", + + "session.new.worktree.main": "メインブランチ", + "session.new.worktree.mainWithBranch": "メインブランチ ({{branch}})", + "session.new.worktree.create": "新しいワークツリーを作成", + "session.new.lastModified": "最終更新", + + "session.header.search.placeholder": "{{project}}を検索", + + "session.share.popover.title": "ウェブで公開", + "session.share.popover.description.shared": + "このセッションはウェブで公開されています。リンクを知っている人なら誰でもアクセスできます。", + "session.share.popover.description.unshared": + "セッションをウェブで公開します。リンクを知っている人なら誰でもアクセスできるようになります。", + "session.share.action.share": "共有", + "session.share.action.publish": "公開", + "session.share.action.publishing": "公開中...", + "session.share.action.unpublish": "非公開にする", + "session.share.action.unpublishing": "非公開にしています...", + "session.share.action.view": "表示", + "session.share.copy.copied": "コピーしました", + "session.share.copy.copyLink": "リンクをコピー", + + "lsp.tooltip.none": "LSPサーバーなし", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "プロンプトを読み込み中...", + "terminal.loading": "ターミナルを読み込み中...", + "terminal.title": "ターミナル", + "terminal.title.numbered": "ターミナル {{number}}", + + "common.closeTab": "タブを閉じる", + "common.dismiss": "閉じる", + "common.requestFailed": "リクエスト失敗", + "common.moreOptions": "その他のオプション", + "common.learnMore": "詳細", + "common.rename": "名前変更", + "common.reset": "リセット", + "common.delete": "削除", + "common.close": "閉じる", + "common.edit": "編集", + "common.loadMore": "さらに読み込む", + + "sidebar.settings": "設定", + "sidebar.help": "ヘルプ", + "sidebar.workspaces.enable": "ワークスペースを有効化", + "sidebar.workspaces.disable": "ワークスペースを無効化", + "sidebar.gettingStarted.title": "はじめに", + "sidebar.gettingStarted.line1": "OpenCodeには無料モデルが含まれているため、すぐに開始できます。", + "sidebar.gettingStarted.line2": "プロバイダーを接続して、Claude、GPT、Geminiなどのモデルを使用できます。", + "sidebar.project.recentSessions": "最近のセッション", + "sidebar.project.viewAllSessions": "すべてのセッションを表示", + + "settings.section.desktop": "デスクトップ", + "settings.tab.general": "一般", + "settings.tab.shortcuts": "ショートカット", + + "settings.general.section.appearance": "外観", + "settings.general.section.notifications": "システム通知", + "settings.general.section.sounds": "効果音", + + "settings.general.row.language.title": "言語", + "settings.general.row.language.description": "OpenCodeの表示言語を変更します", + "settings.general.row.appearance.title": "外観", + "settings.general.row.appearance.description": "デバイスでのOpenCodeの表示をカスタマイズします", + "settings.general.row.theme.title": "テーマ", + "settings.general.row.theme.description": "OpenCodeのテーマをカスタマイズします。", + "settings.general.row.font.title": "フォント", + "settings.general.row.font.description": "コードブロックで使用する等幅フォントをカスタマイズします", + + "settings.general.notifications.agent.title": "エージェント", + "settings.general.notifications.agent.description": + "エージェントが完了したか、注意が必要な場合にシステム通知を表示します", + "settings.general.notifications.permissions.title": "権限", + "settings.general.notifications.permissions.description": "権限が必要な場合にシステム通知を表示します", + "settings.general.notifications.errors.title": "エラー", + "settings.general.notifications.errors.description": "エラーが発生した場合にシステム通知を表示します", + + "settings.general.sounds.agent.title": "エージェント", + "settings.general.sounds.agent.description": "エージェントが完了したか、注意が必要な場合に音を再生します", + "settings.general.sounds.permissions.title": "権限", + "settings.general.sounds.permissions.description": "権限が必要な場合に音を再生します", + "settings.general.sounds.errors.title": "エラー", + "settings.general.sounds.errors.description": "エラーが発生した場合に音を再生します", + + "settings.shortcuts.title": "キーボードショートカット", + "settings.shortcuts.reset.button": "デフォルトにリセット", + "settings.shortcuts.reset.toast.title": "ショートカットをリセットしました", + "settings.shortcuts.reset.toast.description": "キーボードショートカットがデフォルトにリセットされました。", + "settings.shortcuts.conflict.title": "ショートカットは既に使用されています", + "settings.shortcuts.conflict.description": "{{keybind}} は既に {{titles}} に割り当てられています。", + "settings.shortcuts.unassigned": "未割り当て", + "settings.shortcuts.pressKeys": "キーを押してください", + "settings.shortcuts.search.placeholder": "ショートカットを検索", + "settings.shortcuts.search.empty": "ショートカットが見つかりません", + + "settings.shortcuts.group.general": "一般", + "settings.shortcuts.group.session": "セッション", + "settings.shortcuts.group.navigation": "ナビゲーション", + "settings.shortcuts.group.modelAndAgent": "モデルとエージェント", + "settings.shortcuts.group.terminal": "ターミナル", + "settings.shortcuts.group.prompt": "プロンプト", + + "settings.providers.title": "プロバイダー", + "settings.providers.description": "プロバイダー設定はここで構成できます。", + "settings.models.title": "モデル", + "settings.models.description": "モデル設定はここで構成できます。", + "settings.agents.title": "エージェント", + "settings.agents.description": "エージェント設定はここで構成できます。", + "settings.commands.title": "コマンド", + "settings.commands.description": "コマンド設定はここで構成できます。", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP設定はここで構成できます。", + + "settings.permissions.title": "権限", + "settings.permissions.description": "サーバーがデフォルトで使用できるツールを制御します。", + "settings.permissions.section.tools": "ツール", + "settings.permissions.toast.updateFailed.title": "権限の更新に失敗しました", + + "settings.permissions.action.allow": "許可", + "settings.permissions.action.ask": "確認", + "settings.permissions.action.deny": "拒否", + + "settings.permissions.tool.read.title": "読み込み", + "settings.permissions.tool.read.description": "ファイルの読み込み (ファイルパスに一致)", + "settings.permissions.tool.edit.title": "編集", + "settings.permissions.tool.edit.description": "ファイルの変更(編集、書き込み、パッチ、複数編集を含む)", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Globパターンを使用したファイルの一致", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "正規表現を使用したファイル内容の検索", + "settings.permissions.tool.list.title": "リスト", + "settings.permissions.tool.list.description": "ディレクトリ内のファイル一覧表示", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "シェルコマンドの実行", + "settings.permissions.tool.task.title": "タスク", + "settings.permissions.tool.task.description": "サブエージェントの起動", + "settings.permissions.tool.skill.title": "スキル", + "settings.permissions.tool.skill.description": "名前によるスキルの読み込み", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "言語サーバークエリの実行", + "settings.permissions.tool.todoread.title": "Todo読み込み", + "settings.permissions.tool.todoread.description": "Todoリストの読み込み", + "settings.permissions.tool.todowrite.title": "Todo書き込み", + "settings.permissions.tool.todowrite.description": "Todoリストの更新", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "URLからコンテンツを取得", + "settings.permissions.tool.websearch.title": "Web Search", + "settings.permissions.tool.websearch.description": "ウェブを検索", + "settings.permissions.tool.codesearch.title": "Code Search", + "settings.permissions.tool.codesearch.description": "ウェブ上のコードを検索", + "settings.permissions.tool.external_directory.title": "外部ディレクトリ", + "settings.permissions.tool.external_directory.description": "プロジェクトディレクトリ外のファイルへのアクセス", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "同一入力による繰り返しのツール呼び出しを検出", + + "workspace.new": "新しいワークスペース", + "workspace.type.local": "ローカル", + "workspace.type.sandbox": "サンドボックス", + "workspace.create.failed.title": "ワークスペースの作成に失敗しました", + "workspace.delete.failed.title": "ワークスペースの削除に失敗しました", + "workspace.resetting.title": "ワークスペースをリセット中", + "workspace.resetting.description": "これには少し時間がかかる場合があります。", + "workspace.reset.failed.title": "ワークスペースのリセットに失敗しました", + "workspace.reset.success.title": "ワークスペースをリセットしました", + "workspace.reset.success.description": "ワークスペースはデフォルトブランチと一致しています。", + "workspace.status.checking": "未マージの変更を確認中...", + "workspace.status.error": "gitステータスを確認できません。", + "workspace.status.clean": "未マージの変更は検出されませんでした。", + "workspace.status.dirty": "このワークスペースで未マージの変更が検出されました。", + "workspace.delete.title": "ワークスペースの削除", + "workspace.delete.confirm": 'ワークスペース "{{name}}" を削除しますか?', + "workspace.delete.button": "ワークスペースを削除", + "workspace.reset.title": "ワークスペースのリセット", + "workspace.reset.confirm": 'ワークスペース "{{name}}" をリセットしますか?', + "workspace.reset.button": "ワークスペースをリセット", + "workspace.reset.archived.none": "アクティブなセッションはアーカイブされません。", + "workspace.reset.archived.one": "1つのセッションがアーカイブされます。", + "workspace.reset.archived.many": "{{count}}個のセッションがアーカイブされます。", + "workspace.reset.note": "これにより、ワークスペースはデフォルトブランチと一致するようにリセットされます。", +} diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts new file mode 100644 index 00000000000..dbd02270cb8 --- /dev/null +++ b/packages/app/src/i18n/ko.ts @@ -0,0 +1,555 @@ +import { dict as en } from "./en" + +type Keys = keyof typeof en + +export const dict = { + "command.category.suggested": "추천", + "command.category.view": "보기", + "command.category.project": "프로젝트", + "command.category.provider": "공급자", + "command.category.server": "서버", + "command.category.session": "세션", + "command.category.theme": "테마", + "command.category.language": "언어", + "command.category.file": "파일", + "command.category.terminal": "터미널", + "command.category.model": "모델", + "command.category.mcp": "MCP", + "command.category.agent": "에이전트", + "command.category.permissions": "권한", + "command.category.workspace": "작업 공간", + + "theme.scheme.system": "시스템", + "theme.scheme.light": "라이트", + "theme.scheme.dark": "다크", + + "command.sidebar.toggle": "사이드바 토글", + "command.project.open": "프로젝트 열기", + "command.provider.connect": "공급자 연결", + "command.server.switch": "서버 전환", + "command.session.previous": "이전 세션", + "command.session.next": "다음 세션", + "command.session.archive": "세션 보관", + + "command.palette": "명령 팔레트", + + "command.theme.cycle": "테마 순환", + "command.theme.set": "테마 사용: {{theme}}", + "command.theme.scheme.cycle": "색상 테마 순환", + "command.theme.scheme.set": "색상 테마 사용: {{scheme}}", + + "command.language.cycle": "언어 순환", + "command.language.set": "언어 사용: {{language}}", + + "command.session.new": "새 세션", + "command.file.open": "파일 열기", + "command.file.open.description": "파일 및 명령어 검색", + "command.terminal.toggle": "터미널 토글", + "command.review.toggle": "검토 토글", + "command.terminal.new": "새 터미널", + "command.terminal.new.description": "새 터미널 탭 생성", + "command.steps.toggle": "단계 토글", + "command.steps.toggle.description": "현재 메시지의 단계 표시/숨기기", + "command.message.previous": "이전 메시지", + "command.message.previous.description": "이전 사용자 메시지로 이동", + "command.message.next": "다음 메시지", + "command.message.next.description": "다음 사용자 메시지로 이동", + "command.model.choose": "모델 선택", + "command.model.choose.description": "다른 모델 선택", + "command.mcp.toggle": "MCP 토글", + "command.mcp.toggle.description": "MCP 토글", + "command.agent.cycle": "에이전트 순환", + "command.agent.cycle.description": "다음 에이전트로 전환", + "command.agent.cycle.reverse": "에이전트 역순환", + "command.agent.cycle.reverse.description": "이전 에이전트로 전환", + "command.model.variant.cycle": "생각 수준 순환", + "command.model.variant.cycle.description": "다음 생각 수준으로 전환", + "command.permissions.autoaccept.enable": "편집 자동 수락", + "command.permissions.autoaccept.disable": "편집 자동 수락 중지", + "command.session.undo": "실행 취소", + "command.session.undo.description": "마지막 메시지 실행 취소", + "command.session.redo": "다시 실행", + "command.session.redo.description": "마지막 실행 취소된 메시지 다시 실행", + "command.session.compact": "세션 압축", + "command.session.compact.description": "컨텍스트 크기를 줄이기 위해 세션 요약", + "command.session.fork": "메시지에서 분기", + "command.session.fork.description": "이전 메시지에서 새 세션 생성", + "command.session.share": "세션 공유", + "command.session.share.description": "이 세션을 공유하고 URL을 클립보드에 복사", + "command.session.unshare": "세션 공유 중지", + "command.session.unshare.description": "이 세션 공유 중지", + + "palette.search.placeholder": "파일 및 명령어 검색", + "palette.empty": "결과 없음", + "palette.group.commands": "명령어", + "palette.group.files": "파일", + + "dialog.provider.search.placeholder": "공급자 검색", + "dialog.provider.empty": "공급자 없음", + "dialog.provider.group.popular": "인기", + "dialog.provider.group.other": "기타", + "dialog.provider.tag.recommended": "추천", + "dialog.provider.anthropic.note": "Claude Pro/Max 또는 API 키로 연결", + + "dialog.model.select.title": "모델 선택", + "dialog.model.search.placeholder": "모델 검색", + "dialog.model.empty": "모델 결과 없음", + "dialog.model.manage": "모델 관리", + "dialog.model.manage.description": "모델 선택기에 표시할 모델 사용자 지정", + + "dialog.model.unpaid.freeModels.title": "OpenCode에서 제공하는 무료 모델", + "dialog.model.unpaid.addMore.title": "인기 공급자의 모델 추가", + + "dialog.provider.viewAll": "모든 공급자 보기", + + "provider.connect.title": "{{provider}} 연결", + "provider.connect.title.anthropicProMax": "Claude Pro/Max로 로그인", + "provider.connect.selectMethod": "{{provider}} 로그인 방법 선택", + "provider.connect.method.apiKey": "API 키", + "provider.connect.status.inProgress": "인증 진행 중...", + "provider.connect.status.waiting": "인증 대기 중...", + "provider.connect.status.failed": "인증 실패: {{error}}", + "provider.connect.apiKey.description": + "{{provider}} API 키를 입력하여 계정을 연결하고 OpenCode에서 {{provider}} 모델을 사용하세요.", + "provider.connect.apiKey.label": "{{provider}} API 키", + "provider.connect.apiKey.placeholder": "API 키", + "provider.connect.apiKey.required": "API 키가 필요합니다", + "provider.connect.opencodeZen.line1": + "OpenCode Zen은 코딩 에이전트를 위해 최적화된 신뢰할 수 있는 엄선된 모델에 대한 액세스를 제공합니다.", + "provider.connect.opencodeZen.line2": "단일 API 키로 Claude, GPT, Gemini, GLM 등 다양한 모델에 액세스할 수 있습니다.", + "provider.connect.opencodeZen.visit.prefix": "", + "provider.connect.opencodeZen.visit.suffix": "를 방문하여 API 키를 받으세요.", + "provider.connect.oauth.code.visit.prefix": "", + "provider.connect.oauth.code.visit.link": "이 링크", + "provider.connect.oauth.code.visit.suffix": + "를 방문하여 인증 코드를 받아 계정을 연결하고 OpenCode에서 {{provider}} 모델을 사용하세요.", + "provider.connect.oauth.code.label": "{{method}} 인증 코드", + "provider.connect.oauth.code.placeholder": "인증 코드", + "provider.connect.oauth.code.required": "인증 코드가 필요합니다", + "provider.connect.oauth.code.invalid": "유효하지 않은 인증 코드", + "provider.connect.oauth.auto.visit.prefix": "", + "provider.connect.oauth.auto.visit.link": "이 링크", + "provider.connect.oauth.auto.visit.suffix": + "를 방문하고 아래 코드를 입력하여 계정을 연결하고 OpenCode에서 {{provider}} 모델을 사용하세요.", + "provider.connect.oauth.auto.confirmationCode": "확인 코드", + "provider.connect.toast.connected.title": "{{provider}} 연결됨", + "provider.connect.toast.connected.description": "이제 {{provider}} 모델을 사용할 수 있습니다.", + + "model.tag.free": "무료", + "model.tag.latest": "최신", + + "common.search.placeholder": "검색", + "common.loading": "로딩 중", + "common.cancel": "취소", + "common.submit": "제출", + "common.save": "저장", + "common.saving": "저장 중...", + "common.default": "기본값", + "common.attachment": "첨부 파일", + + "prompt.placeholder.shell": "셸 명령어 입력...", + "prompt.placeholder.normal": '무엇이든 물어보세요... "{{example}}"', + "prompt.mode.shell": "셸", + "prompt.mode.shell.exit": "종료하려면 esc", + + "prompt.example.1": "코드베이스의 TODO 수정", + "prompt.example.2": "이 프로젝트의 기술 스택이 무엇인가요?", + "prompt.example.3": "고장 난 테스트 수정", + "prompt.example.4": "인증 작동 방식 설명", + "prompt.example.5": "보안 취약점 찾기 및 수정", + "prompt.example.6": "사용자 서비스에 단위 테스트 추가", + "prompt.example.7": "이 함수를 더 읽기 쉽게 리팩터링", + "prompt.example.8": "이 오류는 무엇을 의미하나요?", + "prompt.example.9": "이 문제 디버깅 도와줘", + "prompt.example.10": "API 문서 생성", + "prompt.example.11": "데이터베이스 쿼리 최적화", + "prompt.example.12": "입력 유효성 검사 추가", + "prompt.example.13": "...를 위한 새 컴포넌트 생성", + "prompt.example.14": "이 프로젝트를 어떻게 배포하나요?", + "prompt.example.15": "모범 사례를 기준으로 내 코드 검토", + "prompt.example.16": "이 함수에 오류 처리 추가", + "prompt.example.17": "이 정규식 패턴 설명", + "prompt.example.18": "이것을 TypeScript로 변환", + "prompt.example.19": "코드베이스 전체에 로깅 추가", + "prompt.example.20": "오래된 종속성은 무엇인가요?", + "prompt.example.21": "마이그레이션 스크립트 작성 도와줘", + "prompt.example.22": "이 엔드포인트에 캐싱 구현", + "prompt.example.23": "이 목록에 페이지네이션 추가", + "prompt.example.24": "...를 위한 CLI 명령어 생성", + "prompt.example.25": "여기서 환경 변수는 어떻게 작동하나요?", + + "prompt.popover.emptyResults": "일치하는 결과 없음", + "prompt.popover.emptyCommands": "일치하는 명령어 없음", + "prompt.dropzone.label": "이미지나 PDF를 여기에 드롭하세요", + "prompt.slash.badge.custom": "사용자 지정", + "prompt.context.active": "활성", + "prompt.context.includeActiveFile": "활성 파일 포함", + "prompt.action.attachFile": "파일 첨부", + "prompt.action.send": "전송", + "prompt.action.stop": "중지", + + "prompt.toast.pasteUnsupported.title": "지원되지 않는 붙여넣기", + "prompt.toast.pasteUnsupported.description": "이미지나 PDF만 붙여넣을 수 있습니다.", + "prompt.toast.modelAgentRequired.title": "에이전트 및 모델 선택", + "prompt.toast.modelAgentRequired.description": "프롬프트를 보내기 전에 에이전트와 모델을 선택하세요.", + "prompt.toast.worktreeCreateFailed.title": "작업 트리 생성 실패", + "prompt.toast.sessionCreateFailed.title": "세션 생성 실패", + "prompt.toast.shellSendFailed.title": "셸 명령 전송 실패", + "prompt.toast.commandSendFailed.title": "명령 전송 실패", + "prompt.toast.promptSendFailed.title": "프롬프트 전송 실패", + + "dialog.mcp.title": "MCP", + "dialog.mcp.description": "{{total}}개 중 {{enabled}}개 활성화됨", + "dialog.mcp.empty": "구성된 MCP 없음", + + "mcp.status.connected": "연결됨", + "mcp.status.failed": "실패", + "mcp.status.needs_auth": "인증 필요", + "mcp.status.disabled": "비활성화됨", + + "dialog.fork.empty": "분기할 메시지 없음", + + "dialog.directory.search.placeholder": "폴더 검색", + "dialog.directory.empty": "폴더 없음", + + "dialog.server.title": "서버", + "dialog.server.description": "이 앱이 연결할 OpenCode 서버를 전환합니다.", + "dialog.server.search.placeholder": "서버 검색", + "dialog.server.empty": "서버 없음", + "dialog.server.add.title": "서버 추가", + "dialog.server.add.url": "서버 URL", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "서버에 연결할 수 없습니다", + "dialog.server.add.checking": "확인 중...", + "dialog.server.add.button": "추가", + "dialog.server.default.title": "기본 서버", + "dialog.server.default.description": + "로컬 서버를 시작하는 대신 앱 실행 시 이 서버에 연결합니다. 다시 시작해야 합니다.", + "dialog.server.default.none": "선택된 서버 없음", + "dialog.server.default.set": "현재 서버를 기본값으로 설정", + "dialog.server.default.clear": "지우기", + + "dialog.project.edit.title": "프로젝트 편집", + "dialog.project.edit.name": "이름", + "dialog.project.edit.icon": "아이콘", + "dialog.project.edit.icon.alt": "프로젝트 아이콘", + "dialog.project.edit.icon.hint": "이미지를 클릭하거나 드래그하세요", + "dialog.project.edit.icon.recommended": "권장: 128x128px", + "dialog.project.edit.color": "색상", + + "context.breakdown.title": "컨텍스트 분석", + "context.breakdown.note": '입력 토큰의 대략적인 분석입니다. "기타"에는 도구 정의 및 오버헤드가 포함됩니다.', + "context.breakdown.system": "시스템", + "context.breakdown.user": "사용자", + "context.breakdown.assistant": "어시스턴트", + "context.breakdown.tool": "도구 호출", + "context.breakdown.other": "기타", + + "context.systemPrompt.title": "시스템 프롬프트", + "context.rawMessages.title": "원시 메시지", + + "context.stats.session": "세션", + "context.stats.messages": "메시지", + "context.stats.provider": "공급자", + "context.stats.model": "모델", + "context.stats.limit": "컨텍스트 제한", + "context.stats.totalTokens": "총 토큰", + "context.stats.usage": "사용량", + "context.stats.inputTokens": "입력 토큰", + "context.stats.outputTokens": "출력 토큰", + "context.stats.reasoningTokens": "추론 토큰", + "context.stats.cacheTokens": "캐시 토큰 (읽기/쓰기)", + "context.stats.userMessages": "사용자 메시지", + "context.stats.assistantMessages": "어시스턴트 메시지", + "context.stats.totalCost": "총 비용", + "context.stats.sessionCreated": "세션 생성됨", + "context.stats.lastActivity": "최근 활동", + + "context.usage.tokens": "토큰", + "context.usage.usage": "사용량", + "context.usage.cost": "비용", + "context.usage.clickToView": "컨텍스트를 보려면 클릭", + + "language.en": "영어", + "language.zh": "중국어", + "language.ko": "한국어", + "language.de": "독일어", + "language.es": "스페인어", + "language.fr": "프랑스어", + "language.ja": "일본어", + "language.da": "덴마크어", + + "toast.language.title": "언어", + "toast.language.description": "{{language}}(으)로 전환됨", + + "toast.theme.title": "테마 전환됨", + "toast.scheme.title": "색상 테마", + + "toast.permissions.autoaccept.on.title": "편집 자동 수락 중", + "toast.permissions.autoaccept.on.description": "편집 및 쓰기 권한이 자동으로 승인됩니다", + "toast.permissions.autoaccept.off.title": "편집 자동 수락 중지됨", + "toast.permissions.autoaccept.off.description": "편집 및 쓰기 권한 승인이 필요합니다", + + "toast.model.none.title": "선택된 모델 없음", + "toast.model.none.description": "이 세션을 요약하려면 공급자를 연결하세요", + + "toast.file.loadFailed.title": "파일 로드 실패", + + "toast.session.share.copyFailed.title": "URL 클립보드 복사 실패", + "toast.session.share.success.title": "세션 공유됨", + "toast.session.share.success.description": "공유 URL이 클립보드에 복사되었습니다!", + "toast.session.share.failed.title": "세션 공유 실패", + "toast.session.share.failed.description": "세션을 공유하는 동안 오류가 발생했습니다", + + "toast.session.unshare.success.title": "세션 공유 해제됨", + "toast.session.unshare.success.description": "세션 공유가 성공적으로 해제되었습니다!", + "toast.session.unshare.failed.title": "세션 공유 해제 실패", + "toast.session.unshare.failed.description": "세션 공유를 해제하는 동안 오류가 발생했습니다", + + "toast.session.listFailed.title": "{{project}}에 대한 세션을 로드하지 못했습니다", + + "toast.update.title": "업데이트 가능", + "toast.update.description": "OpenCode의 새 버전({{version}})을 설치할 수 있습니다.", + "toast.update.action.installRestart": "설치 및 다시 시작", + "toast.update.action.notYet": "나중에", + + "error.page.title": "문제가 발생했습니다", + "error.page.description": "애플리케이션을 로드하는 동안 오류가 발생했습니다.", + "error.page.details.label": "오류 세부 정보", + "error.page.action.restart": "다시 시작", + "error.page.action.checking": "확인 중...", + "error.page.action.checkUpdates": "업데이트 확인", + "error.page.action.updateTo": "{{version}} 버전으로 업데이트", + "error.page.report.prefix": "이 오류를 OpenCode 팀에 제보해 주세요: ", + "error.page.report.discord": "Discord", + "error.page.version": "버전: {{version}}", + + "error.dev.rootNotFound": + "루트 요소를 찾을 수 없습니다. index.html에 추가하는 것을 잊으셨나요? 또는 id 속성의 철자가 틀렸을 수 있습니다.", + + "error.globalSync.connectFailed": "서버에 연결할 수 없습니다. `{{url}}`에서 서버가 실행 중인가요?", + + "error.chain.unknown": "알 수 없는 오류", + "error.chain.causedBy": "원인:", + "error.chain.apiError": "API 오류", + "error.chain.status": "상태: {{status}}", + "error.chain.retryable": "재시도 가능: {{retryable}}", + "error.chain.responseBody": "응답 본문:\n{{body}}", + "error.chain.didYouMean": "혹시 {{suggestions}}을(를) 의미하셨나요?", + "error.chain.modelNotFound": "모델을 찾을 수 없음: {{provider}}/{{model}}", + "error.chain.checkConfig": "구성(opencode.json)의 공급자/모델 이름을 확인하세요", + "error.chain.mcpFailed": 'MCP 서버 "{{name}}" 실패. 참고: OpenCode는 아직 MCP 인증을 지원하지 않습니다.', + "error.chain.providerAuthFailed": "공급자 인증 실패 ({{provider}}): {{message}}", + "error.chain.providerInitFailed": '공급자 "{{provider}}" 초기화 실패. 자격 증명과 구성을 확인하세요.', + "error.chain.configJsonInvalid": "{{path}}의 구성 파일이 유효한 JSON(C)가 아닙니다", + "error.chain.configJsonInvalidWithMessage": "{{path}}의 구성 파일이 유효한 JSON(C)가 아닙니다: {{message}}", + "error.chain.configDirectoryTypo": + '{{path}}의 "{{dir}}" 디렉터리가 유효하지 않습니다. 디렉터리 이름을 "{{suggestion}}"으로 변경하거나 제거하세요. 이는 흔한 오타입니다.', + "error.chain.configFrontmatterError": "{{path}}의 frontmatter 파싱 실패:\n{{message}}", + "error.chain.configInvalid": "{{path}}의 구성 파일이 유효하지 않습니다", + "error.chain.configInvalidWithMessage": "{{path}}의 구성 파일이 유효하지 않습니다: {{message}}", + + "notification.permission.title": "권한 필요", + "notification.permission.description": "{{projectName}}의 {{sessionTitle}}에서 권한이 필요합니다", + "notification.question.title": "질문", + "notification.question.description": "{{projectName}}의 {{sessionTitle}}에서 질문이 있습니다", + "notification.action.goToSession": "세션으로 이동", + + "notification.session.responseReady.title": "응답 준비됨", + "notification.session.error.title": "세션 오류", + "notification.session.error.fallbackDescription": "오류가 발생했습니다", + + "home.recentProjects": "최근 프로젝트", + "home.empty.title": "최근 프로젝트 없음", + "home.empty.description": "로컬 프로젝트를 열어 시작하세요", + + "session.tab.session": "세션", + "session.tab.review": "검토", + "session.tab.context": "컨텍스트", + "session.review.filesChanged": "{{count}}개 파일 변경됨", + "session.review.loadingChanges": "변경 사항 로드 중...", + "session.review.empty": "이 세션에 변경 사항이 아직 없습니다", + "session.messages.renderEarlier": "이전 메시지 렌더링", + "session.messages.loadingEarlier": "이전 메시지 로드 중...", + "session.messages.loadEarlier": "이전 메시지 로드", + "session.messages.loading": "메시지 로드 중...", + + "session.context.addToContext": "컨텍스트에 {{selection}} 추가", + + "session.new.worktree.main": "메인 브랜치", + "session.new.worktree.mainWithBranch": "메인 브랜치 ({{branch}})", + "session.new.worktree.create": "새 작업 트리 생성", + "session.new.lastModified": "최근 수정", + + "session.header.search.placeholder": "{{project}} 검색", + + "session.share.popover.title": "웹에 게시", + "session.share.popover.description.shared": "이 세션은 웹에 공개되었습니다. 링크가 있는 누구나 액세스할 수 있습니다.", + "session.share.popover.description.unshared": + "세션을 웹에 공개적으로 공유합니다. 링크가 있는 누구나 액세스할 수 있습니다.", + "session.share.action.share": "공유", + "session.share.action.publish": "게시", + "session.share.action.publishing": "게시 중...", + "session.share.action.unpublish": "게시 취소", + "session.share.action.unpublishing": "게시 취소 중...", + "session.share.action.view": "보기", + "session.share.copy.copied": "복사됨", + "session.share.copy.copyLink": "링크 복사", + + "lsp.tooltip.none": "LSP 서버 없음", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "프롬프트 로드 중...", + "terminal.loading": "터미널 로드 중...", + "terminal.title": "터미널", + "terminal.title.numbered": "터미널 {{number}}", + + "common.closeTab": "탭 닫기", + "common.dismiss": "닫기", + "common.requestFailed": "요청 실패", + "common.moreOptions": "더 많은 옵션", + "common.learnMore": "더 알아보기", + "common.rename": "이름 바꾸기", + "common.reset": "초기화", + "common.delete": "삭제", + "common.close": "닫기", + "common.edit": "편집", + "common.loadMore": "더 불러오기", + + "sidebar.settings": "설정", + "sidebar.help": "도움말", + "sidebar.workspaces.enable": "작업 공간 활성화", + "sidebar.workspaces.disable": "작업 공간 비활성화", + "sidebar.gettingStarted.title": "시작하기", + "sidebar.gettingStarted.line1": "OpenCode에는 무료 모델이 포함되어 있어 즉시 시작할 수 있습니다.", + "sidebar.gettingStarted.line2": "Claude, GPT, Gemini 등을 포함한 모델을 사용하려면 공급자를 연결하세요.", + "sidebar.project.recentSessions": "최근 세션", + "sidebar.project.viewAllSessions": "모든 세션 보기", + + "settings.section.desktop": "데스크톱", + "settings.tab.general": "일반", + "settings.tab.shortcuts": "단축키", + + "settings.general.section.appearance": "모양", + "settings.general.section.notifications": "시스템 알림", + "settings.general.section.sounds": "효과음", + + "settings.general.row.language.title": "언어", + "settings.general.row.language.description": "OpenCode 표시 언어 변경", + "settings.general.row.appearance.title": "모양", + "settings.general.row.appearance.description": "기기에서 OpenCode가 보이는 방식 사용자 지정", + "settings.general.row.theme.title": "테마", + "settings.general.row.theme.description": "OpenCode 테마 사용자 지정", + "settings.general.row.font.title": "글꼴", + "settings.general.row.font.description": "코드 블록에 사용되는 고정폭 글꼴 사용자 지정", + + "settings.general.notifications.agent.title": "에이전트", + "settings.general.notifications.agent.description": "에이전트가 완료되거나 주의가 필요할 때 시스템 알림 표시", + "settings.general.notifications.permissions.title": "권한", + "settings.general.notifications.permissions.description": "권한이 필요할 때 시스템 알림 표시", + "settings.general.notifications.errors.title": "오류", + "settings.general.notifications.errors.description": "오류가 발생했을 때 시스템 알림 표시", + + "settings.general.sounds.agent.title": "에이전트", + "settings.general.sounds.agent.description": "에이전트가 완료되거나 주의가 필요할 때 소리 재생", + "settings.general.sounds.permissions.title": "권한", + "settings.general.sounds.permissions.description": "권한이 필요할 때 소리 재생", + "settings.general.sounds.errors.title": "오류", + "settings.general.sounds.errors.description": "오류가 발생했을 때 소리 재생", + + "settings.shortcuts.title": "키보드 단축키", + "settings.shortcuts.reset.button": "기본값으로 초기화", + "settings.shortcuts.reset.toast.title": "단축키 초기화됨", + "settings.shortcuts.reset.toast.description": "키보드 단축키가 기본값으로 초기화되었습니다.", + "settings.shortcuts.conflict.title": "단축키가 이미 사용 중임", + "settings.shortcuts.conflict.description": "{{keybind}}은(는) 이미 {{titles}}에 할당되어 있습니다.", + "settings.shortcuts.unassigned": "할당되지 않음", + "settings.shortcuts.pressKeys": "키 누르기", + "settings.shortcuts.search.placeholder": "단축키 검색", + "settings.shortcuts.search.empty": "단축키를 찾을 수 없습니다", + + "settings.shortcuts.group.general": "일반", + "settings.shortcuts.group.session": "세션", + "settings.shortcuts.group.navigation": "탐색", + "settings.shortcuts.group.modelAndAgent": "모델 및 에이전트", + "settings.shortcuts.group.terminal": "터미널", + "settings.shortcuts.group.prompt": "프롬프트", + + "settings.providers.title": "공급자", + "settings.providers.description": "공급자 설정은 여기서 구성할 수 있습니다.", + "settings.models.title": "모델", + "settings.models.description": "모델 설정은 여기서 구성할 수 있습니다.", + "settings.agents.title": "에이전트", + "settings.agents.description": "에이전트 설정은 여기서 구성할 수 있습니다.", + "settings.commands.title": "명령어", + "settings.commands.description": "명령어 설정은 여기서 구성할 수 있습니다.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP 설정은 여기서 구성할 수 있습니다.", + + "settings.permissions.title": "권한", + "settings.permissions.description": "서버가 기본적으로 사용할 수 있는 도구를 제어합니다.", + "settings.permissions.section.tools": "도구", + "settings.permissions.toast.updateFailed.title": "권한 업데이트 실패", + + "settings.permissions.action.allow": "허용", + "settings.permissions.action.ask": "묻기", + "settings.permissions.action.deny": "거부", + + "settings.permissions.tool.read.title": "읽기", + "settings.permissions.tool.read.description": "파일 읽기 (파일 경로와 일치)", + "settings.permissions.tool.edit.title": "편집", + "settings.permissions.tool.edit.description": "파일 수정 (편집, 쓰기, 패치 및 다중 편집 포함)", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "glob 패턴을 사용하여 파일 일치", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "정규식을 사용하여 파일 내용 검색", + "settings.permissions.tool.list.title": "목록", + "settings.permissions.tool.list.description": "디렉터리 내 파일 나열", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "셸 명령어 실행", + "settings.permissions.tool.task.title": "작업", + "settings.permissions.tool.task.description": "하위 에이전트 실행", + "settings.permissions.tool.skill.title": "기술", + "settings.permissions.tool.skill.description": "이름으로 기술 로드", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "언어 서버 쿼리 실행", + "settings.permissions.tool.todoread.title": "할 일 읽기", + "settings.permissions.tool.todoread.description": "할 일 목록 읽기", + "settings.permissions.tool.todowrite.title": "할 일 쓰기", + "settings.permissions.tool.todowrite.description": "할 일 목록 업데이트", + "settings.permissions.tool.webfetch.title": "웹 가져오기", + "settings.permissions.tool.webfetch.description": "URL에서 콘텐츠 가져오기", + "settings.permissions.tool.websearch.title": "웹 검색", + "settings.permissions.tool.websearch.description": "웹 검색", + "settings.permissions.tool.codesearch.title": "코드 검색", + "settings.permissions.tool.codesearch.description": "웹에서 코드 검색", + "settings.permissions.tool.external_directory.title": "외부 디렉터리", + "settings.permissions.tool.external_directory.description": "프로젝트 디렉터리 외부의 파일에 액세스", + "settings.permissions.tool.doom_loop.title": "무한 반복", + "settings.permissions.tool.doom_loop.description": "동일한 입력으로 반복되는 도구 호출 감지", + + "workspace.new": "새 작업 공간", + "workspace.type.local": "로컬", + "workspace.type.sandbox": "샌드박스", + "workspace.create.failed.title": "작업 공간 생성 실패", + "workspace.delete.failed.title": "작업 공간 삭제 실패", + "workspace.resetting.title": "작업 공간 재설정 중", + "workspace.resetting.description": "잠시 시간이 걸릴 수 있습니다.", + "workspace.reset.failed.title": "작업 공간 재설정 실패", + "workspace.reset.success.title": "작업 공간 재설정됨", + "workspace.reset.success.description": "작업 공간이 이제 기본 브랜치와 일치합니다.", + "workspace.status.checking": "병합되지 않은 변경 사항 확인 중...", + "workspace.status.error": "Git 상태를 확인할 수 없습니다.", + "workspace.status.clean": "병합되지 않은 변경 사항이 감지되지 않았습니다.", + "workspace.status.dirty": "이 작업 공간에서 병합되지 않은 변경 사항이 감지되었습니다.", + "workspace.delete.title": "작업 공간 삭제", + "workspace.delete.confirm": '"{{name}}" 작업 공간을 삭제하시겠습니까?', + "workspace.delete.button": "작업 공간 삭제", + "workspace.reset.title": "작업 공간 재설정", + "workspace.reset.confirm": '"{{name}}" 작업 공간을 재설정하시겠습니까?', + "workspace.reset.button": "작업 공간 재설정", + "workspace.reset.archived.none": "활성 세션이 보관되지 않습니다.", + "workspace.reset.archived.one": "1개의 세션이 보관됩니다.", + "workspace.reset.archived.many": "{{count}}개의 세션이 보관됩니다.", + "workspace.reset.note": "이 작업은 작업 공간을 기본 브랜치와 일치하도록 재설정합니다.", +} diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts new file mode 100644 index 00000000000..ac976b627b0 --- /dev/null +++ b/packages/app/src/i18n/zh.ts @@ -0,0 +1,549 @@ +import { dict as en } from "./en" + +type Keys = keyof typeof en + +export const dict = { + "command.category.suggested": "建议", + "command.category.view": "视图", + "command.category.project": "项目", + "command.category.provider": "提供商", + "command.category.server": "服务器", + "command.category.session": "会话", + "command.category.theme": "主题", + "command.category.language": "语言", + "command.category.file": "文件", + "command.category.terminal": "终端", + "command.category.model": "模型", + "command.category.mcp": "MCP", + "command.category.agent": "智能体", + "command.category.permissions": "权限", + "command.category.workspace": "工作区", + + "theme.scheme.system": "系统", + "theme.scheme.light": "浅色", + "theme.scheme.dark": "深色", + + "command.sidebar.toggle": "切换侧边栏", + "command.project.open": "打开项目", + "command.provider.connect": "连接提供商", + "command.server.switch": "切换服务器", + "command.session.previous": "上一个会话", + "command.session.next": "下一个会话", + "command.session.archive": "归档会话", + + "command.palette": "命令面板", + + "command.theme.cycle": "切换主题", + "command.theme.set": "使用主题: {{theme}}", + "command.theme.scheme.cycle": "切换配色方案", + "command.theme.scheme.set": "使用配色方案: {{scheme}}", + + "command.language.cycle": "切换语言", + "command.language.set": "使用语言: {{language}}", + + "command.session.new": "新建会话", + "command.file.open": "打开文件", + "command.file.open.description": "搜索文件和命令", + "command.terminal.toggle": "切换终端", + "command.review.toggle": "切换审查", + "command.terminal.new": "新建终端", + "command.terminal.new.description": "创建新的终端标签页", + "command.steps.toggle": "切换步骤", + "command.steps.toggle.description": "显示或隐藏当前消息的步骤", + "command.message.previous": "上一条消息", + "command.message.previous.description": "跳转到上一条用户消息", + "command.message.next": "下一条消息", + "command.message.next.description": "跳转到下一条用户消息", + "command.model.choose": "选择模型", + "command.model.choose.description": "选择不同的模型", + "command.mcp.toggle": "切换 MCPs", + "command.mcp.toggle.description": "切换 MCPs", + "command.agent.cycle": "切换智能体", + "command.agent.cycle.description": "切换到下一个智能体", + "command.agent.cycle.reverse": "反向切换智能体", + "command.agent.cycle.reverse.description": "切换到上一个智能体", + "command.model.variant.cycle": "切换思考强度", + "command.model.variant.cycle.description": "切换到下一个强度等级", + "command.permissions.autoaccept.enable": "自动接受编辑", + "command.permissions.autoaccept.disable": "停止自动接受编辑", + "command.session.undo": "撤销", + "command.session.undo.description": "撤销上一条消息", + "command.session.redo": "重做", + "command.session.redo.description": "重做上一条撤销的消息", + "command.session.compact": "精简会话", + "command.session.compact.description": "总结会话以减少上下文大小", + "command.session.fork": "从消息分叉", + "command.session.fork.description": "从之前的消息创建新会话", + "command.session.share": "分享会话", + "command.session.share.description": "分享此会话并将链接复制到剪贴板", + "command.session.unshare": "取消分享会话", + "command.session.unshare.description": "停止分享此会话", + + "palette.search.placeholder": "搜索文件和命令", + "palette.empty": "未找到结果", + "palette.group.commands": "命令", + "palette.group.files": "文件", + + "dialog.provider.search.placeholder": "搜索提供商", + "dialog.provider.empty": "未找到提供商", + "dialog.provider.group.popular": "热门", + "dialog.provider.group.other": "其他", + "dialog.provider.tag.recommended": "推荐", + "dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接", + + "dialog.model.select.title": "选择模型", + "dialog.model.search.placeholder": "搜索模型", + "dialog.model.empty": "未找到模型", + "dialog.model.manage": "管理模型", + "dialog.model.manage.description": "自定义模型选择器中显示的模型。", + + "dialog.model.unpaid.freeModels.title": "OpenCode 提供的免费模型", + "dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型", + + "dialog.provider.viewAll": "查看全部提供商", + + "provider.connect.title": "连接 {{provider}}", + "provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登录", + "provider.connect.selectMethod": "选择 {{provider}} 的登录方式。", + "provider.connect.method.apiKey": "API 密钥", + "provider.connect.status.inProgress": "正在授权...", + "provider.connect.status.waiting": "等待授权...", + "provider.connect.status.failed": "授权失败: {{error}}", + "provider.connect.apiKey.description": + "输入你的 {{provider}} API 密钥以连接帐户,并在 OpenCode 中使用 {{provider}} 模型。", + "provider.connect.apiKey.label": "{{provider}} API 密钥", + "provider.connect.apiKey.placeholder": "API 密钥", + "provider.connect.apiKey.required": "API 密钥为必填项", + "provider.connect.opencodeZen.line1": "OpenCode Zen 为你提供一组精选的可靠优化模型,用于代码智能体。", + "provider.connect.opencodeZen.line2": "只需一个 API 密钥,你就能使用 Claude、GPT、Gemini、GLM 等模型。", + "provider.connect.opencodeZen.visit.prefix": "访问 ", + "provider.connect.opencodeZen.visit.suffix": " 获取你的 API 密钥。", + "provider.connect.oauth.code.visit.prefix": "访问 ", + "provider.connect.oauth.code.visit.link": "此链接", + "provider.connect.oauth.code.visit.suffix": " 获取授权码,以连接你的帐户并在 OpenCode 中使用 {{provider}} 模型。", + "provider.connect.oauth.code.label": "{{method}} 授权码", + "provider.connect.oauth.code.placeholder": "授权码", + "provider.connect.oauth.code.required": "授权码为必填项", + "provider.connect.oauth.code.invalid": "授权码无效", + "provider.connect.oauth.auto.visit.prefix": "访问 ", + "provider.connect.oauth.auto.visit.link": "此链接", + "provider.connect.oauth.auto.visit.suffix": " 并输入以下代码,以连接你的帐户并在 OpenCode 中使用 {{provider}} 模型。", + "provider.connect.oauth.auto.confirmationCode": "确认码", + "provider.connect.toast.connected.title": "{{provider}} 已连接", + "provider.connect.toast.connected.description": "现在可以使用 {{provider}} 模型了。", + + "model.tag.free": "免费", + "model.tag.latest": "最新", + + "common.search.placeholder": "搜索", + "common.loading": "加载中", + "common.cancel": "取消", + "common.submit": "提交", + "common.save": "保存", + "common.saving": "保存中...", + "common.default": "默认", + "common.attachment": "附件", + + "prompt.placeholder.shell": "输入 shell 命令...", + "prompt.placeholder.normal": '随便问点什么... "{{example}}"', + "prompt.mode.shell": "Shell", + "prompt.mode.shell.exit": "按 esc 退出", + + "prompt.example.1": "修复代码库中的一个 TODO", + "prompt.example.2": "这个项目的技术栈是什么?", + "prompt.example.3": "修复失败的测试", + "prompt.example.4": "解释认证是如何工作的", + "prompt.example.5": "查找并修复安全漏洞", + "prompt.example.6": "为用户服务添加单元测试", + "prompt.example.7": "重构这个函数,让它更易读", + "prompt.example.8": "这个错误是什么意思?", + "prompt.example.9": "帮我调试这个问题", + "prompt.example.10": "生成 API 文档", + "prompt.example.11": "优化数据库查询", + "prompt.example.12": "添加输入校验", + "prompt.example.13": "创建一个新的组件用于...", + "prompt.example.14": "我该如何部署这个项目?", + "prompt.example.15": "审查我的代码并给出最佳实践建议", + "prompt.example.16": "为这个函数添加错误处理", + "prompt.example.17": "解释这个正则表达式", + "prompt.example.18": "把它转换成 TypeScript", + "prompt.example.19": "在整个代码库中添加日志", + "prompt.example.20": "哪些依赖已经过期?", + "prompt.example.21": "帮我写一个迁移脚本", + "prompt.example.22": "为这个接口实现缓存", + "prompt.example.23": "给这个列表添加分页", + "prompt.example.24": "创建一个 CLI 命令用于...", + "prompt.example.25": "这里的环境变量是怎么工作的?", + + "prompt.popover.emptyResults": "没有匹配的结果", + "prompt.popover.emptyCommands": "没有匹配的命令", + "prompt.dropzone.label": "将图片或 PDF 拖到这里", + "prompt.slash.badge.custom": "自定义", + "prompt.context.active": "当前", + "prompt.context.includeActiveFile": "包含当前文件", + "prompt.action.attachFile": "附加文件", + "prompt.action.send": "发送", + "prompt.action.stop": "停止", + + "prompt.toast.pasteUnsupported.title": "不支持的粘贴", + "prompt.toast.pasteUnsupported.description": "这里只能粘贴图片或 PDF 文件。", + "prompt.toast.modelAgentRequired.title": "请选择智能体和模型", + "prompt.toast.modelAgentRequired.description": "发送提示前请先选择智能体和模型。", + "prompt.toast.worktreeCreateFailed.title": "创建工作树失败", + "prompt.toast.sessionCreateFailed.title": "创建会话失败", + "prompt.toast.shellSendFailed.title": "发送 shell 命令失败", + "prompt.toast.commandSendFailed.title": "发送命令失败", + "prompt.toast.promptSendFailed.title": "发送提示失败", + + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "已启用 {{enabled}} / {{total}}", + "dialog.mcp.empty": "未配置 MCPs", + + "mcp.status.connected": "已连接", + "mcp.status.failed": "失败", + "mcp.status.needs_auth": "需要授权", + "mcp.status.disabled": "已禁用", + + "dialog.fork.empty": "没有可用于分叉的消息", + + "dialog.directory.search.placeholder": "搜索文件夹", + "dialog.directory.empty": "未找到文件夹", + + "dialog.server.title": "服务器", + "dialog.server.description": "切换此应用连接的 OpenCode 服务器。", + "dialog.server.search.placeholder": "搜索服务器", + "dialog.server.empty": "暂无服务器", + "dialog.server.add.title": "添加服务器", + "dialog.server.add.url": "服务器 URL", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "无法连接到服务器", + "dialog.server.add.checking": "检查中...", + "dialog.server.add.button": "添加", + "dialog.server.default.title": "默认服务器", + "dialog.server.default.description": "应用启动时连接此服务器,而不是启动本地服务器。需要重启。", + "dialog.server.default.none": "未选择服务器", + "dialog.server.default.set": "将当前服务器设为默认", + "dialog.server.default.clear": "清除", + + "dialog.project.edit.title": "编辑项目", + "dialog.project.edit.name": "名称", + "dialog.project.edit.icon": "图标", + "dialog.project.edit.icon.alt": "项目图标", + "dialog.project.edit.icon.hint": "点击或拖拽图片", + "dialog.project.edit.icon.recommended": "建议:128x128px", + "dialog.project.edit.color": "颜色", + + "context.breakdown.title": "上下文拆分", + "context.breakdown.note": "输入 token 的大致拆分。“其他”包含工具定义和开销。", + "context.breakdown.system": "系统", + "context.breakdown.user": "用户", + "context.breakdown.assistant": "助手", + "context.breakdown.tool": "工具调用", + "context.breakdown.other": "其他", + + "context.systemPrompt.title": "系统提示词", + "context.rawMessages.title": "原始消息", + + "context.stats.session": "会话", + "context.stats.messages": "消息数", + "context.stats.provider": "提供商", + "context.stats.model": "模型", + "context.stats.limit": "上下文限制", + "context.stats.totalTokens": "总 token", + "context.stats.usage": "使用率", + "context.stats.inputTokens": "输入 token", + "context.stats.outputTokens": "输出 token", + "context.stats.reasoningTokens": "推理 token", + "context.stats.cacheTokens": "缓存 token(读/写)", + "context.stats.userMessages": "用户消息", + "context.stats.assistantMessages": "助手消息", + "context.stats.totalCost": "总成本", + "context.stats.sessionCreated": "创建时间", + "context.stats.lastActivity": "最后活动", + + "context.usage.tokens": "Token", + "context.usage.usage": "使用率", + "context.usage.cost": "成本", + "context.usage.clickToView": "点击查看上下文", + + "language.en": "英语", + "language.zh": "中文", + "language.ko": "韩语", + "language.de": "德语", + "language.es": "西班牙语", + "language.fr": "法语", + "language.ja": "日语", + "language.da": "丹麦语", + + "toast.language.title": "语言", + "toast.language.description": "已切换到{{language}}", + + "toast.theme.title": "主题已切换", + "toast.scheme.title": "配色方案", + + "toast.permissions.autoaccept.on.title": "自动接受编辑", + "toast.permissions.autoaccept.on.description": "编辑和写入权限将自动获批", + "toast.permissions.autoaccept.off.title": "已停止自动接受编辑", + "toast.permissions.autoaccept.off.description": "编辑和写入权限将需要手动批准", + + "toast.model.none.title": "未选择模型", + "toast.model.none.description": "请先连接提供商以总结此会话", + + "toast.file.loadFailed.title": "加载文件失败", + + "toast.session.share.copyFailed.title": "无法复制链接到剪贴板", + "toast.session.share.success.title": "会话已分享", + "toast.session.share.success.description": "分享链接已复制到剪贴板", + "toast.session.share.failed.title": "分享会话失败", + "toast.session.share.failed.description": "分享会话时发生错误", + + "toast.session.unshare.success.title": "已取消分享会话", + "toast.session.unshare.success.description": "会话已成功取消分享", + "toast.session.unshare.failed.title": "取消分享失败", + "toast.session.unshare.failed.description": "取消分享会话时发生错误", + + "toast.session.listFailed.title": "无法加载 {{project}} 的会话", + + "toast.update.title": "有可用更新", + "toast.update.description": "OpenCode 有新版本 ({{version}}) 可安装。", + "toast.update.action.installRestart": "安装并重启", + "toast.update.action.notYet": "稍后", + + "error.page.title": "出了点问题", + "error.page.description": "加载应用程序时发生错误。", + "error.page.details.label": "错误详情", + "error.page.action.restart": "重启", + "error.page.action.checking": "检查中...", + "error.page.action.checkUpdates": "检查更新", + "error.page.action.updateTo": "更新到 {{version}}", + "error.page.report.prefix": "请将此错误报告给 OpenCode 团队", + "error.page.report.discord": "在 Discord 上", + "error.page.version": "版本: {{version}}", + + "error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html? 或者 id 属性拼写错了?", + + "error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?", + + "error.chain.unknown": "未知错误", + "error.chain.causedBy": "原因:", + "error.chain.apiError": "API 错误", + "error.chain.status": "状态: {{status}}", + "error.chain.retryable": "可重试: {{retryable}}", + "error.chain.responseBody": "响应内容:\n{{body}}", + "error.chain.didYouMean": "你是不是想输入: {{suggestions}}", + "error.chain.modelNotFound": "未找到模型: {{provider}}/{{model}}", + "error.chain.checkConfig": "请检查你的配置 (opencode.json) 中的 provider/model 名称", + "error.chain.mcpFailed": 'MCP 服务器 "{{name}}" 启动失败。注意: OpenCode 暂不支持 MCP 认证。', + "error.chain.providerAuthFailed": "提供商认证失败 ({{provider}}): {{message}}", + "error.chain.providerInitFailed": '无法初始化提供商 "{{provider}}"。请检查凭据和配置。', + "error.chain.configJsonInvalid": "配置文件 {{path}} 不是有效的 JSON(C)", + "error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + '{{path}} 中的目录 "{{dir}}" 无效。请将目录重命名为 "{{suggestion}}" 或移除它。这是一个常见拼写错误。', + "error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter:\n{{message}}", + "error.chain.configInvalid": "配置文件 {{path}} 无效", + "error.chain.configInvalidWithMessage": "配置文件 {{path}} 无效: {{message}}", + + "notification.permission.title": "需要权限", + "notification.permission.description": "{{sessionTitle}}({{projectName}})需要权限", + "notification.question.title": "问题", + "notification.question.description": "{{sessionTitle}}({{projectName}})有一个问题", + "notification.action.goToSession": "前往会话", + + "notification.session.responseReady.title": "回复已就绪", + "notification.session.error.title": "会话错误", + "notification.session.error.fallbackDescription": "发生错误", + + "home.recentProjects": "最近项目", + "home.empty.title": "没有最近项目", + "home.empty.description": "通过打开本地项目开始使用", + + "session.tab.session": "会话", + "session.tab.review": "审查", + "session.tab.context": "上下文", + "session.review.filesChanged": "{{count}} 个文件变更", + "session.review.loadingChanges": "正在加载更改...", + "session.review.empty": "此会话暂无更改", + "session.messages.renderEarlier": "显示更早的消息", + "session.messages.loadingEarlier": "正在加载更早的消息...", + "session.messages.loadEarlier": "加载更早的消息", + "session.messages.loading": "正在加载消息...", + + "session.context.addToContext": "将 {{selection}} 添加到上下文", + + "session.new.worktree.main": "主分支", + "session.new.worktree.mainWithBranch": "主分支 ({{branch}})", + "session.new.worktree.create": "创建新的 worktree", + "session.new.lastModified": "最后修改", + + "session.header.search.placeholder": "搜索 {{project}}", + + "session.share.popover.title": "发布到网页", + "session.share.popover.description.shared": "此会话已在网页上公开。任何拥有链接的人都可以访问。", + "session.share.popover.description.unshared": "在网页上公开分享此会话。任何拥有链接的人都可以访问。", + "session.share.action.share": "分享", + "session.share.action.publish": "发布", + "session.share.action.publishing": "正在发布...", + "session.share.action.unpublish": "取消发布", + "session.share.action.unpublishing": "正在取消发布...", + "session.share.action.view": "查看", + "session.share.copy.copied": "已复制", + "session.share.copy.copyLink": "复制链接", + + "lsp.tooltip.none": "没有 LSP 服务器", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "正在加载提示...", + "terminal.loading": "正在加载终端...", + "terminal.title": "终端", + "terminal.title.numbered": "终端 {{number}}", + + "common.closeTab": "关闭标签页", + "common.dismiss": "忽略", + "common.requestFailed": "请求失败", + "common.moreOptions": "更多选项", + "common.learnMore": "了解更多", + "common.rename": "重命名", + "common.reset": "重置", + "common.delete": "删除", + "common.close": "关闭", + "common.edit": "编辑", + "common.loadMore": "加载更多", + + "sidebar.settings": "设置", + "sidebar.help": "帮助", + "sidebar.workspaces.enable": "启用工作区", + "sidebar.workspaces.disable": "禁用工作区", + "sidebar.gettingStarted.title": "入门", + "sidebar.gettingStarted.line1": "OpenCode 提供免费模型,你可以立即开始使用。", + "sidebar.gettingStarted.line2": "连接任意提供商即可使用更多模型,如 Claude、GPT、Gemini 等。", + "sidebar.project.recentSessions": "最近会话", + "sidebar.project.viewAllSessions": "查看全部会话", + + "settings.section.desktop": "桌面", + "settings.tab.general": "通用", + "settings.tab.shortcuts": "快捷键", + + "settings.general.section.appearance": "外观", + "settings.general.section.notifications": "系统通知", + "settings.general.section.sounds": "音效", + + "settings.general.row.language.title": "语言", + "settings.general.row.language.description": "更改 OpenCode 的显示语言", + "settings.general.row.appearance.title": "外观", + "settings.general.row.appearance.description": "自定义 OpenCode 在你的设备上的外观", + "settings.general.row.theme.title": "主题", + "settings.general.row.theme.description": "自定义 OpenCode 的主题。", + "settings.general.row.font.title": "字体", + "settings.general.row.font.description": "自定义代码块使用的等宽字体", + + "settings.general.notifications.agent.title": "智能体", + "settings.general.notifications.agent.description": "当智能体完成或需要注意时显示系统通知", + "settings.general.notifications.permissions.title": "权限", + "settings.general.notifications.permissions.description": "当需要权限时显示系统通知", + "settings.general.notifications.errors.title": "错误", + "settings.general.notifications.errors.description": "发生错误时显示系统通知", + + "settings.general.sounds.agent.title": "智能体", + "settings.general.sounds.agent.description": "当智能体完成或需要注意时播放声音", + "settings.general.sounds.permissions.title": "权限", + "settings.general.sounds.permissions.description": "当需要权限时播放声音", + "settings.general.sounds.errors.title": "错误", + "settings.general.sounds.errors.description": "发生错误时播放声音", + + "settings.shortcuts.title": "键盘快捷键", + "settings.shortcuts.reset.button": "重置为默认值", + "settings.shortcuts.reset.toast.title": "快捷键已重置", + "settings.shortcuts.reset.toast.description": "键盘快捷键已重置为默认设置。", + "settings.shortcuts.conflict.title": "快捷键已被占用", + "settings.shortcuts.conflict.description": "{{keybind}} 已分配给 {{titles}}。", + "settings.shortcuts.unassigned": "未设置", + "settings.shortcuts.pressKeys": "按下按键", + "settings.shortcuts.search.placeholder": "搜索快捷键", + "settings.shortcuts.search.empty": "未找到快捷键", + + "settings.shortcuts.group.general": "通用", + "settings.shortcuts.group.session": "会话", + "settings.shortcuts.group.navigation": "导航", + "settings.shortcuts.group.modelAndAgent": "模型与智能体", + "settings.shortcuts.group.terminal": "终端", + "settings.shortcuts.group.prompt": "提示", + + "settings.providers.title": "提供商", + "settings.providers.description": "提供商设置将在此处可配置。", + "settings.models.title": "模型", + "settings.models.description": "模型设置将在此处可配置。", + "settings.agents.title": "智能体", + "settings.agents.description": "智能体设置将在此处可配置。", + "settings.commands.title": "命令", + "settings.commands.description": "命令设置将在此处可配置。", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP 设置将在此处可配置。", + + "settings.permissions.title": "权限", + "settings.permissions.description": "控制服务器默认可以使用哪些工具。", + "settings.permissions.section.tools": "工具", + "settings.permissions.toast.updateFailed.title": "更新权限失败", + + "settings.permissions.action.allow": "允许", + "settings.permissions.action.ask": "询问", + "settings.permissions.action.deny": "拒绝", + + "settings.permissions.tool.read.title": "读取", + "settings.permissions.tool.read.description": "读取文件(匹配文件路径)", + "settings.permissions.tool.edit.title": "编辑", + "settings.permissions.tool.edit.description": "修改文件,包括编辑、写入、补丁和多重编辑", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "使用 glob 模式匹配文件", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "使用正则表达式搜索文件内容", + "settings.permissions.tool.list.title": "列表", + "settings.permissions.tool.list.description": "列出目录中的文件", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "运行 shell 命令", + "settings.permissions.tool.task.title": "Task", + "settings.permissions.tool.task.description": "启动子智能体", + "settings.permissions.tool.skill.title": "Skill", + "settings.permissions.tool.skill.description": "按名称加载技能", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "运行语言服务器查询", + "settings.permissions.tool.todoread.title": "读取待办", + "settings.permissions.tool.todoread.description": "读取待办列表", + "settings.permissions.tool.todowrite.title": "更新待办", + "settings.permissions.tool.todowrite.description": "更新待办列表", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "从 URL 获取内容", + "settings.permissions.tool.websearch.title": "Web Search", + "settings.permissions.tool.websearch.description": "搜索网页", + "settings.permissions.tool.codesearch.title": "Code Search", + "settings.permissions.tool.codesearch.description": "在网上搜索代码", + "settings.permissions.tool.external_directory.title": "外部目录", + "settings.permissions.tool.external_directory.description": "访问项目目录之外的文件", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "检测具有相同输入的重复工具调用", + + "workspace.new": "新建工作区", + "workspace.type.local": "本地", + "workspace.type.sandbox": "沙盒", + "workspace.create.failed.title": "创建工作区失败", + "workspace.delete.failed.title": "删除工作区失败", + "workspace.resetting.title": "正在重置工作区", + "workspace.resetting.description": "这可能需要一点时间。", + "workspace.reset.failed.title": "重置工作区失败", + "workspace.reset.success.title": "工作区已重置", + "workspace.reset.success.description": "工作区已与默认分支保持一致。", + "workspace.status.checking": "正在检查未合并的更改...", + "workspace.status.error": "无法验证 git 状态。", + "workspace.status.clean": "未检测到未合并的更改。", + "workspace.status.dirty": "检测到未合并的更改。", + "workspace.delete.title": "删除工作区", + "workspace.delete.confirm": '删除工作区 "{{name}}"?', + "workspace.delete.button": "删除工作区", + "workspace.reset.title": "重置工作区", + "workspace.reset.confirm": '重置工作区 "{{name}}"?', + "workspace.reset.button": "重置工作区", + "workspace.reset.archived.none": "不会归档任何活跃会话。", + "workspace.reset.archived.one": "将归档 1 个会话。", + "workspace.reset.archived.many": "将归档 {{count}} 个会话。", + "workspace.reset.note": "这将把工作区重置为与默认分支一致。", +} satisfies Partial> diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 6b75540a690..249ea7a0eb4 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -1,7 +1,7 @@ @import "@opencode-ai/ui/styles/tailwind"; :root { - /* Font is set via JavaScript in index.html and font-picker.tsx */ + /* Font is set via JavaScript in index.html */ /* Default fallback handled in inline script */ /* Safe area insets for mobile devices (notch, home indicator, etc.) */ @@ -167,6 +167,26 @@ body { } } +[data-component="markdown"] ul { + list-style: disc outside; + padding-left: 1.5rem; +} + +[data-component="markdown"] ol { + list-style: decimal outside; + padding-left: 1.5rem; +} + +[data-component="markdown"] li > p:first-child { + display: inline; + margin: 0; +} + +[data-component="markdown"] li > p + p { + display: block; + margin-top: 0.5rem; +} + *[data-tauri-drag-region] { app-region: drag; } diff --git a/packages/app/src/pages/error.tsx b/packages/app/src/pages/error.tsx index ba1b9cd2654..f27ec414c35 100644 --- a/packages/app/src/pages/error.tsx +++ b/packages/app/src/pages/error.tsx @@ -4,6 +4,7 @@ import { Button } from "@opencode-ai/ui/button" import { Component, Show } from "solid-js" import { createStore } from "solid-js/store" import { usePlatform } from "@/context/platform" +import { useLanguage } from "@/context/language" import { Icon } from "@opencode-ai/ui/icon" export type InitError = { @@ -11,6 +12,8 @@ export type InitError = { data: Record } +type Translator = ReturnType["t"] + function isInitError(error: unknown): error is InitError { return ( typeof error === "object" && @@ -38,30 +41,32 @@ function safeJson(value: unknown): string { return json ?? String(value) } -function formatInitError(error: InitError): string { +function formatInitError(error: InitError, t: Translator): string { const data = error.data switch (error.name) { - case "MCPFailed": - return `MCP server "${data.name}" failed. Note, opencode does not support MCP authentication yet.` + case "MCPFailed": { + const name = typeof data.name === "string" ? data.name : "" + return t("error.chain.mcpFailed", { name }) + } case "ProviderAuthError": { const providerID = typeof data.providerID === "string" ? data.providerID : "unknown" const message = typeof data.message === "string" ? data.message : safeJson(data.message) - return `Provider authentication failed (${providerID}): ${message}` + return t("error.chain.providerAuthFailed", { provider: providerID, message }) } case "APIError": { - const message = typeof data.message === "string" ? data.message : "API error" + const message = typeof data.message === "string" ? data.message : t("error.chain.apiError") const lines: string[] = [message] if (typeof data.statusCode === "number") { - lines.push(`Status: ${data.statusCode}`) + lines.push(t("error.chain.status", { status: data.statusCode })) } if (typeof data.isRetryable === "boolean") { - lines.push(`Retryable: ${data.isRetryable}`) + lines.push(t("error.chain.retryable", { retryable: data.isRetryable })) } if (typeof data.responseBody === "string" && data.responseBody) { - lines.push(`Response body:\n${data.responseBody}`) + lines.push(t("error.chain.responseBody", { body: data.responseBody })) } return lines.join("\n") @@ -72,24 +77,39 @@ function formatInitError(error: InitError): string { modelID: string suggestions?: string[] } + + const suggestionsLine = + Array.isArray(suggestions) && suggestions.length + ? [t("error.chain.didYouMean", { suggestions: suggestions.join(", ") })] + : [] + return [ - `Model not found: ${providerID}/${modelID}`, - ...(Array.isArray(suggestions) && suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []), - `Check your config (opencode.json) provider/model names`, + t("error.chain.modelNotFound", { provider: providerID, model: modelID }), + ...suggestionsLine, + t("error.chain.checkConfig"), ].join("\n") } case "ProviderInitError": { const providerID = typeof data.providerID === "string" ? data.providerID : "unknown" - return `Failed to initialize provider "${providerID}". Check credentials and configuration.` + return t("error.chain.providerInitFailed", { provider: providerID }) } case "ConfigJsonError": { + const path = typeof data.path === "string" ? data.path : safeJson(data.path) const message = typeof data.message === "string" ? data.message : "" - return `Config file at ${data.path} is not valid JSON(C)` + (message ? `: ${message}` : "") + if (message) return t("error.chain.configJsonInvalidWithMessage", { path, message }) + return t("error.chain.configJsonInvalid", { path }) + } + case "ConfigDirectoryTypoError": { + const path = typeof data.path === "string" ? data.path : safeJson(data.path) + const dir = typeof data.dir === "string" ? data.dir : safeJson(data.dir) + const suggestion = typeof data.suggestion === "string" ? data.suggestion : safeJson(data.suggestion) + return t("error.chain.configDirectoryTypo", { dir, path, suggestion }) + } + case "ConfigFrontmatterError": { + const path = typeof data.path === "string" ? data.path : safeJson(data.path) + const message = typeof data.message === "string" ? data.message : safeJson(data.message) + return t("error.chain.configFrontmatterError", { path, message }) } - case "ConfigDirectoryTypoError": - return `Directory "${data.dir}" in ${data.path} is not valid. Rename the directory to "${data.suggestion}" or remove it. This is a common typo.` - case "ConfigFrontmatterError": - return `Failed to parse frontmatter in ${data.path}:\n${data.message}` case "ConfigInvalidError": { const issues = Array.isArray(data.issues) ? data.issues.map( @@ -97,7 +117,13 @@ function formatInitError(error: InitError): string { ) : [] const message = typeof data.message === "string" ? data.message : "" - return [`Config file at ${data.path} is invalid` + (message ? `: ${message}` : ""), ...issues].join("\n") + const path = typeof data.path === "string" ? data.path : safeJson(data.path) + + const line = message + ? t("error.chain.configInvalidWithMessage", { path, message }) + : t("error.chain.configInvalid", { path }) + + return [line, ...issues].join("\n") } case "UnknownError": return typeof data.message === "string" ? data.message : safeJson(data) @@ -107,20 +133,20 @@ function formatInitError(error: InitError): string { } } -function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): string { - if (!error) return "Unknown error" +function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessage?: string): string { + if (!error) return t("error.chain.unknown") if (isInitError(error)) { - const message = formatInitError(error) + const message = formatInitError(error, t) if (depth > 0 && parentMessage === message) return "" - const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : "" + const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : "" return indent + `${error.name}\n${message}` } if (error instanceof Error) { const isDuplicate = depth > 0 && parentMessage === error.message const parts: string[] = [] - const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : "" + const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : "" const header = `${error.name}${error.message ? `: ${error.message}` : ""}` const stack = error.stack?.trim() @@ -153,7 +179,7 @@ function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): st } if (error.cause) { - const causeResult = formatErrorChain(error.cause, depth + 1, error.message) + const causeResult = formatErrorChain(error.cause, t, depth + 1, error.message) if (causeResult) { parts.push(causeResult) } @@ -164,16 +190,16 @@ function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): st if (typeof error === "string") { if (depth > 0 && parentMessage === error) return "" - const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : "" + const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : "" return indent + error } - const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : "" + const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : "" return indent + safeJson(error) } -function formatError(error: unknown): string { - return formatErrorChain(error, 0) +function formatError(error: unknown, t: Translator): string { + return formatErrorChain(error, t, 0) } interface ErrorPageProps { @@ -182,6 +208,7 @@ interface ErrorPageProps { export const ErrorPage: Component = (props) => { const platform = usePlatform() + const language = useLanguage() const [store, setStore] = createStore({ checking: false, version: undefined as string | undefined, @@ -206,51 +233,55 @@ export const ErrorPage: Component = (props) => {
-

Something went wrong

-

An error occurred while loading the application.

+

{language.t("error.page.title")}

+

{language.t("error.page.description")}

- {store.checking ? "Checking..." : "Check for updates"} + {store.checking + ? language.t("error.page.action.checking") + : language.t("error.page.action.checkUpdates")} } >
- Please report this error to the shuvcode team + {language.t("error.page.report.prefix")}
-

Version: {platform.version}

+ {(version) => ( +

{language.t("error.page.version", { version: version() })}

+ )}
diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 5213c945a71..fd7c5709a1d 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -62,12 +62,7 @@ export default function Home() {
Recent projects
- diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 5d3536a48ed..1c77fb34592 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -21,7 +21,6 @@ import { useGlobalSync } from "@/context/global-sync" import { Persist, persisted } from "@/utils/persist" import { base64Decode, base64Encode } from "@opencode-ai/util/encode" import { Avatar } from "@opencode-ai/ui/avatar" -import { AsciiLogo, AsciiMark } from "@opencode-ai/ui/logo" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" @@ -61,20 +60,18 @@ import { playSound, soundSrc } from "@/utils/sound" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme" -import { FontPicker } from "@/components/font-picker" -import { ThemePicker, DialogSelectTheme } from "@/components/theme-picker" import { DialogSelectProvider } from "@/components/dialog-select-provider" import { DialogSelectServer } from "@/components/dialog-select-server" import { DialogSettings } from "@/components/dialog-settings" import { useCommand, type CommandOption } from "@/context/command" import { ConstrainDragXAxis } from "@/utils/solid-dnd" import { navStart } from "@/utils/perf" -import { DialogCreateProject } from "@/components/dialog-create-project" +import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { DialogSessionRenameGlobal } from "@/components/dialog-session-rename-global" import { DialogEditProject } from "@/components/dialog-edit-project" import { Titlebar } from "@/components/titlebar" import { useServer } from "@/context/server" -import { applyTheme } from "@/theme/apply-theme" +import { useLanguage, type Locale } from "@/context/language" export default function Layout(props: ParentProps) { const [store, setStore, , ready] = persisted( @@ -93,11 +90,18 @@ export default function Layout(props: ParentProps) { const pageReady = createMemo(() => ready()) let scrollContainerRef: HTMLDivElement | undefined + const smQuery = window.matchMedia("(min-width: 640px)") const xlQuery = window.matchMedia("(min-width: 1280px)") + const [isSmallViewport, setIsSmallViewport] = createSignal(smQuery.matches) const [isLargeViewport, setIsLargeViewport] = createSignal(xlQuery.matches) - const handleViewportChange = (e: MediaQueryListEvent) => setIsLargeViewport(e.matches) - xlQuery.addEventListener("change", handleViewportChange) - onCleanup(() => xlQuery.removeEventListener("change", handleViewportChange)) + const handleSmallViewportChange = (e: MediaQueryListEvent) => setIsSmallViewport(e.matches) + const handleLargeViewportChange = (e: MediaQueryListEvent) => setIsLargeViewport(e.matches) + smQuery.addEventListener("change", handleSmallViewportChange) + xlQuery.addEventListener("change", handleLargeViewportChange) + onCleanup(() => { + smQuery.removeEventListener("change", handleSmallViewportChange) + xlQuery.removeEventListener("change", handleLargeViewportChange) + }) const params = useParams() const [autoselect, setAutoselect] = createSignal(!params.dir) @@ -115,14 +119,16 @@ export default function Layout(props: ParentProps) { const dialog = useDialog() const command = useCommand() const theme = useTheme() + const language = useLanguage() const initialDir = params.dir const availableThemeEntries = createMemo(() => Object.entries(theme.themes())) const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"] - const colorSchemeLabel: Record = { - system: "System", - light: "Light", - dark: "Dark", + const colorSchemeKey: Record = { + system: "theme.scheme.system", + light: "theme.scheme.light", + dark: "theme.scheme.dark", } + const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme]) const [editor, setEditor] = createStore({ active: "" as string, @@ -141,6 +147,8 @@ export default function Layout(props: ParentProps) { const isBusy = (directory: string) => busyWorkspaces().has(workspaceKey(directory)) const editorRef = { current: undefined as HTMLInputElement | undefined } + const [hoverSession, setHoverSession] = createSignal() + const autoselecting = createMemo(() => { if (params.dir) return false if (initialDir) return false @@ -256,7 +264,7 @@ export default function Layout(props: ParentProps) { theme.setTheme(nextThemeId) const nextTheme = theme.themes()[nextThemeId] showToast({ - title: "Theme switched", + title: language.t("toast.theme.title"), description: nextTheme?.name ?? nextThemeId, }) } @@ -269,11 +277,29 @@ export default function Layout(props: ParentProps) { const next = colorSchemeOrder[nextIndex] theme.setColorScheme(next) showToast({ - title: "Color scheme", - description: colorSchemeLabel[next], + title: language.t("toast.scheme.title"), + description: colorSchemeLabel(next), + }) + } + + function setLocale(next: Locale) { + if (next === language.locale()) return + language.setLocale(next) + showToast({ + title: language.t("toast.language.title"), + description: language.t("toast.language.description", { language: language.label(next) }), }) } + function cycleLanguage(direction = 1) { + const locales = language.locales + const currentIndex = locales.indexOf(language.locale()) + const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + locales.length) % locales.length + const next = locales[nextIndex] + if (!next) return + setLocale(next) + } + onMount(() => { if (!platform.checkUpdate || !platform.update || !platform.restart) return @@ -285,18 +311,18 @@ export default function Layout(props: ParentProps) { toastId = showToast({ persistent: true, icon: "download", - title: "Update available", - description: `A new version of OpenCode (${version}) is now available to install.`, + title: language.t("toast.update.title"), + description: language.t("toast.update.description", { version: version ?? "" }), actions: [ { - label: "Install and restart", + label: language.t("toast.update.action.installRestart"), onClick: async () => { await platform.update!() await platform.restart!() }, }, { - label: "Not yet", + label: language.t("toast.update.action.notYet"), onClick: "dismiss", }, ], @@ -310,27 +336,17 @@ export default function Layout(props: ParentProps) { }) onMount(() => { - const alerts = { - "permission.asked": { - title: "Permission required", - icon: "checklist" as const, - description: (sessionTitle: string, projectName: string) => - `${sessionTitle} in ${projectName} needs permission`, - }, - "question.asked": { - title: "Question", - icon: "bubble-5" as const, - description: (sessionTitle: string, projectName: string) => `${sessionTitle} in ${projectName} has a question`, - }, - } - const toastBySession = new Map() const alertedAtBySession = new Map() const cooldownMs = 5000 const unsub = globalSDK.event.listen((e) => { if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return - const config = alerts[e.details.type] + const title = + e.details.type === "permission.asked" + ? language.t("notification.permission.title") + : language.t("notification.question.title") + const icon = e.details.type === "permission.asked" ? ("checklist" as const) : ("bubble-5" as const) const directory = e.name const props = e.details.properties if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return @@ -339,9 +355,12 @@ export default function Layout(props: ParentProps) { const session = store.session.find((s) => s.id === props.sessionID) const sessionKey = `${directory}:${props.sessionID}` - const sessionTitle = session?.title ?? "New session" + const sessionTitle = session?.title ?? language.t("command.session.new") const projectName = getFilename(directory) - const description = config.description(sessionTitle, projectName) + const description = + e.details.type === "permission.asked" + ? language.t("notification.permission.description", { sessionTitle, projectName }) + : language.t("notification.question.description", { sessionTitle, projectName }) const href = `/${base64Encode(directory)}/session/${props.sessionID}` const now = Date.now() @@ -352,13 +371,13 @@ export default function Layout(props: ParentProps) { if (e.details.type === "permission.asked") { playSound(soundSrc(settings.sounds.permissions())) if (settings.notifications.permissions()) { - void platform.notify(config.title, description, href) + void platform.notify(title, description, href) } } if (e.details.type === "question.asked") { if (settings.notifications.agent()) { - void platform.notify(config.title, description, href) + void platform.notify(title, description, href) } } @@ -372,16 +391,16 @@ export default function Layout(props: ParentProps) { const toastId = showToast({ persistent: true, - icon: config.icon, - title: config.title, + icon, + title, description, actions: [ { - label: "Go to session", + label: language.t("notification.action.goToSession"), onClick: () => navigate(href), }, { - label: "Dismiss", + label: language.t("common.dismiss"), onClick: "dismiss", }, ], @@ -813,41 +832,55 @@ export default function Layout(props: ParentProps) { const commands: CommandOption[] = [ { id: "sidebar.toggle", - title: "Toggle sidebar", - category: "View", + title: language.t("command.sidebar.toggle"), + category: language.t("command.category.view"), keybind: "mod+b", onSelect: () => layout.sidebar.toggle(), }, + { + id: "project.open", + title: language.t("command.project.open"), + category: language.t("command.category.project"), + keybind: "mod+o", + onSelect: () => chooseProject(), + }, { id: "provider.connect", - title: "Connect provider", - category: "Provider", + title: language.t("command.provider.connect"), + category: language.t("command.category.provider"), onSelect: () => connectProvider(), }, { id: "server.switch", - title: "Switch server", - category: "Server", + title: language.t("command.server.switch"), + category: language.t("command.category.server"), onSelect: () => openServer(), }, + { + id: "settings.open", + title: language.t("command.settings.open"), + category: language.t("command.category.settings"), + keybind: "mod+comma", + onSelect: () => openSettings(), + }, { id: "session.previous", - title: "Previous session", - category: "Session", + title: language.t("command.session.previous"), + category: language.t("command.category.session"), keybind: "alt+arrowup", onSelect: () => navigateSessionByOffset(-1), }, { id: "session.next", - title: "Next session", - category: "Session", + title: language.t("command.session.next"), + category: language.t("command.category.session"), keybind: "alt+arrowdown", onSelect: () => navigateSessionByOffset(1), }, { id: "session.archive", - title: "Archive session", - category: "Session", + title: language.t("command.session.archive"), + category: language.t("command.category.session"), keybind: "mod+shift+backspace", disabled: !params.dir || !params.id, onSelect: () => { @@ -855,19 +888,10 @@ export default function Layout(props: ParentProps) { if (session) archiveSession(session) }, }, - { - id: "theme.picker", - title: "Theme picker", - category: "Theme", - onSelect: () => { - const originalTheme = theme.themeId() - dialog.show(() => , () => applyTheme(theme.themeId())) - }, - }, { id: "theme.cycle", - title: "Cycle theme", - category: "Theme", + title: language.t("command.theme.cycle"), + category: language.t("command.category.theme"), keybind: "mod+shift+t", onSelect: () => cycleTheme(1), }, @@ -876,8 +900,8 @@ export default function Layout(props: ParentProps) { for (const [id, definition] of availableThemeEntries()) { commands.push({ id: `theme.set.${id}`, - title: `Use theme: ${definition.name ?? id}`, - category: "Theme", + title: language.t("command.theme.set", { theme: definition.name ?? id }), + category: language.t("command.category.theme"), onSelect: () => theme.commitPreview(), onHighlight: () => { theme.previewTheme(id) @@ -888,8 +912,8 @@ export default function Layout(props: ParentProps) { commands.push({ id: "theme.scheme.cycle", - title: "Cycle color scheme", - category: "Theme", + title: language.t("command.theme.scheme.cycle"), + category: language.t("command.category.theme"), keybind: "mod+shift+s", onSelect: () => cycleColorScheme(1), }) @@ -897,8 +921,8 @@ export default function Layout(props: ParentProps) { for (const scheme of colorSchemeOrder) { commands.push({ id: `theme.scheme.${scheme}`, - title: `Use color scheme: ${colorSchemeLabel[scheme]}`, - category: "Theme", + title: language.t("command.theme.scheme.set", { scheme: colorSchemeLabel(scheme) }), + category: language.t("command.category.theme"), onSelect: () => theme.commitPreview(), onHighlight: () => { theme.previewColorScheme(scheme) @@ -907,6 +931,22 @@ export default function Layout(props: ParentProps) { }) } + commands.push({ + id: "language.cycle", + title: language.t("command.language.cycle"), + category: language.t("command.category.language"), + onSelect: () => cycleLanguage(1), + }) + + for (const locale of language.locales) { + commands.push({ + id: `language.set.${locale}`, + title: language.t("command.language.set", { language: language.label(locale) }), + category: language.t("command.category.language"), + onSelect: () => setLocale(locale), + }) + } + return commands }) @@ -922,10 +962,6 @@ export default function Layout(props: ParentProps) { dialog.show(() => ) } - function createProject() { - dialog.show(() => ) - } - function navigateToProject(directory: string | undefined) { if (!directory) return server.projects.touch(directory) @@ -952,7 +988,7 @@ export default function Layout(props: ParentProps) { const current = displayName(project) if (next === current) return const name = next === getFilename(project.worktree) ? "" : next - await globalSDK.client.project.update({ projectID: project.id, name }) + await globalSDK.client.project.update({ projectID: project.id, directory: project.worktree, name }) } async function renameSession(session: Session, next: string) { @@ -978,6 +1014,31 @@ export default function Layout(props: ParentProps) { else navigate("/") } + async function chooseProject() { + function resolve(result: string | string[] | null) { + if (Array.isArray(result)) { + for (const directory of result) { + openProject(directory, false) + } + navigateToProject(result[0]) + } else if (result) { + openProject(result) + } + } + + if (platform.openDirectoryPickerDialog && server.isLocal()) { + const result = await platform.openDirectoryPickerDialog?.({ + title: language.t("command.project.open"), + multiple: true, + }) + resolve(result) + } else { + dialog.show( + () => , + () => resolve(null), + ) + } + } const errorMessage = (err: unknown) => { if (err && typeof err === "object" && "data" in err) { @@ -985,7 +1046,7 @@ export default function Layout(props: ParentProps) { if (data?.message) return data.message } if (err instanceof Error) return err.message - return "Request failed" + return language.t("common.requestFailed") } const deleteWorkspace = async (directory: string) => { @@ -1000,7 +1061,7 @@ export default function Layout(props: ParentProps) { .then((x) => x.data) .catch((err) => { showToast({ - title: "Failed to delete workspace", + title: language.t("workspace.delete.failed.title"), description: errorMessage(err), }) return false @@ -1022,9 +1083,15 @@ export default function Layout(props: ParentProps) { const current = currentProject() if (!current) return if (directory === current.worktree) return - setBusy(directory, true) + const progress = showToast({ + persistent: true, + title: language.t("workspace.resetting.title"), + description: language.t("workspace.resetting.description"), + }) + const dismiss = () => toaster.dismiss(progress) + const sessions = await globalSDK.client.session .list({ directory }) .then((x) => x.data ?? []) @@ -1035,7 +1102,7 @@ export default function Layout(props: ParentProps) { .then((x) => x.data) .catch((err) => { showToast({ - title: "Failed to reset workspace", + title: language.t("workspace.reset.failed.title"), description: errorMessage(err), }) return false @@ -1043,6 +1110,7 @@ export default function Layout(props: ParentProps) { if (!result) { setBusy(directory, false) + dismiss() return } @@ -1064,14 +1132,15 @@ export default function Layout(props: ParentProps) { await globalSDK.client.instance.dispose({ directory }).catch(() => undefined) setBusy(directory, false) + dismiss() const href = `/${base64Encode(directory)}/session` navigate(href) layout.mobileSidebar.hide() showToast({ - title: "Workspace reset", - description: "Workspace now matches the default branch.", + title: language.t("workspace.reset.success.title"), + description: language.t("workspace.reset.success.description"), }) } @@ -1107,25 +1176,27 @@ export default function Layout(props: ParentProps) { } const description = () => { - if (data.status === "loading") return "Checking for unmerged changes..." - if (data.status === "error") return "Unable to verify git status." - if (!data.dirty) return "No unmerged changes detected." - return "Unmerged changes detected in this workspace." + if (data.status === "loading") return language.t("workspace.status.checking") + if (data.status === "error") return language.t("workspace.status.error") + if (!data.dirty) return language.t("workspace.status.clean") + return language.t("workspace.status.dirty") } return ( - -
+ +
- Delete workspace "{name()}"? + + {language.t("workspace.delete.confirm", { name: name() })} + {description()}
@@ -1178,34 +1249,36 @@ export default function Layout(props: ParentProps) { const archivedCount = () => state.sessions.length const description = () => { - if (state.status === "loading") return "Checking for unmerged changes..." - if (state.status === "error") return "Unable to verify git status." - if (!state.dirty) return "No unmerged changes detected." - return "Unmerged changes detected in this workspace." + if (state.status === "loading") return language.t("workspace.status.checking") + if (state.status === "error") return language.t("workspace.status.error") + if (!state.dirty) return language.t("workspace.status.clean") + return language.t("workspace.status.dirty") } const archivedLabel = () => { const count = archivedCount() - if (count === 0) return "No active sessions will be archived." - const label = count === 1 ? "1 session" : `${count} sessions` - return `${label} will be archived.` + if (count === 0) return language.t("workspace.reset.archived.none") + if (count === 1) return language.t("workspace.reset.archived.one") + return language.t("workspace.reset.archived.many", { count }) } return ( - -
+ +
- Reset workspace "{name()}"? + + {language.t("workspace.reset.confirm", { name: name() })} + - {description()} {archivedLabel()} This will reset the workspace to match the default branch. + {description()} {archivedLabel()} {language.t("workspace.reset.note")}
@@ -1478,8 +1551,19 @@ export default function Layout(props: ParentProps) { } > - - Loading messages…
}> + setHoverSession(open ? props.session.id : undefined)} + > + {language.t("session.messages.loading")}
} + > - archiveSession(props.session)} /> + archiveSession(props.session)} + aria-label={language.t("command.session.archive")} + />
@@ -1554,7 +1643,8 @@ export default function Layout(props: ParentProps) { if (!directory) return const [workspaceStore] = globalSync.child(directory) - const kind = directory === project.worktree ? "local" : "sandbox" + const kind = + directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") const name = workspaceLabel(directory, workspaceStore.vcs?.branch, project.id) return `${kind} : ${name}` }) @@ -1621,7 +1711,9 @@ export default function Layout(props: ParentProps) {
- {local() ? "local" : "sandbox"} : + + {local() ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")} : + - - + + navigate(`/${slug()}/session`)}> - New session + {language.t("command.session.new")} - Rename + {language.t("common.rename")} dialog.show(() => )} > - Reset + {language.t("common.reset")} dialog.show(() => )} > - Delete + {language.t("common.delete")} - + navigate(`/${slug()}/session`)} + aria-label={language.t("command.session.new")} />
@@ -1749,7 +1852,7 @@ export default function Layout(props: ParentProps) { icon="edit" class="hidden _flex w-full text-left justify-start text-text-base rounded-md px-3" > - New session + {language.t("command.session.new")} @@ -1768,7 +1871,7 @@ export default function Layout(props: ParentProps) { ;(e.currentTarget as HTMLButtonElement).blur() }} > - Load more + {language.t("common.loadMore")}
@@ -1792,7 +1895,8 @@ export default function Layout(props: ParentProps) { const label = (directory: string) => { const [data] = globalSync.child(directory) - const kind = directory === props.project.worktree ? "local" : "sandbox" + const kind = + directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") const name = workspaceLabel(directory, data.vcs?.branch, props.project.id) return `${kind} : ${name}` } @@ -1815,9 +1919,11 @@ export default function Layout(props: ParentProps) { .slice(0, 2) } + const projectName = () => props.project.name || getFilename(props.project.worktree) const trigger = (
@@ -1952,7 +2061,7 @@ export default function Layout(props: ParentProps) { ;(e.currentTarget as HTMLButtonElement).blur() }} > - Load more + {language.t("common.loadMore")}
@@ -1983,7 +2092,7 @@ export default function Layout(props: ParentProps) { .then((x) => x.data) .catch((err) => { showToast({ - title: "Failed to create workspace", + title: language.t("workspace.create.failed.title"), description: errorMessage(err), }) return undefined @@ -1998,8 +2107,8 @@ export default function Layout(props: ParentProps) { command.register(() => [ { id: "workspace.new", - title: "New workspace", - category: "Workspace", + title: language.t("workspace.new"), + category: language.t("command.category.workspace"), keybind: "mod+shift+w", disabled: !layout.sidebar.workspaces(project()?.worktree ?? "")(), onSelect: createWorkspace, @@ -2009,68 +2118,82 @@ export default function Layout(props: ParentProps) { const homedir = createMemo(() => sync.data.path.home) return ( -
- -
-
-
- - - -
- p.worktree)}> - - {(project) => } - - - - - -
- - - -
-
-
- - - - - platform.openLink("https://opencode.ai/desktop-feedback")} - /> - -
+
+
+
+ + + +
+ p.worktree)}> + + {(project) => } + + + + {language.t("command.project.open")} + + {command.keybind("project.open")} + +
+ } + > + + +
+ + + +
- - -
+ + + + + platform.openLink("https://opencode.ai/desktop-feedback")} + aria-label={language.t("sidebar.help")} + /> + +
+
+ + +
{(p) => ( <> @@ -2108,20 +2231,23 @@ export default function Layout(props: ParentProps) { icon="dot-grid" variant="ghost" class="shrink-0 size-6 rounded-md opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100 data-[expanded]:bg-surface-base-active" + aria-label={language.t("common.moreOptions")} /> dialog.show(() => )}> - Edit + {language.t("common.edit")} layout.sidebar.toggleWorkspaces(p.worktree)}> - {layout.sidebar.workspaces(p.worktree)() ? "Disable workspaces" : "Enable workspaces"} + {layout.sidebar.workspaces(p.worktree)() + ? language.t("sidebar.workspaces.disable") + : language.t("sidebar.workspaces.enable")} closeProject(p.worktree)}> - Close + {language.t("common.close")} @@ -2134,7 +2260,11 @@ export default function Layout(props: ParentProps) { fallback={ <>
- +
@@ -2157,12 +2287,12 @@ export default function Layout(props: ParentProps) { <>
@@ -2204,9 +2334,9 @@ export default function Layout(props: ParentProps) {
-
Getting started
-
OpenCode includes free models so you can start immediately.
-
Connect any provider to use models, inc. Claude, GPT, Gemini etc.
+
{language.t("sidebar.gettingStarted.title")}
+
{language.t("sidebar.gettingStarted.line1")}
+
{language.t("sidebar.gettingStarted.line2")}
-
-
- - - -
- - -
-
- v{__APP_VERSION__} ({__COMMIT_HASH__}) -
-
-
-
) } return (
-
+
@@ -2317,15 +2406,21 @@ export default function Layout(props: ParentProps) {
}> - -
- {props.children} -
+ + {props.children} +
+ } + > +
{props.children}
+
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index f40c18973ba..b3fc0cb7bd5 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1,4 +1,4 @@ -import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on } from "solid-js" +import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on, createSignal } from "solid-js" import { createMediaQuery } from "@solid-primitives/media" import { createResizeObserver } from "@solid-primitives/resize-observer" import { Dynamic } from "solid-js/web" @@ -32,6 +32,7 @@ import { DialogSelectModel } from "@/components/dialog-select-model" import { DialogSelectMcp } from "@/components/dialog-select-mcp" import { DialogFork } from "@/components/dialog-fork" import { useCommand } from "@/context/command" +import { useLanguage } from "@/context/language" import { useNavigate, useParams } from "@solidjs/router" import { UserMessage } from "@opencode-ai/sdk/v2" import type { FileDiff } from "@opencode-ai/sdk/v2/client" @@ -162,6 +163,7 @@ export default function Page() { const dialog = useDialog() const codeComponent = useCodeComponent() const command = useCommand() + const language = useLanguage() const platform = usePlatform() const params = useParams() const navigate = useNavigate() @@ -172,6 +174,8 @@ export default function Page() { // Initialize keyboard visibility tracking for mobile terminal support useKeyboardVisibility() + const [pendingMessage, setPendingMessage] = createSignal(undefined) + const [pendingHash, setPendingHash] = createSignal(undefined) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) const view = createMemo(() => layout.view(sessionKey())) @@ -396,14 +400,7 @@ export default function Page() { const current = activeMessage() const currentIndex = current ? msgs.findIndex((m) => m.id === current.id) : -1 - - let targetIndex: number - if (currentIndex === -1) { - targetIndex = offset > 0 ? 0 : msgs.length - 1 - } else { - targetIndex = currentIndex + offset - } - + const targetIndex = currentIndex === -1 ? (offset > 0 ? 0 : msgs.length - 1) : currentIndex + offset if (targetIndex < 0 || targetIndex >= msgs.length) return scrollToMessage(msgs[targetIndex], "auto") @@ -427,11 +424,16 @@ export default function Page() { sync.session.sync(params.id) }) + const [autoCreated, setAutoCreated] = createSignal(false) + createEffect(() => { - if (!view().terminal.opened()) return - if (!terminal.ready()) return - if (terminal.all().length !== 0) return + if (!view().terminal.opened()) { + setAutoCreated(false) + return + } + if (!terminal.ready() || terminal.all().length !== 0 || autoCreated()) return terminal.new() + setAutoCreated(true) }) createEffect( @@ -447,6 +449,32 @@ export default function Page() { ), ) + createEffect( + on( + () => terminal.active(), + (activeId) => { + if (!activeId || !view().terminal.opened()) return + // Immediately remove focus + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur() + } + const wrapper = document.getElementById(`terminal-wrapper-${activeId}`) + const element = wrapper?.querySelector('[data-component="terminal"]') as HTMLElement + if (!element) return + + // Find and focus the ghostty textarea (the actual input element) + const textarea = element.querySelector("textarea") as HTMLTextAreaElement + if (textarea) { + textarea.focus() + return + } + // Fallback: focus container and dispatch pointer event + element.focus() + element.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true })) + }, + ), + ) + createEffect( on( () => visibleUserMessages().at(-1)?.id, @@ -481,51 +509,51 @@ export default function Page() { command.register(() => [ { id: "session.new", - title: "New session", - category: "Session", + title: language.t("command.session.new"), + category: language.t("command.category.session"), keybind: "mod+shift+s", slash: "new", onSelect: () => navigate(`/${params.dir}/session`), }, { id: "file.open", - title: "Open file", - description: "Search files and commands", - category: "File", + title: language.t("command.file.open"), + description: language.t("command.file.open.description"), + category: language.t("command.category.file"), keybind: "mod+p", slash: "open", onSelect: () => dialog.show(() => ), }, { id: "terminal.toggle", - title: "Toggle terminal", + title: language.t("command.terminal.toggle"), description: "", - category: "View", + category: language.t("command.category.view"), keybind: "ctrl+`", slash: "terminal", onSelect: () => view().terminal.toggle(), }, { id: "review.toggle", - title: "Toggle review", + title: language.t("command.review.toggle"), description: "", - category: "View", + category: language.t("command.category.view"), keybind: "mod+shift+r", onSelect: () => view().reviewPanel.toggle(), }, { id: "terminal.new", - title: "New terminal", - description: "Create a new terminal tab", - category: "Terminal", + title: language.t("command.terminal.new"), + description: language.t("command.terminal.new.description"), + category: language.t("command.category.terminal"), keybind: "ctrl+alt+t", onSelect: () => terminal.new(), }, { id: "steps.toggle", - title: "Toggle steps", - description: "Show or hide steps for the current message", - category: "View", + title: language.t("command.steps.toggle"), + description: language.t("command.steps.toggle.description"), + category: language.t("command.category.view"), keybind: "mod+e", slash: "steps", disabled: !params.id, @@ -537,54 +565,54 @@ export default function Page() { }, { id: "message.previous", - title: "Previous message", - description: "Go to the previous user message", - category: "Session", + title: language.t("command.message.previous"), + description: language.t("command.message.previous.description"), + category: language.t("command.category.session"), keybind: "mod+arrowup", disabled: !params.id, onSelect: () => navigateMessageByOffset(-1), }, { id: "message.next", - title: "Next message", - description: "Go to the next user message", - category: "Session", + title: language.t("command.message.next"), + description: language.t("command.message.next.description"), + category: language.t("command.category.session"), keybind: "mod+arrowdown", disabled: !params.id, onSelect: () => navigateMessageByOffset(1), }, { id: "model.choose", - title: "Choose model", - description: "Select a different model", - category: "Model", + title: language.t("command.model.choose"), + description: language.t("command.model.choose.description"), + category: language.t("command.category.model"), keybind: "mod+'", slash: "model", onSelect: () => dialog.show(() => ), }, { id: "mcp.toggle", - title: "Toggle MCPs", - description: "Toggle MCPs", - category: "MCP", + title: language.t("command.mcp.toggle"), + description: language.t("command.mcp.toggle.description"), + category: language.t("command.category.mcp"), keybind: "mod+;", slash: "mcp", onSelect: () => dialog.show(() => ), }, { id: "agent.cycle", - title: "Cycle agent", - description: "Switch to the next agent", - category: "Agent", + title: language.t("command.agent.cycle"), + description: language.t("command.agent.cycle.description"), + category: language.t("command.category.agent"), keybind: "mod+.", slash: "agent", onSelect: () => local.agent.move(1), }, { id: "agent.cycle.reverse", - title: "Cycle agent backwards", - description: "Switch to the previous agent", - category: "Agent", + title: language.t("command.agent.cycle.reverse"), + description: language.t("command.agent.cycle.reverse.description"), + category: language.t("command.category.agent"), keybind: "shift+mod+.", onSelect: () => local.agent.move(-1), }, @@ -606,30 +634,31 @@ export default function Page() { id: "permissions.autoaccept", title: params.id && permission.isAutoAccepting(params.id, sdk.directory) - ? "Stop auto-accepting edits" - : "Auto-accept edits", - category: "Permissions", + ? language.t("command.permissions.autoaccept.disable") + : language.t("command.permissions.autoaccept.enable"), + category: language.t("command.category.permissions"), keybind: "mod+shift+a", disabled: !params.id || !permission.permissionsEnabled(), onSelect: () => { const sessionID = params.id if (!sessionID) return permission.toggleAutoAccept(sessionID, sdk.directory) + const enabled = permission.isAutoAccepting(sessionID, sdk.directory) showToast({ - title: permission.isAutoAccepting(sessionID, sdk.directory) - ? "Auto-accepting edits" - : "Stopped auto-accepting edits", - description: permission.isAutoAccepting(sessionID, sdk.directory) - ? "Edit and write permissions will be automatically approved" - : "Edit and write permissions will require approval", + title: enabled + ? language.t("toast.permissions.autoaccept.on.title") + : language.t("toast.permissions.autoaccept.off.title"), + description: enabled + ? language.t("toast.permissions.autoaccept.on.description") + : language.t("toast.permissions.autoaccept.off.description"), }) }, }, { id: "session.undo", - title: "Undo", - description: "Undo the last message", - category: "Session", + title: language.t("command.session.undo"), + description: language.t("command.session.undo.description"), + category: language.t("command.category.session"), slash: "undo", disabled: !params.id || visibleUserMessages().length === 0, onSelect: async () => { @@ -646,7 +675,10 @@ export default function Page() { // Restore the prompt from the reverted message const parts = sync.data.part[message.id] if (parts) { - const restored = extractPromptFromParts(parts, { directory: sdk.directory }) + const restored = extractPromptFromParts(parts, { + directory: sdk.directory, + attachmentName: language.t("common.attachment"), + }) prompt.set(restored) } // Navigate to the message before the reverted one (which will be the new last visible message) @@ -656,9 +688,9 @@ export default function Page() { }, { id: "session.redo", - title: "Redo", - description: "Redo the last undone message", - category: "Session", + title: language.t("command.session.redo"), + description: language.t("command.session.redo.description"), + category: language.t("command.category.session"), slash: "redo", disabled: !params.id || !info()?.revert?.messageID, onSelect: async () => { @@ -685,9 +717,9 @@ export default function Page() { }, { id: "session.compact", - title: "Compact session", - description: "Summarize the session to reduce context size", - category: "Session", + title: language.t("command.session.compact"), + description: language.t("command.session.compact.description"), + category: language.t("command.category.session"), slash: "compact", disabled: !params.id || visibleUserMessages().length === 0, onSelect: async () => { @@ -696,8 +728,8 @@ export default function Page() { const model = local.model.current() if (!model) { showToast({ - title: "No model selected", - description: "Connect a provider to summarize this session", + title: language.t("toast.model.none.title"), + description: language.t("toast.model.none.description"), }) return } @@ -710,13 +742,79 @@ export default function Page() { }, { id: "session.fork", - title: "Fork from message", - description: "Create a new session from a previous message", - category: "Session", + title: language.t("command.session.fork"), + description: language.t("command.session.fork.description"), + category: language.t("command.category.session"), slash: "fork", disabled: !params.id || visibleUserMessages().length === 0, onSelect: () => dialog.show(() => ), }, + ...(sync.data.config.share !== "disabled" + ? [ + { + id: "session.share", + title: language.t("command.session.share"), + description: language.t("command.session.share.description"), + category: language.t("command.category.session"), + slash: "share", + disabled: !params.id || !!info()?.share?.url, + onSelect: async () => { + if (!params.id) return + await sdk.client.session + .share({ sessionID: params.id }) + .then((res) => { + navigator.clipboard.writeText(res.data!.share!.url).catch(() => + showToast({ + title: language.t("toast.session.share.copyFailed.title"), + variant: "error", + }), + ) + }) + .then(() => + showToast({ + title: language.t("toast.session.share.success.title"), + description: language.t("toast.session.share.success.description"), + variant: "success", + }), + ) + .catch(() => + showToast({ + title: language.t("toast.session.share.failed.title"), + description: language.t("toast.session.share.failed.description"), + variant: "error", + }), + ) + }, + }, + { + id: "session.unshare", + title: language.t("command.session.unshare"), + description: language.t("command.session.unshare.description"), + category: language.t("command.category.session"), + slash: "unshare", + disabled: !params.id || !info()?.share?.url, + onSelect: async () => { + if (!params.id) return + await sdk.client.session + .unshare({ sessionID: params.id }) + .then(() => + showToast({ + title: language.t("toast.session.unshare.success.title"), + description: language.t("toast.session.unshare.success.description"), + variant: "success", + }), + ) + .catch(() => + showToast({ + title: language.t("toast.session.unshare.failed.title"), + description: language.t("toast.session.unshare.failed.description"), + variant: "error", + }), + ) + }, + }, + ] + : []), ]) const handleKeyDown = (event: KeyboardEvent) => { @@ -733,6 +831,9 @@ export default function Page() { return } + // Don't autofocus chat if terminal panel is open + if (view().terminal.opened()) return + if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) { inputRef?.focus() } @@ -780,6 +881,23 @@ export default function Page() { const handleTerminalDragEnd = () => { setStore("activeTerminalDraggable", undefined) + const activeId = terminal.active() + if (!activeId) return + setTimeout(() => { + const wrapper = document.getElementById(`terminal-wrapper-${activeId}`) + const element = wrapper?.querySelector('[data-component="terminal"]') as HTMLElement + if (!element) return + + // Find and focus the ghostty textarea (the actual input element) + const textarea = element.querySelector("textarea") as HTMLTextAreaElement + if (textarea) { + textarea.focus() + return + } + // Fallback: focus container and dispatch pointer event + element.focus() + element.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true })) + }, 0) } const contextOpen = createMemo(() => tabs().active() === "context" || tabs().all().includes("context")) @@ -830,13 +948,27 @@ export default function Page() { const autoScroll = createAutoScroll({ working: () => true, + overflowAnchor: "dynamic", }) + // When the user returns to the bottom, treat the active message as "latest". + createEffect( + on( + autoScroll.userScrolled, + (scrolled) => { + if (scrolled) return + setStore("messageId", undefined) + }, + { defer: true }, + ), + ) + createEffect( on( isWorking, (working, prev) => { if (!working || prev) return + if (autoScroll.userScrolled()) return autoScroll.forceScrollToBottom() }, { defer: true }, @@ -968,33 +1100,68 @@ export default function Page() { const a = el.getBoundingClientRect() const b = root.getBoundingClientRect() - const top = a.top - b.top + root.scrollTop - root.scrollTo({ top, behavior }) + const offset = (info()?.title ? 40 : 0) + 12 + const top = a.top - b.top + root.scrollTop - offset + root.scrollTo({ top: top > 0 ? top : 0, behavior }) + return true } const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => { + // Navigating to a specific message should always pause auto-follow. + autoScroll.pause() setActiveMessage(message) + updateHash(message.id) const msgs = visibleUserMessages() const index = msgs.findIndex((m) => m.id === message.id) if (index !== -1 && index < store.turnStart) { setStore("turnStart", index) scheduleTurnBackfill() + } - requestAnimationFrame(() => { - const el = document.getElementById(anchor(message.id)) - if (el) scrollToElement(el, behavior) - }) + const id = anchor(message.id) + const attempt = (tries: number) => { + const el = document.getElementById(id) + if (el && scrollToElement(el, behavior)) return + if (tries >= 8) return + requestAnimationFrame(() => attempt(tries + 1)) + } + attempt(0) + } - updateHash(message.id) + const applyHash = (behavior: ScrollBehavior) => { + const hash = window.location.hash.slice(1) + if (!hash) { + setPendingHash(undefined) + autoScroll.forceScrollToBottom() return } - const el = document.getElementById(anchor(message.id)) - if (el) scrollToElement(el, behavior) - updateHash(message.id) - } + const match = hash.match(/^message-(.+)$/) + if (match) { + const msg = visibleUserMessages().find((m) => m.id === match[1]) + if (msg) { + setPendingHash(undefined) + scrollToMessage(msg, behavior) + return + } + // If we have a message hash but the message isn't loaded/rendered yet, + // don't fall back to "bottom". We'll retry once messages arrive. + setPendingHash(match[1]) + return + } + + const target = document.getElementById(hash) + if (target) { + setPendingHash(undefined) + scrollToElement(target, behavior) + return + } + + setPendingHash(undefined) + autoScroll.forceScrollToBottom() + } const getActiveMessageId = (container: HTMLDivElement) => { const cutoff = container.scrollTop + 100 const nodes = container.querySelectorAll("[data-message-id]") @@ -1060,6 +1227,36 @@ export default function Page() { }) }) + // Retry message navigation once the target message is actually loaded. + createEffect(() => { + const sessionID = params.id + const ready = messagesReady() + if (!sessionID || !ready) return + + // dependencies + visibleUserMessages().length + store.turnStart + + const targetId = pendingMessage() ?? pendingHash() + if (!targetId) return + if (store.messageId === targetId) return + + const msg = visibleUserMessages().find((m) => m.id === targetId) + if (!msg) return + if (pendingMessage() === targetId) setPendingMessage(undefined) + if (pendingHash() === targetId) setPendingHash(undefined) + requestAnimationFrame(() => scrollToMessage(msg, "auto")) + }) + + createEffect(() => { + const sessionID = params.id + const ready = messagesReady() + if (!sessionID || !ready) return + + const handler = () => requestAnimationFrame(() => applyHash("auto")) + window.addEventListener("hashchange", handler) + onCleanup(() => window.removeEventListener("hashchange", handler)) + }) createEffect(() => { document.addEventListener("keydown", handleKeyDown) }) @@ -1083,7 +1280,22 @@ export default function Page() { createEffect(() => { if (!terminal.ready()) return - handoff.terminals = terminal.all().map((t) => t.title) + language.locale() + + const label = (pty: LocalPTY) => { + const title = pty.title + const number = pty.titleNumber + const match = title.match(/^Terminal (\d+)$/) + const parsed = match ? Number(match[1]) : undefined + const isDefaultTitle = Number.isFinite(number) && number > 0 && Number.isFinite(parsed) && parsed === number + + if (title && !isDefaultTitle) return title + if (Number.isFinite(number) && number > 0) return language.t("terminal.title.numbered", { number }) + if (title) return title + return language.t("terminal.title") + } + + handoff.terminals = terminal.all().map(label) }) createEffect(() => { @@ -1119,7 +1331,7 @@ export default function Page() { classes={{ button: "w-full" }} onClick={() => setStore("mobileTab", "session")} > - Session + {language.t("session.tab.session")} setStore("mobileTab", "review")} > - {reviewCount()} Files Changed + + + {language.t("session.review.filesChanged", { count: reviewCount() })} + + {language.t("session.tab.review")} + @@ -1156,7 +1373,11 @@ export default function Page() { Loading changes...
} + fallback={ +
+ {language.t("session.review.loadingChanges")} +
+ } >
+ +
+ +
+
{ autoScroll.handleScroll() - if (isDesktop()) scheduleScrollSpy(e.currentTarget) + if (isDesktop() && autoScroll.userScrolled()) scheduleScrollSpy(e.currentTarget) }} onClick={autoScroll.handleInteraction} class="session-scroll-container relative min-w-0 w-full h-full overflow-y-auto no-scrollbar" + style={{ "--session-title-height": info()?.title ? "40px" : "0px" }} >
setStore("turnStart", 0)} > - Render earlier messages + {language.t("session.messages.renderEarlier")}
@@ -1229,7 +1468,9 @@ export default function Page() { sync.session.history.loadMore(id) }} > - {historyLoading() ? "Loading earlier messages..." : "Load earlier messages"} + {historyLoading() + ? language.t("session.messages.loadingEarlier") + : language.t("session.messages.loadEarlier")}
@@ -1334,11 +1575,9 @@ export default function Page() { -
- {handoff.prompt || "Loading prompt..."} -
-
+
+ {handoff.prompt || language.t("prompt.loading")} +
} >
-
Review
+
{language.t("session.tab.review")}
{info()?.summary?.files ?? 0} @@ -1398,7 +1637,7 @@ export default function Page() { + tabs().close("context")} /> } @@ -1407,7 +1646,7 @@ export default function Page() { >
-
Context
+
{language.t("session.tab.context")}
@@ -1416,7 +1655,7 @@ export default function Page() {
@@ -1438,7 +1677,11 @@ export default function Page() { Loading changes...
} + fallback={ +
+ {language.t("session.review.loadingChanges")} +
+ } > - Add {selectionLabel()} to context + + {language.t("session.context.addToContext", { selection: selectionLabel() ?? "" })} +
)} @@ -1693,7 +1938,7 @@ export default function Page() { /> -
Loading...
+
{language.t("common.loading")}...
{(err) =>
{err()}
} @@ -1724,7 +1969,7 @@ export default function Page() {
-
Loading...
+
{language.t("common.loading")}...
+
+
+ {language.t("terminal.loading")}
-
Loading terminal...
} > @@ -1763,29 +2010,106 @@ export default function Page() { > - - - t.id)}> - {(pty) => } - -
- - - -
-
- - {(pty) => ( - - terminal.clone(pty.id)} /> - - )} - -
+
+ { + // Only switch tabs if not in the middle of starting edit mode + terminal.open(id) + }} + class="!h-auto !flex-none" + > + + t.id)}> + + {(pty) => ( + { + view().terminal.close() + setAutoCreated(false) + }} + /> + )} + + +
+ + + +
+
+
+
+ + {(pty) => { + const [dismissed, setDismissed] = createSignal(false) + return ( +
+ terminal.update({ ...data, id: pty.id })} + onConnect={() => { + terminal.update({ id: pty.id, error: false }) + setDismissed(false) + }} + onConnectError={() => { + setDismissed(false) + terminal.update({ id: pty.id, error: true }) + }} + /> + +
+ +
+
{language.t("terminal.connectionLost.title")}
+
+ {language.t("terminal.connectionLost.description")} +
+
+ +
+
+
+ ) + }} +
+
+
{(draggedId) => { @@ -1794,7 +2118,20 @@ export default function Page() { {(t) => (
- {t().title} + {(() => { + const title = t().title + const number = t().titleNumber + const match = title.match(/^Terminal (\d+)$/) + const parsed = match ? Number(match[1]) : undefined + const isDefaultTitle = + Number.isFinite(number) && number > 0 && Number.isFinite(parsed) && parsed === number + + if (title && !isDefaultTitle) return title + if (Number.isFinite(number) && number > 0) + return language.t("terminal.title.numbered", { number }) + if (title) return title + return language.t("terminal.title") + })()}
)}
diff --git a/packages/app/src/utils/prompt.ts b/packages/app/src/utils/prompt.ts index 5d9edfed109..35aec0071aa 100644 --- a/packages/app/src/utils/prompt.ts +++ b/packages/app/src/utils/prompt.ts @@ -53,10 +53,11 @@ function textPartValue(parts: Part[]) { * Extract prompt content from message parts for restoring into the prompt input. * This is used by undo to restore the original user prompt. */ -export function extractPromptFromParts(parts: Part[], opts?: { directory?: string }): Prompt { +export function extractPromptFromParts(parts: Part[], opts?: { directory?: string; attachmentName?: string }): Prompt { const textPart = textPartValue(parts) const text = textPart?.text ?? "" const directory = opts?.directory + const attachmentName = opts?.attachmentName ?? "attachment" const toRelative = (path: string) => { if (!directory) return path @@ -104,7 +105,7 @@ export function extractPromptFromParts(parts: Part[], opts?: { directory?: strin images.push({ type: "image", id: filePart.id, - filename: filePart.filename ?? "attachment", + filename: filePart.filename ?? attachmentName, mime: filePart.mime, dataUrl: filePart.url, }) diff --git a/packages/app/src/utils/sound.ts b/packages/app/src/utils/sound.ts index e8db0bf7b9a..d5e606c6750 100644 --- a/packages/app/src/utils/sound.ts +++ b/packages/app/src/utils/sound.ts @@ -1,8 +1,35 @@ +import alert01 from "@opencode-ai/ui/audio/alert-01.aac" +import alert02 from "@opencode-ai/ui/audio/alert-02.aac" +import alert03 from "@opencode-ai/ui/audio/alert-03.aac" +import alert04 from "@opencode-ai/ui/audio/alert-04.aac" +import alert05 from "@opencode-ai/ui/audio/alert-05.aac" +import alert06 from "@opencode-ai/ui/audio/alert-06.aac" +import alert07 from "@opencode-ai/ui/audio/alert-07.aac" +import alert08 from "@opencode-ai/ui/audio/alert-08.aac" +import alert09 from "@opencode-ai/ui/audio/alert-09.aac" +import alert10 from "@opencode-ai/ui/audio/alert-10.aac" +import bipbop01 from "@opencode-ai/ui/audio/bip-bop-01.aac" +import bipbop02 from "@opencode-ai/ui/audio/bip-bop-02.aac" +import bipbop03 from "@opencode-ai/ui/audio/bip-bop-03.aac" +import bipbop04 from "@opencode-ai/ui/audio/bip-bop-04.aac" +import bipbop05 from "@opencode-ai/ui/audio/bip-bop-05.aac" +import bipbop06 from "@opencode-ai/ui/audio/bip-bop-06.aac" +import bipbop07 from "@opencode-ai/ui/audio/bip-bop-07.aac" +import bipbop08 from "@opencode-ai/ui/audio/bip-bop-08.aac" +import bipbop09 from "@opencode-ai/ui/audio/bip-bop-09.aac" +import bipbop10 from "@opencode-ai/ui/audio/bip-bop-10.aac" import nope01 from "@opencode-ai/ui/audio/nope-01.aac" import nope02 from "@opencode-ai/ui/audio/nope-02.aac" import nope03 from "@opencode-ai/ui/audio/nope-03.aac" import nope04 from "@opencode-ai/ui/audio/nope-04.aac" import nope05 from "@opencode-ai/ui/audio/nope-05.aac" +import nope06 from "@opencode-ai/ui/audio/nope-06.aac" +import nope07 from "@opencode-ai/ui/audio/nope-07.aac" +import nope08 from "@opencode-ai/ui/audio/nope-08.aac" +import nope09 from "@opencode-ai/ui/audio/nope-09.aac" +import nope10 from "@opencode-ai/ui/audio/nope-10.aac" +import nope11 from "@opencode-ai/ui/audio/nope-11.aac" +import nope12 from "@opencode-ai/ui/audio/nope-12.aac" import staplebops01 from "@opencode-ai/ui/audio/staplebops-01.aac" import staplebops02 from "@opencode-ai/ui/audio/staplebops-02.aac" import staplebops03 from "@opencode-ai/ui/audio/staplebops-03.aac" @@ -10,20 +37,59 @@ import staplebops04 from "@opencode-ai/ui/audio/staplebops-04.aac" import staplebops05 from "@opencode-ai/ui/audio/staplebops-05.aac" import staplebops06 from "@opencode-ai/ui/audio/staplebops-06.aac" import staplebops07 from "@opencode-ai/ui/audio/staplebops-07.aac" +import yup01 from "@opencode-ai/ui/audio/yup-01.aac" +import yup02 from "@opencode-ai/ui/audio/yup-02.aac" +import yup03 from "@opencode-ai/ui/audio/yup-03.aac" +import yup04 from "@opencode-ai/ui/audio/yup-04.aac" +import yup05 from "@opencode-ai/ui/audio/yup-05.aac" +import yup06 from "@opencode-ai/ui/audio/yup-06.aac" export const SOUND_OPTIONS = [ - { id: "staplebops-01", label: "Boopy", src: staplebops01 }, - { id: "staplebops-02", label: "Beepy", src: staplebops02 }, - { id: "staplebops-03", label: "Staplebops 03", src: staplebops03 }, - { id: "staplebops-04", label: "Staplebops 04", src: staplebops04 }, - { id: "staplebops-05", label: "Staplebops 05", src: staplebops05 }, - { id: "staplebops-06", label: "Staplebops 06", src: staplebops06 }, - { id: "staplebops-07", label: "Staplebops 07", src: staplebops07 }, - { id: "nope-01", label: "Nope 01", src: nope01 }, - { id: "nope-02", label: "Nope 02", src: nope02 }, - { id: "nope-03", label: "Oopsie", src: nope03 }, - { id: "nope-04", label: "Nope 04", src: nope04 }, - { id: "nope-05", label: "Nope 05", src: nope05 }, + { id: "alert-01", label: "sound.option.alert01", src: alert01 }, + { id: "alert-02", label: "sound.option.alert02", src: alert02 }, + { id: "alert-03", label: "sound.option.alert03", src: alert03 }, + { id: "alert-04", label: "sound.option.alert04", src: alert04 }, + { id: "alert-05", label: "sound.option.alert05", src: alert05 }, + { id: "alert-06", label: "sound.option.alert06", src: alert06 }, + { id: "alert-07", label: "sound.option.alert07", src: alert07 }, + { id: "alert-08", label: "sound.option.alert08", src: alert08 }, + { id: "alert-09", label: "sound.option.alert09", src: alert09 }, + { id: "alert-10", label: "sound.option.alert10", src: alert10 }, + { id: "bip-bop-01", label: "sound.option.bipbop01", src: bipbop01 }, + { id: "bip-bop-02", label: "sound.option.bipbop02", src: bipbop02 }, + { id: "bip-bop-03", label: "sound.option.bipbop03", src: bipbop03 }, + { id: "bip-bop-04", label: "sound.option.bipbop04", src: bipbop04 }, + { id: "bip-bop-05", label: "sound.option.bipbop05", src: bipbop05 }, + { id: "bip-bop-06", label: "sound.option.bipbop06", src: bipbop06 }, + { id: "bip-bop-07", label: "sound.option.bipbop07", src: bipbop07 }, + { id: "bip-bop-08", label: "sound.option.bipbop08", src: bipbop08 }, + { id: "bip-bop-09", label: "sound.option.bipbop09", src: bipbop09 }, + { id: "bip-bop-10", label: "sound.option.bipbop10", src: bipbop10 }, + { id: "staplebops-01", label: "sound.option.staplebops01", src: staplebops01 }, + { id: "staplebops-02", label: "sound.option.staplebops02", src: staplebops02 }, + { id: "staplebops-03", label: "sound.option.staplebops03", src: staplebops03 }, + { id: "staplebops-04", label: "sound.option.staplebops04", src: staplebops04 }, + { id: "staplebops-05", label: "sound.option.staplebops05", src: staplebops05 }, + { id: "staplebops-06", label: "sound.option.staplebops06", src: staplebops06 }, + { id: "staplebops-07", label: "sound.option.staplebops07", src: staplebops07 }, + { id: "nope-01", label: "sound.option.nope01", src: nope01 }, + { id: "nope-02", label: "sound.option.nope02", src: nope02 }, + { id: "nope-03", label: "sound.option.nope03", src: nope03 }, + { id: "nope-04", label: "sound.option.nope04", src: nope04 }, + { id: "nope-05", label: "sound.option.nope05", src: nope05 }, + { id: "nope-06", label: "sound.option.nope06", src: nope06 }, + { id: "nope-07", label: "sound.option.nope07", src: nope07 }, + { id: "nope-08", label: "sound.option.nope08", src: nope08 }, + { id: "nope-09", label: "sound.option.nope09", src: nope09 }, + { id: "nope-10", label: "sound.option.nope10", src: nope10 }, + { id: "nope-11", label: "sound.option.nope11", src: nope11 }, + { id: "nope-12", label: "sound.option.nope12", src: nope12 }, + { id: "yup-01", label: "sound.option.yup01", src: yup01 }, + { id: "yup-02", label: "sound.option.yup02", src: yup02 }, + { id: "yup-03", label: "sound.option.yup03", src: yup03 }, + { id: "yup-04", label: "sound.option.yup04", src: yup04 }, + { id: "yup-05", label: "sound.option.yup05", src: yup05 }, + { id: "yup-06", label: "sound.option.yup06", src: yup06 }, ] as const export type SoundOption = (typeof SOUND_OPTIONS)[number] diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 241fc864b6f..237b6630f2f 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.28-1", + "version": "1.1.30", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/app/public/apple-touch-icon-v3.png b/packages/console/app/public/apple-touch-icon-v3.png new file mode 120000 index 00000000000..ddd1d1ac33c --- /dev/null +++ b/packages/console/app/public/apple-touch-icon-v3.png @@ -0,0 +1 @@ +../../../ui/src/assets/favicon/apple-touch-icon-v3.png \ No newline at end of file diff --git a/packages/console/app/public/favicon-96x96-v3.png b/packages/console/app/public/favicon-96x96-v3.png new file mode 120000 index 00000000000..5f4b8a73bbf --- /dev/null +++ b/packages/console/app/public/favicon-96x96-v3.png @@ -0,0 +1 @@ +../../../ui/src/assets/favicon/favicon-96x96-v3.png \ No newline at end of file diff --git a/packages/console/app/public/favicon-v3.ico b/packages/console/app/public/favicon-v3.ico new file mode 120000 index 00000000000..6e1f48aec90 --- /dev/null +++ b/packages/console/app/public/favicon-v3.ico @@ -0,0 +1 @@ +../../../ui/src/assets/favicon/favicon-v3.ico \ No newline at end of file diff --git a/packages/console/app/public/favicon-v3.svg b/packages/console/app/public/favicon-v3.svg new file mode 120000 index 00000000000..77814acf5c1 --- /dev/null +++ b/packages/console/app/public/favicon-v3.svg @@ -0,0 +1 @@ +../../../ui/src/assets/favicon/favicon-v3.svg \ No newline at end of file diff --git a/packages/console/app/src/routes/index.tsx b/packages/console/app/src/routes/index.tsx index 6bcf9dbb532..252dcbb9743 100644 --- a/packages/console/app/src/routes/index.tsx +++ b/packages/console/app/src/routes/index.tsx @@ -192,7 +192,7 @@ export default function Home() {
  • [*]
    - Claude Pro Log in with Anthropic to use your Claude Pro or Max account + GitHub Copilot Log in with GitHub to use your Copilot account
  • diff --git a/packages/console/app/src/routes/temp.tsx b/packages/console/app/src/routes/temp.tsx index 68bda67da05..ccd95681068 100644 --- a/packages/console/app/src/routes/temp.tsx +++ b/packages/console/app/src/routes/temp.tsx @@ -89,7 +89,7 @@ export default function Home() { Shareable links Share a link to any sessions for reference or to debug
  • - Claude Pro Log in with Anthropic to use your Claude Pro or Max account + GitHub Copilot Log in with GitHub to use your Copilot account
  • ChatGPT Plus/Pro Log in with OpenAI to use your ChatGPT Plus or Pro account diff --git a/packages/console/core/package.json b/packages/console/core/package.json index c1fd78675f6..3aa1f9e8acd 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.1.28-1", + "version": "1.1.30", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/core/script/black-gift.ts b/packages/console/core/script/black-gift.ts index 3fbf210ab5c..ec7db879914 100644 --- a/packages/console/core/script/black-gift.ts +++ b/packages/console/core/script/black-gift.ts @@ -68,7 +68,7 @@ const subscription = await Billing.stripe().subscriptions.create({ { price: `price_1SmfyI2StuRr0lbXovxJNeZn`, discounts: [{ coupon: couponID }], - quantity: 2, + quantity: seats, }, ], }) diff --git a/packages/console/core/sst-env.d.ts b/packages/console/core/sst-env.d.ts index 3710cb77f83..66c06a824a9 100644 --- a/packages/console/core/sst-env.d.ts +++ b/packages/console/core/sst-env.d.ts @@ -34,6 +34,14 @@ declare module "sst" { "type": "sst.cloudflare.SolidStart" "url": string } + "DISCORD_SUPPORT_BOT_TOKEN": { + "type": "sst.sst.Secret" + "value": string + } + "DISCORD_SUPPORT_CHANNEL_ID": { + "type": "sst.sst.Secret" + "value": string + } "Database": { "database": string "host": string @@ -46,6 +54,14 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "FEISHU_APP_ID": { + "type": "sst.sst.Secret" + "value": string + } + "FEISHU_APP_SECRET": { + "type": "sst.sst.Secret" + "value": string + } "GITHUB_APP_ID": { "type": "sst.sst.Secret" "value": string diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 217f2e53364..37a09255c1d 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.28-1", + "version": "1.1.30", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/function/src/auth.ts b/packages/console/function/src/auth.ts index ee68dffff53..c26ab215b32 100644 --- a/packages/console/function/src/auth.ts +++ b/packages/console/function/src/auth.ts @@ -35,7 +35,7 @@ export const subjects = createSubjects({ const MY_THEME: Theme = { ...THEME_OPENAUTH, - logo: "https://opencode.ai/favicon-v2.svg", + logo: "https://opencode.ai/favicon-v3.svg", } export default { diff --git a/packages/console/function/sst-env.d.ts b/packages/console/function/sst-env.d.ts index 3710cb77f83..66c06a824a9 100644 --- a/packages/console/function/sst-env.d.ts +++ b/packages/console/function/sst-env.d.ts @@ -34,6 +34,14 @@ declare module "sst" { "type": "sst.cloudflare.SolidStart" "url": string } + "DISCORD_SUPPORT_BOT_TOKEN": { + "type": "sst.sst.Secret" + "value": string + } + "DISCORD_SUPPORT_CHANNEL_ID": { + "type": "sst.sst.Secret" + "value": string + } "Database": { "database": string "host": string @@ -46,6 +54,14 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "FEISHU_APP_ID": { + "type": "sst.sst.Secret" + "value": string + } + "FEISHU_APP_SECRET": { + "type": "sst.sst.Secret" + "value": string + } "GITHUB_APP_ID": { "type": "sst.sst.Secret" "value": string diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 6c01976e8b6..882e1f21a5c 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.28-1", + "version": "1.1.30", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/console/resource/sst-env.d.ts b/packages/console/resource/sst-env.d.ts index 3710cb77f83..66c06a824a9 100644 --- a/packages/console/resource/sst-env.d.ts +++ b/packages/console/resource/sst-env.d.ts @@ -34,6 +34,14 @@ declare module "sst" { "type": "sst.cloudflare.SolidStart" "url": string } + "DISCORD_SUPPORT_BOT_TOKEN": { + "type": "sst.sst.Secret" + "value": string + } + "DISCORD_SUPPORT_CHANNEL_ID": { + "type": "sst.sst.Secret" + "value": string + } "Database": { "database": string "host": string @@ -46,6 +54,14 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "FEISHU_APP_ID": { + "type": "sst.sst.Secret" + "value": string + } + "FEISHU_APP_SECRET": { + "type": "sst.sst.Secret" + "value": string + } "GITHUB_APP_ID": { "type": "sst.sst.Secret" "value": string diff --git a/packages/desktop/index.html b/packages/desktop/index.html index d854110df95..84a2a626260 100644 --- a/packages/desktop/index.html +++ b/packages/desktop/index.html @@ -36,7 +36,8 @@ -
    +
    +