diff --git a/.changeset/30512-service-settings-locales-b.md b/.changeset/30512-service-settings-locales-b.md new file mode 100644 index 00000000000..1202a0c7263 --- /dev/null +++ b/.changeset/30512-service-settings-locales-b.md @@ -0,0 +1,10 @@ +--- +'@objectstack/service-settings': patch +--- + +i18n(service-settings): the built-in Setup settings pages ship in Russian, German and Korean + +Clause-②: no +Metadata-protocol: none + +The built-in settings pages (Mail, SMS, Branding, Company, Localization, Authentication, File Storage, AI & Embedder, Knowledge, and the Data Lifecycle page `@objectstack/objectql` registers) now carry `ru`, `de` and `ko` beside the existing eight locales: page titles and descriptions, group headings, field labels and help, option labels, banners, action buttons and the source badges. An administrator reading Setup in one of those languages sees these pages in their own language instead of English. Country pickers name each country in the reader's language through `Intl.DisplayNames`, as the existing locales do. The settings coverage test now holds each new locale to the same full coverage. Second half of package 6 of #30512; with it every built-in settings page carries all eleven platform locales. diff --git a/packages/services/service-settings/src/translations/de.ts b/packages/services/service-settings/src/translations/de.ts new file mode 100644 index 00000000000..de066d7efbd --- /dev/null +++ b/packages/services/service-settings/src/translations/de.ts @@ -0,0 +1,743 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { PlatformTranslationData } from '@objectstack/spec/system'; +import { countryOptionLabels } from '../manifests/company.manifest.js'; + +/** + * Deutsch (de) — built-in settings manifest translations. + */ +export const de: PlatformTranslationData = { + settingsCommon: { + sourceLabels: { env: 'Umgebung', global: 'Global', tenant: 'Organisation', user: 'Benutzer', default: 'Standard' }, + }, + settings: { + mail: { + title: 'E-Mail-Versand', + description: 'Konfiguration von SMTP und dem Anbieter für Transaktions-E-Mails.', + groups: { + provider: { title: 'Anbieter', description: 'Legen Sie fest, wie dieses System ausgehende E-Mails versendet.' }, + smtp: { title: 'SMTP' }, + api_key: { title: 'API-Schlüssel' }, + from_address: { title: 'Absenderadresse' }, + delivery: { title: 'Zustellung', description: 'Wie ausgehende E-Mails an den Anbieter übergeben werden.' }, + }, + keys: { + provider: { + label: 'Anbieter', + help: 'Aufgeführt sind nur Anbieter, über die dieser Server tatsächlich zustellen kann. SendGrid und Amazon SES werden als SMTP konfiguriert.', + options: { + smtp: 'SMTP', + resend: 'Resend', + postmark: 'Postmark', + log: 'Keiner (nur Protokoll — keine echte Zustellung)', + }, + }, + smtp_host: { label: 'Host', help: 'Beispiel: smtp.example.com' }, + smtp_port: { label: 'Port' }, + smtp_secure: { label: 'TLS verwenden' }, + smtp_user: { label: 'Benutzername' }, + smtp_password: { label: 'Passwort' }, + api_key: { label: 'API-Schlüssel' }, + from_email: { label: 'Absender-E-Mail', help: 'Beispiel: no-reply@example.com' }, + from_name: { + label: 'Absendername', + help: 'Leer lassen, um den Produktnamen (Branding) oder den bei der Einrichtung dieser Bereitstellung vergebenen Namen zu verwenden.', + }, + queue_delivery: { + label: 'Zustellung über dauerhafte Warteschlange', + help: 'Übergibt jede Nachricht an die Job-Warteschlange, statt sie direkt zu senden, sodass ein Worker eine fehlgeschlagene Zustellung mit Backoff wiederholt und sie einen Neustart übersteht. Erfordert die Warteschlangen-Funktion mit einem dauerhaften Adapter. „Test-E-Mail senden“ sendet immer direkt.', + }, + }, + actions: { + test: { label: 'Test-E-Mail senden' }, + }, + }, + sms: { + title: 'SMS-Versand', + description: 'SMS-Anbieterkonfiguration für die Anmeldung per OTP, Einladungen und Benachrichtigungen.', + groups: { + provider: { title: 'Anbieter', description: 'Legen Sie fest, wie dieses System ausgehende SMS versendet.' }, + aliyun: { title: 'Aliyun SMS' }, + twilio: { title: 'Twilio' }, + limits: { + title: 'Ausgabenlimits', + description: 'Begrenzt das ausgehende SMS-Volumen der Bereitstellung. SMS ist ein kostenpflichtiger Kanal, und jeder Versand kostet echtes Geld.', + }, + }, + keys: { + provider: { + label: 'Anbieter', + options: { + log: 'Keiner (nur Protokoll — keine echte Zustellung)', + aliyun: 'Aliyun SMS (阿里云短信)', + twilio: 'Twilio', + }, + }, + aliyun_access_key_id: { label: 'AccessKey ID' }, + aliyun_access_key_secret: { label: 'AccessKey Secret' }, + aliyun_sign_name: { label: 'Signaturname (短信签名)' }, + aliyun_template_code: { + label: 'Standard-Vorlagencode (短信模板)', + help: 'Wird verwendet, wenn ein Versand keine Vorlage angibt. Eine Sammelvorlage mit einer einzigen Variablen ${content} ermöglicht allgemeine Benachrichtigungs-SMS.', + }, + twilio_account_sid: { label: 'Account SID' }, + twilio_auth_token: { label: 'Auth-Token' }, + twilio_from_number: { + label: 'Absendernummer', + help: 'Absender im E.164-Format, z. B. +15005550006. Entweder dies oder eine Messaging Service SID.', + }, + twilio_messaging_service_sid: { label: 'Messaging Service SID' }, + daily_quota: { + label: 'Tägliches Versandlimit', + help: 'Höchstzahl an SMS, die diese Bereitstellung pro UTC-Tag senden darf, über OTP-Anmeldung, Einladungen und Benachrichtigungen hinweg. 0 bedeutet kein Limit. Sendungen über dem Limit werden bis 00:00 UTC abgelehnt.', + }, + }, + actions: { + test: { label: 'Test-SMS senden' }, + }, + }, + branding: { + title: 'Branding', + description: 'Name, Logo und Farbe, die Konsole und Anmeldeseite anzeigen. Ein nicht gesetzter Wert fällt auf das Branding der Bereitstellung zurück.', + groups: { + identity: { title: 'Identität' }, + appearance: { title: 'Erscheinungsbild' }, + }, + keys: { + workspace_name: { + label: 'Produktname', + help: 'Der Produktname in der Konsole, auf der Anmeldeseite und im Browsertitel sowie in Anmelde-E-Mails, der Authenticator-App und der MCP-Anmeldung. Leer lassen, um den bei der Einrichtung dieser Bereitstellung vergebenen Namen zu verwenden. Dies ist nicht der Name der Organisation selbst; der steht im Feld „Name“ des Organisationsdatensatzes.', + }, + accent_color: { + label: 'Akzentfarbe', + help: 'Die Designfarbe von Browser und installierter App. Jede App behält ihre eigenen Farben.', + }, + logo_url: { + label: 'Logo-URL', + help: 'Wird auf der Anmeldeseite, dem Ladebildschirm und im Startseiten-Header angezeigt. Beispiel: https://…/logo.svg', + }, + }, + }, + company: { + title: 'Unternehmen', + description: 'Angaben zur juristischen Person — eingetragener Name, Anschrift, Steuernummern und Hauptansprechpartner.', + groups: { + identity: { title: 'Identität' }, + address: { title: 'Eingetragene Anschrift' }, + contact: { title: 'Kontakt' }, + }, + keys: { + legal_name: { + label: 'Rechtlicher Name', + help: 'Eingetragener rechtlicher Name der Organisation, wie er auf Verträgen und Rechnungen steht.', + }, + registration_number: { + label: 'Registernummer', + help: 'Handelsregister- bzw. Gründungsnummer des Unternehmens (z. B. EIN).', + }, + tax_id: { label: 'Steuer-/USt-ID', help: 'Wird auf Rechnungen angezeigt (z. B. VAT, GST, ABN).' }, + address_line1: { label: 'Adresszeile 1' }, + address_line2: { label: 'Adresszeile 2' }, + city: { label: 'Stadt' }, + state: { label: 'Bundesland / Provinz' }, + postal_code: { label: 'Postleitzahl' }, + country: { + label: 'Land', + help: 'Land der eingetragenen Anschrift. Geben Sie einen Ländernamen ein und wählen Sie ihn aus der Liste.', + options: countryOptionLabels('de'), + }, + phone: { label: 'Telefon', help: 'E.164 empfohlen, z. B. +49 30 5555 0100.' }, + website: { label: 'Website', help: 'Beispiel: https://example.com' }, + primary_contact_name: { label: 'Name des Hauptansprechpartners' }, + primary_contact_email: { label: 'E-Mail des Hauptansprechpartners', help: 'Beispiel: ops@example.com' }, + }, + }, + localization: { + title: 'Lokalisierung', + description: 'Standard-Zeitzone, -Sprache, -Land, -Währung und Geschäftsjahr.', + groups: { + region: { title: 'Region' }, + finance: { title: 'Finanzen' }, + }, + keys: { + timezone: { + label: 'Standard-Zeitzone', + help: 'Bestimmt, was „heute“ in Formeln bedeutet, wie Berichte Datumswerte gruppieren und wie Datum und Uhrzeit angezeigt werden. Akzeptiert jeden IANA-Zeitzonennamen (z. B. Europe/Berlin). Dies ist die für die Organisation geltende Zeitzone: Das Feld „Zeitzone“ im Organisationsdatensatz ändert sie nicht.', + options: { + UTC: 'UTC', + 'America/Los_Angeles': '(UTC−08/−07) Los Angeles', + 'America/Denver': '(UTC−07/−06) Denver', + 'America/Chicago': '(UTC−06/−05) Chicago', + 'America/New_York': '(UTC−05/−04) New York', + 'America/Sao_Paulo': '(UTC−03) São Paulo', + 'Europe/London': '(UTC±00/+01) London', + 'Europe/Paris': '(UTC+01/+02) Paris', + 'Europe/Berlin': '(UTC+01/+02) Berlin', + 'Europe/Moscow': '(UTC+03) Moskau', + 'Asia/Dubai': '(UTC+04) Dubai', + 'Asia/Kolkata': '(UTC+05:30) Kolkata', + 'Asia/Singapore': '(UTC+08) Singapur', + 'Asia/Shanghai': '(UTC+08) Shanghai', + 'Asia/Tokyo': '(UTC+09) Tokio', + 'Australia/Sydney': '(UTC+10/+11) Sydney', + 'Pacific/Auckland': '(UTC+12/+13) Auckland', + }, + }, + locale: { + label: 'Standardsprache', + help: 'BCP-47-Sprache für Meldungskataloge sowie Zahlen- und Datumsformate.', + }, + default_country: { + label: 'Standardland', + help: 'Dient als Standard für Adressen und Telefonnummern. Geben Sie einen Ländernamen ein und wählen Sie ihn aus der Liste.', + options: countryOptionLabels('de'), + }, + currency: { + label: 'Standardwährung', + help: 'ISO-4217-Code, der gilt, wenn ein Währungsfeld keinen eigenen angibt. Leer lassen, um Beträge ohne Code als reine Zahlen darzustellen.', + options: { + USD: 'USD — US-Dollar', + EUR: 'EUR — Euro', + GBP: 'GBP — Britisches Pfund', + JPY: 'JPY — Japanischer Yen', + CNY: 'CNY — Chinesischer Yuan', + INR: 'INR — Indische Rupie', + AUD: 'AUD — Australischer Dollar', + CAD: 'CAD — Kanadischer Dollar', + BRL: 'BRL — Brasilianischer Real', + }, + }, + fiscal_year_start: { + label: 'Beginn des Geschäftsjahres', + help: 'Erster Monat des Geschäftsjahres — bestimmt „dieses Quartal / Geschäftsjahr“ in Berichten.', + options: { + january: 'Januar', + february: 'Februar', + march: 'März', + april: 'April', + may: 'Mai', + june: 'Juni', + july: 'Juli', + august: 'August', + september: 'September', + october: 'Oktober', + november: 'November', + december: 'Dezember', + }, + }, + }, + }, + auth: { + title: 'Authentifizierung', + description: 'Anmeldung, Registrierung und integrierte Authentifizierungsfunktionen.', + groups: { + email_password: { + title: 'E-Mail und Passwort', + description: 'Steuert die lokale Anmeldung per E-Mail und Passwort sowie die Selbstregistrierung.', + }, + membership: { + title: 'Mitgliedschaft', + description: 'Wem ein neu angelegter Benutzer beitritt. Gehört zur Selbstregistrierung oben.', + }, + audience: { + title: 'Zielgruppe', + description: 'Wer Benutzer der Apps dieses Systems werden darf. Zugelassene E-Mail-Domains erzwingen immer die E-Mail-Bestätigung. „Offen“ erzwingt sie ebenfalls, sofern die Bereitstellung sie nicht abschaltet; ein in dieser Konsole gespeicherter Wert kann das nicht. Einladungen, von Administratoren angelegte Benutzer, SCIM-Provisionierung und Unternehmens-SSO werden in jedem Modus zugelassen.', + }, + password_policy: { + title: 'Passwortrichtlinie', + description: 'Längengrenzen, die der Authentifizierungsanbieter bei Registrierung und Passwortzurücksetzung durchsetzt.', + }, + anti_abuse: { + title: 'Missbrauchsschutz', + description: 'Schutz vor Brute-Force: Kontosperre pro Identität und Ratenbegrenzung pro IP an den Authentifizierungs-Endpunkten.', + }, + multi_factor: { + title: 'Mehrfaktor', + description: 'Verlangt, dass Mitglieder ihr Konto mit einer Authenticator-App (TOTP) schützen.', + }, + sessions: { title: 'Sitzungen', description: 'Wie lange eine angemeldete Sitzung gültig bleibt.' }, + network: { title: 'Netzwerk', description: 'Schränkt ein, von wo aus sich Benutzer anmelden können.' }, + api_keys: { + title: 'API-Schlüssel', + description: 'Wie lange persönliche API-Schlüssel gelten. Ein abgelaufener, über das Inaktivitätsfenster hinaus ungenutzter oder widerrufener Schlüssel wird mit eigenem Fehlercode abgelehnt; Abhilfe ist immer ein neuer Schlüssel.', + }, + social: { + title: 'Social Login', + description: 'Konfiguriert den integrierten Google-Anmeldeanbieter. In der Serverkonfiguration gesetzte Anmeldedaten haben weiterhin Vorrang.', + }, + }, + keys: { + email_password_enabled: { + label: 'Anmeldung per E-Mail/Passwort aktivieren', + confirmOffText: 'Wer sich mit Passwort anmeldet, braucht dann einen anderen Zugang.', + }, + signup_enabled: { + label: 'E-Mail-Registrierung erlauben', + help: 'Öffnet das Registrierungsformular mit E-Mail und Passwort. Wen es zulässt, bestimmt unten die Zielgruppe der Selbstregistrierung: bei „Nur auf Einladung“ nur eine Adresse mit offener Einladung; bei „Zugelassene E-Mail-Domains“ die zugelassenen Domains und eingeladene Adressen; bei „Offen“ jeden. Aus schließt das Formular bei jeder Zielgruppe, Einladungen eingeschlossen.', + }, + require_email_verification: { label: 'E-Mail-Bestätigung verlangen' }, + membership_policy: { + label: 'Mitgliedschaft neuer Benutzer', + help: '„Automatisch“ bindet jeden neuen Benutzer an die Standardorganisation. „Nur auf Einladung“ gewährt die Mitgliedschaft nur durch eine ausdrückliche Handlung — eine Organisation anlegen, eine Einladung annehmen, von einem Administrator hinzugefügt oder per SSO Just-in-Time provisioniert werden.', + options: { + auto: 'Automatisch der Standardorganisation beitreten', + 'invite-only': 'Nur auf Einladung — nie automatisch beitreten', + }, + }, + audience_posture: { + label: 'Zielgruppe der Selbstregistrierung', + help: '„Nur auf Einladung“ schließt die Selbstregistrierung: Personen werden nur Benutzer, wenn ein Administrator sie einlädt, anlegt oder importiert, oder über Verzeichnis-Provisionierung (SCIM) oder Unternehmens-Single-Sign-on. „Zugelassene E-Mail-Domains“ öffnet sie für die unten aufgeführten Domains. „Offen“ lässt jeden zu. Sowohl „Zugelassene E-Mail-Domains“ als auch „Offen“ benötigen den Berechtigungssatz für Selbstregistrierung unten. „Zugelassene E-Mail-Domains“ verlangt immer die E-Mail-Bestätigung. „Offen“ verlangt sie, sofern die Serverkonfiguration sie nicht abschaltet; sie in dieser Konsole abzuschalten, wird unter „Offen“ abgelehnt.', + options: { + invite_only: 'Nur auf Einladung — keine Selbstregistrierung (Standard)', + email_domain: 'Nur zugelassene E-Mail-Domains', + open: 'Offen — jeder darf sich selbst registrieren', + }, + }, + audience_allowed_email_domains: { + label: 'Zugelassene E-Mail-Domains', + help: 'Reine Domains, eine pro Zeile oder durch Kommas getrennt (z. B. acme.com). Der Abgleich ist exakt und unabhängig von Groß-/Kleinschreibung; Subdomains brauchen eigene Einträge; keine Platzhalter.', + }, + audience_self_registration_permission_set: { + label: 'Berechtigungssatz für Selbstregistrierung', + help: 'Name des Berechtigungssatzes, den jeder selbst registrierte Benutzer erhält. Der Standard-Mitgliedersatz genügt; der vollständige Administratorsatz wird abgelehnt. Pflicht, sobald die Zielgruppe Selbstregistrierung erlaubt.', + }, + password_min_length: { label: 'Minimale Passwortlänge' }, + password_max_length: { + label: 'Maximale Passwortlänge', + help: 'Schützt vor Denial-of-Service durch das Hashen sehr langer Passwörter.', + }, + password_reject_breached: { + label: 'Kompromittierte Passwörter ablehnen', + help: 'Blockiert Passwörter aus öffentlichen Datenlecks über Have I Been Pwned (Bereichsprüfung mit k-Anonymität; das Passwort wird nie vollständig gesendet).', + }, + password_require_complexity: { + label: 'Komplexe Passwörter verlangen', + help: 'Verlangt bei Registrierung sowie Passwortänderung und -zurücksetzung eine Mischung von Zeichenklassen (Groß-, Kleinbuchstaben, Ziffern, Sonderzeichen).', + }, + password_min_classes: { + label: 'Mindestanzahl Zeichenklassen', + help: 'Wie viele der vier Klassen (Groß / Klein / Ziffer / Sonderzeichen) ein Passwort enthalten muss.', + }, + password_history_count: { + label: 'Passwortverlauf (keine Wiederverwendung)', + help: 'Verhindert bei Änderung oder Zurücksetzung die Wiederverwendung so vieler früherer Passwörter. 0 deaktiviert die Prüfung.', + }, + password_expiry_days: { + label: 'Passwortablauf (Tage)', + help: 'Erzwingt nach so vielen Tagen eine Passwortänderung. 0 deaktiviert den Ablauf. Solange das Passwort abgelaufen ist, hat der Benutzer bis zur Änderung keinen Zugriff auf Daten.', + }, + lockout_threshold: { + label: 'Schwelle für Kontosperre', + help: 'Sperrt ein Konto nach so vielen aufeinanderfolgenden fehlgeschlagenen Anmeldeversuchen — falsche Passwörter ebenso wie falsche Zwei-Faktor-Codes. Während der Sperre wird die Anmeldung selbst mit korrekten Anmeldedaten abgelehnt. 0 deaktiviert die Sperre in der Passwortstufe; die Zwei-Faktor-Prüfung behält dann ihr eingebautes Limit von 10 Versuchen pro 15 Minuten, da sie die letzte Prüfung vor der Ausgabe einer Sitzung ist.', + }, + lockout_duration_minutes: { + label: 'Sperrdauer (Minuten)', + help: 'Wie lange ein Konto nach Überschreiten der Schwelle gesperrt bleibt, in beiden Anmeldestufen.', + }, + rate_limit_max: { + label: 'Auth-Ratenlimit: max. Anfragen', + help: 'Maximale Anfragen pro IP und Zeitfenster an die Endpunkte für Anmeldung / Registrierung / Passwortzurücksetzung.', + }, + rate_limit_window_seconds: { + label: 'Auth-Ratenlimit: Zeitfenster (Sekunden)', + help: 'Gleitendes Zeitfenster, in dem die obige Anfragegrenze gezählt wird.', + }, + mfa_required: { + label: 'Mehrfaktor-Authentifizierung verlangen', + help: 'Benutzer ohne registrierten Authenticator verlieren nach Ablauf ihrer Schonfrist den Zugriff auf Daten. Das Aktivieren schaltet auch die Zwei-Faktor-Funktion ein, damit Benutzer sich registrieren können.', + }, + mfa_grace_period_days: { + label: 'MFA-Schonfrist (Tage)', + help: 'Wie lange Benutzer die Registrierung vor der harten Sperre aufschieben dürfen. 0 sperrt sofort.', + }, + session_expiry_days: { + label: 'Sitzungsdauer (Tage)', + help: 'Eine Sitzung läuft so viele Tage nach der Anmeldung ab.', + }, + session_refresh_days: { + label: 'Verlängerungsschwelle (Tage)', + help: 'Eine aktive Sitzung wird verlängert, wenn sie älter als dieser Wert ist.', + }, + session_idle_timeout_minutes: { + label: 'Leerlauf-Timeout (Minuten)', + help: 'Meldet einen Benutzer nach so vielen Minuten Inaktivität ab. 0 deaktiviert.', + }, + session_absolute_max_hours: { + label: 'Absolute Sitzungsdauer (Stunden)', + help: 'Erzwingt so viele Stunden nach der Anmeldung eine erneute Authentifizierung, unabhängig von Aktivität. 0 deaktiviert.', + }, + max_concurrent_sessions_per_user: { + label: 'Max. gleichzeitige Sitzungen pro Benutzer', + help: 'Begrenzt gleichzeitige Sitzungen pro Benutzer; über dem Limit werden die ältesten abgemeldet. 0 = unbegrenzt.', + }, + allowed_ip_ranges: { + label: 'Zugelassene IP-Bereiche', + help: 'CIDR-Bereiche oder exakte IPs (eine pro Zeile oder durch Kommas getrennt), z. B. 203.0.113.0/24. Wenn gesetzt, wird die Anmeldung von außerhalb dieser Bereiche abgelehnt, ebenso jede Anfrage, deren Client-Adresse der Server nicht ermitteln kann. Leer = keine Einschränkung. Geprüft wird die Adresse, von der die Verbindung kommt; hinter einem Reverse-Proxy muss der Server so konfiguriert sein, dass er diesem Proxy vertraut, damit stattdessen die ursprüngliche Client-Adresse geprüft wird.', + }, + api_key_default_expiry_days: { + label: 'Standard-Schlüssellaufzeit (Tage)', + help: 'Ein ohne Ablaufdatum erstellter Schlüssel läuft so viele Tage nach seiner Erstellung ab. Nie länger als die Höchstlaufzeit unten.', + }, + api_key_max_expiry_days: { + label: 'Maximale Schlüssellaufzeit (Tage)', + help: 'Das späteste Ablaufdatum, das ein neuer Schlüssel anfordern darf, gerechnet ab Erstellung. Ein späteres wird abgelehnt; genau so viele Tage werden akzeptiert.', + }, + api_key_inactivity_days: { + label: 'Ablauf bei Inaktivität (Tage)', + help: 'Ein Schlüssel ohne erfasste Nutzung seit so vielen Tagen wird abgelehnt. Er wird nicht widerrufen; erstellen Sie einen neuen Schlüssel. Ein nie genutzter Schlüssel zählt ab seiner Erstellung oder, falls älter, ab dem Richtlinienbeginn unten. 0 deaktiviert den Ablauf bei Inaktivität.', + }, + api_key_policy_started_at: { + label: 'Beginn der Schlüsselrichtlinie', + help: 'Wird beim ersten Lauf des Servers mit dieser Richtlinie automatisch erfasst (UTC, ISO 8601). Davor ohne Ablaufdatum erstellte Schlüssel laufen ein Jahr danach ab; Schlüssel ohne erfasste Nutzung zählen ihre Inaktivität ab diesem Zeitpunkt. Eine Änderung verschiebt beide Daten für Schlüssel, denen noch keins zugewiesen ist.', + }, + google_enabled: { + label: 'Google-Anmeldung aktivieren', + help: 'Bietet die Google-Anmeldung an, sobald Client-ID und Client-Secret beide verfügbar sind: unten gespeichert oder in der Serverkonfiguration gesetzt, die Vorrang vor gespeicherten Werten hat. Solange nicht beide verfügbar sind, zeigt die Anmeldeseite keine Google-Schaltfläche, auch wenn dies eingeschaltet ist. Aus entzieht die Google-Anmeldung, selbst wenn der Server beide setzt. Legen Sie den OAuth-Client in der Google Cloud Console an.', + }, + google_client_id: { + label: 'Google-Client-ID', + help: 'OAuth-Client-ID aus der Google Cloud Console. Kann auch in der Serverkonfiguration gesetzt werden.', + }, + google_client_secret: { + label: 'Google-Client-Secret', + help: 'Wird verschlüsselt gespeichert. Kann auch in der Serverkonfiguration gesetzt werden.', + }, + }, + }, + storage: { + title: 'Dateispeicher', + description: 'Backend für Anhänge, Exporte und Benutzer-Uploads.', + groups: { + adapter: { title: 'Backend', description: 'Legen Sie fest, wo hochgeladene Dateien gespeichert werden.' }, + local: { title: 'Lokal' }, + s3: { title: 'S3' }, + limits: { title: 'Grenzen' }, + }, + keys: { + adapter: { + label: 'Adapter', + options: { local: 'Lokales Dateisystem', s3: 'S3 / S3-kompatibel' }, + }, + local_root: { + label: 'Stammverzeichnis', + help: 'Dateisystempfad, unter dem Dateien gespeichert werden. Relative Pfade werden vom CWD des Servers aus aufgelöst.', + }, + s3_bucket: { + label: 'Bucket', + help: 'Gemeinsamer Host-Bucket. Dateien pro Umgebung werden über das Präfix projects// getrennt.', + }, + s3_region: { label: 'Region', help: 'Beispiel: us-east-1' }, + s3_endpoint: { + label: 'Endpunkt', + help: 'Eigener Endpunkt für S3-kompatible Anbieter (R2, MinIO, Wasabi). Für AWS S3 leer lassen.', + }, + s3_access_key_id: { label: 'Zugriffsschlüssel-ID' }, + s3_secret_access_key: { label: 'Geheimer Zugriffsschlüssel' }, + s3_force_path_style: { + label: 'URLs im Pfadstil erzwingen', + help: 'Für MinIO und die meisten S3-kompatiblen Anbieter einschalten; für AWS S3 ausschalten.', + }, + presigned_ttl: { label: 'Gültigkeit vorsignierter URLs (Sekunden)' }, + session_ttl: { + label: 'Gültigkeit der Upload-Sitzung (Sekunden)', + help: 'Wie lange eine Upload-Sitzung in Teilen fortsetzbar bleibt.', + }, + max_upload_mb: { label: 'Max. Uploadgröße (MB)' }, + }, + actions: { + test: { label: 'Verbindung testen' }, + }, + banners: { + adapter_switch_warning: { + title: 'Vorhandene Dateien werden nicht migriert', + text: 'Ein Adapterwechsel migriert vorhandene Dateien nicht — mit dem bisherigen Adapter hochgeladene Dateien sind über den neuen nicht erreichbar.', + }, + }, + }, + ai: { + title: 'KI und Embedder', + description: 'LLM-Anbieter, Modell, Anmeldedaten und Embedder-Konfiguration für die KI- und Wissensdienste der Plattform.', + groups: { + provider: { + title: 'Anbieter', + description: 'Wählen Sie das LLM-Backend. Der Memory-Modus gibt die Eingabe zurück — nützlich für Tests, nie für die Produktion.', + }, + gateway: { + title: 'Vercel AI Gateway', + description: 'Router für mehrere Anbieter. Das Modell wird als provider/model geschrieben, z. B. openai/gpt-4o.', + }, + openai: { title: 'OpenAI' }, + anthropic: { title: 'Anthropic' }, + google: { title: 'Google' }, + deepseek: { + title: 'DeepSeek', + description: 'OpenAI-kompatible API unter https://api.deepseek.com. Die Base URL wird automatisch ausgefüllt.', + }, + dashscope: { + title: '阿里通义 DashScope', + description: 'OpenAI-kompatibler Endpunkt unter dashscope.aliyuncs.com/compatible-mode/v1. Die Base URL wird automatisch ausgefüllt.', + }, + cloudflare: { + title: 'Cloudflare AI Gateway', + description: 'Nutzt den /compat-Endpunkt, daher wird das Modell als provider/model geschrieben (z. B. openai/gpt-4o-mini, anthropic/claude-3-5-sonnet, deepseek/deepseek-chat). Hinweis: alibaba/qwen* wird von Cloudflare /compat NICHT unterstützt — für Qwen den Anbieter DashScope verwenden.', + }, + siliconflow: { + title: '硅基流动 SiliconFlow', + description: 'OpenAI-kompatibler Endpunkt unter api.siliconflow.cn/v1. Die Base URL wird automatisch ausgefüllt.', + }, + openrouter: { + title: 'OpenRouter', + description: 'Router für mehrere Anbieter unter openrouter.ai/api/v1. Die Base URL wird automatisch ausgefüllt.', + }, + titles: { + title: 'Gesprächstitel', + description: 'Erzeugt automatisch einen kurzen zusammenfassenden Titel für neue Gespräche.', + }, + defaults: { + title: 'Standardwerte für die Generierung', + description: 'Gelten, wenn eine Agent- oder Chat-Anfrage keinen eigenen Wert angibt.', + }, + observability: { title: 'Beobachtbarkeit' }, + embedder: { + title: 'Embedder', + description: 'Text→Vektor-Anbieter für Wissensquellen und RAG. Unabhängig vom Chat-Anbieter oben.', + }, + }, + keys: { + provider: { + label: 'Anbieter', + options: { + memory: 'Memory (Echo — nur für Tests)', + gateway: 'Vercel AI Gateway', + openai: 'OpenAI', + anthropic: 'Anthropic', + google: 'Google Generative AI', + deepseek: 'DeepSeek (OpenAI-kompatibel)', + dashscope: '阿里通义 DashScope (OpenAI-kompatibel)', + cloudflare: 'Cloudflare AI Gateway (OpenAI-kompatibel)', + siliconflow: '硅基流动 SiliconFlow (OpenAI-kompatibel)', + openrouter: 'OpenRouter (OpenAI-kompatibel)', + }, + }, + gateway_model: { + label: 'Gateway-Modell', + help: 'Wird anstelle des in der Serverkonfiguration gesetzten Modells verwendet. Beispiel: openai/gpt-4o', + }, + gateway_api_key: { + label: 'Gateway-API-Schlüssel', + help: 'Optional — nur nötig, wenn das Gateway Authentifizierung erzwingt.', + }, + openai_api_key: { + label: 'OpenAI-API-Schlüssel', + help: 'Wird anstelle des in der Serverkonfiguration gesetzten Schlüssels verwendet. Verschlüsselt gespeichert.', + }, + openai_model: { label: 'Modell', help: 'Standard-Modell-ID. Überschreibungen pro Agent haben Vorrang.' }, + openai_base_url: { + label: 'Base URL', + help: 'Überschreibung für Azure OpenAI oder selbst gehostete Gateways. Für api.openai.com leer lassen.', + }, + anthropic_api_key: { + label: 'Anthropic-API-Schlüssel', + help: 'Wird anstelle des in der Serverkonfiguration gesetzten Schlüssels verwendet. Verschlüsselt gespeichert.', + }, + anthropic_model: { label: 'Modell' }, + google_api_key: { + label: 'Google-API-Schlüssel', + help: 'Wird anstelle des in der Serverkonfiguration gesetzten Schlüssels verwendet. Verschlüsselt gespeichert.', + }, + google_model: { label: 'Modell' }, + deepseek_api_key: { label: 'DeepSeek-API-Schlüssel', help: 'sk-... — ausgestellt auf platform.deepseek.com.' }, + deepseek_model: { label: 'Modell', help: 'Beispiele: deepseek-chat (V3), deepseek-reasoner (R1 Reasoning).' }, + dashscope_api_key: { + label: 'DashScope-API-Schlüssel', + help: 'sk-... — ausgestellt auf dashscope.console.aliyun.com.', + }, + dashscope_model: { label: 'Modell' }, + cloudflare_account_id: { + label: 'Cloudflare-Konto-ID', + help: 'Die 32-stellige Hex-ID aus der URL Ihres Cloudflare-Dashboards.', + }, + cloudflare_gateway_id: { + label: 'Gateway-ID', + help: 'In Cloudflare → AI Gateway konfigurierter Gateway-Name. Standard ist „default“.', + }, + cloudflare_api_key: { + label: 'Cloudflare-AI-Gateway-Token', + help: 'Ausgestellt in AI Gateway → Registerkarte „API tokens“ (cfut_… oder sk_…).', + }, + cloudflare_model: { label: 'Modell' }, + siliconflow_api_key: { label: 'SiliconFlow-API-Schlüssel' }, + siliconflow_model: { + label: 'Modell', + help: 'Beispiele: Qwen/Qwen2.5-72B-Instruct, deepseek-ai/DeepSeek-V3, meta-llama/Meta-Llama-3.1-8B-Instruct.', + }, + openrouter_api_key: { label: 'OpenRouter-API-Schlüssel', help: 'sk-or-...' }, + openrouter_model: { + label: 'Modell', + help: 'Format: provider/model (z. B. anthropic/claude-3.5-sonnet, deepseek/deepseek-chat).', + }, + title_generation_enabled: { label: 'Gesprächstitel automatisch zusammenfassen' }, + title_max_length: { + label: 'Max. Titellänge (Zeichen)', + help: 'Harte Obergrenze für den erzeugten Titel. Längeres wird serverseitig abgeschnitten.', + }, + temperature: { label: 'Temperatur', help: '0 = deterministisch, 2 = sehr kreativ.' }, + max_tokens: { label: 'Max. Ausgabe-Tokens', help: 'Harte Obergrenze für die pro Antwort erzeugten Tokens.' }, + request_timeout_ms: { label: 'Anfrage-Timeout (ms)' }, + trace_enabled: { + label: 'Traces aufzeichnen', + help: 'Zeichnet jede KI-Anfrage samt Antwort auf, damit sich ein Gespräch bei Problemen prüfen und wiedergeben lässt.', + }, + log_prompts: { + label: 'Vollständige Prompts protokollieren', + help: 'Nimmt die gerenderten Prompts (nicht nur Metadaten) in die Trace-Zeilen auf.', + }, + embedder_provider: { + label: 'Anbieter', + options: { + none: 'Deaktiviert (keine Embeddings)', + openai: 'OpenAI', + azure: 'Azure OpenAI', + dashscope: '阿里通义 DashScope', + zhipu: '智谱 BigModel', + siliconflow: '硅基流动 SiliconFlow', + doubao: '火山引擎 Doubao', + minimax: 'MiniMax', + ollama: 'Ollama (lokal)', + custom: 'Benutzerdefiniert (OpenAI-kompatibel)', + }, + }, + embedder_api_key: { + label: 'Embedder-API-Schlüssel', + help: 'Bearer-Token, das im Authorization-Header gesendet wird. Für Ollama genügt jeder nicht leere Wert.', + }, + embedder_model: { + label: 'Modell', + help: 'Beispiele — OpenAI: text-embedding-3-small · 阿里通义: text-embedding-v3 · 智谱: embedding-3 · 硅基流动: BAAI/bge-m3 · Ollama: bge-m3', + }, + embedder_base_url: { + label: 'Base URL', + help: 'Wurzel des Endpunkts (ohne /embeddings). Wird aus der Voreinstellung ausgefüllt; für Proxys oder selbst gehostete Gateways überschreiben.', + }, + embedder_dimensions: { + label: 'Dimensionen', + help: 'Überschreibt die Ausgabedimension (nur Matryoshka-Modelle). Leer lassen für den Modellstandard.', + }, + embedder_batch_size: { + label: 'Batchgröße', + help: 'Anzahl der Chunks, die pro Anfrage an den Embedding-Anbieter gehen. Verringern, wenn Raten- oder Größenlimits des Anbieters greifen.', + }, + }, + actions: { + test: { label: 'Verbindung testen' }, + test_embedder: { label: 'Embedder testen' }, + reset: { label: 'Auf Umgebungsstandards zurücksetzen' }, + }, + banners: { + log_prompts_warning: { + title: 'Prompts können personenbezogene Daten enthalten', + text: 'Kann personenbezogene Daten preisgeben — in regulierten Umgebungen abschalten.', + }, + }, + }, + knowledge: { + title: 'Wissen', + description: 'Vektorspeicher-Backend für RAG und Wissensquellen.', + groups: { + adapter: { + title: 'Backend', + description: 'Legen Sie fest, wo Dokument-Chunks und ihre Vektoren gespeichert werden.', + }, + turso: { + title: 'Turso / libSQL', + description: 'Funktioniert mit verwaltetem Turso, lokaler Datei oder im Arbeitsspeicher.', + }, + ragflow: { + title: 'RAGFlow', + description: 'Externe RAGFlow-Bereitstellung. Anleitung zum Selbsthosten unter https://ragflow.io.', + }, + indexing: { + title: 'Standardwerte für die Indexierung', + description: 'Ein an der Wissensquelle selbst gesetzter Wert hat Vorrang vor diesen Standardwerten.', + }, + permissions: { title: 'Berechtigungen' }, + }, + keys: { + adapter: { + label: 'Adapter', + options: { + memory: 'Im Arbeitsspeicher (nur Entwicklung / Test)', + turso: 'Turso / libSQL (Cloud oder lokal)', + ragflow: 'RAGFlow (extern)', + }, + }, + turso_url: { + label: 'Verbindungs-URL', + help: 'Beispiele: libsql://your-tenant.turso.io · file:./.objectstack/knowledge.db · :memory:', + }, + turso_auth_token: { label: 'Auth-Token', help: 'Nur für verwaltete Turso-URLs erforderlich.' }, + ragflow_base_url: { label: 'Base URL', help: 'Beispiel: http://localhost:9380' }, + ragflow_api_key: { label: 'API-Schlüssel' }, + ragflow_default_dataset: { + label: 'Standard-Dataset-ID', + help: 'Wird verwendet, wenn eine Wissensquelle kein eigenes RAGFlow-Dataset nennt.', + }, + chunk_target: { + label: 'Ziel-Chunkgröße (Zeichen)', + help: 'Weiche Obergrenze der Chunkgröße, bevor die tokenbasierte Aufteilung greift.', + }, + chunk_overlap: { + label: 'Chunk-Überlappung (Zeichen)', + help: 'Aus dem vorigen Chunk übernommene Zeichen, damit der Kontext über die Grenze erhalten bleibt.', + }, + over_fetch: { + label: 'Over-Fetch-Faktor', + help: 'Die Suche holt das Vielfache der angeforderten Ergebnisanzahl, damit nach dem Filtern nach Dokumentdetails genug zum Zurückgeben bleiben.', + }, + enforce_rls: { + label: 'RLS bei der Suche erzwingen', + help: 'Prüft jeden Treffer erneut gegen die Berechtigungen des Aufrufers auf Datensatzebene.', + }, + }, + actions: { + test: { label: 'Verbindung testen' }, + }, + banners: { + adapter_switch_warning: { + title: 'Vorhandene Indizes werden nicht migriert', + text: 'Ein Adapterwechsel migriert vorhandene Indizes NICHT — mit dem bisherigen Adapter indexierte Dokumente sind bis zur Neuindexierung nicht erreichbar.', + }, + enforce_rls_warning: { + title: 'RLS in der Produktion eingeschaltet lassen', + text: 'Das Abschalten umgeht den plattformeigenen Schutz vor Datenabfluss aus dem Vektorspeicher — in der Produktion eingeschaltet lassen.', + }, + }, + }, + lifecycle: { + title: 'Datenlebenszyklus', + description: 'Wie lange Systemdaten aufbewahrt werden: Aufbewahrungsfenster überschreiben, Zeilenanzahl pro Tabelle begrenzen und benachrichtigt werden, wenn eine Tabelle unerwartet wächst. Die Plattform wendet dies bei ihrer periodischen Bereinigung an.', + keys: { + enabled: { + label: 'Lebenszyklus-Richtlinien durchsetzen', + help: 'Hauptschalter der periodischen Bereinigung. Aus = deklarierte Aufbewahrungs-/TTL-/Rotationsrichtlinien werden nicht mehr durchgesetzt (Daten wachsen wieder unbegrenzt).', + }, + retention_overrides: { + label: 'Aufbewahrungs-Überschreibungen', + help: 'Fensterüberschreibungen pro Objekt: { "": { "maxAge": "1y", "expireAfter": "30d" } }. Dauern verwenden h/d/w/y. Pro Organisation gesetzt, sodass eine regulierte Organisation Jahre aufbewahren kann, eine Entwicklungsorganisation nur Tage. Eine Überschreibung, die kürzer ist als das Minimum, das eine Plattformfunktion braucht (etwa das Duplikaterkennungsfenster der Job-Warteschlange), wird ignoriert und als Fehler protokolliert; das Standardfenster gilt weiter.', + }, + quotas: { + label: 'Zeilenkontingente', + help: 'Maximale Zeilenanzahl pro Objekt: { "": 1000000 }. Eine Überschreitung löst eine Governance-Warnung aus — Kontingente löschen nie über die deklarierte Richtlinie hinaus.', + }, + quota_defaults: { + label: 'Standardkontingente pro Klasse', + help: 'Ersatzkontingente nach Lebenszyklusklasse: { "telemetry": 1000000, "transient": 500000, "event": 100000, "audit": 5000000 }.', + }, + growth_alert_rows: { + label: 'Wachstums-Warnschwelle (Zeilen pro Durchlauf)', + help: 'Warnt, wenn eine Tabelle mit deklariertem Lebenszyklus zwischen zwei Durchläufen um mehr als so viele Zeilen wächst. 0 deaktiviert Wachstumswarnungen.', + }, + }, + }, + }, +}; diff --git a/packages/services/service-settings/src/translations/index.ts b/packages/services/service-settings/src/translations/index.ts index c219b037772..58bb370a791 100644 --- a/packages/services/service-settings/src/translations/index.ts +++ b/packages/services/service-settings/src/translations/index.ts @@ -26,8 +26,11 @@ import { zhHant } from './zh-Hant.js'; import { ar } from './ar.js'; import { fr } from './fr.js'; import { ptBR } from './pt-BR.js'; +import { ru } from './ru.js'; +import { de } from './de.js'; +import { ko } from './ko.js'; -export { en, zhCN, jaJP, esES, zhHant, ar, fr, ptBR }; +export { en, zhCN, jaJP, esES, zhHant, ar, fr, ptBR, ru, de, ko }; export const settingsBuiltinTranslations: PlatformTranslationBundle = { en, @@ -38,4 +41,7 @@ export const settingsBuiltinTranslations: PlatformTranslationBundle = { 'ar': ar, 'fr': fr, 'pt-BR': ptBR, + 'ru': ru, + 'de': de, + 'ko': ko, }; diff --git a/packages/services/service-settings/src/translations/ko.ts b/packages/services/service-settings/src/translations/ko.ts new file mode 100644 index 00000000000..2c343bcc0a8 --- /dev/null +++ b/packages/services/service-settings/src/translations/ko.ts @@ -0,0 +1,558 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { PlatformTranslationData } from '@objectstack/spec/system'; +import { countryOptionLabels } from '../manifests/company.manifest.js'; + +/** + * 한국어 (ko) — built-in settings manifest translations. + */ +export const ko: PlatformTranslationData = { + settingsCommon: { + sourceLabels: { env: '환경 변수', global: '전역', tenant: '조직', user: '사용자', default: '기본값' }, + }, + settings: { + mail: { + title: '메일 전송', + description: 'SMTP 및 트랜잭션 이메일 공급자 설정입니다.', + groups: { + provider: { title: '공급자', description: '이 시스템이 발신 이메일을 보내는 방식을 선택합니다.' }, + smtp: { title: 'SMTP' }, + api_key: { title: 'API 키' }, + from_address: { title: '보내는 주소' }, + delivery: { title: '전송 방식', description: '발신 메일을 공급자에게 넘기는 방식입니다.' }, + }, + keys: { + provider: { + label: '공급자', + help: '이 서버가 실제로 전송할 수 있는 공급자만 표시됩니다. SendGrid와 Amazon SES는 SMTP로 설정합니다.', + options: { smtp: 'SMTP', resend: 'Resend', postmark: 'Postmark', log: '없음(로그만 기록 — 실제 전송 안 함)' }, + }, + smtp_host: { label: '호스트', help: '예: smtp.example.com' }, + smtp_port: { label: '포트' }, + smtp_secure: { label: 'TLS 사용' }, + smtp_user: { label: '사용자 이름' }, + smtp_password: { label: '비밀번호' }, + api_key: { label: 'API 키' }, + from_email: { label: '보내는 이메일', help: '예: no-reply@example.com' }, + from_name: { label: '보내는 사람 이름', help: '비워 두면 제품 이름(브랜딩) 또는 이 배포를 설정할 때 지정한 이름을 사용합니다.' }, + queue_delivery: { + label: '영구 대기열 전송', + help: '각 메일을 바로 보내지 않고 작업 대기열에 넘깁니다. 전송에 실패하면 worker가 백오프로 재시도하며, 재시작 후에도 유지됩니다. 영구 어댑터를 갖춘 대기열 기능이 필요합니다. "테스트 이메일 보내기"는 항상 바로 보냅니다.', + }, + }, + actions: { + test: { label: '테스트 이메일 보내기' }, + }, + }, + sms: { + title: 'SMS 전송', + description: 'OTP 로그인, 초대, 알림에 쓰는 SMS 공급자 설정입니다.', + groups: { + provider: { title: '공급자', description: '이 시스템이 발신 SMS를 보내는 방식을 선택합니다.' }, + aliyun: { title: 'Aliyun SMS' }, + twilio: { title: 'Twilio' }, + limits: { title: '지출 한도', description: '배포의 발신 SMS 양을 제한합니다. SMS는 유료 채널이며 발송할 때마다 실제 비용이 듭니다.' }, + }, + keys: { + provider: { + label: '공급자', + options: { log: '없음(로그만 기록 — 실제 전송 안 함)', aliyun: 'Aliyun SMS (阿里云短信)', twilio: 'Twilio' }, + }, + aliyun_access_key_id: { label: 'AccessKey ID' }, + aliyun_access_key_secret: { label: 'AccessKey Secret' }, + aliyun_sign_name: { label: '서명 이름(短信签名)' }, + aliyun_template_code: { + label: '기본 템플릿 코드(短信模板)', + help: '발송에 템플릿이 지정되지 않았을 때 사용합니다. ${content} 변수 하나만 있는 범용 템플릿을 쓰면 일반 알림 SMS를 보낼 수 있습니다.', + }, + twilio_account_sid: { label: 'Account SID' }, + twilio_auth_token: { label: '인증 토큰' }, + twilio_from_number: { + label: '발신 번호', + help: 'E.164 형식의 발신자(예: +15005550006). 이 항목과 Messaging Service SID 중 하나를 사용합니다.', + }, + twilio_messaging_service_sid: { label: 'Messaging Service SID' }, + daily_quota: { + label: '일일 발송 한도', + help: '이 배포가 UTC 기준 하루에 보낼 수 있는 최대 SMS 수로, OTP 로그인·초대·알림을 모두 포함합니다. 0은 제한 없음입니다. 한도를 넘는 발송은 00:00 UTC까지 거부됩니다.', + }, + }, + actions: { + test: { label: '테스트 SMS 보내기' }, + }, + }, + branding: { + title: '브랜딩', + description: '콘솔과 로그인 페이지에 표시되는 이름, 로고, 색상입니다. 설정하지 않은 값은 배포 자체의 브랜딩을 따릅니다.', + groups: { + identity: { title: 'ID 정보' }, + appearance: { title: '모양' }, + }, + keys: { + workspace_name: { + label: '제품 이름', + help: '콘솔, 로그인 페이지, 브라우저 제목에 표시되는 제품 이름이며 로그인 이메일, 인증 앱, MCP 로그인에도 쓰입니다. 비워 두면 이 배포를 설정할 때 지정한 이름을 사용합니다. 조직 자체의 이름이 아니며, 그 이름은 조직 레코드의 "이름"입니다.', + }, + accent_color: { label: '강조 색상', help: '브라우저와 설치된 앱의 테마 색상입니다. 각 앱은 자체 색상을 유지합니다.' }, + logo_url: { label: '로고 URL', help: '로그인 페이지, 로딩 화면, 홈 헤더에 표시됩니다. 예: https://…/logo.svg' }, + }, + }, + company: { + title: '회사', + description: '법인 정보 — 등록 상호, 주소, 세금 ID, 주요 연락처입니다.', + groups: { + identity: { title: '법인 정보' }, + address: { title: '등록 주소' }, + contact: { title: '연락처' }, + }, + keys: { + legal_name: { label: '법적 상호', help: '계약서와 청구서에 기재되는 조직의 등록 법적 상호입니다.' }, + registration_number: { label: '등록 번호', help: '회사 등록/설립 번호(예: EIN)입니다.' }, + tax_id: { label: '세금 ID / VAT', help: '청구서에 표시됩니다(예: VAT, GST, ABN).' }, + address_line1: { label: '주소 1' }, + address_line2: { label: '주소 2' }, + city: { label: '도시' }, + state: { label: '주/도' }, + postal_code: { label: '우편번호' }, + country: { + label: '국가', + help: '등록 주소의 국가입니다. 국가 이름을 입력한 뒤 목록에서 선택하세요.', + options: countryOptionLabels('ko'), + }, + phone: { label: '전화', help: 'E.164 형식을 권장합니다(예: +82 2 5555 0100).' }, + website: { label: '웹사이트', help: '예: https://example.com' }, + primary_contact_name: { label: '주요 연락처 이름' }, + primary_contact_email: { label: '주요 연락처 이메일', help: '예: ops@example.com' }, + }, + }, + localization: { + title: '현지화', + description: '기본 시간대, 언어, 국가, 통화, 회계연도입니다.', + groups: { + region: { title: '지역' }, + finance: { title: '재무' }, + }, + keys: { + timezone: { + label: '기본 시간대', + help: '수식에서 "오늘"이 언제인지, 보고서가 날짜를 어떻게 묶는지, 날짜와 시간을 어떻게 표시하는지를 정합니다. 모든 IANA 시간대 이름(예: Asia/Seoul)을 사용할 수 있습니다. 조직에 실제로 적용되는 시간대이며, 조직 레코드의 "시간대" 필드는 이를 바꾸지 않습니다.', + options: { + UTC: 'UTC', + 'America/Los_Angeles': '(UTC−08/−07) 로스앤젤레스', + 'America/Denver': '(UTC−07/−06) 덴버', + 'America/Chicago': '(UTC−06/−05) 시카고', + 'America/New_York': '(UTC−05/−04) 뉴욕', + 'America/Sao_Paulo': '(UTC−03) 상파울루', + 'Europe/London': '(UTC±00/+01) 런던', + 'Europe/Paris': '(UTC+01/+02) 파리', + 'Europe/Berlin': '(UTC+01/+02) 베를린', + 'Europe/Moscow': '(UTC+03) 모스크바', + 'Asia/Dubai': '(UTC+04) 두바이', + 'Asia/Kolkata': '(UTC+05:30) 콜카타', + 'Asia/Singapore': '(UTC+08) 싱가포르', + 'Asia/Shanghai': '(UTC+08) 상하이', + 'Asia/Tokyo': '(UTC+09) 도쿄', + 'Australia/Sydney': '(UTC+10/+11) 시드니', + 'Pacific/Auckland': '(UTC+12/+13) 오클랜드', + }, + }, + locale: { label: '기본 언어', help: '메시지 카탈로그와 숫자/날짜 형식에 쓰는 BCP-47 언어입니다.' }, + default_country: { + label: '기본 국가', + help: '주소와 전화번호의 기본값으로 쓰입니다. 국가 이름을 입력한 뒤 목록에서 선택하세요.', + options: countryOptionLabels('ko'), + }, + currency: { + label: '기본 통화', + help: '통화 필드에 자체 코드가 없을 때 적용되는 ISO 4217 코드입니다. 비워 두면 코드 없는 금액을 일반 숫자로 표시합니다.', + options: { + USD: 'USD — 미국 달러', + EUR: 'EUR — 유로', + GBP: 'GBP — 영국 파운드', + JPY: 'JPY — 일본 엔', + CNY: 'CNY — 중국 위안', + INR: 'INR — 인도 루피', + AUD: 'AUD — 호주 달러', + CAD: 'CAD — 캐나다 달러', + BRL: 'BRL — 브라질 헤알', + }, + }, + fiscal_year_start: { + label: '회계연도 시작', + help: '회계연도의 첫 달입니다. 보고서의 "이번 분기 / 회계연도"를 결정합니다.', + options: { + january: '1월', + february: '2월', + march: '3월', + april: '4월', + may: '5월', + june: '6월', + july: '7월', + august: '8월', + september: '9월', + october: '10월', + november: '11월', + december: '12월', + }, + }, + }, + }, + auth: { + title: '인증', + description: '로그인, 가입, 기본 제공 인증 기능을 제어합니다.', + groups: { + email_password: { title: '이메일 및 비밀번호', description: '로컬 이메일/비밀번호 로그인과 셀프 가입을 제어합니다.' }, + membership: { title: '멤버십', description: '새로 만든 사용자가 무엇에 가입되는지 정합니다. 위의 셀프 가입과 함께 설정합니다.' }, + audience: { + title: '가입 대상', + description: '이 시스템의 앱 사용자가 될 수 있는 사람을 정합니다. 허용 목록 이메일 도메인은 항상 이메일 인증을 강제합니다. "개방"도 배포에서 끄지 않는 한 이를 강제하며, 이 콘솔에 저장한 값으로는 끌 수 없습니다. 초대, 관리자가 만든 사용자, SCIM 프로비저닝, 엔터프라이즈 SSO는 모든 모드에서 허용됩니다.', + }, + password_policy: { title: '비밀번호 정책', description: '가입 및 비밀번호 재설정 시 인증 공급자가 적용하는 길이 제한입니다.' }, + anti_abuse: { title: '악용 방지', description: '무차별 대입 방지: ID별 계정 잠금과 인증 엔드포인트에 대한 IP별 속도 제한입니다.' }, + multi_factor: { title: '다단계', description: '구성원이 인증 앱(TOTP)으로 계정을 보호하도록 요구합니다.' }, + sessions: { title: '세션', description: '로그인 세션이 유효한 기간입니다.' }, + network: { title: '네트워크', description: '사용자가 인증할 수 있는 위치를 제한합니다.' }, + api_keys: { + title: 'API 키', + description: '개인 API 키의 유효 기간입니다. 만료되었거나, 비활성 기간을 넘겨 사용되지 않았거나, 취소된 키는 각각의 오류 코드로 거부되며, 해결 방법은 항상 새 키를 만드는 것입니다.', + }, + social: { title: '소셜 로그인', description: '기본 제공 Google 로그인 공급자를 설정합니다. 서버 설정에 지정된 자격 증명이 여전히 우선합니다.' }, + }, + keys: { + email_password_enabled: { label: '이메일/비밀번호 로그인 사용', confirmOffText: '비밀번호로 로그인하는 사람은 다른 로그인 방법이 필요합니다.' }, + signup_enabled: { + label: '이메일 가입 허용', + help: '이메일·비밀번호 가입 양식을 엽니다. 누구를 받아들일지는 아래의 "셀프 가입 대상"이 정합니다. "초대 전용"에서는 대기 중인 초대가 있는 주소만, "허용 목록 이메일 도메인"에서는 허용된 도메인과 초대된 주소를, "개방"에서는 누구나 받아들입니다. 끄면 대상과 관계없이 양식이 닫히며 초대받은 사람도 예외가 아닙니다.', + }, + require_email_verification: { label: '이메일 인증 요구' }, + membership_policy: { + label: '새 사용자 멤버십', + help: '"자동"은 모든 새 사용자를 기본 조직에 연결합니다. "초대 전용"은 명시적인 행동이 있을 때만 멤버십을 부여합니다. 조직 생성, 초대 수락, 관리자의 추가, SSO 적시 프로비저닝이 그런 행동입니다.', + options: { auto: '기본 조직에 자동 가입', 'invite-only': '초대 전용 — 자동 가입 안 함' }, + }, + audience_posture: { + label: '셀프 가입 대상', + help: '"초대 전용"은 셀프 가입을 닫습니다. 관리자가 초대·생성·가져오기를 하거나 디렉터리 프로비저닝(SCIM) 또는 엔터프라이즈 SSO를 통해서만 사용자가 됩니다. "허용 목록 이메일 도메인"은 아래에 나열한 도메인에 가입을 엽니다. "개방"은 누구나 받아들입니다. "허용 목록 이메일 도메인"과 "개방" 모두 아래의 셀프 가입 권한 세트가 필요합니다. "허용 목록 이메일 도메인"은 항상 이메일 인증을 요구합니다. "개방"도 서버 설정에서 끄지 않는 한 요구하며, "개방"에서 이 콘솔로 끄려고 하면 거부됩니다.', + options: { invite_only: '초대 전용 — 셀프 가입 없음(기본값)', email_domain: '허용 목록 이메일 도메인만', open: '개방 — 누구나 셀프 가입 가능' }, + }, + audience_allowed_email_domains: { + label: '허용된 이메일 도메인', + help: '도메인만 한 줄에 하나씩 또는 쉼표로 구분해 입력합니다(예: acme.com). 대소문자를 구분하지 않고 정확히 일치해야 하며, 하위 도메인은 따로 입력해야 하고, 와일드카드는 지원하지 않습니다.', + }, + audience_self_registration_permission_set: { + label: '셀프 가입 권한 세트', + help: '셀프 가입한 각 사용자가 받는 권한 세트의 이름입니다. 표준 구성원 세트면 충분하며, 전체 관리자 세트는 거부됩니다. 가입 대상이 셀프 가입을 허용하면 필수입니다.', + }, + password_min_length: { label: '최소 비밀번호 길이' }, + password_max_length: { label: '최대 비밀번호 길이', help: '매우 긴 비밀번호 해싱으로 인한 서비스 거부를 막습니다.' }, + password_reject_breached: { + label: '유출된 비밀번호 거부', + help: 'Have I Been Pwned를 통해 공개 유출 자료에 있는 비밀번호를 차단합니다(k-익명성 범위 검사, 비밀번호 전체는 절대 전송하지 않음).', + }, + password_require_complexity: { + label: '복잡한 비밀번호 요구', + help: '가입 및 비밀번호 변경/재설정 시 비밀번호에 여러 문자 종류(대문자, 소문자, 숫자, 기호)를 섞도록 요구합니다.', + }, + password_min_classes: { label: '최소 문자 종류 수', help: '비밀번호에 포함해야 하는 네 가지 종류(대문자/소문자/숫자/기호)의 개수입니다.' }, + password_history_count: { + label: '비밀번호 기록(재사용 금지)', + help: '변경/재설정 시 이 개수만큼의 이전 비밀번호를 재사용하지 못하게 합니다. 0이면 검사를 끕니다.', + }, + password_expiry_days: { + label: '비밀번호 만료(일)', + help: '이 일수가 지나면 비밀번호 변경을 강제합니다. 0이면 만료되지 않습니다. 만료된 동안 사용자는 비밀번호를 바꿀 때까지 데이터에 접근할 수 없습니다.', + }, + lockout_threshold: { + label: '계정 잠금 임계값', + help: '연속 로그인 실패가 이 횟수에 이르면 계정을 잠급니다. 잘못된 비밀번호와 잘못된 2단계 인증 코드가 모두 포함됩니다. 잠긴 동안에는 올바른 자격 증명으로도 로그인이 거부됩니다. 0이면 비밀번호 단계의 잠금을 끕니다. 이때 2단계 인증은 세션 발급 전 마지막 검사이므로 15분당 10회라는 기본 제한을 유지합니다.', + }, + lockout_duration_minutes: { label: '잠금 시간(분)', help: '임계값을 넘은 뒤 계정이 잠긴 상태로 유지되는 시간이며, 두 로그인 단계 모두에 적용됩니다.' }, + rate_limit_max: { label: '인증 속도 제한: 최대 요청 수', help: '창당 IP별로 로그인/가입/비밀번호 재설정 엔드포인트에 보낼 수 있는 최대 요청 수입니다.' }, + rate_limit_window_seconds: { label: '인증 속도 제한: 창(초)', help: '위의 요청 상한을 세는 슬라이딩 창입니다.' }, + mfa_required: { + label: '다단계 인증 요구', + help: '인증 앱을 등록하지 않은 사용자는 유예 기간이 끝나면 데이터에 접근할 수 없습니다. 이 항목을 켜면 사용자가 등록할 수 있도록 2단계 인증 기능도 켜집니다.', + }, + mfa_grace_period_days: { label: 'MFA 유예 기간(일)', help: '강제 차단 전까지 사용자가 등록을 미룰 수 있는 기간입니다. 0이면 즉시 차단합니다.' }, + session_expiry_days: { label: '세션 유효 기간(일)', help: '세션은 로그인 후 이 일수가 지나면 만료됩니다.' }, + session_refresh_days: { label: '갱신 임계값(일)', help: '활성 세션이 이 기간보다 오래되면 연장됩니다.' }, + session_idle_timeout_minutes: { label: '유휴 시간 제한(분)', help: '이 시간(분) 동안 활동이 없으면 사용자를 로그아웃합니다. 0이면 끕니다.' }, + session_absolute_max_hours: { + label: '세션 절대 유효 기간(시간)', + help: '활동 여부와 관계없이 로그인 후 이 시간이 지나면 다시 인증하도록 합니다. 0이면 끕니다.', + }, + max_concurrent_sessions_per_user: { + label: '사용자당 최대 동시 세션 수', + help: '사용자당 동시에 로그인한 세션 수를 제한하며, 상한을 넘으면 가장 오래된 세션부터 로그아웃됩니다. 0 = 무제한.', + }, + allowed_ip_ranges: { + label: '허용된 IP 범위', + help: 'CIDR 범위 또는 정확한 IP(한 줄에 하나 또는 쉼표로 구분)입니다(예: 203.0.113.0/24). 설정하면 이 범위 밖에서의 로그인이 거부되며, 서버가 클라이언트 주소를 확인할 수 없는 요청도 거부됩니다. 비워 두면 제한이 없습니다. 판단 대상은 연결이 들어온 주소이며, 리버스 프록시 뒤에 있다면 서버가 해당 프록시를 신뢰하도록 설정해야 원래 클라이언트 주소로 판단합니다.', + }, + api_key_default_expiry_days: { + label: '기본 키 유효 기간(일)', + help: '만료일 없이 만든 키는 생성 후 이 일수가 지나면 만료됩니다. 아래의 최대 유효 기간을 넘지 않습니다.', + }, + api_key_max_expiry_days: { + label: '최대 키 유효 기간(일)', + help: '새 키가 요청할 수 있는 가장 먼 만료일로, 생성 시점부터 계산합니다. 더 길면 거부되고, 정확히 이 일수는 허용됩니다.', + }, + api_key_inactivity_days: { + label: '비활성 만료(일)', + help: '이 일수 동안 사용 기록이 없는 키는 거부됩니다. 키가 취소되지는 않으니 새 키를 만드세요. 한 번도 쓰지 않은 키는 생성 시점부터, 그보다 오래된 키는 아래의 정책 시작 시점부터 계산합니다. 0이면 비활성 만료를 끕니다.', + }, + api_key_policy_started_at: { + label: '키 정책 시작 시각', + help: '서버가 이 정책으로 처음 실행될 때 자동으로 기록됩니다(UTC, ISO 8601). 그 전에 만료일 없이 만든 키는 이 시점으로부터 1년 뒤에 만료되고, 사용 기록이 없는 키는 이 시점부터 비활성 기간을 셉니다. 이 값을 바꾸면 아직 날짜가 정해지지 않은 키의 두 날짜가 함께 바뀝니다.', + }, + google_enabled: { + label: 'Google 로그인 사용', + help: '클라이언트 ID와 클라이언트 보안 비밀이 모두 있을 때 Google 로그인을 제공합니다. 아래에 저장한 값이나 서버 설정(저장한 값보다 우선)에서 가져옵니다. 둘 다 있기 전까지는 이 항목이 켜져 있어도 로그인 페이지에 Google 버튼이 나타나지 않습니다. 끄면 서버가 둘 다 설정했더라도 Google 로그인을 철회합니다. OAuth 클라이언트는 Google Cloud Console에서 만드세요.', + }, + google_client_id: { + label: 'Google 클라이언트 ID', + help: 'Google Cloud Console에서 발급한 OAuth 클라이언트 ID입니다. 서버 설정에서도 지정할 수 있습니다.', + }, + google_client_secret: { label: 'Google 클라이언트 보안 비밀', help: '암호화해 저장합니다. 서버 설정에서도 지정할 수 있습니다.' }, + }, + }, + storage: { + title: '파일 저장소', + description: '첨부 파일, 내보내기, 사용자 업로드에 쓰는 백엔드입니다.', + groups: { + adapter: { title: '백엔드', description: '업로드한 파일을 저장할 위치를 선택합니다.' }, + local: { title: '로컬' }, + s3: { title: 'S3' }, + limits: { title: '제한' }, + }, + keys: { + adapter: { + label: '어댑터', + options: { local: '로컬 파일 시스템', s3: 'S3 / S3 호환' }, + }, + local_root: { label: '루트 디렉터리', help: '파일을 저장할 파일 시스템 경로입니다. 상대 경로는 서버의 CWD를 기준으로 해석됩니다.' }, + s3_bucket: { label: '버킷', help: '공유 호스트 버킷입니다. 환경별 파일은 projects// 접두사로 구분됩니다.' }, + s3_region: { label: '리전', help: '예: us-east-1' }, + s3_endpoint: { label: '엔드포인트', help: 'S3 호환 공급자(R2, MinIO, Wasabi)용 사용자 지정 엔드포인트입니다. AWS S3는 비워 두세요.' }, + s3_access_key_id: { label: '액세스 키 ID' }, + s3_secret_access_key: { label: '보안 액세스 키' }, + s3_force_path_style: { label: '경로 스타일 URL 강제', help: 'MinIO와 대부분의 S3 호환 공급자는 켜고, AWS S3는 끄세요.' }, + presigned_ttl: { label: '미리 서명된 URL 유효 시간(초)' }, + session_ttl: { label: '업로드 세션 유효 시간(초)', help: '분할 업로드 세션을 이어서 진행할 수 있는 시간입니다.' }, + max_upload_mb: { label: '최대 업로드 크기(MB)' }, + }, + actions: { + test: { label: '연결 테스트' }, + }, + banners: { + adapter_switch_warning: { + title: '기존 파일은 이전되지 않습니다', + text: '어댑터를 바꿔도 기존 파일은 이전되지 않습니다. 이전 어댑터로 업로드한 파일은 새 어댑터로 접근할 수 없게 됩니다.', + }, + }, + }, + ai: { + title: 'AI 및 Embedder', + description: '플랫폼 AI 및 지식 서비스가 쓰는 LLM 공급자, 모델, 자격 증명, Embedder 설정입니다.', + groups: { + provider: { + title: '공급자', + description: 'LLM 백엔드를 선택합니다. Memory 모드는 입력을 그대로 돌려주므로 테스트에는 유용하지만 운영 환경에는 절대 쓰지 마세요.', + }, + gateway: { + title: 'Vercel AI Gateway', + description: '여러 공급자를 잇는 라우터입니다. 모델은 provider/model 형식으로 씁니다(예: openai/gpt-4o).', + }, + openai: { title: 'OpenAI' }, + anthropic: { title: 'Anthropic' }, + google: { title: 'Google' }, + deepseek: { + title: 'DeepSeek', + description: 'https://api.deepseek.com의 OpenAI 호환 API입니다. Base URL은 자동으로 채워집니다.', + }, + dashscope: { + title: '阿里通义 DashScope', + description: 'dashscope.aliyuncs.com/compatible-mode/v1의 OpenAI 호환 엔드포인트입니다. Base URL은 자동으로 채워집니다.', + }, + cloudflare: { + title: 'Cloudflare AI Gateway', + description: '/compat 엔드포인트를 사용하므로 모델은 provider/model 형식으로 씁니다(예: openai/gpt-4o-mini, anthropic/claude-3-5-sonnet, deepseek/deepseek-chat). 참고: Cloudflare /compat는 alibaba/qwen*을 지원하지 않으니 Qwen에는 DashScope 공급자를 쓰세요.', + }, + siliconflow: { + title: '硅基流动 SiliconFlow', + description: 'api.siliconflow.cn/v1의 OpenAI 호환 엔드포인트입니다. Base URL은 자동으로 채워집니다.', + }, + openrouter: { title: 'OpenRouter', description: 'openrouter.ai/api/v1의 다중 공급자 라우터입니다. Base URL은 자동으로 채워집니다.' }, + titles: { title: '대화 제목', description: '새 대화에 짧은 요약 제목을 자동으로 만듭니다.' }, + defaults: { title: '생성 기본값', description: '에이전트나 채팅 요청이 자체 값을 지정하지 않을 때 적용됩니다.' }, + observability: { title: '관측성' }, + embedder: { title: 'Embedder', description: '지식 소스와 RAG에 쓰는 텍스트→벡터 공급자입니다. 위의 채팅 공급자와는 별개입니다.' }, + }, + keys: { + provider: { + label: '공급자', + options: { + memory: 'Memory(에코 — 테스트 전용)', + gateway: 'Vercel AI Gateway', + openai: 'OpenAI', + anthropic: 'Anthropic', + google: 'Google Generative AI', + deepseek: 'DeepSeek(OpenAI 호환)', + dashscope: '阿里通义 DashScope(OpenAI 호환)', + cloudflare: 'Cloudflare AI Gateway(OpenAI 호환)', + siliconflow: '硅基流动 SiliconFlow(OpenAI 호환)', + openrouter: 'OpenRouter(OpenAI 호환)', + }, + }, + gateway_model: { label: 'Gateway 모델', help: '서버 설정의 모델 대신 사용합니다. 예: openai/gpt-4o' }, + gateway_api_key: { label: 'Gateway API 키', help: '선택 사항 — 게이트웨이가 인증을 요구할 때만 필요합니다.' }, + openai_api_key: { label: 'OpenAI API 키', help: '서버 설정의 키 대신 사용합니다. 암호화해 저장합니다.' }, + openai_model: { label: '모델', help: '기본 모델 ID입니다. 에이전트별 재정의가 우선합니다.' }, + openai_base_url: { + label: 'Base URL', + help: 'Azure OpenAI나 자체 호스팅 게이트웨이용 재정의 값입니다. api.openai.com을 쓰려면 비워 두세요.', + }, + anthropic_api_key: { label: 'Anthropic API 키', help: '서버 설정의 키 대신 사용합니다. 암호화해 저장합니다.' }, + anthropic_model: { label: '모델' }, + google_api_key: { label: 'Google API 키', help: '서버 설정의 키 대신 사용합니다. 암호화해 저장합니다.' }, + google_model: { label: '모델' }, + deepseek_api_key: { label: 'DeepSeek API 키', help: 'sk-... — platform.deepseek.com에서 발급합니다.' }, + deepseek_model: { label: '모델', help: '예: deepseek-chat(V3), deepseek-reasoner(R1 추론).' }, + dashscope_api_key: { label: 'DashScope API 키', help: 'sk-... — dashscope.console.aliyun.com에서 발급합니다.' }, + dashscope_model: { label: '모델' }, + cloudflare_account_id: { label: 'Cloudflare 계정 ID', help: 'Cloudflare 대시보드 URL에 있는 32자 16진수 ID입니다.' }, + cloudflare_gateway_id: { + label: 'Gateway ID', + help: 'Cloudflare → AI Gateway에서 설정한 게이트웨이 이름입니다. 기본값은 "default"입니다.', + }, + cloudflare_api_key: { + label: 'Cloudflare AI Gateway 토큰', + help: 'AI Gateway → "API tokens" 탭에서 발급합니다(cfut_… 또는 sk_…).', + }, + cloudflare_model: { label: '모델' }, + siliconflow_api_key: { label: 'SiliconFlow API 키' }, + siliconflow_model: { + label: '모델', + help: '예: Qwen/Qwen2.5-72B-Instruct, deepseek-ai/DeepSeek-V3, meta-llama/Meta-Llama-3.1-8B-Instruct.', + }, + openrouter_api_key: { label: 'OpenRouter API 키', help: 'sk-or-...' }, + openrouter_model: { + label: '모델', + help: '형식: provider/model(예: anthropic/claude-3.5-sonnet, deepseek/deepseek-chat).', + }, + title_generation_enabled: { label: '대화 제목 자동 요약' }, + title_max_length: { label: '제목 최대 길이(자)', help: '생성된 제목의 하드 상한입니다. 더 긴 부분은 서버에서 잘립니다.' }, + temperature: { label: '온도', help: '0 = 결정적, 2 = 매우 창의적.' }, + max_tokens: { label: '최대 출력 토큰', help: '응답 하나당 생성되는 토큰의 하드 상한입니다.' }, + request_timeout_ms: { label: '요청 시간 제한(ms)' }, + trace_enabled: { label: '추적 기록', help: '각 AI 요청과 응답을 기록해 두어, 문제가 생기면 대화를 살펴보고 다시 재생할 수 있게 합니다.' }, + log_prompts: { label: '전체 프롬프트 기록', help: '추적 행에 메타데이터뿐 아니라 렌더링된 프롬프트도 포함합니다.' }, + embedder_provider: { + label: '공급자', + options: { + none: '사용 안 함(임베딩 없음)', + openai: 'OpenAI', + azure: 'Azure OpenAI', + dashscope: '阿里通义 DashScope', + zhipu: '智谱 BigModel', + siliconflow: '硅基流动 SiliconFlow', + doubao: '火山引擎 Doubao', + minimax: 'MiniMax', + ollama: 'Ollama(로컬)', + custom: '사용자 지정(OpenAI 호환)', + }, + }, + embedder_api_key: { + label: 'Embedder API 키', + help: 'Authorization 헤더로 보내는 Bearer 토큰입니다. Ollama는 비어 있지 않은 아무 값이나 됩니다.', + }, + embedder_model: { + label: '모델', + help: '예 — OpenAI: text-embedding-3-small · 阿里通义: text-embedding-v3 · 智谱: embedding-3 · 硅基流动: BAAI/bge-m3 · Ollama: bge-m3', + }, + embedder_base_url: { + label: 'Base URL', + help: '엔드포인트 루트(/embeddings 제외)입니다. 프리셋에서 자동으로 채워지며, 프록시나 자체 호스팅 게이트웨이를 쓰면 재정의하세요.', + }, + embedder_dimensions: { label: '차원', help: '출력 차원을 재정의합니다(Matryoshka 모델만). 비워 두면 모델 기본값을 씁니다.' }, + embedder_batch_size: { label: '배치 크기', help: '한 번의 요청으로 임베딩 공급자에게 보내는 청크 수입니다. 공급자의 속도/크기 제한에 걸리면 줄이세요.' }, + }, + actions: { + test: { label: '연결 테스트' }, + test_embedder: { label: 'Embedder 테스트' }, + reset: { label: '환경 기본값으로 재설정' }, + }, + banners: { + log_prompts_warning: { title: '프롬프트에 개인 정보가 포함될 수 있습니다', text: '개인 정보가 유출될 수 있으니 규제 환경에서는 끄세요.' }, + }, + }, + knowledge: { + title: '지식', + description: 'RAG / 지식 소스용 벡터 저장소 백엔드입니다.', + groups: { + adapter: { title: '백엔드', description: '문서 청크와 그 벡터를 저장할 위치를 선택합니다.' }, + turso: { title: 'Turso / libSQL', description: '관리형 Turso, 로컬 파일, 인메모리 모두에서 동작합니다.' }, + ragflow: { title: 'RAGFlow', description: '외부 RAGFlow 배포입니다. 자체 호스팅 방법은 https://ragflow.io를 참고하세요.' }, + indexing: { title: '인덱싱 기본값', description: '지식 소스 자체에 설정한 값이 이 기본값보다 우선합니다.' }, + permissions: { title: '권한' }, + }, + keys: { + adapter: { + label: '어댑터', + options: { memory: '인메모리(개발/테스트 전용)', turso: 'Turso / libSQL(클라우드 또는 로컬)', ragflow: 'RAGFlow(외부)' }, + }, + turso_url: { + label: '연결 URL', + help: '예: libsql://your-tenant.turso.io · file:./.objectstack/knowledge.db · :memory:', + }, + turso_auth_token: { label: '인증 토큰', help: '관리형 Turso URL에만 필요합니다.' }, + ragflow_base_url: { label: 'Base URL', help: '예: http://localhost:9380' }, + ragflow_api_key: { label: 'API 키' }, + ragflow_default_dataset: { label: '기본 데이터 세트 ID', help: '지식 소스가 자체 RAGFlow 데이터 세트를 지정하지 않을 때 사용합니다.' }, + chunk_target: { label: '목표 청크 크기(자)', help: '토큰 기준 분할이 시작되기 전의 청크 크기 소프트 상한입니다.' }, + chunk_overlap: { label: '청크 겹침(자)', help: '경계를 넘어 문맥이 이어지도록 이전 청크에서 유지하는 문자 수입니다.' }, + over_fetch: { label: '초과 조회 배수', help: '요청한 결과 수의 이 배수만큼 검색해, 문서 정보로 필터링한 뒤에도 돌려줄 결과가 충분히 남게 합니다.' }, + enforce_rls: { label: '검색 시 RLS 적용', help: '호출자의 레코드 수준 권한으로 모든 결과를 다시 확인합니다.' }, + }, + actions: { + test: { label: '연결 테스트' }, + }, + banners: { + adapter_switch_warning: { + title: '기존 인덱스는 이전되지 않습니다', + text: '어댑터를 바꿔도 기존 인덱스는 이전되지 않습니다. 이전 어댑터로 인덱싱한 문서는 다시 인덱싱할 때까지 접근할 수 없습니다.', + }, + enforce_rls_warning: { + title: '운영 환경에서는 RLS를 켜 두세요', + text: '끄면 벡터 저장소 데이터 유출을 막는 플랫폼 고유의 보호 장치를 건너뜁니다. 운영 환경에서는 켜 두세요.', + }, + }, + }, + lifecycle: { + title: '데이터 수명 주기', + description: '시스템 데이터를 얼마나 보관할지 정합니다. 보존 기간을 재정의하고, 테이블별 행 수를 제한하며, 테이블이 예상 밖으로 커지면 알림을 받습니다. 플랫폼은 주기적인 정리 작업 때 이를 적용합니다.', + keys: { + enabled: { + label: '수명 주기 정책 적용', + help: '주기적 정리 작업의 마스터 스위치입니다. 끄면 선언된 보존/TTL/교체 정책이 더 이상 적용되지 않습니다(데이터가 다시 무제한으로 늘어남).', + }, + retention_overrides: { + label: '보존 기간 재정의', + help: '객체별 보존 기간 재정의입니다: { "": { "maxAge": "1y", "expireAfter": "30d" } }. 기간 단위는 h/d/w/y입니다. 조직별로 설정하므로 규제 대상 조직은 수년, 개발용 조직은 며칠만 보관할 수 있습니다. 플랫폼 기능에 필요한 최소 기간(예: 작업 대기열의 중복 감지 기간)보다 짧은 재정의는 무시되고 오류로 기록되며, 기본 기간이 계속 적용됩니다.', + }, + quotas: { + label: '행 할당량', + help: '객체별 최대 행 수입니다: { "": 1000000 }. 초과하면 거버넌스 알림이 발생하며, 할당량은 선언된 정책을 넘어 데이터를 삭제하지 않습니다.', + }, + quota_defaults: { + label: '클래스별 기본 할당량', + help: '수명 주기 클래스별 대체 할당량입니다: { "telemetry": 1000000, "transient": 500000, "event": 100000, "audit": 5000000 }.', + }, + growth_alert_rows: { + label: '증가 알림 임계값(정리당 행 수)', + help: '수명 주기가 선언된 테이블이 두 번의 정리 사이에 이 행 수보다 많이 늘어나면 알립니다. 0이면 증가 알림을 끕니다.', + }, + }, + }, + }, +}; diff --git a/packages/services/service-settings/src/translations/ru.ts b/packages/services/service-settings/src/translations/ru.ts new file mode 100644 index 00000000000..28b6d7e471e --- /dev/null +++ b/packages/services/service-settings/src/translations/ru.ts @@ -0,0 +1,746 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { PlatformTranslationData } from '@objectstack/spec/system'; +import { countryOptionLabels } from '../manifests/company.manifest.js'; + +/** + * Русский (ru) — built-in settings manifest translations. + */ +export const ru: PlatformTranslationData = { + settingsCommon: { + sourceLabels: { + env: 'Окружение', + global: 'Глобально', + tenant: 'Организация', + user: 'Пользователь', + default: 'По умолчанию', + }, + }, + settings: { + mail: { + title: 'Доставка почты', + description: 'Настройка SMTP и провайдера транзакционной почты.', + groups: { + provider: { title: 'Провайдер', description: 'Выберите, как эта система отправляет исходящую почту.' }, + smtp: { title: 'SMTP' }, + api_key: { title: 'Ключ API' }, + from_address: { title: 'Адрес отправителя' }, + delivery: { title: 'Доставка', description: 'Как исходящая почта передаётся провайдеру.' }, + }, + keys: { + provider: { + label: 'Провайдер', + help: 'Показаны только провайдеры, через которые этот сервер действительно может доставлять почту. SendGrid и Amazon SES настраиваются как SMTP.', + options: { + smtp: 'SMTP', + resend: 'Resend', + postmark: 'Postmark', + log: 'Нет (только журнал — без реальной доставки)', + }, + }, + smtp_host: { label: 'Хост', help: 'Пример: smtp.example.com' }, + smtp_port: { label: 'Порт' }, + smtp_secure: { label: 'Использовать TLS' }, + smtp_user: { label: 'Имя пользователя' }, + smtp_password: { label: 'Пароль' }, + api_key: { label: 'Ключ API' }, + from_email: { label: 'E-mail отправителя', help: 'Пример: no-reply@example.com' }, + from_name: { + label: 'Имя отправителя', + help: 'Оставьте пустым, чтобы использовать название продукта (Брендинг) или имя, заданное при настройке этого развёртывания.', + }, + queue_delivery: { + label: 'Доставка через надёжную очередь', + help: 'Каждое письмо передаётся в очередь заданий вместо прямой отправки: неудачная доставка повторяется воркером с нарастающей задержкой и переживает перезапуск. Требуется возможность очереди с надёжным адаптером. «Отправить тестовое письмо» всегда отправляет напрямую.', + }, + }, + actions: { + test: { label: 'Отправить тестовое письмо' }, + }, + }, + sms: { + title: 'Доставка SMS', + description: 'Настройка SMS-провайдера для входа по OTP, приглашений и уведомлений.', + groups: { + provider: { title: 'Провайдер', description: 'Выберите, как эта система отправляет исходящие SMS.' }, + aliyun: { title: 'Aliyun SMS' }, + twilio: { title: 'Twilio' }, + limits: { + title: 'Лимиты расходов', + description: 'Ограничивает объём исходящих SMS развёртывания. SMS — платный канал, и каждая отправка стоит реальных денег.', + }, + }, + keys: { + provider: { + label: 'Провайдер', + options: { + log: 'Нет (только журнал — без реальной доставки)', + aliyun: 'Aliyun SMS (阿里云短信)', + twilio: 'Twilio', + }, + }, + aliyun_access_key_id: { label: 'AccessKey ID' }, + aliyun_access_key_secret: { label: 'AccessKey Secret' }, + aliyun_sign_name: { label: 'Подпись отправителя (短信签名)' }, + aliyun_template_code: { + label: 'Код шаблона по умолчанию (短信模板)', + help: 'Используется, если при отправке шаблон не указан. Универсальный шаблон с одной переменной ${content} позволяет отправлять обычные SMS-уведомления.', + }, + twilio_account_sid: { label: 'Account SID' }, + twilio_auth_token: { label: 'Токен авторизации' }, + twilio_from_number: { + label: 'Номер отправителя', + help: 'Отправитель в формате E.164, например +15005550006. Либо это поле, либо Messaging Service SID.', + }, + twilio_messaging_service_sid: { label: 'Messaging Service SID' }, + daily_quota: { + label: 'Дневной лимит отправки', + help: 'Максимум SMS, которые это развёртывание может отправить за сутки по UTC, включая вход по OTP, приглашения и уведомления. 0 — без ограничений. Отправки сверх лимита отклоняются до 00:00 UTC.', + }, + }, + actions: { + test: { label: 'Отправить тестовое SMS' }, + }, + }, + branding: { + title: 'Брендинг', + description: 'Название, логотип и цвет, которые показывают консоль и страница входа. Незаданное значение берётся из брендинга самого развёртывания.', + groups: { + identity: { title: 'Идентификация' }, + appearance: { title: 'Оформление' }, + }, + keys: { + workspace_name: { + label: 'Название продукта', + help: 'Название продукта в консоли, на странице входа и в заголовке браузера, а также в письмах для входа, приложении-аутентификаторе и при входе через MCP. Оставьте пустым, чтобы использовать имя, заданное при настройке этого развёртывания. Это не название самой организации — оно указано в поле «Название» записи организации.', + }, + accent_color: { + label: 'Акцентный цвет', + help: 'Цвет темы браузера и установленного приложения. Каждое приложение сохраняет собственные цвета.', + }, + logo_url: { + label: 'URL логотипа', + help: 'Показывается на странице входа, экране загрузки и в заголовке главной страницы. Пример: https://…/logo.svg', + }, + }, + }, + company: { + title: 'Компания', + description: 'Сведения о юридическом лице — зарегистрированное наименование, адрес, налоговые номера и основной контакт.', + groups: { + identity: { title: 'Реквизиты' }, + address: { title: 'Юридический адрес' }, + contact: { title: 'Контакты' }, + }, + keys: { + legal_name: { + label: 'Юридическое наименование', + help: 'Зарегистрированное юридическое наименование организации, как в договорах и счетах.', + }, + registration_number: { + label: 'Регистрационный номер', + help: 'Регистрационный номер компании (например, EIN).', + }, + tax_id: { label: 'ИНН / НДС', help: 'Указывается в счетах (например, VAT, GST, ABN).' }, + address_line1: { label: 'Адрес, строка 1' }, + address_line2: { label: 'Адрес, строка 2' }, + city: { label: 'Город' }, + state: { label: 'Штат / область' }, + postal_code: { label: 'Почтовый индекс' }, + country: { + label: 'Страна', + help: 'Страна юридического адреса. Введите название страны и выберите её из списка.', + options: countryOptionLabels('ru'), + }, + phone: { label: 'Телефон', help: 'Рекомендуется формат E.164, например +7 495 555 01 00.' }, + website: { label: 'Веб-сайт', help: 'Пример: https://example.com' }, + primary_contact_name: { label: 'Имя основного контакта' }, + primary_contact_email: { label: 'E-mail основного контакта', help: 'Пример: ops@example.com' }, + }, + }, + localization: { + title: 'Локализация', + description: 'Часовой пояс, язык, страна, валюта и финансовый год по умолчанию.', + groups: { + region: { title: 'Регион' }, + finance: { title: 'Финансы' }, + }, + keys: { + timezone: { + label: 'Часовой пояс по умолчанию', + help: 'Определяет, что означает «сегодня» в формулах, как отчёты группируют даты и как отображаются даты и время. Принимается любое имя часового пояса IANA (например, Europe/Moscow). Это действующий часовой пояс организации: поле «Часовой пояс» в записи организации его не меняет.', + options: { + UTC: 'UTC', + 'America/Los_Angeles': '(UTC−08/−07) Лос-Анджелес', + 'America/Denver': '(UTC−07/−06) Денвер', + 'America/Chicago': '(UTC−06/−05) Чикаго', + 'America/New_York': '(UTC−05/−04) Нью-Йорк', + 'America/Sao_Paulo': '(UTC−03) Сан-Паулу', + 'Europe/London': '(UTC±00/+01) Лондон', + 'Europe/Paris': '(UTC+01/+02) Париж', + 'Europe/Berlin': '(UTC+01/+02) Берлин', + 'Europe/Moscow': '(UTC+03) Москва', + 'Asia/Dubai': '(UTC+04) Дубай', + 'Asia/Kolkata': '(UTC+05:30) Калькутта', + 'Asia/Singapore': '(UTC+08) Сингапур', + 'Asia/Shanghai': '(UTC+08) Шанхай', + 'Asia/Tokyo': '(UTC+09) Токио', + 'Australia/Sydney': '(UTC+10/+11) Сидней', + 'Pacific/Auckland': '(UTC+12/+13) Окленд', + }, + }, + locale: { + label: 'Язык по умолчанию', + help: 'Язык BCP-47 для каталогов сообщений и форматирования чисел и дат.', + }, + default_country: { + label: 'Страна по умолчанию', + help: 'Используется по умолчанию для адресов и телефонов. Введите название страны и выберите её из списка.', + options: countryOptionLabels('ru'), + }, + currency: { + label: 'Валюта по умолчанию', + help: 'Код ISO 4217, применяемый, если поле валюты не задаёт свой. Оставьте пустым, чтобы суммы без кода отображались как обычные числа.', + options: { + USD: 'USD — Доллар США', + EUR: 'EUR — Евро', + GBP: 'GBP — Фунт стерлингов', + JPY: 'JPY — Японская иена', + CNY: 'CNY — Китайский юань', + INR: 'INR — Индийская рупия', + AUD: 'AUD — Австралийский доллар', + CAD: 'CAD — Канадский доллар', + BRL: 'BRL — Бразильский реал', + }, + }, + fiscal_year_start: { + label: 'Начало финансового года', + help: 'Первый месяц финансового года — определяет «этот квартал / финансовый год» в отчётах.', + options: { + january: 'Январь', + february: 'Февраль', + march: 'Март', + april: 'Апрель', + may: 'Май', + june: 'Июнь', + july: 'Июль', + august: 'Август', + september: 'Сентябрь', + october: 'Октябрь', + november: 'Ноябрь', + december: 'Декабрь', + }, + }, + }, + }, + auth: { + title: 'Аутентификация', + description: 'Вход, регистрация и встроенные функции аутентификации.', + groups: { + email_password: { + title: 'Электронная почта и пароль', + description: 'Управление локальным входом по почте и паролю и самостоятельной регистрацией.', + }, + membership: { + title: 'Членство', + description: 'Куда попадает новый пользователь. Работает в паре с самостоятельной регистрацией выше.', + }, + audience: { + title: 'Аудитория', + description: 'Кто может стать пользователем приложений этой системы. Разрешённые почтовые домены всегда включают обязательное подтверждение почты. Режим «Открыто» тоже включает его, если развёртывание не отключит; значение, сохранённое в этой консоли, отключить его не может. Приглашения, пользователи, созданные администратором, провижининг SCIM и корпоративный SSO допускаются в любом режиме.', + }, + password_policy: { + title: 'Политика паролей', + description: 'Ограничения длины, которые провайдер аутентификации применяет при регистрации и сбросе пароля.', + }, + anti_abuse: { + title: 'Защита от злоупотреблений', + description: 'Защита от перебора: блокировка учётной записи для каждой идентичности и ограничение частоты запросов по IP к конечным точкам аутентификации.', + }, + multi_factor: { + title: 'Многофакторная', + description: 'Требовать от участников защищать учётную запись приложением-аутентификатором (TOTP).', + }, + sessions: { title: 'Сеансы', description: 'Сколько действует сеанс после входа.' }, + network: { + title: 'Сеть', + description: 'Ограничить места, откуда пользователи могут проходить аутентификацию.', + }, + api_keys: { + title: 'Ключи API', + description: 'Срок жизни личных ключей API. Истёкший ключ, ключ, не использовавшийся дольше периода неактивности, или отозванный ключ отклоняется со своим кодом ошибки, и решение всегда одно — новый ключ.', + }, + social: { + title: 'Вход через соцсети', + description: 'Настройка встроенного провайдера входа через Google. Учётные данные из конфигурации сервера по-прежнему имеют приоритет.', + }, + }, + keys: { + email_password_enabled: { + label: 'Включить вход по почте и паролю', + confirmOffText: 'Тем, кто входит по паролю, понадобится другой способ входа.', + }, + signup_enabled: { + label: 'Разрешить регистрацию по почте', + help: 'Открывает форму регистрации по почте и паролю. Кого она допускает, определяет «Аудитория самостоятельной регистрации» ниже: при «Только по приглашению» — только адрес с ожидающим приглашением; при «Разрешённые почтовые домены» — разрешённые домены и приглашённые адреса; при «Открыто» — любого. Выключение закрывает форму при любой аудитории, в том числе для приглашённых.', + }, + require_email_verification: { label: 'Требовать подтверждение почты' }, + membership_policy: { + label: 'Членство новых пользователей', + help: '«Автоматически» привязывает каждого нового пользователя к организации по умолчанию. «Только по приглашению» даёт членство лишь после явного действия — создания организации, принятия приглашения, добавления администратором или своевременного провижининга через SSO.', + options: { + auto: 'Автоматически вступать в организацию по умолчанию', + 'invite-only': 'Только по приглашению — никогда не вступать автоматически', + }, + }, + audience_posture: { + label: 'Аудитория самостоятельной регистрации', + help: '«Только по приглашению» закрывает самостоятельную регистрацию: люди становятся пользователями только когда администратор приглашает, создаёт или импортирует их, либо через провижининг каталога (SCIM) или корпоративный единый вход. «Разрешённые почтовые домены» открывает её для доменов, перечисленных ниже. «Открыто» допускает любого. И «Разрешённые почтовые домены», и «Открыто» требуют набора разрешений самостоятельной регистрации ниже. «Разрешённые почтовые домены» всегда требует подтверждения почты. «Открыто» требует его, если конфигурация сервера не отключает; отключить его в этой консоли в режиме «Открыто» нельзя.', + options: { + invite_only: 'Только по приглашению — без самостоятельной регистрации (по умолчанию)', + email_domain: 'Только разрешённые почтовые домены', + open: 'Открыто — зарегистрироваться может любой', + }, + }, + audience_allowed_email_domains: { + label: 'Разрешённые почтовые домены', + help: 'Голые домены, по одному в строке или через запятую (например, acme.com). Сравнение точное и без учёта регистра; для поддоменов нужны отдельные записи; подстановочные знаки не поддерживаются.', + }, + audience_self_registration_permission_set: { + label: 'Набор разрешений для самостоятельной регистрации', + help: 'Имя набора разрешений, который получает каждый самостоятельно зарегистрированный пользователь. Стандартный набор участника подходит; полный набор администратора отклоняется. Обязательно, если аудитория допускает самостоятельную регистрацию.', + }, + password_min_length: { label: 'Минимальная длина пароля' }, + password_max_length: { + label: 'Максимальная длина пароля', + help: 'Защищает от отказа в обслуживании при хешировании очень длинных паролей.', + }, + password_reject_breached: { + label: 'Отклонять скомпрометированные пароли', + help: 'Блокировать пароли из публичных утечек через Have I Been Pwned (проверка диапазона с k-анонимностью; пароль никогда не передаётся целиком).', + }, + password_require_complexity: { + label: 'Требовать сложные пароли', + help: 'Требовать сочетания классов символов (заглавные, строчные, цифры, символы) при регистрации, смене и сбросе пароля.', + }, + password_min_classes: { + label: 'Минимум классов символов', + help: 'Сколько из четырёх классов (заглавные / строчные / цифры / символы) должен содержать пароль.', + }, + password_history_count: { + label: 'История паролей (без повторного использования)', + help: 'Запрещает повторно использовать столько предыдущих паролей при смене или сбросе. 0 отключает проверку.', + }, + password_expiry_days: { + label: 'Срок действия пароля (дни)', + help: 'Требовать смену пароля через столько дней. 0 отключает истечение. Пока пароль просрочен, пользователь не получает доступ к данным, пока не сменит его.', + }, + lockout_threshold: { + label: 'Порог блокировки учётной записи', + help: 'Блокировать учётную запись после стольких неудачных попыток входа подряд — как неверных паролей, так и неверных кодов двухфакторной проверки. Во время блокировки вход отклоняется даже с правильными учётными данными. 0 отключает блокировку на этапе пароля; двухфакторная проверка тогда сохраняет встроенный лимит 10 попыток за 15 минут, поскольку это последняя проверка перед выдачей сеанса.', + }, + lockout_duration_minutes: { + label: 'Длительность блокировки (минуты)', + help: 'Сколько учётная запись остаётся заблокированной после превышения порога на любом этапе входа.', + }, + rate_limit_max: { + label: 'Лимит аутентификации: макс. запросов', + help: 'Максимум запросов с одного IP за окно к конечным точкам входа / регистрации / сброса пароля.', + }, + rate_limit_window_seconds: { + label: 'Лимит аутентификации: окно (секунды)', + help: 'Скользящее окно, в котором считается указанный выше лимит запросов.', + }, + mfa_required: { + label: 'Требовать многофакторную аутентификацию', + help: 'Пользователи без зарегистрированного аутентификатора теряют доступ к данным по окончании льготного периода. Включение также активирует двухфакторную функцию, чтобы пользователи могли зарегистрироваться.', + }, + mfa_grace_period_days: { + label: 'Льготный период MFA (дни)', + help: 'Сколько пользователи могут откладывать регистрацию до жёсткой блокировки. 0 — блокировать сразу.', + }, + session_expiry_days: { + label: 'Срок действия сеанса (дни)', + help: 'Сеанс истекает через столько дней после входа.', + }, + session_refresh_days: { + label: 'Порог продления (дни)', + help: 'Активный сеанс продлевается, когда становится старше этого срока.', + }, + session_idle_timeout_minutes: { + label: 'Тайм-аут бездействия (минуты)', + help: 'Выходить из системы после стольких минут бездействия. 0 — отключено.', + }, + session_absolute_max_hours: { + label: 'Абсолютный срок сеанса (часы)', + help: 'Требовать повторную аутентификацию через столько часов после входа независимо от активности. 0 — отключено.', + }, + max_concurrent_sessions_per_user: { + label: 'Макс. одновременных сеансов на пользователя', + help: 'Ограничивает число одновременных сеансов на пользователя; сверх лимита самые старые завершаются. 0 = без ограничений.', + }, + allowed_ip_ranges: { + label: 'Разрешённые диапазоны IP', + help: 'Диапазоны CIDR или точные IP (по одному в строке или через запятую), например 203.0.113.0/24. Если задано, вход извне этих диапазонов отклоняется, как и любой запрос, адрес клиента которого сервер не может установить. Пусто = без ограничений. Проверяется адрес, с которого пришло соединение; за обратным прокси сервер нужно настроить на доверие этому прокси, чтобы проверялся исходный адрес клиента.', + }, + api_key_default_expiry_days: { + label: 'Срок действия ключа по умолчанию (дни)', + help: 'Ключ, созданный без даты истечения, истекает через столько дней после создания. Не дольше максимального срока ниже.', + }, + api_key_max_expiry_days: { + label: 'Максимальный срок действия ключа (дни)', + help: 'Самая поздняя дата истечения, которую может запросить новый ключ, считая от создания. Более поздняя отклоняется; ровно столько дней — допускается.', + }, + api_key_inactivity_days: { + label: 'Истечение при неактивности (дни)', + help: 'Ключ без зарегистрированного использования в течение стольких дней отклоняется. Он не отзывается; создайте новый ключ. Неиспользованный ключ отсчитывается от создания или от начала политики ниже, если он старше. 0 отключает истечение при неактивности.', + }, + api_key_policy_started_at: { + label: 'Начало политики ключей', + help: 'Записывается автоматически (UTC, ISO 8601) при первом запуске сервера с этой политикой. Ключи, созданные до этого без даты истечения, истекают через год после неё; ключи без зарегистрированного использования отсчитывают неактивность от неё. Изменение сдвигает обе даты для ключей, которым они ещё не назначены.', + }, + google_enabled: { + label: 'Включить вход через Google', + help: 'Предлагает вход через Google, когда доступны и идентификатор клиента, и секрет клиента: сохранённые ниже или заданные в конфигурации сервера, которая важнее сохранённых значений. Пока оба недоступны, на странице входа нет кнопки Google, даже если параметр включён. Выключение убирает вход через Google, даже если сервер задаёт оба значения. Создайте клиент OAuth в Google Cloud Console.', + }, + google_client_id: { + label: 'Идентификатор клиента Google', + help: 'Идентификатор клиента OAuth из Google Cloud Console. Его также можно задать в конфигурации сервера.', + }, + google_client_secret: { + label: 'Секрет клиента Google', + help: 'Хранится в зашифрованном виде. Его также можно задать в конфигурации сервера.', + }, + }, + }, + storage: { + title: 'Хранилище файлов', + description: 'Бэкенд для вложений, экспорта и файлов, загружаемых пользователями.', + groups: { + adapter: { title: 'Бэкенд', description: 'Выберите, где хранятся загруженные файлы.' }, + local: { title: 'Локально' }, + s3: { title: 'S3' }, + limits: { title: 'Ограничения' }, + }, + keys: { + adapter: { + label: 'Адаптер', + options: { local: 'Локальная файловая система', s3: 'S3 / совместимое с S3' }, + }, + local_root: { + label: 'Корневой каталог', + help: 'Путь в файловой системе, где хранятся файлы. Относительные пути отсчитываются от рабочего каталога (CWD) сервера.', + }, + s3_bucket: { + label: 'Бакет', + help: 'Общий бакет хоста. Файлы каждого окружения разделяются префиксом projects//.', + }, + s3_region: { label: 'Регион', help: 'Пример: us-east-1' }, + s3_endpoint: { + label: 'Конечная точка', + help: 'Пользовательская конечная точка для S3-совместимых провайдеров (R2, MinIO, Wasabi). Для AWS S3 оставьте пустым.', + }, + s3_access_key_id: { label: 'Идентификатор ключа доступа' }, + s3_secret_access_key: { label: 'Секретный ключ доступа' }, + s3_force_path_style: { + label: 'Принудительно использовать URL в стиле пути', + help: 'Включите для MinIO и большинства S3-совместимых провайдеров; для AWS S3 отключите.', + }, + presigned_ttl: { label: 'Время жизни предподписанного URL (секунды)' }, + session_ttl: { + label: 'Время жизни сеанса загрузки (секунды)', + help: 'Сколько сеанс загрузки частями можно возобновить.', + }, + max_upload_mb: { label: 'Макс. размер загрузки (МБ)' }, + }, + actions: { + test: { label: 'Проверить подключение' }, + }, + banners: { + adapter_switch_warning: { + title: 'Существующие файлы не переносятся', + text: 'Смена адаптера не переносит существующие файлы — файлы, загруженные через прежний адаптер, становятся недоступны через новый.', + }, + }, + }, + ai: { + title: 'ИИ и Embedder', + description: 'Провайдер LLM, модель, учётные данные и настройка Embedder для служб ИИ и знаний платформы.', + groups: { + provider: { + title: 'Провайдер', + description: 'Выберите бэкенд LLM. Режим Memory возвращает ввод как есть — полезно для тестов, но не для продакшена.', + }, + gateway: { + title: 'Vercel AI Gateway', + description: 'Маршрутизатор для нескольких провайдеров. Модель записывается как provider/model, например openai/gpt-4o.', + }, + openai: { title: 'OpenAI' }, + anthropic: { title: 'Anthropic' }, + google: { title: 'Google' }, + deepseek: { + title: 'DeepSeek', + description: 'OpenAI-совместимый API по адресу https://api.deepseek.com. Base URL заполняется автоматически.', + }, + dashscope: { + title: '阿里通义 DashScope', + description: 'OpenAI-совместимая конечная точка dashscope.aliyuncs.com/compatible-mode/v1. Base URL заполняется автоматически.', + }, + cloudflare: { + title: 'Cloudflare AI Gateway', + description: 'Использует конечную точку /compat, поэтому модель записывается как provider/model (например, openai/gpt-4o-mini, anthropic/claude-3-5-sonnet, deepseek/deepseek-chat). Примечание: alibaba/qwen* НЕ поддерживается Cloudflare /compat — для Qwen используйте провайдера DashScope.', + }, + siliconflow: { + title: '硅基流动 SiliconFlow', + description: 'OpenAI-совместимая конечная точка api.siliconflow.cn/v1. Base URL заполняется автоматически.', + }, + openrouter: { + title: 'OpenRouter', + description: 'Маршрутизатор для нескольких провайдеров на openrouter.ai/api/v1. Base URL заполняется автоматически.', + }, + titles: { + title: 'Заголовки бесед', + description: 'Автоматически создавать короткий заголовок-резюме для новых бесед.', + }, + defaults: { + title: 'Параметры генерации по умолчанию', + description: 'Применяются, если запрос агента или чата не задаёт собственное значение.', + }, + observability: { title: 'Наблюдаемость' }, + embedder: { + title: 'Embedder', + description: 'Провайдер «текст → вектор» для источников знаний и RAG. Не зависит от провайдера чата выше.', + }, + }, + keys: { + provider: { + label: 'Провайдер', + options: { + memory: 'Memory (эхо — только для тестов)', + gateway: 'Vercel AI Gateway', + openai: 'OpenAI', + anthropic: 'Anthropic', + google: 'Google Generative AI', + deepseek: 'DeepSeek (совместим с OpenAI)', + dashscope: '阿里通义 DashScope (совместим с OpenAI)', + cloudflare: 'Cloudflare AI Gateway (совместим с OpenAI)', + siliconflow: '硅基流动 SiliconFlow (совместим с OpenAI)', + openrouter: 'OpenRouter (совместим с OpenAI)', + }, + }, + gateway_model: { + label: 'Модель Gateway', + help: 'Используется вместо модели из конфигурации сервера. Пример: openai/gpt-4o', + }, + gateway_api_key: { + label: 'Ключ API Gateway', + help: 'Необязательно — нужно, только если шлюз требует аутентификацию.', + }, + openai_api_key: { + label: 'Ключ API OpenAI', + help: 'Используется вместо ключа из конфигурации сервера. Хранится в зашифрованном виде.', + }, + openai_model: { + label: 'Модель', + help: 'Идентификатор модели по умолчанию. Переопределения для агентов имеют приоритет.', + }, + openai_base_url: { + label: 'Base URL', + help: 'Переопределение для Azure OpenAI или собственных шлюзов. Для api.openai.com оставьте пустым.', + }, + anthropic_api_key: { + label: 'Ключ API Anthropic', + help: 'Используется вместо ключа из конфигурации сервера. Хранится в зашифрованном виде.', + }, + anthropic_model: { label: 'Модель' }, + google_api_key: { + label: 'Ключ API Google', + help: 'Используется вместо ключа из конфигурации сервера. Хранится в зашифрованном виде.', + }, + google_model: { label: 'Модель' }, + deepseek_api_key: { label: 'Ключ API DeepSeek', help: 'sk-... — выдаётся на platform.deepseek.com.' }, + deepseek_model: { label: 'Модель', help: 'Примеры: deepseek-chat (V3), deepseek-reasoner (рассуждения R1).' }, + dashscope_api_key: { label: 'Ключ API DashScope', help: 'sk-... — выдаётся на dashscope.console.aliyun.com.' }, + dashscope_model: { label: 'Модель' }, + cloudflare_account_id: { + label: 'Идентификатор аккаунта Cloudflare', + help: '32-символьный шестнадцатеричный идентификатор из URL панели Cloudflare.', + }, + cloudflare_gateway_id: { + label: 'Идентификатор шлюза', + help: 'Имя шлюза, настроенное в Cloudflare → AI Gateway. По умолчанию "default".', + }, + cloudflare_api_key: { + label: 'Токен Cloudflare AI Gateway', + help: 'Выдаётся в AI Gateway → вкладка «API tokens» (cfut_… или sk_…).', + }, + cloudflare_model: { label: 'Модель' }, + siliconflow_api_key: { label: 'Ключ API SiliconFlow' }, + siliconflow_model: { + label: 'Модель', + help: 'Примеры: Qwen/Qwen2.5-72B-Instruct, deepseek-ai/DeepSeek-V3, meta-llama/Meta-Llama-3.1-8B-Instruct.', + }, + openrouter_api_key: { label: 'Ключ API OpenRouter', help: 'sk-or-...' }, + openrouter_model: { + label: 'Модель', + help: 'Формат: provider/model (например, anthropic/claude-3.5-sonnet, deepseek/deepseek-chat).', + }, + title_generation_enabled: { label: 'Автоматически создавать заголовки бесед' }, + title_max_length: { + label: 'Макс. длина заголовка (символы)', + help: 'Жёсткий предел длины созданного заголовка. Всё, что длиннее, обрезается на сервере.', + }, + temperature: { label: 'Температура', help: '0 = детерминированно, 2 = очень креативно.' }, + max_tokens: { label: 'Макс. токенов вывода', help: 'Жёсткий предел числа токенов в одном ответе.' }, + request_timeout_ms: { label: 'Тайм-аут запроса (мс)' }, + trace_enabled: { + label: 'Записывать трассировки', + help: 'Сохранять каждый запрос к ИИ и его ответ, чтобы при сбое беседу можно было изучить и воспроизвести.', + }, + log_prompts: { + label: 'Журналировать полные промпты', + help: 'Включать в строки трассировки готовые промпты (а не только метаданные).', + }, + embedder_provider: { + label: 'Провайдер', + options: { + none: 'Отключено (без эмбеддингов)', + openai: 'OpenAI', + azure: 'Azure OpenAI', + dashscope: '阿里通义 DashScope', + zhipu: '智谱 BigModel', + siliconflow: '硅基流动 SiliconFlow', + doubao: '火山引擎 Doubao', + minimax: 'MiniMax', + ollama: 'Ollama (локально)', + custom: 'Свой (совместим с OpenAI)', + }, + }, + embedder_api_key: { + label: 'Ключ API Embedder', + help: 'Токен Bearer, передаваемый в заголовке Authorization. Для Ollama подходит любое непустое значение.', + }, + embedder_model: { + label: 'Модель', + help: 'Примеры — OpenAI: text-embedding-3-small · 阿里通义: text-embedding-v3 · 智谱: embedding-3 · 硅基流动: BAAI/bge-m3 · Ollama: bge-m3', + }, + embedder_base_url: { + label: 'Base URL', + help: 'Корень конечной точки (без /embeddings). Заполняется из пресета; переопределите для прокси или собственного шлюза.', + }, + embedder_dimensions: { + label: 'Размерность', + help: 'Переопределить размерность вывода (только модели Matryoshka). Оставьте пустым для значения модели по умолчанию.', + }, + embedder_batch_size: { + label: 'Размер пакета', + help: 'Число фрагментов, отправляемых провайдеру эмбеддингов в одном запросе. Уменьшите при упоре в лимиты частоты или размера провайдера.', + }, + }, + actions: { + test: { label: 'Проверить подключение' }, + test_embedder: { label: 'Проверить Embedder' }, + reset: { label: 'Сбросить к значениям окружения' }, + }, + banners: { + log_prompts_warning: { + title: 'Промпты могут содержать персональные данные', + text: 'Возможна утечка персональных данных — отключите в регулируемых средах.', + }, + }, + }, + knowledge: { + title: 'Знания', + description: 'Бэкенд векторного хранилища для RAG и источников знаний.', + groups: { + adapter: { title: 'Бэкенд', description: 'Выберите, где хранятся фрагменты документов и их векторы.' }, + turso: { title: 'Turso / libSQL', description: 'Работает с управляемым Turso, локальным файлом или в памяти.' }, + ragflow: { + title: 'RAGFlow', + description: 'Внешнее развёртывание RAGFlow. Инструкции по самостоятельному размещению: https://ragflow.io.', + }, + indexing: { + title: 'Параметры индексации по умолчанию', + description: 'Значение, заданное в самом источнике знаний, важнее этих значений по умолчанию.', + }, + permissions: { title: 'Разрешения' }, + }, + keys: { + adapter: { + label: 'Адаптер', + options: { + memory: 'В памяти (только разработка / тесты)', + turso: 'Turso / libSQL (облако или локально)', + ragflow: 'RAGFlow (внешний)', + }, + }, + turso_url: { + label: 'URL подключения', + help: 'Примеры: libsql://your-tenant.turso.io · file:./.objectstack/knowledge.db · :memory:', + }, + turso_auth_token: { label: 'Токен авторизации', help: 'Нужен только для URL управляемого Turso.' }, + ragflow_base_url: { label: 'Base URL', help: 'Пример: http://localhost:9380' }, + ragflow_api_key: { label: 'Ключ API' }, + ragflow_default_dataset: { + label: 'Идентификатор набора данных по умолчанию', + help: 'Используется, если источник знаний не указывает собственный набор данных RAGFlow.', + }, + chunk_target: { + label: 'Целевой размер фрагмента (символы)', + help: 'Мягкий предел размера фрагмента до начала разбиения с учётом токенов.', + }, + chunk_overlap: { + label: 'Перекрытие фрагментов (символы)', + help: 'Символы, сохраняемые из предыдущего фрагмента, чтобы контекст не терялся на границе.', + }, + over_fetch: { + label: 'Множитель избыточной выборки', + help: 'Поиск извлекает во столько раз больше результатов, чем запрошено, чтобы после фильтрации по сведениям документов их осталось достаточно.', + }, + enforce_rls: { + label: 'Применять RLS при поиске', + help: 'Повторно проверять каждое совпадение по разрешениям вызывающего на уровне записей.', + }, + }, + actions: { + test: { label: 'Проверить подключение' }, + }, + banners: { + adapter_switch_warning: { + title: 'Существующие индексы не переносятся', + text: 'Смена адаптера НЕ переносит существующие индексы — документы, проиндексированные через прежний адаптер, недоступны до переиндексации.', + }, + enforce_rls_warning: { + title: 'Не отключайте RLS в продакшене', + text: 'Отключение обходит уникальную защиту платформы от утечки данных векторного хранилища — в продакшене не отключайте.', + }, + }, + }, + lifecycle: { + title: 'Жизненный цикл данных', + description: 'Сколько хранятся системные данные: переопределение сроков хранения, лимиты строк для таблиц и оповещения о неожиданном росте таблицы. Платформа применяет это при периодической очистке.', + keys: { + enabled: { + label: 'Применять политики жизненного цикла', + help: 'Главный переключатель периодической очистки. Выключено = объявленные политики хранения/TTL/ротации больше не применяются (данные снова растут без ограничений).', + }, + retention_overrides: { + label: 'Переопределения хранения', + help: 'Переопределения срока для объектов: { "": { "maxAge": "1y", "expireAfter": "30d" } }. Длительности задаются в h/d/w/y. Задаются для каждой организации, так что регулируемая организация может хранить годы, а организация для разработки — дни. Переопределение короче минимума, нужного функции платформы (например, окна обнаружения дубликатов очереди заданий), игнорируется и записывается как ошибка, а срок по умолчанию продолжает действовать.', + }, + quotas: { + label: 'Квоты строк', + help: 'Максимум строк для объекта: { "": 1000000 }. Превышение вызывает оповещение управления — квоты никогда не удаляют сверх объявленной политики.', + }, + quota_defaults: { + label: 'Квоты по умолчанию для классов', + help: 'Резервные квоты по классу жизненного цикла: { "telemetry": 1000000, "transient": 500000, "event": 100000, "audit": 5000000 }.', + }, + growth_alert_rows: { + label: 'Порог оповещения о росте (строк за проход)', + help: 'Оповещать, когда таблица с объявленным жизненным циклом вырастает более чем на столько строк между двумя проходами. 0 отключает оповещения о росте.', + }, + }, + }, + }, +}; diff --git a/packages/services/service-settings/src/translations/settings-translation-coverage.test.ts b/packages/services/service-settings/src/translations/settings-translation-coverage.test.ts index 9b054b2a54c..f4aaef16c30 100644 --- a/packages/services/service-settings/src/translations/settings-translation-coverage.test.ts +++ b/packages/services/service-settings/src/translations/settings-translation-coverage.test.ts @@ -24,7 +24,7 @@ import { describe, it, expect } from 'vitest'; import type { SettingsManifest } from '@objectstack/spec/system'; import { lifecycleSettingsManifest } from '@objectstack/objectql'; import * as manifestsModule from '../manifests/index.js'; -import { en, zhCN, jaJP, esES, zhHant, ar, fr, ptBR } from './index.js'; +import { en, zhCN, jaJP, esES, zhHant, ar, fr, ptBR, ru, de, ko } from './index.js'; // The manifests barrel also exports action handlers and the aggregate array; // keep only the manifest objects. @@ -57,6 +57,9 @@ const LOCALES: Array<[string, { settings?: Record }]> = [ ['ar', ar], ['fr', fr], ['pt-BR', ptBR], + ['ru', ru], + ['de', de], + ['ko', ko], ]; function missingFor(data: { settings?: Record }, m: SettingsManifest): string[] {