From cc78aca2d364ef829519d7702e4ba6e52254f474 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 04:11:42 +0000 Subject: [PATCH 1/2] feat(i18n): add the second-pass console keys for objectui#13460 Audit Log, Flow Runs, the Integrations page's agent card, the share-link dialog and the shared-record landing, translated in all ten packs. Claude-Session: https://claude.ai/code/session_018yk9f7S7q4BrEeWZGRcTKF Co-authored-by: Claude --- packages/i18n/src/locales/ar.ts | 150 ++++++++++++++++++++++++++++++++ packages/i18n/src/locales/de.ts | 150 ++++++++++++++++++++++++++++++++ packages/i18n/src/locales/en.ts | 150 ++++++++++++++++++++++++++++++++ packages/i18n/src/locales/es.ts | 150 ++++++++++++++++++++++++++++++++ packages/i18n/src/locales/fr.ts | 150 ++++++++++++++++++++++++++++++++ packages/i18n/src/locales/ja.ts | 150 ++++++++++++++++++++++++++++++++ packages/i18n/src/locales/ko.ts | 150 ++++++++++++++++++++++++++++++++ packages/i18n/src/locales/pt.ts | 150 ++++++++++++++++++++++++++++++++ packages/i18n/src/locales/ru.ts | 150 ++++++++++++++++++++++++++++++++ packages/i18n/src/locales/zh.ts | 150 ++++++++++++++++++++++++++++++++ 10 files changed, 1500 insertions(+) diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 2ba8acba0c..ac057c1c96 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -2584,6 +2584,61 @@ const ar = { redirectHint: "مسار داخل التطبيق يبدأ بـ {{slash}} — أدرج حقلًا من السجل المُرسَل بالصيغة {{token}}. لإرسال المتصفح خارج التطبيق، استخدم بدلًا من ذلك عنصر تنقّل في التطبيق.", saving: "جارٍ الحفظ…", }, + // objectui#13460 — Developer → Flow Runs (`FlowRunsPage`). + flowRuns: { + title: 'عمليات تشغيل التدفقات', + subtitle: 'اختر تدفقًا، وشغّل تشغيلاً تجريبيًا بمدخلات نموذجية، واطّلع على سجل عمليات التشغيل الأخيرة', + flowTitle: 'التدفق', + flowDescription: 'اختر تعريف تدفق لاختباره', + noFlows: 'لم يتم العثور على تعريفات تدفقات.', + testRunTitle: 'تشغيل تجريبي', + testRunDescription: 'قدّم قيم الإدخال واستدعِ التدفق', + noInputs: 'لا يعلن هذا التدفق عن أي متغيرات إدخال. انقر على {{action}} لاستدعائه بدون معلمات.', + running: 'جارٍ التشغيل…', + runFlow: 'تشغيل التدفق', + failed: 'فشل', + waitingForInput: 'في انتظار الإدخال', + result: 'النتيجة', + pausedAtScreen: 'هذا التشغيل متوقف مؤقتًا عند الشاشة {{node}}.', + continueRun: 'متابعة التشغيل', + recentRuns: 'عمليات التشغيل الأخيرة', + noRuns: 'لا توجد عمليات تشغيل بعد. استخدم لوحة {{panel}} لاستدعاء هذا التدفق.', + columnRun: 'التشغيل', + columnStatus: 'الحالة', + columnStarted: 'البدء', + columnDuration: 'المدة', + runTitle: 'التشغيل {{id}}', + stepInput: 'الإدخال', + stepOutput: 'الإخراج', + stepError: 'خطأ', + }, + // objectui#13460 — the Integrations page's "Connect an AI agent" card (`AgentConnectSection`). + agentConnect: { + title: 'ربط وكيل ذكاء اصطناعي (MCP)', + description: 'كل تطبيق في هذه البيئة مجموعة أدوات جاهزة للوكلاء. اربط Claude أو Cursor أو Codex أو أي عميل MCP — اتصال واحد يغطي جميع تطبيقاتك (تظهر التطبيقات الجديدة تلقائيًا).', + endpoint: 'نقطة نهاية MCP', + disabledTitle: 'لم يتم تفعيل MCP في هذه البيئة', + disabledBody: 'ربط الوكلاء اختياري. يمكن للمسؤول تفعيله بضبط {{flag}}. لا يزال بإمكانك إنشاء مفتاح وتنزيل المهارة أدناه.', + copyMcpUrl: 'نسخ عنوان URL لـ MCP', + apiKey: 'مفتاح API', + keyNamePlaceholder: 'اسم المفتاح (مثل Claude Desktop)', + generating: 'جارٍ الإنشاء…', + generateKey: 'إنشاء مفتاح', + keyHint: 'يعمل المفتاح بهويتك (أذوناتك + الأمان على مستوى الصف). يُعرض مرة واحدة فقط — انسخه فورًا. يمكنك إلغاؤه في أي وقت من قائمة مفاتيح API.', + noKeyInResponse: 'نجح الطلب لكن الاستجابة لم تتضمن مفتاح API. لم يفشل شيء أثناء النقل — افحص نص استجابة {{route}}.', + requestFailed: 'فشل الطلب ({{status}})', + generateFailed: 'تعذّر إنشاء المفتاح', + skillTitle: 'مهارة الوكيل (قابلة للنقل)', + skillHint: 'يعمل مع أي وكيل يدعم المهارات (Claude وCodex وGemini وCursor…).', + stepsTitle: 'الربط في 3 خطوات', + step1: 'أضف خادم MCP بعيدًا يشير إلى نقطة النهاية أعلاه.', + step2: 'اضبط الترويسة {{header}} (رمز Bearer الخاص بالجلسة ليس مفتاح API).', + step3: 'أضف {{file}} ليعرف الوكيل كيف يتعامل مع بياناتك.', + newKeyTitle: 'مفتاح API الجديد', + newKeyDescription: 'انسخه الآن — {{notice}}', + newKeyNotice: 'لأمانك، لن يُعرض مرة أخرى.', + sendAs: 'أرسله بصفته {{header}}. يحمل أذوناتك — احفظه ككلمة مرور وألغِه إذا تسرّب.', + }, }, system: { aiApprovals: { @@ -2592,6 +2647,45 @@ const ar = { queueTitle: "قائمة الانتظار", queueDescription: "يُستطلع كل 5 ثوانٍ. تُسجَّل القرارات باسمك.", }, + // objectui#13460 — System → Audit Log (`AuditLogPage`). + auditLog: { + title: 'سجل التدقيق', + subtitle: 'نشاط النظام وتغييرات السجلات للقراءة فقط. الأحدث أولاً.', + openAsList: 'فتح كقائمة', + openPresetAsList: 'فتح {{label}} كقائمة', + presets: 'إعدادات مسبقة', + presetOtherSource: 'يقرأ هذا العرض مصدر بيانات آخر.', + presetFilterUnsupported: 'لا يمكن تطبيق عامل تصفية هذا العرض هنا: {{message}}', + action: 'الإجراء', + allActions: 'كل الإجراءات', + object: 'الكائن', + anyObject: 'أي كائن', + searchObjects: 'البحث عن الكائنات…', + loadingObjects: 'جارٍ تحميل الكائنات…', + noObjects: 'لم يتم العثور على كائنات.', + actor: 'المنفذ', + anyUser: 'أي مستخدم', + from: 'من', + to: 'إلى', + clearFilters: 'مسح عوامل التصفية ({{n}})', + loadFailed: 'تعذّر تحميل سجل التدقيق', + emptyTitle: 'لا توجد إدخالات تدقيق', + emptyFiltered: 'جرّب مسح عوامل التصفية أو توسيع نطاق التاريخ.', + emptyUnfiltered: 'ستظهر الأحداث هنا بمجرد تعديل الكائنات المتتبَّعة.', + timestamp: 'الطابع الزمني', + page: 'الصفحة {{page}}', + pageEnd: 'الصفحة {{page}} · النهاية', + eventFallback: 'حدث تدقيق', + ipAddress: 'عنوان IP', + tenant: 'المستأجر', + targetRecord: 'السجل المستهدف', + userAgent: 'وكيل المستخدم', + changePayload: 'محتوى التغيير', + before: 'قبل', + after: 'بعد', + metadata: 'البيانات الوصفية', + system: 'النظام', + }, }, search: "بحث…", nav: { @@ -3585,6 +3679,62 @@ const ar = { filterUnread: "غير مقروء", filterAll: "الكل", }, + // objectui#13460 — the share-link dialog (`@object-ui/components` `ShareDialog`). + shareDialog: { + title: 'مشاركة {{label}}', + thisRecord: 'هذا السجل', + description: 'أنشئ رابطًا يمكن لأي شخص (أو لأشخاص محددين) استخدامه لفتح هذا السجل. يمكن إلغاء الروابط في أي وقت.', + whoCanAccess: 'من يمكنه الوصول', + audienceLinkOnly: 'أي شخص لديه الرابط', + audienceLinkOnlyHelp: 'لا يلزم تسجيل الدخول', + audienceSignedIn: 'المستخدمون المسجّلو الدخول', + audienceSignedInHelp: 'يجب تسجيل الدخول للعرض', + expiresIn: 'تنتهي الصلاحية بعد', + expiry1d: 'يوم واحد', + expiry7d: '7 أيام', + expiry30d: '30 يومًا', + expiry90d: '90 يومًا', + expiryNever: 'أبدًا', + password: 'كلمة المرور (اختياري)', + passwordPlaceholder: 'طلب كلمة مرور للفتح', + label: 'تسمية (تظهر لك فقط)', + labelPlaceholder: 'مثال: أُرسل لمراجعة التصميم', + createLink: 'إنشاء رابط', + existingLinks: 'الروابط الحالية', + existingLinksCount: 'الروابط الحالية ({{n}})', + noLinks: 'لا توجد روابط مشاركة بعد. أنشئ رابطًا أعلاه.', + revokedHeading: 'الملغاة', + revokedBadge: 'ملغى', + copy: 'نسخ', + copied: 'تم النسخ!', + revoke: 'إلغاء', + loadFailed: 'تعذّر تحميل روابط المشاركة', + createFailed: 'تعذّر إنشاء رابط المشاركة', + revokeFailed: 'تعذّر إلغاء الرابط', + }, + // objectui#13460 — the public landing of a `/s/:token` share link (`SharedRecordPage`). + sharedRecord: { + signInTitle: 'يلزم تسجيل الدخول', + signInBody: 'شارك المالك هذا الرابط مع المستخدمين المسجّلي الدخول فقط.', + signIn: 'تسجيل الدخول', + passwordTitle: 'كلمة المرور مطلوبة', + passwordBody: 'قام المالك بحماية هذا الرابط بكلمة مرور.', + passwordLabel: 'كلمة المرور', + passwordPlaceholder: 'أدخل كلمة المرور', + continue: 'متابعة', + cannotOpen: 'تعذّر فتح هذا الرابط', + passwordUnsendable: 'تحتوي كلمة المرور هذه على أحرف لا يمكن إرسالها، مثل الحروف العربية أو الرموز التعبيرية. اطلب من مالك الرابط تعيين كلمة مرور بدونها.', + wrongPassword: 'كلمة مرور خاطئة.', + loadFailedStatus: 'تعذّر التحميل (HTTP {{status}}).', + invalidLink: 'هذا الرابط غير صالح أو لم يعد متاحًا.', + expiredLink: 'انتهت صلاحية هذا الرابط أو تم إلغاؤه.', + loadFailed: 'تعذّر تحميل المحتوى المشترك.', + sharedConversation: 'محادثة مشتركة', + loadingMessages: 'جارٍ تحميل الرسائل…', + noMessages: 'لا تحتوي هذه المحادثة على رسائل بعد.', + sharedObject: '{{object}} مشترك', + redactedFields: 'أخفى المالك بعض الحقول: {{fields}}', + }, publicForm: { submit: "إرسال", submitting: "جارٍ الإرسال…", diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index 22ad7632dd..2dca8512ae 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -2382,6 +2382,61 @@ const de = { redirectHint: "Ein Pfad innerhalb der App, beginnend mit {{slash}} – ein Feld des eingesendeten Datensatzes fügen Sie als {{token}} ein. Um den Browser aus der App heraus zu leiten, verwenden Sie stattdessen einen Navigationseintrag der App.", saving: "Wird gespeichert…", }, + // objectui#13460 — Developer → Flow Runs (`FlowRunsPage`). + flowRuns: { + title: 'Flow-Ausführungen', + subtitle: 'Wählen Sie einen Flow, starten Sie einen Testlauf mit Beispieleingaben und prüfen Sie den letzten Ausführungsverlauf', + flowTitle: 'Flow', + flowDescription: 'Wählen Sie eine Flow-Definition zum Testen', + noFlows: 'Keine Flow-Definitionen gefunden.', + testRunTitle: 'Testlauf', + testRunDescription: 'Eingabewerte angeben und den Flow aufrufen', + noInputs: 'Dieser Flow deklariert keine Eingabevariablen. Klicken Sie auf {{action}}, um ihn ohne Parameter aufzurufen.', + running: 'Wird ausgeführt…', + runFlow: 'Flow ausführen', + failed: 'Fehlgeschlagen', + waitingForInput: 'Wartet auf Eingabe', + result: 'Ergebnis', + pausedAtScreen: 'Dieser Lauf ist am Bildschirm {{node}} angehalten.', + continueRun: 'Lauf fortsetzen', + recentRuns: 'Letzte Ausführungen', + noRuns: 'Noch keine Ausführungen. Rufen Sie diesen Flow über das Panel {{panel}} auf.', + columnRun: 'Lauf', + columnStatus: 'Status', + columnStarted: 'Gestartet', + columnDuration: 'Dauer', + runTitle: 'Lauf {{id}}', + stepInput: 'Eingabe', + stepOutput: 'Ausgabe', + stepError: 'Fehler', + }, + // objectui#13460 — the Integrations page's "Connect an AI agent" card (`AgentConnectSection`). + agentConnect: { + title: 'KI-Agenten verbinden (MCP)', + description: 'Jede App in dieser Umgebung ist ein agentenfähiges Toolset. Verbinden Sie Claude, Cursor, Codex oder einen beliebigen MCP-Client — eine Verbindung deckt alle Ihre Apps ab (neue erscheinen automatisch).', + endpoint: 'MCP-Endpunkt', + disabledTitle: 'MCP ist für diese Umgebung nicht aktiviert', + disabledBody: 'Das Verbinden von Agenten ist optional. Ein Admin kann es aktivieren, indem er {{flag}} setzt. Sie können unten trotzdem einen Schlüssel erzeugen und den Skill herunterladen.', + copyMcpUrl: 'MCP-URL kopieren', + apiKey: 'API-Schlüssel', + keyNamePlaceholder: 'Schlüsselname (z. B. Claude Desktop)', + generating: 'Wird erzeugt…', + generateKey: 'Schlüssel erzeugen', + keyHint: 'Der Schlüssel handelt als Sie (Ihre Berechtigungen + Sicherheit auf Zeilenebene). Er wird nur einmal angezeigt — kopieren Sie ihn sofort. Widerrufen Sie ihn jederzeit in der Liste der API-Schlüssel.', + noKeyInResponse: 'Die Anfrage war erfolgreich, aber die Antwort enthielt keinen API-Schlüssel. Bei der Übertragung ist nichts fehlgeschlagen — prüfen Sie den Antworttext von {{route}}.', + requestFailed: 'Anfrage fehlgeschlagen ({{status}})', + generateFailed: 'Schlüssel konnte nicht erzeugt werden', + skillTitle: 'Agenten-Skill (portabel)', + skillHint: 'Funktioniert mit jedem skillfähigen Agenten (Claude, Codex, Gemini, Cursor…).', + stepsTitle: 'In 3 Schritten verbinden', + step1: 'Fügen Sie einen Remote-MCP-Server hinzu, der auf den obigen Endpunkt zeigt.', + step2: 'Setzen Sie den Header {{header}} (ein Sitzungs-Bearer-Token ist kein API-Schlüssel).', + step3: 'Legen Sie {{file}} ab, damit der Agent weiß, wie er mit Ihren Daten arbeitet.', + newKeyTitle: 'Ihr neuer API-Schlüssel', + newKeyDescription: 'Kopieren Sie ihn jetzt — {{notice}}', + newKeyNotice: 'aus Sicherheitsgründen wird er nicht erneut angezeigt.', + sendAs: 'Senden Sie ihn als {{header}}. Er trägt Ihre Berechtigungen — bewahren Sie ihn wie ein Passwort auf und widerrufen Sie ihn bei einem Leck.', + }, }, system: { aiApprovals: { @@ -2390,6 +2445,45 @@ const de = { queueTitle: "Warteschlange", queueDescription: "Wird alle 5 Sekunden abgefragt. Entscheidungen werden unter Ihrer Identität erfasst.", }, + // objectui#13460 — System → Audit Log (`AuditLogPage`). + auditLog: { + title: 'Audit-Protokoll', + subtitle: 'Schreibgeschützte Systemaktivität und Datensatzänderungen. Neueste zuerst.', + openAsList: 'Als Liste öffnen', + openPresetAsList: '{{label}} als Liste öffnen', + presets: 'Voreinstellungen', + presetOtherSource: 'Diese Ansicht liest eine andere Datenquelle.', + presetFilterUnsupported: 'Der Filter dieser Ansicht kann hier nicht angewendet werden: {{message}}', + action: 'Aktion', + allActions: 'Alle Aktionen', + object: 'Objekt', + anyObject: 'Beliebiges Objekt', + searchObjects: 'Objekte suchen…', + loadingObjects: 'Objekte werden geladen…', + noObjects: 'Keine Objekte gefunden.', + actor: 'Akteur', + anyUser: 'Beliebiger Benutzer', + from: 'Von', + to: 'Bis', + clearFilters: 'Filter löschen ({{n}})', + loadFailed: 'Audit-Protokoll konnte nicht geladen werden', + emptyTitle: 'Keine Audit-Einträge', + emptyFiltered: 'Löschen Sie die Filter oder erweitern Sie den Datumsbereich.', + emptyUnfiltered: 'Ereignisse erscheinen hier, sobald überwachte Objekte geändert werden.', + timestamp: 'Zeitstempel', + page: 'Seite {{page}}', + pageEnd: 'Seite {{page}} · Ende', + eventFallback: 'Audit-Ereignis', + ipAddress: 'IP-Adresse', + tenant: 'Mandant', + targetRecord: 'Zieldatensatz', + userAgent: 'User-Agent', + changePayload: 'Änderungsdaten', + before: 'Vorher', + after: 'Nachher', + metadata: 'Metadaten', + system: 'System', + }, }, search: "Suchen…", nav: { @@ -3332,6 +3426,62 @@ const de = { filterUnread: "Ungelesen", filterAll: "Alle", }, + // objectui#13460 — the share-link dialog (`@object-ui/components` `ShareDialog`). + shareDialog: { + title: '{{label}} teilen', + thisRecord: 'diesen Datensatz', + description: 'Erzeugen Sie einen Link, mit dem jeder (oder ausgewählte Personen) diesen Datensatz öffnen kann. Links lassen sich jederzeit widerrufen.', + whoCanAccess: 'Wer hat Zugriff', + audienceLinkOnly: 'Jeder mit dem Link', + audienceLinkOnlyHelp: 'Keine Anmeldung erforderlich', + audienceSignedIn: 'Angemeldete Benutzer', + audienceSignedInHelp: 'Zum Ansehen ist eine Anmeldung nötig', + expiresIn: 'Läuft ab in', + expiry1d: '1 Tag', + expiry7d: '7 Tage', + expiry30d: '30 Tage', + expiry90d: '90 Tage', + expiryNever: 'Nie', + password: 'Passwort (optional)', + passwordPlaceholder: 'Zum Öffnen ein Passwort verlangen', + label: 'Bezeichnung (nur für Sie sichtbar)', + labelPlaceholder: 'z. B. An Design-Review gesendet', + createLink: 'Link erstellen', + existingLinks: 'Vorhandene Links', + existingLinksCount: 'Vorhandene Links ({{n}})', + noLinks: 'Noch keine Freigabelinks. Erstellen Sie oben einen.', + revokedHeading: 'Widerrufen', + revokedBadge: 'widerrufen', + copy: 'Kopieren', + copied: 'Kopiert!', + revoke: 'Widerrufen', + loadFailed: 'Freigabelinks konnten nicht geladen werden', + createFailed: 'Freigabelink konnte nicht erstellt werden', + revokeFailed: 'Link konnte nicht widerrufen werden', + }, + // objectui#13460 — the public landing of a `/s/:token` share link (`SharedRecordPage`). + sharedRecord: { + signInTitle: 'Anmeldung erforderlich', + signInBody: 'Der Eigentümer hat diesen Link nur für angemeldete Benutzer freigegeben.', + signIn: 'Anmelden', + passwordTitle: 'Passwort erforderlich', + passwordBody: 'Der Eigentümer hat diesen Link mit einem Passwort geschützt.', + passwordLabel: 'Passwort', + passwordPlaceholder: 'Passwort eingeben', + continue: 'Weiter', + cannotOpen: 'Dieser Link lässt sich nicht öffnen', + passwordUnsendable: 'Dieses Passwort enthält Zeichen, die nicht gesendet werden können, etwa chinesische Schriftzeichen oder Emojis. Bitten Sie den Eigentümer des Links, ein Passwort ohne solche Zeichen festzulegen.', + wrongPassword: 'Falsches Passwort.', + loadFailedStatus: 'Laden fehlgeschlagen (HTTP {{status}}).', + invalidLink: 'Dieser Link ist ungültig oder nicht mehr verfügbar.', + expiredLink: 'Dieser Link ist abgelaufen oder wurde widerrufen.', + loadFailed: 'Geteilte Inhalte konnten nicht geladen werden.', + sharedConversation: 'Geteilte Unterhaltung', + loadingMessages: 'Nachrichten werden geladen…', + noMessages: 'Diese Unterhaltung enthält noch keine Nachrichten.', + sharedObject: 'Geteilt: {{object}}', + redactedFields: 'Einige Felder hat der Eigentümer ausgeblendet: {{fields}}', + }, publicForm: { submit: "Absenden", submitting: "Sende…", diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 671c70594f..ef2ecfa2f7 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -2905,6 +2905,61 @@ const en = { redirectHint: 'An in-app path, starting with {{slash}} — interpolate a field of the submitted record as {{token}}. To send the browser out of the app, use an app navigation item instead.', saving: 'Saving…', }, + // objectui#13460 — Developer → Flow Runs (`FlowRunsPage`). + flowRuns: { + title: 'Flow Runs', + subtitle: 'Pick a flow, trigger a test run with sample inputs, and inspect recent run history', + flowTitle: 'Flow', + flowDescription: 'Select a flow definition to test', + noFlows: 'No flow definitions found.', + testRunTitle: 'Test Run', + testRunDescription: 'Provide input values and invoke the flow', + noInputs: 'This flow declares no input variables. Click {{action}} to invoke it with no parameters.', + running: 'Running…', + runFlow: 'Run Flow', + failed: 'Failed', + waitingForInput: 'Waiting for input', + result: 'Result', + pausedAtScreen: 'This run is paused at screen {{node}}.', + continueRun: 'Continue run', + recentRuns: 'Recent Runs', + noRuns: 'No runs yet. Use the {{panel}} panel to invoke this flow.', + columnRun: 'Run', + columnStatus: 'Status', + columnStarted: 'Started', + columnDuration: 'Duration', + runTitle: 'Run {{id}}', + stepInput: 'Input', + stepOutput: 'Output', + stepError: 'Error', + }, + // objectui#13460 — the Integrations page's "Connect an AI agent" card (`AgentConnectSection`). + agentConnect: { + title: 'Connect an AI agent (MCP)', + description: 'Every app in this environment is an agent-ready toolset. Connect Claude, Cursor, Codex or any MCP client — one connection covers all your apps (new ones appear automatically).', + endpoint: 'MCP endpoint', + disabledTitle: 'MCP is not enabled for this environment', + disabledBody: 'Connecting agents is opt-in. An admin can enable it by setting {{flag}}. You can still generate a key and download the skill below.', + copyMcpUrl: 'Copy MCP URL', + apiKey: 'API key', + keyNamePlaceholder: 'Key name (e.g. Claude desktop)', + generating: 'Generating…', + generateKey: 'Generate key', + keyHint: 'The key acts as you (your permissions + row-level security). Shown once — copy it immediately. Revoke anytime from the API Keys list.', + noKeyInResponse: 'The request succeeded but the response carried no API key. Nothing failed in transit — inspect the response body of {{route}}.', + requestFailed: 'Request failed ({{status}})', + generateFailed: 'Failed to generate key', + skillTitle: 'Agent skill (portable)', + skillHint: 'Works with any skills-capable agent (Claude, Codex, Gemini, Cursor…).', + stepsTitle: 'Connect in 3 steps', + step1: 'Add a remote MCP server pointing at the endpoint above.', + step2: 'Set the header {{header}} (a session Bearer token is not an API key).', + step3: 'Drop in {{file}} so the agent knows how to drive your data.', + newKeyTitle: 'Your new API key', + newKeyDescription: 'Copy this now — {{notice}}', + newKeyNotice: 'for your security it will not be shown again.', + sendAs: 'Send it as {{header}}. It carries your permissions — store it like a password and revoke it if leaked.', + }, }, system: { aiApprovals: { @@ -2913,6 +2968,45 @@ const en = { queueTitle: 'Queue', queueDescription: 'Polled every 5 seconds. Decisions are recorded under your identity.', }, + // objectui#13460 — System → Audit Log (`AuditLogPage`). + auditLog: { + title: 'Audit Log', + subtitle: 'Read-only system activity and record changes. Newest first.', + openAsList: 'Open as list', + openPresetAsList: 'Open {{label}} as list', + presets: 'Presets', + presetOtherSource: 'This view reads another data source.', + presetFilterUnsupported: "This view's filter cannot be applied here: {{message}}", + action: 'Action', + allActions: 'All actions', + object: 'Object', + anyObject: 'Any object', + searchObjects: 'Search objects…', + loadingObjects: 'Loading objects…', + noObjects: 'No objects found.', + actor: 'Actor', + anyUser: 'Any user', + from: 'From', + to: 'To', + clearFilters: 'Clear filters ({{n}})', + loadFailed: 'Failed to load audit log', + emptyTitle: 'No audit entries', + emptyFiltered: 'Try clearing filters or widening the date range.', + emptyUnfiltered: 'Events will appear here once tracked objects are modified.', + timestamp: 'Timestamp', + page: 'Page {{page}}', + pageEnd: 'Page {{page}} · end', + eventFallback: 'Audit Event', + ipAddress: 'IP Address', + tenant: 'Tenant', + targetRecord: 'Target record', + userAgent: 'User Agent', + changePayload: 'Change payload', + before: 'Before', + after: 'After', + metadata: 'Metadata', + system: 'System', + }, }, }, auth: { @@ -4040,6 +4134,62 @@ const en = { desktopBlocked: 'Blocked. Allow notifications for this site in your browser settings.', desktopUnsupported: 'This browser does not support desktop notifications.', }, + // objectui#13460 — the share-link dialog (`@object-ui/components` `ShareDialog`). + shareDialog: { + title: 'Share {{label}}', + thisRecord: 'this record', + description: 'Generate a link anyone (or selected people) can use to open this record. Links can be revoked at any time.', + whoCanAccess: 'Who can access', + audienceLinkOnly: 'Anyone with the link', + audienceLinkOnlyHelp: 'No sign-in required', + audienceSignedIn: 'Signed-in users', + audienceSignedInHelp: 'Must be logged in to view', + expiresIn: 'Expires in', + expiry1d: '1 day', + expiry7d: '7 days', + expiry30d: '30 days', + expiry90d: '90 days', + expiryNever: 'Never', + password: 'Password (optional)', + passwordPlaceholder: 'Require a password to open', + label: 'Label (only you see this)', + labelPlaceholder: 'e.g. Sent to design review', + createLink: 'Create link', + existingLinks: 'Existing links', + existingLinksCount: 'Existing links ({{n}})', + noLinks: 'No share links yet. Create one above.', + revokedHeading: 'Revoked', + revokedBadge: 'revoked', + copy: 'Copy', + copied: 'Copied!', + revoke: 'Revoke', + loadFailed: 'Failed to load share links', + createFailed: 'Failed to create share link', + revokeFailed: 'Failed to revoke link', + }, + // objectui#13460 — the public landing of a `/s/:token` share link (`SharedRecordPage`). + sharedRecord: { + signInTitle: 'Sign in required', + signInBody: 'The owner shared this link with signed-in users only.', + signIn: 'Sign in', + passwordTitle: 'Password required', + passwordBody: 'The owner protected this link with a password.', + passwordLabel: 'Password', + passwordPlaceholder: 'Enter password', + continue: 'Continue', + cannotOpen: "Can't open this link", + passwordUnsendable: "This password has characters that can't be sent, such as Chinese characters or emoji. Ask the link's owner to set a password without them.", + wrongPassword: 'Wrong password.', + loadFailedStatus: 'Failed to load (HTTP {{status}}).', + invalidLink: 'This link is invalid or no longer available.', + expiredLink: 'This link has expired or was revoked.', + loadFailed: 'Failed to load shared content.', + sharedConversation: 'Shared conversation', + loadingMessages: 'Loading messages…', + noMessages: 'This conversation has no messages yet.', + sharedObject: 'Shared {{object}}', + redactedFields: 'Some fields are hidden by the owner: {{fields}}', + }, publicForm: { submit: 'Submit', submitting: 'Submitting…', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index d16c7740cd..464ece759f 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -2434,6 +2434,61 @@ const es = { redirectHint: "Una ruta dentro de la aplicación que empieza por {{slash}}: inserta un campo del registro enviado como {{token}}. Para llevar el navegador fuera de la aplicación, usa en su lugar un elemento de navegación de la aplicación.", saving: "Guardando…", }, + // objectui#13460 — Developer → Flow Runs (`FlowRunsPage`). + flowRuns: { + title: 'Ejecuciones de flujo', + subtitle: 'Elija un flujo, lance una ejecución de prueba con entradas de ejemplo y revise el historial reciente de ejecuciones', + flowTitle: 'Flujo', + flowDescription: 'Seleccione una definición de flujo para probar', + noFlows: 'No se encontraron definiciones de flujo.', + testRunTitle: 'Ejecución de prueba', + testRunDescription: 'Proporcione valores de entrada e invoque el flujo', + noInputs: 'Este flujo no declara variables de entrada. Haga clic en {{action}} para invocarlo sin parámetros.', + running: 'Ejecutando…', + runFlow: 'Ejecutar flujo', + failed: 'Falló', + waitingForInput: 'Esperando entrada', + result: 'Resultado', + pausedAtScreen: 'Esta ejecución está en pausa en la pantalla {{node}}.', + continueRun: 'Continuar ejecución', + recentRuns: 'Ejecuciones recientes', + noRuns: 'Aún no hay ejecuciones. Use el panel {{panel}} para invocar este flujo.', + columnRun: 'Ejecución', + columnStatus: 'Estado', + columnStarted: 'Iniciada', + columnDuration: 'Duración', + runTitle: 'Ejecución {{id}}', + stepInput: 'Entrada', + stepOutput: 'Salida', + stepError: 'Error', + }, + // objectui#13460 — the Integrations page's "Connect an AI agent" card (`AgentConnectSection`). + agentConnect: { + title: 'Conectar un agente de IA (MCP)', + description: 'Cada aplicación de este entorno es un conjunto de herramientas listo para agentes. Conecte Claude, Cursor, Codex o cualquier cliente MCP: una sola conexión cubre todas sus aplicaciones (las nuevas aparecen automáticamente).', + endpoint: 'Punto de conexión MCP', + disabledTitle: 'MCP no está habilitado en este entorno', + disabledBody: 'Conectar agentes es opcional. Un administrador puede habilitarlo configurando {{flag}}. Aun así, puede generar una clave y descargar el skill a continuación.', + copyMcpUrl: 'Copiar la URL de MCP', + apiKey: 'Clave API', + keyNamePlaceholder: 'Nombre de la clave (p. ej. Claude Desktop)', + generating: 'Generando…', + generateKey: 'Generar clave', + keyHint: 'La clave actúa como usted (sus permisos + seguridad a nivel de fila). Se muestra una sola vez: cópiela de inmediato. Revóquela cuando quiera desde la lista de claves API.', + noKeyInResponse: 'La solicitud se completó, pero la respuesta no incluía ninguna clave API. Nada falló en tránsito: inspeccione el cuerpo de la respuesta de {{route}}.', + requestFailed: 'La solicitud falló ({{status}})', + generateFailed: 'No se pudo generar la clave', + skillTitle: 'Skill de agente (portátil)', + skillHint: 'Funciona con cualquier agente compatible con skills (Claude, Codex, Gemini, Cursor…).', + stepsTitle: 'Conectar en 3 pasos', + step1: 'Agregue un servidor MCP remoto que apunte al punto de conexión de arriba.', + step2: 'Configure el encabezado {{header}} (un token Bearer de sesión no es una clave API).', + step3: 'Agregue {{file}} para que el agente sepa cómo manejar sus datos.', + newKeyTitle: 'Su nueva clave API', + newKeyDescription: 'Cópiela ahora: {{notice}}', + newKeyNotice: 'por su seguridad, no se volverá a mostrar.', + sendAs: 'Envíela como {{header}}. Lleva sus permisos: guárdela como una contraseña y revóquela si se filtra.', + }, }, system: { aiApprovals: { @@ -2442,6 +2497,45 @@ const es = { queueTitle: "Cola", queueDescription: "Se consulta cada 5 segundos. Las decisiones se registran con tu identidad.", }, + // objectui#13460 — System → Audit Log (`AuditLogPage`). + auditLog: { + title: 'Registro de auditoría', + subtitle: 'Actividad del sistema y cambios de registros, de solo lectura. Lo más reciente primero.', + openAsList: 'Abrir como lista', + openPresetAsList: 'Abrir {{label}} como lista', + presets: 'Ajustes predefinidos', + presetOtherSource: 'Esta vista lee otra fuente de datos.', + presetFilterUnsupported: 'El filtro de esta vista no se puede aplicar aquí: {{message}}', + action: 'Acción', + allActions: 'Todas las acciones', + object: 'Objeto', + anyObject: 'Cualquier objeto', + searchObjects: 'Buscar objetos…', + loadingObjects: 'Cargando objetos…', + noObjects: 'No se encontraron objetos.', + actor: 'Actor', + anyUser: 'Cualquier usuario', + from: 'Desde', + to: 'Hasta', + clearFilters: 'Borrar filtros ({{n}})', + loadFailed: 'No se pudo cargar el registro de auditoría', + emptyTitle: 'No hay entradas de auditoría', + emptyFiltered: 'Pruebe a borrar los filtros o ampliar el rango de fechas.', + emptyUnfiltered: 'Los eventos aparecerán aquí cuando se modifiquen objetos rastreados.', + timestamp: 'Fecha y hora', + page: 'Página {{page}}', + pageEnd: 'Página {{page}} · fin', + eventFallback: 'Evento de auditoría', + ipAddress: 'Dirección IP', + tenant: 'Inquilino', + targetRecord: 'Registro de destino', + userAgent: 'Agente de usuario', + changePayload: 'Contenido del cambio', + before: 'Antes', + after: 'Después', + metadata: 'Metadatos', + system: 'Sistema', + }, }, search: "Buscar…", nav: { @@ -3396,6 +3490,62 @@ const es = { filterUnread: "No leídos", filterAll: "Todos", }, + // objectui#13460 — the share-link dialog (`@object-ui/components` `ShareDialog`). + shareDialog: { + title: 'Compartir {{label}}', + thisRecord: 'este registro', + description: 'Genere un enlace que cualquiera (o las personas que elija) pueda usar para abrir este registro. Los enlaces se pueden revocar en cualquier momento.', + whoCanAccess: 'Quién puede acceder', + audienceLinkOnly: 'Cualquiera con el enlace', + audienceLinkOnlyHelp: 'No requiere iniciar sesión', + audienceSignedIn: 'Usuarios con sesión iniciada', + audienceSignedInHelp: 'Debe iniciar sesión para verlo', + expiresIn: 'Caduca en', + expiry1d: '1 día', + expiry7d: '7 días', + expiry30d: '30 días', + expiry90d: '90 días', + expiryNever: 'Nunca', + password: 'Contraseña (opcional)', + passwordPlaceholder: 'Exigir una contraseña para abrir', + label: 'Etiqueta (solo usted la ve)', + labelPlaceholder: 'p. ej. Enviado a revisión de diseño', + createLink: 'Crear enlace', + existingLinks: 'Enlaces existentes', + existingLinksCount: 'Enlaces existentes ({{n}})', + noLinks: 'Aún no hay enlaces para compartir. Cree uno arriba.', + revokedHeading: 'Revocados', + revokedBadge: 'revocado', + copy: 'Copiar', + copied: '¡Copiado!', + revoke: 'Revocar', + loadFailed: 'No se pudieron cargar los enlaces para compartir', + createFailed: 'No se pudo crear el enlace para compartir', + revokeFailed: 'No se pudo revocar el enlace', + }, + // objectui#13460 — the public landing of a `/s/:token` share link (`SharedRecordPage`). + sharedRecord: { + signInTitle: 'Debe iniciar sesión', + signInBody: 'El propietario compartió este enlace solo con usuarios que hayan iniciado sesión.', + signIn: 'Iniciar sesión', + passwordTitle: 'Se requiere contraseña', + passwordBody: 'El propietario protegió este enlace con una contraseña.', + passwordLabel: 'Contraseña', + passwordPlaceholder: 'Introduzca la contraseña', + continue: 'Continuar', + cannotOpen: 'No se puede abrir este enlace', + passwordUnsendable: 'Esta contraseña contiene caracteres que no se pueden enviar, como caracteres chinos o emojis. Pida al propietario del enlace que establezca una contraseña sin ellos.', + wrongPassword: 'Contraseña incorrecta.', + loadFailedStatus: 'No se pudo cargar (HTTP {{status}}).', + invalidLink: 'Este enlace no es válido o ya no está disponible.', + expiredLink: 'Este enlace caducó o fue revocado.', + loadFailed: 'No se pudo cargar el contenido compartido.', + sharedConversation: 'Conversación compartida', + loadingMessages: 'Cargando mensajes…', + noMessages: 'Esta conversación aún no tiene mensajes.', + sharedObject: '{{object}} compartido', + redactedFields: 'El propietario ocultó algunos campos: {{fields}}', + }, publicForm: { submit: "Enviar", submitting: "Enviando…", diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index f64f4f22b1..16c2d6bef7 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -2430,6 +2430,61 @@ const fr = { redirectHint: "Un chemin interne à l'application, commençant par {{slash}} — insérez un champ de l'enregistrement envoyé sous la forme {{token}}. Pour envoyer le navigateur hors de l'application, utilisez plutôt un élément de navigation de l'application.", saving: "Enregistrement…", }, + // objectui#13460 — Developer → Flow Runs (`FlowRunsPage`). + flowRuns: { + title: 'Exécutions de flux', + subtitle: 'Choisissez un flux, lancez une exécution de test avec des entrées d’exemple et consultez l’historique récent des exécutions', + flowTitle: 'Flux', + flowDescription: 'Sélectionnez une définition de flux à tester', + noFlows: 'Aucune définition de flux trouvée.', + testRunTitle: 'Exécution de test', + testRunDescription: 'Renseignez les valeurs d’entrée et invoquez le flux', + noInputs: 'Ce flux ne déclare aucune variable d’entrée. Cliquez sur {{action}} pour l’invoquer sans paramètre.', + running: 'Exécution…', + runFlow: 'Exécuter le flux', + failed: 'Échec', + waitingForInput: 'En attente de saisie', + result: 'Résultat', + pausedAtScreen: 'Cette exécution est en pause sur l’écran {{node}}.', + continueRun: 'Reprendre l’exécution', + recentRuns: 'Exécutions récentes', + noRuns: 'Aucune exécution pour l’instant. Utilisez le panneau {{panel}} pour invoquer ce flux.', + columnRun: 'Exécution', + columnStatus: 'Statut', + columnStarted: 'Démarrée', + columnDuration: 'Durée', + runTitle: 'Exécution {{id}}', + stepInput: 'Entrée', + stepOutput: 'Sortie', + stepError: 'Erreur', + }, + // objectui#13460 — the Integrations page's "Connect an AI agent" card (`AgentConnectSection`). + agentConnect: { + title: 'Connecter un agent IA (MCP)', + description: 'Chaque application de cet environnement est une boîte à outils prête pour les agents. Connectez Claude, Cursor, Codex ou tout client MCP — une seule connexion couvre toutes vos applications (les nouvelles apparaissent automatiquement).', + endpoint: 'Point de terminaison MCP', + disabledTitle: 'MCP n’est pas activé pour cet environnement', + disabledBody: 'La connexion d’agents est facultative. Un administrateur peut l’activer en définissant {{flag}}. Vous pouvez tout de même générer une clé et télécharger le skill ci-dessous.', + copyMcpUrl: 'Copier l’URL MCP', + apiKey: 'Clé API', + keyNamePlaceholder: 'Nom de la clé (ex. Claude Desktop)', + generating: 'Génération…', + generateKey: 'Générer une clé', + keyHint: 'La clé agit en votre nom (vos autorisations + sécurité au niveau des lignes). Elle n’est affichée qu’une fois — copiez-la immédiatement. Révoquez-la à tout moment depuis la liste des clés API.', + noKeyInResponse: 'La requête a réussi, mais la réponse ne contenait aucune clé API. Rien n’a échoué en transit — inspectez le corps de la réponse de {{route}}.', + requestFailed: 'Échec de la requête ({{status}})', + generateFailed: 'Échec de la génération de la clé', + skillTitle: 'Skill d’agent (portable)', + skillHint: 'Fonctionne avec tout agent compatible avec les skills (Claude, Codex, Gemini, Cursor…).', + stepsTitle: 'Connexion en 3 étapes', + step1: 'Ajoutez un serveur MCP distant pointant vers le point de terminaison ci-dessus.', + step2: 'Définissez l’en-tête {{header}} (un jeton Bearer de session n’est pas une clé API).', + step3: 'Déposez {{file}} pour que l’agent sache comment piloter vos données.', + newKeyTitle: 'Votre nouvelle clé API', + newKeyDescription: 'Copiez-la maintenant — {{notice}}', + newKeyNotice: 'pour votre sécurité, elle ne sera plus affichée.', + sendAs: 'Envoyez-la en tant que {{header}}. Elle porte vos autorisations — conservez-la comme un mot de passe et révoquez-la en cas de fuite.', + }, }, system: { aiApprovals: { @@ -2438,6 +2493,45 @@ const fr = { queueTitle: "File d'attente", queueDescription: "Interrogée toutes les 5 secondes. Les décisions sont enregistrées sous votre identité.", }, + // objectui#13460 — System → Audit Log (`AuditLogPage`). + auditLog: { + title: 'Journal d’audit', + subtitle: 'Activité système et modifications d’enregistrements, en lecture seule. Les plus récentes en premier.', + openAsList: 'Ouvrir en liste', + openPresetAsList: 'Ouvrir {{label}} en liste', + presets: 'Préréglages', + presetOtherSource: 'Cette vue lit une autre source de données.', + presetFilterUnsupported: 'Le filtre de cette vue ne peut pas être appliqué ici : {{message}}', + action: 'Action', + allActions: 'Toutes les actions', + object: 'Objet', + anyObject: 'N’importe quel objet', + searchObjects: 'Rechercher des objets…', + loadingObjects: 'Chargement des objets…', + noObjects: 'Aucun objet trouvé.', + actor: 'Acteur', + anyUser: 'N’importe quel utilisateur', + from: 'Du', + to: 'Au', + clearFilters: 'Effacer les filtres ({{n}})', + loadFailed: 'Échec du chargement du journal d’audit', + emptyTitle: 'Aucune entrée d’audit', + emptyFiltered: 'Essayez d’effacer les filtres ou d’élargir la plage de dates.', + emptyUnfiltered: 'Les événements apparaîtront ici dès que des objets suivis seront modifiés.', + timestamp: 'Horodatage', + page: 'Page {{page}}', + pageEnd: 'Page {{page}} · fin', + eventFallback: 'Événement d’audit', + ipAddress: 'Adresse IP', + tenant: 'Locataire', + targetRecord: 'Enregistrement cible', + userAgent: 'Agent utilisateur', + changePayload: 'Contenu de la modification', + before: 'Avant', + after: 'Après', + metadata: 'Métadonnées', + system: 'Système', + }, }, search: "Rechercher…", nav: { @@ -3392,6 +3486,62 @@ const fr = { filterUnread: "Non lus", filterAll: "Tous", }, + // objectui#13460 — the share-link dialog (`@object-ui/components` `ShareDialog`). + shareDialog: { + title: 'Partager {{label}}', + thisRecord: 'cet enregistrement', + description: 'Générez un lien que n’importe qui (ou certaines personnes) peut utiliser pour ouvrir cet enregistrement. Les liens peuvent être révoqués à tout moment.', + whoCanAccess: 'Qui peut accéder', + audienceLinkOnly: 'Toute personne disposant du lien', + audienceLinkOnlyHelp: 'Aucune connexion requise', + audienceSignedIn: 'Utilisateurs connectés', + audienceSignedInHelp: 'Connexion requise pour consulter', + expiresIn: 'Expire dans', + expiry1d: '1 jour', + expiry7d: '7 jours', + expiry30d: '30 jours', + expiry90d: '90 jours', + expiryNever: 'Jamais', + password: 'Mot de passe (facultatif)', + passwordPlaceholder: 'Exiger un mot de passe pour ouvrir', + label: 'Libellé (visible par vous seul)', + labelPlaceholder: 'ex. Envoyé pour la revue de design', + createLink: 'Créer le lien', + existingLinks: 'Liens existants', + existingLinksCount: 'Liens existants ({{n}})', + noLinks: 'Aucun lien de partage pour l’instant. Créez-en un ci-dessus.', + revokedHeading: 'Révoqués', + revokedBadge: 'révoqué', + copy: 'Copier', + copied: 'Copié !', + revoke: 'Révoquer', + loadFailed: 'Échec du chargement des liens de partage', + createFailed: 'Échec de la création du lien de partage', + revokeFailed: 'Échec de la révocation du lien', + }, + // objectui#13460 — the public landing of a `/s/:token` share link (`SharedRecordPage`). + sharedRecord: { + signInTitle: 'Connexion requise', + signInBody: 'Le propriétaire a partagé ce lien avec les utilisateurs connectés uniquement.', + signIn: 'Se connecter', + passwordTitle: 'Mot de passe requis', + passwordBody: 'Le propriétaire a protégé ce lien par un mot de passe.', + passwordLabel: 'Mot de passe', + passwordPlaceholder: 'Saisissez le mot de passe', + continue: 'Continuer', + cannotOpen: 'Impossible d’ouvrir ce lien', + passwordUnsendable: 'Ce mot de passe contient des caractères qui ne peuvent pas être envoyés, comme des caractères chinois ou des emoji. Demandez au propriétaire du lien de définir un mot de passe sans ces caractères.', + wrongPassword: 'Mot de passe incorrect.', + loadFailedStatus: 'Échec du chargement (HTTP {{status}}).', + invalidLink: 'Ce lien est invalide ou n’est plus disponible.', + expiredLink: 'Ce lien a expiré ou a été révoqué.', + loadFailed: 'Échec du chargement du contenu partagé.', + sharedConversation: 'Conversation partagée', + loadingMessages: 'Chargement des messages…', + noMessages: 'Cette conversation ne contient encore aucun message.', + sharedObject: '{{object}} partagé', + redactedFields: 'Certains champs sont masqués par le propriétaire : {{fields}}', + }, publicForm: { submit: "Envoyer", submitting: "Envoi…", diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index 0cab7d003c..ae922186d1 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -2384,6 +2384,61 @@ const ja = { redirectHint: "{{slash}} で始まるアプリ内のパスです。送信されたレコードのフィールドは {{token}} の形で埋め込めます。ブラウザーをアプリの外へ移動させるには、代わりにアプリのナビゲーション項目を使用してください。", saving: "保存しています…", }, + // objectui#13460 — Developer → Flow Runs (`FlowRunsPage`). + flowRuns: { + title: 'フロー実行', + subtitle: 'フローを選び、サンプル入力でテスト実行して、最近の実行履歴を確認します', + flowTitle: 'フロー', + flowDescription: 'テストするフロー定義を選択', + noFlows: 'フロー定義が見つかりません。', + testRunTitle: 'テスト実行', + testRunDescription: '入力値を指定してフローを呼び出します', + noInputs: 'このフローには入力変数が宣言されていません。「{{action}}」をクリックすると、パラメーターなしで呼び出します。', + running: '実行中…', + runFlow: 'フローを実行', + failed: '失敗', + waitingForInput: '入力待ち', + result: '結果', + pausedAtScreen: 'この実行は画面 {{node}} で一時停止しています。', + continueRun: '実行を続行', + recentRuns: '最近の実行', + noRuns: 'まだ実行はありません。「{{panel}}」パネルからこのフローを呼び出してください。', + columnRun: '実行', + columnStatus: 'ステータス', + columnStarted: '開始日時', + columnDuration: '所要時間', + runTitle: '実行 {{id}}', + stepInput: '入力', + stepOutput: '出力', + stepError: 'エラー', + }, + // objectui#13460 — the Integrations page's "Connect an AI agent" card (`AgentConnectSection`). + agentConnect: { + title: 'AI エージェントを接続 (MCP)', + description: 'この環境のすべてのアプリは、エージェントがそのまま使えるツールセットです。Claude、Cursor、Codex など任意の MCP クライアントを接続できます — 1 回の接続ですべてのアプリをカバーします(新しいアプリも自動で表示されます)。', + endpoint: 'MCP エンドポイント', + disabledTitle: 'この環境では MCP が有効になっていません', + disabledBody: 'エージェントの接続はオプトインです。管理者が {{flag}} を設定すると有効になります。下でキーの生成とスキルのダウンロードは引き続き行えます。', + copyMcpUrl: 'MCP URL をコピー', + apiKey: 'API キー', + keyNamePlaceholder: 'キー名(例: Claude デスクトップ)', + generating: '生成中…', + generateKey: 'キーを生成', + keyHint: 'キーはあなたとして動作します(あなたの権限 + 行レベルセキュリティ)。表示は一度きりです — すぐにコピーしてください。API キー一覧からいつでも取り消せます。', + noKeyInResponse: 'リクエストは成功しましたが、レスポンスに API キーが含まれていません。通信は失敗していません — {{route}} のレスポンス本文を確認してください。', + requestFailed: 'リクエストに失敗しました ({{status}})', + generateFailed: 'キーを生成できませんでした', + skillTitle: 'エージェントスキル(ポータブル)', + skillHint: 'スキル対応のエージェントならどれでも使えます(Claude、Codex、Gemini、Cursor…)。', + stepsTitle: '3 ステップで接続', + step1: '上のエンドポイントを指すリモート MCP サーバーを追加します。', + step2: 'ヘッダー {{header}} を設定します(セッションの Bearer トークンは API キーではありません)。', + step3: '{{file}} を配置すると、エージェントがデータの扱い方を把握できます。', + newKeyTitle: '新しい API キー', + newKeyDescription: '今すぐコピーしてください — {{notice}}', + newKeyNotice: 'セキュリティのため、二度と表示されません。', + sendAs: '{{header}} として送信します。あなたの権限を持つため、パスワードと同様に保管し、漏えいした場合は取り消してください。', + }, }, system: { aiApprovals: { @@ -2392,6 +2447,45 @@ const ja = { queueTitle: "キュー", queueDescription: "5 秒ごとに更新されます。判断はあなたの名前で記録されます。", }, + // objectui#13460 — System → Audit Log (`AuditLogPage`). + auditLog: { + title: '監査ログ', + subtitle: '読み取り専用のシステムアクティビティとレコード変更。新しい順に表示します。', + openAsList: 'リストで開く', + openPresetAsList: '{{label}} をリストで開く', + presets: 'プリセット', + presetOtherSource: 'このビューは別のデータソースを読み取ります。', + presetFilterUnsupported: 'このビューのフィルターはここでは適用できません: {{message}}', + action: '操作', + allActions: 'すべての操作', + object: 'オブジェクト', + anyObject: 'すべてのオブジェクト', + searchObjects: 'オブジェクトを検索…', + loadingObjects: 'オブジェクトを読み込み中…', + noObjects: 'オブジェクトが見つかりません。', + actor: '実行者', + anyUser: 'すべてのユーザー', + from: '開始日', + to: '終了日', + clearFilters: 'フィルターをクリア ({{n}})', + loadFailed: '監査ログを読み込めませんでした', + emptyTitle: '監査エントリはありません', + emptyFiltered: 'フィルターをクリアするか、日付範囲を広げてください。', + emptyUnfiltered: '追跡対象のオブジェクトが変更されると、ここにイベントが表示されます。', + timestamp: '日時', + page: '{{page}} ページ', + pageEnd: '{{page}} ページ · 最後', + eventFallback: '監査イベント', + ipAddress: 'IP アドレス', + tenant: 'テナント', + targetRecord: '対象レコード', + userAgent: 'ユーザーエージェント', + changePayload: '変更内容', + before: '変更前', + after: '変更後', + metadata: 'メタデータ', + system: 'システム', + }, }, search: "検索…", nav: { @@ -3334,6 +3428,62 @@ const ja = { filterUnread: "未読", filterAll: "すべて", }, + // objectui#13460 — the share-link dialog (`@object-ui/components` `ShareDialog`). + shareDialog: { + title: '{{label}}を共有', + thisRecord: 'このレコード', + description: '誰でも(または指定したユーザーが)このレコードを開けるリンクを作成します。リンクはいつでも取り消せます。', + whoCanAccess: 'アクセスできるユーザー', + audienceLinkOnly: 'リンクを知っている全員', + audienceLinkOnlyHelp: 'サインイン不要', + audienceSignedIn: 'サインイン済みのユーザー', + audienceSignedInHelp: '表示するにはサインインが必要です', + expiresIn: '有効期限', + expiry1d: '1 日', + expiry7d: '7 日', + expiry30d: '30 日', + expiry90d: '90 日', + expiryNever: '無期限', + password: 'パスワード(任意)', + passwordPlaceholder: '開くときにパスワードを要求', + label: 'ラベル(自分だけに表示)', + labelPlaceholder: '例: デザインレビューに送付', + createLink: 'リンクを作成', + existingLinks: '既存のリンク', + existingLinksCount: '既存のリンク ({{n}})', + noLinks: '共有リンクはまだありません。上で作成してください。', + revokedHeading: '取り消し済み', + revokedBadge: '取り消し済み', + copy: 'コピー', + copied: 'コピーしました', + revoke: '取り消す', + loadFailed: '共有リンクを読み込めませんでした', + createFailed: '共有リンクを作成できませんでした', + revokeFailed: 'リンクを取り消せませんでした', + }, + // objectui#13460 — the public landing of a `/s/:token` share link (`SharedRecordPage`). + sharedRecord: { + signInTitle: 'サインインが必要です', + signInBody: '所有者はこのリンクをサインイン済みのユーザーにのみ共有しています。', + signIn: 'サインイン', + passwordTitle: 'パスワードが必要です', + passwordBody: '所有者がこのリンクをパスワードで保護しています。', + passwordLabel: 'パスワード', + passwordPlaceholder: 'パスワードを入力', + continue: '続行', + cannotOpen: 'このリンクを開けません', + passwordUnsendable: 'このパスワードには、日本語の文字や絵文字など送信できない文字が含まれています。リンクの所有者に、それらを含まないパスワードを設定するよう依頼してください。', + wrongPassword: 'パスワードが違います。', + loadFailedStatus: '読み込みに失敗しました (HTTP {{status}})。', + invalidLink: 'このリンクは無効か、すでに利用できません。', + expiredLink: 'このリンクは期限切れか、取り消されています。', + loadFailed: '共有コンテンツを読み込めませんでした。', + sharedConversation: '共有された会話', + loadingMessages: 'メッセージを読み込み中…', + noMessages: 'この会話にはまだメッセージがありません。', + sharedObject: '共有された {{object}}', + redactedFields: '一部の項目は所有者によって非表示にされています: {{fields}}', + }, publicForm: { submit: "送信", submitting: "送信中…", diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 98f5aae0db..4d2e691ec5 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -2382,6 +2382,61 @@ const ko = { redirectHint: "{{slash}}(으)로 시작하는 앱 내부 경로입니다. 제출된 레코드의 필드는 {{token}} 형식으로 넣을 수 있습니다. 브라우저를 앱 밖으로 보내려면 대신 앱 탐색 항목을 사용하세요.", saving: "저장 중…", }, + // objectui#13460 — Developer → Flow Runs (`FlowRunsPage`). + flowRuns: { + title: '플로우 실행', + subtitle: '플로우를 선택하고 샘플 입력으로 테스트 실행을 시작한 뒤 최근 실행 기록을 확인합니다', + flowTitle: '플로우', + flowDescription: '테스트할 플로우 정의를 선택하세요', + noFlows: '플로우 정의를 찾을 수 없습니다.', + testRunTitle: '테스트 실행', + testRunDescription: '입력값을 지정하고 플로우를 호출합니다', + noInputs: '이 플로우에는 선언된 입력 변수가 없습니다. {{action}}을(를) 클릭하면 매개변수 없이 호출합니다.', + running: '실행 중…', + runFlow: '플로우 실행', + failed: '실패', + waitingForInput: '입력 대기 중', + result: '결과', + pausedAtScreen: '이 실행은 화면 {{node}}에서 일시 중지되었습니다.', + continueRun: '실행 계속', + recentRuns: '최근 실행', + noRuns: '아직 실행 기록이 없습니다. {{panel}} 패널에서 이 플로우를 호출하세요.', + columnRun: '실행', + columnStatus: '상태', + columnStarted: '시작 시각', + columnDuration: '소요 시간', + runTitle: '실행 {{id}}', + stepInput: '입력', + stepOutput: '출력', + stepError: '오류', + }, + // objectui#13460 — the Integrations page's "Connect an AI agent" card (`AgentConnectSection`). + agentConnect: { + title: 'AI 에이전트 연결(MCP)', + description: '이 환경의 모든 앱은 에이전트가 바로 쓸 수 있는 도구 모음입니다. Claude, Cursor, Codex 또는 모든 MCP 클라이언트를 연결하세요 — 한 번의 연결로 모든 앱을 사용할 수 있습니다(새 앱은 자동으로 나타납니다).', + endpoint: 'MCP 엔드포인트', + disabledTitle: '이 환경에서는 MCP가 활성화되어 있지 않습니다', + disabledBody: '에이전트 연결은 옵트인 방식입니다. 관리자가 {{flag}}을(를) 설정하면 활성화됩니다. 아래에서 키 생성과 스킬 다운로드는 계속할 수 있습니다.', + copyMcpUrl: 'MCP URL 복사', + apiKey: 'API 키', + keyNamePlaceholder: '키 이름(예: Claude 데스크톱)', + generating: '생성 중…', + generateKey: '키 생성', + keyHint: '키는 본인으로 동작합니다(본인 권한 + 행 수준 보안). 한 번만 표시되니 바로 복사하세요. API 키 목록에서 언제든 해지할 수 있습니다.', + noKeyInResponse: '요청은 성공했지만 응답에 API 키가 없습니다. 전송 중에는 실패하지 않았습니다 — {{route}}의 응답 본문을 확인하세요.', + requestFailed: '요청 실패({{status}})', + generateFailed: '키를 생성하지 못했습니다', + skillTitle: '에이전트 스킬(이식 가능)', + skillHint: '스킬을 지원하는 모든 에이전트에서 동작합니다(Claude, Codex, Gemini, Cursor…).', + stepsTitle: '3단계로 연결', + step1: '위 엔드포인트를 가리키는 원격 MCP 서버를 추가합니다.', + step2: '헤더 {{header}}를 설정합니다(세션 Bearer 토큰은 API 키가 아닙니다).', + step3: '{{file}}을(를) 넣으면 에이전트가 데이터를 다루는 방법을 알게 됩니다.', + newKeyTitle: '새 API 키', + newKeyDescription: '지금 복사하세요 — {{notice}}', + newKeyNotice: '보안을 위해 다시 표시되지 않습니다.', + sendAs: '{{header}}로 전송하세요. 본인 권한을 가지므로 비밀번호처럼 보관하고, 유출되면 해지하세요.', + }, }, system: { aiApprovals: { @@ -2390,6 +2445,45 @@ const ko = { queueTitle: "대기열", queueDescription: "5초마다 새로 고칩니다. 결정은 사용자 본인 이름으로 기록됩니다.", }, + // objectui#13460 — System → Audit Log (`AuditLogPage`). + auditLog: { + title: '감사 로그', + subtitle: '읽기 전용 시스템 활동 및 레코드 변경 내역입니다. 최신 항목이 먼저 표시됩니다.', + openAsList: '목록으로 열기', + openPresetAsList: '{{label}}을(를) 목록으로 열기', + presets: '프리셋', + presetOtherSource: '이 뷰는 다른 데이터 소스를 읽습니다.', + presetFilterUnsupported: '이 뷰의 필터는 여기에 적용할 수 없습니다: {{message}}', + action: '작업', + allActions: '모든 작업', + object: '개체', + anyObject: '모든 개체', + searchObjects: '개체 검색…', + loadingObjects: '개체를 불러오는 중…', + noObjects: '개체를 찾을 수 없습니다.', + actor: '수행자', + anyUser: '모든 사용자', + from: '시작일', + to: '종료일', + clearFilters: '필터 지우기({{n}})', + loadFailed: '감사 로그를 불러오지 못했습니다', + emptyTitle: '감사 항목이 없습니다', + emptyFiltered: '필터를 지우거나 날짜 범위를 넓혀 보세요.', + emptyUnfiltered: '추적 대상 개체가 수정되면 여기에 이벤트가 표시됩니다.', + timestamp: '시각', + page: '{{page}}페이지', + pageEnd: '{{page}}페이지 · 끝', + eventFallback: '감사 이벤트', + ipAddress: 'IP 주소', + tenant: '테넌트', + targetRecord: '대상 레코드', + userAgent: '사용자 에이전트', + changePayload: '변경 내용', + before: '변경 전', + after: '변경 후', + metadata: '메타데이터', + system: '시스템', + }, }, search: "검색…", nav: { @@ -3331,6 +3425,62 @@ const ko = { filterUnread: "읽지 않음", filterAll: "전체", }, + // objectui#13460 — the share-link dialog (`@object-ui/components` `ShareDialog`). + shareDialog: { + title: '{{label}} 공유', + thisRecord: '이 레코드', + description: '누구나(또는 지정한 사람이) 이 레코드를 열 수 있는 링크를 만듭니다. 링크는 언제든지 해지할 수 있습니다.', + whoCanAccess: '액세스 가능 대상', + audienceLinkOnly: '링크가 있는 모든 사용자', + audienceLinkOnlyHelp: '로그인 필요 없음', + audienceSignedIn: '로그인한 사용자', + audienceSignedInHelp: '보려면 로그인해야 합니다', + expiresIn: '만료 기간', + expiry1d: '1일', + expiry7d: '7일', + expiry30d: '30일', + expiry90d: '90일', + expiryNever: '만료 없음', + password: '비밀번호(선택)', + passwordPlaceholder: '열 때 비밀번호 요구', + label: '라벨(본인만 볼 수 있음)', + labelPlaceholder: '예: 디자인 검토에 전달함', + createLink: '링크 만들기', + existingLinks: '기존 링크', + existingLinksCount: '기존 링크({{n}})', + noLinks: '아직 공유 링크가 없습니다. 위에서 만드세요.', + revokedHeading: '해지됨', + revokedBadge: '해지됨', + copy: '복사', + copied: '복사됨!', + revoke: '해지', + loadFailed: '공유 링크를 불러오지 못했습니다', + createFailed: '공유 링크를 만들지 못했습니다', + revokeFailed: '링크를 해지하지 못했습니다', + }, + // objectui#13460 — the public landing of a `/s/:token` share link (`SharedRecordPage`). + sharedRecord: { + signInTitle: '로그인 필요', + signInBody: '소유자가 이 링크를 로그인한 사용자에게만 공유했습니다.', + signIn: '로그인', + passwordTitle: '비밀번호 필요', + passwordBody: '소유자가 이 링크를 비밀번호로 보호했습니다.', + passwordLabel: '비밀번호', + passwordPlaceholder: '비밀번호 입력', + continue: '계속', + cannotOpen: '이 링크를 열 수 없습니다', + passwordUnsendable: '이 비밀번호에는 한글이나 이모지처럼 전송할 수 없는 문자가 포함되어 있습니다. 링크 소유자에게 이러한 문자가 없는 비밀번호를 설정해 달라고 요청하세요.', + wrongPassword: '비밀번호가 틀렸습니다.', + loadFailedStatus: '불러오지 못했습니다(HTTP {{status}}).', + invalidLink: '이 링크는 유효하지 않거나 더 이상 사용할 수 없습니다.', + expiredLink: '이 링크는 만료되었거나 해지되었습니다.', + loadFailed: '공유된 콘텐츠를 불러오지 못했습니다.', + sharedConversation: '공유된 대화', + loadingMessages: '메시지를 불러오는 중…', + noMessages: '이 대화에는 아직 메시지가 없습니다.', + sharedObject: '공유된 {{object}}', + redactedFields: '일부 필드는 소유자가 숨겼습니다: {{fields}}', + }, publicForm: { submit: "제출", submitting: "제출 중…", diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index 68a9b63dd7..6947902e67 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -2429,6 +2429,61 @@ const pt = { redirectHint: "Um caminho dentro do app, começando com {{slash}} — insira um campo do registro enviado como {{token}}. Para levar o navegador para fora do app, use um item de navegação do app.", saving: "Salvando…", }, + // objectui#13460 — Developer → Flow Runs (`FlowRunsPage`). + flowRuns: { + title: 'Execuções de fluxo', + subtitle: 'Escolha um fluxo, dispare uma execução de teste com entradas de exemplo e confira o histórico recente de execuções', + flowTitle: 'Fluxo', + flowDescription: 'Selecione uma definição de fluxo para testar', + noFlows: 'Nenhuma definição de fluxo encontrada.', + testRunTitle: 'Execução de teste', + testRunDescription: 'Informe os valores de entrada e invoque o fluxo', + noInputs: 'Este fluxo não declara variáveis de entrada. Clique em {{action}} para invocá-lo sem parâmetros.', + running: 'Executando…', + runFlow: 'Executar fluxo', + failed: 'Falhou', + waitingForInput: 'Aguardando entrada', + result: 'Resultado', + pausedAtScreen: 'Esta execução está pausada na tela {{node}}.', + continueRun: 'Continuar execução', + recentRuns: 'Execuções recentes', + noRuns: 'Nenhuma execução ainda. Use o painel {{panel}} para invocar este fluxo.', + columnRun: 'Execução', + columnStatus: 'Status', + columnStarted: 'Iniciada', + columnDuration: 'Duração', + runTitle: 'Execução {{id}}', + stepInput: 'Entrada', + stepOutput: 'Saída', + stepError: 'Erro', + }, + // objectui#13460 — the Integrations page's "Connect an AI agent" card (`AgentConnectSection`). + agentConnect: { + title: 'Conectar um agente de IA (MCP)', + description: 'Cada aplicativo deste ambiente é um conjunto de ferramentas pronto para agentes. Conecte Claude, Cursor, Codex ou qualquer cliente MCP — uma única conexão cobre todos os seus aplicativos (os novos aparecem automaticamente).', + endpoint: 'Endpoint do MCP', + disabledTitle: 'O MCP não está habilitado neste ambiente', + disabledBody: 'Conectar agentes é opcional. Um administrador pode habilitar definindo {{flag}}. Você ainda pode gerar uma chave e baixar o skill abaixo.', + copyMcpUrl: 'Copiar URL do MCP', + apiKey: 'Chave de API', + keyNamePlaceholder: 'Nome da chave (ex. Claude Desktop)', + generating: 'Gerando…', + generateKey: 'Gerar chave', + keyHint: 'A chave age como você (suas permissões + segurança em nível de linha). É mostrada uma única vez — copie-a imediatamente. Revogue-a quando quiser na lista de chaves de API.', + noKeyInResponse: 'A solicitação foi bem-sucedida, mas a resposta não trouxe nenhuma chave de API. Nada falhou no transporte — inspecione o corpo da resposta de {{route}}.', + requestFailed: 'Falha na solicitação ({{status}})', + generateFailed: 'Falha ao gerar a chave', + skillTitle: 'Skill de agente (portátil)', + skillHint: 'Funciona com qualquer agente compatível com skills (Claude, Codex, Gemini, Cursor…).', + stepsTitle: 'Conectar em 3 passos', + step1: 'Adicione um servidor MCP remoto apontando para o endpoint acima.', + step2: 'Defina o cabeçalho {{header}} (um token Bearer de sessão não é uma chave de API).', + step3: 'Adicione {{file}} para que o agente saiba como operar seus dados.', + newKeyTitle: 'Sua nova chave de API', + newKeyDescription: 'Copie agora — {{notice}}', + newKeyNotice: 'por segurança, ela não será mostrada novamente.', + sendAs: 'Envie-a como {{header}}. Ela carrega suas permissões — guarde-a como uma senha e revogue-a se vazar.', + }, }, system: { aiApprovals: { @@ -2437,6 +2492,45 @@ const pt = { queueTitle: "Fila", queueDescription: "Consultada a cada 5 segundos. As decisões são registradas com a sua identidade.", }, + // objectui#13460 — System → Audit Log (`AuditLogPage`). + auditLog: { + title: 'Log de auditoria', + subtitle: 'Atividade do sistema e alterações de registros, somente leitura. Mais recentes primeiro.', + openAsList: 'Abrir como lista', + openPresetAsList: 'Abrir {{label}} como lista', + presets: 'Predefinições', + presetOtherSource: 'Esta visualização lê outra fonte de dados.', + presetFilterUnsupported: 'O filtro desta visualização não pode ser aplicado aqui: {{message}}', + action: 'Ação', + allActions: 'Todas as ações', + object: 'Objeto', + anyObject: 'Qualquer objeto', + searchObjects: 'Pesquisar objetos…', + loadingObjects: 'Carregando objetos…', + noObjects: 'Nenhum objeto encontrado.', + actor: 'Ator', + anyUser: 'Qualquer usuário', + from: 'De', + to: 'Até', + clearFilters: 'Limpar filtros ({{n}})', + loadFailed: 'Falha ao carregar o log de auditoria', + emptyTitle: 'Nenhuma entrada de auditoria', + emptyFiltered: 'Tente limpar os filtros ou ampliar o intervalo de datas.', + emptyUnfiltered: 'Os eventos aparecerão aqui quando objetos rastreados forem modificados.', + timestamp: 'Data e hora', + page: 'Página {{page}}', + pageEnd: 'Página {{page}} · fim', + eventFallback: 'Evento de auditoria', + ipAddress: 'Endereço IP', + tenant: 'Locatário', + targetRecord: 'Registro de destino', + userAgent: 'Agente do usuário', + changePayload: 'Conteúdo da alteração', + before: 'Antes', + after: 'Depois', + metadata: 'Metadados', + system: 'Sistema', + }, }, search: "Pesquisar…", nav: { @@ -3391,6 +3485,62 @@ const pt = { filterUnread: "Não lidos", filterAll: "Todos", }, + // objectui#13460 — the share-link dialog (`@object-ui/components` `ShareDialog`). + shareDialog: { + title: 'Compartilhar {{label}}', + thisRecord: 'este registro', + description: 'Gere um link que qualquer pessoa (ou pessoas selecionadas) possa usar para abrir este registro. Os links podem ser revogados a qualquer momento.', + whoCanAccess: 'Quem pode acessar', + audienceLinkOnly: 'Qualquer pessoa com o link', + audienceLinkOnlyHelp: 'Não é necessário entrar', + audienceSignedIn: 'Usuários conectados', + audienceSignedInHelp: 'É preciso entrar para visualizar', + expiresIn: 'Expira em', + expiry1d: '1 dia', + expiry7d: '7 dias', + expiry30d: '30 dias', + expiry90d: '90 dias', + expiryNever: 'Nunca', + password: 'Senha (opcional)', + passwordPlaceholder: 'Exigir uma senha para abrir', + label: 'Rótulo (só você vê)', + labelPlaceholder: 'ex. Enviado para revisão de design', + createLink: 'Criar link', + existingLinks: 'Links existentes', + existingLinksCount: 'Links existentes ({{n}})', + noLinks: 'Ainda não há links de compartilhamento. Crie um acima.', + revokedHeading: 'Revogados', + revokedBadge: 'revogado', + copy: 'Copiar', + copied: 'Copiado!', + revoke: 'Revogar', + loadFailed: 'Falha ao carregar os links de compartilhamento', + createFailed: 'Falha ao criar o link de compartilhamento', + revokeFailed: 'Falha ao revogar o link', + }, + // objectui#13460 — the public landing of a `/s/:token` share link (`SharedRecordPage`). + sharedRecord: { + signInTitle: 'É necessário entrar', + signInBody: 'O proprietário compartilhou este link apenas com usuários conectados.', + signIn: 'Entrar', + passwordTitle: 'Senha necessária', + passwordBody: 'O proprietário protegeu este link com uma senha.', + passwordLabel: 'Senha', + passwordPlaceholder: 'Digite a senha', + continue: 'Continuar', + cannotOpen: 'Não é possível abrir este link', + passwordUnsendable: 'Esta senha contém caracteres que não podem ser enviados, como caracteres chineses ou emojis. Peça ao proprietário do link que defina uma senha sem eles.', + wrongPassword: 'Senha incorreta.', + loadFailedStatus: 'Falha ao carregar (HTTP {{status}}).', + invalidLink: 'Este link é inválido ou não está mais disponível.', + expiredLink: 'Este link expirou ou foi revogado.', + loadFailed: 'Falha ao carregar o conteúdo compartilhado.', + sharedConversation: 'Conversa compartilhada', + loadingMessages: 'Carregando mensagens…', + noMessages: 'Esta conversa ainda não tem mensagens.', + sharedObject: '{{object}} compartilhado', + redactedFields: 'Alguns campos foram ocultados pelo proprietário: {{fields}}', + }, publicForm: { submit: "Enviar", submitting: "Enviando…", diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index 39b999508a..bd5316c691 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -2491,6 +2491,61 @@ const ru = { redirectHint: "Путь внутри приложения, начинающийся с {{slash}}, — подставьте поле отправленной записи как {{token}}. Чтобы вывести браузер за пределы приложения, используйте вместо этого пункт навигации приложения.", saving: "Сохранение…", }, + // objectui#13460 — Developer → Flow Runs (`FlowRunsPage`). + flowRuns: { + title: 'Запуски потоков', + subtitle: 'Выберите поток, запустите тестовый прогон с примерами входных данных и просмотрите недавнюю историю запусков', + flowTitle: 'Поток', + flowDescription: 'Выберите определение потока для теста', + noFlows: 'Определения потоков не найдены.', + testRunTitle: 'Тестовый запуск', + testRunDescription: 'Укажите входные значения и вызовите поток', + noInputs: 'Этот поток не объявляет входных переменных. Нажмите «{{action}}», чтобы вызвать его без параметров.', + running: 'Выполняется…', + runFlow: 'Запустить поток', + failed: 'Ошибка', + waitingForInput: 'Ожидает ввода', + result: 'Результат', + pausedAtScreen: 'Этот запуск приостановлен на экране {{node}}.', + continueRun: 'Продолжить запуск', + recentRuns: 'Недавние запуски', + noRuns: 'Запусков пока нет. Вызовите этот поток на панели «{{panel}}».', + columnRun: 'Запуск', + columnStatus: 'Статус', + columnStarted: 'Начало', + columnDuration: 'Длительность', + runTitle: 'Запуск {{id}}', + stepInput: 'Вход', + stepOutput: 'Выход', + stepError: 'Ошибка', + }, + // objectui#13460 — the Integrations page's "Connect an AI agent" card (`AgentConnectSection`). + agentConnect: { + title: 'Подключение ИИ-агента (MCP)', + description: 'Каждое приложение в этой среде — готовый набор инструментов для агентов. Подключите Claude, Cursor, Codex или любой MCP-клиент — одно подключение охватывает все ваши приложения (новые появляются автоматически).', + endpoint: 'Конечная точка MCP', + disabledTitle: 'MCP не включён в этой среде', + disabledBody: 'Подключение агентов включается по желанию. Администратор может включить его, задав {{flag}}. Ниже вы всё равно можете создать ключ и скачать навык.', + copyMcpUrl: 'Копировать URL MCP', + apiKey: 'API-ключ', + keyNamePlaceholder: 'Имя ключа (напр. Claude Desktop)', + generating: 'Создание…', + generateKey: 'Создать ключ', + keyHint: 'Ключ действует от вашего имени (ваши права + безопасность на уровне строк). Показывается один раз — скопируйте его сразу. Отозвать ключ можно в любой момент в списке API-ключей.', + noKeyInResponse: 'Запрос выполнен, но в ответе нет API-ключа. При передаче ничего не сломалось — проверьте тело ответа {{route}}.', + requestFailed: 'Ошибка запроса ({{status}})', + generateFailed: 'Не удалось создать ключ', + skillTitle: 'Навык агента (переносимый)', + skillHint: 'Работает с любым агентом, поддерживающим навыки (Claude, Codex, Gemini, Cursor…).', + stepsTitle: 'Подключение за 3 шага', + step1: 'Добавьте удалённый MCP-сервер, указывающий на конечную точку выше.', + step2: 'Задайте заголовок {{header}} (Bearer-токен сеанса — это не API-ключ).', + step3: 'Добавьте {{file}}, чтобы агент знал, как работать с вашими данными.', + newKeyTitle: 'Ваш новый API-ключ', + newKeyDescription: 'Скопируйте его сейчас — {{notice}}', + newKeyNotice: 'в целях безопасности он больше не будет показан.', + sendAs: 'Передавайте его как {{header}}. Он несёт ваши права — храните его как пароль и отзовите при утечке.', + }, }, system: { aiApprovals: { @@ -2499,6 +2554,45 @@ const ru = { queueTitle: "Очередь", queueDescription: "Опрашивается каждые 5 секунд. Решения записываются от вашего имени.", }, + // objectui#13460 — System → Audit Log (`AuditLogPage`). + auditLog: { + title: 'Журнал аудита', + subtitle: 'Активность системы и изменения записей, только для чтения. Сначала новые.', + openAsList: 'Открыть списком', + openPresetAsList: 'Открыть «{{label}}» списком', + presets: 'Пресеты', + presetOtherSource: 'Это представление читает другой источник данных.', + presetFilterUnsupported: 'Фильтр этого представления нельзя применить здесь: {{message}}', + action: 'Действие', + allActions: 'Все действия', + object: 'Объект', + anyObject: 'Любой объект', + searchObjects: 'Поиск объектов…', + loadingObjects: 'Загрузка объектов…', + noObjects: 'Объекты не найдены.', + actor: 'Исполнитель', + anyUser: 'Любой пользователь', + from: 'С', + to: 'По', + clearFilters: 'Сбросить фильтры ({{n}})', + loadFailed: 'Не удалось загрузить журнал аудита', + emptyTitle: 'Нет записей аудита', + emptyFiltered: 'Попробуйте сбросить фильтры или расширить диапазон дат.', + emptyUnfiltered: 'События появятся здесь после изменения отслеживаемых объектов.', + timestamp: 'Время', + page: 'Страница {{page}}', + pageEnd: 'Страница {{page}} · конец', + eventFallback: 'Событие аудита', + ipAddress: 'IP-адрес', + tenant: 'Арендатор', + targetRecord: 'Целевая запись', + userAgent: 'Агент пользователя', + changePayload: 'Содержимое изменения', + before: 'До', + after: 'После', + metadata: 'Метаданные', + system: 'Система', + }, }, search: "Поиск…", nav: { @@ -3469,6 +3563,62 @@ const ru = { filterUnread: "Непрочитанные", filterAll: "Все", }, + // objectui#13460 — the share-link dialog (`@object-ui/components` `ShareDialog`). + shareDialog: { + title: 'Поделиться: {{label}}', + thisRecord: 'эта запись', + description: 'Создайте ссылку, по которой любой (или выбранные люди) сможет открыть эту запись. Ссылки можно отозвать в любой момент.', + whoCanAccess: 'Кто может открыть', + audienceLinkOnly: 'Все, у кого есть ссылка', + audienceLinkOnlyHelp: 'Вход не требуется', + audienceSignedIn: 'Вошедшие пользователи', + audienceSignedInHelp: 'Для просмотра нужно войти', + expiresIn: 'Срок действия', + expiry1d: '1 день', + expiry7d: '7 дней', + expiry30d: '30 дней', + expiry90d: '90 дней', + expiryNever: 'Никогда', + password: 'Пароль (необязательно)', + passwordPlaceholder: 'Запрашивать пароль при открытии', + label: 'Метка (видна только вам)', + labelPlaceholder: 'напр. Отправлено на ревью дизайна', + createLink: 'Создать ссылку', + existingLinks: 'Существующие ссылки', + existingLinksCount: 'Существующие ссылки ({{n}})', + noLinks: 'Ссылок для доступа пока нет. Создайте ссылку выше.', + revokedHeading: 'Отозванные', + revokedBadge: 'отозвана', + copy: 'Копировать', + copied: 'Скопировано!', + revoke: 'Отозвать', + loadFailed: 'Не удалось загрузить ссылки для доступа', + createFailed: 'Не удалось создать ссылку для доступа', + revokeFailed: 'Не удалось отозвать ссылку', + }, + // objectui#13460 — the public landing of a `/s/:token` share link (`SharedRecordPage`). + sharedRecord: { + signInTitle: 'Требуется вход', + signInBody: 'Владелец открыл эту ссылку только для вошедших пользователей.', + signIn: 'Войти', + passwordTitle: 'Требуется пароль', + passwordBody: 'Владелец защитил эту ссылку паролем.', + passwordLabel: 'Пароль', + passwordPlaceholder: 'Введите пароль', + continue: 'Продолжить', + cannotOpen: 'Не удаётся открыть ссылку', + passwordUnsendable: 'В этом пароле есть символы, которые нельзя отправить, например кириллица или эмодзи. Попросите владельца ссылки задать пароль без них.', + wrongPassword: 'Неверный пароль.', + loadFailedStatus: 'Не удалось загрузить (HTTP {{status}}).', + invalidLink: 'Ссылка недействительна или больше недоступна.', + expiredLink: 'Срок действия ссылки истёк, или она была отозвана.', + loadFailed: 'Не удалось загрузить общий контент.', + sharedConversation: 'Общий диалог', + loadingMessages: 'Загрузка сообщений…', + noMessages: 'В этом диалоге пока нет сообщений.', + sharedObject: 'Общий доступ: {{object}}', + redactedFields: 'Некоторые поля скрыты владельцем: {{fields}}', + }, publicForm: { submit: "Отправить", submitting: "Отправка…", diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 89dd2bf61d..e7253201c8 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -2475,6 +2475,61 @@ const zh = { redirectHint: '应用内路径,以 {{slash}} 开头——用 {{token}} 插入所提交记录的字段。若要让浏览器离开应用,请改用应用导航项。', saving: '正在保存…', }, + // objectui#13460 — Developer → Flow Runs (`FlowRunsPage`). + flowRuns: { + title: '流程运行', + subtitle: '选择一个流程,用示例输入触发测试运行,并查看最近的运行记录', + flowTitle: '流程', + flowDescription: '选择要测试的流程定义', + noFlows: '未找到流程定义。', + testRunTitle: '测试运行', + testRunDescription: '填写输入值并调用流程', + noInputs: '此流程未声明输入变量。点击「{{action}}」即可不带参数调用。', + running: '运行中…', + runFlow: '运行流程', + failed: '失败', + waitingForInput: '等待输入', + result: '结果', + pausedAtScreen: '此运行已在界面 {{node}} 处暂停。', + continueRun: '继续运行', + recentRuns: '最近运行', + noRuns: '暂无运行记录。请使用「{{panel}}」面板调用此流程。', + columnRun: '运行', + columnStatus: '状态', + columnStarted: '开始时间', + columnDuration: '耗时', + runTitle: '运行 {{id}}', + stepInput: '输入', + stepOutput: '输出', + stepError: '错误', + }, + // objectui#13460 — the Integrations page's "Connect an AI agent" card (`AgentConnectSection`). + agentConnect: { + title: '连接 AI 智能体(MCP)', + description: '本系统中的每个应用都是可供智能体使用的工具集。连接 Claude、Cursor、Codex 或任意 MCP 客户端——一次连接即可覆盖你的所有应用(新应用会自动出现)。', + endpoint: 'MCP 端点', + disabledTitle: '本系统未启用 MCP', + disabledBody: '连接智能体需要主动开启。管理员可通过设置 {{flag}} 启用。你仍可以在下方生成密钥并下载技能文件。', + copyMcpUrl: '复制 MCP URL', + apiKey: 'API 密钥', + keyNamePlaceholder: '密钥名称(如 Claude 桌面版)', + generating: '生成中…', + generateKey: '生成密钥', + keyHint: '密钥以你的身份运行(你的权限 + 行级安全)。仅显示一次——请立即复制。可随时在 API 密钥列表中撤销。', + noKeyInResponse: '请求已成功,但响应中没有 API 密钥。传输过程没有出错——请检查 {{route}} 的响应体。', + requestFailed: '请求失败({{status}})', + generateFailed: '生成密钥失败', + skillTitle: '智能体技能(可移植)', + skillHint: '适用于任何支持技能的智能体(Claude、Codex、Gemini、Cursor…)。', + stepsTitle: '三步完成连接', + step1: '添加一个指向上方端点的远程 MCP 服务器。', + step2: '设置请求头 {{header}}(会话 Bearer 令牌不是 API 密钥)。', + step3: '放入 {{file}},让智能体知道如何操作你的数据。', + newKeyTitle: '你的新 API 密钥', + newKeyDescription: '请立即复制——{{notice}}', + newKeyNotice: '出于安全考虑,它不会再次显示。', + sendAs: '请以 {{header}} 发送。它携带你的权限——请像密码一样保管,泄露后请立即撤销。', + }, }, system: { aiApprovals: { @@ -2483,6 +2538,45 @@ const zh = { queueTitle: '队列', queueDescription: '每 5 秒轮询一次。决定会以你的身份记录。', }, + // objectui#13460 — System → Audit Log (`AuditLogPage`). + auditLog: { + title: '审计日志', + subtitle: '只读的系统活动与记录变更,最新的在前。', + openAsList: '以列表打开', + openPresetAsList: '以列表打开 {{label}}', + presets: '预设', + presetOtherSource: '此视图读取的是其他数据源。', + presetFilterUnsupported: '此视图的筛选条件无法在此应用:{{message}}', + action: '操作', + allActions: '全部操作', + object: '对象', + anyObject: '任意对象', + searchObjects: '搜索对象…', + loadingObjects: '正在加载对象…', + noObjects: '未找到对象。', + actor: '操作者', + anyUser: '任意用户', + from: '起始日期', + to: '截止日期', + clearFilters: '清除筛选({{n}})', + loadFailed: '加载审计日志失败', + emptyTitle: '暂无审计记录', + emptyFiltered: '请尝试清除筛选条件或扩大日期范围。', + emptyUnfiltered: '被跟踪的对象发生修改后,事件会显示在这里。', + timestamp: '时间', + page: '第 {{page}} 页', + pageEnd: '第 {{page}} 页 · 已到末尾', + eventFallback: '审计事件', + ipAddress: 'IP 地址', + tenant: '租户', + targetRecord: '目标记录', + userAgent: '用户代理', + changePayload: '变更内容', + before: '变更前', + after: '变更后', + metadata: '元数据', + system: '系统', + }, }, }, auth: { @@ -3512,6 +3606,62 @@ const zh = { desktopBlocked: '已被拦截。请在浏览器设置中允许本站发送通知。', desktopUnsupported: '当前浏览器不支持桌面通知。', }, + // objectui#13460 — the share-link dialog (`@object-ui/components` `ShareDialog`). + shareDialog: { + title: '分享{{label}}', + thisRecord: '此记录', + description: '生成一个任何人(或指定人员)都能用来打开此记录的链接。链接可随时撤销。', + whoCanAccess: '谁可以访问', + audienceLinkOnly: '任何拥有链接的人', + audienceLinkOnlyHelp: '无需登录', + audienceSignedIn: '已登录用户', + audienceSignedInHelp: '需登录后才能查看', + expiresIn: '有效期', + expiry1d: '1 天', + expiry7d: '7 天', + expiry30d: '30 天', + expiry90d: '90 天', + expiryNever: '永不过期', + password: '密码(可选)', + passwordPlaceholder: '打开时需要输入密码', + label: '标签(仅你可见)', + labelPlaceholder: '如:已发给设计评审', + createLink: '创建链接', + existingLinks: '已有链接', + existingLinksCount: '已有链接({{n}})', + noLinks: '暂无分享链接。请在上方创建一个。', + revokedHeading: '已撤销', + revokedBadge: '已撤销', + copy: '复制', + copied: '已复制!', + revoke: '撤销', + loadFailed: '加载分享链接失败', + createFailed: '创建分享链接失败', + revokeFailed: '撤销链接失败', + }, + // objectui#13460 — the public landing of a `/s/:token` share link (`SharedRecordPage`). + sharedRecord: { + signInTitle: '需要登录', + signInBody: '所有者仅将此链接分享给已登录用户。', + signIn: '登录', + passwordTitle: '需要密码', + passwordBody: '所有者为此链接设置了密码。', + passwordLabel: '密码', + passwordPlaceholder: '输入密码', + continue: '继续', + cannotOpen: '无法打开此链接', + passwordUnsendable: '此密码包含无法发送的字符,例如中文字符或表情符号。请让链接所有者设置不含这些字符的密码。', + wrongPassword: '密码错误。', + loadFailedStatus: '加载失败(HTTP {{status}})。', + invalidLink: '此链接无效或已不可用。', + expiredLink: '此链接已过期或已被撤销。', + loadFailed: '加载分享内容失败。', + sharedConversation: '分享的对话', + loadingMessages: '正在加载消息…', + noMessages: '此对话暂无消息。', + sharedObject: '分享的 {{object}}', + redactedFields: '部分字段已被所有者隐藏:{{fields}}', + }, publicForm: { submit: '提交', submitting: '提交中…', From 58af2ad0c63359425bc247ed4c6b398800338cbe Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 04:34:20 +0000 Subject: [PATCH 2/2] feat(console): second-pass i18n for Audit Log, Flow Runs, Agent Connect and share links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of objectui#13460. System -> Audit Log, Developer -> Flow Runs, the "Connect an AI agent" card, ShareDialog and the /s/:token share landing now read console.system.auditLog.*, console.developer.flowRuns.*, console.developer.agentConnect.*, shareDialog.* and sharedRecord.* with en inline defaults. Error states are kept as codes and worded at render. The dev-only PerformanceDashboard moves to apps/console/src/dev/ and the components package's debug/ folder moves to dev/; exports are unchanged. Clause-②: no Metadata-protocol: none Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_018yk9f7S7q4BrEeWZGRcTKF --- .changeset/13460-console-second-pass.md | 12 ++ .../PerformanceDashboard.tsx | 0 apps/console/src/pages/SharedRecordPage.tsx | 99 ++++++--- ...nsolePagesLocale-secondPass-13460.test.tsx | 186 +++++++++++++++++ ...nnectSection.keyNameConfirm-13383.test.tsx | 9 +- .../pages/developer/AgentConnectSection.tsx | 134 ++++++++---- .../src/pages/developer/FlowRunsPage.tsx | 78 ++++--- .../console/src/pages/system/AuditLogPage.tsx | 192 +++++++++++------- .../src/__tests__/displayLocale-9909.test.tsx | 2 +- .../share-dialog-locale-13460.test.tsx | 96 +++++++++ .../src/{debug => dev}/DebugPanel.tsx | 0 .../components/src/{debug => dev}/index.ts | 0 packages/components/src/index.ts | 2 +- packages/components/src/share/ShareDialog.tsx | 123 +++++++---- .../ariaLabelLiteralCensus-12145.test.ts | 2 +- 15 files changed, 721 insertions(+), 214 deletions(-) create mode 100644 .changeset/13460-console-second-pass.md rename apps/console/src/{components => dev}/PerformanceDashboard.tsx (100%) create mode 100644 apps/console/src/pages/consolePagesLocale-secondPass-13460.test.tsx create mode 100644 packages/components/src/__tests__/share-dialog-locale-13460.test.tsx rename packages/components/src/{debug => dev}/DebugPanel.tsx (100%) rename packages/components/src/{debug => dev}/index.ts (100%) diff --git a/.changeset/13460-console-second-pass.md b/.changeset/13460-console-second-pass.md new file mode 100644 index 0000000000..4f12e73ad5 --- /dev/null +++ b/.changeset/13460-console-second-pass.md @@ -0,0 +1,12 @@ +--- +"@object-ui/console": patch +"@object-ui/components": patch +"@object-ui/i18n": patch +--- + +Clause-②: no +Metadata-protocol: none + +More console surfaces now speak the session's language (objectui#13460, second pass). System → Audit Log, Developer → Flow Runs, the Integrations page's "Connect an AI agent" card, the share-link dialog and the public `/s/:token` share landing drew their copy as English literals. They now read `console.system.auditLog.*`, `console.developer.flowRuns.*`, `console.developer.agentConnect.*`, `shareDialog.*` and `sharedRecord.*`, each with an `en` inline default and a row in all ten packs. A sentence that names another control names it by that control's translated label, and a sentence with inline code is one translated string. Error states are kept as codes and worded at render. + +The dev-only surfaces moved under `dev/`: `PerformanceDashboard` to `apps/console/src/dev/`, and the components package's `debug/` folder (the `DebugPanel`) to `dev/`. The package's exports are unchanged. diff --git a/apps/console/src/components/PerformanceDashboard.tsx b/apps/console/src/dev/PerformanceDashboard.tsx similarity index 100% rename from apps/console/src/components/PerformanceDashboard.tsx rename to apps/console/src/dev/PerformanceDashboard.tsx diff --git a/apps/console/src/pages/SharedRecordPage.tsx b/apps/console/src/pages/SharedRecordPage.tsx index 5a0ea29292..15dcead8e5 100644 --- a/apps/console/src/pages/SharedRecordPage.tsx +++ b/apps/console/src/pages/SharedRecordPage.tsx @@ -39,6 +39,7 @@ import { type RawAiMessageRow, } from '@object-ui/app-shell'; import { ChatbotEnhanced } from '@object-ui/plugin-chatbot'; +import { useObjectTranslation } from '@object-ui/i18n'; import { canSendSharePassword, @@ -49,6 +50,20 @@ import { type ResolveErrorBody, } from './shared-record-shape'; +/** + * Why the link did not open. The server's own message is shown as it came; + * every other case is a code the page words in the visitor's language at + * render (objectui#13460), so the resolve callback needs no `t`. + */ +type ShareOpenFailure = + | { kind: 'message'; message: string } + | { kind: 'unsendable-password' } + | { kind: 'wrong-password' } + | { kind: 'status'; status: number } + | { kind: 'invalid' } + | { kind: 'expired' } + | { kind: 'unknown' }; + function resolveServerUrl(): string { const env = (import.meta as any).env ?? {}; const explicit = (env.VITE_SERVER_URL as string | undefined) ?? ''; @@ -58,13 +73,14 @@ function resolveServerUrl(): string { } export default function SharedRecordPage() { + const { t } = useObjectTranslation(); const { token } = useParams<{ token: string }>(); const location = useLocation(); const serverUrl = useMemo(() => resolveServerUrl(), []); const apiBase = `${serverUrl}/api/v1`; const [data, setData] = useState(null); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const [gate, setGate] = useState<'password' | 'sign-in' | null>(null); const [password, setPassword] = useState(''); @@ -80,9 +96,7 @@ export default function SharedRecordPage() { if (pw && !canSendSharePassword(pw)) { // Stays on the prompt: the request is never built, so there is no // answer to show — only why nothing was sent. - setError( - "This password has characters that can't be sent, such as Chinese characters or emoji. Ask the link's owner to set a password without them.", - ); + setError({ kind: 'unsendable-password' }); return; } setLoading(true); @@ -99,27 +113,29 @@ export default function SharedRecordPage() { setGate('sign-in'); } else if (answer) { setGate('password'); - setError(answer === 'wrong-password' ? 'Wrong password.' : null); + setError(answer === 'wrong-password' ? { kind: 'wrong-password' } : null); } else { setGate(null); - setError(body?.error?.message ?? `Failed to load (HTTP ${res.status}).`); + const message = body?.error?.message; + setError(message ? { kind: 'message', message } : { kind: 'status', status: res.status }); } setLoading(false); return; } if (res.status === 404) { - setError('This link is invalid or no longer available.'); + setError({ kind: 'invalid' }); setLoading(false); return; } if (res.status === 410) { - setError('This link has expired or was revoked.'); + setError({ kind: 'expired' }); setLoading(false); return; } if (!res.ok) { const body = await res.json().catch(() => null); - setError(body?.error?.message ?? `Failed to load (HTTP ${res.status}).`); + const message = body?.error?.message; + setError(message ? { kind: 'message', message } : { kind: 'status', status: res.status }); setLoading(false); return; } @@ -129,7 +145,7 @@ export default function SharedRecordPage() { setData(resolved); setGate(null); } catch (e: any) { - setError(e?.message ?? 'Failed to load shared content.'); + setError(e?.message ? { kind: 'message', message: e.message } : { kind: 'unknown' }); } finally { setLoading(false); } @@ -167,11 +183,35 @@ export default function SharedRecordPage() { [messages], ); + const errorText = (() => { + switch (error?.kind) { + case undefined: + return null; + case 'message': + return error.message; + case 'unsendable-password': + return t('sharedRecord.passwordUnsendable', { + defaultValue: + "This password has characters that can't be sent, such as Chinese characters or emoji. Ask the link's owner to set a password without them.", + }); + case 'wrong-password': + return t('sharedRecord.wrongPassword', { defaultValue: 'Wrong password.' }); + case 'status': + return t('sharedRecord.loadFailedStatus', { defaultValue: 'Failed to load (HTTP {{status}}).', status: error.status }); + case 'invalid': + return t('sharedRecord.invalidLink', { defaultValue: 'This link is invalid or no longer available.' }); + case 'expired': + return t('sharedRecord.expiredLink', { defaultValue: 'This link has expired or was revoked.' }); + case 'unknown': + return t('sharedRecord.loadFailed', { defaultValue: 'Failed to load shared content.' }); + } + })(); + if (loading) { return (
- Loading… + {t('common.loading', { defaultValue: 'Loading…' })}
); } @@ -189,12 +229,12 @@ export default function SharedRecordPage() { return (
-

Sign in required

+

{t('sharedRecord.signInTitle', { defaultValue: 'Sign in required' })}

- The owner shared this link with signed-in users only. + {t('sharedRecord.signInBody', { defaultValue: 'The owner shared this link with signed-in users only.' })}

); @@ -204,9 +244,9 @@ export default function SharedRecordPage() { return (
-

Password required

+

{t('sharedRecord.passwordTitle', { defaultValue: 'Password required' })}

- The owner protected this link with a password. + {t('sharedRecord.passwordBody', { defaultValue: 'The owner protected this link with a password.' })}

setPassword(e.target.value)} autoFocus - placeholder="Enter password" + placeholder={t('sharedRecord.passwordPlaceholder', { defaultValue: 'Enter password' })} /> - {error &&

{error}

} + {errorText !== null &&

{errorText}

}
); } - if (error) { + if (errorText !== null) { return (
-

Can't open this link

-

{error}

+

{t('sharedRecord.cannotOpen', { defaultValue: "Can't open this link" })}

+

{errorText}

); } @@ -255,7 +295,7 @@ export default function SharedRecordPage() {

- {conv.title ?? 'Shared conversation'} + {conv.title ?? t('sharedRecord.sharedConversation', { defaultValue: 'Shared conversation' })}

Read-only · Shared via ObjectStack @@ -269,11 +309,11 @@ export default function SharedRecordPage() { {messages == null ? (

- Loading messages… + {t('sharedRecord.loadingMessages', { defaultValue: 'Loading messages…' })}
) : messages.length === 0 ? (

- This conversation has no messages yet. + {t('sharedRecord.noMessages', { defaultValue: 'This conversation has no messages yet.' })}

) : (

- Shared {data.link.object_name} + {t('sharedRecord.sharedObject', { defaultValue: 'Shared {{object}}', object: data.link.object_name })}

@@ -307,7 +347,10 @@ export default function SharedRecordPage() { {data.redactedFields && data.redactedFields.length > 0 && (

- Some fields are hidden by the owner: {data.redactedFields.join(', ')} + {t('sharedRecord.redactedFields', { + defaultValue: 'Some fields are hidden by the owner: {{fields}}', + fields: data.redactedFields.join(', '), + })}

)}
diff --git a/apps/console/src/pages/consolePagesLocale-secondPass-13460.test.tsx b/apps/console/src/pages/consolePagesLocale-secondPass-13460.test.tsx new file mode 100644 index 0000000000..411919a55b --- /dev/null +++ b/apps/console/src/pages/consolePagesLocale-secondPass-13460.test.tsx @@ -0,0 +1,186 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * objectui#13460, second pass — four more console surfaces speak the session's + * language. + * + * System → Audit Log, Developer → Flow Runs, the Integrations page's "Connect + * an AI agent" card and the public `/s/:token` share landing drew their copy as + * English literals. They now read `console.system.auditLog.*`, + * `console.developer.flowRuns.*`, `console.developer.agentConnect.*` and + * `sharedRecord.*`. + * + * What this file pins, through the real pages and a real `I18nProvider`: + * - under zh each page draws its heading and a sample of its copy from the + * zh pack (expectations read from the pack, guarded as real zh rows); + * - a sentence that names another control (Flow Runs' "Click Run Flow", + * "the Test Run panel") names it by that control's zh label; + * - a sentence with an inline `` is ONE translated string, the code + * sits where the zh string put it, and no `{{slot}}` or private-use mark + * is left in the text; + * - control: with no provider at all each page still reads its English. + * + * Every network read answers empty, and a share-link resolve answers 404. + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { cleanup, render, screen } from '@testing-library/react'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { I18nProvider } from '@object-ui/i18n'; +import { builtInLocales } from '@object-ui/i18n/locales'; + +const ADAPTER = vi.hoisted(() => { + // A STABLE singleton: a fresh object per render loops the pages' load effects. + const client = { + meta: { getItems: async () => ({ type: 'flow', items: [{ name: 'welcome_flow', label: 'Welcome' }] }) }, + automation: { execute: async () => ({}), listRuns: async () => ({ runs: [] }) }, + }; + return { + getClient: () => client, + find: async () => ({ data: [], total: 0 }), + findOne: async () => null, + getObjectSchema: async () => undefined, + }; +}); +vi.mock('@object-ui/app-shell', async (importOriginal) => ({ + ...(await importOriginal>()), + useAdapter: () => ADAPTER, + useMetadata: () => ({ objects: [], getTypeStatus: () => 'loaded' }), +})); + +// Imported after the mocks so the pages pick them up. +import { AuditLogPage } from './system/AuditLogPage'; +import { FlowRunsPage } from './developer/FlowRunsPage'; +import { AgentConnectSection } from './developer/AgentConnectSection'; +import SharedRecordPage from './SharedRecordPage'; + +beforeEach(() => { + vi.stubGlobal( + 'fetch', + vi.fn(async (input: RequestInfo | URL) => { + if (String(input).includes('/share-links/')) return new Response('{}', { status: 404 }); + return new Response(JSON.stringify({ records: [], data: [] }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }), + ); +}); +afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); +}); + +const { zh, en } = builtInLocales; +const zhAudit = zh.console.system.auditLog; +const zhFlow = zh.console.developer.flowRuns; +const zhAgent = zh.console.developer.agentConnect; +const zhShared = zh.sharedRecord; + +/** No `{{slot}}` and no private-use slot mark survives into the rendered text. */ +const LEAK = /\{\{|[\u{E000}-\u{F8FF}]/u; + +function page(element: React.ReactElement, language: 'zh' | null, path = '/x', route = '/x') { + const routed = ( + + + + + + ); + return render( + language ? ( + + {routed} + + ) : ( + routed + ), + ); +} + +describe('console pages under zh (objectui#13460, second pass)', () => { + it('guard: the zh values read below are real zh rows, not English copies', () => { + for (const [zhValue, enValue] of [ + [zhAudit.title, en.console.system.auditLog.title], + [zhFlow.title, en.console.developer.flowRuns.title], + [zhAgent.title, en.console.developer.agentConnect.title], + [zhShared.cannotOpen, en.sharedRecord.cannotOpen], + ]) { + expect(zhValue).not.toBe(enValue); + expect(zhValue).toMatch(/\p{Script=Han}/u); + } + }); + + it('Audit Log: the heading, the filter labels and the empty state', async () => { + const { container } = page(, 'zh'); + expect(await screen.findByText(zhAudit.emptyTitle)).toBeTruthy(); + expect(screen.getByRole('heading', { level: 1 }).textContent).toBe(zhAudit.title); + expect(screen.getByText(zhAudit.emptyUnfiltered)).toBeTruthy(); + for (const label of [zhAudit.action, zhAudit.object, zhAudit.actor, zhAudit.from, zhAudit.to]) { + expect(screen.getByText(label, { selector: 'label' })).toBeTruthy(); + } + expect(screen.getByRole('link', { name: new RegExp(zhAudit.openAsList) })).toBeTruthy(); + expect(container.textContent).not.toMatch(LEAK); + }); + + it('Flow Runs: the cards, and the sentences that name another control by its zh label', async () => { + const { container } = page(, 'zh'); + expect(await screen.findByText(zhFlow.recentRuns)).toBeTruthy(); + expect(screen.getByRole('heading', { level: 1 }).textContent).toBe(zhFlow.title); + expect(screen.getByText(zhFlow.testRunTitle)).toBeTruthy(); + expect(screen.getByRole('button', { name: zhFlow.runFlow })).toBeTruthy(); + expect(screen.getByText(zhFlow.noInputs.replace('{{action}}', zhFlow.runFlow))).toBeTruthy(); + expect(await screen.findByText(zhFlow.noRuns.replace('{{panel}}', zhFlow.testRunTitle))).toBeTruthy(); + expect(container.textContent).not.toMatch(LEAK); + }); + + it('Connect an AI agent: the card, and the disabled notice with its in the zh place', async () => { + const { container } = page(, 'zh'); + expect(await screen.findByText(zhAgent.disabledTitle)).toBeTruthy(); + expect(screen.getByText(zhAgent.title)).toBeTruthy(); + expect(screen.getByRole('button', { name: zhAgent.generateKey })).toBeTruthy(); + expect(screen.getByPlaceholderText(zhAgent.keyNamePlaceholder)).toBeTruthy(); + const flag = screen.getByText('OS_MCP_SERVER_ENABLED=true', { selector: 'code' }); + expect(flag.parentElement!.textContent).toBe( + zhAgent.disabledBody.replace('{{flag}}', 'OS_MCP_SERVER_ENABLED=true'), + ); + expect(container.textContent).not.toMatch(LEAK); + }); + + it('share landing: an unknown link says so in zh', async () => { + const { container } = page(, 'zh', '/s/abc', '/s/:token'); + expect(await screen.findByText(zhShared.cannotOpen)).toBeTruthy(); + expect(screen.getByText(zhShared.invalidLink)).toBeTruthy(); + expect(container.textContent).not.toMatch(LEAK); + }); +}); + +describe('control: with no provider each page still reads its English (objectui#13460)', () => { + it('Audit Log', async () => { + page(, null); + expect(await screen.findByText('No audit entries')).toBeTruthy(); + expect(screen.getByRole('heading', { level: 1 }).textContent).toBe('Audit Log'); + }); + + it('Flow Runs', async () => { + page(, null); + expect(await screen.findByText('Recent Runs')).toBeTruthy(); + expect( + screen.getByText('This flow declares no input variables. Click Run Flow to invoke it with no parameters.'), + ).toBeTruthy(); + expect(await screen.findByText('No runs yet. Use the Test Run panel to invoke this flow.')).toBeTruthy(); + }); + + it('Connect an AI agent', async () => { + page(, null); + expect(await screen.findByText('MCP is not enabled for this environment')).toBeTruthy(); + expect(screen.getByText('Connect an AI agent (MCP)')).toBeTruthy(); + }); + + it('share landing', async () => { + page(, null, '/s/abc', '/s/:token'); + expect(await screen.findByText("Can't open this link")).toBeTruthy(); + expect(screen.getByText('This link is invalid or no longer available.')).toBeTruthy(); + }); +}); diff --git a/apps/console/src/pages/developer/AgentConnectSection.keyNameConfirm-13383.test.tsx b/apps/console/src/pages/developer/AgentConnectSection.keyNameConfirm-13383.test.tsx index 95a3804095..a43b7b48c4 100644 --- a/apps/console/src/pages/developer/AgentConnectSection.keyNameConfirm-13383.test.tsx +++ b/apps/console/src/pages/developer/AgentConnectSection.keyNameConfirm-13383.test.tsx @@ -20,7 +20,8 @@ * * `fetch` is stubbed by path; `t` echoes the KEY (with the `name` option * appended when one is passed), as the Connect an Agent suite does, so the - * assertions read which string the dialog asked for and with which name. + * assertions read which string the dialog asked for and with which name. The + * section's own chrome reads `console.developer.agentConnect.*` (objectui#13460). */ import '@testing-library/jest-dom/vitest'; @@ -78,10 +79,10 @@ afterEach(() => { async function renderSettled() { render(); await waitFor(() => expect(fetchMock).toHaveBeenCalled()); - return screen.getByPlaceholderText('Key name (e.g. Claude desktop)'); + return screen.getByPlaceholderText('«console.developer.agentConnect.keyNamePlaceholder»'); } -const generateButton = () => screen.getByRole('button', { name: 'Generate key' }); +const generateButton = () => screen.getByRole('button', { name: '«console.developer.agentConnect.generateKey»' }); describe('AgentConnectSection mints a key only under a chosen, confirmed name (objectui#13383)', () => { it('§1 a blank or whitespace-only name cannot mint', async () => { @@ -127,7 +128,7 @@ describe('AgentConnectSection mints a key only under a chosen, confirmed name (o fireEvent.click(within(dialog).getByRole('button', { name: '«connectAgent.apiKey.mint»' })); await waitFor(() => expect(keyPosts()).toEqual([{ name: 'Claude desktop' }])); - const shown = await screen.findByRole('dialog', { name: /Your new API key/ }); + const shown = await screen.findByRole('dialog', { name: /«console\.developer\.agentConnect\.newKeyTitle»/ }); expect(within(shown).getByText(MINTED_KEY)).toBeInTheDocument(); expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument(); }); diff --git a/apps/console/src/pages/developer/AgentConnectSection.tsx b/apps/console/src/pages/developer/AgentConnectSection.tsx index 618555d4fa..44b202ac60 100644 --- a/apps/console/src/pages/developer/AgentConnectSection.tsx +++ b/apps/console/src/pages/developer/AgentConnectSection.tsx @@ -48,6 +48,7 @@ import { Bot, KeyRound, Copy, Check, Download, ShieldAlert } from 'lucide-react' import { readEnvelopeFailureText } from '@object-ui/app-shell'; import { useObjectTranslation } from '@object-ui/i18n'; +import { SLOT, withSlots } from '../../lib/inlineSlots'; import { renderObjectStackSkill } from './objectstack-skill'; interface GeneratedKey { @@ -67,7 +68,9 @@ function useCopy(): [boolean, (v: string) => void] { return [copied, copy]; } -function InlineCopy({ value, label }: { value: string; label?: string }) { +/** `ariaLabel` is the button's whole translated name (objectui#13460), never glued from parts. */ +function InlineCopy({ value, ariaLabel }: { value: string; ariaLabel: string }) { + const { t } = useObjectTranslation(); const [copied, copy] = useCopy(); return ( ); } @@ -158,7 +161,13 @@ export function AgentConnectSection() { // `??`, not `||`: the helper answers `null` for a body carrying no // prose (never an empty string), and that `null` is exactly the signal // to state this caller's own fallback below. - throw new Error(readEnvelopeFailureText(json) ?? `Request failed (${res.status})`); + throw new Error( + readEnvelopeFailureText(json) + ?? t('console.developer.agentConnect.requestFailed', { + defaultValue: 'Request failed ({{status}})', + status: res.status, + }), + ); } if (!data?.key) { // Reachability of this arm, measured rather than assumed: the mint @@ -173,14 +182,20 @@ export function AgentConnectSection() { // absent from the sentence below -- quoting a 2xx here is what made // the old message read as a transport failure that never happened. throw new Error( - 'The request succeeded but the response carried no API key. ' - + 'Nothing failed in transit — inspect the response body of POST /api/v1/keys.', + t('console.developer.agentConnect.noKeyInResponse', { + defaultValue: + 'The request succeeded but the response carried no API key. Nothing failed in transit — inspect the response body of {{route}}.', + route: 'POST /api/v1/keys', + }), ); } setGenerated(data as GeneratedKey); setDialogOpen(true); } catch (e) { - setError((e as Error)?.message ?? 'Failed to generate key'); + setError( + (e as Error)?.message + ?? t('console.developer.agentConnect.generateFailed', { defaultValue: 'Failed to generate key' }), + ); } finally { setGenerating(false); } @@ -204,25 +219,32 @@ export function AgentConnectSection() { - Connect an AI agent (MCP) + {t('console.developer.agentConnect.title', { defaultValue: 'Connect an AI agent (MCP)' })} - Every app in this environment is an agent-ready toolset. Connect Claude, Cursor, Codex or - any MCP client — one connection covers all your apps (new ones appear automatically). + {t('console.developer.agentConnect.description', { + defaultValue: + 'Every app in this environment is an agent-ready toolset. Connect Claude, Cursor, Codex or any MCP client — one connection covers all your apps (new ones appear automatically).', + })} {/* MCP endpoint */}
-
MCP endpoint
+
{t('console.developer.agentConnect.endpoint', { defaultValue: 'MCP endpoint' })}
{mcpEnabled === false ? ( - MCP is not enabled for this environment + {t('console.developer.agentConnect.disabledTitle', { defaultValue: 'MCP is not enabled for this environment' })} - Connecting agents is opt-in. An admin can enable it by setting{' '} - OS_MCP_SERVER_ENABLED=true. You can still - generate a key and download the skill below. + {withSlots( + t('console.developer.agentConnect.disabledBody', { + defaultValue: + 'Connecting agents is opt-in. An admin can enable it by setting {{flag}}. You can still generate a key and download the skill below.', + flag: SLOT[0], + }), + [OS_MCP_SERVER_ENABLED=true], + )} ) : ( @@ -230,7 +252,10 @@ export function AgentConnectSection() { {effectiveUrl} - +
)}
@@ -238,13 +263,15 @@ export function AgentConnectSection() { {/* Generate key */}
- API key + {t('console.developer.agentConnect.apiKey', { defaultValue: 'API key' })}
) => setKeyName(e.target.value)} - placeholder="Key name (e.g. Claude desktop)" + placeholder={t('console.developer.agentConnect.keyNamePlaceholder', { + defaultValue: 'Key name (e.g. Claude desktop)', + })} className="h-8 max-w-xs text-sm" />

- The key acts as you (your permissions + row-level security). Shown once — copy it - immediately. Revoke anytime from the API Keys list. + {t('console.developer.agentConnect.keyHint', { + defaultValue: + 'The key acts as you (your permissions + row-level security). Shown once — copy it immediately. Revoke anytime from the API Keys list.', + })}

{error &&

{error}

}
{/* Skill */}
-
Agent skill (portable)
+
{t('console.developer.agentConnect.skillTitle', { defaultValue: 'Agent skill (portable)' })}
- Works with any skills-capable agent (Claude, Codex, Gemini, Cursor…). + {t('console.developer.agentConnect.skillHint', { + defaultValue: 'Works with any skills-capable agent (Claude, Codex, Gemini, Cursor…).', + })}
{/* Connect steps */}
-
Connect in 3 steps
+
{t('console.developer.agentConnect.stepsTitle', { defaultValue: 'Connect in 3 steps' })}
    -
  1. Add a remote MCP server pointing at the endpoint above.
  2. +
  3. {t('console.developer.agentConnect.step1', { defaultValue: 'Add a remote MCP server pointing at the endpoint above.' })}
  4. +
  5. + {withSlots( + t('console.developer.agentConnect.step2', { + defaultValue: 'Set the header {{header}} (a session Bearer token is not an API key).', + header: SLOT[0], + }), + [x-api-key: <your key>], + )} +
  6. - Set the header x-api-key: <your key> (a - session Bearer is not an API key). + {withSlots( + t('console.developer.agentConnect.step3', { + defaultValue: 'Drop in {{file}} so the agent knows how to drive your data.', + file: SLOT[0], + }), + [SKILL.md], + )}
  7. -
  8. Drop in SKILL.md so the agent knows how to drive your data.
@@ -322,10 +368,22 @@ export function AgentConnectSection() { - Your new API key + {t('console.developer.agentConnect.newKeyTitle', { defaultValue: 'Your new API key' })} - Copy this now — for your security it will not be shown again. + {withSlots( + t('console.developer.agentConnect.newKeyDescription', { + defaultValue: 'Copy this now — {{notice}}', + notice: SLOT[0], + }), + [ + + {t('console.developer.agentConnect.newKeyNotice', { + defaultValue: 'for your security it will not be shown again.', + })} + , + ], + )}
@@ -341,20 +399,26 @@ export function AgentConnectSection() { onClick={() => generated && copyKey(generated.key)} > {keyCopied ? : } - {keyCopied ? 'Copied' : 'Copy'} + {keyCopied ? t('console.developer.integrations.copied', { defaultValue: 'Copied' }) : t('console.developer.integrations.copy', { defaultValue: 'Copy' })}
- Send it as x-api-key. It carries your - permissions — store it like a password and revoke it if leaked. + {withSlots( + t('console.developer.agentConnect.sendAs', { + defaultValue: + 'Send it as {{header}}. It carries your permissions — store it like a password and revoke it if leaked.', + header: SLOT[0], + }), + [x-api-key], + )}
diff --git a/apps/console/src/pages/developer/FlowRunsPage.tsx b/apps/console/src/pages/developer/FlowRunsPage.tsx index 0b2baeeb1a..4599c67b09 100644 --- a/apps/console/src/pages/developer/FlowRunsPage.tsx +++ b/apps/console/src/pages/developer/FlowRunsPage.tsx @@ -17,7 +17,7 @@ */ import { useEffect, useMemo, useState, useCallback } from 'react'; -import { useDisplayLocale } from '@object-ui/i18n'; +import { useDisplayLocale, useObjectTranslation } from '@object-ui/i18n'; import { ConsolePageHeader, useAdapter, useMetadata, FlowRunner, type ScreenFlowState } from '@object-ui/app-shell'; import { createAuthenticatedFetch } from '@object-ui/auth'; import { @@ -51,6 +51,7 @@ import { SelectTrigger, SelectValue, } from '@object-ui/components'; +import { SLOT, withSlots } from '../../lib/inlineSlots'; import { Play, Loader2, CheckCircle2, XCircle, Clock, AlertCircle, RefreshCw, Workflow, } from 'lucide-react'; @@ -190,6 +191,7 @@ function FlowPicker({ } export function FlowRunsPage() { + const { t } = useObjectTranslation(); const adapter = useAdapter(); const client: any = adapter?.getClient?.(); @@ -251,8 +253,10 @@ export function FlowRunsPage() { subtitle beside the title from `sm` up, in one row across the page above the padded body. Below `sm` the header hides the subtitle. */} } />
@@ -260,12 +264,12 @@ export function FlowRunsPage() {
- Flow - Select a flow definition to test + {t('console.developer.flowRuns.flowTitle', { defaultValue: 'Flow' })} + {t('console.developer.flowRuns.flowDescription', { defaultValue: 'Select a flow definition to test' })}
@@ -275,7 +279,7 @@ export function FlowRunsPage() {

{flowsError}

)} {!flowsLoading && !flowsError && flows.length === 0 && ( -

No flow definitions found.

+

{t('console.developer.flowRuns.noFlows', { defaultValue: 'No flow definitions found.' })}

)} {!flowsLoading && !flowsError && flows.length > 0 && ( void; }) { + const { t } = useObjectTranslation(); const inputVars = useMemo( () => (flow?.variables ?? []).filter((v: FlowVariable) => v?.isInput), [flow], @@ -380,13 +385,16 @@ function FlowTestRunner({ return ( - Test Run - Provide input values and invoke the flow + {t('console.developer.flowRuns.testRunTitle', { defaultValue: 'Test Run' })} + {t('console.developer.flowRuns.testRunDescription', { defaultValue: 'Provide input values and invoke the flow' })} {inputVars.length === 0 && (

- This flow declares no input variables. Click Run to invoke with no parameters. + {t('console.developer.flowRuns.noInputs', { + defaultValue: 'This flow declares no input variables. Click {{action}} to invoke it with no parameters.', + action: t('console.developer.flowRuns.runFlow', { defaultValue: 'Run Flow' }), + })}

)} {inputVars.map(v => ( @@ -425,7 +433,9 @@ function FlowTestRunner({
@@ -433,11 +443,11 @@ function FlowTestRunner({
{error || result?.success === false ? ( - <> Failed + <> {t('console.developer.flowRuns.failed', { defaultValue: 'Failed' })} ) : result?.status === 'paused' ? ( - <> Waiting for input + <> {t('console.developer.flowRuns.waitingForInput', { defaultValue: 'Waiting for input' })} ) : ( - <> Result + <> {t('console.developer.flowRuns.result', { defaultValue: 'Result' })} )} {typeof result?.durationMs === 'number' && ( @@ -451,7 +461,13 @@ function FlowTestRunner({ {result?.status === 'paused' && result?.screen && result?.runId && !screenFlow && (

- This run is paused at screen {result.screen.nodeId}. + {withSlots( + t('console.developer.flowRuns.pausedAtScreen', { + defaultValue: 'This run is paused at screen {{node}}.', + node: SLOT[0], + }), + [{result.screen.nodeId}], + )}

)} @@ -498,6 +514,7 @@ function FlowRunsPanel({ flowName: string; refreshKey?: number; }) { + const { t } = useObjectTranslation(); const displayLocale = useDisplayLocale(); const [runs, setRuns] = useState([]); const [loading, setLoading] = useState(true); @@ -550,12 +567,12 @@ function FlowRunsPanel({
- Recent Runs + {t('console.developer.flowRuns.recentRuns', { defaultValue: 'Recent Runs' })} {flowName}
@@ -572,17 +589,20 @@ function FlowRunsPanel({ )} {!loading && !error && runs.length === 0 && (

- No runs yet. Use the Test Run panel to invoke this flow. + {t('console.developer.flowRuns.noRuns', { + defaultValue: 'No runs yet. Use the {{panel}} panel to invoke this flow.', + panel: t('console.developer.flowRuns.testRunTitle', { defaultValue: 'Test Run' }), + })}

)} {!loading && !error && runs.length > 0 && ( - Run - Status - Started - Duration + {t('console.developer.flowRuns.columnRun', { defaultValue: 'Run' })} + {t('console.developer.flowRuns.columnStatus', { defaultValue: 'Status' })} + {t('console.developer.flowRuns.columnStarted', { defaultValue: 'Started' })} + {t('console.developer.flowRuns.columnDuration', { defaultValue: 'Duration' })} @@ -609,7 +629,9 @@ function FlowRunsPanel({ { if (!o) { setOpenRunId(null); setRunDetail(null); } }}> - Run {openRunId} + + {t('console.developer.flowRuns.runTitle', { defaultValue: 'Run {{id}}', id: openRunId ?? '' })} + {runDetail && ( @@ -653,19 +675,19 @@ function FlowRunsPanel({ {step.input !== undefined && (
-
Input
+
{t('console.developer.flowRuns.stepInput', { defaultValue: 'Input' })}
)} {step.output !== undefined && (
-
Output
+
{t('console.developer.flowRuns.stepOutput', { defaultValue: 'Output' })}
)} {step.error && (
-
Error
+
{t('console.developer.flowRuns.stepError', { defaultValue: 'Error' })}
)} @@ -673,7 +695,7 @@ function FlowRunsPanel({ )) ) : (
-
Run
+
{t('console.developer.flowRuns.columnRun', { defaultValue: 'Run' })}
)} diff --git a/apps/console/src/pages/system/AuditLogPage.tsx b/apps/console/src/pages/system/AuditLogPage.tsx index 3d71fbd174..1cac5a3032 100644 --- a/apps/console/src/pages/system/AuditLogPage.tsx +++ b/apps/console/src/pages/system/AuditLogPage.tsx @@ -37,7 +37,7 @@ import { useCallback, useEffect, useId, useMemo, useState } from 'react'; import { Link, useParams } from 'react-router-dom'; import { mergeFilterNodes, toFilterNodeSafely } from '@object-ui/core'; -import { useDisplayLocale } from '@object-ui/i18n'; +import { useDisplayLocale, useObjectTranslation } from '@object-ui/i18n'; import { Button, Card, @@ -134,9 +134,17 @@ function auditObjectOf(objects: readonly unknown[]): unknown { /** A lowered filter, as `@object-ui/core`'s filter lowering hands it back. */ type WhereNode = Exclude, undefined>; +/** + * Why a preset is offered disabled. The page words it in the viewer's + * language (objectui#13460); `message` is the lowering's own refusal. + */ +type PresetRefusal = + | { kind: 'other-source' } + | { kind: 'filter'; message: string }; + type AuditPreset = | { key: string; label: string; applies: true; node: WhereNode | undefined } - | { key: string; label: string; applies: false; reason: string }; + | { key: string; label: string; applies: false; reason: PresetRefusal }; /** * The presets for `sys_audit_log`'s object document as the console's metadata @@ -160,12 +168,12 @@ function auditPresetsOf(objectDef: unknown): AuditPreset[] { // page's fetch of `sys_audit_log`. const data = view.data as { provider?: unknown; object?: unknown } | undefined; if (data && typeof data === 'object' && (data.provider !== 'object' || data.object !== AUDIT_OBJECT)) { - presets.push({ key, label, applies: false, reason: 'This view reads another data source.' }); + presets.push({ key, label, applies: false, reason: { kind: 'other-source' } }); continue; } const lowered = toFilterNodeSafely(view.filter); if (!lowered.ok) { - presets.push({ key, label, applies: false, reason: `This view's filter cannot be applied here: ${lowered.refusal.message}` }); + presets.push({ key, label, applies: false, reason: { kind: 'filter', message: lowered.refusal.message } }); continue; } presets.push({ key, label, applies: true, node: lowered.node }); @@ -263,8 +271,13 @@ function actorOf(row: AuditRow): ActorFace { /** The actor column's face: the name with the id on hover, else the id, else "System". */ function ActorCell({ face }: { face: ActorFace }) { + const { t } = useObjectTranslation(); if (face.kind === 'system') { - return System; + return ( + + {t('console.system.auditLog.system', { defaultValue: 'System' })} + + ); } if (face.name) return {face.name}; return {truncate(face.id, 18)}; @@ -272,10 +285,11 @@ function ActorCell({ face }: { face: ActorFace }) { /** The drawer's actor row: the name, then the full id (or the principal) to copy. */ function ActorDetail({ face }: { face: ActorFace }) { + const { t } = useObjectTranslation(); if (face.kind === 'system') { return ( <> -
System
+
{t('console.system.auditLog.system', { defaultValue: 'System' })}
{face.principal &&
{face.principal}
} ); @@ -289,6 +303,7 @@ function ActorDetail({ face }: { face: ActorFace }) { } export function AuditLogPage() { + const { t } = useObjectTranslation(); const displayLocale = useDisplayLocale(); // The actor filter's user lookup queries `sys_user` through the console's adapter. const adapter = useAdapter(); @@ -305,6 +320,7 @@ export function AuditLogPage() { const objectsLoading = getTypeStatus?.('object') === 'loading'; const [rows, setRows] = useState([]); const [loading, setLoading] = useState(false); + /** The load's failure: its own message, or '' when it carried none (the page words that one). */ const [error, setError] = useState(null); // Filters. Each filter control is named by its visible label: the label's @@ -383,7 +399,7 @@ export function AuditLogPage() { setRows(items.slice(0, PAGE_SIZE)); } catch (err: any) { console.warn('[AuditLogPage] load failed:', err); - setError(err?.message || 'Failed to load audit log'); + setError(err?.message || ''); setRows([]); setHasMore(false); } finally { @@ -427,16 +443,18 @@ export function AuditLogPage() { one row across the page above the padded body. Below `sm` the header hides the subtitle and keeps the icon, title and Refresh. */} } // The header's own Refresh icon button (objectui#12606). - refresh={{ onRefresh: load, label: 'Refresh', busy: loading }} + refresh={{ onRefresh: load, label: t('common.refresh', { defaultValue: 'Refresh' }), busy: loading }} actions={ } @@ -447,40 +465,56 @@ export function AuditLogPage() { {presets.length > 0 && (
- Presets - {presets.map((p) => p.applies ? ( - - ) : ( - - - - - - - ))} + + {t('console.system.auditLog.presets', { defaultValue: 'Presets' })} + + {presets.map((p) => { + if (p.applies) { + return ( + + ); + } + const reason = p.reason.kind === 'other-source' + ? t('console.system.auditLog.presetOtherSource', { defaultValue: 'This view reads another data source.' }) + : t('console.system.auditLog.presetFilterUnsupported', { + defaultValue: "This view's filter cannot be applied here: {{message}}", + message: p.reason.message, + }); + return ( + + + + + + + ); + })}
)}
- +
- + {/* Picked from the served objects, never typed (objectui#13165): it commits the object's API name, and choosing the chosen object again commits '' for any object. A combobox takes no @@ -498,14 +532,16 @@ export function AuditLogPage() { options={objectOptions} value={objectFilter} onValueChange={(v) => { setObjectFilter(v); setPage(0); }} - placeholder="Any object" - searchPlaceholder="Search objects…" - emptyText={objectsLoading ? 'Loading objects…' : 'No objects found.'} + placeholder={t('console.system.auditLog.anyObject', { defaultValue: 'Any object' })} + searchPlaceholder={t('console.system.auditLog.searchObjects', { defaultValue: 'Search objects…' })} + emptyText={objectsLoading + ? t('console.system.auditLog.loadingObjects', { defaultValue: 'Loading objects…' }) + : t('console.system.auditLog.noObjects', { defaultValue: 'No objects found.' })} className="h-9 w-full" />
- + {/* The same lookup a `sys_user` reference field gets. It commits the chosen user's id, and removing the chip commits null. Its `id` reaches the lookup's trigger button, the control the label @@ -518,35 +554,37 @@ export function AuditLogPage() { field={{ type: 'lookup', name: 'user_id', - label: 'Actor', + label: t('console.system.auditLog.actor', { defaultValue: 'Actor' }), reference: 'sys_user', - placeholder: 'Any user', + placeholder: t('console.system.auditLog.anyUser', { defaultValue: 'Any user' }), }} />
- + { setFromDate(e.target.value); setPage(0); }} className="h-9" />
- + { setToDate(e.target.value); setPage(0); }} className="h-9" />
{activeFilterCount > 0 && (
)}
- {error && ( + {error !== null && ( - {error} + + {error || t('console.system.auditLog.loadFailed', { defaultValue: 'Failed to load audit log' })} + )} @@ -561,11 +599,15 @@ export function AuditLogPage() {
- No audit entries + {t('console.system.auditLog.emptyTitle', { defaultValue: 'No audit entries' })} {activeFilterCount > 0 - ? 'Try clearing filters or widening the date range.' - : 'Events will appear here once tracked objects are modified.'} + ? t('console.system.auditLog.emptyFiltered', { + defaultValue: 'Try clearing filters or widening the date range.', + }) + : t('console.system.auditLog.emptyUnfiltered', { + defaultValue: 'Events will appear here once tracked objects are modified.', + })}
@@ -573,11 +615,11 @@ export function AuditLogPage() {
- Timestamp - Action - Object - Record - Actor + {t('console.system.auditLog.timestamp', { defaultValue: 'Timestamp' })} + {t('console.system.auditLog.action', { defaultValue: 'Action' })} + {t('console.system.auditLog.object', { defaultValue: 'Object' })} + {t('common.record', { defaultValue: 'Record' })} + {t('console.system.auditLog.actor', { defaultValue: 'Actor' })} IP @@ -608,13 +650,17 @@ export function AuditLogPage() { {/* Pagination */} {(rows.length > 0 || page > 0) && (
- Page {page + 1}{hasMore ? '' : ' · end'} + + {hasMore + ? t('console.system.auditLog.page', { defaultValue: 'Page {{page}}', page: page + 1 }) + : t('console.system.auditLog.pageEnd', { defaultValue: 'Page {{page}} · end', page: page + 1 })} +
@@ -628,7 +674,7 @@ export function AuditLogPage() { {selected?.action || '—'} - {selected?.object_name || 'Audit Event'} + {selected?.object_name || t('console.system.auditLog.eventFallback', { defaultValue: 'Audit Event' })} {selected?.id ? {selected.id} : null} @@ -639,30 +685,30 @@ export function AuditLogPage() {
-
Timestamp
+
{t('console.system.auditLog.timestamp', { defaultValue: 'Timestamp' })}
{formatDate(selected.created_at, displayLocale)}
-
IP Address
+
{t('console.system.auditLog.ipAddress', { defaultValue: 'IP Address' })}
{selected.ip_address || '—'}
-
Actor
+
{t('console.system.auditLog.actor', { defaultValue: 'Actor' })}
-
Tenant
+
{t('console.system.auditLog.tenant', { defaultValue: 'Tenant' })}
{selected.tenant_id || '—'}
-
Target record
+
{t('console.system.auditLog.targetRecord', { defaultValue: 'Target record' })}
{selected.object_name || '—'} / {selected.record_id || '—'}
{selected.user_agent && (
-
User Agent
+
{t('console.system.auditLog.userAgent', { defaultValue: 'User Agent' })}
{selected.user_agent}
)} @@ -672,10 +718,10 @@ export function AuditLogPage() { <>
-
Change payload
+
{t('console.system.auditLog.changePayload', { defaultValue: 'Change payload' })}
{selected.old_value && (
-
Before
+
{t('console.system.auditLog.before', { defaultValue: 'Before' })}
                           {tryPrettyJson(selected.old_value)}
                         
@@ -683,7 +729,7 @@ export function AuditLogPage() { )} {selected.new_value && (
-
After
+
{t('console.system.auditLog.after', { defaultValue: 'After' })}
                           {tryPrettyJson(selected.new_value)}
                         
@@ -697,7 +743,7 @@ export function AuditLogPage() { <>
-
Metadata
+
{t('console.system.auditLog.metadata', { defaultValue: 'Metadata' })}
                       {tryPrettyJson(selected.metadata)}
                     
diff --git a/packages/components/src/__tests__/displayLocale-9909.test.tsx b/packages/components/src/__tests__/displayLocale-9909.test.tsx index 548827bc56..6e30531220 100644 --- a/packages/components/src/__tests__/displayLocale-9909.test.tsx +++ b/packages/components/src/__tests__/displayLocale-9909.test.tsx @@ -26,7 +26,7 @@ import * as React from 'react'; import { I18nProvider, LocalizationProvider } from '@object-ui/i18n'; import { DebugCollector } from '@object-ui/core'; import { isMachineLocale, recordLocaleArguments } from '@object-ui/test-support'; -import { DebugPanel } from '../debug/DebugPanel'; +import { DebugPanel } from '../dev/DebugPanel'; afterEach(() => { cleanup(); diff --git a/packages/components/src/__tests__/share-dialog-locale-13460.test.tsx b/packages/components/src/__tests__/share-dialog-locale-13460.test.tsx new file mode 100644 index 0000000000..b4708bea84 --- /dev/null +++ b/packages/components/src/__tests__/share-dialog-locale-13460.test.tsx @@ -0,0 +1,96 @@ +/** + * objectui#13460 — the share-link dialog speaks the session's language. + * + * `ShareDialog` drew its whole chrome as English literals: the title, the + * audience and expiry choices, the form labels and placeholders, the empty + * list and the footer. It now reads `shareDialog.*` (and `common.done`). + * + * What this file pins, through the real dialog and a real `I18nProvider`: + * - under zh the title (with the "this record" fallback), the description, + * the audience and expiry triggers, the labels, the placeholders, the + * create button and the empty list read the zh pack (guarded as real zh + * rows, not English copies); + * - a caller's `recordLabel` lands in the zh title's hole; + * - control: with no provider the dialog still reads its English. + * + * The link list answers empty. + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { cleanup, render, screen } from '@testing-library/react'; +import { I18nProvider } from '@object-ui/i18n'; +import { builtInLocales } from '@object-ui/i18n/locales'; +import { ShareDialog } from '../share/ShareDialog'; + +beforeEach(() => { + vi.stubGlobal( + 'fetch', + vi.fn(async () => new Response(JSON.stringify({ links: [] }), { status: 200, headers: { 'Content-Type': 'application/json' } })), + ); +}); +afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); +}); + +const { zh, en } = builtInLocales; +const zhShare = zh.shareDialog; + +function dialog(language: 'zh' | null, recordLabel?: string) { + const node = ( + {}} objectName="ai_conversations" recordId="r1" recordLabel={recordLabel} apiBase="/api" /> + ); + return render( + language ? ( + + {node} + + ) : ( + node + ), + ); +} + +describe('ShareDialog under zh (objectui#13460)', () => { + it('guard: the zh values read below are real zh rows, not English copies', () => { + for (const [zhValue, enValue] of [ + [zhShare.thisRecord, en.shareDialog.thisRecord], + [zhShare.noLinks, en.shareDialog.noLinks], + [zhShare.audienceLinkOnly, en.shareDialog.audienceLinkOnly], + ]) { + expect(zhValue).not.toBe(enValue); + expect(zhValue).toMatch(/\p{Script=Han}/u); + } + }); + + it('draws its chrome from the zh pack', async () => { + dialog('zh'); + expect(await screen.findByText(zhShare.noLinks)).toBeTruthy(); + expect(screen.getByRole('heading', { name: zhShare.title.replace('{{label}}', zhShare.thisRecord) })).toBeTruthy(); + expect(screen.getByText(zhShare.description)).toBeTruthy(); + expect(screen.getByTestId('share-audience').textContent).toContain(zhShare.audienceLinkOnly); + expect(screen.getByTestId('share-expiry').textContent).toBe(zhShare.expiry7d); + for (const label of [zhShare.whoCanAccess, zhShare.expiresIn, zhShare.password, zhShare.label]) { + expect(screen.getByText(label)).toBeTruthy(); + } + expect(screen.getByPlaceholderText(zhShare.passwordPlaceholder)).toBeTruthy(); + expect(screen.getByPlaceholderText(zhShare.labelPlaceholder)).toBeTruthy(); + expect(screen.getByTestId('share-create').textContent).toBe(zhShare.createLink); + expect(screen.getByRole('button', { name: zh.common.done })).toBeTruthy(); + }); + + it("puts the caller's record label in the zh title's hole", async () => { + dialog('zh', 'Q3 plan'); + expect(await screen.findByRole('heading', { name: zhShare.title.replace('{{label}}', 'Q3 plan') })).toBeTruthy(); + }); +}); + +describe('control: with no provider the dialog still reads its English (objectui#13460)', () => { + it('reads the en values', async () => { + dialog(null); + expect(await screen.findByText('No share links yet. Create one above.')).toBeTruthy(); + expect(screen.getByRole('heading', { name: 'Share this record' })).toBeTruthy(); + expect(screen.getByTestId('share-expiry').textContent).toBe('7 days'); + expect(screen.getByTestId('share-create').textContent).toBe('Create link'); + }); +}); diff --git a/packages/components/src/debug/DebugPanel.tsx b/packages/components/src/dev/DebugPanel.tsx similarity index 100% rename from packages/components/src/debug/DebugPanel.tsx rename to packages/components/src/dev/DebugPanel.tsx diff --git a/packages/components/src/debug/index.ts b/packages/components/src/dev/index.ts similarity index 100% rename from packages/components/src/debug/index.ts rename to packages/components/src/dev/index.ts diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index a8fd48a095..7e224025b6 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -164,7 +164,7 @@ export function initializeComponents() { } // Debug panel (tree-shakeable — only included when imported) -export * from './debug'; +export * from './dev'; // Platform share-link dialog (Notion / Figma-style "anyone with the link") export * from './share'; diff --git a/packages/components/src/share/ShareDialog.tsx b/packages/components/src/share/ShareDialog.tsx index 75a1cfcfa9..8935d69470 100644 --- a/packages/components/src/share/ShareDialog.tsx +++ b/packages/components/src/share/ShareDialog.tsx @@ -18,6 +18,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { Copy, Link2, Loader2, ShieldOff, Trash2 } from 'lucide-react'; +import { useObjectTranslation } from '@object-ui/i18n'; import type { ShareLink as SpecShareLink, ShareLinkAudience, @@ -115,23 +116,12 @@ const DEFAULT_DRAFT: CreateLinkDraft = { label: '', }; -const EXPIRY_OPTIONS: { label: string; value: number | null }[] = [ - { label: '1 day', value: 1 }, - { label: '7 days', value: 7 }, - { label: '30 days', value: 30 }, - { label: '90 days', value: 90 }, - { label: 'Never', value: null }, -]; - -// Audience values match the framework's ShareLinkAudience contract -// (@objectstack/spec). Only the two that need no extra input are offered; -// 'email' (allowlist) / 'public' require UI not yet built. The target -// object's `publicSharing.allowedAudiences` further constrains this — the -// server returns 422 AUDIENCE_NOT_ALLOWED if a value is not permitted. -const AUDIENCE_OPTIONS: { value: ShareLinkAudience; label: string; help: string }[] = [ - { value: 'link_only', label: 'Anyone with the link', help: 'No sign-in required' }, - { value: 'signed_in', label: 'Signed-in users', help: 'Must be logged in to view' }, -]; +/** + * A failure the dialog shows: the server's or the transport's own message, or, + * when it carried none, which step failed, worded in the viewer's language at + * render (objectui#13460). Kept as data so the fetch callbacks need no `t`. + */ +type ShareDialogFailure = { message: string | undefined; step: 'load' | 'create' | 'revoke' }; function buildPublicUrl(base: string | undefined, token: string): string { const root = @@ -177,10 +167,36 @@ export function ShareDialog({ publicBaseUrl, fetchHeaders, }: ShareDialogProps) { + const { t } = useObjectTranslation(); + // Built per render so each choice is a literal `t()` call the i18n gates can read. + const expiryOptions: { label: string; value: number | null }[] = [ + { label: t('shareDialog.expiry1d', { defaultValue: '1 day' }), value: 1 }, + { label: t('shareDialog.expiry7d', { defaultValue: '7 days' }), value: 7 }, + { label: t('shareDialog.expiry30d', { defaultValue: '30 days' }), value: 30 }, + { label: t('shareDialog.expiry90d', { defaultValue: '90 days' }), value: 90 }, + { label: t('shareDialog.expiryNever', { defaultValue: 'Never' }), value: null }, + ]; + // Audience values match the framework's ShareLinkAudience contract + // (@objectstack/spec). Only the two that need no extra input are offered; + // 'email' (allowlist) / 'public' require UI not yet built. The target + // object's `publicSharing.allowedAudiences` further constrains this — the + // server returns 422 AUDIENCE_NOT_ALLOWED if a value is not permitted. + const audienceOptions: { value: ShareLinkAudience; label: string; help: string }[] = [ + { + value: 'link_only', + label: t('shareDialog.audienceLinkOnly', { defaultValue: 'Anyone with the link' }), + help: t('shareDialog.audienceLinkOnlyHelp', { defaultValue: 'No sign-in required' }), + }, + { + value: 'signed_in', + label: t('shareDialog.audienceSignedIn', { defaultValue: 'Signed-in users' }), + help: t('shareDialog.audienceSignedInHelp', { defaultValue: 'Must be logged in to view' }), + }, + ]; const [links, setLinks] = useState([]); const [loading, setLoading] = useState(false); const [creating, setCreating] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const [draft, setDraft] = useState(DEFAULT_DRAFT); const [justCopied, setJustCopied] = useState(null); @@ -209,7 +225,7 @@ export function ShareDialog({ const body = (await res.json()) as { links?: ShareLink[]; data?: ShareLink[] }; setLinks(body.links ?? body.data ?? []); } catch (err: any) { - setError(err?.message ?? 'Failed to load share links'); + setError({ message: err?.message, step: 'load' }); } finally { setLoading(false); } @@ -261,7 +277,7 @@ export function ShareDialog({ } setDraft(DEFAULT_DRAFT); } catch (err: any) { - setError(err?.message ?? 'Failed to create share link'); + setError({ message: err?.message, step: 'create' }); } finally { setCreating(false); } @@ -284,7 +300,7 @@ export function ShareDialog({ ), ); } catch (err: any) { - setError(err?.message ?? 'Failed to revoke link'); + setError({ message: err?.message, step: 'revoke' }); } }, [apiBase, headers], @@ -304,6 +320,15 @@ export function ShareDialog({ const activeLinks = links.filter((l) => !l.revoked_at); const revokedLinks = links.filter((l) => l.revoked_at); + const errorText = + error === null + ? null + : error.message ?? + (error.step === 'load' + ? t('shareDialog.loadFailed', { defaultValue: 'Failed to load share links' }) + : error.step === 'create' + ? t('shareDialog.createFailed', { defaultValue: 'Failed to create share link' }) + : t('shareDialog.revokeFailed', { defaultValue: 'Failed to revoke link' })); return ( @@ -311,20 +336,25 @@ export function ShareDialog({ - Share {recordLabel ?? 'this record'} + {t('shareDialog.title', { + defaultValue: 'Share {{label}}', + label: recordLabel ?? t('shareDialog.thisRecord', { defaultValue: 'this record' }), + })} - Generate a link anyone (or selected people) can use to open this record. - Links can be revoked at any time. + {t('shareDialog.description', { + defaultValue: + 'Generate a link anyone (or selected people) can use to open this record. Links can be revoked at any time.', + })} - {error && ( + {errorText !== null && (
- {error} + {errorText}
)} @@ -332,7 +362,7 @@ export function ShareDialog({
- +
- + setDraft((d) => ({ ...d, password: e.target.value }))} - placeholder="Require a password to open" + placeholder={t('shareDialog.passwordPlaceholder', { defaultValue: 'Require a password to open' })} className="h-8 text-xs" data-testid="share-password" />
- + setDraft((d) => ({ ...d, label: e.target.value }))} - placeholder="e.g. Sent to design review" + placeholder={t('shareDialog.labelPlaceholder', { defaultValue: 'e.g. Sent to design review' })} className="h-8 text-xs" data-testid="share-label" /> @@ -415,7 +447,7 @@ export function ShareDialog({ ) : ( )} - Create link + {t('shareDialog.createLink', { defaultValue: 'Create link' })}
@@ -424,13 +456,15 @@ export function ShareDialog({

- Existing links{activeLinks.length ? ` (${activeLinks.length})` : ''} + {activeLinks.length + ? t('shareDialog.existingLinksCount', { defaultValue: 'Existing links ({{n}})', n: activeLinks.length }) + : t('shareDialog.existingLinks', { defaultValue: 'Existing links' })}

{loading && }
{!loading && activeLinks.length === 0 && revokedLinks.length === 0 && (

- No share links yet. Create one above. + {t('shareDialog.noLinks', { defaultValue: 'No share links yet. Create one above.' })}

)}
    @@ -446,7 +480,7 @@ export function ShareDialog({ ))} {revokedLinks.length > 0 && (
  • - Revoked + {t('shareDialog.revokedHeading', { defaultValue: 'Revoked' })}
  • )} {revokedLinks.map((link) => ( @@ -465,7 +499,7 @@ export function ShareDialog({ @@ -483,6 +517,7 @@ interface LinkRowProps { } function LinkRow({ link, copied, onCopy, onRevoke, publicBaseUrl, disabled }: LinkRowProps) { + const { t } = useObjectTranslation(); const url = buildPublicUrl(publicBaseUrl, link.token); return (
  • - {copied ? 'Copied!' : 'Copy'} + {copied + ? t('shareDialog.copied', { defaultValue: 'Copied!' }) + : t('shareDialog.copy', { defaultValue: 'Copy' })} @@ -533,7 +570,7 @@ function LinkRow({ link, copied, onCopy, onRevoke, publicBaseUrl, disabled }: Li {disabled && ( - revoked + {t('shareDialog.revokedBadge', { defaultValue: 'revoked' })} )}
  • diff --git a/packages/i18n/src/__tests__/ariaLabelLiteralCensus-12145.test.ts b/packages/i18n/src/__tests__/ariaLabelLiteralCensus-12145.test.ts index dc2317d2c8..dc1a925e71 100644 --- a/packages/i18n/src/__tests__/ariaLabelLiteralCensus-12145.test.ts +++ b/packages/i18n/src/__tests__/ariaLabelLiteralCensus-12145.test.ts @@ -182,7 +182,7 @@ interface Declared { */ const EXEMPT: Declared[] = [ { - file: 'packages/components/src/debug/DebugPanel.tsx', + file: 'packages/components/src/dev/DebugPanel.tsx', names: ['Developer Debug Panel', 'Close debug panel'], verdict: 'exempt', reason: