chore: move plugins to their own repos and drop the plugins folder

This commit is contained in:
LukeGus committed 2026-10-04 22:40:06 -05:00
1 parent 29a946d02e
commit e02b5fe6e4
3167 files changed
+286 -440749

No files matched your search

-4
View File
@@ -84,16 +84,12 @@ jobs:
- name: Type check
run: npm run type-check
# Before the tests: the plugin boot tests load the built bundles.
- name: Build
run: npm run build
- name: Run unit tests
run: npm run test
- name: Run plugin tests
run: npm run test:plugins
create-release:
needs: [prep, verify]
if: ${{ needs.prep.outputs.dev_branch != '' && inputs.dry_run != true }}
+1 -1
View File
@@ -74,7 +74,7 @@ jobs:
git config user.name "LukeGus"
git config user.email "bugattiguy527@gmail.com"
git add src/ui/locales/translated plugins/*/locales/translated
git add src/ui/locales/translated
if git diff --cached --quiet; then
echo "No translation changes to commit."
exit 0
-3
View File
@@ -44,9 +44,6 @@ jobs:
- name: Run unit tests
run: npm run test
- name: Run plugin tests
run: npm run test:plugins
database-dialects:
name: Postgres and MySQL
runs-on: blacksmith-2vcpu-ubuntu-2404
+1 -8
View File
@@ -110,16 +110,9 @@ jobs:
npm run format
npm run lint
# The plugin boot tests load the built bundles.
- name: Build plugins
run: npm run build:plugins
- name: Run unit tests
run: npm run test
- name: Run plugin tests
run: npm run test:plugins
- name: Sync version
run: node scripts/sync-version.cjs --version "${{ needs.prep.outputs.version }}"
@@ -217,7 +210,7 @@ jobs:
git config user.name "LukeGus"
git config user.email "bugattiguy527@gmail.com"
git add src/ui/locales/translated plugins/*/locales/translated
git add src/ui/locales/translated
if git diff --cached --quiet; then
echo "No translation changes to commit."
exit 0
-1
View File
@@ -15,7 +15,6 @@ coverage
# Plugin build output. termix-plugin build writes it, the packaged server
# gets its copy under dist/plugins.
/plugins/*/dist/
*.tmxplug
*.tmxplug.sig
termix-plugin-signing.key
-2
View File
@@ -7,5 +7,3 @@ files:
- source: /src/ui/locales/en.json
dest: /en.json
translation: /src/ui/locales/translated/%locale_with_underscore%.json
- source: /plugins/*/locales/en.json
translation: /%original_path%/translated/%locale_with_underscore%.json
+2 -18
View File
@@ -1,12 +1,3 @@
# Stage 0: every plugin's package.json and manifest.json and nothing else.
# npm ci needs each workspace package.json in place or it silently skips it,
# and a glob COPY flattens paths, so the tree is trimmed here instead. The
# deps layer below stays cached until one of these files changes.
FROM node:26-slim AS workspace-manifests
WORKDIR /src
COPY plugins ./plugins
RUN find plugins -mindepth 2 -maxdepth 2 ! -name package.json ! -name manifest.json -exec rm -rf {} +
# Stage 1: Install dependencies
# The builders only produce JS, so they run on the build machine's own arch
# once instead of under QEMU per target. Rolldown hangs under arm64 emulation.
@@ -17,7 +8,6 @@ RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt
COPY package*.json ./
COPY packages/plugin-sdk/package.json ./packages/plugin-sdk/package.json
COPY --from=workspace-manifests /src/plugins ./plugins
COPY .npmrc ./
COPY vendor ./vendor
@@ -80,15 +70,9 @@ COPY packages/plugin-sdk/package.json ./packages/plugin-sdk/package.json
COPY .npmrc ./
COPY vendor ./vendor
COPY scripts/patch-ssh2-agent.cjs scripts/install-native-plugin-deps.cjs ./scripts/
# Plugin manifests only, for their nativeDependencies. Outside plugins/ so npm
# ci does not treat them as workspaces: everything else a plugin uses is
# already inside its bundle.
COPY --from=workspace-manifests /src/plugins ./plugin-manifests
COPY scripts/patch-ssh2-agent.cjs ./scripts/
RUN npm ci --omit=dev --ignore-scripts && \
node scripts/install-native-plugin-deps.cjs plugin-manifests && \
rm -rf plugin-manifests && \
node scripts/patch-ssh2-agent.cjs && \
rm -rf node_modules/better-sqlite3/prebuilds && \
npm run build-release --prefix node_modules/better-sqlite3 && \
@@ -128,7 +112,7 @@ COPY --chown=node:node --from=frontend-builder /app/dist /app/html
COPY --chown=node:node --from=production-deps /app/node_modules /app/node_modules
COPY --chown=node:node --from=backend-builder /app/dist/backend ./dist/backend
# Bundled first-party plugins, resolved as dist/plugins by
# Plugins pinned in docker/bundled-plugins.json, resolved as dist/plugins by
# getBundledPluginsDir().
COPY --chown=node:node --from=backend-builder /app/dist/plugins ./dist/plugins
# Target of node_modules/@termix/plugin-sdk, which core and the plugins import.
+1 -138
View File
@@ -1,140 +1,3 @@
{
"plugins": [
{
"id": "acme-ssl",
"source": "workspace"
},
{
"id": "ai",
"source": "workspace"
},
{
"id": "alerts",
"source": "workspace"
},
{
"id": "automations",
"source": "workspace"
},
{
"id": "docker",
"source": "workspace"
},
{
"id": "file-manager",
"source": "workspace"
},
{
"id": "fleets",
"source": "workspace"
},
{
"id": "homepage",
"source": "workspace"
},
{
"id": "host-metrics",
"source": "workspace"
},
{
"id": "ldap",
"source": "workspace"
},
{
"id": "network-topology",
"source": "workspace"
},
{
"id": "opkssh",
"source": "workspace"
},
{
"id": "proxmox",
"source": "workspace"
},
{
"id": "remote-desktop",
"source": "workspace"
},
{
"id": "secret-sources",
"source": "workspace"
},
{
"id": "serial",
"source": "workspace"
},
{
"id": "session-recording",
"source": "workspace"
},
{
"id": "session-sharing",
"source": "workspace"
},
{
"id": "snippets",
"source": "workspace"
},
{
"id": "ssh-terminal",
"source": "workspace"
},
{
"id": "sso",
"source": "workspace"
},
{
"id": "step-ca",
"source": "workspace"
},
{
"id": "tailscale",
"source": "workspace"
},
{
"id": "telemetry",
"source": "workspace"
},
{
"id": "termix-identity",
"source": "workspace"
},
{
"id": "tmux-monitor",
"source": "workspace"
},
{
"id": "totp",
"source": "workspace"
},
{
"id": "tunnels",
"source": "workspace"
},
{
"id": "vault",
"source": "workspace"
},
{
"id": "wake-on-lan",
"source": "workspace"
},
{
"id": "warpgate",
"source": "workspace"
},
{
"id": "web-endpoint",
"source": "workspace"
},
{
"id": "webauthn",
"source": "workspace"
},
{
"id": "workspaces",
"source": "workspace"
}
]
"plugins": []
}
-129
View File
@@ -5,64 +5,6 @@ import reactRefresh from "eslint-plugin-react-refresh";
import unusedImports from "eslint-plugin-unused-imports";
import tseslint from "typescript-eslint";
import { globalIgnores } from "eslint/config";
import path from "node:path";
// A plugin may import @termix/plugin-sdk, npm packages and its own files.
// A relative path that climbs out of plugins/<id>/ reaches core or another
// plugin, and "@/..." is core's alias.
const pluginBoundary = {
rules: {
"stay-inside": {
meta: {
type: "problem",
messages: {
core: "A plugin reaches core only through @termix/plugin-sdk. Use an SDK API, or add one.",
outside:
"A plugin cannot import files outside its own folder. Use the SDK, ctx.services, events, actions or slots.",
},
},
create(context) {
const file = context.filename.replace(/\\/g, "/");
const root = /^(.*\/plugins\/[^/]+)\//.exec(file)?.[1];
const check = (node, source) => {
if (typeof source !== "string") return;
if (source === "@" || source.startsWith("@/")) {
context.report({ node, messageId: "core" });
return;
}
if (!root || !source.startsWith(".")) return;
const target = path.posix.normalize(
path.posix.join(path.posix.dirname(file), source),
);
if (target !== root && !target.startsWith(`${root}/`)) {
context.report({ node, messageId: "outside" });
}
};
return {
ImportDeclaration: (node) => check(node, node.source.value),
ExportNamedDeclaration: (node) => check(node, node.source?.value),
ExportAllDeclaration: (node) => check(node, node.source.value),
ImportExpression: (node) => check(node, node.source.value),
// vi.mock("@/x") and require("../../x") name a module too.
CallExpression: (node) => {
const callee = node.callee;
const named =
(callee.type === "Identifier" && callee.name === "require") ||
(callee.type === "MemberExpression" &&
callee.object.type === "Identifier" &&
callee.object.name === "vi" &&
callee.property.type === "Identifier" &&
["mock", "doMock", "importActual", "unmock"].includes(
callee.property.name,
));
const first = node.arguments[0];
if (named && first?.type === "Literal") check(node, first.value);
},
};
},
},
},
};
export default tseslint.config([
globalIgnores([
@@ -162,75 +104,4 @@ export default tseslint.config([
],
},
},
{
// Core must not reach into a plugin. A plugin can be disabled, upgraded
// or removed, so an import from core turns "disabled" into a broken
// build rather than a missing feature. Core talks to plugins through the
// runtime in src/backend/plugins/, which handles absence. Core tests only
// use fixture plugins.
files: ["src/**/*.{ts,tsx}"],
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["**/plugins/*/src/backend/**"],
message:
"Core must not import a plugin backend. Go through the plugin runtime, so disabling the plugin degrades cleanly.",
},
],
},
],
},
},
{
// The shell never imports a plugin. It reaches plugins only through the
// registries their frontends fill via the app object (A7). This block
// also carries the backend rule for src/ui files, because flat config
// replaces a rule's options rather than merging them.
files: ["src/ui/**/*.{ts,tsx}", "src/main.tsx"],
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["**/plugins/*/src/**", "**/plugins/*/dist/**"],
message:
"The shell does not import plugins. Register the surface through the app object and read it from a registry.",
},
],
},
],
},
},
{
// The other direction. A plugin reaches core only through
// @termix/plugin-sdk, and never reaches into another plugin's source.
files: ["plugins/*/src/**/*.{ts,tsx,mjs}", "plugins/*/tests/**/*.{ts,tsx}"],
plugins: { "termix-plugins": pluginBoundary },
rules: {
"termix-plugins/stay-inside": "error",
},
},
{
// react-i18next's hook reads core's namespace, so plugin strings show as raw keys.
files: ["plugins/*/src/**/*.{ts,tsx}"],
rules: {
"no-restricted-imports": [
"error",
{
paths: [
{
name: "react-i18next",
importNames: ["useTranslation", "Trans", "withTranslation"],
message:
"Use useTranslation from @termix/plugin-sdk/frontend so keys resolve in this plugin's namespace.",
},
],
},
],
},
},
]);
-9
View File
@@ -76,15 +76,6 @@
"packages/plugin-sdk": {
"entry": ["src/*.ts", "src/testing/*.ts", "cli/**/*.mjs"],
"project": ["src/**/*.ts", "cli/**/*.mjs"]
},
"plugins/*": {
"entry": [
"src/backend/index.ts",
"src/frontend/index.{ts,tsx}",
"tests/**/*.{ts,tsx}",
"vitest.config.ts"
],
"project": ["src/**/*.{ts,tsx}", "tests/**/*.{ts,tsx}"]
}
}
}
+98 -3637
View File
File diff suppressed because it is too large. Load diff
+4 -8
View File
@@ -2,8 +2,7 @@
"name": "termix",
"private": true,
"workspaces": [
"packages/*",
"plugins/*"
"packages/*"
],
"version": "26.10.0",
"description": "Self-hosted SSH and remote desktop management.",
@@ -16,10 +15,10 @@
"scripts": {
"format": "prettier --write .",
"format:check": "prettier --check .",
"postinstall": "node scripts/patch-app-builder-lib.cjs && node scripts/patch-better-sqlite3.cjs && node scripts/patch-nan.cjs && node scripts/patch-node-pty.cjs && node scripts/patch-ssh2-agent.cjs && node scripts/apply-plugin-patches.cjs",
"postinstall": "node scripts/patch-app-builder-lib.cjs && node scripts/patch-better-sqlite3.cjs && node scripts/patch-nan.cjs && node scripts/patch-node-pty.cjs && node scripts/patch-ssh2-agent.cjs",
"prepare": "husky || true",
"prebuild": "node scripts/write-electron-build-info.cjs",
"lint": "node scripts/generate-dialect-schema.cjs --check && npm run plugin:validate-manifest && eslint . && node scripts/check-importmap-csp.cjs && node scripts/check-core-plugin-ids.cjs && node scripts/check-sdk-ui-exports.cjs && node scripts/check-plugin-locales.cjs && node scripts/check-plugin-uses.cjs && node scripts/check-test-locations.cjs",
"lint": "node scripts/generate-dialect-schema.cjs --check && eslint . && node scripts/check-importmap-csp.cjs && node scripts/check-core-plugin-ids.cjs && node scripts/check-sdk-ui-exports.cjs && node scripts/check-test-locations.cjs",
"lint:fix": "eslint --fix .",
"type-check": "npm run build:sdk && tsc -b --force",
"test": "vitest run",
@@ -48,11 +47,8 @@
"schema:generate": "node scripts/generate-dialect-schema.cjs",
"schema:check": "node scripts/generate-dialect-schema.cjs --check",
"schema:migrations": "drizzle-kit generate --config=drizzle.config.sqlite.ts && drizzle-kit generate --config=drizzle.config.pg.ts && drizzle-kit generate --config=drizzle.config.mysql.ts",
"plugin:validate-manifest": "node scripts/validate-plugin-manifest.cjs",
"build:sdk": "npm run build --workspace @termix/plugin-sdk && node scripts/build-sdk-host.mjs",
"build:plugins": "node scripts/build-plugins.cjs",
"test:plugins": "node scripts/run-plugin-tests.cjs",
"test:all": "npm run test && npm run test:plugins"
"build:plugins": "node scripts/build-plugins.cjs"
},
"dependencies": {
"@tanstack/react-virtual": "^3.14.13",
+1 -1
View File
@@ -2,7 +2,7 @@
/**
* termix-plugin: build, validate, test, pack and sign a Termix plugin.
*
* Run from a plugin directory (npm run build inside plugins/<id>/ does).
* Run from a plugin directory (a plugin repo's npm run build does).
*/
import process from "node:process";
-21
View File
@@ -1,21 +0,0 @@
{
"extends": ["@commitlint/config-conventional"],
"rules": {
"type-enum": [
2,
"always",
[
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"test",
"chore",
"revert"
]
],
"subject-case": [0]
}
}
-1
View File
@@ -1 +0,0 @@
custom: https://donate.termix.site/
-34
View File
@@ -1,34 +0,0 @@
# Overview
_Short summary of what this PR does_
- [ ] Added: ...
- [ ] Updated: ...
- [ ] Removed: ...
- [ ] Fixed: ...
# Changes Made
_Detailed explanation of changes (if needed)_
- ...
# Related Issues
_Link any issues this PR addresses_
- Closes #ISSUE_NUMBER
- Related to #ISSUE_NUMBER
# Screenshots / Demos
_(Optional: add before/after screenshots, GIFs, or console output)_
# Checklist
- [ ] Code follows project style guidelines
- [ ] Supports mobile and desktop UI/app (if applicable)
- [ ] Added the change to `CHANGELOG.json`
- [ ] `npm run test`, `npm run typecheck` and `npm run validate` pass
- [ ] I have read `CONTRIBUTING.md`
- [ ] This is not a translation request. See [docs](https://docs.termix.site/translations)
-9
View File
@@ -1,9 +0,0 @@
node_modules
dist
coverage
*.log
.env
*.tmxplug
*.tmxplug.sig
termix-plugin-signing.key
.termix-tables-*
-1
View File
@@ -1 +0,0 @@
npx --no -- commitlint --edit "$1"
-1
View File
@@ -1 +0,0 @@
npx lint-staged
-4
View File
@@ -1,4 +0,0 @@
dist
coverage
node_modules
package-lock.json
-9
View File
@@ -1,9 +0,0 @@
{
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80,
"arrowParens": "always",
"endOfLine": "lf"
}
-26
View File
@@ -1,26 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/Termix-SSH/Termix/main/packages/plugin-sdk/schema/changelog.schema.json",
"releases": [
{
"version": "1.0.0",
"changes": [
{
"type": "added",
"text": "Certificates from Let's Encrypt or any ACME directory"
},
{
"type": "added",
"text": "HTTP challenge on port 80, or a Cloudflare DNS challenge when port 80 is closed"
},
{
"type": "added",
"text": "Renews before the certificate expires"
},
{
"type": "added",
"text": "Swaps in the new certificate without a restart"
}
]
}
]
}
-27
View File
@@ -1,27 +0,0 @@
# Contributing to ACME Certificates
## Development
```bash
npm run build # build into dist/
npm run test # run this plugin's tests
npm run typecheck # type-check this plugin
npm run validate # check manifest.json
npm run format # format the code with Prettier
```
## Settings
### Admin
- **Renew automatically:** checks twice a day and renews when there is no certificate, it is self-signed, it does not cover the domain, or it expires within 30 days
- **Domain:** the public domain name the certificate covers
- **Email:** the contact email for the ACME account
- **Certificate authority:** Let's Encrypt, Let's Encrypt staging, or a custom ACME directory
- **ACME directory URL:** only for a custom authority, and must be https
- **Challenge type:** HTTP or DNS (Cloudflare)
- **Cloudflare API token:** used by the DNS challenge, stored encrypted
## Permissions
- `acme-ssl.manage`: see the certificate status and request a new certificate. Only admins have it by default.
-38
View File
@@ -1,38 +0,0 @@
<div align="center">
<img src="https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/icon.svg" width="120" height="120" alt="Termix Logo" />
<h1>ACME Certificates</h1>
<p>Free HTTPS certificates for your Termix server, renewed for you</p>
</div>
<br />
## Overview
ACME Certificates gets the HTTPS certificate Termix serves from Let's Encrypt or any other ACME provider and keeps it renewed.
<br />
## Features
- Certificates from Let's Encrypt or any ACME directory
- HTTP challenge on port 80, or a Cloudflare DNS challenge when port 80 is closed
- Renews before the certificate expires
- Swaps in the new certificate without a restart
<br />
## Support
To report a bug or request a feature, open a [support ticket](https://github.com/Termix-SSH/Support/issues/new/choose). You need to be logged in to GitHub. Please be as detailed as possible, preferably in English.
For discussions and questions, join the [Discord](https://discord.gg/jVQGdvHDrf) server.
<br />
## License
Distributed under the Apache License Version 2.0. See [LICENSE](https://github.com/Termix-SSH/Termix/blob/main/LICENSE) for more information.
-61
View File
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Manage ACME certificates",
"description": "See the certificate status and request a new certificate now."
}
},
"settings": {
"autoRenew": {
"label": "Renew automatically",
"description": "Check twice a day and get a new certificate when there is none, it does not cover the domain, or it expires within 30 days."
},
"domain": {
"label": "Domain",
"description": "The public domain name for the certificate.",
"placeholder": "termix.example.com"
},
"email": {
"label": "Email",
"description": "Contact email for the ACME account and expiry notices.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Certificate authority",
"description": "Where to get the certificate. Use staging to test without hitting Let's Encrypt rate limits.",
"letsencrypt": "Let's Encrypt",
"letsencryptStaging": "Let's Encrypt (staging)",
"custom": "Custom ACME directory"
},
"directoryUrl": {
"label": "ACME directory URL",
"description": "Only for a custom certificate authority. Must be an https URL."
},
"challengeType": {
"label": "Challenge type",
"description": "How to prove you own the domain.",
"http": "HTTP - needs port 80 reachable from the internet",
"cloudflare": "DNS (Cloudflare) - needs a Cloudflare API token"
},
"cloudflareToken": {
"label": "Cloudflare API token",
"description": "Scoped token with Zone:DNS:Edit permission for your domain."
}
},
"status": {
"title": "Certificate status",
"none": "No certificate",
"names": "Covers {{names}}",
"issuer": "Issued by {{issuer}}",
"expires": "Expires {{date}}",
"lastIssued": "Last issued {{date}}",
"lastError": "Last attempt failed: {{error}}",
"missing": "Set the domain, email and the fields your challenge type needs, then save, before requesting a certificate.",
"request": "Request certificate now",
"requesting": "Requesting certificate...",
"issued": "Certificate issued and installed"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Bestuur ACME-sertifikate",
"description": "Sien die sertifikaatstatus en versoek nou 'n nuwe sertifikaat."
}
},
"settings": {
"autoRenew": {
"label": "Hernu outomaties",
"description": "Kontroleer twee keer per dag en kry 'n nuwe sertifikaat wanneer daar geen is nie, dit nie die domein dek nie, of dit binne 30 dae verval."
},
"domain": {
"label": "Domein",
"description": "Die publieke domeinnaam vir die sertifikaat.",
"placeholder": "termix.voorbeeld.com"
},
"email": {
"label": "E-pos",
"description": "Kontak-e-posadres vir die ACME-rekening en vervalkennisgewings.",
"placeholder": "admin@voorbeeld.com"
},
"provider": {
"label": "Sertifikaatowerheid",
"description": "Waar om die sertifikaat te kry. Gebruik staging om te toets sonder om Let's Encrypt-tarieflimiete te bereik.",
"letsencrypt": "Kom ons enkripteer",
"letsencryptStaging": "Kom ons enkripteer (opvoering)",
"custom": "Pasgemaakte ACME-gids"
},
"directoryUrl": {
"label": "ACME-gids-URL",
"description": "Slegs vir 'n persoonlike sertifikaatowerheid. Moet 'n https-URL wees."
},
"challengeType": {
"label": "Uitdagingsoort",
"description": "Hoe om te bewys dat jy die domein besit.",
"http": "HTTP - benodig poort 80 wat vanaf die internet bereikbaar is",
"cloudflare": "DNS (Cloudflare) - benodig 'n Cloudflare API-token"
},
"cloudflareToken": {
"label": "Cloudflare API-token",
"description": "Omvangryke token met Zone:DNS:Wysig toestemming vir jou domein."
}
},
"status": {
"title": "Sertifikaatstatus",
"none": "Geen sertifikaat nie",
"names": "Omslae {{names}}",
"issuer": "Uitgereik deur {{issuer}}",
"expires": "Vervaldatum {{date}}",
"lastIssued": "Laas uitgereik {{date}}",
"lastError": "Laaste poging het misluk: {{error}}",
"missing": "Stel die domein, e-posadres en die velde wat jou uitdagingsoort benodig, en stoor dit dan voordat jy 'n sertifikaat aanvra.",
"request": "Versoek nou 'n sertifikaat",
"requesting": "Versoek sertifikaat...",
"issued": "Sertifikaat uitgereik en geïnstalleer"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "إدارة شهادات ACME",
"description": "اطلع على حالة الشهادة واطلب شهادة جديدة الآن."
}
},
"settings": {
"autoRenew": {
"label": "يتم التجديد تلقائياً",
"description": "تحقق مرتين في اليوم واحصل على شهادة جديدة عندما لا تكون هناك شهادة، أو عندما لا تغطي النطاق، أو عندما تنتهي صلاحيتها في غضون 30 يومًا."
},
"domain": {
"label": "النطاق",
"description": "اسم النطاق العام للشهادة.",
"placeholder": "termix.example.com"
},
"email": {
"label": "البريد الإلكتروني",
"description": "البريد الإلكتروني للتواصل بخصوص حساب ACME وإشعارات انتهاء الصلاحية.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "المرجع المصدق (CA)",
"description": "من أين أحصل على الشهادة؟ استخدم بيئة الاختبار لإجراء الاختبارات دون تجاوز حدود معدل Let's Encrypt.",
"letsencrypt": "Let's Encrypt",
"letsencryptStaging": "Let's Encrypt (مرحلة تجريبية)",
"custom": "دليل ACME مخصص"
},
"directoryUrl": {
"label": "رابط دليل ACME",
"description": "مخصص فقط لهيئة إصدار شهادات مخصصة. يجب أن يكون عنوان URL من نوع https."
},
"challengeType": {
"label": "نوع التحدي",
"description": "كيفية إثبات ملكيتك للنطاق.",
"http": "بروتوكول HTTP - يتطلب منفذ 80 يمكن الوصول إليه من الإنترنت",
"cloudflare": "نظام أسماء النطاقات (Cloudflare) - يتطلب رمز API من Cloudflare"
},
"cloudflareToken": {
"label": "رمز واجهة برمجة تطبيقات Cloudflare",
"description": "رمز مقيد بصلاحية Zone:DNS:Edit للنطاق الخاص بك."
}
},
"status": {
"title": "حالة الشهادة",
"none": "لا توجد شهادة",
"names": "يغطي {{names}}",
"issuer": "صادر عن {{issuer}}",
"expires": "تنتهي في {{date}}",
"lastIssued": "آخر إصدار {{date}}",
"lastError": "فشلت المحاولة الأخيرة: {{error}}",
"missing": "قم بتعيين النطاق والبريد الإلكتروني والحقول التي يحتاجها نوع التحدي الخاص بك، ثم احفظ التغييرات قبل طلب الشهادة.",
"request": "اطلب الشهادة الآن",
"requesting": "جارٍ طلب الشهادة...",
"issued": "تم إصدار الشهادة وتركيبها"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Управление на ACME сертификати",
"description": "Вижте състоянието на сертификата и заявете нов сертификат сега."
}
},
"settings": {
"autoRenew": {
"label": "Подновяване автоматично",
"description": "Проверявайте два пъти на ден и получавайте нов сертификат, когато няма такъв, не покрива домейна или изтича в рамките на 30 дни."
},
"domain": {
"label": "Домейн",
"description": "Публичното домейн име за сертификата.",
"placeholder": "termix.example.com"
},
"email": {
"label": "Имейл",
"description": "Контактен имейл за ACME акаунта и известия за изтичане.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Сертифициращ орган",
"description": "Къде да взема сертификата. Използвайте етапно тестване, за да не се достигат ограниченията на скоростта на Let's Encrypt.",
"letsencrypt": "Нека криптираме",
"letsencryptStaging": "Нека криптираме (подготовка)",
"custom": "Персонализирана ACME директория"
},
"directoryUrl": {
"label": "URL адрес на директорията на ACME",
"description": "Само за персонализиран сертифициращ орган. Трябва да е https URL адрес."
},
"challengeType": {
"label": "Тип предизвикателство",
"description": "Как да докажете, че притежавате домейна.",
"http": "HTTP - изисква порт 80, достъпен от интернет",
"cloudflare": "DNS (Cloudflare) - изисква Cloudflare API токен"
},
"cloudflareToken": {
"label": "Cloudflare API токен",
"description": "Ограничен токен с разрешение Zone:DNS:Edit за вашия домейн."
}
},
"status": {
"title": "Състояние на сертификата",
"none": "Няма сертификат",
"names": "Обхваща {{names}}",
"issuer": "Издадено от {{issuer}}",
"expires": "Изтича {{date}}",
"lastIssued": "Последно издадено {{date}}",
"lastError": "Последният опит не беше успешен: {{error}}",
"missing": "Задайте домейна, имейла и полетата, необходими за вашия тип предизвикателство, след което ги запазете, преди да заявите сертификат.",
"request": "Заявете сертификат сега",
"requesting": "Заявка за сертификат...",
"issued": "Сертификат издаден и инсталиран"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "ACME সার্টিফিকেটগুলি পরিচালনা করুন",
"description": "সার্টিফিকেটের অবস্থা দেখুন এবং এখনই একটি নতুন সার্টিফিকেটের জন্য অনুরোধ করুন।"
}
},
"settings": {
"autoRenew": {
"label": "স্বয়ংক্রিয়ভাবে নবায়ন করুন",
"description": "দিনে দুইবার পরীক্ষা করুন এবং যখন কোনো সার্টিফিকেট থাকবে না, সেটি ডোমেইনকে কভার করবে না, অথবা ৩০ দিনের মধ্যে এর মেয়াদ শেষ হয়ে যাবে, তখন একটি নতুন সার্টিফিকেট সংগ্রহ করুন।"
},
"domain": {
"label": "ডোমেইন",
"description": "সার্টিফিকেটটির পাবলিক ডোমেইন নাম।",
"placeholder": "termix.example.com"
},
"email": {
"label": "ইমেল",
"description": "ACME অ্যাকাউন্ট এবং মেয়াদ শেষ হওয়ার বিজ্ঞপ্তির জন্য যোগাযোগের ইমেল।",
"placeholder": "admin@example.com"
},
"provider": {
"label": "শংসাপত্র কর্তৃপক্ষ",
"description": "সার্টিফিকেটটি কোথা থেকে পাবেন। Let's Encrypt-এর রেট লিমিট অতিক্রম না করে পরীক্ষা করার জন্য স্টেজিং ব্যবহার করুন।",
"letsencrypt": "আসুন এনক্রিপ্ট করি",
"letsencryptStaging": "লেটস এনক্রিপ্ট (স্টেজিং)",
"custom": "কাস্টম ACME ডিরেক্টরি"
},
"directoryUrl": {
"label": "ACME ডিরেক্টরি URL",
"description": "শুধুমাত্র কাস্টম সার্টিফিকেট অথরিটির জন্য। অবশ্যই একটি https URL হতে হবে।"
},
"challengeType": {
"label": "চ্যালেঞ্জের ধরণ",
"description": "ডোমেইনটির মালিকানা কীভাবে প্রমাণ করবেন।",
"http": "HTTP - এর জন্য ইন্টারনেট থেকে পোর্ট ৮০ অ্যাক্সেসযোগ্য হতে হবে।",
"cloudflare": "ডিএনএস (ক্লাউডফ্লেয়ার) - একটি ক্লাউডফ্লেয়ার এপিআই টোকেন প্রয়োজন"
},
"cloudflareToken": {
"label": "ক্লাউডফ্লেয়ার এপিআই টোকেন",
"description": "আপনার ডোমেনের জন্য Zone:DNS:Edit অনুমতিসহ স্কোপড টোকেন।"
}
},
"status": {
"title": "সার্টিফিকেটের অবস্থা",
"none": "কোন সার্টিফিকেট নেই",
"names": "{{names}} কভার করে",
"issuer": "{{issuer}} দ্বারা জারি করা হয়েছে",
"expires": "মেয়াদ শেষ হবে {{date}}",
"lastIssued": "সর্বশেষ প্রকাশিত {{date}}",
"lastError": "শেষ চেষ্টা ব্যর্থ: {{error}}",
"missing": "সার্টিফিকেটের জন্য অনুরোধ করার আগে, ডোমেইন, ইমেল এবং আপনার চ্যালেঞ্জ টাইপের জন্য প্রয়োজনীয় ফিল্ডগুলো সেট করুন, তারপর সেভ করুন।",
"request": "এখনই সার্টিফিকেটের জন্য অনুরোধ করুন",
"requesting": "সার্টিফিকেটের জন্য অনুরোধ...",
"issued": "সার্টিফিকেট ইস্যু এবং ইনস্টল করা হয়েছে"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Gestionar certificats ACME",
"description": "Consulta l'estat del certificat i sol·licita'n un de nou ara."
}
},
"settings": {
"autoRenew": {
"label": "Renovar automàticament",
"description": "Revisa dues vegades al dia i aconsegueix un certificat nou quan no n'hi ha cap, no cobreix el domini o caduca en 30 dies."
},
"domain": {
"label": "Domini",
"description": "El nom de domini públic del certificat.",
"placeholder": "termix.example.com"
},
"email": {
"label": "Correu electrònic",
"description": "Correu electrònic de contacte per al compte ACME i avisos de caducitat.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Autoritat de certificació",
"description": "On s'obté el certificat. Utilitzeu la configuració estadística per fer proves sense arribar als límits de velocitat de Let's Encrypt.",
"letsencrypt": "Xifrem",
"letsencryptStaging": "Xifrem (posada en escena)",
"custom": "Directori ACME personalitzat"
},
"directoryUrl": {
"label": "URL del directori ACME",
"description": "Només per a una autoritat de certificació personalitzada. Ha de ser una URL https."
},
"challengeType": {
"label": "Tipus de repte",
"description": "Com demostrar que ets el propietari del domini.",
"http": "HTTP: necessita que el port 80 sigui accessible des d'Internet",
"cloudflare": "DNS (Cloudflare): necessita un token d'API de Cloudflare"
},
"cloudflareToken": {
"label": "Token de l'API de Cloudflare",
"description": "Token amb àmbit i permís Zone:DNS:Edit per al vostre domini."
}
},
"status": {
"title": "Estat del certificat",
"none": "Sense certificat",
"names": "Cobertes {{names}}",
"issuer": "Publicat per {{issuer}}",
"expires": "Caduca {{date}}",
"lastIssued": "Última emissió {{date}}",
"lastError": "Últim intent fallit: {{error}}",
"missing": "Definiu el domini, el correu electrònic i els camps que necessita el vostre tipus de repte i, a continuació, deseu-ho abans de sol·licitar un certificat.",
"request": "Sol·licita el certificat ara",
"requesting": "Sol·licitant certificat...",
"issued": "Certificat emès i instal·lat"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Správa certifikátů ACME",
"description": "Zkontrolujte stav certifikátu a požádejte o nový certifikát."
}
},
"settings": {
"autoRenew": {
"label": "Automaticky obnovit",
"description": "Kontrolujte dvakrát denně a požádejte o nový certifikát, pokud žádný neexistuje, pokud se nevztahuje na danou doménu nebo pokud jeho platnost vyprší do 30 dnů."
},
"domain": {
"label": "Doména",
"description": "Název veřejné domény pro certifikát.",
"placeholder": "termix.example.com"
},
"email": {
"label": "E-mail",
"description": "Kontaktní e-mail pro účet ACME a oznámení o vypršení platnosti.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Certifikační autorita",
"description": "Kde získat certifikát. Použijte staging k testování bez překročení limitů rychlosti Let's Encrypt.",
"letsencrypt": "Pojďme šifrovat",
"letsencryptStaging": "Pojďme šifrovat (příprava)",
"custom": "Vlastní adresář ACME"
},
"directoryUrl": {
"label": "URL adresáře ACME",
"description": "Pouze pro vlastní certifikační autoritu. Musí se jednat o URL adresu typu https."
},
"challengeType": {
"label": "Typ výzvy",
"description": "Jak prokázat, že jste vlastníkem domény.",
"http": "HTTP - vyžaduje dostupnost portu 80 z internetu",
"cloudflare": "DNS (Cloudflare) – vyžaduje token Cloudflare API"
},
"cloudflareToken": {
"label": "Token API Cloudflare",
"description": "Token s vymezeným rozsahem a oprávněním Zóna:DNS:Úpravy pro vaši doménu."
}
},
"status": {
"title": "Stav certifikátu",
"none": "Žádný certifikát",
"names": "Kryty {{names}}",
"issuer": "Vydáno uživatelem {{issuer}}",
"expires": "Platnost vyprší {{date}}",
"lastIssued": "Naposledy vydáno {{date}}",
"lastError": "Poslední pokus se nezdařil: {{error}}",
"missing": "Před vyžádáním certifikátu nastavte doménu, e-mail a pole, která váš typ výzvy vyžaduje, a poté je uložte.",
"request": "Vyžádejte si certifikát nyní",
"requesting": "Žádost o certifikát...",
"issued": "Certifikát vydán a nainstalován"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Administrer ACME-certifikater",
"description": "Se certifikatstatussen, og anmod om et nyt certifikat nu."
}
},
"settings": {
"autoRenew": {
"label": "Forny automatisk",
"description": "Tjek to gange om dagen, og få et nyt certifikat, hvis der ikke er noget, det ikke dækker domænet, eller det udløber inden for 30 dage."
},
"domain": {
"label": "Domæne",
"description": "Det offentlige domænenavn for certifikatet.",
"placeholder": "termix.example.com"
},
"email": {
"label": "E-mail",
"description": "Kontakt-e-mailadresse for ACME-kontoen og udløbsmeddelelser.",
"placeholder": "admin@eksempel.com"
},
"provider": {
"label": "Certifikatmyndighed",
"description": "Hvor kan man få certifikatet? Brug staging til at teste uden at nå Let's Encrypt-hastighedsgrænserne.",
"letsencrypt": "Lad os kryptere",
"letsencryptStaging": "Lad os kryptere (staging)",
"custom": "Brugerdefineret ACME-mappe"
},
"directoryUrl": {
"label": "ACME-katalogets URL",
"description": "Kun for en brugerdefineret certifikatmyndighed. Skal være en https-URL."
},
"challengeType": {
"label": "Udfordringstype",
"description": "Sådan beviser du, at du ejer domænet.",
"http": "HTTP - kræver port 80, der kan nås fra internettet",
"cloudflare": "DNS (Cloudflare) - kræver et Cloudflare API-token"
},
"cloudflareToken": {
"label": "Cloudflare API-token",
"description": "Scoped-token med Zone:DNS:Rediger-tilladelse for dit domæne."
}
},
"status": {
"title": "Certifikatstatus",
"none": "Intet certifikat",
"names": "Dækker {{names}}",
"issuer": "Udstedt af {{issuer}}",
"expires": "Udløber {{date}}",
"lastIssued": "Sidst udstedt {{date}}",
"lastError": "Sidste forsøg mislykkedes: {{error}}",
"missing": "Angiv domæne, e-mail og de felter, som din udfordringstype kræver, og gem derefter, før du anmoder om et certifikat.",
"request": "Anmod om certifikat nu",
"requesting": "Anmoder om certifikat...",
"issued": "Certifikat udstedt og installeret"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "ACME-Zertifikate verwalten",
"description": "Prüfen Sie den Zertifikatsstatus und fordern Sie jetzt ein neues Zertifikat an."
}
},
"settings": {
"autoRenew": {
"label": "Automatische Verlängerung",
"description": "Prüfen Sie zweimal täglich, ob ein Zertifikat vorhanden ist, ob das aktuelle Zertifikat die Domain nicht abdeckt oder ob es innerhalb von 30 Tagen abläuft."
},
"domain": {
"label": "Domain",
"description": "Der öffentliche Domainname für das Zertifikat.",
"placeholder": "termix.example.com"
},
"email": {
"label": "E-Mail",
"description": "Kontakt-E-Mail-Adresse für das ACME-Konto und Benachrichtigungen zum Ablaufdatum.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Zertifizierungsstelle",
"description": "Wo Sie das Zertifikat erhalten. Nutzen Sie die Staging-Umgebung, um zu testen, ohne die Ratenbegrenzungen von Let's Encrypt zu erreichen.",
"letsencrypt": "Let's Encrypt",
"letsencryptStaging": "Let's Encrypt (Staging)",
"custom": "Benutzerdefiniertes ACME-Verzeichnis"
},
"directoryUrl": {
"label": "ACME-Verzeichnis-URL",
"description": "Nur für eine benutzerdefinierte Zertifizierungsstelle. Muss eine HTTPS-URL sein."
},
"challengeType": {
"label": "Herausforderungstyp",
"description": "Wie Sie nachweisen, dass Sie die Domain besitzen.",
"http": "HTTP – benötigt Port 80, der aus dem Internet erreichbar sein muss.",
"cloudflare": "DNS (Cloudflare) – benötigt ein Cloudflare-API-Token"
},
"cloudflareToken": {
"label": "Cloudflare API-Token",
"description": "Bereichsbezogener Token mit Zone:DNS:Edit-Berechtigung für Ihre Domain."
}
},
"status": {
"title": "Zertifikatsstatus",
"none": "Kein Zertifikat",
"names": "Cover {{names}}",
"issuer": "Herausgegeben von {{issuer}}",
"expires": "Läuft ab {{date}}",
"lastIssued": "Zuletzt ausgestellt {{date}}",
"lastError": "Letzter Versuch fehlgeschlagen: {{error}}",
"missing": "Legen Sie die Domain, die E-Mail-Adresse und die Felder fest, die Ihr Herausforderungstyp benötigt, und speichern Sie die Einstellungen, bevor Sie ein Zertifikat anfordern.",
"request": "Zertifikat jetzt anfordern",
"requesting": "Zertifikat wird angefordert...",
"issued": "Zertifikat ausgestellt und installiert"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Διαχείριση πιστοποιητικών ACME",
"description": "Δείτε την κατάσταση του πιστοποιητικού και ζητήστε ένα νέο πιστοποιητικό τώρα."
}
},
"settings": {
"autoRenew": {
"label": "Αυτόματη ανανέωση",
"description": "Ελέγχετε δύο φορές την ημέρα και λαμβάνετε ένα νέο πιστοποιητικό όταν δεν υπάρχει, δεν καλύπτει τον τομέα ή λήγει εντός 30 ημερών."
},
"domain": {
"label": "Πεδίο ορισμού",
"description": "Το όνομα δημόσιου τομέα για το πιστοποιητικό.",
"placeholder": "termix.example.com"
},
"email": {
"label": "E-mail",
"description": "Ηλεκτρονική διεύθυνση ηλεκτρονικού ταχυδρομείου επικοινωνίας για τον λογαριασμό ACME και τις ειδοποιήσεις λήξης.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Αρχή έκδοσης πιστοποιητικών",
"description": "Πού να λάβετε το πιστοποιητικό. Χρησιμοποιήστε τη σταδιακή επεξεργασία για να δοκιμάσετε χωρίς να υπερβείτε τα όρια ρυθμού του Let's Encrypt.",
"letsencrypt": "Ας κρυπτογραφήσουμε",
"letsencryptStaging": "Ας κρυπτογραφήσουμε (σταδιοποίηση)",
"custom": "Προσαρμοσμένος κατάλογος ACME"
},
"directoryUrl": {
"label": "URL καταλόγου ACME",
"description": "Μόνο για μια προσαρμοσμένη αρχή έκδοσης πιστοποιητικών. Πρέπει να είναι μια διεύθυνση URL https."
},
"challengeType": {
"label": "Τύπος πρόκλησης",
"description": "Πώς να αποδείξετε ότι είστε ο κάτοχος του domain.",
"http": "HTTP - χρειάζεται θύρα 80 προσβάσιμη από το διαδίκτυο",
"cloudflare": "DNS (Cloudflare) - χρειάζεται ένα διακριτικό API Cloudflare"
},
"cloudflareToken": {
"label": "Διακριτικό API Cloudflare",
"description": "Διακριτικό πεδίου εφαρμογής με δικαίωμα Zone:DNS:Edit για τον τομέα σας."
}
},
"status": {
"title": "Κατάσταση πιστοποιητικού",
"none": "Δεν υπάρχει πιστοποιητικό",
"names": "Καλύμματα {{names}}",
"issuer": "Εκδόθηκε από {{issuer}}",
"expires": "Λήγει {{date}}",
"lastIssued": "Τελευταία έκδοση {{date}}",
"lastError": "Η τελευταία προσπάθεια απέτυχε: {{error}}",
"missing": "Ορίστε τον τομέα, το email και τα πεδία που χρειάζεται ο τύπος της πρόκλησής σας και, στη συνέχεια, αποθηκεύστε το, πριν ζητήσετε πιστοποιητικό.",
"request": "Ζητήστε πιστοποιητικό τώρα",
"requesting": "Αίτημα πιστοποιητικού...",
"issued": "Έκδοση και εγκατάσταση πιστοποιητικού"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Gestionar certificados ACME",
"description": "Consulte el estado del certificado y solicite uno nuevo ahora."
}
},
"settings": {
"autoRenew": {
"label": "Renovación automática",
"description": "Compruebe dos veces al día y obtenga un nuevo certificado si no tiene ninguno, si no cubre el dominio o si caduca en los próximos 30 días."
},
"domain": {
"label": "Dominio",
"description": "El nombre de dominio público para el certificado.",
"placeholder": "termix.example.com"
},
"email": {
"label": "Correo electrónico",
"description": "Correo electrónico de contacto para la cuenta ACME y avisos de vencimiento.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Autoridad de certificación",
"description": "Dónde obtener el certificado. Utilice el entorno de pruebas para realizar pruebas sin alcanzar los límites de uso de Let's Encrypt.",
"letsencrypt": "Let's Encrypt",
"letsencryptStaging": "Let's Encrypt (en fase de pruebas)",
"custom": "Directorio ACME personalizado"
},
"directoryUrl": {
"label": "URL del directorio ACME",
"description": "Solo para una autoridad de certificación personalizada. Debe ser una URL HTTPS."
},
"challengeType": {
"label": "Tipo de desafío",
"description": "Cómo demostrar que usted es el propietario del dominio.",
"http": "HTTP: necesita que el puerto 80 sea accesible desde Internet.",
"cloudflare": "DNS (Cloudflare): requiere un token de API de Cloudflare."
},
"cloudflareToken": {
"label": "Token de API de Cloudflare",
"description": "Token con permiso Zone:DNS:Edit limitado para tu dominio."
}
},
"status": {
"title": "Estado del certificado",
"none": "Sin certificado",
"names": "Portadas {{names}}",
"issuer": "Emitido por {{issuer}}",
"expires": "Caduca {{date}}",
"lastIssued": "Última emisión {{date}}",
"lastError": "Último intento fallido: {{error}}",
"missing": "Configure el dominio, el correo electrónico y los campos que requiere su tipo de desafío, luego guarde los cambios y, a continuación, solicite un certificado.",
"request": "Solicita tu certificado ahora",
"requesting": "Solicitando certificado...",
"issued": "Certificado emitido e instalado"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Hallinnoi ACME-sertifikaatteja",
"description": "Tarkista varmenteen tila ja pyydä uusi varmenne nyt."
}
},
"settings": {
"autoRenew": {
"label": "Uudista automaattisesti",
"description": "Tarkista kahdesti päivässä ja hanki uusi varmenne, jos sitä ei ole, se ei kata verkkotunnusta tai se vanhenee 30 päivän kuluessa."
},
"domain": {
"label": "Verkkotunnus",
"description": "Varmenteen julkinen verkkotunnus.",
"placeholder": "termix.esimerkki.com"
},
"email": {
"label": "Sähköposti",
"description": "ACME-tilin yhteystiedot ja vanhenemisilmoitukset.",
"placeholder": "admin@esimerkki.com"
},
"provider": {
"label": "Varmentaja",
"description": "Mistä varmenteen saa? Käytä testausympäristöä Let's Encrypt -nopeusrajoitusten ylittämättä.",
"letsencrypt": "Salataanpa",
"letsencryptStaging": "Let's Encrypt (vaiheistus)",
"custom": "Mukautettu ACME-hakemisto"
},
"directoryUrl": {
"label": "ACME-hakemiston URL-osoite",
"description": "Vain mukautetulle varmentajalle. URL-osoitteen on oltava https."
},
"challengeType": {
"label": "Haasteen tyyppi",
"description": "Kuinka todistaa omistavasi verkkotunnuksen.",
"http": "HTTP - vaatii portin 80, johon internetistä voi päästä käsiksi",
"cloudflare": "DNS (Cloudflare) – vaatii Cloudflare API -tunnuksen"
},
"cloudflareToken": {
"label": "Cloudflare API -tunnus",
"description": "Laajuusaluetunnus, jolla on Zone:DNS:Edit-käyttöoikeus verkkotunnuksellesi."
}
},
"status": {
"title": "Sertifikaatin tila",
"none": "Ei todistusta",
"names": "Kannet {{names}}",
"issuer": "Myöntäjä {{issuer}}",
"expires": "Vanhenee {{date}}",
"lastIssued": "Viimeksi myönnetty {{date}}",
"lastError": "Viimeisin yritys epäonnistui: {{error}}",
"missing": "Aseta verkkotunnus, sähköpostiosoite ja haastetyyppisi tarvitsemat kentät ja tallenna ne ennen varmenteen pyytämistä.",
"request": "Pyydä todistus nyt",
"requesting": "Pyydetään varmennetta...",
"issued": "Sertifikaatti myönnetty ja asennettu"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Gérer les certificats ACME",
"description": "Consultez l'état du certificat et demandez un nouveau certificat dès maintenant."
}
},
"settings": {
"autoRenew": {
"label": "Renouvellement automatique",
"description": "Vérifiez deux fois par jour et obtenez un nouveau certificat s'il n'y en a pas, s'il ne couvre pas le domaine ou s'il expire dans les 30 jours."
},
"domain": {
"label": "Domaine",
"description": "Le nom de domaine public pour le certificat.",
"placeholder": "termix.example.com"
},
"email": {
"label": "E-mail",
"description": "Adresse électronique de contact pour le compte ACME et les avis d'expiration.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Autorité de certification",
"description": "Où obtenir le certificat ? Utilisez un environnement de test pour effectuer des tests sans atteindre les limites de requêtes de Let's Encrypt.",
"letsencrypt": "Let's Encrypt",
"letsencryptStaging": "Let's Encrypt (phase de test)",
"custom": "Répertoire ACME personnalisé"
},
"directoryUrl": {
"label": "URL du répertoire ACME",
"description": "Uniquement pour une autorité de certification personnalisée. Doit être une URL HTTPS."
},
"challengeType": {
"label": "Type de défi",
"description": "Comment prouver que vous êtes propriétaire du domaine.",
"http": "HTTP – nécessite que le port 80 soit accessible depuis Internet",
"cloudflare": "DNS (Cloudflare) - nécessite un jeton d'API Cloudflare"
},
"cloudflareToken": {
"label": "Jeton d'API Cloudflare",
"description": "Jeton limité avec l'autorisation Zone:DNS:Edit pour votre domaine."
}
},
"status": {
"title": "Statut du certificat",
"none": "Aucun certificat",
"names": "Couvertures {{names}}",
"issuer": "Émis par {{issuer}}",
"expires": "Expire le {{date}}",
"lastIssued": "Dernière émission le {{date}}",
"lastError": "Dernière tentative échouée : {{error}}",
"missing": "Configurez le domaine, l'adresse e-mail et les champs requis par votre type de défi, puis enregistrez, avant de demander un certificat.",
"request": "Demandez votre certificat dès maintenant",
"requesting": "Demande de certificat en cours...",
"issued": "Certificat délivré et installé"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "ניהול אישורי ACME",
"description": "ראה את סטטוס האישור ובקש אישור חדש כעת."
}
},
"settings": {
"autoRenew": {
"label": "חידוש אוטומטי",
"description": "בדקו פעמיים ביום וקבלו תעודה חדשה אם אין, אם היא לא מכסה את הדומיין, או אם היא פגה תוך 30 יום."
},
"domain": {
"label": "תְחוּם",
"description": "שם התחום הציבורי עבור התעודה.",
"placeholder": "termix.example.com"
},
"email": {
"label": "אֶלֶקטרוֹנִי",
"description": "כתובת דוא\"ל ליצירת קשר עבור חשבון ACME והודעות תפוגה.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "רשות אישורים",
"description": "היכן ניתן להשיג את האישור. השתמשו ב-staging כדי לבדוק מבלי להגיע למגבלות התעריף של Let's Encrypt.",
"letsencrypt": "בואו נצפין",
"letsencryptStaging": "בואו נצפין (שלבי)",
"custom": "ספריית ACME מותאמת אישית"
},
"directoryUrl": {
"label": "כתובת אתר של ספריית ACME",
"description": "רק עבור רשות אישורים מותאמת אישית. חייבת להיות כתובת URL מסוג https."
},
"challengeType": {
"label": "סוג האתגר",
"description": "איך להוכיח שאתה הבעלים של הדומיין.",
"http": "HTTP - דורש גישה לפורט 80 מהאינטרנט",
"cloudflare": "DNS (Cloudflare) - דורש אסימון API של Cloudflare"
},
"cloudflareToken": {
"label": "אסימון API של Cloudflare",
"description": "אסימון בטווח עם הרשאת עריכה של אזור:DNS: עבור הדומיין שלך."
}
},
"status": {
"title": "סטטוס התעודה",
"none": "אין תעודה",
"names": "כריכות {{names}}",
"issuer": "הונפק על ידי {{issuer}}",
"expires": "פג תוקף {{date}}",
"lastIssued": "הוצאה אחרונה {{date}}",
"lastError": "ניסיון אחרון נכשל: {{error}}",
"missing": "הגדר את הדומיין, האימייל והשדות הנדרשים עבור סוג האתגר שלך, ולאחר מכן שמור, לפני בקשת האישור.",
"request": "בקשת תעודה עכשיו",
"requesting": "מבקש תעודה...",
"issued": "תעודה הונפקה והותקנה"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "ACME प्रमाणपत्रों का प्रबंधन करें",
"description": "प्रमाणपत्र की स्थिति देखें और अभी नए प्रमाणपत्र के लिए अनुरोध करें।"
}
},
"settings": {
"autoRenew": {
"label": "स्वचालित रूप से नवीनीकरण करें",
"description": "दिन में दो बार जांच करें और यदि कोई प्रमाणपत्र मौजूद नहीं है, या वह डोमेन को कवर नहीं करता है, या 30 दिनों के भीतर समाप्त हो रहा है, तो नया प्रमाणपत्र प्राप्त करें।"
},
"domain": {
"label": "डोमेन",
"description": "प्रमाणपत्र के लिए सार्वजनिक डोमेन नाम।",
"placeholder": "termix.example.com"
},
"email": {
"label": "ईमेल",
"description": "ACME खाते और समाप्ति सूचनाओं के लिए संपर्क ईमेल पता।",
"placeholder": "admin@example.com"
},
"provider": {
"label": "प्रमाणपत्र प्राधिकरण",
"description": "सर्टिफिकेट कहां से प्राप्त करें। Let's Encrypt की दर सीमा का उल्लंघन किए बिना परीक्षण के लिए स्टेजिंग का उपयोग करें।",
"letsencrypt": "आइए एन्क्रिप्ट करें",
"letsencryptStaging": "लेट्स एनक्रिप्ट (चरणबद्ध)",
"custom": "कस्टम एसीएमई निर्देशिका"
},
"directoryUrl": {
"label": "ACME निर्देशिका URL",
"description": "केवल कस्टम सर्टिफिकेट अथॉरिटी के लिए। यूआरएल HTTPS होना चाहिए।"
},
"challengeType": {
"label": "चुनौती का प्रकार",
"description": "आप डोमेन के मालिक होने का प्रमाण कैसे दे सकते हैं?",
"http": "HTTP - इसके लिए इंटरनेट से पोर्ट 80 तक पहुंच आवश्यक है।",
"cloudflare": "DNS (Cloudflare) - इसके लिए Cloudflare API टोकन की आवश्यकता होती है।"
},
"cloudflareToken": {
"label": "क्लाउडफ्लेयर एपीआई टोकन",
"description": "आपके डोमेन के लिए Zone:DNS:Edit अनुमति वाला स्कोप्ड टोकन।"
}
},
"status": {
"title": "प्रमाणपत्र की स्थिति",
"none": "कोई प्रमाणपत्र नहीं",
"names": "कवर {{names}}",
"issuer": "{{issuer}} द्वारा जारी किया गया",
"expires": "{{date}} को समाप्त होगा",
"lastIssued": "अंतिम जारी: {{date}}",
"lastError": "अंतिम प्रयास विफल रहा: {{error}}",
"missing": "डोमेन, ईमेल और आपके चैलेंज टाइप के लिए आवश्यक फ़ील्ड सेट करें, फिर प्रमाणपत्र का अनुरोध करने से पहले सेव करें।",
"request": "अभी प्रमाणपत्र का अनुरोध करें",
"requesting": "प्रमाणपत्र अनुरोध किया जा रहा है...",
"issued": "प्रमाणपत्र जारी और स्थापित किया गया"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "ACME tanúsítványok kezelése",
"description": "Tekintse meg a tanúsítvány állapotát, és igényeljen új tanúsítványt most."
}
},
"settings": {
"autoRenew": {
"label": "Automatikus megújítás",
"description": "Naponta kétszer ellenőrizd, és szerezz be új tanúsítványt, ha nincs még tanúsítvány, ha nem fedi le a domaint, vagy ha 30 napon belül lejár."
},
"domain": {
"label": "Domain",
"description": "A tanúsítvány nyilvános domainneve.",
"placeholder": "termix.example.com"
},
"email": {
"label": "Email",
"description": "Az ACME fiókkal és a lejárati értesítésekkel kapcsolatos elérhetőségi e-mail cím.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Tanúsítványkibocsátó",
"description": "Hol lehet beszerezni a tanúsítványt? Használjon előzetes tesztelést a Let's Encrypt sebességkorlátainak elérése nélkül.",
"letsencrypt": "Titkosítsunk",
"letsencryptStaging": "Titkosítsuk (előkészítés)",
"custom": "Egyéni ACME címtár"
},
"directoryUrl": {
"label": "ACME könyvtár URL-címe",
"description": "Csak egyéni hitelesítésszolgáltató esetén. Https URL-címnek kell lennie."
},
"challengeType": {
"label": "Kihívás típusa",
"description": "Hogyan bizonyíthatod, hogy te vagy a domain tulajdonosa?",
"http": "HTTP – internetről elérhető 80-as port szükséges",
"cloudflare": "DNS (Cloudflare) – Cloudflare API token szükséges"
},
"cloudflareToken": {
"label": "Cloudflare API token",
"description": "Hatókörbe tartozó token Zóna:DNS:Szerkesztés engedéllyel a domainhez."
}
},
"status": {
"title": "Tanúsítvány állapota",
"none": "Nincs tanúsítvány",
"names": "Borítók {{names}}",
"issuer": "Kibocsátó: {{issuer}}",
"expires": "Lejár {{date}}",
"lastIssued": "Utolsó kiadás {{date}}",
"lastError": "Utolsó kísérlet sikertelen: {{error}}",
"missing": "Állítsa be a domaint, az e-mail címet és a kihívástípushoz szükséges mezőket, majd mentse el a beállításokat a tanúsítvány igénylése előtt.",
"request": "Igényeljen tanúsítványt most",
"requesting": "Tanúsítvány kérése...",
"issued": "Tanúsítvány kiállítva és telepítve"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Kelola sertifikat ACME",
"description": "Lihat status sertifikat dan ajukan permohonan sertifikat baru sekarang."
}
},
"settings": {
"autoRenew": {
"label": "Perpanjang secara otomatis",
"description": "Periksa dua kali sehari dan dapatkan sertifikat baru jika tidak ada, tidak mencakup domain, atau akan kedaluwarsa dalam 30 hari."
},
"domain": {
"label": "Domain",
"description": "Nama domain publik untuk sertifikat tersebut.",
"placeholder": "termix.example.com"
},
"email": {
"label": "E-mail",
"description": "Alamat email kontak untuk akun ACME dan pemberitahuan masa berlaku.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Lembaga sertifikasi",
"description": "Di mana mendapatkan sertifikat? Gunakan staging untuk pengujian tanpa melampaui batas kuota Let's Encrypt.",
"letsencrypt": "Mari Enkripsi",
"letsencryptStaging": "Let's Encrypt (pementasan)",
"custom": "Direktori ACME khusus"
},
"directoryUrl": {
"label": "URL direktori ACME",
"description": "Hanya untuk otoritas sertifikat khusus. Harus berupa URL https."
},
"challengeType": {
"label": "Jenis tantangan",
"description": "Bagaimana cara membuktikan bahwa Anda pemilik domain tersebut.",
"http": "HTTP - memerlukan port 80 yang dapat diakses dari internet.",
"cloudflare": "DNS (Cloudflare) - memerlukan token API Cloudflare"
},
"cloudflareToken": {
"label": "Token API Cloudflare",
"description": "Token terlingkup dengan izin Zone:DNS:Edit untuk domain Anda."
}
},
"status": {
"title": "Status sertifikat",
"none": "Tidak ada sertifikat",
"names": "Sampul {{names}}",
"issuer": "Dikeluarkan oleh {{issuer}}",
"expires": "Berakhir {{date}}",
"lastIssued": "Terakhir diterbitkan {{date}}",
"lastError": "Upaya terakhir gagal: {{error}}",
"missing": "Atur domain, email, dan kolom yang dibutuhkan oleh tipe tantangan Anda, lalu simpan, sebelum meminta sertifikat.",
"request": "Ajukan sertifikat sekarang",
"requesting": "Meminta sertifikat...",
"issued": "Sertifikat telah diterbitkan dan dipasang."
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Gestire i certificati ACME",
"description": "Verifica lo stato del certificato e richiedine uno nuovo ora."
}
},
"settings": {
"autoRenew": {
"label": "Rinnovo automatico",
"description": "Controlla due volte al giorno e richiedi un nuovo certificato se non ne trovi uno, se non copre il dominio o se scade entro 30 giorni."
},
"domain": {
"label": "Dominio",
"description": "Il nome di dominio pubblico per il certificato.",
"placeholder": "termix.example.com"
},
"email": {
"label": "Email",
"description": "Indirizzo email per contattare l'account ACME e per ricevere avvisi di scadenza.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Autorità di certificazione",
"description": "Dove ottenere il certificato. Utilizza un ambiente di staging per effettuare dei test senza superare i limiti di frequenza di Let's Encrypt.",
"letsencrypt": "Let's Encrypt",
"letsencryptStaging": "Let's Encrypt (staging)",
"custom": "Elenco personalizzato ACME"
},
"directoryUrl": {
"label": "URL della directory ACME",
"description": "Solo per un'autorità di certificazione personalizzata. Deve essere un URL HTTPS."
},
"challengeType": {
"label": "Tipo di sfida",
"description": "Come dimostrare di essere il proprietario del dominio.",
"http": "HTTP - richiede che la porta 80 sia raggiungibile da Internet",
"cloudflare": "DNS (Cloudflare) - richiede un token API di Cloudflare"
},
"cloudflareToken": {
"label": "Token API di Cloudflare",
"description": "Token con autorizzazione Zone:DNS:Edit per il tuo dominio."
}
},
"status": {
"title": "Stato del certificato",
"none": "Nessun certificato",
"names": "Copertine {{names}}",
"issuer": "Emesso da {{issuer}}",
"expires": "Scade il {{date}}",
"lastIssued": "Ultimo rilascio il {{date}}",
"lastError": "Ultimo tentativo fallito: {{error}}",
"missing": "Imposta il dominio, l'indirizzo email e i campi richiesti dal tuo tipo di sfida, quindi salva prima di richiedere un certificato.",
"request": "Richiedi subito il certificato",
"requesting": "Richiesta certificato in corso...",
"issued": "Certificato rilasciato e installato"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "ACME証明書を管理する",
"description": "証明書のステータスを確認し、新しい証明書を今すぐリクエストしてください。"
}
},
"settings": {
"autoRenew": {
"label": "自動更新",
"description": "1日に2回確認し、証明書がない場合、ドメインをカバーしていない場合、または有効期限が30日以内に切れる場合は、新しい証明書を取得してください。"
},
"domain": {
"label": "ドメイン",
"description": "証明書の公開ドメイン名。",
"placeholder": "termix.example.com"
},
"email": {
"label": "メールアドレス",
"description": "ACMEアカウントおよび有効期限通知に関する連絡先メールアドレス。",
"placeholder": "admin@example.com"
},
"provider": {
"label": "認証局",
"description": "証明書の入手方法。Let's Encryptのレート制限に抵触しないよう、ステージング環境を使用してテストしてください。",
"letsencrypt": "レッツ・エンクリプト",
"letsencryptStaging": "Let's Encrypt(ステージング環境)",
"custom": "カスタムACMEディレクトリ"
},
"directoryUrl": {
"label": "ACMEディレクトリURL",
"description": "カスタム認証局のみ対象です。URLはhttpsでなければなりません。"
},
"challengeType": {
"label": "チャレンジの種類",
"description": "ドメインの所有権を証明する方法。",
"http": "HTTP - インターネットからアクセス可能なポート80が必要です",
"cloudflare": "DNS(Cloudflare) - Cloudflare APIトークンが必要です"
},
"cloudflareToken": {
"label": "Cloudflare APIトークン",
"description": "ドメインに対するZone:DNS:Edit権限を持つスコープ付きトークン。"
}
},
"status": {
"title": "証明書のステータス",
"none": "証明書なし",
"names": "カバー {{names}}",
"issuer": "{{issuer}} によって発行されました",
"expires": "有効期限 {{date}}",
"lastIssued": "最終発行日 {{date}}",
"lastError": "最後の試行が失敗しました: {{error}}",
"missing": "証明書をリクエストする前に、ドメイン、メールアドレス、およびチャレンジタイプに必要なフィールドを設定して保存してください。",
"request": "今すぐ証明書を申請する",
"requesting": "証明書をリクエスト中...",
"issued": "証明書が発行され、インストールされました。"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "ACME 인증서를 관리하세요",
"description": "인증서 상태를 확인하고 지금 바로 새 인증서를 신청하세요."
}
},
"settings": {
"autoRenew": {
"label": "자동으로 갱신됩니다",
"description": "하루에 두 번 확인하고, 인증서가 없거나, 도메인을 포함하지 않거나, 30일 이내에 만료되는 경우 새 인증서를 발급받으세요."
},
"domain": {
"label": "도메인",
"description": "인증서에 사용할 공개 도메인 이름입니다.",
"placeholder": "termix.example.com"
},
"email": {
"label": "이메일",
"description": "ACME 계정 및 만료 알림을 받을 수 있는 이메일 주소입니다.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "인증 기관",
"description": "인증서는 어디서 발급받나요? Let's Encrypt의 사용량 제한에 걸리지 않고 테스트하려면 스테이징 환경을 사용하세요.",
"letsencrypt": "렛츠인크립트",
"letsencryptStaging": "렛츠인크립트(스테이징)",
"custom": "사용자 지정 ACME 디렉토리"
},
"directoryUrl": {
"label": "ACME 디렉토리 URL",
"description": "사용자 지정 인증 기관에만 해당됩니다. URL은 HTTPS여야 합니다."
},
"challengeType": {
"label": "도전 유형",
"description": "도메인 소유권을 증명하는 방법.",
"http": "HTTP - 인터넷에서 접근 가능한 80번 포트가 필요합니다.",
"cloudflare": "DNS(클라우드플레어) - 클라우드플레어 API 토큰이 필요합니다."
},
"cloudflareToken": {
"label": "클라우드플레어 API 토큰",
"description": "도메인에 대한 Zone:DNS:Edit 권한이 있는 범위 지정 토큰입니다."
}
},
"status": {
"title": "인증서 상태",
"none": "인증서 없음",
"names": "{{names}}을 덮습니다.",
"issuer": "{{issuer}}에서 발행함",
"expires": "만료일: {{date}}",
"lastIssued": "마지막 발급일: {{date}}",
"lastError": "마지막 시도가 실패했습니다: {{error}}",
"missing": "인증서를 요청하기 전에 도메인, 이메일 및 인증 유형에 필요한 필드를 설정하고 저장하세요.",
"request": "지금 인증서를 요청하세요",
"requesting": "인증서 요청 중...",
"issued": "인증서가 발급 및 설치되었습니다."
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Beheer ACME-certificaten",
"description": "Bekijk de certificaatstatus en vraag nu een nieuw certificaat aan."
}
},
"settings": {
"autoRenew": {
"label": "Automatisch verlengen",
"description": "Controleer tweemaal per dag en vraag een nieuw certificaat aan als er geen certificaat is, het huidige certificaat het domein niet dekt of het huidige certificaat binnen 30 dagen verloopt."
},
"domain": {
"label": "Domein",
"description": "De publieke domeinnaam voor het certificaat.",
"placeholder": "termix.example.com"
},
"email": {
"label": "E-mail",
"description": "Contact-e-mailadres voor het ACME-account en meldingen over vervaldatums.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Certificeringsinstantie",
"description": "Waar kan ik het certificaat verkrijgen? Gebruik een testomgeving om te testen zonder de limieten van Let's Encrypt te overschrijden.",
"letsencrypt": "Let's Encrypt",
"letsencryptStaging": "Let's Encrypt (testomgeving)",
"custom": "Aangepaste ACME-directory"
},
"directoryUrl": {
"label": "ACME-directory-URL",
"description": "Alleen voor een aangepaste certificeringsinstantie. Moet een https-URL zijn."
},
"challengeType": {
"label": "Uitdagingstype",
"description": "Hoe bewijs je dat je de domeinnaam bezit?",
"http": "HTTP - vereist poort 80 die bereikbaar is vanaf internet.",
"cloudflare": "DNS (Cloudflare) - vereist een Cloudflare API-token"
},
"cloudflareToken": {
"label": "Cloudflare API-token",
"description": "Een scoped token met Zone:DNS:Edit-machtiging voor uw domein."
}
},
"status": {
"title": "Certificaatstatus",
"none": "Geen certificaat",
"names": "Omslagen {{names}}",
"issuer": "Uitgegeven door {{issuer}}",
"expires": "Verloopt {{date}}",
"lastIssued": "Laatst uitgegeven {{date}}",
"lastError": "Laatste poging mislukt: {{error}}",
"missing": "Stel het domein, het e-mailadres en de velden in die uw uitdagingstype vereist, sla de wijzigingen op en vraag vervolgens een certificaat aan.",
"request": "Vraag nu uw certificaat aan",
"requesting": "Certificaat aanvragen...",
"issued": "Certificaat afgegeven en geïnstalleerd"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Administrer ACME-sertifikater",
"description": "Se sertifikatstatusen og be om et nytt sertifikat nå."
}
},
"settings": {
"autoRenew": {
"label": "Forny automatisk",
"description": "Sjekk to ganger om dagen og få et nytt sertifikat når det ikke finnes noe, det ikke dekker domenet, eller det utløper innen 30 dager."
},
"domain": {
"label": "Domene",
"description": "Det offentlige domenenavnet for sertifikatet.",
"placeholder": "termix.example.com"
},
"email": {
"label": "E-post",
"description": "Kontakt-e-postadresse for ACME-kontoen og utløpsvarsler.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Sertifikatmyndighet",
"description": "Hvor får man tak i sertifikatet? Bruk staging for å teste uten å nå hastighetsgrensene til Let's Encrypt.",
"letsencrypt": "La oss kryptere",
"letsencryptStaging": "La oss kryptere (staging)",
"custom": "Tilpasset ACME-katalog"
},
"directoryUrl": {
"label": "URL-adressen til ACME-katalogen",
"description": "Kun for en tilpasset sertifiseringsinstans. Må være en https-URL."
},
"challengeType": {
"label": "Utfordringstype",
"description": "Hvordan bevise at du eier domenet.",
"http": "HTTP - trenger port 80 som kan nås fra internett",
"cloudflare": "DNS (Cloudflare) – trenger et Cloudflare API-token"
},
"cloudflareToken": {
"label": "Cloudflare API-token",
"description": "Scope-token med Zone:DNS:Rediger-tillatelse for domenet ditt."
}
},
"status": {
"title": "Sertifikatstatus",
"none": "Ingen sertifikat",
"names": "Omslag {{names}}",
"issuer": "Utstedt av {{issuer}}",
"expires": "Utløper {{date}}",
"lastIssued": "Sist utstedt {{date}}",
"lastError": "Siste forsøk mislyktes: {{error}}",
"missing": "Angi domenet, e-postadressen og feltene utfordringstypen din trenger, og lagre deretter før du ber om et sertifikat.",
"request": "Be om sertifikat nå",
"requesting": "Ber om sertifikat...",
"issued": "Sertifikat utstedt og installert"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Zarządzaj certyfikatami ACME",
"description": "Sprawdź status certyfikatu i poproś o nowy certyfikat już teraz."
}
},
"settings": {
"autoRenew": {
"label": "Odnów automatycznie",
"description": "Sprawdzaj dwa razy dziennie i wykup nowy certyfikat, jeśli go nie masz, nie obejmuje on tej domeny lub wygasa w ciągu 30 dni."
},
"domain": {
"label": "Domena",
"description": "Publiczna nazwa domeny certyfikatu.",
"placeholder": "termix.example.com"
},
"email": {
"label": "E-mail",
"description": "Adres e-mail kontaktowy do konta ACME i powiadomień o wygaśnięciu.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Urząd certyfikacji (CA)",
"description": "Gdzie uzyskać certyfikat? Użyj środowiska testowego do testowania bez przekraczania limitów przepustowości Let's Encrypt.",
"letsencrypt": "Szyfrujmy",
"letsencryptStaging": "Let's Encrypt (etap przygotowawczy)",
"custom": "Niestandardowy katalog ACME"
},
"directoryUrl": {
"label": "Adres URL katalogu ACME",
"description": "Tylko dla niestandardowego urzędu certyfikacji. Adres URL musi być w formacie https."
},
"challengeType": {
"label": "Typ wyzwania",
"description": "Jak udowodnić, że jesteś właścicielem domeny.",
"http": "HTTP - wymaga dostępu do portu 80 z Internetu",
"cloudflare": "DNS (Cloudflare) – wymaga tokenu API Cloudflare"
},
"cloudflareToken": {
"label": "Token API Cloudflare",
"description": "Token zakresowy z uprawnieniem Zone:DNS:Edit dla Twojej domeny."
}
},
"status": {
"title": "Status certyfikatu",
"none": "Brak certyfikatu",
"names": "Obejmuje {{names}}",
"issuer": "Wydane przez {{issuer}}",
"expires": "Wygasa {{date}}",
"lastIssued": "Ostatnio wystawiono {{date}}",
"lastError": "Ostatnia próba nieudana: {{error}}",
"missing": "Ustaw domenę, adres e-mail i pola, których wymaga Twój typ wyzwania, a następnie zapisz, zanim poprosisz o certyfikat.",
"request": "Złóż wniosek o certyfikat już teraz",
"requesting": "Żądanie certyfikatu...",
"issued": "Certyfikat wydany i zainstalowany"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Gerenciar certificados ACME",
"description": "Veja o status do certificado e solicite um novo agora mesmo."
}
},
"settings": {
"autoRenew": {
"label": "Renovar automaticamente",
"description": "Verifique duas vezes ao dia e obtenha um novo certificado quando não houver nenhum, se ele não cobrir o domínio ou se expirar em 30 dias."
},
"domain": {
"label": "Domínio",
"description": "O nome de domínio público do certificado.",
"placeholder": "termix.example.com"
},
"email": {
"label": "E-mail",
"description": "E-mail de contato para a conta ACME e avisos de vencimento.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Autoridade certificadora",
"description": "Onde obter o certificado. Use o ambiente de teste para realizar os testes sem atingir os limites de requisição do Let's Encrypt.",
"letsencrypt": "Vamos criptografar",
"letsencryptStaging": "Let's Encrypt (em fase de testes)",
"custom": "Diretório ACME personalizado"
},
"directoryUrl": {
"label": "URL do diretório ACME",
"description": "Somente para uma autoridade de certificação personalizada. Deve ser um URL https."
},
"challengeType": {
"label": "Tipo de desafio",
"description": "Como comprovar que você é o proprietário do domínio.",
"http": "HTTP - requer que a porta 80 esteja acessível pela internet.",
"cloudflare": "DNS (Cloudflare) - requer um token de API do Cloudflare"
},
"cloudflareToken": {
"label": "Token da API Cloudflare",
"description": "Token com escopo definido e permissão Zone:DNS:Edit para o seu domínio."
}
},
"status": {
"title": "Status do certificado",
"none": "Sem certificado",
"names": "Capas {{names}}",
"issuer": "Emitido por {{issuer}}",
"expires": "Expira em {{date}}",
"lastIssued": "Última edição {{date}}",
"lastError": "Última tentativa falhou: {{error}}",
"missing": "Defina o domínio, o e-mail e os campos necessários para o seu tipo de desafio e salve as alterações antes de solicitar um certificado.",
"request": "Solicite o certificado agora",
"requesting": "Solicitar certificado...",
"issued": "Certificado emitido e instalado."
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Gerir certificados ACME",
"description": "Veja o estado do certificado e solicite um novo agora mesmo."
}
},
"settings": {
"autoRenew": {
"label": "Renovar automaticamente",
"description": "Verifique duas vezes por dia e obtenha um novo certificado quando não houver nenhum, se não cobrir o domínio ou se expirar dentro de 30 dias."
},
"domain": {
"label": "Domínio",
"description": "O nome de domínio público para o certificado.",
"placeholder": "termix.example.com"
},
"email": {
"label": "E-mail",
"description": "E-mail de contacto para a conta ACME e avisos de vencimento.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Autoridade de certificação",
"description": "Onde obter o certificado. Utilize o ambiente de teste para realizar os testes sem atingir os limites de requisição do Let's Encrypt.",
"letsencrypt": "Vamos encriptar",
"letsencryptStaging": "Let's Encrypt (em fase de testes)",
"custom": "Diretório ACME personalizado"
},
"directoryUrl": {
"label": "URL do diretório ACME",
"description": "Apenas para uma autoridade de certificação personalizada. Deve ser um URL https."
},
"challengeType": {
"label": "Tipo de desafio",
"description": "Como comprovar que é o proprietário do domínio.",
"http": "HTTP - requer que a porta 80 esteja acessível através da internet.",
"cloudflare": "DNS (Cloudflare) - requer um token API do Cloudflare"
},
"cloudflareToken": {
"label": "Token da API Cloudflare",
"description": "Token com âmbito e permissão Zone:DNS:Edit para o seu domínio."
}
},
"status": {
"title": "Estado do certificado",
"none": "Sem certificado",
"names": "Capas {{names}}",
"issuer": "Emitido por {{issuer}}",
"expires": "Expira a {{date}}",
"lastIssued": "Última emissão a {{date}}",
"lastError": "Última tentativa falhada: {{error}}",
"missing": "Defina o domínio, o e-mail e os campos necessários para o seu tipo de desafio e guarde as alterações antes de solicitar um certificado.",
"request": "Solicite já o certificado",
"requesting": "A solicitar certificado...",
"issued": "Certificado emitido e instalado."
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Gestionați certificatele ACME",
"description": "Vedeți starea certificatului și solicitați un certificat nou acum."
}
},
"settings": {
"autoRenew": {
"label": "Reînnoire automată",
"description": "Verificați de două ori pe zi și obțineți un certificat nou atunci când nu există, nu acoperă domeniul sau expiră în termen de 30 de zile."
},
"domain": {
"label": "Domeniu",
"description": "Numele de domeniu public pentru certificat.",
"placeholder": "termix.example.com"
},
"email": {
"label": "E-mail",
"description": "Adresă de e-mail de contact pentru contul ACME și notificări de expirare.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Autoritatea de certificare",
"description": "Unde se obține certificatul. Folosește staging-ul pentru a testa fără a atinge limitele de rată Let's Encrypt.",
"letsencrypt": "Să criptăm",
"letsencryptStaging": "Să criptăm (punct în scenă)",
"custom": "Director ACME personalizat"
},
"directoryUrl": {
"label": "URL-ul directorului ACME",
"description": "Numai pentru o autoritate de certificare personalizată. Trebuie să fie o adresă URL https."
},
"challengeType": {
"label": "Tipul de provocare",
"description": "Cum să dovedești că deții domeniul.",
"http": "HTTP - necesită ca portul 80 să fie accesibil de pe internet",
"cloudflare": "DNS (Cloudflare) - necesită un token API Cloudflare"
},
"cloudflareToken": {
"label": "Tokenul API Cloudflare",
"description": "Token cu scop specific și permisiune Zone:DNS:Edit pentru domeniul dvs."
}
},
"status": {
"title": "Starea certificatului",
"none": "Fără certificat",
"names": "Acoperă {{names}}",
"issuer": "Emis de {{issuer}}",
"expires": "Expiră la {{date}}",
"lastIssued": "Ultima emisiune {{date}}",
"lastError": "Ultima încercare a eșuat: {{error}}",
"missing": "Setați domeniul, adresa de e-mail și câmpurile necesare tipului dvs. de provocare, apoi salvați, înainte de a solicita un certificat.",
"request": "Solicitați certificatul acum",
"requesting": "Se solicită certificatul...",
"issued": "Certificat emis și instalat"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Управление сертификатами ACME",
"description": "Проверьте статус сертификата и запросите новый сертификат прямо сейчас."
}
},
"settings": {
"autoRenew": {
"label": "Автоматическое продление",
"description": "Проверяйте дважды в день и получайте новый сертификат, если его нет, если он не покрывает домен или если срок действия старого истекает в течение 30 дней."
},
"domain": {
"label": "Домен",
"description": "Общедоступное доменное имя для сертификата.",
"placeholder": "termix.example.com"
},
"email": {
"label": "Email",
"description": "Адрес электронной почты, указанный в учетной записи ACME, для получения уведомлений об истечении срока действия.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Центр сертификации",
"description": "Где получить сертификат. Используйте тестовую среду (staging) для проверки без превышения лимитов запросов Let's Encrypt.",
"letsencrypt": "Let's Encrypt",
"letsencryptStaging": "Let's Encrypt (тестовая версия)",
"custom": "Пользовательский каталог ACME"
},
"directoryUrl": {
"label": "URL-адрес каталога ACME",
"description": "Только для пользовательского центра сертификации. URL-адрес должен быть HTTPS."
},
"challengeType": {
"label": "Тип задачи",
"description": "Как доказать, что вы являетесь владельцем домена.",
"http": "HTTP — требуется доступ к порту 80 из интернета.",
"cloudflare": "DNS (Cloudflare) — требуется токен API Cloudflare."
},
"cloudflareToken": {
"label": "API-токен Cloudflare",
"description": "Токен с ограниченной областью действия и разрешением Zone:DNS:Edit для вашего домена."
}
},
"status": {
"title": "Статус сертификата",
"none": "Сертификат отсутствует",
"names": "Обложки {{names}}",
"issuer": "Выпущено {{issuer}}",
"expires": "Истекает {{date}}",
"lastIssued": "Последний выпуск {{date}}",
"lastError": "Последняя попытка не удалась: {{error}}",
"missing": "Укажите домен, адрес электронной почты и поля, необходимые для типа проверки подлинности, затем сохраните изменения, прежде чем запрашивать сертификат.",
"request": "Запросить сертификат сейчас",
"requesting": "Запрос сертификата...",
"issued": "Сертификат выдан и установлен."
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Управљање ACME сертификатима",
"description": "Погледајте статус сертификата и одмах затражите нови сертификат."
}
},
"settings": {
"autoRenew": {
"label": "Аутоматски обнови",
"description": "Проверавајте два пута дневно и набавите нови сертификат када га нема, када не покрива домен или када истиче у року од 30 дана."
},
"domain": {
"label": "Домен",
"description": "Јавно име домена за сертификат.",
"placeholder": "termix.example.com"
},
"email": {
"label": "Е-пошта",
"description": "Контакт имејл за ACME налог и обавештења о истеку.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Сертификатски ауторитет",
"description": "Где добити сертификат. Користите припрему за тестирање без достизања ограничења брзине Let's Encrypt-а.",
"letsencrypt": "Хајде да шифрујемо",
"letsencryptStaging": "Хајде да шифрујемо (припрема)",
"custom": "Прилагођени ACME директоријум"
},
"directoryUrl": {
"label": "URL ACME директоријума",
"description": "Само за прилагођени ауторитет за сертификате. Мора бити https URL."
},
"challengeType": {
"label": "Тип изазова",
"description": "Како доказати да сте власник домена.",
"http": "HTTP - потребан је порт 80 доступан са интернета",
"cloudflare": "DNS (Cloudflare) - потребан је Cloudflare API токен"
},
"cloudflareToken": {
"label": "Клаудфлејр АПИ токен",
"description": "Ограничени токен са дозволом Zone:DNS:Edit за ваш домен."
}
},
"status": {
"title": "Статус сертификата",
"none": "Без сертификата",
"names": "Навлаке {{names}}",
"issuer": "Издао/ла {{issuer}}",
"expires": "Истиче {{date}}",
"lastIssued": "Последње издање {{date}}",
"lastError": "Последњи покушај није успео: {{error}}",
"missing": "Подесите домен, имејл адресу и поља која су потребна вашем типу изазова, а затим сачувајте подешавања пре него што затражите сертификат.",
"request": "Затражите сертификат сада",
"requesting": "Захтева се сертификат...",
"issued": "Сертификат издат и инсталиран"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Hantera ACME-certifikat",
"description": "Se certifikatets status och begär ett nytt certifikat nu."
}
},
"settings": {
"autoRenew": {
"label": "Förnya automatiskt",
"description": "Kontrollera två gånger om dagen och skaffa ett nytt certifikat om det inte finns något, om det inte täcker domänen eller om det löper ut inom 30 dagar."
},
"domain": {
"label": "Domän",
"description": "Det offentliga domännamnet för certifikatet.",
"placeholder": "termix.example.com"
},
"email": {
"label": "E-post",
"description": "Kontakta e-postadressen för ACME-kontot och meddelanden om utgångsdatum.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Certifikatutfärdare",
"description": "Var man kan hämta certifikatet. Använd staging för att testa utan att nå hastighetsgränserna för Let's Encrypt.",
"letsencrypt": "Låt oss kryptera",
"letsencryptStaging": "Låt oss kryptera (staging)",
"custom": "Anpassad ACME-katalog"
},
"directoryUrl": {
"label": "ACME-katalogens URL",
"description": "Endast för en anpassad certifikatutfärdare. Måste vara en https-URL."
},
"challengeType": {
"label": "Utmaningstyp",
"description": "Hur man bevisar att man äger domänen.",
"http": "HTTP - behöver port 80 som kan nås från internet",
"cloudflare": "DNS (Cloudflare) - behöver en Cloudflare API-token"
},
"cloudflareToken": {
"label": "Cloudflare API-token",
"description": "Scoped-token med Zone:DNS:Edit-behörighet för din domän."
}
},
"status": {
"title": "Certifikatstatus",
"none": "Inget certifikat",
"names": "Omslag {{names}}",
"issuer": "Utfärdad av {{issuer}}",
"expires": "Utgångsdatum {{date}}",
"lastIssued": "Senast utfärdad {{date}}",
"lastError": "Senaste försöket misslyckades: {{error}}",
"missing": "Ange domän, e-postadress och de fält som din utmaningstyp behöver, spara sedan innan du begär ett certifikat.",
"request": "Begär certifikat nu",
"requesting": "Begär certifikat...",
"issued": "Certifikat utfärdat och installerat"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "จัดการใบรับรอง ACME",
"description": "ตรวจสอบสถานะใบรับรองและขอใบรับรองใหม่ได้เลยตอนนี้"
}
},
"settings": {
"autoRenew": {
"label": "ต่ออายุอัตโนมัติ",
"description": "ตรวจสอบวันละสองครั้ง และขอใบรับรองใหม่เมื่อไม่มีใบรับรอง หากใบรับรองไม่ครอบคลุมโดเมน หรือหากใบรับรองหมดอายุภายใน 30 วัน"
},
"domain": {
"label": "โดเมน",
"description": "ชื่อโดเมนสาธารณะสำหรับใบรับรอง",
"placeholder": "เทอร์มิกซ์.ตัวอย่าง.com"
},
"email": {
"label": "อีเมล",
"description": "อีเมลสำหรับติดต่อเพื่อรับข้อมูลเกี่ยวกับบัญชี ACME และประกาศแจ้งเตือนวันหมดอายุ",
"placeholder": "admin@example.com"
},
"provider": {
"label": "หน่วยงานออกใบรับรอง",
"description": "จะขอรับใบรับรองได้จากที่ไหน ใช้ระบบทดสอบ (staging) เพื่อทดสอบโดยไม่ให้เกินขีดจำกัดการใช้งานของ Let's Encrypt",
"letsencrypt": "เลทส์ เอนคริปท์",
"letsencryptStaging": "Let's Encrypt (รุ่นทดสอบ)",
"custom": "ไดเร็กทอรี ACME แบบกำหนดเอง"
},
"directoryUrl": {
"label": "URL ของไดเร็กทอรี ACME",
"description": "ใช้ได้เฉพาะกับหน่วยงานออกใบรับรองที่กำหนดเองเท่านั้น ต้องเป็น URL ที่ใช้ HTTPS"
},
"challengeType": {
"label": "ประเภทความท้าทาย",
"description": "วิธีพิสูจน์ว่าคุณเป็นเจ้าของโดเมน",
"http": "HTTP - ต้องมีพอร์ต 80 ที่สามารถเข้าถึงได้จากอินเทอร์เน็ต",
"cloudflare": "DNS (Cloudflare) - ต้องใช้โทเค็น API ของ Cloudflare"
},
"cloudflareToken": {
"label": "โทเค็น API ของ Cloudflare",
"description": "โทเค็นแบบจำกัดขอบเขตพร้อมสิทธิ์ Zone:DNS:Edit สำหรับโดเมนของคุณ"
}
},
"status": {
"title": "สถานะใบรับรอง",
"none": "ไม่มีใบรับรอง",
"names": "ปก {{names}}",
"issuer": "ออกโดย {{issuer}}",
"expires": "หมดอายุ {{date}}",
"lastIssued": "ออกครั้งล่าสุด {{date}}",
"lastError": "ความพยายามครั้งล่าสุดล้มเหลว: {{error}}",
"missing": "ตั้งค่าโดเมน อีเมล และข้อมูลที่จำเป็นสำหรับประเภทการตรวจสอบสิทธิ์ของคุณ จากนั้นบันทึกก่อนที่จะขอใบรับรอง",
"request": "ขอใบรับรองตอนนี้",
"requesting": "ขอใบรับรอง...",
"issued": "ออกและติดตั้งใบรับรองแล้ว"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "ACME sertifikalarını yönetin",
"description": "Sertifika durumunu kontrol edin ve şimdi yeni bir sertifika talep edin."
}
},
"settings": {
"autoRenew": {
"label": "Otomatik olarak yenile",
"description": "Sertifikayı günde iki kez kontrol edin ve yoksa, alan adını kapsamıyorsa veya 30 gün içinde süresi dolacaksa yenisini alın."
},
"domain": {
"label": "Alan Adı",
"description": "Sertifika için genel alan adı.",
"placeholder": "termix.example.com"
},
"email": {
"label": "E-posta",
"description": "ACME hesabı ve sona erme bildirimleri için iletişim e-posta adresi.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Sertifika yetkilisi",
"description": "Sertifikayı nereden alabilirsiniz? Let's Encrypt'in hız sınırlamalarına takılmadan test etmek için hazırlık ortamını kullanın.",
"letsencrypt": "Let's Encrypt",
"letsencryptStaging": "Let's Encrypt (test aşaması)",
"custom": "Özel ACME dizini"
},
"directoryUrl": {
"label": "ACME dizin URL'si",
"description": "Yalnızca özel sertifika yetkilisi için geçerlidir. HTTPS URL'si olmalıdır."
},
"challengeType": {
"label": "Meydan okuma türü",
"description": "Alan adının size ait olduğunu nasıl kanıtlayabilirsiniz?",
"http": "HTTP - internetten erişilebilir 80 numaralı porta ihtiyaç duyar.",
"cloudflare": "DNS (Cloudflare) - Cloudflare API belirteci gerektirir."
},
"cloudflareToken": {
"label": "Cloudflare API belirteci",
"description": "Alan adınız için Zone:DNS:Edit iznine sahip kapsamlı belirteç."
}
},
"status": {
"title": "Sertifika durumu",
"none": "Sertifika yok",
"names": "Kapaklar {{names}}",
"issuer": "{{issuer}} tarafından yayınlandı",
"expires": "Son kullanma: {{date}}",
"lastIssued": "Son veriliş: {{date}}",
"lastError": "Son deneme başarısız oldu: {{error}}",
"missing": "Sertifika talebinde bulunmadan önce etki alanını, e-postayı ve doğrulama türünüzün gerektirdiği alanları ayarlayın, ardından kaydedin.",
"request": "Şimdi sertifika talep edin",
"requesting": "Sertifika isteniyor...",
"issued": "Sertifika düzenlendi ve kuruldu."
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Керування сертифікатами ACME",
"description": "Перегляньте статус сертифіката та запросіть новий сертифікат зараз."
}
},
"settings": {
"autoRenew": {
"label": "Автоматичне поновлення",
"description": "Перевіряйте двічі на день та отримуйте новий сертифікат, коли його немає, він не поширюється на домен або термін його дії закінчується протягом 30 днів."
},
"domain": {
"label": "Домен",
"description": "Ім'я публічного домену для сертифіката.",
"placeholder": "termix.example.com"
},
"email": {
"label": "Електронна пошта",
"description": "Контактна електронна адреса для облікового запису ACME та повідомлень про закінчення терміну дії.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Центр сертифікації",
"description": "Де отримати сертифікат. Використовуйте проміжну версію для тестування без перевищення обмежень швидкості Let's Encrypt.",
"letsencrypt": "Давайте зашифруємо",
"letsencryptStaging": "Давайте зашифруємо (підготовка)",
"custom": "Користувацький каталог ACME"
},
"directoryUrl": {
"label": "URL-адреса каталогу ACME",
"description": "Тільки для власного центру сертифікації. Має бути URL-адреса https."
},
"challengeType": {
"label": "Тип виклику",
"description": "Як довести, що ви є власником домену.",
"http": "HTTP – потрібен порт 80, доступний з Інтернету",
"cloudflare": "DNS (Cloudflare) – потрібен токен API Cloudflare"
},
"cloudflareToken": {
"label": "Токен API Cloudflare",
"description": "Токен з обмеженою областю дії та дозволом Zone:DNS:Edit для вашого домену."
}
},
"status": {
"title": "Статус сертифіката",
"none": "Без сертифіката",
"names": "Обкладинки {{names}}",
"issuer": "Видано {{issuer}}",
"expires": "Термін дії закінчується {{date}}",
"lastIssued": "Останній виданий {{date}}",
"lastError": "Остання спроба не вдалася: {{error}}",
"missing": "Вкажіть домен, електронну пошту та поля, необхідні для вашого типу завдання, а потім збережіть зміни, перш ніж запитувати сертифікат.",
"request": "Замовити сертифікат зараз",
"requesting": "Запит сертифіката...",
"issued": "Сертифікат видано та встановлено"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "Quản lý chứng chỉ ACME",
"description": "Xem trạng thái chứng chỉ và yêu cầu chứng chỉ mới ngay bây giờ."
}
},
"settings": {
"autoRenew": {
"label": "Tự động gia hạn",
"description": "Hãy kiểm tra hai lần một ngày và lấy chứng chỉ mới nếu chứng chỉ hiện không có, không bao phủ tên miền hoặc hết hạn trong vòng 30 ngày."
},
"domain": {
"label": "Tên miền",
"description": "Tên miền công khai để cấp chứng chỉ.",
"placeholder": "termix.example.com"
},
"email": {
"label": "Email",
"description": "Địa chỉ email liên hệ cho tài khoản ACME và thông báo hết hạn.",
"placeholder": "admin@example.com"
},
"provider": {
"label": "Cơ quan chứng thực",
"description": "Lấy chứng chỉ ở đâu? Sử dụng môi trường thử nghiệm để kiểm tra mà không vượt quá giới hạn số lượng yêu cầu của Let's Encrypt.",
"letsencrypt": "Hãy mã hóa",
"letsencryptStaging": "Let's Encrypt (phiên bản thử nghiệm)",
"custom": "Danh mục ACME tùy chỉnh"
},
"directoryUrl": {
"label": "URL thư mục ACME",
"description": "Chỉ dành cho cơ quan cấp chứng chỉ tùy chỉnh. Phải là URL HTTPS."
},
"challengeType": {
"label": "Loại thử thách",
"description": "Cách chứng minh bạn sở hữu tên miền.",
"http": "HTTP - cần cổng 80 có thể truy cập được từ internet",
"cloudflare": "DNS (Cloudflare) - cần mã thông báo API của Cloudflare."
},
"cloudflareToken": {
"label": "Mã thông báo API Cloudflare",
"description": "Token có phạm vi với quyền Zone:DNS:Edit cho tên miền của bạn."
}
},
"status": {
"title": "Trạng thái chứng chỉ",
"none": "Không có chứng chỉ",
"names": "Ảnh bìa {{names}}",
"issuer": "Được phát hành bởi {{issuer}}",
"expires": "Hết hạn {{date}}",
"lastIssued": "Cấp lần cuối {{date}}",
"lastError": "Lần thử cuối cùng thất bại: {{error}}",
"missing": "Hãy thiết lập tên miền, email và các trường thông tin mà loại thử thách của bạn yêu cầu, sau đó lưu lại trước khi yêu cầu chứng chỉ.",
"request": "Yêu cầu chứng chỉ ngay bây giờ",
"requesting": "Đang yêu cầu chứng chỉ...",
"issued": "Giấy chứng nhận đã được cấp và cài đặt"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "管理 ACME 证书",
"description": "查看证书状态并立即申请新证书。"
}
},
"settings": {
"autoRenew": {
"label": "自动续订",
"description": "每天检查两次,如果没有证书、证书不涵盖该域名或证书将在 30 天内过期,则获取新证书。"
},
"domain": {
"label": "域名",
"description": "证书的公共域名。",
"placeholder": "termix.example.com"
},
"email": {
"label": "邮箱",
"description": "ACME账户及到期通知的联系邮箱。",
"placeholder": "admin@example.com"
},
"provider": {
"label": "证书颁发机构",
"description": "证书获取途径。使用测试环境进行测试,避免超出 Let's Encrypt 的速率限制。",
"letsencrypt": "Let's Encrypt",
"letsencryptStaging": "Let's Encrypt(测试版)",
"custom": "自定义 ACME 目录"
},
"directoryUrl": {
"label": "ACME目录URL",
"description": "仅适用于自定义证书颁发机构。必须是 https URL。"
},
"challengeType": {
"label": "挑战类型",
"description": "如何证明您拥有该域名。",
"http": "HTTP协议需要可通过互联网访问的80端口。",
"cloudflare": "DNS(Cloudflare)——需要 Cloudflare API 令牌"
},
"cloudflareToken": {
"label": "Cloudflare API 令牌",
"description": "具有您域名的 Zone:DNS:Edit 权限的限定作用域令牌。"
}
},
"status": {
"title": "证书状态",
"none": "无证书",
"names": "封面 {{names}}",
"issuer": "由 {{issuer}} 发行",
"expires": "过期于 {{date}}",
"lastIssued": "上次签发于 {{date}}",
"lastError": "最后一次尝试失败: {{error}}",
"missing": "设置域名、电子邮件和您的质询类型所需的字段,然后保存,再申请证书。",
"request": "立即申请证书",
"requesting": "正在请求证书...",
"issued": "证书已颁发并安装"
},
"plugin": {
"name": "ACME Certificates"
}
}
@@ -1,61 +0,0 @@
{
"permissions": {
"manage": {
"title": "管理 ACME 證書",
"description": "查看證書狀態並立即申請新證書。"
}
},
"settings": {
"autoRenew": {
"label": "自動續訂",
"description": "每天檢查兩次,如果沒有證書、證書不涵蓋該網域或證書將在 30 天內過期,則取得新證書。"
},
"domain": {
"label": "網域",
"description": "憑證使用的公開網域名稱。",
"placeholder": "termix.example.com"
},
"email": {
"label": "電子郵件",
"description": "ACME帳戶及到期通知的聯絡信箱。",
"placeholder": "admin@example.com"
},
"provider": {
"label": "憑證授權中心",
"description": "證書取得途徑。使用測試環境進行測試,避免超出 Let's Encrypt 的速率限制。",
"letsencrypt": "Let's Encrypt",
"letsencryptStaging": "Let's Encrypt(測試版)",
"custom": "自訂 ACME 目錄"
},
"directoryUrl": {
"label": "ACME目錄URL",
"description": "僅適用於自訂憑證授權單位。必須是 https URL。"
},
"challengeType": {
"label": "挑戰類型",
"description": "如何證明您擁有該網域。",
"http": "HTTP協定需要80個可透過網際網路存取的連接埠。",
"cloudflare": "DNS(Cloudflare)-需要 Cloudflare API 令牌"
},
"cloudflareToken": {
"label": "Cloudflare API 令牌",
"description": "具有您網域 Zone:DNS:Edit 權限的限定範圍權杖。"
}
},
"status": {
"title": "證書狀態",
"none": "無憑證",
"names": "封面 {{names}}",
"issuer": "由 {{issuer}} 發行",
"expires": "到期日 {{date}}",
"lastIssued": "上次簽發日期 {{date}}",
"lastError": "最後嘗試失敗: {{error}}",
"missing": "設定網域名稱、電子郵件和您的質詢類型所需的字段,然後儲存,再申請憑證。",
"request": "立即申請證書",
"requesting": "請求憑證中...",
"issued": "證書已頒發並安裝"
},
"plugin": {
"name": "ACME Certificates"
}
}
-122
View File
@@ -1,122 +0,0 @@
{
"id": "acme-ssl",
"name": "ACME Certificates",
"version": "1.0.0",
"description": "Gets the Termix HTTPS certificate from Let's Encrypt or any ACME provider and renews it for you.",
"author": {
"name": "Termix"
},
"license": "Apache-2.0",
"repository": "https://github.com/Termix-SSH/Termix",
"category": "Access & Security",
"icon": "Lock",
"engine": {
"termix": ">=26.10.0",
"api": "1"
},
"capabilities": [
"system:tls",
"kv:own",
"secrets:own",
"network:outbound",
"network:serve",
"notify:send",
"ui:surface"
],
"contributes": {
"permissions": [
{
"name": "manage",
"titleKey": "permissions.manage.title",
"descriptionKey": "permissions.manage.description",
"defaultRoles": ["admin"]
}
],
"settings": {
"admin": [
{
"key": "status",
"type": "custom",
"component": "status"
},
{
"key": "autoRenew",
"type": "boolean",
"labelKey": "settings.autoRenew.label",
"descriptionKey": "settings.autoRenew.description",
"default": false
},
{
"key": "domain",
"type": "string",
"labelKey": "settings.domain.label",
"descriptionKey": "settings.domain.description",
"placeholderKey": "settings.domain.placeholder",
"default": ""
},
{
"key": "email",
"type": "string",
"labelKey": "settings.email.label",
"descriptionKey": "settings.email.description",
"placeholderKey": "settings.email.placeholder",
"default": ""
},
{
"key": "provider",
"type": "select",
"labelKey": "settings.provider.label",
"descriptionKey": "settings.provider.description",
"default": "letsencrypt",
"options": [
{
"value": "letsencrypt",
"labelKey": "settings.provider.letsencrypt"
},
{
"value": "letsencrypt-staging",
"labelKey": "settings.provider.letsencryptStaging"
},
{
"value": "custom",
"labelKey": "settings.provider.custom"
}
]
},
{
"key": "directoryUrl",
"type": "string",
"labelKey": "settings.directoryUrl.label",
"descriptionKey": "settings.directoryUrl.description",
"default": ""
},
{
"key": "challengeType",
"type": "select",
"labelKey": "settings.challengeType.label",
"descriptionKey": "settings.challengeType.description",
"default": "http-01",
"options": [
{
"value": "http-01",
"labelKey": "settings.challengeType.http"
},
{
"value": "dns-cloudflare",
"labelKey": "settings.challengeType.cloudflare"
}
]
},
{
"key": "cloudflareToken",
"type": "secret",
"labelKey": "settings.cloudflareToken.label",
"descriptionKey": "settings.cloudflareToken.description"
}
]
}
},
"locales": "locales",
"platforms": ["linux", "win32", "darwin"],
"desktop": "server"
}
-44
View File
@@ -1,44 +0,0 @@
{
"name": "@termix-plugin/acme-ssl",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "termix-plugin build",
"test": "termix-plugin test",
"typecheck": "tsc -p .",
"validate": "termix-plugin validate",
"format": "prettier --write .",
"format:check": "prettier --check .",
"prepare": "husky || true"
},
"dependencies": {
"acme-client": "^5.4.0"
},
"devDependencies": {
"@commitlint/cli": "^21.2.2",
"@commitlint/config-conventional": "^21.2.0",
"@termix/plugin-sdk": "^1.0.0",
"@testing-library/dom": "^10.4.2",
"@testing-library/react": "^16.3.3",
"@types/node": "^26.2.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"husky": "^9.1.7",
"jsdom": "^30.0.1",
"lint-staged": "^17.2.0",
"prettier": "3.9.9",
"vitest": "^4.1.11"
},
"peerDependencies": {
"@termix/plugin-sdk": "^1.0.0",
"express": "^5.2.1",
"react": "^19.2.8",
"sonner": "^2.0.8"
},
"lint-staged": {
"*.{ts,tsx,js,jsx,mjs,cjs,json,css,md}": [
"prettier --write"
]
}
}
-217
View File
@@ -1,217 +0,0 @@
/**
* Issuing a certificate with acme-client. Every request it makes goes through
* ctx.fetch, so ACME traffic gets core's outbound guard and audit like any
* other plugin request.
*/
import * as acme from "acme-client";
import type { PluginContext, PluginFetch } from "@termix/plugin-sdk/backend";
import { createTxtRecord } from "./cloudflare.js";
import { directoryUrlFor, type AcmeSettings } from "./settings.js";
const ACCOUNT_KEY = "account";
const DNS_PROPAGATION_MS = 30_000;
interface StoredAccount {
directoryUrl: string;
sealedKey: string;
url: string | null;
}
export interface IssuedCertificate {
certificate: string;
privateKey: string;
}
type AxiosLikeConfig = {
url?: string;
method?: string;
headers?: unknown;
data?: unknown;
timeout?: number;
validateStatus?: ((status: number) => boolean) | null;
};
function plainHeaders(headers: unknown): Record<string, string> {
const source =
headers && typeof (headers as { toJSON?: unknown }).toJSON === "function"
? (headers as { toJSON: () => Record<string, unknown> }).toJSON()
: ((headers ?? {}) as Record<string, unknown>);
const out: Record<string, string> = {};
for (const [key, value] of Object.entries(source)) {
if (typeof value === "string" || typeof value === "number") {
out[key] = String(value);
}
}
return out;
}
/**
* An axios adapter over ctx.fetch. It settles the way axios' own adapters do,
* because acme-client's retry interceptor relies on a rejected error that
* carries the response.
*/
export function createFetchAdapter(
fetch: PluginFetch,
allowPrivateHosts: readonly string[] = [],
) {
return async (config: AxiosLikeConfig) => {
const method = (config.method ?? "get").toUpperCase();
const response = await fetch(String(config.url), {
method,
headers: plainHeaders(config.headers),
body:
config.data == null
? undefined
: typeof config.data === "string"
? config.data
: JSON.stringify(config.data),
timeoutMs: config.timeout || 30_000,
allowPrivateHosts,
});
const headers: Record<string, string> = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
const result = {
data: method === "HEAD" ? "" : await response.text(),
status: response.status,
statusText: response.statusText,
headers,
config,
request: {},
};
const validate = config.validateStatus;
if (!result.status || !validate || validate(result.status)) return result;
throw Object.assign(
new Error(`Request failed with status code ${result.status}`),
{
isAxiosError: true,
code: result.status >= 500 ? "ERR_BAD_RESPONSE" : "ERR_BAD_REQUEST",
config,
response: result,
},
);
};
}
/** Private hosts a custom directory on the local network may use. */
function privateHostsFor(settings: AcmeSettings): string[] {
if (settings.provider !== "custom") return [];
try {
return [new URL(settings.directoryUrl).hostname];
} catch {
return [];
}
}
async function loadAccount(
ctx: PluginContext,
directoryUrl: string,
): Promise<{ key: string; url: string | null }> {
const stored = (await ctx.kv.get(ACCOUNT_KEY)) as StoredAccount | null;
if (stored && stored.directoryUrl === directoryUrl) {
const key = await ctx.secrets.unseal(stored.sealedKey);
if (key) return { key, url: stored.url };
}
const key = (await acme.crypto.createPrivateKey()).toString();
await ctx.kv.set(ACCOUNT_KEY, {
directoryUrl,
sealedKey: await ctx.secrets.seal(key),
url: null,
} satisfies StoredAccount);
return { key, url: null };
}
async function saveAccountUrl(
ctx: PluginContext,
directoryUrl: string,
url: string,
): Promise<void> {
const stored = (await ctx.kv.get(ACCOUNT_KEY)) as StoredAccount | null;
if (!stored || stored.directoryUrl !== directoryUrl || stored.url === url) {
return;
}
await ctx.kv.set(ACCOUNT_KEY, { ...stored, url });
}
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
/** Runs one ACME order for `settings.domain` and returns the new pair. */
export async function issueCertificate(
ctx: PluginContext,
settings: AcmeSettings,
options: { propagationMs?: number } = {},
): Promise<IssuedCertificate> {
const directoryUrl = directoryUrlFor(settings);
acme.axios.defaults.adapter = createFetchAdapter(
ctx.fetch,
privateHostsFor(settings),
) as never;
const account = await loadAccount(ctx, directoryUrl);
const client = new acme.Client({
directoryUrl,
accountKey: account.key,
...(account.url ? { accountUrl: account.url } : {}),
});
const [key, csr] = await acme.crypto.createCsr({
commonName: settings.domain,
altNames: [settings.domain],
});
const cleanups = new Map<string, () => void | Promise<void>>();
const certificate = await client.auto({
csr,
email: settings.email,
termsOfServiceAgreed: true,
// Checking our own answer first needs port 80 or DNS from inside the
// container, which often is not how the CA sees it.
skipChallengeVerification: true,
challengePriority:
settings.challengeType === "dns-cloudflare" ? ["dns-01"] : ["http-01"],
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
if (challenge.type === "http-01") {
cleanups.set(
challenge.token,
await ctx.system.publishHttpChallenge(
challenge.token,
keyAuthorization,
),
);
return;
}
if (challenge.type === "dns-01") {
cleanups.set(
challenge.token,
await createTxtRecord(
ctx.fetch,
settings.cloudflareToken,
`_acme-challenge.${authz.identifier.value}`,
keyAuthorization,
),
);
await sleep(options.propagationMs ?? DNS_PROPAGATION_MS);
return;
}
throw new Error(
`Unsupported challenge type ${(challenge as { type: string }).type}`,
);
},
challengeRemoveFn: async (_authz, challenge) => {
const cleanup = cleanups.get(challenge.token);
cleanups.delete(challenge.token);
try {
await cleanup?.();
} catch (error) {
ctx.log.warn(
`Could not clean up the ${challenge.type} challenge: ${error instanceof Error ? error.message : String(error)}`,
);
}
},
});
await saveAccountUrl(ctx, directoryUrl, client.getAccountUrl());
return { certificate, privateKey: key.toString() };
}
@@ -1,78 +0,0 @@
import type { PluginFetch } from "@termix/plugin-sdk/backend";
const API = "https://api.cloudflare.com/client/v4";
interface CloudflareEnvelope<T> {
success: boolean;
errors?: Array<{ message?: string }>;
result: T;
}
async function call<T>(
fetch: PluginFetch,
token: string,
path: string,
init: { method?: string; body?: unknown } = {},
): Promise<T> {
const response = await fetch(`${API}${path}`, {
method: init.method ?? "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: init.body === undefined ? undefined : JSON.stringify(init.body),
});
const payload = (await response
.json()
.catch(() => null)) as CloudflareEnvelope<T> | null;
if (!response.ok || !payload?.success) {
const reason =
payload?.errors
?.map((error) => error.message)
.filter(Boolean)
.join("; ") || `HTTP ${response.status}`;
throw new Error(`Cloudflare API error: ${reason}`);
}
return payload.result;
}
/** The zone holding `name`, found by trying each parent domain. */
export async function findZoneId(
fetch: PluginFetch,
token: string,
name: string,
): Promise<string> {
const labels = name.replace(/\.$/, "").split(".");
for (let i = 0; i < labels.length - 1; i++) {
const candidate = labels.slice(i).join(".");
const zones = await call<Array<{ id: string; name: string }>>(
fetch,
token,
`/zones?name=${encodeURIComponent(candidate)}`,
);
const zone = zones.find((entry) => entry.name === candidate);
if (zone) return zone.id;
}
throw new Error(`No Cloudflare zone found for ${name}`);
}
/** Creates the TXT record and returns a function that deletes it. */
export async function createTxtRecord(
fetch: PluginFetch,
token: string,
name: string,
content: string,
): Promise<() => Promise<void>> {
const zoneId = await findZoneId(fetch, token, name);
const record = await call<{ id: string }>(
fetch,
token,
`/zones/${zoneId}/dns_records`,
{ method: "POST", body: { type: "TXT", name, content, ttl: 120 } },
);
return async () => {
await call(fetch, token, `/zones/${zoneId}/dns_records/${record.id}`, {
method: "DELETE",
});
};
}
-50
View File
@@ -1,50 +0,0 @@
import type { Router } from "express";
import type { PluginContext } from "@termix/plugin-sdk/backend";
import { issueCertificate } from "./acme.js";
import {
CHECK_INTERVAL_MS,
createAcmeRunner,
watchSettings,
type IssueFn,
} from "./runner.js";
import { registerRoutes } from "./routes.js";
/** `issue` is swapped out by tests; the plugin always uses acme-client. */
export async function activateWith(ctx: PluginContext, issue: IssueFn) {
const runner = createAcmeRunner(ctx, issue);
await runner.syncRenewer();
watchSettings(ctx, () => {
runner
.syncRenewer()
.catch((error) =>
ctx.log.warn(
`Could not update the renewal registration: ${error instanceof Error ? error.message : String(error)}`,
),
);
});
// The first check lands within a minute of boot, after core's boot-time
// settings migrations, so it also picks up settings moved from 2.8.
ctx.schedule.every(
CHECK_INTERVAL_MS,
async () => {
await runner.syncRenewer();
await runner.check();
},
{ jitterMs: 60_000 },
);
registerRoutes(ctx, ctx.http.router<Router>(), runner);
return runner;
}
export async function activate(ctx: PluginContext) {
await activateWith(ctx, (pluginCtx, settings) =>
issueCertificate(pluginCtx, settings),
);
}
export async function deactivate() {
// Everything above was registered through ctx and is disposed by core.
}
-28
View File
@@ -1,28 +0,0 @@
import type { PluginTlsStatus } from "@termix/plugin-sdk/backend";
export const RENEW_BEFORE_MS = 30 * 86_400_000;
export type RenewalReason = "missing" | "self-signed" | "domain" | "expiring";
/**
* Whether the served certificate should be replaced for `domain`: there is
* none, it is core's self-signed one, it does not cover the domain, or it
* expires within 30 days.
*/
export function renewalReason(
status: PluginTlsStatus,
domain: string,
now: Date = new Date(),
): RenewalReason | null {
const cert = status.certificate;
if (!cert) return "missing";
if (cert.selfSigned) return "self-signed";
const wanted = domain.toLowerCase();
if (!cert.names.some((name) => name.toLowerCase() === wanted)) {
return "domain";
}
if (new Date(cert.notAfter).getTime() - now.getTime() < RENEW_BEFORE_MS) {
return "expiring";
}
return null;
}
-73
View File
@@ -1,73 +0,0 @@
import type { Request, RequestHandler, Response, Router } from "express";
import type { PluginContext } from "@termix/plugin-sdk/backend";
import { AcmeNotConfiguredError, type AcmeRunner } from "./runner.js";
import { missingSetting, readSettings } from "./settings.js";
export function registerRoutes(
ctx: PluginContext,
router: Router,
runner: AcmeRunner,
): void {
router.use(ctx.rbac.require("manage") as unknown as RequestHandler);
/**
* @openapi
* /plugin-api/acme-ssl/status:
* get:
* summary: ACME certificate status
* description: The certificate core serves, whether ACME is configured, and the last issue attempt.
* tags: [ACME Certificates]
* responses:
* 200:
* description: Status.
* 403:
* description: Missing the acme-ssl.manage permission.
*/
router.get("/status", async (_req: Request, res: Response) => {
const settings = await readSettings(ctx);
res.json({
tls: await ctx.system.tlsStatus(),
state: await runner.state(),
autoRenew: settings.autoRenew,
missing: missingSetting(settings),
});
});
/**
* @openapi
* /plugin-api/acme-ssl/request:
* post:
* summary: Request a certificate now
* description: Runs an ACME order for the configured domain, installs the certificate and reloads HTTPS.
* tags: [ACME Certificates]
* responses:
* 200:
* description: Certificate issued and installed.
* 400:
* description: A required setting is missing.
* 403:
* description: Missing the acme-ssl.manage permission.
* 500:
* description: The ACME order failed.
*/
router.post("/request", async (_req: Request, res: Response) => {
try {
const result = await runner.issue();
res.json({
success: true,
reloadMessage: result.reload.message,
tls: result.tls,
state: await runner.state(),
});
} catch (error) {
if (error instanceof AcmeNotConfiguredError) {
return res
.status(400)
.json({ error: error.message, missing: error.field });
}
const message = error instanceof Error ? error.message : String(error);
ctx.log.warn(`Certificate request failed: ${message}`);
res.status(500).json({ error: `Certificate request failed: ${message}` });
}
});
}
-173
View File
@@ -1,173 +0,0 @@
import type {
PluginContext,
PluginTlsReloadResult,
PluginTlsStatus,
} from "@termix/plugin-sdk/backend";
import type { IssuedCertificate } from "./acme.js";
import { renewalReason } from "./renewal.js";
import {
SETTING_KEYS,
missingSetting,
readSettings,
type AcmeSettings,
} from "./settings.js";
export const CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
/** After a failed attempt, the scheduled check waits this long to retry. */
export const RETRY_AFTER_MS = 6 * 60 * 60 * 1000;
const STATE_KEY = "state";
export interface AcmeState {
lastAttemptAt: string | null;
lastIssuedAt: string | null;
lastError: string | null;
}
export type IssueFn = (
ctx: PluginContext,
settings: AcmeSettings,
) => Promise<IssuedCertificate>;
export class AcmeNotConfiguredError extends Error {
constructor(readonly field: string) {
super(`ACME is not configured: ${field} is missing`);
}
}
export interface IssueResult {
tls: PluginTlsStatus;
reload: PluginTlsReloadResult;
}
export interface AcmeRunner {
state: () => Promise<AcmeState>;
/** Issues now, whatever the served certificate looks like. */
issue: () => Promise<IssueResult>;
/** The scheduled check: issues only when renewal is on and due. */
check: (now?: Date) => Promise<boolean>;
/** Registers or drops the renewer to match the current settings. */
syncRenewer: () => Promise<void>;
}
export function createAcmeRunner(
ctx: PluginContext,
issue: IssueFn,
): AcmeRunner {
let running: Promise<IssueResult> | null = null;
let dropRenewer: (() => void) | null = null;
const state = async (): Promise<AcmeState> => {
const stored = (await ctx.kv.get(STATE_KEY)) as Partial<AcmeState> | null;
return {
lastAttemptAt: stored?.lastAttemptAt ?? null,
lastIssuedAt: stored?.lastIssuedAt ?? null,
lastError: stored?.lastError ?? null,
};
};
const record = async (patch: Partial<AcmeState>) => {
await ctx.kv.set(STATE_KEY, { ...(await state()), ...patch });
};
const runIssue = async (): Promise<IssueResult> => {
const settings = await readSettings(ctx);
const missing = missingSetting(settings);
if (missing) throw new AcmeNotConfiguredError(missing);
const attemptAt = new Date().toISOString();
try {
const issued = await issue(ctx, settings);
await ctx.system.writeTlsCertificate(
issued.certificate,
issued.privateKey,
);
const reload = await ctx.system.reloadTls();
await record({
lastAttemptAt: attemptAt,
lastIssuedAt: new Date().toISOString(),
lastError: null,
});
ctx.log.info(`Certificate issued for ${settings.domain}`);
return { tls: await ctx.system.tlsStatus(), reload };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
await record({ lastAttemptAt: attemptAt, lastError: message });
throw error;
}
};
const runner: AcmeRunner = {
state,
issue: () => {
running ??= runIssue().finally(() => {
running = null;
});
return running;
},
check: async (now = new Date()) => {
const settings = await readSettings(ctx);
if (!settings.autoRenew || missingSetting(settings)) return false;
const last = await state();
if (
last.lastError &&
last.lastAttemptAt &&
now.getTime() - new Date(last.lastAttemptAt).getTime() < RETRY_AFTER_MS
) {
return false;
}
const reason = renewalReason(
await ctx.system.tlsStatus(),
settings.domain,
now,
);
if (!reason) return false;
ctx.log.info(
`Renewing the certificate for ${settings.domain} (${reason})`,
);
try {
await runner.issue();
return true;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
ctx.log.error(
"Certificate renewal failed",
error instanceof Error ? error : new Error(message),
);
await ctx.notify
.send({
title: `Certificate renewal failed for ${settings.domain}`,
body: message,
severity: "critical",
category: "acme-ssl.renewal_failed",
audience: "admins",
dedupeKey: `acme-ssl.renewal_failed:${settings.domain}`,
})
.catch(() => {});
return false;
}
},
syncRenewer: async () => {
const settings = await readSettings(ctx);
const wanted = settings.autoRenew && !missingSetting(settings);
if (wanted && !dropRenewer) {
dropRenewer = await ctx.system.registerTlsRenewer();
} else if (!wanted && dropRenewer) {
dropRenewer();
dropRenewer = null;
}
},
};
return runner;
}
export function watchSettings(ctx: PluginContext, onChange: () => void): void {
for (const key of SETTING_KEYS) ctx.settings.onChange(key, onChange);
}
-77
View File
@@ -1,77 +0,0 @@
import type { PluginContext } from "@termix/plugin-sdk/backend";
export type AcmeProvider = "letsencrypt" | "letsencrypt-staging" | "custom";
export type AcmeChallengeType = "http-01" | "dns-cloudflare";
export interface AcmeSettings {
autoRenew: boolean;
domain: string;
email: string;
provider: AcmeProvider;
directoryUrl: string;
challengeType: AcmeChallengeType;
cloudflareToken: string;
}
export const SETTING_KEYS = [
"autoRenew",
"domain",
"email",
"provider",
"directoryUrl",
"challengeType",
"cloudflareToken",
] as const;
export const DIRECTORIES: Record<Exclude<AcmeProvider, "custom">, string> = {
letsencrypt: "https://acme-v02.api.letsencrypt.org/directory",
"letsencrypt-staging":
"https://acme-staging-v02.api.letsencrypt.org/directory",
};
const text = (value: unknown) =>
typeof value === "string" ? value.trim() : "";
export async function readSettings(ctx: PluginContext): Promise<AcmeSettings> {
const all = await ctx.settings.getAll("admin");
const provider = text(all.provider);
const challengeType = text(all.challengeType);
return {
autoRenew: all.autoRenew === true,
domain: text(all.domain).toLowerCase(),
email: text(all.email),
provider:
provider === "letsencrypt-staging" || provider === "custom"
? provider
: "letsencrypt",
directoryUrl: text(all.directoryUrl),
challengeType:
challengeType === "dns-cloudflare" ? challengeType : "http-01",
cloudflareToken: text(all.cloudflareToken),
};
}
export function directoryUrlFor(settings: AcmeSettings): string {
return settings.provider === "custom"
? settings.directoryUrl
: DIRECTORIES[settings.provider];
}
/** The reason issuing cannot start, or null when everything is set. */
export function missingSetting(settings: AcmeSettings): string | null {
if (!settings.domain) return "domain";
if (!settings.email) return "email";
if (
settings.provider === "custom" &&
!/^https:\/\//i.test(settings.directoryUrl)
) {
return "directoryUrl";
}
if (
settings.challengeType === "dns-cloudflare" &&
!settings.cloudflareToken
) {
return "cloudflareToken";
}
return null;
}
@@ -1,121 +0,0 @@
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import {
usePluginApi,
useTranslation,
type SettingsComponentProps,
} from "@termix/plugin-sdk/frontend";
import { Button } from "@termix/plugin-sdk/ui";
export interface AcmeStatus {
tls: {
enabled: boolean;
certificate: {
issuer: string;
names: string[];
notAfter: string;
selfSigned: boolean;
} | null;
};
state: {
lastAttemptAt: string | null;
lastIssuedAt: string | null;
lastError: string | null;
};
autoRenew: boolean;
missing: string | null;
}
function errorMessage(error: unknown): string {
const data = (error as { response?: { data?: { error?: string } } })?.response
?.data;
if (data?.error) return data.error;
return error instanceof Error ? error.message : String(error);
}
export function AcmeStatusSetting({ running }: SettingsComponentProps) {
const { t } = useTranslation();
const api = usePluginApi();
const [status, setStatus] = useState<AcmeStatus | null>(null);
const [requesting, setRequesting] = useState(false);
const load = useCallback(async () => {
try {
const { data } = await api.get<AcmeStatus>("/status");
setStatus(data);
} catch {
setStatus(null);
}
}, [api]);
useEffect(() => {
if (running) void load();
}, [running, load]);
async function request() {
setRequesting(true);
try {
const { data } = await api.post<{ reloadMessage?: string }>("/request");
toast.success(t("status.issued"));
if (data.reloadMessage) toast.info(data.reloadMessage);
} catch (error) {
toast.error(errorMessage(error));
} finally {
setRequesting(false);
await load();
}
}
const cert = status?.tls.certificate;
return (
<div className="flex flex-col gap-2 border border-border bg-background/50 p-2">
<span className="text-xs font-medium">{t("status.title")}</span>
{cert ? (
<>
<span className="text-[10px] text-muted-foreground">
{t("status.names", { names: cert.names.join(", ") || "-" })}
</span>
<span className="text-[10px] text-muted-foreground">
{t("status.issuer", { issuer: cert.issuer })}
</span>
<span className="text-[10px] text-muted-foreground">
{t("status.expires", {
date: new Date(cert.notAfter).toLocaleDateString(),
})}
</span>
</>
) : (
<span className="text-[10px] text-muted-foreground">
{t("status.none")}
</span>
)}
{status?.state.lastIssuedAt && (
<span className="text-[10px] text-muted-foreground">
{t("status.lastIssued", {
date: new Date(status.state.lastIssuedAt).toLocaleString(),
})}
</span>
)}
{status?.state.lastError && (
<span className="text-[10px] text-destructive">
{t("status.lastError", { error: status.state.lastError })}
</span>
)}
{status?.missing && (
<span className="text-[10px] text-muted-foreground">
{t("status.missing")}
</span>
)}
<Button
type="button"
variant="outline"
className="rounded-none self-start"
disabled={!running || requesting || !!status?.missing}
onClick={request}
>
{requesting ? t("status.requesting") : t("status.request")}
</Button>
</div>
);
}
-10
View File
@@ -1,10 +0,0 @@
import type { TermixApp } from "@termix/plugin-sdk/frontend";
import { AcmeStatusSetting } from "./AcmeStatusSetting";
export function activate(app: TermixApp): void {
app.registerSettingsComponent("status", AcmeStatusSetting);
}
export function deactivate(): void {
// Registrations through app are disposed by core.
}
@@ -1,269 +0,0 @@
import http from "node:http";
import type { AddressInfo } from "node:net";
import express, { type Router } from "express";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createMockCtx,
type MockPluginContext,
} from "@termix/plugin-sdk/testing";
import type {
PluginCapabilityError as CapabilityError,
PluginTlsCertificateInfo,
PluginTlsStatus,
} from "@termix/plugin-sdk/backend";
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
import manifestJson from "../../manifest.json";
import { activateWith } from "../../src/backend/index.js";
import { CHECK_INTERVAL_MS, RETRY_AFTER_MS } from "../../src/backend/runner.js";
import { renewalReason } from "../../src/backend/renewal.js";
const manifest = manifestJson as unknown as PluginManifest;
const DAY = 86_400_000;
const CONFIGURED = {
autoRenew: true,
domain: "termix.example.com",
email: "admin@example.com",
};
function cert(
overrides: Partial<PluginTlsCertificateInfo> = {},
): PluginTlsCertificateInfo {
return {
subject: "CN=termix.example.com",
issuer: "CN=R11, O=Let's Encrypt",
names: ["termix.example.com"],
notBefore: new Date(Date.now() - 10 * DAY).toISOString(),
notAfter: new Date(Date.now() + 60 * DAY).toISOString(),
selfSigned: false,
fingerprint: "AA",
...overrides,
};
}
const issue = vi.fn(async () => ({
certificate: "-----BEGIN CERTIFICATE-----new",
privateKey: "-----BEGIN PRIVATE KEY-----new",
}));
let servers: http.Server[] = [];
afterEach(async () => {
issue.mockClear();
await Promise.all(
servers.map(
(s) => new Promise<void>((resolve) => s.close(() => resolve())),
),
);
servers = [];
});
async function setup(
options: {
settings?: Record<string, unknown>;
certificate?: PluginTlsCertificateInfo | null;
permissions?: string[];
capabilities?: string[];
validateTls?: () => PluginTlsCertificateInfo;
} = {},
) {
let router: Router | null = null;
const tlsStatus: Omit<PluginTlsStatus, "renewal"> = {
enabled: true,
certificate: options.certificate === undefined ? null : options.certificate,
};
const mock: MockPluginContext = createMockCtx({
pluginId: manifest.id,
manifest,
capabilities: options.capabilities ?? manifest.capabilities,
router: () => (router = express.Router()),
settings: options.settings,
permissions: options.permissions ?? ["manage"],
tlsStatus,
validateTls: options.validateTls,
});
const runner = await activateWith(mock.ctx, issue);
const app = express();
app.use((req, res, next) => router!(req, res, next));
const server = http.createServer(app);
servers.push(server);
await new Promise<void>((resolve) => server.listen(0, resolve));
const { port } = server.address() as AddressInfo;
const call = async (method: string, path: string) => {
const response = await fetch(`http://127.0.0.1:${port}${path}`, {
method,
});
return { status: response.status, body: await response.json() };
};
return { mock, runner, call };
}
describe("renewalReason", () => {
const status = (certificate: PluginTlsCertificateInfo | null) => ({
enabled: true,
certificate,
renewal: null,
});
it("renews when missing, self-signed, for another domain or expiring", () => {
expect(renewalReason(status(null), "termix.example.com")).toBe("missing");
expect(
renewalReason(status(cert({ selfSigned: true })), "termix.example.com"),
).toBe("self-signed");
expect(renewalReason(status(cert()), "other.example.com")).toBe("domain");
expect(
renewalReason(
status(
cert({ notAfter: new Date(Date.now() + 10 * DAY).toISOString() }),
),
"termix.example.com",
),
).toBe("expiring");
expect(renewalReason(status(cert()), "TERMIX.example.com")).toBeNull();
});
});
describe("scheduled renewal", () => {
it("checks on a 12 hour timer and renews an expiring certificate", async () => {
const { mock } = await setup({
settings: CONFIGURED,
certificate: cert({
notAfter: new Date(Date.now() + 5 * DAY).toISOString(),
}),
});
const job = mock.scheduled.find((entry) => entry.kind === "every");
expect(job?.ms).toBe(CHECK_INTERVAL_MS);
await mock.runScheduled();
expect(issue).toHaveBeenCalledOnce();
expect(mock.tls.writes).toEqual([
{
certificatePem: "-----BEGIN CERTIFICATE-----new",
privateKeyPem: "-----BEGIN PRIVATE KEY-----new",
},
]);
const write = mock.tls.calls.indexOf("writeTlsCertificate");
expect(mock.tls.calls.indexOf("reloadTls")).toBeGreaterThan(write);
expect(mock.tls.reloads).toBe(1);
});
it("leaves a valid certificate alone", async () => {
const { mock } = await setup({ settings: CONFIGURED, certificate: cert() });
await mock.runScheduled();
expect(issue).not.toHaveBeenCalled();
expect(mock.tls.writes).toEqual([]);
});
it("does nothing and registers no renewer while renewal is off", async () => {
const { mock } = await setup({
settings: { ...CONFIGURED, autoRenew: false },
});
await mock.runScheduled();
expect(issue).not.toHaveBeenCalled();
expect(mock.tls.renewers).toBe(0);
});
it("registers as the renewer when configured and drops it when turned off", async () => {
const { mock } = await setup({ settings: CONFIGURED, certificate: cert() });
expect(mock.tls.renewers).toBe(1);
expect((await mock.ctx.system.tlsStatus()).renewal?.pluginId).toBe(
"acme-ssl",
);
await mock.ctx.settings.set("autoRenew", false);
await vi.waitFor(() => expect(mock.tls.renewers).toBe(0));
});
it("alerts every admin when a renewal fails", async () => {
const { mock, runner } = await setup({ settings: CONFIGURED });
issue.mockRejectedValueOnce(new Error("rate limited"));
expect(await runner.check()).toBe(false);
expect(mock.notifications).toEqual([
{
actor: undefined,
notification: expect.objectContaining({
body: "rate limited",
severity: "critical",
category: "acme-ssl.renewal_failed",
audience: "admins",
}),
},
]);
});
it("waits before retrying after a failed attempt", async () => {
const { mock, runner } = await setup({ settings: CONFIGURED });
issue.mockRejectedValueOnce(new Error("rate limited"));
expect(await runner.check()).toBe(false);
expect((await runner.state()).lastError).toBe("rate limited");
expect(await runner.check()).toBe(false);
expect(issue).toHaveBeenCalledOnce();
expect(
await runner.check(new Date(Date.now() + RETRY_AFTER_MS + 1000)),
).toBe(true);
expect(issue).toHaveBeenCalledTimes(2);
expect(mock.tls.reloads).toBe(1);
});
it("does not reload when core rejects the certificate", async () => {
const { mock, runner } = await setup({
settings: CONFIGURED,
validateTls: () => {
throw new Error("The certificate and private key do not match");
},
});
await expect(runner.issue()).rejects.toThrow(/do not match/);
expect(mock.tls.reloads).toBe(0);
expect((await runner.state()).lastError).toMatch(/do not match/);
});
});
describe("routes", () => {
it("reports status and issues on request", async () => {
const { call, mock } = await setup({ settings: CONFIGURED });
const status = await call("GET", "/status");
expect(status.status).toBe(200);
expect(status.body.missing).toBeNull();
expect(status.body.tls.certificate).toBeNull();
const issued = await call("POST", "/request");
expect(issued.status).toBe(200);
expect(issued.body.success).toBe(true);
expect(issued.body.tls.certificate).not.toBeNull();
expect(mock.tls.reloads).toBe(1);
});
it("answers 400 when a setting is missing", async () => {
const { call } = await setup({ settings: { domain: "a.example" } });
const result = await call("POST", "/request");
expect(result.status).toBe(400);
expect(result.body.missing).toBe("email");
expect(issue).not.toHaveBeenCalled();
});
it("refuses a user without acme-ssl.manage on every route", async () => {
const { call } = await setup({ settings: CONFIGURED, permissions: [] });
expect((await call("GET", "/status")).status).toBe(403);
expect((await call("POST", "/request")).status).toBe(403);
expect(issue).not.toHaveBeenCalled();
});
});
describe("capabilities", () => {
it("fails closed without system:tls", async () => {
await expect(
setup({
settings: CONFIGURED,
capabilities: manifest.capabilities.filter(
(capability) => capability !== "system:tls",
),
}),
).rejects.toMatchObject({
capability: "system:tls",
} satisfies Partial<CapabilityError>);
});
});
@@ -1,102 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import type { PluginFetchInit } from "@termix/plugin-sdk/backend";
import { createFetchAdapter } from "../../src/backend/acme.js";
import { createTxtRecord } from "../../src/backend/cloudflare.js";
describe("createFetchAdapter", () => {
it("sends acme-client requests through ctx.fetch", async () => {
const fetch = vi.fn(
async (_url: string, _init?: PluginFetchInit) =>
new Response('{"ok":true}', {
status: 201,
headers: { "replay-nonce": "n1", location: "https://ca/acct/1" },
}),
);
const adapter = createFetchAdapter(fetch, ["ca.internal"]);
const result = await adapter({
url: "https://ca.internal/new-acct",
method: "post",
headers: { "Content-Type": "application/jose+json" },
data: '{"payload":"x"}',
validateStatus: null,
});
expect(fetch).toHaveBeenCalledWith(
"https://ca.internal/new-acct",
expect.objectContaining({
method: "POST",
body: '{"payload":"x"}',
allowPrivateHosts: ["ca.internal"],
headers: { "Content-Type": "application/jose+json" },
}),
);
expect(result.status).toBe(201);
expect(result.data).toBe('{"ok":true}');
expect(result.headers["replay-nonce"]).toBe("n1");
});
it("rejects with the response attached when validateStatus says no", async () => {
const adapter = createFetchAdapter(
async () => new Response("slow down", { status: 429 }),
);
await expect(
adapter({ url: "https://ca/x", validateStatus: () => false }),
).rejects.toMatchObject({
code: "ERR_BAD_REQUEST",
response: { status: 429, data: "slow down" },
});
});
});
describe("createTxtRecord", () => {
it("finds the zone by parent domain, creates the record and deletes it", async () => {
const calls: Array<{ url: string; method: string; body?: string }> = [];
const fetch = vi.fn(async (url: string, init?: PluginFetchInit) => {
calls.push({ url, method: init?.method ?? "GET", body: init?.body });
if (url.includes("/zones?name=example.com")) {
return Response.json({
success: true,
result: [{ id: "zone-1", name: "example.com" }],
});
}
if (url.includes("/zones?name=")) {
return Response.json({ success: true, result: [] });
}
if (init?.method === "POST") {
return Response.json({ success: true, result: { id: "rec-1" } });
}
return Response.json({ success: true, result: {} });
});
const remove = await createTxtRecord(
fetch,
"token",
"_acme-challenge.termix.example.com",
"digest",
);
const post = calls.find((call) => call.method === "POST")!;
expect(post.url).toContain("/zones/zone-1/dns_records");
expect(JSON.parse(post.body!)).toMatchObject({
type: "TXT",
name: "_acme-challenge.termix.example.com",
content: "digest",
});
await remove();
expect(calls.at(-1)).toMatchObject({
method: "DELETE",
url: expect.stringContaining("/zones/zone-1/dns_records/rec-1"),
});
});
it("surfaces the Cloudflare error message", async () => {
const fetch = async () =>
Response.json(
{ success: false, errors: [{ message: "Invalid token" }] },
{ status: 403 },
);
await expect(
createTxtRecord(fetch, "bad", "_acme-challenge.a.example", "d"),
).rejects.toThrow(/Invalid token/);
});
});
@@ -1,85 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import {
renderWithApp,
type RenderedPluginApp,
} from "@termix/plugin-sdk/testing";
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
import * as plugin from "../../src/frontend/index";
import manifestJson from "../../manifest.json";
import locales from "../../locales/en.json";
const api = vi.hoisted(() => ({
get: vi.fn(),
post: vi.fn(),
}));
vi.mock("@termix/plugin-sdk/frontend", async (importOriginal) => ({
...(await importOriginal<typeof import("@termix/plugin-sdk/frontend")>()),
useTranslation: () => ({
t: (key: string, values?: Record<string, string>) =>
values ? `${key} ${Object.values(values).join(" ")}` : key,
}),
usePluginApi: () => api,
}));
const { AcmeStatusSetting } =
await import("../../src/frontend/AcmeStatusSetting");
const manifest = manifestJson as unknown as PluginManifest;
let rendered: RenderedPluginApp | null = null;
afterEach(async () => {
await rendered?.deactivate();
rendered = null;
api.get.mockReset();
});
describe("acme-ssl activate", () => {
it("registers the status settings component and removes it on deactivate", async () => {
rendered = await renderWithApp(plugin, { manifest, locales });
expect(rendered.registered.settingsComponents()).toEqual(["status"]);
await rendered.deactivate();
expect(rendered.registered.settingsComponents()).toEqual([]);
rendered = null;
});
});
describe("AcmeStatusSetting", () => {
it("shows the served certificate and the last error", async () => {
api.get.mockResolvedValue({
data: {
tls: {
enabled: true,
certificate: {
issuer: "CN=R11",
names: ["termix.example.com"],
notAfter: "2030-01-01T00:00:00.000Z",
selfSigned: false,
},
},
state: {
lastAttemptAt: null,
lastIssuedAt: null,
lastError: "rate limited",
},
autoRenew: true,
missing: null,
},
});
render(
<AcmeStatusSetting
pluginId="acme-ssl"
values={{}}
setValue={() => {}}
running
/>,
);
await waitFor(() =>
expect(screen.getByText("status.names termix.example.com")).toBeTruthy(),
);
expect(screen.getByText("status.lastError rate limited")).toBeTruthy();
expect(api.get).toHaveBeenCalledWith("/status");
});
});
-4
View File
@@ -1,4 +0,0 @@
{
"extends": "@termix/plugin-sdk/tsconfig.plugin.json",
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"]
}
-3
View File
@@ -1,3 +0,0 @@
import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset";
export default pluginVitestConfig(import.meta.url);
-21
View File
@@ -1,21 +0,0 @@
{
"extends": ["@commitlint/config-conventional"],
"rules": {
"type-enum": [
2,
"always",
[
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"test",
"chore",
"revert"
]
],
"subject-case": [0]
}
}
-1
View File
@@ -1 +0,0 @@
custom: https://donate.termix.site/
-34
View File
@@ -1,34 +0,0 @@
# Overview
_Short summary of what this PR does_
- [ ] Added: ...
- [ ] Updated: ...
- [ ] Removed: ...
- [ ] Fixed: ...
# Changes Made
_Detailed explanation of changes (if needed)_
- ...
# Related Issues
_Link any issues this PR addresses_
- Closes #ISSUE_NUMBER
- Related to #ISSUE_NUMBER
# Screenshots / Demos
_(Optional: add before/after screenshots, GIFs, or console output)_
# Checklist
- [ ] Code follows project style guidelines
- [ ] Supports mobile and desktop UI/app (if applicable)
- [ ] Added the change to `CHANGELOG.json`
- [ ] `npm run test`, `npm run typecheck` and `npm run validate` pass
- [ ] I have read `CONTRIBUTING.md`
- [ ] This is not a translation request. See [docs](https://docs.termix.site/translations)
-9
View File
@@ -1,9 +0,0 @@
node_modules
dist
coverage
*.log
.env
*.tmxplug
*.tmxplug.sig
termix-plugin-signing.key
.termix-tables-*
-1
View File
@@ -1 +0,0 @@
npx --no -- commitlint --edit "$1"
-1
View File
@@ -1 +0,0 @@
npx lint-staged
-4
View File
@@ -1,4 +0,0 @@
dist
coverage
node_modules
package-lock.json
-9
View File
@@ -1,9 +0,0 @@
{
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80,
"arrowParens": "always",
"endOfLine": "lf"
}
-47
View File
@@ -1,47 +0,0 @@
# Remote AI agents
The **AI Agent** host action appears on the host card and in the SSH terminal toolbar. It opens a host-scoped session tab. Enable AI globally, opt in for the user, and grant `ai.agents` (administrators by default).
Choose Pi, OpenCode, Claude Code, or Codex; an existing Termix AI provider; a model; and an absolute working directory. The target needs POSIX SSH, Node.js 22.19+, the selected CLI on its login-shell PATH (or an explicit executable path), and SSH remote forwarding to `127.0.0.1`. Missing dependencies fail visibly. The explicit Install button installs the selected CLI and an isolated Node.js 24 LTS runtime; starting an agent never installs software automatically.
Termix owns the SSH connection, session history, streaming UI, cancellation, and permission responses. The remote runner uses the CLI's native protocol: Pi RPC, Claude streaming JSON, Codex app-server, or OpenCode's loopback HTTP API. No Paseo runtime or daemon is required.
Model requests travel over an SSH reverse tunnel to a session-scoped proxy. The proxy authenticates a random token, checks host access and AI permissions, restricts inference paths and the selected model, and uses the current user's saved Provider key and the existing AI private-endpoint allowlist. Provider keys never go to the browser or target. OpenAI/compatible endpoints support Pi and OpenCode; Claude needs Anthropic; Codex needs **Responses**, not just Chat Completions. Selecting an OpenAI-compatible record does not prove Responses support.
Agent state lives in the target user's `~/.local/state/termix-agents/<session-id>` and the AI plugin's own tables (`agent_sessions`, with events and queued prompts as separate rows). Prompts, drafts and stored event text are limited to 65,535 UTF-8 bytes, the MySQL TEXT limit. Closing a tab leaves the session running; stop ends it. A server restart stops active transports; Resume reopens the native session where supported. Sessions expire after eight hours. Up to four sessions may run per user. The UI retains the most recent 2,000 events; native history remains on the target. Deleting a Termix session does not delete its target-side directory.
The agent runs with the SSH user's authority. Claude/OpenCode/Codex approval requests are shown in the tab; Pi's ordinary tools run without per-tool approval. Pi extension prompts are forwarded. Structured native questions currently accept JSON answers. Root SSH gives the agent root privileges; select the account and directory accordingly. No global agent config is overwritten.
The initial host action asks for the working directory explicitly; it does not infer one from terminal buffer text. Mobile-native UI and cross-host session migration are outside this release.
## Validation
`tests/backend/agents/agents.test.ts` covers target/command validation, inference-path restrictions, protocol compatibility, and four native-protocol fixture processes including an approval round trip. A real provider turn must also be checked separately; fixture success is not model access verification.
## Official runtime installation
The host panel provides **Install selected agent and Node.js**, with streamed progress and version verification. It uses the same host access, AI opt-in, and `ai.agents` permission as remote sessions, and records an audit event. Installation needs curl, tar, a SHA-256 utility, and Linux/macOS x64 or arm64. It requires no sudo and does not change system packages, shell profiles, existing CLI installations, or SSH forwarding policy. Stop active agents on the host before installing.
Node.js 24 LTS comes from https://nodejs.org/dist/ and must match the official SHA-256 manifest. Agent package names are fixed to the official documentation: [Pi](https://pi.dev/docs/latest/quickstart), [OpenCode](https://opencode.ai/docs/), [Claude Code](https://code.claude.com/docs/en/setup), and [Codex](https://developers.openai.com/cookbook/examples/codex/secure_quality_gitlab). Only https://registry.npmjs.org dependencies are accepted; missing SHA-512 lock entries are filled from matching official version metadata. npm ci checks the locked integrity values. User/global npm configuration and lifecycle scripts are disabled. Claude/OpenCode use the verified optional platform binary directly, without running download scripts. No third-party mirror or arbitrary installer URL is accepted.
Files live under `~/.local/share/termix-agent-runtime`. Termix automatically uses this runtime for launches with the default executable. A verified replacement retains the previous installation in `<agent>.previous`. Unsupported systems, missing prerequisites, download failures, checksum mismatches, and unavailable platform binaries fail visibly without activating the replacement. SSH remote forwarding remains a separate administrator setting.
## Enable Agent forwarding
The separate **Enable Agent forwarding** button configures standard Linux/systemd OpenSSH servers using root or non-interactive sudo. It derives the SSH account and actual peer address from the SSH connection, rather than accepting them from the browser. Existing working remote forwarding is left unchanged. Otherwise, it inserts a scoped `Match User ... Address ...` block allowing `127.0.0.1:*` remote listeners with `GatewayPorts no`; existing local forwarding is preserved. This uses the same host access and `ai.agents` gates and records an audit event.
The operation backs up sshd_config under `/etc/ssh/termix-agent-backups`, validates both syntax and effective rules before activation, and reloads the active SSH service. Failed activation/reload restores the previous configuration. Existing `DisableForwarding` policies are not overridden. Start/resume creates a new SSH connection after a successful change. Idempotency, preservation of local forwarding, invalid configuration rejection, and reload-failure rollback are covered by executable shell fixtures.
After configuration, Termix opens a fresh SSH connection and verifies an actual loopback remote listener before reporting success. Key-level forwarding restrictions or a different active server configuration therefore remain visible failures.
The conversation composer sends with Enter and inserts a newline with Shift+Enter. Escape interrupts a running agent when focus is inside its panel. IME composition and repeated keydown events do not submit or interrupt; a failed send preserves the draft.
## Conversation workflow
- Enter sends an idle turn or queues a message while running. A session stores up to 20 queued messages, each editable/removable until dispatched. Dispatch is FIFO after completion; cancellation, archive, and remote errors pause the queue. Continue explicitly resumes it. A stopped session must be resumed first. Messages already dispatched are not automatically replayed after a transport failure.
- Rename and search sessions by name/model/directory. Archive stops the remote process and pauses its queue; restore preserves its transcript. Composer drafts are saved per session after 500 ms and on tab/session unmount. Unsent attachments remain reusable in the session attachment picker.
- Upload a file, paste an image into the composer, or reference a regular file within the working directory (symlinks outside it are rejected). Limits: 1 MiB per file, four per message, 100 per session. Uploads live in the remote user's private session directory. Image MIME is detected from bytes and sent through native Pi/Claude/Codex/OpenCode image inputs; the selected provider/model must support vision. Other files are referenced by their host path for the agent's tools. No file is sent to the model until a message using it is dispatched.
- Git review lists changed/untracked files and shows unstaged and staged diffs, including deleted files. Worktree creation makes a new branch from HEAD in the remote user's Termix state directory. It preserves uncommitted changes in the original workspace, does not overwrite an existing branch, and offers a new session in the resulting directory. Repository hooks, external diff commands, and textconv are disabled for these operations. Worktrees are retained until the user removes them explicitly.
- Browser event streams reconnect with bounded backoff and refresh a persisted snapshot before continuing from its sequence cursor. Duplicate events are suppressed and events are written in batches at most once a second. Access-denied sessions stop retrying. A server/SSH restart still requires Resume; the UI never replays a potentially executed command. The 2,000-event UI retention limit still applies.
Tests cover FIFO/cancel behavior through the HTTP routes, cross-user metadata/attachment access, native image payloads for four CLI protocols, real temporary Git repositories, path/size boundaries, archive persistence, keyboard/IME handling, and dropped-stream recovery.
-38
View File
@@ -1,38 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/Termix-SSH/Termix/main/packages/plugin-sdk/schema/changelog.schema.json",
"releases": [
{
"version": "1.0.0",
"changes": [
{
"type": "added",
"text": "Ask about your hosts, snippets, fleets, alerts, automations and more"
},
{
"type": "added",
"text": "Proposes changes instead of making them, and nothing runs until you approve it"
},
{
"type": "added",
"text": "Can run safe read-only commands like `df` and `uptime` if you allow it"
},
{
"type": "added",
"text": "Ask about a host right from its terminal"
},
{
"type": "added",
"text": "Works with Ollama, Anthropic, OpenAI, Google Gemini and any OpenAI compatible endpoint"
},
{
"type": "added",
"text": "AI Agents: run Pi, OpenCode, Claude Code or Codex on an SSH host using one of your Termix providers, with tool approvals, queued messages, attachments, Git diffs and worktrees"
},
{
"type": "added",
"text": "One-click install of the selected agent and Node.js, and a button to set up SSH loopback forwarding"
}
]
}
]
}
-37
View File
@@ -1,37 +0,0 @@
# Contributing to AI Assistant
## Development
```bash
npm run build # build into dist/
npm run test # run this plugin's tests
npm run typecheck # type-check this plugin
npm run validate # check manifest.json
npm run format # format the code with Prettier
```
## Settings
### Admin
- **AI assistant:** let users turn the assistant on. While this is off it is hidden and blocked for everyone
- **Allowed private AI hosts:** hosts on your private network a provider may point at, such as a self-hosted Ollama
### User
- **Enable the AI assistant:** turn the assistant on for yourself
- **Allow read-only diagnostic commands:** let the assistant run safe commands without asking
- **Providers:** your AI providers, models and API keys
### Host
- **Enable AI Assistant:** let the assistant help in this host's terminal. Off by default
## Permissions
- `ai.use`: use the assistant. Admins and users have it by default.
- `ai.manage_providers`: add, edit and remove AI providers and their API keys. Admins and users have it by default.
- `ai.apply_proposals`: let the assistant carry out the changes it suggests. Admins and users have it by default.
- `ai.services.use`: let other features call the assistant for you. Admins and users have it by default.
- `ai.secrets.share`: let other features use the stored API key without seeing it. Admins and users have it by default.
- `ai.agents`: run coding agents on SSH hosts. Only admins have it by default.
-48
View File
@@ -1,48 +0,0 @@
<div align="center">
<img src="https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/icon.svg" width="120" height="120" alt="Termix Logo" />
<h1>AI Assistant</h1>
<p>An optional AI assistant that knows your servers</p>
</div>
<br />
## Overview
AI Assistant is a chat that can read your Termix setup and propose changes for you to approve. It can also run coding agents on your SSH hosts.
<br />
## Features
- Ask about your hosts, snippets, fleets, alerts, automations and more
- Proposes changes instead of making them, and nothing runs until you approve it
- Can run safe read-only commands like `df` and `uptime` if you allow it
- Ask about a host right from its terminal
- Works with Ollama, Anthropic, OpenAI, Google Gemini and any OpenAI compatible endpoint
- Run Pi, OpenCode, Claude Code or Codex as a coding agent on an SSH host, using your own provider
<br />
## Services
Uses from other plugins, each one optional:
- `workspaces.saved`, `network-topology.graph`, `snippets.access`, `fleets.access`, `terminal.history`, `automations.access` and `homepage.items` to read your data
<br />
## Support
To report a bug or request a feature, open a [support ticket](https://github.com/Termix-SSH/Support/issues/new/choose). You need to be logged in to GitHub. Please be as detailed as possible, preferably in English.
For discussions and questions, join the [Discord](https://discord.gg/jVQGdvHDrf) server.
<br />
## License
Distributed under the Apache License Version 2.0. See [LICENSE](https://github.com/Termix-SSH/Termix/blob/main/LICENSE) for more information.
-198
View File
@@ -1,198 +0,0 @@
{
"permissions": {
"use": {
"title": "Use the assistant",
"description": "Open the assistant and ask it things."
},
"manage_providers": {
"title": "Manage providers",
"description": "Add, edit and remove the AI providers and their API keys."
},
"apply_proposals": {
"title": "Apply proposals",
"description": "Let the assistant carry out the changes it suggests."
},
"services.use": {
"title": "Use AI from other features",
"description": "Other features may call the assistant on your behalf."
},
"secrets.share": {
"title": "Share the API key",
"description": "Other features may use the stored AI API key without seeing it."
}
},
"settings": {
"assistantUnavailable": "The AI assistant is turned off on this server.",
"providersNeedOptIn": "Turn on the AI assistant above to add providers.",
"admin": {
"globallyEnabled": {
"label": "AI assistant",
"description": "Let users turn on the AI assistant. While this is off, the assistant is hidden and blocked for everyone."
},
"privateEndpoints": {
"label": "Allowed private AI hosts",
"description": "Hosts on your private network that users may point a provider at, such as a self-hosted Ollama. Put one host on each line."
}
},
"host": {
"enableAiAssistant": {
"label": "Enable AI Assistant",
"description": "Allow the AI assistant to help in this host's terminal. Off by default, and still needs AI enabled for your account."
}
}
},
"ai": {
"title": "Assistant",
"send": "Send",
"stop": "Stop",
"save": "Save",
"cancel": "Cancel",
"running": "running",
"approve": "Approve",
"reject": "Reject",
"inputPlaceholder": "Ask about your servers, or describe a change to propose",
"selectProvider": "Select a provider",
"providerSettings": "Provider settings",
"addProvider": "Add provider",
"editProvider": "Edit provider",
"removeProvider": "Remove provider",
"providerType": "Provider",
"providerLabel": "Name",
"providerLabelPlaceholder": "For example: Local Ollama",
"baseUrl": "Server address",
"apiKey": "API key",
"defaultModel": "Default model",
"defaultModelPlaceholder": "For example: llama3.1 or claude-opus-5",
"labelRequired": "Give this provider a name",
"providerSaveFailed": "Could not save the provider",
"providerUpdated": "Provider updated",
"providerDeleteFailed": "Could not remove the provider",
"privateEndpointHint": "Addresses on your private network need an administrator to allow the host first.",
"providerOllama": "Ollama",
"providerAnthropic": "Anthropic",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "OpenAI compatible",
"toolArguments": "Input",
"toolResult": "Result",
"statusApplied": "Applied",
"statusRejected": "Rejected",
"proposalApplied": "Change applied",
"proposalApplyFailed": "Could not apply the change",
"proposalRejectFailed": "Could not reject the change",
"enableTitle": "Enable the AI assistant",
"enableDescription": "Let the assistant read your setup and suggest changes for you to approve.",
"readOnlyCommands": "Allow read-only diagnostic commands",
"readOnlyCommandsDescription": "The assistant may run a short list of safe commands like df and uptime without asking each time. Anything that changes a server is always proposed first.",
"modelDetectFailed": "Could not reach the provider, so this is a general list. You can type any model name.",
"modelCustom": "Enter a model name",
"modelRefresh": "Refresh list",
"modelPlaceholder": "Choose or type a model",
"attachHint": "Type @ to reference a host, snippet or automation. Changes are proposed for you to approve.",
"runCommandTitle": "Run command",
"assistant": "AI Assistant",
"terminalPanelEmpty": "Ask the assistant about this host. Anything that changes state needs your approval first.",
"terminalPanelPlaceholder": "Ask about this host...",
"noProvidersConfigured": "No AI provider is configured yet. Add one in the AI assistant tab.",
"runsInOpenTerminal": "Runs in your open terminal session.",
"runInTerminal": "Run",
"terminalContextPrefill": "Here is my terminal output:\n\n{{output}}\n\n",
"secrets": {
"shareApiKey": "Let other features use your AI provider API key for their own requests. The key is never copied, and you can revoke this at any time."
}
},
"nav": {
"ai": "AI"
},
"onboarding": {
"aiTitle": "AI assistant",
"aiIntro": "Termix can include an optional AI assistant. You can change this later in your settings.",
"aiSkipTitle": "Leave it off",
"aiSkipDesc": "Termix works as it does now. The assistant stays hidden.",
"aiEnableTitle": "Turn it on",
"aiEnableDesc": "Adds an assistant that reads your setup and suggests changes you approve. Needs a provider you supply, such as a local Ollama.",
"aiNote": "Nothing is sent anywhere until you add a provider and send a message."
},
"hosts": {
"docsLink": "View docs"
},
"agents": {
"title": "AI Agent",
"permissionTitle": "Run remote AI agents",
"permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.",
"new": "New session",
"description": "Run an agent in a directory on this host using a Termix AI provider.",
"runtime": "Agent",
"provider": "Termix provider",
"chooseProvider": "Choose a compatible provider",
"model": "Model",
"directory": "Working directory",
"executable": "Executable (optional absolute path)",
"requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.",
"responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.",
"piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.",
"start": "Start agent",
"resume": "Resume",
"stop": "Stop session",
"interrupt": "Interrupt turn",
"send": "Send",
"prompt": "Ask the agent to work in this directory…",
"noSessions": "No agent sessions for this host.",
"approval": "Agent needs your decision",
"answer": "Answer (JSON for structured questions)",
"allow": "Allow once",
"deny": "Deny",
"tool": "Tool activity",
"streamFailed": "Agent stream disconnected. Reopen the session to reconnect.",
"status": {
"starting": "Starting",
"ready": "Ready",
"running": "Working",
"stopped": "Stopped",
"error": "Error"
},
"installRuntime": "Install selected agent and Node.js",
"installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.",
"installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.",
"installFailed": "Installation failed. Review the log and retry.",
"installLog": "Installation log",
"enableForwarding": "Enable Agent forwarding",
"forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.",
"forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.",
"forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.",
"search": "Search sessions",
"showArchived": "Show archived",
"sessionName": "Session name",
"rename": "Rename",
"archive": "Archive and stop",
"restore": "Restore session",
"queue": "Queued messages",
"enqueue": "Queue message",
"continueQueue": "Continue queue",
"pauseQueue": "Pause queue",
"editQueued": "Edit queued message",
"save": "Save",
"remove": "Remove",
"upload": "Upload attachment",
"remoteFile": "File path inside working directory",
"attach": "Attach file",
"attachments": "Reuse a session attachment",
"pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.",
"attachmentLimit": "Up to 4 attachments, 1 MiB each",
"review": "Git changes and worktrees",
"gitStatus": "Refresh Git status",
"diffFile": "File path to review",
"diff": "View diff",
"branch": "New branch name",
"createWorktree": "Create worktree",
"useWorktree": "New session in this worktree",
"worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.",
"clean": "No changes",
"reconnecting": "Reconnecting and restoring messages…",
"unavailable": "Session is no longer accessible. Check your permissions or sign in again.",
"shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue"
},
"plugin": {
"name": "AI Assistant"
}
}
-198
View File
@@ -1,198 +0,0 @@
{
"permissions": {
"use": {
"title": "Gebruik die assistent",
"description": "Maak die assistent oop en vra dit dinge."
},
"manage_providers": {
"title": "Bestuur verskaffers",
"description": "Voeg die KI-verskaffers en hul API-sleutels by, wysig en verwyder hulle."
},
"apply_proposals": {
"title": "Pas voorstelle toe",
"description": "Laat die assistent die veranderinge wat dit voorstel, uitvoer."
},
"services.use": {
"title": "Gebruik KI van ander funksies",
"description": "Ander funksies kan die assistent namens jou skakel."
},
"secrets.share": {
"title": "Deel die API-sleutel",
"description": "Ander funksies mag die gestoorde KI API-sleutel gebruik sonder om dit te sien."
}
},
"settings": {
"assistantUnavailable": "Die KI-assistent is afgeskakel op hierdie bediener.",
"providersNeedOptIn": "Skakel die KI-assistent hierbo aan om verskaffers by te voeg.",
"admin": {
"globallyEnabled": {
"label": "KI-assistent",
"description": "Laat gebruikers die KI-assistent aanskakel. Terwyl dit af is, is die assistent vir almal versteek en geblokkeer."
},
"privateEndpoints": {
"label": "Toegelate private KI-gashere",
"description": "Gashere op jou privaat netwerk waarna gebruikers 'n verskaffer kan verwys, soos 'n self-gehoste Ollama. Plaas een gasheer op elke lyn."
}
},
"host": {
"enableAiAssistant": {
"label": "Aktiveer KI-assistent",
"description": "Laat die KI-assistent toe om in hierdie gasheer se terminaal te help. Standaard afgeskakel, en KI moet steeds vir jou rekening geaktiveer word."
}
}
},
"ai": {
"title": "Assistent",
"send": "Stuur",
"stop": "Stop",
"save": "Stoor",
"cancel": "Kanselleer",
"running": "hardloop",
"approve": "Goedkeur",
"reject": "Verwerp",
"inputPlaceholder": "Vra oor jou bedieners, of beskryf 'n verandering om voor te stel",
"selectProvider": "Kies 'n verskaffer",
"providerSettings": "Verskafferinstellings",
"addProvider": "Voeg verskaffer by",
"editProvider": "Wysig verskaffer",
"removeProvider": "Verwyder verskaffer",
"providerType": "Verskaffer",
"providerLabel": "Naam",
"providerLabelPlaceholder": "Byvoorbeeld: Plaaslike Ollama",
"baseUrl": "Bedieneradres",
"apiKey": "API-sleutel",
"defaultModel": "Standaardmodel",
"defaultModelPlaceholder": "Byvoorbeeld: llama3.1 of claude-opus-5",
"labelRequired": "Gee hierdie verskaffer 'n naam",
"providerSaveFailed": "Kon nie die verskaffer stoor nie",
"providerUpdated": "Verskaffer opgedateer",
"providerDeleteFailed": "Kon nie die verskaffer verwyder nie",
"privateEndpointHint": "Adresse op jou private netwerk benodig eers 'n administrateur om die gasheer toe te laat.",
"providerOllama": "Ollama",
"providerAnthropic": "Antropiese",
"providerOpenai": "OpenAI",
"providerGemini": "Google Tweeling",
"providerOpenaiCompatible": "OpenAI-versoenbaar",
"toolArguments": "Invoer",
"toolResult": "Resultaat",
"statusApplied": "Toegepas",
"statusRejected": "Verwerp",
"proposalApplied": "Verandering toegepas",
"proposalApplyFailed": "Kon nie die verandering toepas nie",
"proposalRejectFailed": "Kon nie die verandering verwerp nie",
"enableTitle": "Aktiveer die KI-assistent",
"enableDescription": "Laat die assistent jou opstelling lees en veranderinge voorstel wat jy kan goedkeur.",
"readOnlyCommands": "Laat leesalleen-diagnostiese bevele toe",
"readOnlyCommandsDescription": "Die assistent kan 'n kort lys veilige opdragte soos df en uptime uitvoer sonder om elke keer te vra. Enigiets wat 'n bediener verander, word altyd eerste voorgestel.",
"modelDetectFailed": "Kon nie die verskaffer bereik nie, so dit is 'n algemene lys. Jy kan enige modelnaam tik.",
"modelCustom": "Voer 'n modelnaam in",
"modelRefresh": "Verfris lys",
"modelPlaceholder": "Kies of tik 'n model",
"attachHint": "Tik @ om na 'n gasheer, brokkie of outomatisering te verwys. Veranderinge word voorgestel vir jou om goed te keur.",
"runCommandTitle": "Voer opdrag uit",
"assistant": "KI-assistent",
"terminalPanelEmpty": "Vra die assistent oor hierdie gasheer. Enigiets wat van status verander, benodig eers jou goedkeuring.",
"terminalPanelPlaceholder": "Vra oor hierdie gasheer...",
"noProvidersConfigured": "Geen KI-verskaffer is nog gekonfigureer nie. Voeg een by in die KI-assistent-oortjie.",
"runsInOpenTerminal": "Loop in jou oop terminaalsessie.",
"runInTerminal": "Hardloop",
"terminalContextPrefill": "Hier is my terminaaluitvoer:\n\n{{output}}\n\n",
"secrets": {
"shareApiKey": "Laat ander funksies jou KI-verskaffer API-sleutel vir hul eie versoeke gebruik. Die sleutel word nooit gekopieer nie, en jy kan dit te eniger tyd herroep."
}
},
"nav": {
"ai": "KI"
},
"onboarding": {
"aiTitle": "KI-assistent",
"aiIntro": "Termix kan 'n opsionele KI-assistent insluit. Jy kan dit later in jou instellings verander.",
"aiSkipTitle": "Los dit af",
"aiSkipDesc": "Termix werk soos dit nou doen. Die assistent bly versteek.",
"aiEnableTitle": "Skakel dit aan",
"aiEnableDesc": "Voeg 'n assistent by wat jou opstelling lees en veranderinge voorstel wat jy goedkeur. Benodig 'n verskaffer wat jy verskaf, soos 'n plaaslike Ollama.",
"aiNote": "Niks word êrens heen gestuur totdat jy 'n verskaffer byvoeg en 'n boodskap stuur nie."
},
"hosts": {
"docsLink": "Bekyk dokumente"
},
"agents": {
"title": "AI Agent",
"permissionTitle": "Run remote AI agents",
"permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.",
"new": "New session",
"description": "Run an agent in a directory on this host using a Termix AI provider.",
"runtime": "Agent",
"provider": "Termix provider",
"chooseProvider": "Choose a compatible provider",
"model": "Model",
"directory": "Working directory",
"executable": "Executable (optional absolute path)",
"requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.",
"responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.",
"piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.",
"start": "Start agent",
"resume": "Resume",
"stop": "Stop session",
"interrupt": "Interrupt turn",
"send": "Send",
"prompt": "Ask the agent to work in this directory…",
"noSessions": "No agent sessions for this host.",
"approval": "Agent needs your decision",
"answer": "Answer (JSON for structured questions)",
"allow": "Allow once",
"deny": "Deny",
"tool": "Tool activity",
"streamFailed": "Agent stream disconnected. Reopen the session to reconnect.",
"status": {
"starting": "Starting",
"ready": "Ready",
"running": "Working",
"stopped": "Stopped",
"error": "Error"
},
"installRuntime": "Install selected agent and Node.js",
"installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.",
"installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.",
"installFailed": "Installation failed. Review the log and retry.",
"installLog": "Installation log",
"enableForwarding": "Enable Agent forwarding",
"forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.",
"forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.",
"forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.",
"search": "Search sessions",
"showArchived": "Show archived",
"sessionName": "Session name",
"rename": "Rename",
"archive": "Archive and stop",
"restore": "Restore session",
"queue": "Queued messages",
"enqueue": "Queue message",
"continueQueue": "Continue queue",
"pauseQueue": "Pause queue",
"editQueued": "Edit queued message",
"save": "Save",
"remove": "Remove",
"upload": "Upload attachment",
"remoteFile": "File path inside working directory",
"attach": "Attach file",
"attachments": "Reuse a session attachment",
"pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.",
"attachmentLimit": "Up to 4 attachments, 1 MiB each",
"review": "Git changes and worktrees",
"gitStatus": "Refresh Git status",
"diffFile": "File path to review",
"diff": "View diff",
"branch": "New branch name",
"createWorktree": "Create worktree",
"useWorktree": "New session in this worktree",
"worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.",
"clean": "No changes",
"reconnecting": "Reconnecting and restoring messages…",
"unavailable": "Session is no longer accessible. Check your permissions or sign in again.",
"shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue"
},
"plugin": {
"name": "AI Assistant"
}
}
-198
View File
@@ -1,198 +0,0 @@
{
"permissions": {
"use": {
"title": "استخدم المساعد",
"description": "افتح المساعد واسأله ما تريد."
},
"manage_providers": {
"title": "إدارة الموفر",
"description": "أضف، عدّل، واحذف مزودي خدمات الذكاء الاصطناعي ومفاتيح واجهة برمجة التطبيقات الخاصة بهم."
},
"apply_proposals": {
"title": "تقديم المقترحات",
"description": "دع المساعد يقوم بتنفيذ التغييرات التي يقترحها."
},
"services.use": {
"title": "استخدم الذكاء الاصطناعي من ميزات أخرى",
"description": "قد تقوم ميزات أخرى بالاتصال بالمساعد نيابةً عنك."
},
"secrets.share": {
"title": "شارك مفتاح API",
"description": "قد تستخدم ميزات أخرى مفتاح واجهة برمجة تطبيقات الذكاء الاصطناعي المخزن دون رؤيته."
}
},
"settings": {
"assistantUnavailable": "تم إيقاف تشغيل مساعد الذكاء الاصطناعي على هذا الخادم.",
"providersNeedOptIn": "قم بتشغيل مساعد الذكاء الاصطناعي أعلاه لإضافة مزودي الخدمة.",
"admin": {
"globallyEnabled": {
"label": "مساعد الذكاء الاصطناعي",
"description": "اسمح للمستخدمين بتفعيل مساعد الذكاء الاصطناعي. عند إيقاف تشغيله، يكون المساعد مخفيًا ومحظورًا عن الجميع."
},
"privateEndpoints": {
"label": "المضيفون الخاصون المسموح بهم للذكاء الاصطناعي",
"description": "المضيفون على شبكتك الخاصة الذين يمكن للمستخدمين توجيه مزود الخدمة إليهم، مثل Ollama المُستضاف ذاتيًا. ضع مضيفًا واحدًا على كل سطر."
}
},
"host": {
"enableAiAssistant": {
"label": "تفعيل مساعد الذكاء الاصطناعي",
"description": "اسمح للمساعد الذكي بالمساعدة في طرفية هذا المضيف. هذه الميزة معطلة افتراضيًا، ولا يزال يتعين تفعيلها لحسابك."
}
}
},
"ai": {
"title": "مساعد",
"send": "يرسل",
"stop": "التوقف",
"save": "حفظ",
"cancel": "إلغاء",
"running": "قيد التشغيل",
"approve": "يعتمد",
"reject": "يرفض",
"inputPlaceholder": "استفسر عن خوادمك، أو صف تغييرًا ترغب في اقتراحه.",
"selectProvider": "اختر مزود الخدمة",
"providerSettings": "إعدادات المزوّد",
"addProvider": "إضافة موفر",
"editProvider": "موفر التحرير",
"removeProvider": "إزالة الموفر",
"providerType": "مزود",
"providerLabel": "الاسم",
"providerLabelPlaceholder": "على سبيل المثال: أولاما المحلي",
"baseUrl": "عنوان الخادم",
"apiKey": "مفتاح API",
"defaultModel": "النموذج الافتراضي",
"defaultModelPlaceholder": "على سبيل المثال: llama3.1 أو claude-opus-5",
"labelRequired": "أعطِ هذا المزوّد اسمًا",
"providerSaveFailed": "تعذر حفظ بيانات المزوّد",
"providerUpdated": "تم تحديث المزوّد",
"providerDeleteFailed": "تعذر إزالة الموفر",
"privateEndpointHint": "تحتاج العناوين الموجودة على شبكتك الخاصة إلى موافقة المسؤول أولاً للسماح للمضيف بالوصول إليها.",
"providerOllama": "أولاما",
"providerAnthropic": "الأنثروبيك",
"providerOpenai": "أوبن إيه آي",
"providerGemini": "جوجل جيميني",
"providerOpenaiCompatible": "متوافق مع OpenAI",
"toolArguments": "إدخال",
"toolResult": "نتيجة",
"statusApplied": "مُطبَّق",
"statusRejected": "مرفوض",
"proposalApplied": "تم تطبيق التغيير",
"proposalApplyFailed": "تعذر تطبيق التغيير",
"proposalRejectFailed": "لم يكن بالإمكان رفض التغيير",
"enableTitle": "قم بتفعيل مساعد الذكاء الاصطناعي",
"enableDescription": "دع المساعد يقرأ إعداداتك ويقترح تغييرات لتوافق عليها.",
"readOnlyCommands": "السماح بأوامر التشخيص للقراءة فقط",
"readOnlyCommandsDescription": "قد يقوم المساعد بتشغيل قائمة مختصرة من الأوامر الآمنة مثل df و uptime دون الحاجة إلى طلب ذلك في كل مرة. ويتم اقتراح أي أمر يُغيّر حالة الخادم أولاً.",
"modelDetectFailed": "تعذر الوصول إلى المورّد، لذا فهذه قائمة عامة. يمكنك كتابة أي اسم طراز.",
"modelCustom": "أدخل اسم النموذج",
"modelRefresh": "تحديث القائمة",
"modelPlaceholder": "اختر أو اكتب طرازًا",
"attachHint": "اكتب @ للإشارة إلى مضيف أو مقتطف أو عملية آلية. سيتم اقتراح تغييرات عليك الموافقة عليها.",
"runCommandTitle": "تشغيل الأمر",
"assistant": "مساعد الذكاء الاصطناعي",
"terminalPanelEmpty": "استفسر من المساعد عن هذا المضيف. أي تغيير في حالته يتطلب موافقتك أولاً.",
"terminalPanelPlaceholder": "اسأل عن هذا المضيف...",
"noProvidersConfigured": "لم يتم إعداد أي موفر للذكاء الاصطناعي حتى الآن. أضف واحداً في علامة تبويب مساعد الذكاء الاصطناعي.",
"runsInOpenTerminal": "يتم تشغيله في جلسة الطرفية المفتوحة لديك.",
"runInTerminal": "تشغيل",
"terminalContextPrefill": "هذا هو ناتج الطرفية الخاص بي:\n\n{{output}}\n\n",
"secrets": {
"shareApiKey": "اسمح للميزات الأخرى باستخدام مفتاح واجهة برمجة التطبيقات (API) الخاص بمزود الذكاء الاصطناعي لطلباتها الخاصة. لا يتم نسخ المفتاح مطلقًا، ويمكنك إلغاء هذا في أي وقت."
}
},
"nav": {
"ai": "الذكاء الاصطناعي"
},
"onboarding": {
"aiTitle": "مساعد الذكاء الاصطناعي",
"aiIntro": "يمكن أن يتضمن تطبيق Termix مساعدًا ذكيًا اختياريًا. يمكنك تغيير هذا الخيار لاحقًا من الإعدادات.",
"aiSkipTitle": "اتركه مغلقًا",
"aiSkipDesc": "يعمل برنامج Termix كما هو الآن. ويبقى المساعد مخفياً.",
"aiEnableTitle": "قم بتشغيله",
"aiEnableDesc": "يُضيف هذا البرنامج مساعدًا يقرأ إعداداتك ويقترح تغييرات توافق عليها. يتطلب ذلك مزود خدمة تُحدده بنفسك، مثل مزود خدمة Ollama محلي.",
"aiNote": "لن يتم إرسال أي شيء إلى أي مكان حتى تقوم بإضافة مزود خدمة وإرسال رسالة."
},
"hosts": {
"docsLink": "عرض المستندات"
},
"agents": {
"title": "AI Agent",
"permissionTitle": "Run remote AI agents",
"permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.",
"new": "New session",
"description": "Run an agent in a directory on this host using a Termix AI provider.",
"runtime": "Agent",
"provider": "Termix provider",
"chooseProvider": "Choose a compatible provider",
"model": "Model",
"directory": "Working directory",
"executable": "Executable (optional absolute path)",
"requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.",
"responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.",
"piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.",
"start": "Start agent",
"resume": "Resume",
"stop": "Stop session",
"interrupt": "Interrupt turn",
"send": "Send",
"prompt": "Ask the agent to work in this directory…",
"noSessions": "No agent sessions for this host.",
"approval": "Agent needs your decision",
"answer": "Answer (JSON for structured questions)",
"allow": "Allow once",
"deny": "Deny",
"tool": "Tool activity",
"streamFailed": "Agent stream disconnected. Reopen the session to reconnect.",
"status": {
"starting": "Starting",
"ready": "Ready",
"running": "Working",
"stopped": "Stopped",
"error": "Error"
},
"installRuntime": "Install selected agent and Node.js",
"installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.",
"installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.",
"installFailed": "Installation failed. Review the log and retry.",
"installLog": "Installation log",
"enableForwarding": "Enable Agent forwarding",
"forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.",
"forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.",
"forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.",
"search": "Search sessions",
"showArchived": "Show archived",
"sessionName": "Session name",
"rename": "Rename",
"archive": "Archive and stop",
"restore": "Restore session",
"queue": "Queued messages",
"enqueue": "Queue message",
"continueQueue": "Continue queue",
"pauseQueue": "Pause queue",
"editQueued": "Edit queued message",
"save": "Save",
"remove": "Remove",
"upload": "Upload attachment",
"remoteFile": "File path inside working directory",
"attach": "Attach file",
"attachments": "Reuse a session attachment",
"pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.",
"attachmentLimit": "Up to 4 attachments, 1 MiB each",
"review": "Git changes and worktrees",
"gitStatus": "Refresh Git status",
"diffFile": "File path to review",
"diff": "View diff",
"branch": "New branch name",
"createWorktree": "Create worktree",
"useWorktree": "New session in this worktree",
"worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.",
"clean": "No changes",
"reconnecting": "Reconnecting and restoring messages…",
"unavailable": "Session is no longer accessible. Check your permissions or sign in again.",
"shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue"
},
"plugin": {
"name": "AI Assistant"
}
}
-198
View File
@@ -1,198 +0,0 @@
{
"permissions": {
"use": {
"title": "Използвайте асистента",
"description": "Отворете асистента и го попитайте нещо."
},
"manage_providers": {
"title": "Управление на доставчици",
"description": "Добавяне, редактиране и премахване на доставчици на изкуствен интелект и техните API ключове."
},
"apply_proposals": {
"title": "Прилагане на предложения",
"description": "Нека асистентът извърши предложените от него промени."
},
"services.use": {
"title": "Използвайте изкуствен интелект от други функции",
"description": "Други функции може да извикат асистента от ваше име."
},
"secrets.share": {
"title": "Споделете API ключа",
"description": "Други функции могат да използват съхранения AI API ключ, без да го виждат."
}
},
"settings": {
"assistantUnavailable": "Асистентът с изкуствен интелект е изключен на този сървър.",
"providersNeedOptIn": "Включете AI асистента по-горе, за да добавите доставчици.",
"admin": {
"globallyEnabled": {
"label": "Асистент с изкуствен интелект",
"description": "Позволете на потребителите да включат асистента с изкуствен интелект. Докато това е изключено, асистентът е скрит и блокиран за всички."
},
"privateEndpoints": {
"label": "Разрешени частни хостове с изкуствен интелект",
"description": "Хостове във вашата частна мрежа, към които потребителите могат да насочат доставчик, като например самостоятелно хостван Ollama. Поставете по един хост на всеки ред."
}
},
"host": {
"enableAiAssistant": {
"label": "Активиране на AI асистент",
"description": "Разрешете на AI асистента да помага в терминала на този хост. Изключено по подразбиране и все още изисква AI да е активиран за вашия акаунт."
}
}
},
"ai": {
"title": "Асистент",
"send": "Изпрати",
"stop": "Спри",
"save": "Запазване",
"cancel": "Отказ",
"running": "бягане",
"approve": "Одобряване",
"reject": "Отхвърляне",
"inputPlaceholder": "Попитайте за вашите сървъри или опишете промяна, която да предложите",
"selectProvider": "Изберете доставчик",
"providerSettings": "Настройки на доставчика",
"addProvider": "Добавяне на доставчик",
"editProvider": "Редактиране на доставчика",
"removeProvider": "Премахване на доставчика",
"providerType": "Доставчик",
"providerLabel": "Име",
"providerLabelPlaceholder": "Например: Местна Олама",
"baseUrl": "Адрес на сървъра",
"apiKey": "API ключ",
"defaultModel": "Модел по подразбиране",
"defaultModelPlaceholder": "Например: llama3.1 или claude-opus-5",
"labelRequired": "Дайте име на този доставчик",
"providerSaveFailed": "Доставчикът не можа да бъде запазен",
"providerUpdated": "Доставчикът е актуализиран",
"providerDeleteFailed": "Доставчикът не можа да бъде премахнат",
"privateEndpointHint": "Адресите във вашата частна мрежа се нуждаят първо от администратор, който да разреши хоста.",
"providerOllama": "Олама",
"providerAnthropic": "Антропен",
"providerOpenai": "OpenAI",
"providerGemini": "Google Джемини",
"providerOpenaiCompatible": "Съвместим с OpenAI",
"toolArguments": "Вход",
"toolResult": "Резултат",
"statusApplied": "Приложено",
"statusRejected": "Отхвърлено",
"proposalApplied": "Промяната е приложена",
"proposalApplyFailed": "Промяната не можа да се приложи",
"proposalRejectFailed": "Промяната не можа да бъде отхвърлена",
"enableTitle": "Активирайте AI асистента",
"enableDescription": "Нека асистентът прочете настройката ви и предложи промени, които да одобрите.",
"readOnlyCommands": "Разрешаване на диагностични команди само за четене",
"readOnlyCommandsDescription": "Асистентът може да изпълни кратък списък с безопасни команди като df и uptime, без да пита всеки път. Всичко, което променя сървър, винаги се предлага първо.",
"modelDetectFailed": "Не можахме да се свържем с доставчика, така че това е общ списък. Можете да въведете име на произволен модел.",
"modelCustom": "Въведете име на модел",
"modelRefresh": "Обновяване на списъка",
"modelPlaceholder": "Изберете или въведете модел",
"attachHint": "Въведете @, за да се обърнете към хост, фрагмент или автоматизация. Предлагат се промени за одобрение.",
"runCommandTitle": "Изпълнете командата",
"assistant": "Асистент с изкуствен интелект",
"terminalPanelEmpty": "Попитайте асистента за този хост. Всичко, което променя състоянието, първо изисква вашето одобрение.",
"terminalPanelPlaceholder": "Попитайте за този домакин...",
"noProvidersConfigured": "Все още не е конфигуриран доставчик на ИИ. Добавете такъв в раздела „Асистент за ИИ“.",
"runsInOpenTerminal": "Изпълнява се във вашата отворена терминална сесия.",
"runInTerminal": "Бягане",
"terminalContextPrefill": "Ето изхода на моя терминал:\n\n{{output}}\n\n",
"secrets": {
"shareApiKey": "Позволете на други функции да използват вашия API ключ на доставчика на изкуствен интелект за свои собствени заявки. Ключът никога не се копира и можете да го отмените по всяко време."
}
},
"nav": {
"ai": "Изкуствен интелект"
},
"onboarding": {
"aiTitle": "Асистент с изкуствен интелект",
"aiIntro": "Termix може да включва опционален AI асистент. Можете да промените това по-късно в настройките си.",
"aiSkipTitle": "Остави го изключено",
"aiSkipDesc": "Termix работи както сега. Асистентът остава скрит.",
"aiEnableTitle": "Включете го",
"aiEnableDesc": "Добавя асистент, който чете настройките ви и предлага промени, които одобрявате. Необходим е доставчик, когото вие предоставяте, например местен Ollama.",
"aiNote": "Нищо не се изпраща никъде, докато не добавите доставчик и не изпратите съобщение."
},
"hosts": {
"docsLink": "Преглед на документи"
},
"agents": {
"title": "AI Agent",
"permissionTitle": "Run remote AI agents",
"permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.",
"new": "New session",
"description": "Run an agent in a directory on this host using a Termix AI provider.",
"runtime": "Agent",
"provider": "Termix provider",
"chooseProvider": "Choose a compatible provider",
"model": "Model",
"directory": "Working directory",
"executable": "Executable (optional absolute path)",
"requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.",
"responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.",
"piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.",
"start": "Start agent",
"resume": "Resume",
"stop": "Stop session",
"interrupt": "Interrupt turn",
"send": "Send",
"prompt": "Ask the agent to work in this directory…",
"noSessions": "No agent sessions for this host.",
"approval": "Agent needs your decision",
"answer": "Answer (JSON for structured questions)",
"allow": "Allow once",
"deny": "Deny",
"tool": "Tool activity",
"streamFailed": "Agent stream disconnected. Reopen the session to reconnect.",
"status": {
"starting": "Starting",
"ready": "Ready",
"running": "Working",
"stopped": "Stopped",
"error": "Error"
},
"installRuntime": "Install selected agent and Node.js",
"installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.",
"installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.",
"installFailed": "Installation failed. Review the log and retry.",
"installLog": "Installation log",
"enableForwarding": "Enable Agent forwarding",
"forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.",
"forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.",
"forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.",
"search": "Search sessions",
"showArchived": "Show archived",
"sessionName": "Session name",
"rename": "Rename",
"archive": "Archive and stop",
"restore": "Restore session",
"queue": "Queued messages",
"enqueue": "Queue message",
"continueQueue": "Continue queue",
"pauseQueue": "Pause queue",
"editQueued": "Edit queued message",
"save": "Save",
"remove": "Remove",
"upload": "Upload attachment",
"remoteFile": "File path inside working directory",
"attach": "Attach file",
"attachments": "Reuse a session attachment",
"pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.",
"attachmentLimit": "Up to 4 attachments, 1 MiB each",
"review": "Git changes and worktrees",
"gitStatus": "Refresh Git status",
"diffFile": "File path to review",
"diff": "View diff",
"branch": "New branch name",
"createWorktree": "Create worktree",
"useWorktree": "New session in this worktree",
"worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.",
"clean": "No changes",
"reconnecting": "Reconnecting and restoring messages…",
"unavailable": "Session is no longer accessible. Check your permissions or sign in again.",
"shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue"
},
"plugin": {
"name": "AI Assistant"
}
}
-198
View File
@@ -1,198 +0,0 @@
{
"permissions": {
"use": {
"title": "সহকারী ব্যবহার করুন",
"description": "অ্যাসিস্ট্যান্টটি খুলুন এবং এটিকে বিভিন্ন জিনিস জিজ্ঞাসা করুন।"
},
"manage_providers": {
"title": "সরবরাহকারীদের পরিচালনা করুন",
"description": "এআই প্রোভাইডার এবং তাদের এপিআই কীগুলো যোগ, সম্পাদনা ও অপসারণ করুন।"
},
"apply_proposals": {
"title": "প্রস্তাবগুলি প্রয়োগ করুন",
"description": "সহকারীকে তার প্রস্তাবিত পরিবর্তনগুলো কার্যকর করতে দিন।"
},
"services.use": {
"title": "অন্যান্য বৈশিষ্ট্য থেকে এআই ব্যবহার করুন",
"description": "অন্যান্য বৈশিষ্ট্য আপনার পক্ষ থেকে সহকারীকে ডাকতে পারে।"
},
"secrets.share": {
"title": "এপিআই কী শেয়ার করুন",
"description": "অন্যান্য ফিচারগুলো সংরক্ষিত এআই এপিআই কী-টি না দেখেই ব্যবহার করতে পারে।"
}
},
"settings": {
"assistantUnavailable": "এই সার্ভারে এআই অ্যাসিস্ট্যান্ট বন্ধ করা আছে।",
"providersNeedOptIn": "প্রোভাইডার যোগ করতে উপরের এআই অ্যাসিস্ট্যান্টটি চালু করুন।",
"admin": {
"globallyEnabled": {
"label": "এআই সহকারী",
"description": "ব্যবহারকারীদের এআই অ্যাসিস্ট্যান্ট চালু করার সুযোগ দিন। এটি বন্ধ থাকা অবস্থায়, অ্যাসিস্ট্যান্টটি সকলের জন্য লুকানো এবং ব্লক করা থাকবে।"
},
"privateEndpoints": {
"label": "ব্যক্তিগত এআই হোস্ট অনুমোদিত",
"description": "আপনার প্রাইভেট নেটওয়ার্কে এমন হোস্ট রাখুন, যেগুলোকে ব্যবহারকারীরা কোনো প্রোভাইডারের দিকে নির্দেশ করতে পারে, যেমন একটি সেলফ-হোস্টেড ওলামা। প্রতিটি লাইনে একটি করে হোস্ট রাখুন।"
}
},
"host": {
"enableAiAssistant": {
"label": "এআই অ্যাসিস্ট্যান্ট সক্রিয় করুন",
"description": "এই হোস্টের টার্মিনালে সাহায্য করার জন্য এআই অ্যাসিস্ট্যান্টকে অনুমতি দিন। এটি ডিফল্টরূপে বন্ধ থাকে এবং আপনার অ্যাকাউন্টের জন্য এআই চালু করা প্রয়োজন।"
}
}
},
"ai": {
"title": "সহকারী",
"send": "পাঠান",
"stop": "থামুন",
"save": "সংরক্ষণ করুন",
"cancel": "বাতিল করুন",
"running": "দৌড়ানো",
"approve": "অনুমোদন করুন",
"reject": "প্রত্যাখ্যান করুন",
"inputPlaceholder": "আপনার সার্ভারগুলো সম্পর্কে জিজ্ঞাসা করুন, অথবা কোনো পরিবর্তনের প্রস্তাব বর্ণনা করুন।",
"selectProvider": "একজন সরবরাহকারী নির্বাচন করুন",
"providerSettings": "প্রদানকারীর সেটিংস",
"addProvider": "প্রদানকারী যোগ করুন",
"editProvider": "সম্পাদনা প্রদানকারী",
"removeProvider": "প্রদানকারীকে অপসারণ করুন",
"providerType": "সরবরাহকারী",
"providerLabel": "নাম",
"providerLabelPlaceholder": "উদাহরণস্বরূপ: স্থানীয় ওলামা",
"baseUrl": "সার্ভারের ঠিকানা",
"apiKey": "এপিআই কী",
"defaultModel": "ডিফল্ট মডেল",
"defaultModelPlaceholder": "উদাহরণস্বরূপ: llama3.1 অথবা claude-opus-5",
"labelRequired": "এই সরবরাহকারীকে একটি নাম দিন",
"providerSaveFailed": "প্রদানকারীকে সংরক্ষণ করা যায়নি",
"providerUpdated": "প্রদানকারী আপডেট করেছেন",
"providerDeleteFailed": "প্রদানকারীকে সরানো যায়নি",
"privateEndpointHint": "আপনার ব্যক্তিগত নেটওয়ার্কের অ্যাড্রেসগুলো ব্যবহার করার জন্য প্রথমে একজন অ্যাডমিনিস্ট্রেটরের কাছ থেকে হোস্টটিকে অনুমতি নিতে হয়।",
"providerOllama": "ওল্লামা",
"providerAnthropic": "অ্যানথ্রোপিক",
"providerOpenai": "ওপেনএআই",
"providerGemini": "গুগল জেমিনি",
"providerOpenaiCompatible": "ওপেনএআই সামঞ্জস্যপূর্ণ",
"toolArguments": "ইনপুট",
"toolResult": "ফলাফল",
"statusApplied": "প্রয়োগ করা হয়েছে",
"statusRejected": "প্রত্যাখ্যাত",
"proposalApplied": "পরিবর্তন প্রয়োগ করা হয়েছে",
"proposalApplyFailed": "পরিবর্তনটি প্রয়োগ করা যায়নি।",
"proposalRejectFailed": "পরিবর্তনটি প্রত্যাখ্যান করা যায়নি",
"enableTitle": "এআই অ্যাসিস্ট্যান্ট সক্রিয় করুন",
"enableDescription": "সহকারীকে আপনার সেটআপটি পড়তে দিন এবং আপনার অনুমোদনের জন্য পরিবর্তনের পরামর্শ দিতে দিন।",
"readOnlyCommands": "শুধুমাত্র পঠনযোগ্য ডায়াগনস্টিক কমান্ডের অনুমতি দিন",
"readOnlyCommandsDescription": "সহকারী প্রতিবার অনুমতি না নিয়েই df এবং uptime-এর মতো কয়েকটি নিরাপদ কমান্ড চালাতে পারে। সার্ভার পরিবর্তন করে এমন যেকোনো কিছু সর্বদা প্রথমে প্রস্তাব করা হয়।",
"modelDetectFailed": "সরবরাহকারীর সাথে যোগাযোগ করা সম্ভব হয়নি, তাই এটি একটি সাধারণ তালিকা। আপনি যেকোনো মডেলের নাম টাইপ করতে পারেন।",
"modelCustom": "একটি মডেলের নাম লিখুন",
"modelRefresh": "তালিকা রিফ্রেশ করুন",
"modelPlaceholder": "একটি মডেল বেছে নিন বা টাইপ করুন",
"attachHint": "কোনো হোস্ট, স্নিপেট বা অটোমেশন উল্লেখ করতে @ টাইপ করুন। আপনার অনুমোদনের জন্য পরিবর্তনগুলো প্রস্তাব করা হবে।",
"runCommandTitle": "রান কমান্ড",
"assistant": "এআই সহকারী",
"terminalPanelEmpty": "এই হোস্ট সম্পর্কে অ্যাসিস্ট্যান্টকে জিজ্ঞাসা করুন। অবস্থা পরিবর্তনের জন্য প্রথমে আপনার অনুমোদন প্রয়োজন।",
"terminalPanelPlaceholder": "এই হোস্ট সম্পর্কে জিজ্ঞাসা করুন...",
"noProvidersConfigured": "এখনো কোনো এআই প্রোভাইডার কনফিগার করা হয়নি। এআই অ্যাসিস্ট্যান্ট ট্যাবে একটি যোগ করুন।",
"runsInOpenTerminal": "আপনার খোলা টার্মিনাল সেশনে চলে।",
"runInTerminal": "দৌড়",
"terminalContextPrefill": "আমার টার্মিনাল আউটপুট নিচে দেওয়া হলো:\n\n{{output}}\n\n",
"secrets": {
"shareApiKey": "অন্যান্য ফিচারগুলোকে তাদের নিজস্ব অনুরোধের জন্য আপনার এআই প্রোভাইডার এপিআই কী ব্যবহার করতে দিন। কী-টি কখনোই কপি করা হয় না এবং আপনি যেকোনো সময় এটি প্রত্যাহার করতে পারেন।"
}
},
"nav": {
"ai": "এআই"
},
"onboarding": {
"aiTitle": "এআই সহকারী",
"aiIntro": "টারমিক্সে একটি ঐচ্ছিক এআই অ্যাসিস্ট্যান্ট অন্তর্ভুক্ত থাকতে পারে। আপনি পরবর্তীতে আপনার সেটিংসে এটি পরিবর্তন করতে পারবেন।",
"aiSkipTitle": "এটা ছেড়ে দাও",
"aiSkipDesc": "টারমিক্স এখন যেমন কাজ করে, তেমনই কাজ করে। অ্যাসিস্ট্যান্টটি লুকানো থাকে।",
"aiEnableTitle": "এটা চালু করুন",
"aiEnableDesc": "একজন সহকারী যোগ করে যে আপনার সেটআপ পড়ে এবং আপনার অনুমোদিত পরিবর্তনের পরামর্শ দেয়। এর জন্য আপনার সরবরাহ করা একজন প্রদানকারীর প্রয়োজন, যেমন একজন স্থানীয় ওলামা।",
"aiNote": "আপনি একজন প্রোভাইডার যোগ করে মেসেজ না পাঠানো পর্যন্ত কোথাও কিছু পাঠানো হয় না।"
},
"hosts": {
"docsLink": "নথি দেখুন"
},
"agents": {
"title": "AI Agent",
"permissionTitle": "Run remote AI agents",
"permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.",
"new": "New session",
"description": "Run an agent in a directory on this host using a Termix AI provider.",
"runtime": "Agent",
"provider": "Termix provider",
"chooseProvider": "Choose a compatible provider",
"model": "Model",
"directory": "Working directory",
"executable": "Executable (optional absolute path)",
"requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.",
"responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.",
"piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.",
"start": "Start agent",
"resume": "Resume",
"stop": "Stop session",
"interrupt": "Interrupt turn",
"send": "Send",
"prompt": "Ask the agent to work in this directory…",
"noSessions": "No agent sessions for this host.",
"approval": "Agent needs your decision",
"answer": "Answer (JSON for structured questions)",
"allow": "Allow once",
"deny": "Deny",
"tool": "Tool activity",
"streamFailed": "Agent stream disconnected. Reopen the session to reconnect.",
"status": {
"starting": "Starting",
"ready": "Ready",
"running": "Working",
"stopped": "Stopped",
"error": "Error"
},
"installRuntime": "Install selected agent and Node.js",
"installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.",
"installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.",
"installFailed": "Installation failed. Review the log and retry.",
"installLog": "Installation log",
"enableForwarding": "Enable Agent forwarding",
"forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.",
"forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.",
"forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.",
"search": "Search sessions",
"showArchived": "Show archived",
"sessionName": "Session name",
"rename": "Rename",
"archive": "Archive and stop",
"restore": "Restore session",
"queue": "Queued messages",
"enqueue": "Queue message",
"continueQueue": "Continue queue",
"pauseQueue": "Pause queue",
"editQueued": "Edit queued message",
"save": "Save",
"remove": "Remove",
"upload": "Upload attachment",
"remoteFile": "File path inside working directory",
"attach": "Attach file",
"attachments": "Reuse a session attachment",
"pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.",
"attachmentLimit": "Up to 4 attachments, 1 MiB each",
"review": "Git changes and worktrees",
"gitStatus": "Refresh Git status",
"diffFile": "File path to review",
"diff": "View diff",
"branch": "New branch name",
"createWorktree": "Create worktree",
"useWorktree": "New session in this worktree",
"worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.",
"clean": "No changes",
"reconnecting": "Reconnecting and restoring messages…",
"unavailable": "Session is no longer accessible. Check your permissions or sign in again.",
"shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue"
},
"plugin": {
"name": "AI Assistant"
}
}
-198
View File
@@ -1,198 +0,0 @@
{
"permissions": {
"use": {
"title": "Utilitza l'assistent",
"description": "Obre l'assistent i fes-li preguntes."
},
"manage_providers": {
"title": "Gestiona els proveïdors",
"description": "Afegiu, editeu i elimineu els proveïdors d'IA i les seves claus API."
},
"apply_proposals": {
"title": "Aplicar propostes",
"description": "Deixa que l'assistent dugui a terme els canvis que suggereixi."
},
"services.use": {
"title": "Utilitza la IA d'altres funcions",
"description": "Altres funcions poden trucar a l'assistent en nom teu."
},
"secrets.share": {
"title": "Comparteix la clau de l'API",
"description": "Altres funcions poden utilitzar la clau de l'API d'IA emmagatzemada sense veure-la."
}
},
"settings": {
"assistantUnavailable": "L'assistent d'IA està desactivat en aquest servidor.",
"providersNeedOptIn": "Activa l'assistent d'IA que hi ha a sobre per afegir proveïdors.",
"admin": {
"globallyEnabled": {
"label": "Assistent d'IA",
"description": "Permet que els usuaris activin l'assistent d'IA. Mentre estigui desactivat, l'assistent està ocult i bloquejat per a tothom."
},
"privateEndpoints": {
"label": "Amfitrions d'IA privats permesos",
"description": "Amfitrions de la vostra xarxa privada als quals els usuaris poden dirigir un proveïdor, com ara un Ollama autoallotjat. Poseu un amfitrió a cada línia."
}
},
"host": {
"enableAiAssistant": {
"label": "Activa l'Assistent d'IA",
"description": "Permet que l'assistent d'IA ajudi al terminal d'aquest amfitrió. Desactivat per defecte, però encara cal que la IA estigui activada per al teu compte."
}
}
},
"ai": {
"title": "Assistent",
"send": "Enviar",
"stop": "Atura",
"save": "Desa",
"cancel": "Cancel·la",
"running": "corrent",
"approve": "Aprova",
"reject": "Rebutja",
"inputPlaceholder": "Pregunteu pels vostres cambrers o descriviu un canvi que vulgueu proposar",
"selectProvider": "Seleccioneu un proveïdor",
"providerSettings": "Configuració del proveïdor",
"addProvider": "Afegeix un proveïdor",
"editProvider": "Edita el proveïdor",
"removeProvider": "Elimina el proveïdor",
"providerType": "Proveïdor",
"providerLabel": "Nom",
"providerLabelPlaceholder": "Per exemple: Ollama local",
"baseUrl": "Adreça del servidor",
"apiKey": "Clau API",
"defaultModel": "Model per defecte",
"defaultModelPlaceholder": "Per exemple: llama3.1 o claude-opus-5",
"labelRequired": "Doneu un nom a aquest proveïdor",
"providerSaveFailed": "No s'ha pogut desar el proveïdor",
"providerUpdated": "Proveïdor actualitzat",
"providerDeleteFailed": "No s'ha pogut eliminar el proveïdor",
"privateEndpointHint": "Les adreces de la vostra xarxa privada necessiten primer que un administrador permeti l'accés a l'amfitrió.",
"providerOllama": "Ollama",
"providerAnthropic": "Antròpic",
"providerOpenai": "OpenAI",
"providerGemini": "Google Bessons",
"providerOpenaiCompatible": "Compatible amb OpenAI",
"toolArguments": "Entrada",
"toolResult": "Resultat",
"statusApplied": "Aplicat",
"statusRejected": "Rebutjat",
"proposalApplied": "Canvi aplicat",
"proposalApplyFailed": "No s'ha pogut aplicar el canvi",
"proposalRejectFailed": "No s'ha pogut rebutjar el canvi",
"enableTitle": "Activa l'assistent d'IA",
"enableDescription": "Deixa que l'assistent llegeixi la teva configuració i et suggereixi canvis perquè els aprovis.",
"readOnlyCommands": "Permetre ordres de diagnòstic de només lectura",
"readOnlyCommandsDescription": "L'assistent pot executar una llista curta d'ordres segures com ara df i uptime sense preguntar cada vegada. Qualsevol cosa que canviï un servidor sempre es proposa primer.",
"modelDetectFailed": "No s'ha pogut contactar amb el proveïdor, per tant, aquesta és una llista general. Podeu escriure qualsevol nom de model.",
"modelCustom": "Introduïu un nom de model",
"modelRefresh": "Actualitza la llista",
"modelPlaceholder": "Trieu o escriviu un model",
"attachHint": "Escriviu @ per fer referència a un host, fragment o automatització. Es proposen canvis perquè els aprovi.",
"runCommandTitle": "Executa l'ordre",
"assistant": "Assistent d'IA",
"terminalPanelEmpty": "Pregunta a l'assistent sobre aquest amfitrió. Qualsevol cosa que canviï d'estat necessita primer la teva aprovació.",
"terminalPanelPlaceholder": "Pregunta per aquest amfitrió...",
"noProvidersConfigured": "Encara no hi ha cap proveïdor d'IA configurat. Afegeix-ne un a la pestanya Assistent d'IA.",
"runsInOpenTerminal": "S'executa a la sessió de terminal oberta.",
"runInTerminal": "Corre",
"terminalContextPrefill": "Aquí teniu la sortida del meu terminal:\n\n{{output}}\n\n",
"secrets": {
"shareApiKey": "Permet que altres funcions utilitzin la clau de l'API del proveïdor d'IA per a les seves pròpies sol·licituds. La clau no es copia mai i pots revocar-la en qualsevol moment."
}
},
"nav": {
"ai": "IA"
},
"onboarding": {
"aiTitle": "Assistent d'IA",
"aiIntro": "Termix pot incloure un assistent d'IA opcional. Pots canviar-ho més tard a la configuració.",
"aiSkipTitle": "Deixa-ho fora",
"aiSkipDesc": "Termix funciona com ho fa ara. L'assistent roman ocult.",
"aiEnableTitle": "Engega-ho",
"aiEnableDesc": "Afegeix un assistent que llegeix la configuració i suggereix els canvis que aproveu. Necessita un proveïdor que subministreu, com ara un Ollama local.",
"aiNote": "No s'envia res enlloc fins que no afegiu un proveïdor i envieu un missatge."
},
"hosts": {
"docsLink": "Veure documents"
},
"agents": {
"title": "AI Agent",
"permissionTitle": "Run remote AI agents",
"permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.",
"new": "New session",
"description": "Run an agent in a directory on this host using a Termix AI provider.",
"runtime": "Agent",
"provider": "Termix provider",
"chooseProvider": "Choose a compatible provider",
"model": "Model",
"directory": "Working directory",
"executable": "Executable (optional absolute path)",
"requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.",
"responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.",
"piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.",
"start": "Start agent",
"resume": "Resume",
"stop": "Stop session",
"interrupt": "Interrupt turn",
"send": "Send",
"prompt": "Ask the agent to work in this directory…",
"noSessions": "No agent sessions for this host.",
"approval": "Agent needs your decision",
"answer": "Answer (JSON for structured questions)",
"allow": "Allow once",
"deny": "Deny",
"tool": "Tool activity",
"streamFailed": "Agent stream disconnected. Reopen the session to reconnect.",
"status": {
"starting": "Starting",
"ready": "Ready",
"running": "Working",
"stopped": "Stopped",
"error": "Error"
},
"installRuntime": "Install selected agent and Node.js",
"installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.",
"installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.",
"installFailed": "Installation failed. Review the log and retry.",
"installLog": "Installation log",
"enableForwarding": "Enable Agent forwarding",
"forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.",
"forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.",
"forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.",
"search": "Search sessions",
"showArchived": "Show archived",
"sessionName": "Session name",
"rename": "Rename",
"archive": "Archive and stop",
"restore": "Restore session",
"queue": "Queued messages",
"enqueue": "Queue message",
"continueQueue": "Continue queue",
"pauseQueue": "Pause queue",
"editQueued": "Edit queued message",
"save": "Save",
"remove": "Remove",
"upload": "Upload attachment",
"remoteFile": "File path inside working directory",
"attach": "Attach file",
"attachments": "Reuse a session attachment",
"pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.",
"attachmentLimit": "Up to 4 attachments, 1 MiB each",
"review": "Git changes and worktrees",
"gitStatus": "Refresh Git status",
"diffFile": "File path to review",
"diff": "View diff",
"branch": "New branch name",
"createWorktree": "Create worktree",
"useWorktree": "New session in this worktree",
"worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.",
"clean": "No changes",
"reconnecting": "Reconnecting and restoring messages…",
"unavailable": "Session is no longer accessible. Check your permissions or sign in again.",
"shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue"
},
"plugin": {
"name": "AI Assistant"
}
}
-198
View File
@@ -1,198 +0,0 @@
{
"permissions": {
"use": {
"title": "Použijte asistenta",
"description": "Otevřete asistenta a zeptejte se ho na něco."
},
"manage_providers": {
"title": "Spravovat poskytovatele",
"description": "Přidávejte, upravujte a odebírejte poskytovatele umělé inteligence a jejich klíče API."
},
"apply_proposals": {
"title": "Použít návrhy",
"description": "Nechte asistenta provést změny, které navrhuje."
},
"services.use": {
"title": "Používejte umělou inteligenci z jiných funkcí",
"description": "Jiné funkce mohou zavolat asistenta za vás."
},
"secrets.share": {
"title": "Sdílení klíče API",
"description": "Jiné funkce mohou používat uložený klíč API AI, aniž by ho viděly."
}
},
"settings": {
"assistantUnavailable": "Asistent s umělou inteligencí je na tomto serveru vypnutý.",
"providersNeedOptIn": "Zapněte výše uvedeného asistenta s umělou inteligencí a přidejte poskytovatele.",
"admin": {
"globallyEnabled": {
"label": "Asistent s umělou inteligencí",
"description": "Umožněte uživatelům zapnout asistenta s umělou inteligencí. Pokud je tato funkce vypnutá, asistent je skrytý a blokovaný pro všechny."
},
"privateEndpoints": {
"label": "Povolení soukromí hostitelé s umělou inteligencí",
"description": "Hostitelé ve vaší privátní síti, na které mohou uživatelé odkazovat poskytovatele, například Ollama hostovaná na vlastním serveru. Na každý řádek uveďte jednoho hostitele."
}
},
"host": {
"enableAiAssistant": {
"label": "Povolit asistenta s umělou inteligencí",
"description": "Povolit asistentovi s umělou inteligencí pomáhat v terminálu tohoto hostitele. Ve výchozím nastavení vypnuto, ale stále vyžaduje, aby byla pro váš účet povolena umělá inteligence."
}
}
},
"ai": {
"title": "Asistent",
"send": "Poslat",
"stop": "Zastávka",
"save": "Uložit",
"cancel": "Zrušit",
"running": "běh",
"approve": "Schvalovat",
"reject": "Odmítnout",
"inputPlaceholder": "Zeptejte se na své servery nebo popište změnu, kterou chcete navrhnout",
"selectProvider": "Vyberte poskytovatele",
"providerSettings": "Nastavení poskytovatele",
"addProvider": "Přidat poskytovatele",
"editProvider": "Upravit poskytovatele",
"removeProvider": "Odebrat poskytovatele",
"providerType": "Poskytovatel",
"providerLabel": "Jméno",
"providerLabelPlaceholder": "Například: Místní Ollama",
"baseUrl": "Adresa serveru",
"apiKey": "Klíč API",
"defaultModel": "Výchozí model",
"defaultModelPlaceholder": "Například: llama3.1 nebo claude-opus-5",
"labelRequired": "Zadejte tomuto poskytovateli název",
"providerSaveFailed": "Nepodařilo se uložit poskytovatele",
"providerUpdated": "Poskytovatel aktualizován",
"providerDeleteFailed": "Nepodařilo se odebrat poskytovatele",
"privateEndpointHint": "Adresy ve vaší privátní síti vyžadují, aby je správce nejprve povolil hostiteli.",
"providerOllama": "Ollama",
"providerAnthropic": "Antropický",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "Kompatibilní s OpenAI",
"toolArguments": "Vstup",
"toolResult": "Výsledek",
"statusApplied": "Aplikovaný",
"statusRejected": "Odmítnuto",
"proposalApplied": "Změna použita",
"proposalApplyFailed": "Změnu se nepodařilo použít",
"proposalRejectFailed": "Změnu se nepodařilo odmítnout",
"enableTitle": "Povolte asistenta s umělou inteligencí",
"enableDescription": "Nechte asistenta přečíst si vaše nastavení a navrhnout změny ke schválení.",
"readOnlyCommands": "Povolit diagnostické příkazy pouze pro čtení",
"readOnlyCommandsDescription": "Asistent může spustit krátký seznam bezpečných příkazů, jako například df a uptime, aniž by se pokaždé ptal. Vše, co mění server, je vždy navrženo jako první.",
"modelDetectFailed": "Nepodařilo se mi spojit s poskytovatelem, toto je obecný seznam. Můžete zadat libovolný název modelu.",
"modelCustom": "Zadejte název modelu",
"modelRefresh": "Obnovit seznam",
"modelPlaceholder": "Vyberte nebo zadejte model",
"attachHint": "Zadejte @ pro odkaz na hostitele, úryvek kódu nebo automatizaci. Změny jsou navrženy ke schválení.",
"runCommandTitle": "Spustit příkaz",
"assistant": "Asistent umělé inteligence",
"terminalPanelEmpty": "Zeptejte se asistenta na tohoto hostitele. Jakákoli změna stavu vyžaduje váš první souhlas.",
"terminalPanelPlaceholder": "Zeptejte se na tohoto hostitele...",
"noProvidersConfigured": "Zatím není nakonfigurován žádný poskytovatel AI. Přidejte ho na kartě Asistent AI.",
"runsInOpenTerminal": "Spustí se ve vaší otevřené terminálové relaci.",
"runInTerminal": "Běh",
"terminalContextPrefill": "Zde je můj terminálový výstup:\n\n{{output}}\n\n",
"secrets": {
"shareApiKey": "Umožněte ostatním funkcím používat váš klíč API poskytovatele AI pro jejich vlastní požadavky. Klíč se nikdy nekopíruje a jeho použití můžete kdykoli zrušit."
}
},
"nav": {
"ai": "Umělá inteligence"
},
"onboarding": {
"aiTitle": "Asistent s umělou inteligencí",
"aiIntro": "Termix může obsahovat volitelného asistenta s umělou inteligencí. Toto můžete později změnit v nastavení.",
"aiSkipTitle": "Nech to být",
"aiSkipDesc": "Termix funguje stejně jako teď. Asistent zůstává skrytý.",
"aiEnableTitle": "Zapněte to",
"aiEnableDesc": "Přidá asistenta, který si přečte vaše nastavení a navrhne změny, které schválíte. Vyžaduje poskytovatele, kterého dodáte, například místní Ollamu.",
"aiNote": "Nic se nikam neodesílá, dokud nepřidáte poskytovatele a neodešlete zprávu."
},
"hosts": {
"docsLink": "Zobrazit dokumenty"
},
"agents": {
"title": "AI Agent",
"permissionTitle": "Run remote AI agents",
"permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.",
"new": "New session",
"description": "Run an agent in a directory on this host using a Termix AI provider.",
"runtime": "Agent",
"provider": "Termix provider",
"chooseProvider": "Choose a compatible provider",
"model": "Model",
"directory": "Working directory",
"executable": "Executable (optional absolute path)",
"requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.",
"responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.",
"piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.",
"start": "Start agent",
"resume": "Resume",
"stop": "Stop session",
"interrupt": "Interrupt turn",
"send": "Send",
"prompt": "Ask the agent to work in this directory…",
"noSessions": "No agent sessions for this host.",
"approval": "Agent needs your decision",
"answer": "Answer (JSON for structured questions)",
"allow": "Allow once",
"deny": "Deny",
"tool": "Tool activity",
"streamFailed": "Agent stream disconnected. Reopen the session to reconnect.",
"status": {
"starting": "Starting",
"ready": "Ready",
"running": "Working",
"stopped": "Stopped",
"error": "Error"
},
"installRuntime": "Install selected agent and Node.js",
"installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.",
"installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.",
"installFailed": "Installation failed. Review the log and retry.",
"installLog": "Installation log",
"enableForwarding": "Enable Agent forwarding",
"forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.",
"forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.",
"forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.",
"search": "Search sessions",
"showArchived": "Show archived",
"sessionName": "Session name",
"rename": "Rename",
"archive": "Archive and stop",
"restore": "Restore session",
"queue": "Queued messages",
"enqueue": "Queue message",
"continueQueue": "Continue queue",
"pauseQueue": "Pause queue",
"editQueued": "Edit queued message",
"save": "Save",
"remove": "Remove",
"upload": "Upload attachment",
"remoteFile": "File path inside working directory",
"attach": "Attach file",
"attachments": "Reuse a session attachment",
"pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.",
"attachmentLimit": "Up to 4 attachments, 1 MiB each",
"review": "Git changes and worktrees",
"gitStatus": "Refresh Git status",
"diffFile": "File path to review",
"diff": "View diff",
"branch": "New branch name",
"createWorktree": "Create worktree",
"useWorktree": "New session in this worktree",
"worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.",
"clean": "No changes",
"reconnecting": "Reconnecting and restoring messages…",
"unavailable": "Session is no longer accessible. Check your permissions or sign in again.",
"shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue"
},
"plugin": {
"name": "AI Assistant"
}
}
-198
View File
@@ -1,198 +0,0 @@
{
"permissions": {
"use": {
"title": "Brug assistenten",
"description": "Åbn assistenten og stil den spørgsmål."
},
"manage_providers": {
"title": "Administrer udbydere",
"description": "Tilføj, rediger og fjern AI-udbyderne og deres API-nøgler."
},
"apply_proposals": {
"title": "Anvend forslag",
"description": "Lad assistenten udføre de foreslåede ændringer."
},
"services.use": {
"title": "Brug AI fra andre funktioner",
"description": "Andre funktioner kan ringe til assistenten på dine vegne."
},
"secrets.share": {
"title": "Del API-nøglen",
"description": "Andre funktioner kan bruge den gemte AI API-nøgle uden at se den."
}
},
"settings": {
"assistantUnavailable": "AI-assistenten er slået fra på denne server.",
"providersNeedOptIn": "Slå AI-assistenten ovenfor til for at tilføje udbydere.",
"admin": {
"globallyEnabled": {
"label": "AI-assistent",
"description": "Lad brugerne aktivere AI-assistenten. Når dette er slået fra, er assistenten skjult og blokeret for alle."
},
"privateEndpoints": {
"label": "Tilladte private AI-værter",
"description": "Værter på dit private netværk, som brugerne kan henvise en udbyder til, f.eks. en selvhostet Ollama. Sæt én vært på hver linje."
}
},
"host": {
"enableAiAssistant": {
"label": "Aktivér AI-assistent",
"description": "Tillad AI-assistenten at hjælpe i denne værts terminal. Deaktiveret som standard, og kræver stadig, at AI er aktiveret for din konto."
}
}
},
"ai": {
"title": "Assistent",
"send": "Sende",
"stop": "Stop",
"save": "Spare",
"cancel": "Ophæve",
"running": "løb",
"approve": "Godkende",
"reject": "Afvise",
"inputPlaceholder": "Spørg om dine servere, eller beskriv en ændring, du vil foreslå",
"selectProvider": "Vælg en udbyder",
"providerSettings": "Udbyderindstillinger",
"addProvider": "Tilføj udbyder",
"editProvider": "Rediger udbyder",
"removeProvider": "Fjern udbyder",
"providerType": "Udbyder",
"providerLabel": "Navn",
"providerLabelPlaceholder": "For eksempel: Lokal Ollama",
"baseUrl": "Serveradresse",
"apiKey": "API-nøgle",
"defaultModel": "Standardmodel",
"defaultModelPlaceholder": "For eksempel: llama3.1 eller claude-opus-5",
"labelRequired": "Giv denne udbyder et navn",
"providerSaveFailed": "Udbyderen kunne ikke gemmes",
"providerUpdated": "Udbyder opdateret",
"providerDeleteFailed": "Udbyderen kunne ikke fjernes",
"privateEndpointHint": "Adresser på dit private netværk kræver først tilladelse fra en administrator til værten.",
"providerOllama": "Ollama",
"providerAnthropic": "Antropisk",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "OpenAI-kompatibel",
"toolArguments": "Input",
"toolResult": "Resultat",
"statusApplied": "Anvendt",
"statusRejected": "Afvist",
"proposalApplied": "Ændring anvendt",
"proposalApplyFailed": "Ændringen kunne ikke anvendes",
"proposalRejectFailed": "Kunne ikke afvise ændringen",
"enableTitle": "Aktivér AI-assistenten",
"enableDescription": "Lad assistenten læse din opsætning og foreslå ændringer, som du kan godkende.",
"readOnlyCommands": "Tillad skrivebeskyttede diagnosticeringskommandoer",
"readOnlyCommandsDescription": "Assistenten kan køre en kort liste med sikre kommandoer som f.eks. df og uptime uden at spørge hver gang. Alt, der ændrer en server, foreslås altid først.",
"modelDetectFailed": "Kunne ikke få kontakt med udbyderen, så dette er en generel liste. Du kan skrive et hvilket som helst modelnavn.",
"modelCustom": "Indtast et modelnavn",
"modelRefresh": "Opdater listen",
"modelPlaceholder": "Vælg eller skriv en model",
"attachHint": "Skriv @ for at referere til en vært, et kodestykke eller en automatisering. Ændringer foreslås, som du skal godkende.",
"runCommandTitle": "Kør kommando",
"assistant": "AI-assistent",
"terminalPanelEmpty": "Spørg assistenten om denne vært. Alt, der ændrer status, kræver din godkendelse først.",
"terminalPanelPlaceholder": "Spørg om denne vært...",
"noProvidersConfigured": "Der er endnu ikke konfigureret nogen AI-udbyder. Tilføj en i fanen AI-assistent.",
"runsInOpenTerminal": "Kører i din åbne terminalsession.",
"runInTerminal": "Løbe",
"terminalContextPrefill": "Her er mit terminaloutput:\n\n{{output}}\n\n",
"secrets": {
"shareApiKey": "Lad andre funktioner bruge din AI-udbyders API-nøgle til deres egne anmodninger. Nøglen kopieres aldrig, og du kan til enhver tid tilbagekalde den."
}
},
"nav": {
"ai": "AI"
},
"onboarding": {
"aiTitle": "AI-assistent",
"aiIntro": "Termix kan inkludere en valgfri AI-assistent. Du kan ændre dette senere i dine indstillinger.",
"aiSkipTitle": "Lad det være slukket",
"aiSkipDesc": "Termix fungerer som den gør nu. Assistenten forbliver skjult.",
"aiEnableTitle": "Tænd den",
"aiEnableDesc": "Tilføjer en assistent, der læser din opsætning og foreslår ændringer, du godkender. Kræver en udbyder, du angiver, f.eks. en lokal Ollama.",
"aiNote": "Intet sendes nogen steder, før du tilføjer en udbyder og sender en besked."
},
"hosts": {
"docsLink": "Se dokumenter"
},
"agents": {
"title": "AI Agent",
"permissionTitle": "Run remote AI agents",
"permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.",
"new": "New session",
"description": "Run an agent in a directory on this host using a Termix AI provider.",
"runtime": "Agent",
"provider": "Termix provider",
"chooseProvider": "Choose a compatible provider",
"model": "Model",
"directory": "Working directory",
"executable": "Executable (optional absolute path)",
"requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.",
"responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.",
"piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.",
"start": "Start agent",
"resume": "Resume",
"stop": "Stop session",
"interrupt": "Interrupt turn",
"send": "Send",
"prompt": "Ask the agent to work in this directory…",
"noSessions": "No agent sessions for this host.",
"approval": "Agent needs your decision",
"answer": "Answer (JSON for structured questions)",
"allow": "Allow once",
"deny": "Deny",
"tool": "Tool activity",
"streamFailed": "Agent stream disconnected. Reopen the session to reconnect.",
"status": {
"starting": "Starting",
"ready": "Ready",
"running": "Working",
"stopped": "Stopped",
"error": "Error"
},
"installRuntime": "Install selected agent and Node.js",
"installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.",
"installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.",
"installFailed": "Installation failed. Review the log and retry.",
"installLog": "Installation log",
"enableForwarding": "Enable Agent forwarding",
"forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.",
"forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.",
"forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.",
"search": "Search sessions",
"showArchived": "Show archived",
"sessionName": "Session name",
"rename": "Rename",
"archive": "Archive and stop",
"restore": "Restore session",
"queue": "Queued messages",
"enqueue": "Queue message",
"continueQueue": "Continue queue",
"pauseQueue": "Pause queue",
"editQueued": "Edit queued message",
"save": "Save",
"remove": "Remove",
"upload": "Upload attachment",
"remoteFile": "File path inside working directory",
"attach": "Attach file",
"attachments": "Reuse a session attachment",
"pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.",
"attachmentLimit": "Up to 4 attachments, 1 MiB each",
"review": "Git changes and worktrees",
"gitStatus": "Refresh Git status",
"diffFile": "File path to review",
"diff": "View diff",
"branch": "New branch name",
"createWorktree": "Create worktree",
"useWorktree": "New session in this worktree",
"worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.",
"clean": "No changes",
"reconnecting": "Reconnecting and restoring messages…",
"unavailable": "Session is no longer accessible. Check your permissions or sign in again.",
"shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue"
},
"plugin": {
"name": "AI Assistant"
}
}
-198
View File
@@ -1,198 +0,0 @@
{
"permissions": {
"use": {
"title": "Nutzen Sie den Assistenten",
"description": "Öffne den Assistenten und stelle ihm Fragen."
},
"manage_providers": {
"title": "Anbieter verwalten",
"description": "KI-Anbieter und ihre API-Schlüssel hinzufügen, bearbeiten und entfernen."
},
"apply_proposals": {
"title": "Vorschläge einreichen",
"description": "Lassen Sie den Assistenten die von ihm vorgeschlagenen Änderungen durchführen."
},
"services.use": {
"title": "Nutzen Sie KI aus anderen Funktionen",
"description": "Andere Funktionen rufen möglicherweise in Ihrem Namen den Assistenten auf."
},
"secrets.share": {
"title": "Teilen Sie den API-Schlüssel",
"description": "Andere Funktionen verwenden möglicherweise den gespeicherten KI-API-Schlüssel, ohne ihn anzuzeigen."
}
},
"settings": {
"assistantUnavailable": "Der KI-Assistent ist auf diesem Server deaktiviert.",
"providersNeedOptIn": "Aktivieren Sie den obenstehenden KI-Assistenten, um Anbieter hinzuzufügen.",
"admin": {
"globallyEnabled": {
"label": "KI-Assistent",
"description": "Ermöglichen Sie den Nutzern, den KI-Assistenten zu aktivieren. Solange diese Option deaktiviert ist, ist der Assistent für alle ausgeblendet und gesperrt."
},
"privateEndpoints": {
"label": "Zulässige private KI-Hosts",
"description": "Hosts in Ihrem privaten Netzwerk, auf die Benutzer einen Provider verweisen können, wie z. B. ein selbst gehostetes Ollama. Fügen Sie in jede Zeile einen Host ein."
}
},
"host": {
"enableAiAssistant": {
"label": "KI-Assistent aktivieren",
"description": "Erlauben Sie dem KI-Assistenten, im Terminal dieses Hosts Hilfe zu leisten. Standardmäßig deaktiviert; die KI muss weiterhin für Ihr Konto aktiviert sein."
}
}
},
"ai": {
"title": "Assistent",
"send": "Schicken",
"stop": "Stoppen",
"save": "Speichern",
"cancel": "Abbrechen",
"running": "laufend",
"approve": "Genehmigen",
"reject": "Ablehnen",
"inputPlaceholder": "Erkundigen Sie sich nach Ihren Servern oder beschreiben Sie einen Änderungsvorschlag.",
"selectProvider": "Wählen Sie einen Anbieter aus",
"providerSettings": "Anbietereinstellungen",
"addProvider": "Anbieter hinzufügen",
"editProvider": "Anbieter bearbeiten",
"removeProvider": "Anbieter entfernen",
"providerType": "Anbieter",
"providerLabel": "Name",
"providerLabelPlaceholder": "Zum Beispiel: Lokaler Ollama",
"baseUrl": "Serveradresse",
"apiKey": "API-Schlüssel",
"defaultModel": "Standardmodell",
"defaultModelPlaceholder": "Zum Beispiel: llama3.1 oder claude-opus-5",
"labelRequired": "Geben Sie diesem Anbieter einen Namen",
"providerSaveFailed": "Der Anbieter konnte nicht gespeichert werden.",
"providerUpdated": "Anbieter aktualisiert",
"providerDeleteFailed": "Der Anbieter konnte nicht entfernt werden.",
"privateEndpointHint": "Adressen in Ihrem privaten Netzwerk benötigen eine vorherige Genehmigung durch einen Administrator.",
"providerOllama": "Ollama",
"providerAnthropic": "Anthropisch",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "OpenAI-kompatibel",
"toolArguments": "Eingabe",
"toolResult": "Ergebnis",
"statusApplied": "Angewandt",
"statusRejected": "Abgelehnt",
"proposalApplied": "Änderung angewendet",
"proposalApplyFailed": "Die Änderung konnte nicht angewendet werden.",
"proposalRejectFailed": "Die Änderung konnte nicht abgelehnt werden.",
"enableTitle": "Aktivieren Sie den KI-Assistenten",
"enableDescription": "Lassen Sie den Assistenten Ihre Einstellungen lesen und Ihnen Änderungen zur Genehmigung vorschlagen.",
"readOnlyCommands": "Zulassen von schreibgeschützten Diagnosebefehlen",
"readOnlyCommandsDescription": "Der Assistent kann eine kurze Liste sicherer Befehle wie df und uptime ausführen, ohne jedes Mal nachzufragen. Alle Änderungen, die einen Server betreffen, werden immer zuerst vorgeschlagen.",
"modelDetectFailed": "Der Anbieter konnte nicht erreicht werden, daher handelt es sich um eine allgemeine Liste. Sie können einen beliebigen Modellnamen eingeben.",
"modelCustom": "Geben Sie einen Modellnamen ein",
"modelRefresh": "Liste aktualisieren",
"modelPlaceholder": "Wählen Sie ein Modell aus oder geben Sie es ein.",
"attachHint": "Geben Sie @ ein, um auf einen Host, ein Code-Snippet oder eine Automatisierung zu verweisen. Die Änderungen werden Ihnen zur Genehmigung vorgeschlagen.",
"runCommandTitle": "Befehl ausführen",
"assistant": "KI-Assistent",
"terminalPanelEmpty": "Fragen Sie den Assistenten nach diesem Gastgeber. Jede Statusänderung bedarf Ihrer vorherigen Zustimmung.",
"terminalPanelPlaceholder": "Erkundigen Sie sich nach diesem Gastgeber...",
"noProvidersConfigured": "Es ist noch kein KI-Anbieter konfiguriert. Fügen Sie einen im Tab „KI-Assistent“ hinzu.",
"runsInOpenTerminal": "Wird in Ihrer geöffneten Terminal-Sitzung ausgeführt.",
"runInTerminal": "Ausführen",
"terminalContextPrefill": "Hier ist meine Terminalausgabe:\n\n{{output}}\n\n",
"secrets": {
"shareApiKey": "Erlauben Sie anderen Funktionen, Ihren API-Schlüssel Ihres KI-Anbieters für ihre eigenen Anfragen zu verwenden. Der Schlüssel wird niemals kopiert, und Sie können diese Berechtigung jederzeit widerrufen."
}
},
"nav": {
"ai": "KI"
},
"onboarding": {
"aiTitle": "KI-Assistent",
"aiIntro": "Termix kann optional einen KI-Assistenten beinhalten. Sie können dies später in Ihren Einstellungen ändern.",
"aiSkipTitle": "Lass es weg",
"aiSkipDesc": "Termix funktioniert wie bisher. Der Assistent bleibt verborgen.",
"aiEnableTitle": "Schalte es ein",
"aiEnableDesc": "Fügt einen Assistenten hinzu, der Ihre Einstellungen analysiert und Ihnen von Ihnen genehmigte Änderungen vorschlägt. Benötigt einen von Ihnen angegebenen Anbieter, z. B. einen lokalen Ollama-Händler.",
"aiNote": "Es wird nichts gesendet, bis Sie einen Anbieter hinzufügen und eine Nachricht senden."
},
"hosts": {
"docsLink": "Dokumentation anzeigen"
},
"agents": {
"title": "AI Agent",
"permissionTitle": "Run remote AI agents",
"permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.",
"new": "New session",
"description": "Run an agent in a directory on this host using a Termix AI provider.",
"runtime": "Agent",
"provider": "Termix provider",
"chooseProvider": "Choose a compatible provider",
"model": "Model",
"directory": "Working directory",
"executable": "Executable (optional absolute path)",
"requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.",
"responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.",
"piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.",
"start": "Start agent",
"resume": "Resume",
"stop": "Stop session",
"interrupt": "Interrupt turn",
"send": "Send",
"prompt": "Ask the agent to work in this directory…",
"noSessions": "No agent sessions for this host.",
"approval": "Agent needs your decision",
"answer": "Answer (JSON for structured questions)",
"allow": "Allow once",
"deny": "Deny",
"tool": "Tool activity",
"streamFailed": "Agent stream disconnected. Reopen the session to reconnect.",
"status": {
"starting": "Starting",
"ready": "Ready",
"running": "Working",
"stopped": "Stopped",
"error": "Error"
},
"installRuntime": "Install selected agent and Node.js",
"installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.",
"installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.",
"installFailed": "Installation failed. Review the log and retry.",
"installLog": "Installation log",
"enableForwarding": "Enable Agent forwarding",
"forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.",
"forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.",
"forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.",
"search": "Search sessions",
"showArchived": "Show archived",
"sessionName": "Session name",
"rename": "Rename",
"archive": "Archive and stop",
"restore": "Restore session",
"queue": "Queued messages",
"enqueue": "Queue message",
"continueQueue": "Continue queue",
"pauseQueue": "Pause queue",
"editQueued": "Edit queued message",
"save": "Save",
"remove": "Remove",
"upload": "Upload attachment",
"remoteFile": "File path inside working directory",
"attach": "Attach file",
"attachments": "Reuse a session attachment",
"pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.",
"attachmentLimit": "Up to 4 attachments, 1 MiB each",
"review": "Git changes and worktrees",
"gitStatus": "Refresh Git status",
"diffFile": "File path to review",
"diff": "View diff",
"branch": "New branch name",
"createWorktree": "Create worktree",
"useWorktree": "New session in this worktree",
"worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.",
"clean": "No changes",
"reconnecting": "Reconnecting and restoring messages…",
"unavailable": "Session is no longer accessible. Check your permissions or sign in again.",
"shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue"
},
"plugin": {
"name": "AI Assistant"
}
}
-198
View File
@@ -1,198 +0,0 @@
{
"permissions": {
"use": {
"title": "Χρησιμοποιήστε τον βοηθό",
"description": "Άνοιξε τον βοηθό και ρώτα τον πράγματα."
},
"manage_providers": {
"title": "Διαχείριση παρόχων",
"description": "Προσθήκη, επεξεργασία και κατάργηση των παρόχων τεχνητής νοημοσύνης και των κλειδιών API τους."
},
"apply_proposals": {
"title": "Εφαρμογή προτάσεων",
"description": "Αφήστε τον βοηθό να εκτελέσει τις αλλαγές που προτείνει."
},
"services.use": {
"title": "Χρησιμοποιήστε την Τεχνητή Νοημοσύνη από άλλες λειτουργίες",
"description": "Άλλες λειτουργίες ενδέχεται να καλούν τον βοηθό εκ μέρους σας."
},
"secrets.share": {
"title": "Κοινή χρήση του κλειδιού API",
"description": "Άλλες λειτουργίες ενδέχεται να χρησιμοποιούν το αποθηκευμένο κλειδί API AI χωρίς να το βλέπουν."
}
},
"settings": {
"assistantUnavailable": "Ο βοηθός τεχνητής νοημοσύνης είναι απενεργοποιημένος σε αυτόν τον διακομιστή.",
"providersNeedOptIn": "Ενεργοποιήστε τον βοηθό τεχνητής νοημοσύνης παραπάνω για να προσθέσετε παρόχους.",
"admin": {
"globallyEnabled": {
"label": "Βοηθός Τεχνητής Νοημοσύνης",
"description": "Επιτρέψτε στους χρήστες να ενεργοποιήσουν τον βοηθό τεχνητής νοημοσύνης. Όσο είναι απενεργοποιημένος, ο βοηθός είναι κρυφός και αποκλεισμένος για όλους."
},
"privateEndpoints": {
"label": "Επιτρεπόμενοι ιδιωτικοί κεντρικοί υπολογιστές τεχνητής νοημοσύνης",
"description": "Κεντρικοί υπολογιστές στο ιδιωτικό σας δίκτυο στους οποίους οι χρήστες μπορούν να υποδείξουν έναν πάροχο, όπως ένα αυτο-φιλοξενούμενο Ollama. Τοποθετήστε έναν κεντρικό υπολογιστή σε κάθε γραμμή."
}
},
"host": {
"enableAiAssistant": {
"label": "Ενεργοποίηση Βοηθού Τεχνητής Νοημοσύνης",
"description": "Επιτρέψτε στον βοηθό τεχνητής νοημοσύνης να βοηθά στο τερματικό αυτού του κεντρικού υπολογιστή. Απενεργοποιημένο από προεπιλογή και εξακολουθεί να χρειάζεται ενεργοποιημένη την τεχνητή νοημοσύνη για τον λογαριασμό σας."
}
}
},
"ai": {
"title": "Βοηθός",
"send": "Στέλνω",
"stop": "Στάση",
"save": "Εκτός",
"cancel": "Ματαίωση",
"running": "τρέξιμο",
"approve": "Εγκρίνω",
"reject": "Απορρίπτω",
"inputPlaceholder": "Ρωτήστε για τους διακομιστές σας ή περιγράψτε μια αλλαγή που προτείνετε",
"selectProvider": "Επιλέξτε έναν πάροχο",
"providerSettings": "Ρυθμίσεις παρόχου",
"addProvider": "Προσθήκη παρόχου",
"editProvider": "Επεξεργασία παρόχου",
"removeProvider": "Κατάργηση παρόχου",
"providerType": "Προμηθευτής",
"providerLabel": "Ονομα",
"providerLabelPlaceholder": "Για παράδειγμα: Τοπικό Ollama",
"baseUrl": "Διεύθυνση διακομιστή",
"apiKey": "Κλειδί API",
"defaultModel": "Προεπιλεγμένο μοντέλο",
"defaultModelPlaceholder": "Για παράδειγμα: llama3.1 ή claude-opus-5",
"labelRequired": "Δώστε σε αυτόν τον πάροχο ένα όνομα",
"providerSaveFailed": "Δεν ήταν δυνατή η αποθήκευση του παρόχου",
"providerUpdated": "Ο πάροχος ενημερώθηκε",
"providerDeleteFailed": "Δεν ήταν δυνατή η κατάργηση του παρόχου",
"privateEndpointHint": "Οι διευθύνσεις στο ιδιωτικό σας δίκτυο χρειάζονται πρώτα έναν διαχειριστή για να επιτρέψει στον κεντρικό υπολογιστή.",
"providerOllama": "Ολάμα",
"providerAnthropic": "Ανθρώπινο",
"providerOpenai": "Ανοιχτή Τεχνητή Νοημοσύνη",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "Συμβατό με OpenAI",
"toolArguments": "Εισαγωγή",
"toolResult": "Αποτέλεσμα",
"statusApplied": "Εφαρμοσμένος",
"statusRejected": "Απορρίφθηκε",
"proposalApplied": "Η αλλαγή εφαρμόστηκε",
"proposalApplyFailed": "Δεν ήταν δυνατή η εφαρμογή της αλλαγής",
"proposalRejectFailed": "Δεν ήταν δυνατή η απόρριψη της αλλαγής",
"enableTitle": "Ενεργοποίηση του βοηθού τεχνητής νοημοσύνης",
"enableDescription": "Αφήστε τον βοηθό να διαβάσει τη ρύθμισή σας και να σας προτείνει αλλαγές για έγκριση.",
"readOnlyCommands": "Να επιτρέπονται διαγνωστικές εντολές μόνο για ανάγνωση",
"readOnlyCommandsDescription": "Ο βοηθός μπορεί να εκτελέσει μια σύντομη λίστα ασφαλών εντολών όπως df και uptime χωρίς να ρωτάει κάθε φορά. Οτιδήποτε αλλάζει έναν διακομιστή προτείνεται πάντα πρώτο.",
"modelDetectFailed": "Δεν ήταν δυνατή η επικοινωνία με τον πάροχο, επομένως αυτή είναι μια γενική λίστα. Μπορείτε να πληκτρολογήσετε οποιοδήποτε όνομα μοντέλου.",
"modelCustom": "Εισαγάγετε ένα όνομα μοντέλου",
"modelRefresh": "Ανανέωση λίστας",
"modelPlaceholder": "Επιλέξτε ή πληκτρολογήστε ένα μοντέλο",
"attachHint": "Πληκτρολογήστε @ για να αναφέρετε έναν κεντρικό υπολογιστή, ένα απόσπασμα κώδικα ή έναν αυτοματισμό. Προτείνονται αλλαγές για έγκριση.",
"runCommandTitle": "Εκτέλεση εντολής",
"assistant": "Βοηθός Τεχνητής Νοημοσύνης",
"terminalPanelEmpty": "Ρωτήστε τον βοηθό σχετικά με αυτόν τον οικοδεσπότη. Οτιδήποτε αλλάζει κατάσταση χρειάζεται πρώτα την έγκρισή σας.",
"terminalPanelPlaceholder": "Ρωτήστε για αυτόν τον οικοδεσπότη...",
"noProvidersConfigured": "Δεν έχει διαμορφωθεί ακόμη πάροχος τεχνητής νοημοσύνης. Προσθέστε έναν στην καρτέλα βοηθού τεχνητής νοημοσύνης.",
"runsInOpenTerminal": "Εκτελείται στην ανοιχτή συνεδρία τερματικού σας.",
"runInTerminal": "Τρέξιμο",
"terminalContextPrefill": "Εδώ είναι η έξοδος του τερματικού μου:\n\n{{output}}\n\n",
"secrets": {
"shareApiKey": "Επιτρέψτε σε άλλες λειτουργίες να χρησιμοποιούν το κλειδί API του παρόχου τεχνητής νοημοσύνης για τα δικά τους αιτήματα. Το κλειδί δεν αντιγράφεται ποτέ και μπορείτε να το ανακαλέσετε ανά πάσα στιγμή."
}
},
"nav": {
"ai": "Όλα συμπεριλαμβάνονται"
},
"onboarding": {
"aiTitle": "Βοηθός Τεχνητής Νοημοσύνης",
"aiIntro": "Το Termix μπορεί να περιλαμβάνει έναν προαιρετικό βοηθό τεχνητής νοημοσύνης. Μπορείτε να το αλλάξετε αυτό αργότερα στις ρυθμίσεις σας.",
"aiSkipTitle": "Αφήστε το μακριά",
"aiSkipDesc": "Το Termix λειτουργεί όπως τώρα. Ο βοηθός παραμένει κρυφός.",
"aiEnableTitle": "Ενεργοποίησέ το",
"aiEnableDesc": "Προσθέτει έναν βοηθό που διαβάζει τη ρύθμισή σας και προτείνει αλλαγές που εγκρίνετε. Χρειάζεται έναν πάροχο που παρέχετε, όπως ένα τοπικό Ollama.",
"aiNote": "Τίποτα δεν αποστέλλεται πουθενά μέχρι να προσθέσετε έναν πάροχο και να στείλετε ένα μήνυμα."
},
"hosts": {
"docsLink": "Προβολή εγγράφων"
},
"agents": {
"title": "AI Agent",
"permissionTitle": "Run remote AI agents",
"permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.",
"new": "New session",
"description": "Run an agent in a directory on this host using a Termix AI provider.",
"runtime": "Agent",
"provider": "Termix provider",
"chooseProvider": "Choose a compatible provider",
"model": "Model",
"directory": "Working directory",
"executable": "Executable (optional absolute path)",
"requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.",
"responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.",
"piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.",
"start": "Start agent",
"resume": "Resume",
"stop": "Stop session",
"interrupt": "Interrupt turn",
"send": "Send",
"prompt": "Ask the agent to work in this directory…",
"noSessions": "No agent sessions for this host.",
"approval": "Agent needs your decision",
"answer": "Answer (JSON for structured questions)",
"allow": "Allow once",
"deny": "Deny",
"tool": "Tool activity",
"streamFailed": "Agent stream disconnected. Reopen the session to reconnect.",
"status": {
"starting": "Starting",
"ready": "Ready",
"running": "Working",
"stopped": "Stopped",
"error": "Error"
},
"installRuntime": "Install selected agent and Node.js",
"installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.",
"installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.",
"installFailed": "Installation failed. Review the log and retry.",
"installLog": "Installation log",
"enableForwarding": "Enable Agent forwarding",
"forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.",
"forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.",
"forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.",
"search": "Search sessions",
"showArchived": "Show archived",
"sessionName": "Session name",
"rename": "Rename",
"archive": "Archive and stop",
"restore": "Restore session",
"queue": "Queued messages",
"enqueue": "Queue message",
"continueQueue": "Continue queue",
"pauseQueue": "Pause queue",
"editQueued": "Edit queued message",
"save": "Save",
"remove": "Remove",
"upload": "Upload attachment",
"remoteFile": "File path inside working directory",
"attach": "Attach file",
"attachments": "Reuse a session attachment",
"pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.",
"attachmentLimit": "Up to 4 attachments, 1 MiB each",
"review": "Git changes and worktrees",
"gitStatus": "Refresh Git status",
"diffFile": "File path to review",
"diff": "View diff",
"branch": "New branch name",
"createWorktree": "Create worktree",
"useWorktree": "New session in this worktree",
"worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.",
"clean": "No changes",
"reconnecting": "Reconnecting and restoring messages…",
"unavailable": "Session is no longer accessible. Check your permissions or sign in again.",
"shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue"
},
"plugin": {
"name": "AI Assistant"
}
}
-198
View File
@@ -1,198 +0,0 @@
{
"permissions": {
"use": {
"title": "Utilice el asistente",
"description": "Abre el asistente y hazle preguntas."
},
"manage_providers": {
"title": "Gestionar proveedores",
"description": "Agregue, edite y elimine los proveedores de IA y sus claves API."
},
"apply_proposals": {
"title": "Presentar propuestas",
"description": "Deja que el asistente realice los cambios que sugiere."
},
"services.use": {
"title": "Utilizar la IA de otras funciones",
"description": "Otras funciones pueden llamar al asistente en tu nombre."
},
"secrets.share": {
"title": "Comparta la clave API",
"description": "Es posible que otras funciones utilicen la clave API de IA almacenada sin tenerla en cuenta."
}
},
"settings": {
"assistantUnavailable": "El asistente de IA está desactivado en este servidor.",
"providersNeedOptIn": "Activa el asistente de IA que aparece arriba para añadir proveedores.",
"admin": {
"globallyEnabled": {
"label": "Asistente de IA",
"description": "Permita que los usuarios activen el asistente de IA. Mientras esté desactivado, el asistente permanecerá oculto y bloqueado para todos."
},
"privateEndpoints": {
"label": "Hosts de IA privados permitidos",
"description": "Hosts en tu red privada a los que los usuarios pueden dirigir un proveedor, como por ejemplo un servidor Ollama autogestionado. Coloca un host en cada línea."
}
},
"host": {
"enableAiAssistant": {
"label": "Habilitar el asistente de IA",
"description": "Permite que el asistente de IA te ayude en la terminal de este host. Está desactivado por defecto y aún así necesitas tener la IA habilitada para tu cuenta."
}
}
},
"ai": {
"title": "Asistente",
"send": "Enviar",
"stop": "Parada",
"save": "Guardar",
"cancel": "Cancelar",
"running": "en ejecución",
"approve": "Aprobar",
"reject": "Rechazar",
"inputPlaceholder": "Pregunte sobre sus servidores o describa un cambio para proponer.",
"selectProvider": "Seleccione un proveedor",
"providerSettings": "Configuración del proveedor",
"addProvider": "Agregar proveedor",
"editProvider": "Editar proveedor",
"removeProvider": "Eliminar proveedor",
"providerType": "Proveedor",
"providerLabel": "Nombre",
"providerLabelPlaceholder": "Por ejemplo: Ollama local",
"baseUrl": "Dirección del servidor",
"apiKey": "clave API",
"defaultModel": "Modelo predeterminado",
"defaultModelPlaceholder": "Por ejemplo: llama3.1 o claude-opus-5",
"labelRequired": "Dale un nombre a este proveedor.",
"providerSaveFailed": "No se pudo guardar el proveedor",
"providerUpdated": "Proveedor actualizado",
"providerDeleteFailed": "No se pudo eliminar al proveedor.",
"privateEndpointHint": "Las direcciones de su red privada necesitan que un administrador autorice el acceso al host previamente.",
"providerOllama": "Ollama",
"providerAnthropic": "Antrópico",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "Compatible con OpenAI",
"toolArguments": "Entrada",
"toolResult": "Resultado",
"statusApplied": "Aplicado",
"statusRejected": "Rechazado",
"proposalApplied": "Cambio aplicado",
"proposalApplyFailed": "No se pudo aplicar el cambio.",
"proposalRejectFailed": "No se pudo rechazar el cambio",
"enableTitle": "Habilitar el asistente de IA",
"enableDescription": "Deja que el asistente lea tu configuración y te sugiera cambios para que los apruebes.",
"readOnlyCommands": "Permitir comandos de diagnóstico de solo lectura",
"readOnlyCommandsDescription": "El asistente puede ejecutar una breve lista de comandos seguros, como df y uptime, sin preguntar cada vez. Cualquier cambio que afecte al servidor siempre se propone primero.",
"modelDetectFailed": "No se pudo contactar con el proveedor, por lo que esta es una lista general. Puede escribir cualquier nombre de modelo.",
"modelCustom": "Introduzca un nombre de modelo",
"modelRefresh": "Actualizar lista",
"modelPlaceholder": "Seleccione o escriba un modelo.",
"attachHint": "Escribe @ para hacer referencia a un host, fragmento de código o automatización. Se proponen cambios para su aprobación.",
"runCommandTitle": "Ejecutar comando",
"assistant": "Asistente de IA",
"terminalPanelEmpty": "Consulta con el asistente sobre este anfitrión. Cualquier cambio de estado requiere tu aprobación previa.",
"terminalPanelPlaceholder": "Pregunta por este anfitrión...",
"noProvidersConfigured": "Aún no hay ningún proveedor de IA configurado. Añada uno en la pestaña del asistente de IA.",
"runsInOpenTerminal": "Se ejecuta en tu sesión de terminal abierta.",
"runInTerminal": "Ejecutar",
"terminalContextPrefill": "Aquí está la salida de mi terminal:\n\n{{output}}\n\n",
"secrets": {
"shareApiKey": "Permite que otras funciones utilicen la clave API de tu proveedor de IA para sus propias solicitudes. La clave nunca se copia y puedes revocarla en cualquier momento."
}
},
"nav": {
"ai": "AI"
},
"onboarding": {
"aiTitle": "Asistente de IA",
"aiIntro": "Termix puede incluir un asistente de IA opcional. Puedes cambiar esto más adelante en la configuración.",
"aiSkipTitle": "Déjalo apagado",
"aiSkipDesc": "Termix funciona como hasta ahora. El asistente permanece oculto.",
"aiEnableTitle": "Enciéndelo",
"aiEnableDesc": "Agrega un asistente que lee tu configuración y sugiere cambios que apruebas. Requiere un proveedor que tú proporciones, como un Ollama local.",
"aiNote": "No se envía nada a ningún sitio hasta que añadas un proveedor y envíes un mensaje."
},
"hosts": {
"docsLink": "Ver documentación"
},
"agents": {
"title": "AI Agent",
"permissionTitle": "Run remote AI agents",
"permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.",
"new": "New session",
"description": "Run an agent in a directory on this host using a Termix AI provider.",
"runtime": "Agent",
"provider": "Termix provider",
"chooseProvider": "Choose a compatible provider",
"model": "Model",
"directory": "Working directory",
"executable": "Executable (optional absolute path)",
"requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.",
"responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.",
"piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.",
"start": "Start agent",
"resume": "Resume",
"stop": "Stop session",
"interrupt": "Interrupt turn",
"send": "Send",
"prompt": "Ask the agent to work in this directory…",
"noSessions": "No agent sessions for this host.",
"approval": "Agent needs your decision",
"answer": "Answer (JSON for structured questions)",
"allow": "Allow once",
"deny": "Deny",
"tool": "Tool activity",
"streamFailed": "Agent stream disconnected. Reopen the session to reconnect.",
"status": {
"starting": "Starting",
"ready": "Ready",
"running": "Working",
"stopped": "Stopped",
"error": "Error"
},
"installRuntime": "Install selected agent and Node.js",
"installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.",
"installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.",
"installFailed": "Installation failed. Review the log and retry.",
"installLog": "Installation log",
"enableForwarding": "Enable Agent forwarding",
"forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.",
"forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.",
"forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.",
"search": "Search sessions",
"showArchived": "Show archived",
"sessionName": "Session name",
"rename": "Rename",
"archive": "Archive and stop",
"restore": "Restore session",
"queue": "Queued messages",
"enqueue": "Queue message",
"continueQueue": "Continue queue",
"pauseQueue": "Pause queue",
"editQueued": "Edit queued message",
"save": "Save",
"remove": "Remove",
"upload": "Upload attachment",
"remoteFile": "File path inside working directory",
"attach": "Attach file",
"attachments": "Reuse a session attachment",
"pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.",
"attachmentLimit": "Up to 4 attachments, 1 MiB each",
"review": "Git changes and worktrees",
"gitStatus": "Refresh Git status",
"diffFile": "File path to review",
"diff": "View diff",
"branch": "New branch name",
"createWorktree": "Create worktree",
"useWorktree": "New session in this worktree",
"worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.",
"clean": "No changes",
"reconnecting": "Reconnecting and restoring messages…",
"unavailable": "Session is no longer accessible. Check your permissions or sign in again.",
"shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue"
},
"plugin": {
"name": "AI Assistant"
}
}
-198
View File
@@ -1,198 +0,0 @@
{
"permissions": {
"use": {
"title": "Käytä avustajaa",
"description": "Avaa avustaja ja kysy siltä asioita."
},
"manage_providers": {
"title": "Hallinnoi palveluntarjoajia",
"description": "Lisää, muokkaa ja poista tekoälypalveluntarjoajia ja niiden API-avaimia."
},
"apply_proposals": {
"title": "Käytä ehdotuksia",
"description": "Anna avustajan suorittaa ehdottamansa muutokset."
},
"services.use": {
"title": "Käytä tekoälyä muista ominaisuuksista",
"description": "Muut ominaisuudet saattavat soittaa avustajalle puolestasi."
},
"secrets.share": {
"title": "Jaa API-avain",
"description": "Muut ominaisuudet saattavat käyttää tallennettua tekoälyn API-avainta näkemättä sitä."
}
},
"settings": {
"assistantUnavailable": "Tekoälyavustaja on pois päältä tällä palvelimella.",
"providersNeedOptIn": "Ota yllä oleva tekoälyavustaja käyttöön lisätäksesi palveluntarjoajia.",
"admin": {
"globallyEnabled": {
"label": "Tekoälyavustaja",
"description": "Salli käyttäjien ottaa tekoälyavustaja käyttöön. Kun tämä on pois päältä, avustaja on piilotettu ja estetty kaikilta."
},
"privateEndpoints": {
"label": "Sallitut yksityiset tekoälyisännät",
"description": "Yksityisen verkon isännät, joihin käyttäjät voivat ohjata palveluntarjoajan, kuten itse isännöity Ollama. Aseta yksi isäntä kullekin riville."
}
},
"host": {
"enableAiAssistant": {
"label": "Ota tekoälyavustaja käyttöön",
"description": "Salli tekoälyavustaja auttaa tämän isännän terminaalissa. Oletusarvoisesti pois päältä, mutta tekoälyn on silti oltava käytössä tililläsi."
}
}
},
"ai": {
"title": "Assistentti",
"send": "Lähetä",
"stop": "Stop",
"save": "Tallentaa",
"cancel": "Peruuttaa",
"running": "juokseminen",
"approve": "Hyväksyä",
"reject": "Hylätä",
"inputPlaceholder": "Kysy tarjoilijoistasi tai kuvaile ehdottamaasi muutosta",
"selectProvider": "Valitse palveluntarjoaja",
"providerSettings": "Palveluntarjoajan asetukset",
"addProvider": "Lisää palveluntarjoaja",
"editProvider": "Muokkaa palveluntarjoajaa",
"removeProvider": "Poista palveluntarjoaja",
"providerType": "Palveluntarjoaja",
"providerLabel": "Nimi",
"providerLabelPlaceholder": "Esimerkiksi: Paikallinen Ollama",
"baseUrl": "Palvelimen osoite",
"apiKey": "API-avain",
"defaultModel": "Oletusmalli",
"defaultModelPlaceholder": "Esimerkiksi: llama3.1 tai claude-opus-5",
"labelRequired": "Anna tälle palveluntarjoajalle nimi",
"providerSaveFailed": "Palveluntarjoajaa ei voitu tallentaa",
"providerUpdated": "Palveluntarjoaja päivitetty",
"providerDeleteFailed": "Palveluntarjoajaa ei voitu poistaa",
"privateEndpointHint": "Yksityisen verkon osoitteiden ylläpitäjän on ensin sallittava isäntä.",
"providerOllama": "Ollama",
"providerAnthropic": "Antrooppinen",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "OpenAI-yhteensopiva",
"toolArguments": "Syöttö",
"toolResult": "Tulos",
"statusApplied": "Käytetty",
"statusRejected": "Hylätty",
"proposalApplied": "Muutos otettu käyttöön",
"proposalApplyFailed": "Muutosta ei voitu ottaa käyttöön",
"proposalRejectFailed": "Muutosta ei voitu hylätä",
"enableTitle": "Ota tekoälyavustaja käyttöön",
"enableDescription": "Anna avustajan lukea asetuksesi ja ehdottaa muutoksia hyväksyttäväksi.",
"readOnlyCommands": "Salli vain luettavat diagnostiikkakomennot",
"readOnlyCommandsDescription": "Assistantti voi suorittaa lyhyen listan turvallisia komentoja, kuten df ja uptime, kysymättä joka kerta. Kaikki palvelinta muuttavat ehdotukset esitetään aina ensin.",
"modelDetectFailed": "Palveluntarjoajaan ei saatu yhteyttä, joten tämä on yleisluettelo. Voit kirjoittaa minkä tahansa mallin nimen.",
"modelCustom": "Anna mallin nimi",
"modelRefresh": "Päivitä lista",
"modelPlaceholder": "Valitse tai kirjoita malli",
"attachHint": "Kirjoita @ viitataksesi isäntään, koodinpätkään tai automaatioon. Muutoksia ehdotetaan hyväksyttäväksi.",
"runCommandTitle": "Suorita komento",
"assistant": "Tekoälyavustaja",
"terminalPanelEmpty": "Kysy avustajalta tästä majoittajasta. Kaikki osavaltion muutokset vaativat ensin hyväksyntäsi.",
"terminalPanelPlaceholder": "Kysy tästä isännöitsijästä...",
"noProvidersConfigured": "Tekoälypalveluntarjoajaa ei ole vielä määritetty. Lisää sellainen tekoälyavustaja-välilehdellä.",
"runsInOpenTerminal": "Suoritetaan avoimessa pääteistunnossa.",
"runInTerminal": "Juokse",
"terminalContextPrefill": "Tässä on päätteen tuloste:\n\n{{output}}\n\n",
"secrets": {
"shareApiKey": "Anna muiden ominaisuuksien käyttää tekoälypalveluntarjoajasi API-avainta omiin pyyntöihinsä. Avainta ei koskaan kopioida, ja voit peruuttaa tämän milloin tahansa."
}
},
"nav": {
"ai": "Tekoäly"
},
"onboarding": {
"aiTitle": "Tekoälyavustaja",
"aiIntro": "Termix voi sisältää valinnaisen tekoälyavustaja. Voit muuttaa tätä myöhemmin asetuksissasi.",
"aiSkipTitle": "Jätä se pois",
"aiSkipDesc": "Termix toimii kuten nytkin. Avustaja pysyy piilossa.",
"aiEnableTitle": "Käynnistä se",
"aiEnableDesc": "Lisää avustajan, joka lukee asetuksesi ja ehdottaa hyväksymiäsi muutoksia. Edellyttää toimittamaasi palveluntarjoajaa, kuten paikallisen Ollaman.",
"aiNote": "Mitään ei lähetetä mihinkään ennen kuin lisäät palveluntarjoajan ja lähetät viestin."
},
"hosts": {
"docsLink": "Näytä dokumentit"
},
"agents": {
"title": "AI Agent",
"permissionTitle": "Run remote AI agents",
"permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.",
"new": "New session",
"description": "Run an agent in a directory on this host using a Termix AI provider.",
"runtime": "Agent",
"provider": "Termix provider",
"chooseProvider": "Choose a compatible provider",
"model": "Model",
"directory": "Working directory",
"executable": "Executable (optional absolute path)",
"requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.",
"responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.",
"piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.",
"start": "Start agent",
"resume": "Resume",
"stop": "Stop session",
"interrupt": "Interrupt turn",
"send": "Send",
"prompt": "Ask the agent to work in this directory…",
"noSessions": "No agent sessions for this host.",
"approval": "Agent needs your decision",
"answer": "Answer (JSON for structured questions)",
"allow": "Allow once",
"deny": "Deny",
"tool": "Tool activity",
"streamFailed": "Agent stream disconnected. Reopen the session to reconnect.",
"status": {
"starting": "Starting",
"ready": "Ready",
"running": "Working",
"stopped": "Stopped",
"error": "Error"
},
"installRuntime": "Install selected agent and Node.js",
"installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.",
"installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.",
"installFailed": "Installation failed. Review the log and retry.",
"installLog": "Installation log",
"enableForwarding": "Enable Agent forwarding",
"forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.",
"forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.",
"forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.",
"search": "Search sessions",
"showArchived": "Show archived",
"sessionName": "Session name",
"rename": "Rename",
"archive": "Archive and stop",
"restore": "Restore session",
"queue": "Queued messages",
"enqueue": "Queue message",
"continueQueue": "Continue queue",
"pauseQueue": "Pause queue",
"editQueued": "Edit queued message",
"save": "Save",
"remove": "Remove",
"upload": "Upload attachment",
"remoteFile": "File path inside working directory",
"attach": "Attach file",
"attachments": "Reuse a session attachment",
"pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.",
"attachmentLimit": "Up to 4 attachments, 1 MiB each",
"review": "Git changes and worktrees",
"gitStatus": "Refresh Git status",
"diffFile": "File path to review",
"diff": "View diff",
"branch": "New branch name",
"createWorktree": "Create worktree",
"useWorktree": "New session in this worktree",
"worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.",
"clean": "No changes",
"reconnecting": "Reconnecting and restoring messages…",
"unavailable": "Session is no longer accessible. Check your permissions or sign in again.",
"shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue"
},
"plugin": {
"name": "AI Assistant"
}
}
-198
View File
@@ -1,198 +0,0 @@
{
"permissions": {
"use": {
"title": "Utilisez l'assistant",
"description": "Ouvrez l'assistant et posez-lui des questions."
},
"manage_providers": {
"title": "Gérer les fournisseurs",
"description": "Ajoutez, modifiez et supprimez les fournisseurs d'IA et leurs clés API."
},
"apply_proposals": {
"title": "Soumettre les propositions",
"description": "Laissez l'assistant effectuer les modifications qu'il suggère."
},
"services.use": {
"title": "Utiliser l'IA d'autres fonctionnalités",
"description": "D'autres fonctionnalités peuvent appeler l'assistant en votre nom."
},
"secrets.share": {
"title": "Partagez la clé API",
"description": "D'autres fonctionnalités peuvent utiliser la clé API IA stockée sans la voir."
}
},
"settings": {
"assistantUnavailable": "L'assistant IA est désactivé sur ce serveur.",
"providersNeedOptIn": "Activez l'assistant IA ci-dessus pour ajouter des fournisseurs.",
"admin": {
"globallyEnabled": {
"label": "Assistant IA",
"description": "Autoriser les utilisateurs à activer l'assistant IA. Lorsqu'il est désactivé, l'assistant est masqué et inaccessible à tous."
},
"privateEndpoints": {
"label": "Hôtes d'IA privés autorisés",
"description": "Sur votre réseau privé, configurez des hôtes vers lesquels les utilisateurs peuvent orienter un fournisseur, comme un serveur Ollama auto-hébergé. Placez un hôte sur chaque ligne."
}
},
"host": {
"enableAiAssistant": {
"label": "Activer l'assistant IA",
"description": "Autoriser l'assistant IA à vous aider dans le terminal de cet hôte. Désactivé par défaut ; l'IA doit être activée pour votre compte."
}
}
},
"ai": {
"title": "Assistant",
"send": "Envoyer",
"stop": "Stop",
"save": "Enregistrer",
"cancel": "Annuler",
"running": "en cours",
"approve": "Approuver",
"reject": "Rejeter",
"inputPlaceholder": "Renseignez-vous sur vos serveurs ou décrivez un changement à proposer.",
"selectProvider": "Sélectionnez un fournisseur",
"providerSettings": "Paramètres du fournisseur",
"addProvider": "Ajouter un fournisseur",
"editProvider": "Modifier le fournisseur",
"removeProvider": "Supprimer le fournisseur",
"providerType": "Fournisseur",
"providerLabel": "Nom",
"providerLabelPlaceholder": "Par exemple : Ollama local",
"baseUrl": "Adresse du serveur",
"apiKey": "Clé API",
"defaultModel": "Modèle par défaut",
"defaultModelPlaceholder": "Par exemple : llama3.1 ou claude-opus-5",
"labelRequired": "Donnez un nom à ce fournisseur",
"providerSaveFailed": "Impossible de sauver le fournisseur",
"providerUpdated": "Mise à jour du fournisseur",
"providerDeleteFailed": "Impossible de supprimer le fournisseur",
"privateEndpointHint": "Les adresses de votre réseau privé nécessitent l'autorisation préalable d'un administrateur pour accéder à l'hôte.",
"providerOllama": "Ollama",
"providerAnthropic": "Anthropique",
"providerOpenai": "OpenAI",
"providerGemini": "Google Gemini",
"providerOpenaiCompatible": "Compatible avec OpenAI",
"toolArguments": "Entrée",
"toolResult": "Résultat",
"statusApplied": "Appliqué",
"statusRejected": "Rejeté",
"proposalApplied": "Changement appliqué",
"proposalApplyFailed": "Impossible d'appliquer la modification",
"proposalRejectFailed": "Impossible de refuser la modification",
"enableTitle": "Activer l'assistant IA",
"enableDescription": "Laissez l'assistant analyser votre configuration et vous suggérer des modifications à approuver.",
"readOnlyCommands": "Autoriser les commandes de diagnostic en lecture seule",
"readOnlyCommandsDescription": "L'assistant peut exécuter une courte liste de commandes sûres, comme `df` et `uptime`, sans demander l'autorisation à chaque fois. Toute modification apportée au serveur est toujours proposée au préalable.",
"modelDetectFailed": "Impossible de joindre le fournisseur ; voici donc une liste générale. Vous pouvez saisir n’importe quel nom de modèle.",
"modelCustom": "Saisissez le nom du modèle",
"modelRefresh": "Actualiser la liste",
"modelPlaceholder": "Choisissez ou saisissez un modèle",
"attachHint": "Saisissez @ pour faire référence à un hôte, un extrait de code ou une automatisation. Les modifications proposées doivent être approuvées.",
"runCommandTitle": "Exécuter la commande",
"assistant": "Assistant IA",
"terminalPanelEmpty": "Renseignez-vous auprès de l'assistant concernant cet hôte. Tout changement d'état nécessite votre approbation préalable.",
"terminalPanelPlaceholder": "Renseignez-vous sur cet hôte...",
"noProvidersConfigured": "Aucun fournisseur d'IA n'est encore configuré. Ajoutez-en un dans l'onglet Assistant IA.",
"runsInOpenTerminal": "S'exécute dans votre session de terminal ouverte.",
"runInTerminal": "Exécuter",
"terminalContextPrefill": "Voici la sortie de mon terminal :\n\n{{output}}\n\n",
"secrets": {
"shareApiKey": "Autorisez d'autres fonctionnalités à utiliser votre clé API de fournisseur d'IA pour leurs propres requêtes. Cette clé n'est jamais copiée et vous pouvez la révoquer à tout moment."
}
},
"nav": {
"ai": "IA"
},
"onboarding": {
"aiTitle": "Assistant IA",
"aiIntro": "Termix peut inclure un assistant IA optionnel. Vous pouvez modifier ce paramètre ultérieurement.",
"aiSkipTitle": "Laissez-le de côté",
"aiSkipDesc": "Termix fonctionne comme actuellement. L'assistant reste caché.",
"aiEnableTitle": "Allumez-le",
"aiEnableDesc": "Ajoute un assistant qui analyse votre configuration et suggère des modifications que vous approuvez. Nécessite un fournisseur que vous désignez, comme un Ollama local.",
"aiNote": "Aucun message n'est envoyé tant que vous n'avez pas ajouté de fournisseur et envoyé un message."
},
"hosts": {
"docsLink": "Voir la documentation"
},
"agents": {
"title": "AI Agent",
"permissionTitle": "Run remote AI agents",
"permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.",
"new": "New session",
"description": "Run an agent in a directory on this host using a Termix AI provider.",
"runtime": "Agent",
"provider": "Termix provider",
"chooseProvider": "Choose a compatible provider",
"model": "Model",
"directory": "Working directory",
"executable": "Executable (optional absolute path)",
"requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.",
"responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.",
"piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.",
"start": "Start agent",
"resume": "Resume",
"stop": "Stop session",
"interrupt": "Interrupt turn",
"send": "Send",
"prompt": "Ask the agent to work in this directory…",
"noSessions": "No agent sessions for this host.",
"approval": "Agent needs your decision",
"answer": "Answer (JSON for structured questions)",
"allow": "Allow once",
"deny": "Deny",
"tool": "Tool activity",
"streamFailed": "Agent stream disconnected. Reopen the session to reconnect.",
"status": {
"starting": "Starting",
"ready": "Ready",
"running": "Working",
"stopped": "Stopped",
"error": "Error"
},
"installRuntime": "Install selected agent and Node.js",
"installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.",
"installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.",
"installFailed": "Installation failed. Review the log and retry.",
"installLog": "Installation log",
"enableForwarding": "Enable Agent forwarding",
"forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.",
"forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.",
"forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.",
"search": "Search sessions",
"showArchived": "Show archived",
"sessionName": "Session name",
"rename": "Rename",
"archive": "Archive and stop",
"restore": "Restore session",
"queue": "Queued messages",
"enqueue": "Queue message",
"continueQueue": "Continue queue",
"pauseQueue": "Pause queue",
"editQueued": "Edit queued message",
"save": "Save",
"remove": "Remove",
"upload": "Upload attachment",
"remoteFile": "File path inside working directory",
"attach": "Attach file",
"attachments": "Reuse a session attachment",
"pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.",
"attachmentLimit": "Up to 4 attachments, 1 MiB each",
"review": "Git changes and worktrees",
"gitStatus": "Refresh Git status",
"diffFile": "File path to review",
"diff": "View diff",
"branch": "New branch name",
"createWorktree": "Create worktree",
"useWorktree": "New session in this worktree",
"worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.",
"clean": "No changes",
"reconnecting": "Reconnecting and restoring messages…",
"unavailable": "Session is no longer accessible. Check your permissions or sign in again.",
"shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue"
},
"plugin": {
"name": "AI Assistant"
}
}
Loaded 100 of 3167 files, more files were not shown because too many files have changed in this diff. Show more