فهرست منبع

fix(backup): disclose the K-profile exception before the restore, not after (#2656)

    With overwrite off, the confirmation said "Missing entries are added; existing
    entries stay as they are." For archives, spools and settings that is true --
    _apply threads the flag into all three. _restore_kprofiles takes no overwrite
    parameter at all, and deliberately: writing a slot is always an overwrite on the
    printer, so it resolves the live cali_idx and publishes extrusion_cali_set
    either way, replacing whatever calibration that slot currently holds.

    The behaviour is right and the backend does say so, but it says so as a
    kprofilesAlwaysOverwrite note -- which only reaches the user in the result panel,
    after an MQTT send that cannot be taken back. The one screen that explains
    overwrite-off stated the opposite. So the fix is on the frontend, where the
    mismatch is.

    One new leaf, kprofilesOverwriteCaveat, in all 13 locales, rendered in two
    places: appended to the overwrite-off confirmation when kprofiles is among the
    selected categories, and beside the K-profiles row itself as soon as it is
    ticked, which is the same screen as the toggle whose promise it qualifies.
    Neither appears with overwrite on, where nothing is promising otherwise.

    Tests: 2 that fail pre-fix (the caveat beside the row, and inside the
    confirmation the user clicks through) and 2 controls (a spools-only restore keeps
    the plain message; overwrite-on keeps the strong one and adds nothing). Frontend
    suite 2601 -> 2605 tests across 195 files.
maziggy 3 هفته پیش
والد
کامیت
c9ce6dac3c

+ 74 - 0
frontend/src/__tests__/components/GitHubRestoreModal.test.tsx

@@ -63,6 +63,17 @@ const mockPreview = {
   ],
   ],
 };
 };
 
 
+// The default fixture has no K-profiles in the commit, which is the one category
+// whose row cannot be selected there.
+const mockPreviewWithKprofiles = {
+  ...mockPreview,
+  categories: mockPreview.categories.map((c) =>
+    c.category === 'kprofiles'
+      ? { category: 'kprofiles', available: true, item_count: 3, detail: null, detail_code: null, detail_params: {} }
+      : c
+  ),
+};
+
 type JsonBody = Record<string, unknown>;
 type JsonBody = Record<string, unknown>;
 
 
 function mockEndpoints(overrides: { preview?: JsonBody; commits?: JsonBody } = {}) {
 function mockEndpoints(overrides: { preview?: JsonBody; commits?: JsonBody } = {}) {
@@ -330,6 +341,69 @@ describe('GitHubRestoreModal', () => {
     });
     });
   });
   });
 
 
+  // Overwrite-off says existing entries stay as they are. K-profiles are the one
+  // category that cannot honour that — writing a slot always replaces the
+  // calibration on the printer — and the backend's note saying so only arrives
+  // in the result panel, after the MQTT send. So the disclosure has to be on the
+  // screen where the promise is made, before the user commits to it.
+  describe('the K-profile exception to overwrite-off', () => {
+    beforeEach(() => {
+      mockEndpoints({ preview: mockPreviewWithKprofiles as unknown as JsonBody });
+    });
+
+    it('appears beside the category as soon as it is selected', async () => {
+      render(<GitHubRestoreModal onClose={vi.fn()} />);
+
+      const checkboxes = await waitFor(() => screen.getAllByRole('checkbox') as HTMLInputElement[]);
+      await userEvent.click(checkboxes[3]);
+
+      await waitFor(() => {
+        expect(screen.getByText(/K-profiles are the exception/)).toBeInTheDocument();
+      });
+
+      // And it goes once overwrite is on, where nothing is promising otherwise.
+      await userEvent.click(screen.getByRole('switch'));
+      expect(screen.queryByText(/K-profiles are the exception/)).not.toBeInTheDocument();
+    });
+
+    it('is part of the confirmation the user actually clicks through', async () => {
+      render(<GitHubRestoreModal onClose={vi.fn()} />);
+
+      const checkboxes = await waitFor(() => screen.getAllByRole('checkbox') as HTMLInputElement[]);
+      await userEvent.click(checkboxes[3]);
+      await userEvent.click(screen.getByRole('button', { name: /Restore$/ }));
+
+      await waitFor(() => screen.getByText('Restore from backup?'));
+      expect(
+        screen.getByText(/existing entries stay as they are\. K-profiles are the exception/)
+      ).toBeInTheDocument();
+    });
+
+    it('stays out of the confirmation for the categories that do keep the promise', async () => {
+      render(<GitHubRestoreModal onClose={vi.fn()} />);
+
+      const checkboxes = await waitFor(() => screen.getAllByRole('checkbox') as HTMLInputElement[]);
+      await userEvent.click(checkboxes[1]);
+      await userEvent.click(screen.getByRole('button', { name: /Restore$/ }));
+
+      await waitFor(() => screen.getByText('Restore from backup?'));
+      expect(screen.getByText(/existing entries stay as they are\.$/)).toBeInTheDocument();
+      expect(screen.queryByText(/K-profiles are the exception/)).not.toBeInTheDocument();
+    });
+
+    it('is redundant with overwrite on, so it is not shown there', async () => {
+      render(<GitHubRestoreModal onClose={vi.fn()} />);
+
+      const checkboxes = await waitFor(() => screen.getAllByRole('checkbox') as HTMLInputElement[]);
+      await userEvent.click(checkboxes[3]);
+      await userEvent.click(screen.getByRole('switch'));
+      await userEvent.click(screen.getByRole('button', { name: /Restore$/ }));
+
+      await waitFor(() => screen.getByText(/This cannot be undone/));
+      expect(screen.queryByText(/K-profiles are the exception/)).not.toBeInTheDocument();
+    });
+  });
+
   it('surfaces a preview failure instead of an empty category list', async () => {
   it('surfaces a preview failure instead of an empty category list', async () => {
     mockEndpoints({
     mockEndpoints({
       preview: {
       preview: {

+ 18 - 1
frontend/src/components/GitHubRestoreModal.tsx

@@ -131,6 +131,16 @@ export function GitHubRestoreModal({ onClose }: GitHubRestoreModalProps) {
   );
   );
   const selectedCount = selectedCategories.length;
   const selectedCount = selectedCategories.length;
 
 
+  // Overwrite-off tells the user that existing entries stay as they are, and for
+  // three of the four categories it keeps that promise. K-profiles cannot:
+  // _restore_kprofiles takes no overwrite flag, because writing a slot is always
+  // an overwrite on the printer — resolving the live cali_idx and publishing
+  // extrusion_cali_set replaces whatever calibration that slot holds. The
+  // backend does say so, but as a note in the result panel, i.e. after the MQTT
+  // send has already happened and cannot be taken back. So the one screen that
+  // explains overwrite-off has to carry the exception too, before the click.
+  const warnKprofilesOverwrite = !overwriteExisting && selectedCategories.includes('kprofiles');
+
   const restoreMutation = useMutation({
   const restoreMutation = useMutation({
     mutationFn: () =>
     mutationFn: () =>
       api.restoreFromGitHub({
       api.restoreFromGitHub({
@@ -440,6 +450,11 @@ export function GitHubRestoreModal({ onClose }: GitHubRestoreModalProps) {
                                 ) : null}
                                 ) : null}
                               </div>
                               </div>
                               {info?.detail && <div className="text-xs text-bambu-gray">{info.detail}</div>}
                               {info?.detail && <div className="text-xs text-bambu-gray">{info.detail}</div>}
+                              {category.id === 'kprofiles' && isChecked && warnKprofilesOverwrite && (
+                                <div className="text-xs text-yellow-700 dark:text-yellow-200">
+                                  {t('backup.restoreFromGit.kprofilesOverwriteCaveat')}
+                                </div>
+                              )}
                             </div>
                             </div>
                           </label>
                           </label>
                         );
                         );
@@ -525,7 +540,9 @@ export function GitHubRestoreModal({ onClose }: GitHubRestoreModalProps) {
           message={
           message={
             overwriteExisting
             overwriteExisting
               ? t('backup.restoreFromGit.confirmMessageOverwrite')
               ? t('backup.restoreFromGit.confirmMessageOverwrite')
-              : t('backup.restoreFromGit.confirmMessage')
+              : warnKprofilesOverwrite
+                ? `${t('backup.restoreFromGit.confirmMessage')} ${t('backup.restoreFromGit.kprofilesOverwriteCaveat')}`
+                : t('backup.restoreFromGit.confirmMessage')
           }
           }
           confirmText={t('backup.restore')}
           confirmText={t('backup.restore')}
           isLoading={isRestoring}
           isLoading={isRestoring}

+ 1 - 0
frontend/src/i18n/locales/de.ts

@@ -4891,6 +4891,7 @@ export default {
       confirmTitle: 'Aus Backup wiederherstellen?',
       confirmTitle: 'Aus Backup wiederherstellen?',
       confirmMessage: 'Die ausgewählten Kategorien werden aus diesem Commit wiederhergestellt. Fehlende Einträge werden ergänzt, vorhandene bleiben unverändert.',
       confirmMessage: 'Die ausgewählten Kategorien werden aus diesem Commit wiederhergestellt. Fehlende Einträge werden ergänzt, vorhandene bleiben unverändert.',
       confirmMessageOverwrite: 'Die ausgewählten Kategorien werden aus diesem Commit wiederhergestellt und lokal vorhandene Einträge überschrieben. Dies kann nicht rückgängig gemacht werden.',
       confirmMessageOverwrite: 'Die ausgewählten Kategorien werden aus diesem Commit wiederhergestellt und lokal vorhandene Einträge überschrieben. Dies kann nicht rückgängig gemacht werden.',
+      kprofilesOverwriteCaveat: 'K-Profile sind die Ausnahme: Das Schreiben eines Slots ersetzt immer die Kalibrierung auf dem Drucker.',
       tally: '{{restored}} wiederhergestellt, {{skipped}} übersprungen, {{failed}} fehlgeschlagen',
       tally: '{{restored}} wiederhergestellt, {{skipped}} übersprungen, {{failed}} fehlgeschlagen',
       reloadHint: 'Bambuddy neu laden, damit die wiederhergestellten Daten überall erscheinen.',
       reloadHint: 'Bambuddy neu laden, damit die wiederhergestellten Daten überall erscheinen.',
       failed: 'Wiederherstellung fehlgeschlagen.',
       failed: 'Wiederherstellung fehlgeschlagen.',

+ 1 - 0
frontend/src/i18n/locales/en.ts

@@ -4934,6 +4934,7 @@ export default {
       confirmTitle: 'Restore from backup?',
       confirmTitle: 'Restore from backup?',
       confirmMessage: 'The selected categories will be restored from this commit. Missing entries are added; existing entries stay as they are.',
       confirmMessage: 'The selected categories will be restored from this commit. Missing entries are added; existing entries stay as they are.',
       confirmMessageOverwrite: 'The selected categories will be restored from this commit, overwriting entries that already exist locally. This cannot be undone.',
       confirmMessageOverwrite: 'The selected categories will be restored from this commit, overwriting entries that already exist locally. This cannot be undone.',
+      kprofilesOverwriteCaveat: 'K-profiles are the exception: writing a slot always replaces the calibration on the printer.',
       tally: '{{restored}} restored, {{skipped}} skipped, {{failed}} failed',
       tally: '{{restored}} restored, {{skipped}} skipped, {{failed}} failed',
       reloadHint: 'Reload Bambuddy so the restored data appears everywhere.',
       reloadHint: 'Reload Bambuddy so the restored data appears everywhere.',
       failed: 'Restore failed.',
       failed: 'Restore failed.',

+ 1 - 0
frontend/src/i18n/locales/es.ts

@@ -4899,6 +4899,7 @@ export default {
       confirmTitle: '¿Restaurar desde la copia?',
       confirmTitle: '¿Restaurar desde la copia?',
       confirmMessage: 'Las categorías seleccionadas se restaurarán desde este commit. Se añaden las entradas que falten y las existentes se mantienen igual.',
       confirmMessage: 'Las categorías seleccionadas se restaurarán desde este commit. Se añaden las entradas que falten y las existentes se mantienen igual.',
       confirmMessageOverwrite: 'Las categorías seleccionadas se restaurarán desde este commit y se sobrescribirán las entradas que ya existan localmente. Esto no se puede deshacer.',
       confirmMessageOverwrite: 'Las categorías seleccionadas se restaurarán desde este commit y se sobrescribirán las entradas que ya existan localmente. Esto no se puede deshacer.',
+      kprofilesOverwriteCaveat: 'Los perfiles K son la excepción: escribir una ranura siempre reemplaza la calibración en la impresora.',
       tally: '{{restored}} restaurados, {{skipped}} omitidos, {{failed}} fallidos',
       tally: '{{restored}} restaurados, {{skipped}} omitidos, {{failed}} fallidos',
       reloadHint: 'Recarga Bambuddy para que los datos restaurados aparezcan en todas partes.',
       reloadHint: 'Recarga Bambuddy para que los datos restaurados aparezcan en todas partes.',
       failed: 'La restauración ha fallado.',
       failed: 'La restauración ha fallado.',

+ 1 - 0
frontend/src/i18n/locales/fr.ts

@@ -4880,6 +4880,7 @@ export default {
       confirmTitle: 'Restaurer depuis la sauvegarde ?',
       confirmTitle: 'Restaurer depuis la sauvegarde ?',
       confirmMessage: 'Les catégories sélectionnées seront restaurées depuis ce commit. Les entrées manquantes sont ajoutées, les existantes restent inchangées.',
       confirmMessage: 'Les catégories sélectionnées seront restaurées depuis ce commit. Les entrées manquantes sont ajoutées, les existantes restent inchangées.',
       confirmMessageOverwrite: 'Les catégories sélectionnées seront restaurées depuis ce commit et les entrées déjà présentes localement seront écrasées. Cette action est irréversible.',
       confirmMessageOverwrite: 'Les catégories sélectionnées seront restaurées depuis ce commit et les entrées déjà présentes localement seront écrasées. Cette action est irréversible.',
+      kprofilesOverwriteCaveat: "Les profils K sont l'exception : écrire un emplacement remplace toujours la calibration sur l'imprimante.",
       tally: '{{restored}} restaurés, {{skipped}} ignorés, {{failed}} en échec',
       tally: '{{restored}} restaurés, {{skipped}} ignorés, {{failed}} en échec',
       reloadHint: 'Rechargez Bambuddy pour que les données restaurées apparaissent partout.',
       reloadHint: 'Rechargez Bambuddy pour que les données restaurées apparaissent partout.',
       failed: 'Échec de la restauration.',
       failed: 'Échec de la restauration.',

+ 1 - 0
frontend/src/i18n/locales/it.ts

@@ -4879,6 +4879,7 @@ export default {
       confirmTitle: 'Ripristinare dal backup?',
       confirmTitle: 'Ripristinare dal backup?',
       confirmMessage: 'Le categorie selezionate verranno ripristinate da questo commit. Le voci mancanti vengono aggiunte, quelle esistenti restano invariate.',
       confirmMessage: 'Le categorie selezionate verranno ripristinate da questo commit. Le voci mancanti vengono aggiunte, quelle esistenti restano invariate.',
       confirmMessageOverwrite: 'Le categorie selezionate verranno ripristinate da questo commit sovrascrivendo le voci già presenti in locale. Operazione non annullabile.',
       confirmMessageOverwrite: 'Le categorie selezionate verranno ripristinate da questo commit sovrascrivendo le voci già presenti in locale. Operazione non annullabile.',
+      kprofilesOverwriteCaveat: "I profili K sono l'eccezione: scrivere uno slot sostituisce sempre la calibrazione sulla stampante.",
       tally: '{{restored}} ripristinati, {{skipped}} saltati, {{failed}} non riusciti',
       tally: '{{restored}} ripristinati, {{skipped}} saltati, {{failed}} non riusciti',
       reloadHint: 'Ricarica Bambuddy per vedere i dati ripristinati in tutte le sezioni.',
       reloadHint: 'Ricarica Bambuddy per vedere i dati ripristinati in tutte le sezioni.',
       failed: 'Ripristino non riuscito.',
       failed: 'Ripristino non riuscito.',

+ 1 - 0
frontend/src/i18n/locales/ja.ts

@@ -4891,6 +4891,7 @@ export default {
       confirmTitle: 'バックアップから復元しますか?',
       confirmTitle: 'バックアップから復元しますか?',
       confirmMessage: '選択したカテゴリをこのコミットから復元します。不足しているエントリが追加され、既存のエントリはそのまま残ります。',
       confirmMessage: '選択したカテゴリをこのコミットから復元します。不足しているエントリが追加され、既存のエントリはそのまま残ります。',
       confirmMessageOverwrite: '選択したカテゴリをこのコミットから復元し、ローカルに既存のエントリを上書きします。この操作は取り消せません。',
       confirmMessageOverwrite: '選択したカテゴリをこのコミットから復元し、ローカルに既存のエントリを上書きします。この操作は取り消せません。',
+      kprofilesOverwriteCaveat: 'Kプロファイルは例外です。スロットへの書き込みは、プリンター上のキャリブレーションを常に置き換えます。',
       tally: '復元 {{restored}} 件、スキップ {{skipped}} 件、失敗 {{failed}} 件',
       tally: '復元 {{restored}} 件、スキップ {{skipped}} 件、失敗 {{failed}} 件',
       reloadHint: '復元したデータを全体に反映するには Bambuddy を再読み込みしてください。',
       reloadHint: '復元したデータを全体に反映するには Bambuddy を再読み込みしてください。',
       failed: '復元に失敗しました。',
       failed: '復元に失敗しました。',

+ 1 - 0
frontend/src/i18n/locales/ko.ts

@@ -4656,6 +4656,7 @@ export default {
       confirmTitle: '백업에서 복원하시겠습니까?',
       confirmTitle: '백업에서 복원하시겠습니까?',
       confirmMessage: '선택한 항목을 이 커밋에서 복원합니다. 없는 항목은 추가되고 기존 항목은 그대로 유지됩니다.',
       confirmMessage: '선택한 항목을 이 커밋에서 복원합니다. 없는 항목은 추가되고 기존 항목은 그대로 유지됩니다.',
       confirmMessageOverwrite: '선택한 항목을 이 커밋에서 복원하고 로컬에 이미 있는 항목을 덮어씁니다. 이 작업은 취소할 수 없습니다.',
       confirmMessageOverwrite: '선택한 항목을 이 커밋에서 복원하고 로컬에 이미 있는 항목을 덮어씁니다. 이 작업은 취소할 수 없습니다.',
+      kprofilesOverwriteCaveat: 'K 프로파일은 예외입니다. 슬롯에 쓰면 프린터의 캘리브레이션이 항상 교체됩니다.',
       tally: '복원 {{restored}}개, 건너뜀 {{skipped}}개, 실패 {{failed}}개',
       tally: '복원 {{restored}}개, 건너뜀 {{skipped}}개, 실패 {{failed}}개',
       reloadHint: '복원된 데이터가 모든 화면에 반영되도록 Bambuddy를 새로 고치세요.',
       reloadHint: '복원된 데이터가 모든 화면에 반영되도록 Bambuddy를 새로 고치세요.',
       failed: '복원에 실패했습니다.',
       failed: '복원에 실패했습니다.',

+ 1 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -4879,6 +4879,7 @@ export default {
       confirmTitle: 'Restaurar do backup?',
       confirmTitle: 'Restaurar do backup?',
       confirmMessage: 'As categorias selecionadas serão restauradas deste commit. As entradas ausentes são adicionadas e as existentes permanecem como estão.',
       confirmMessage: 'As categorias selecionadas serão restauradas deste commit. As entradas ausentes são adicionadas e as existentes permanecem como estão.',
       confirmMessageOverwrite: 'As categorias selecionadas serão restauradas deste commit, sobrescrevendo as entradas que já existem localmente. Não é possível desfazer.',
       confirmMessageOverwrite: 'As categorias selecionadas serão restauradas deste commit, sobrescrevendo as entradas que já existem localmente. Não é possível desfazer.',
+      kprofilesOverwriteCaveat: 'Os perfis K são a exceção: gravar um slot sempre substitui a calibração na impressora.',
       tally: '{{restored}} restaurados, {{skipped}} ignorados, {{failed}} com falha',
       tally: '{{restored}} restaurados, {{skipped}} ignorados, {{failed}} com falha',
       reloadHint: 'Recarregue o Bambuddy para que os dados restaurados apareçam em todos os lugares.',
       reloadHint: 'Recarregue o Bambuddy para que os dados restaurados apareçam em todos os lugares.',
       failed: 'Falha na restauração.',
       failed: 'Falha na restauração.',

+ 1 - 0
frontend/src/i18n/locales/ru.ts

@@ -4648,6 +4648,7 @@ export default {
       confirmTitle: 'Восстановить из резервной копии?',
       confirmTitle: 'Восстановить из резервной копии?',
       confirmMessage: 'Выбранные категории будут восстановлены из этого коммита. Отсутствующие записи будут добавлены, существующие останутся без изменений.',
       confirmMessage: 'Выбранные категории будут восстановлены из этого коммита. Отсутствующие записи будут добавлены, существующие останутся без изменений.',
       confirmMessageOverwrite: 'Выбранные категории будут восстановлены из этого коммита с перезаписью уже существующих локальных записей. Отменить это действие нельзя.',
       confirmMessageOverwrite: 'Выбранные категории будут восстановлены из этого коммита с перезаписью уже существующих локальных записей. Отменить это действие нельзя.',
+      kprofilesOverwriteCaveat: 'K-профили — исключение: запись в слот всегда заменяет калибровку на принтере.',
       tally: 'восстановлено: {{restored}}, пропущено: {{skipped}}, с ошибкой: {{failed}}',
       tally: 'восстановлено: {{restored}}, пропущено: {{skipped}}, с ошибкой: {{failed}}',
       reloadHint: 'Перезагрузите Bambuddy, чтобы восстановленные данные отобразились везде.',
       reloadHint: 'Перезагрузите Bambuddy, чтобы восстановленные данные отобразились везде.',
       failed: 'Не удалось выполнить восстановление.',
       failed: 'Не удалось выполнить восстановление.',

+ 1 - 0
frontend/src/i18n/locales/tr.ts

@@ -4869,6 +4869,7 @@ export default {
       confirmTitle: 'Yedekten geri yüklensin mi?',
       confirmTitle: 'Yedekten geri yüklensin mi?',
       confirmMessage: 'Seçilen kategoriler bu commit\'ten geri yüklenecek. Eksik kayıtlar eklenir, mevcut kayıtlar olduğu gibi kalır.',
       confirmMessage: 'Seçilen kategoriler bu commit\'ten geri yüklenecek. Eksik kayıtlar eklenir, mevcut kayıtlar olduğu gibi kalır.',
       confirmMessageOverwrite: 'Seçilen kategoriler bu commit\'ten geri yüklenecek ve yerelde bulunan kayıtların üzerine yazılacak. Bu işlem geri alınamaz.',
       confirmMessageOverwrite: 'Seçilen kategoriler bu commit\'ten geri yüklenecek ve yerelde bulunan kayıtların üzerine yazılacak. Bu işlem geri alınamaz.',
+      kprofilesOverwriteCaveat: 'K profilleri istisnadır: bir yuvaya yazmak yazıcıdaki kalibrasyonu her zaman değiştirir.',
       tally: '{{restored}} geri yüklendi, {{skipped}} atlandı, {{failed}} başarısız',
       tally: '{{restored}} geri yüklendi, {{skipped}} atlandı, {{failed}} başarısız',
       reloadHint: 'Geri yüklenen verilerin her yerde görünmesi için Bambuddy\'yi yeniden yükleyin.',
       reloadHint: 'Geri yüklenen verilerin her yerde görünmesi için Bambuddy\'yi yeniden yükleyin.',
       failed: 'Geri yükleme başarısız oldu.',
       failed: 'Geri yükleme başarısız oldu.',

+ 1 - 0
frontend/src/i18n/locales/uk.ts

@@ -4934,6 +4934,7 @@ export default {
       confirmTitle: "Відновити з резервної копії?",
       confirmTitle: "Відновити з резервної копії?",
       confirmMessage: "Вибрані категорії буде відновлено з цього коміту. Відсутні записи буде додано, наявні залишаться без змін.",
       confirmMessage: "Вибрані категорії буде відновлено з цього коміту. Відсутні записи буде додано, наявні залишаться без змін.",
       confirmMessageOverwrite: "Вибрані категорії буде відновлено з цього коміту з перезаписом записів, які вже існують локально. Цю дію не можна скасувати.",
       confirmMessageOverwrite: "Вибрані категорії буде відновлено з цього коміту з перезаписом записів, які вже існують локально. Цю дію не можна скасувати.",
+      kprofilesOverwriteCaveat: 'K-профілі — виняток: запис у слот завжди замінює калібрування на принтері.',
       tally: "відновлено: {{restored}}, пропущено: {{skipped}}, з помилкою: {{failed}}",
       tally: "відновлено: {{restored}}, пропущено: {{skipped}}, з помилкою: {{failed}}",
       reloadHint: "Перезавантажте Bambuddy, щоб відновлені дані відобразилися всюди.",
       reloadHint: "Перезавантажте Bambuddy, щоб відновлені дані відобразилися всюди.",
       failed: "Не вдалося виконати відновлення.",
       failed: "Не вдалося виконати відновлення.",

+ 1 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -4879,6 +4879,7 @@ export default {
       confirmTitle: '要从备份恢复吗?',
       confirmTitle: '要从备份恢复吗?',
       confirmMessage: '将从此提交恢复所选类别。缺失的条目会被添加,已有条目保持不变。',
       confirmMessage: '将从此提交恢复所选类别。缺失的条目会被添加,已有条目保持不变。',
       confirmMessageOverwrite: '将从此提交恢复所选类别,并覆盖本地已存在的条目。此操作无法撤销。',
       confirmMessageOverwrite: '将从此提交恢复所选类别,并覆盖本地已存在的条目。此操作无法撤销。',
+      kprofilesOverwriteCaveat: 'K 值配置是例外:写入插槽总会替换打印机上的校准数据。',
       tally: '已恢复 {{restored}} 项,跳过 {{skipped}} 项,失败 {{failed}} 项',
       tally: '已恢复 {{restored}} 项,跳过 {{skipped}} 项,失败 {{failed}} 项',
       reloadHint: '请重新加载 Bambuddy,以便恢复的数据在各处生效。',
       reloadHint: '请重新加载 Bambuddy,以便恢复的数据在各处生效。',
       failed: '恢复失败。',
       failed: '恢复失败。',

+ 1 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -4879,6 +4879,7 @@ export default {
       confirmTitle: '要從備份還原嗎?',
       confirmTitle: '要從備份還原嗎?',
       confirmMessage: '將從此提交還原所選類別。缺少的項目會被新增,既有項目保持不變。',
       confirmMessage: '將從此提交還原所選類別。缺少的項目會被新增,既有項目保持不變。',
       confirmMessageOverwrite: '將從此提交還原所選類別,並覆寫本機已存在的項目。此操作無法復原。',
       confirmMessageOverwrite: '將從此提交還原所選類別,並覆寫本機已存在的項目。此操作無法復原。',
+      kprofilesOverwriteCaveat: 'K 值設定檔是例外:寫入插槽一定會取代印表機上的校準資料。',
       tally: '已還原 {{restored}} 筆、略過 {{skipped}} 筆、失敗 {{failed}} 筆',
       tally: '已還原 {{restored}} 筆、略過 {{skipped}} 筆、失敗 {{failed}} 筆',
       reloadHint: '請重新載入 Bambuddy,讓還原的資料在各處生效。',
       reloadHint: '請重新載入 Bambuddy,讓還原的資料在各處生效。',
       failed: '還原失敗。',
       failed: '還原失敗。',