From 96bb1cc9b777a690b9930e1c68db6910ce858bf4 Mon Sep 17 00:00:00 2001 From: Aria Zhao Date: Fri, 10 Jul 2026 21:33:06 +0800 Subject: [PATCH] fix(web): localize built-in skill edit as a user-override flow (#4793) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * i18n(web): add built-in skill user-override strings Add skillsOverrideCreate / skillsOverrideSave / skillsBuiltInOverrideWarning to the typed dictionary and every locale, ahead of wiring the built-in skill edit flow through them. * fix(web): localize the built-in skill override confirmation The built-in-skill edit warning was hardcoded English, so it stayed English under non-English locales even though the rest of the dialog followed the active language. Read it from the i18n dictionary instead. * fix(web): label built-in skill editing as create/save user override Editing a built-in skill silently produced a user override, but the affordances read "Edit" / "Save" — so the built-in → user type change was a surprise. The edit action, confirmation, and form heading now read "Create user override" and the submit reads "Save as user override". * test(web): lock built-in skill override wording across locales Cover the regression gap raised on #4793: assert the built-in skill edit flow renders the user-override i18n keys end to end — the edit tooltip, the confirmation warning, the confirm button, the form heading, and the submit button — in the default en locale and a translated zh-CN path, and that a user skill keeps the plain Edit/Save affordance with no override warning. Assertions compare against the locale dictionaries rather than hardcoded literals, so a copy tweak in a locale file stays green while reverting to the old Edit/Save keys (or a hardcoded-English warning) goes red. --- apps/web/src/components/SkillsSection.tsx | 32 +++++-- apps/web/src/i18n/locales/ar.ts | 3 + apps/web/src/i18n/locales/de.ts | 3 + apps/web/src/i18n/locales/en.ts | 3 + apps/web/src/i18n/locales/es-ES.ts | 3 + apps/web/src/i18n/locales/fa.ts | 3 + apps/web/src/i18n/locales/fr.ts | 3 + apps/web/src/i18n/locales/hu.ts | 3 + apps/web/src/i18n/locales/id.ts | 3 + apps/web/src/i18n/locales/it.ts | 3 + apps/web/src/i18n/locales/ja.ts | 3 + apps/web/src/i18n/locales/ko.ts | 3 + apps/web/src/i18n/locales/pl.ts | 3 + apps/web/src/i18n/locales/pt-BR.ts | 3 + apps/web/src/i18n/locales/ru.ts | 3 + apps/web/src/i18n/locales/th.ts | 3 + apps/web/src/i18n/locales/tr.ts | 3 + apps/web/src/i18n/locales/uk.ts | 3 + apps/web/src/i18n/locales/zh-CN.ts | 4 + apps/web/src/i18n/locales/zh-TW.ts | 4 + apps/web/src/i18n/types.ts | 3 + .../tests/components/SkillsSection.test.tsx | 88 +++++++++++++++++++ 22 files changed, 173 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/SkillsSection.tsx b/apps/web/src/components/SkillsSection.tsx index 370ee9e631..2077bd97ca 100644 --- a/apps/web/src/components/SkillsSection.tsx +++ b/apps/web/src/components/SkillsSection.tsx @@ -580,6 +580,10 @@ function SkillRow({ const summaryName = localizeSkillName(locale, skill) || skill.id; const summaryDescription = localizeSkillDescription(locale, skill); const canDelete = skill.source === 'user'; + // Editing a built-in skill does not modify it in place — it writes a + // user-owned shadow copy. Frame the affordance as creating a user override + // so the built-in → user transition is not a surprise. + const isBuiltIn = skill.source !== 'user'; return (
@@ -689,11 +697,7 @@ function SkillRow({ role="alert" data-testid="skills-edit-builtin-warning" > -

- Editing this built-in skill creates a user override. The built-in - entry will be hidden from the list until you delete the override. - Continue? -

+

{t('settings.skillsBuiltInOverrideWarning')}

@@ -759,13 +763,18 @@ function SkillRow({ {editing && draft ? ( @@ -782,6 +791,8 @@ interface SkillDraftFormProps { error: string | null; saving: boolean; isEdit: boolean; + /** Editing a built-in skill: the submit reads "Save as user override". */ + isBuiltInOverride?: boolean; onCancel: () => void; onSubmit: () => void; } @@ -794,6 +805,7 @@ function SkillDraftForm({ error, saving, isEdit, + isBuiltInOverride = false, onCancel, onSubmit, }: SkillDraftFormProps) { @@ -876,7 +888,9 @@ function SkillDraftForm({ {saving ? t('settings.skillsSaving') : isEdit - ? t('settings.skillsSave') + ? isBuiltInOverride + ? t('settings.skillsOverrideSave') + : t('settings.skillsSave') : t('settings.skillsCreate')} diff --git a/apps/web/src/i18n/locales/ar.ts b/apps/web/src/i18n/locales/ar.ts index f327aa9cfa..74e08d5961 100644 --- a/apps/web/src/i18n/locales/ar.ts +++ b/apps/web/src/i18n/locales/ar.ts @@ -3339,6 +3339,9 @@ export const ar: Dict = { 'settings.skillsCreate': 'إنشاء', 'settings.skillsSave': 'حفظ', 'settings.skillsSaving': 'جاري الحفظ…', + 'settings.skillsOverrideCreate': 'إنشاء تجاوز المستخدم', + 'settings.skillsOverrideSave': 'حفظ كتجاوز المستخدم', + 'settings.skillsBuiltInOverrideWarning': 'يؤدي تعديل هذه المهارة المدمجة إلى إنشاء تجاوز خاص بالمستخدم. سيتم إخفاء العنصر المدمج من القائمة حتى تحذف التجاوز. هل تريد المتابعة؟', 'settings.skillsFiles': 'الملفات', 'settings.skillsNoFiles': 'لا توجد ملفات في مجلد هذه المهارة.', 'settings.skillsNameRequired': 'اسم المهارة مطلوب.', diff --git a/apps/web/src/i18n/locales/de.ts b/apps/web/src/i18n/locales/de.ts index 048b4f0ea6..ccd0553ed4 100644 --- a/apps/web/src/i18n/locales/de.ts +++ b/apps/web/src/i18n/locales/de.ts @@ -3339,6 +3339,9 @@ export const de: Dict = { 'settings.skillsCreate': 'Erstellen', 'settings.skillsSave': 'Speichern', 'settings.skillsSaving': 'Speichern…', + 'settings.skillsOverrideCreate': 'Benutzer-Override erstellen', + 'settings.skillsOverrideSave': 'Als Benutzer-Override speichern', + 'settings.skillsBuiltInOverrideWarning': 'Das Bearbeiten dieser integrierten Fähigkeit erstellt einen Benutzer-Override. Der integrierte Eintrag wird aus der Liste ausgeblendet, bis du den Override löschst. Fortfahren?', 'settings.skillsFiles': 'Dateien', 'settings.skillsNoFiles': 'Keine Dateien in diesem Skill-Ordner.', 'settings.skillsNameRequired': 'Skill-Name ist erforderlich.', diff --git a/apps/web/src/i18n/locales/en.ts b/apps/web/src/i18n/locales/en.ts index 5bb27731b4..35f39c7a7e 100644 --- a/apps/web/src/i18n/locales/en.ts +++ b/apps/web/src/i18n/locales/en.ts @@ -3377,6 +3377,9 @@ export const en: Dict = { 'settings.skillsCreate': 'Create', 'settings.skillsSave': 'Save', 'settings.skillsSaving': 'Saving…', + 'settings.skillsOverrideCreate': 'Create user override', + 'settings.skillsOverrideSave': 'Save as user override', + 'settings.skillsBuiltInOverrideWarning': 'Editing this built-in skill creates a user override. The built-in entry will be hidden from the list until you delete the override. Continue?', 'settings.skillsFiles': 'Files', 'settings.skillsNoFiles': 'No files in this skill folder.', 'settings.skillsNameRequired': 'Skill name is required.', diff --git a/apps/web/src/i18n/locales/es-ES.ts b/apps/web/src/i18n/locales/es-ES.ts index c90189fa50..fe397aae96 100644 --- a/apps/web/src/i18n/locales/es-ES.ts +++ b/apps/web/src/i18n/locales/es-ES.ts @@ -3339,6 +3339,9 @@ export const esES: Dict = { 'settings.skillsCreate': 'Crear', 'settings.skillsSave': 'Guardar', 'settings.skillsSaving': 'Guardando…', + 'settings.skillsOverrideCreate': 'Crear anulación de usuario', + 'settings.skillsOverrideSave': 'Guardar como anulación de usuario', + 'settings.skillsBuiltInOverrideWarning': 'Editar esta habilidad integrada crea una anulación de usuario. La entrada integrada se ocultará de la lista hasta que elimines la anulación. ¿Continuar?', 'settings.skillsFiles': 'Archivos', 'settings.skillsNoFiles': 'No hay archivos en esta carpeta de habilidad.', 'settings.skillsNameRequired': 'El nombre de la habilidad es obligatorio.', diff --git a/apps/web/src/i18n/locales/fa.ts b/apps/web/src/i18n/locales/fa.ts index b237b2fd64..b29a8aa197 100644 --- a/apps/web/src/i18n/locales/fa.ts +++ b/apps/web/src/i18n/locales/fa.ts @@ -3339,6 +3339,9 @@ export const fa: Dict = { 'settings.skillsCreate': 'ایجاد', 'settings.skillsSave': 'ذخیره', 'settings.skillsSaving': 'در حال ذخیره…', + 'settings.skillsOverrideCreate': 'ایجاد بازنویسی کاربر', + 'settings.skillsOverrideSave': 'ذخیره به‌عنوان بازنویسی کاربر', + 'settings.skillsBuiltInOverrideWarning': 'ویرایش این مهارت داخلی یک بازنویسی کاربر ایجاد می‌کند. مورد داخلی تا زمانی که بازنویسی را حذف کنید از فهرست پنهان می‌شود. ادامه می‌دهید؟', 'settings.skillsFiles': 'فایل‌ها', 'settings.skillsNoFiles': 'هیچ فایلی در این پوشه مهارت نیست.', 'settings.skillsNameRequired': 'نام مهارت الزامی است.', diff --git a/apps/web/src/i18n/locales/fr.ts b/apps/web/src/i18n/locales/fr.ts index 7319779ee1..9329288f00 100644 --- a/apps/web/src/i18n/locales/fr.ts +++ b/apps/web/src/i18n/locales/fr.ts @@ -3339,6 +3339,9 @@ export const fr: Dict = { 'settings.skillsCreate': 'Créer', 'settings.skillsSave': 'Enregistrer', 'settings.skillsSaving': 'Enregistrement…', + 'settings.skillsOverrideCreate': 'Créer une surcharge utilisateur', + 'settings.skillsOverrideSave': 'Enregistrer comme surcharge utilisateur', + 'settings.skillsBuiltInOverrideWarning': 'Modifier cette compétence intégrée crée une surcharge utilisateur. L\'entrée intégrée sera masquée de la liste jusqu\'à ce que vous supprimiez la surcharge. Continuer ?', 'settings.skillsFiles': 'Fichiers', 'settings.skillsNoFiles': 'Aucun fichier dans ce dossier de Skill.', 'settings.skillsNameRequired': 'Le nom du Skill est requis.', diff --git a/apps/web/src/i18n/locales/hu.ts b/apps/web/src/i18n/locales/hu.ts index 44fdb902aa..7ff5194c55 100644 --- a/apps/web/src/i18n/locales/hu.ts +++ b/apps/web/src/i18n/locales/hu.ts @@ -3339,6 +3339,9 @@ export const hu: Dict = { 'settings.skillsCreate': 'Létrehozás', 'settings.skillsSave': 'Mentés', 'settings.skillsSaving': 'Mentés…', + 'settings.skillsOverrideCreate': 'Felhasználói felülbírálás létrehozása', + 'settings.skillsOverrideSave': 'Mentés felhasználói felülbírálásként', + 'settings.skillsBuiltInOverrideWarning': 'A beépített készség szerkesztése felhasználói felülbírálást hoz létre. A beépített bejegyzés rejtve marad a listából, amíg nem törli a felülbírálást. Folytatja?', 'settings.skillsFiles': 'Fájlok', 'settings.skillsNoFiles': 'Nincs fájl ebben a készségmappában.', 'settings.skillsNameRequired': 'A készség neve kötelező.', diff --git a/apps/web/src/i18n/locales/id.ts b/apps/web/src/i18n/locales/id.ts index 329d1089cd..1ebc2cfd84 100644 --- a/apps/web/src/i18n/locales/id.ts +++ b/apps/web/src/i18n/locales/id.ts @@ -3339,6 +3339,9 @@ export const id: Dict = { 'settings.skillsCreate': 'Buat', 'settings.skillsSave': 'Simpan', 'settings.skillsSaving': 'Menyimpan…', + 'settings.skillsOverrideCreate': 'Buat penggantian pengguna', + 'settings.skillsOverrideSave': 'Simpan sebagai penggantian pengguna', + 'settings.skillsBuiltInOverrideWarning': 'Mengedit keterampilan bawaan ini akan membuat penggantian pengguna. Entri bawaan akan disembunyikan dari daftar hingga Anda menghapus penggantian. Lanjutkan?', 'settings.skillsFiles': 'Berkas', 'settings.skillsNoFiles': 'Tidak ada berkas dalam folder keterampilan ini.', 'settings.skillsNameRequired': 'Nama keterampilan wajib diisi.', diff --git a/apps/web/src/i18n/locales/it.ts b/apps/web/src/i18n/locales/it.ts index 08557621f0..cf3ea28804 100644 --- a/apps/web/src/i18n/locales/it.ts +++ b/apps/web/src/i18n/locales/it.ts @@ -3339,6 +3339,9 @@ export const it: Dict = { 'settings.skillsCreate': 'Crea', 'settings.skillsSave': 'Salva', 'settings.skillsSaving': 'Salvataggio…', + 'settings.skillsOverrideCreate': 'Crea override utente', + 'settings.skillsOverrideSave': 'Salva come override utente', + 'settings.skillsBuiltInOverrideWarning': 'La modifica di questa competenza integrata crea un override utente. La voce integrata verrà nascosta dall\'elenco finché non elimini l\'override. Continuare?', 'settings.skillsFiles': 'File', 'settings.skillsNoFiles': 'Nessun file in questa cartella di competenza.', 'settings.skillsNameRequired': 'Il nome dello skill è obbligatorio.', diff --git a/apps/web/src/i18n/locales/ja.ts b/apps/web/src/i18n/locales/ja.ts index b8080d188a..4e2a9bf0f0 100644 --- a/apps/web/src/i18n/locales/ja.ts +++ b/apps/web/src/i18n/locales/ja.ts @@ -3339,6 +3339,9 @@ export const ja: Dict = { 'settings.skillsCreate': '作成', 'settings.skillsSave': '保存', 'settings.skillsSaving': '保存中…', + 'settings.skillsOverrideCreate': 'ユーザー上書きを作成', + 'settings.skillsOverrideSave': 'ユーザー上書きとして保存', + 'settings.skillsBuiltInOverrideWarning': 'この組み込みスキルを編集すると、ユーザー上書きが作成されます。上書きを削除するまで、組み込みの項目は一覧から非表示になります。続行しますか?', 'settings.skillsFiles': 'ファイル', 'settings.skillsNoFiles': 'このスキルフォルダーにファイルはありません。', 'settings.skillsNameRequired': 'スキル名は必須です。', diff --git a/apps/web/src/i18n/locales/ko.ts b/apps/web/src/i18n/locales/ko.ts index 6037ef678f..be200981aa 100644 --- a/apps/web/src/i18n/locales/ko.ts +++ b/apps/web/src/i18n/locales/ko.ts @@ -3339,6 +3339,9 @@ export const ko: Dict = { 'settings.skillsCreate': '만들기', 'settings.skillsSave': '저장', 'settings.skillsSaving': '저장 중…', + 'settings.skillsOverrideCreate': '사용자 재정의 만들기', + 'settings.skillsOverrideSave': '사용자 재정의로 저장', + 'settings.skillsBuiltInOverrideWarning': '이 기본 제공 스킬을 편집하면 사용자 재정의가 생성됩니다. 재정의를 삭제할 때까지 기본 제공 항목은 목록에서 숨겨집니다. 계속하시겠습니까?', 'settings.skillsFiles': '파일', 'settings.skillsNoFiles': '이 스킬 폴더에 파일이 없습니다.', 'settings.skillsNameRequired': '스킬 이름은 필수입니다.', diff --git a/apps/web/src/i18n/locales/pl.ts b/apps/web/src/i18n/locales/pl.ts index 38e29fedc2..f68160fce9 100644 --- a/apps/web/src/i18n/locales/pl.ts +++ b/apps/web/src/i18n/locales/pl.ts @@ -3339,6 +3339,9 @@ export const pl: Dict = { 'settings.skillsCreate': 'Utwórz', 'settings.skillsSave': 'Zapisz', 'settings.skillsSaving': 'Zapisywanie…', + 'settings.skillsOverrideCreate': 'Utwórz nadpisanie użytkownika', + 'settings.skillsOverrideSave': 'Zapisz jako nadpisanie użytkownika', + 'settings.skillsBuiltInOverrideWarning': 'Edycja tej wbudowanej umiejętności tworzy nadpisanie użytkownika. Wbudowany wpis zostanie ukryty z listy, dopóki nie usuniesz nadpisania. Kontynuować?', 'settings.skillsFiles': 'Pliki', 'settings.skillsNoFiles': 'Brak plików w tym folderze umiejętności.', 'settings.skillsNameRequired': 'Nazwa umiejętności jest wymagana.', diff --git a/apps/web/src/i18n/locales/pt-BR.ts b/apps/web/src/i18n/locales/pt-BR.ts index 5ee6d88b42..fb4f7dc606 100644 --- a/apps/web/src/i18n/locales/pt-BR.ts +++ b/apps/web/src/i18n/locales/pt-BR.ts @@ -3339,6 +3339,9 @@ export const ptBR: Dict = { 'settings.skillsCreate': 'Criar', 'settings.skillsSave': 'Salvar', 'settings.skillsSaving': 'Salvando…', + 'settings.skillsOverrideCreate': 'Criar substituição do usuário', + 'settings.skillsOverrideSave': 'Salvar como substituição do usuário', + 'settings.skillsBuiltInOverrideWarning': 'Editar esta habilidade integrada cria uma substituição do usuário. A entrada integrada ficará oculta na lista até você excluir a substituição. Continuar?', 'settings.skillsFiles': 'Arquivos', 'settings.skillsNoFiles': 'Nenhum arquivo nesta pasta de habilidade.', 'settings.skillsNameRequired': 'O nome da habilidade é obrigatório.', diff --git a/apps/web/src/i18n/locales/ru.ts b/apps/web/src/i18n/locales/ru.ts index bfb75a6c46..408a822fb9 100644 --- a/apps/web/src/i18n/locales/ru.ts +++ b/apps/web/src/i18n/locales/ru.ts @@ -3339,6 +3339,9 @@ export const ru: Dict = { 'settings.skillsCreate': 'Создать', 'settings.skillsSave': 'Сохранить', 'settings.skillsSaving': 'Сохранение…', + 'settings.skillsOverrideCreate': 'Создать пользовательское переопределение', + 'settings.skillsOverrideSave': 'Сохранить как пользовательское переопределение', + 'settings.skillsBuiltInOverrideWarning': 'Редактирование этого встроенного навыка создаёт пользовательское переопределение. Встроенная запись будет скрыта из списка, пока вы не удалите переопределение. Продолжить?', 'settings.skillsFiles': 'Файлы', 'settings.skillsNoFiles': 'В папке этого навыка нет файлов.', 'settings.skillsNameRequired': 'Имя навыка обязательно.', diff --git a/apps/web/src/i18n/locales/th.ts b/apps/web/src/i18n/locales/th.ts index ffaae70d37..6bf06cb492 100644 --- a/apps/web/src/i18n/locales/th.ts +++ b/apps/web/src/i18n/locales/th.ts @@ -3339,6 +3339,9 @@ export const th: Dict = { 'settings.skillsCreate': 'สร้าง', 'settings.skillsSave': 'บันทึก', 'settings.skillsSaving': 'กำลังบันทึก…', + 'settings.skillsOverrideCreate': 'สร้างการแทนที่ของผู้ใช้', + 'settings.skillsOverrideSave': 'บันทึกเป็นการแทนที่ของผู้ใช้', + 'settings.skillsBuiltInOverrideWarning': 'การแก้ไขสกิลในตัวนี้จะสร้างการแทนที่ของผู้ใช้ รายการในตัวจะถูกซ่อนจากรายการจนกว่าคุณจะลบการแทนที่ ดำเนินการต่อหรือไม่?', 'settings.skillsFiles': 'ไฟล์', 'settings.skillsNoFiles': 'ไม่มีไฟล์ในโฟลเดอร์ทักษะนี้', 'settings.skillsNameRequired': 'ต้องระบุชื่อทักษะ', diff --git a/apps/web/src/i18n/locales/tr.ts b/apps/web/src/i18n/locales/tr.ts index 45f95f8ae1..5b76ac3cfa 100644 --- a/apps/web/src/i18n/locales/tr.ts +++ b/apps/web/src/i18n/locales/tr.ts @@ -3339,6 +3339,9 @@ export const tr: Dict = { 'settings.skillsCreate': 'Oluştur', 'settings.skillsSave': 'Kaydet', 'settings.skillsSaving': 'Kaydediliyor…', + 'settings.skillsOverrideCreate': 'Kullanıcı geçersiz kılması oluştur', + 'settings.skillsOverrideSave': 'Kullanıcı geçersiz kılması olarak kaydet', + 'settings.skillsBuiltInOverrideWarning': 'Bu yerleşik beceriyi düzenlemek bir kullanıcı geçersiz kılması oluşturur. Geçersiz kılmayı silene kadar yerleşik giriş listeden gizlenir. Devam edilsin mi?', 'settings.skillsFiles': 'Dosyalar', 'settings.skillsNoFiles': 'Bu yetenek klasöründe dosya yok.', 'settings.skillsNameRequired': 'Beceri adı gereklidir.', diff --git a/apps/web/src/i18n/locales/uk.ts b/apps/web/src/i18n/locales/uk.ts index 55874c7c6b..5a723549da 100644 --- a/apps/web/src/i18n/locales/uk.ts +++ b/apps/web/src/i18n/locales/uk.ts @@ -3339,6 +3339,9 @@ export const uk: Dict = { 'settings.skillsCreate': 'Створити', 'settings.skillsSave': 'Зберегти', 'settings.skillsSaving': 'Збереження…', + 'settings.skillsOverrideCreate': 'Створити користувацьке перевизначення', + 'settings.skillsOverrideSave': 'Зберегти як користувацьке перевизначення', + 'settings.skillsBuiltInOverrideWarning': 'Редагування цього вбудованого вміння створює користувацьке перевизначення. Вбудований запис буде приховано зі списку, доки ви не видалите перевизначення. Продовжити?', 'settings.skillsFiles': 'Файли', 'settings.skillsNoFiles': 'У теці цієї навички немає файлів.', 'settings.skillsNameRequired': 'Ім\'я навички обов\'язкове.', diff --git a/apps/web/src/i18n/locales/zh-CN.ts b/apps/web/src/i18n/locales/zh-CN.ts index 322d1333c0..768619a5d3 100644 --- a/apps/web/src/i18n/locales/zh-CN.ts +++ b/apps/web/src/i18n/locales/zh-CN.ts @@ -3588,6 +3588,10 @@ export const zhCN: Dict = { "settings.skillsCreate": "创建", "settings.skillsSave": "保存", "settings.skillsSaving": "保存中…", + "settings.skillsOverrideCreate": "创建用户覆盖", + "settings.skillsOverrideSave": "另存为用户覆盖", + "settings.skillsBuiltInOverrideWarning": + "编辑此内置技能会创建一份用户覆盖。在你删除该覆盖之前,内置条目将从列表中隐藏。是否继续?", "settings.skillsFiles": "文件", "settings.skillsNoFiles": "该技能目录下暂无文件。", "settings.skillsNameRequired": "技能名称为必填项。", diff --git a/apps/web/src/i18n/locales/zh-TW.ts b/apps/web/src/i18n/locales/zh-TW.ts index eb9a0988a5..81803d7b1b 100644 --- a/apps/web/src/i18n/locales/zh-TW.ts +++ b/apps/web/src/i18n/locales/zh-TW.ts @@ -3597,6 +3597,10 @@ export const zhTW: Dict = { "settings.skillsCreate": "建立", "settings.skillsSave": "儲存", "settings.skillsSaving": "儲存中…", + "settings.skillsOverrideCreate": "建立使用者覆寫", + "settings.skillsOverrideSave": "另存為使用者覆寫", + "settings.skillsBuiltInOverrideWarning": + "編輯此內建技能會建立一份使用者覆寫。在你刪除該覆寫之前,內建項目將從清單中隱藏。是否繼續?", "settings.skillsFiles": "檔案", "settings.skillsNoFiles": "此技能資料夾沒有檔案。", "settings.skillsNameRequired": "技能名稱為必填項。", diff --git a/apps/web/src/i18n/types.ts b/apps/web/src/i18n/types.ts index 8f1fead57a..7058160a59 100644 --- a/apps/web/src/i18n/types.ts +++ b/apps/web/src/i18n/types.ts @@ -567,6 +567,9 @@ export interface Dict { 'settings.skillsCreate': string; 'settings.skillsSave': string; 'settings.skillsSaving': string; + 'settings.skillsOverrideCreate': string; + 'settings.skillsOverrideSave': string; + 'settings.skillsBuiltInOverrideWarning': string; 'settings.skillsFiles': string; 'settings.skillsNoFiles': string; 'settings.skillsNameRequired': string; diff --git a/apps/web/tests/components/SkillsSection.test.tsx b/apps/web/tests/components/SkillsSection.test.tsx index 0b3e4823a1..7ad417b5fc 100644 --- a/apps/web/tests/components/SkillsSection.test.tsx +++ b/apps/web/tests/components/SkillsSection.test.tsx @@ -5,6 +5,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { SkillsSection } from '../../src/components/SkillsSection'; import { I18nProvider } from '../../src/i18n'; +import { en } from '../../src/i18n/locales/en'; +import { zhCN } from '../../src/i18n/locales/zh-CN'; import type { AppConfig } from '../../src/types'; import type { SkillSummary } from '@open-design/contracts'; @@ -224,6 +226,92 @@ describe('SkillsSection', () => { expect(await within(row).findByTestId('skills-edit-form')).toBeTruthy(); }); + // Regression for the nettee / lefarcen follow-up on PR #4793: editing a + // built-in skill must read as an explicit "user override" flow in every + // locale, never a hardcoded-English or plain "Edit" / "Save" affordance. + // Lock the override wording end to end — edit tooltip → confirmation → + // form heading → submit — in the default English locale and one translated + // zh-CN path, so a regression to the old wording (or to a missing locale + // key) goes red. + describe.each([ + { locale: 'en' as const, dict: en }, + { locale: 'zh-CN' as const, dict: zhCN }, + ])('built-in skill override wording ($locale)', ({ locale, dict }) => { + it('frames the edit affordance, confirmation, form, and submit as a user override', async () => { + renderSkillsSection( + [ + makeSkill({ + id: 'builtin-skill', + name: 'Built-in skill', + source: 'built-in', + }), + ], + { locale }, + ); + + const row = await screen.findByTestId('skill-row-builtin-skill'); + + // The edit (pencil) tooltip reads "Create user override". + expect(within(row).getByTestId('skills-edit').title).toBe( + dict['settings.skillsOverrideCreate'], + ); + + fireEvent.click(within(row).getByTestId('skills-edit')); + + // The inline confirmation shows the localized override warning and an + // override-framed confirm button — not the plain "Edit" label. + expect( + await within(row).findByText(dict['settings.skillsBuiltInOverrideWarning']), + ).toBeTruthy(); + expect( + within(row).getByTestId('skills-edit-builtin-confirm').textContent, + ).toBe(dict['settings.skillsOverrideCreate']); + + fireEvent.click(within(row).getByTestId('skills-edit-builtin-confirm')); + + // The override form heading and submit both read as "user override". + const form = await within(row).findByTestId('skills-edit-form'); + expect(within(form).getByRole('heading', { level: 4 }).textContent).toBe( + dict['settings.skillsOverrideCreate'], + ); + expect(within(form).getByTestId('skills-save').textContent).toBe( + dict['settings.skillsOverrideSave'], + ); + }); + }); + + // The override wording must stay scoped to built-in skills: editing a + // user-authored skill keeps the plain Edit / Save affordance and never + // surfaces the override confirmation. + it('keeps the plain edit/save wording for user skills', async () => { + renderSkillsSection( + [ + makeSkill({ + id: 'user-skill', + name: 'User skill', + source: 'user', + }), + ], + { locale: 'en' }, + ); + + const row = await screen.findByTestId('skill-row-user-skill'); + expect(within(row).getByTestId('skills-edit').title).toBe( + en['settings.skillsEdit'], + ); + + fireEvent.click(within(row).getByTestId('skills-edit')); + expect(within(row).queryByTestId('skills-edit-builtin-warning')).toBeNull(); + + const form = await within(row).findByTestId('skills-edit-form'); + expect(within(form).getByRole('heading', { level: 4 }).textContent).toBe( + en['settings.skillsEdit'], + ); + expect(within(form).getByTestId('skills-save').textContent).toBe( + en['settings.skillsSave'], + ); + }); + it('matches localized built-in skill names and descriptions in search', async () => { renderSkillsSection( [