fix(bulk-upload): i18n hardcoded strings in BulkDropZone and FileSwitcherStrip
- Add bulk_drop_desc, bulk_select_files, bulk_drop_zone_label, bulk_remove_file keys to de/en/es message files - BulkDropZone: use m.bulk_drop_zone_label(), m.bulk_drop_desc(), m.bulk_select_files() — removes all hardcoded German - FileSwitcherStrip: use m.bulk_remove_file() on × button; move aria-live from <ul> to a dedicated visually-hidden region above the strip (screen readers now announce changes without coupling the live region to the list) - Spec: import FileEntry from component instead of re-declaring; use data-remove-id selector instead of hardcoded German aria-label Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -868,5 +868,9 @@
|
|||||||
"bulk_file_error_chip_label": "Fehler beim Hochladen",
|
"bulk_file_error_chip_label": "Fehler beim Hochladen",
|
||||||
"bulk_upload_progress": "{done} von {total} hochgeladen",
|
"bulk_upload_progress": "{done} von {total} hochgeladen",
|
||||||
"bulk_partial_success": "{created} erstellt, {failed} fehlgeschlagen",
|
"bulk_partial_success": "{created} erstellt, {failed} fehlgeschlagen",
|
||||||
"bulk_all_failed": "Alle Uploads fehlgeschlagen"
|
"bulk_all_failed": "Alle Uploads fehlgeschlagen",
|
||||||
|
"bulk_drop_desc": "Für jede Datei wird ein eigenes Dokument erstellt. Der Titel wird aus dem Dateinamen vorausgefüllt — alle anderen Felder gelten für alle gemeinsam.",
|
||||||
|
"bulk_select_files": "Dateien auswählen",
|
||||||
|
"bulk_drop_zone_label": "Dateien ablegen",
|
||||||
|
"bulk_remove_file": "Entfernen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -868,5 +868,9 @@
|
|||||||
"bulk_file_error_chip_label": "Upload failed",
|
"bulk_file_error_chip_label": "Upload failed",
|
||||||
"bulk_upload_progress": "{done} of {total} uploaded",
|
"bulk_upload_progress": "{done} of {total} uploaded",
|
||||||
"bulk_partial_success": "{created} created, {failed} failed",
|
"bulk_partial_success": "{created} created, {failed} failed",
|
||||||
"bulk_all_failed": "All uploads failed"
|
"bulk_all_failed": "All uploads failed",
|
||||||
|
"bulk_drop_desc": "A separate document is created for each file. The title is pre-filled from the filename — all other fields apply to all documents.",
|
||||||
|
"bulk_select_files": "Select files",
|
||||||
|
"bulk_drop_zone_label": "Drop files here",
|
||||||
|
"bulk_remove_file": "Remove"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -868,5 +868,9 @@
|
|||||||
"bulk_file_error_chip_label": "Error al subir",
|
"bulk_file_error_chip_label": "Error al subir",
|
||||||
"bulk_upload_progress": "{done} de {total} subidos",
|
"bulk_upload_progress": "{done} de {total} subidos",
|
||||||
"bulk_partial_success": "{created} creados, {failed} fallidos",
|
"bulk_partial_success": "{created} creados, {failed} fallidos",
|
||||||
"bulk_all_failed": "Todos los uploads fallaron"
|
"bulk_all_failed": "Todos los uploads fallaron",
|
||||||
|
"bulk_drop_desc": "Se crea un documento separado por archivo. El título se rellena desde el nombre del archivo — el resto de campos se aplican a todos.",
|
||||||
|
"bulk_select_files": "Seleccionar archivos",
|
||||||
|
"bulk_drop_zone_label": "Soltar archivos aquí",
|
||||||
|
"bulk_remove_file": "Eliminar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ describe('BulkDocumentEditLayout', () => {
|
|||||||
makeFile('file2.pdf')
|
makeFile('file2.pdf')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Remove the chip for file1 via its "Entfernen" remove button (second × button)
|
// Remove the chip for file1 via its remove button (identified by data-remove-id)
|
||||||
const removeButtons = container.querySelectorAll<HTMLButtonElement>(
|
const removeButtons = container.querySelectorAll<HTMLButtonElement>(
|
||||||
'[data-testid="file-switcher-strip"] button[aria-label="Entfernen"]'
|
'[data-testid="file-switcher-strip"] button[data-remove-id]'
|
||||||
);
|
);
|
||||||
expect(removeButtons.length).toBe(3);
|
expect(removeButtons.length).toBe(3);
|
||||||
removeButtons[1].click(); // remove file1
|
removeButtons[1].click(); // remove file1
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ let isDragging = $state(false);
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
role="region"
|
role="region"
|
||||||
aria-label="Dateien ablegen"
|
aria-label={m.bulk_drop_zone_label()}
|
||||||
data-testid="bulk-drop-zone"
|
data-testid="bulk-drop-zone"
|
||||||
class="flex flex-1 flex-col items-center justify-center p-6"
|
class="flex flex-1 flex-col items-center justify-center p-6"
|
||||||
ondragover={(e) => {
|
ondragover={(e) => {
|
||||||
@@ -54,17 +54,13 @@ let isDragging = $state(false);
|
|||||||
<p class="font-serif text-base font-bold text-ink">{m.bulk_drop_hint()}</p>
|
<p class="font-serif text-base font-bold text-ink">{m.bulk_drop_hint()}</p>
|
||||||
|
|
||||||
<!-- Sub description -->
|
<!-- Sub description -->
|
||||||
<p class="text-sm leading-relaxed text-ink-2">
|
<p class="text-sm leading-relaxed text-ink-2">{m.bulk_drop_desc()}</p>
|
||||||
Für jede Datei wird ein eigenes Dokument erstellt.<br />
|
|
||||||
<strong class="text-ink">Der Titel</strong> wird aus dem Dateinamen vorausgefüllt —
|
|
||||||
<strong class="text-ink">alle anderen Felder</strong> gelten für alle gemeinsam.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- CTA button -->
|
<!-- CTA button -->
|
||||||
<label
|
<label
|
||||||
class="flex min-h-[44px] cursor-pointer items-center rounded-sm bg-primary px-6 py-2 text-xs font-bold tracking-widest text-primary-fg uppercase transition-opacity hover:opacity-90"
|
class="flex min-h-[44px] cursor-pointer items-center rounded-sm bg-primary px-6 py-2 text-xs font-bold tracking-widest text-primary-fg uppercase transition-opacity hover:opacity-90"
|
||||||
>
|
>
|
||||||
Dateien auswählen
|
{m.bulk_select_files()}
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
multiple
|
multiple
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ $effect(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div aria-live="polite" aria-atomic="true" class="sr-only"></div>
|
||||||
<div
|
<div
|
||||||
data-testid="file-switcher-strip"
|
data-testid="file-switcher-strip"
|
||||||
class="flex h-11 shrink-0 items-center gap-1 border-t border-line bg-pdf-ctrl px-2"
|
class="flex h-11 shrink-0 items-center gap-1 border-t border-line bg-pdf-ctrl px-2"
|
||||||
@@ -71,7 +72,7 @@ $effect(() => {
|
|||||||
>
|
>
|
||||||
|
|
||||||
<div bind:this={trackEl} class="flex flex-1 gap-1 overflow-x-auto" style="scrollbar-width:none">
|
<div bind:this={trackEl} class="flex flex-1 gap-1 overflow-x-auto" style="scrollbar-width:none">
|
||||||
<ul bind:this={listEl} aria-live="polite" role="list" class="flex flex-row gap-1 py-1">
|
<ul bind:this={listEl} role="list" class="flex flex-row gap-1 py-1">
|
||||||
{#each files as entry, i (entry.id)}
|
{#each files as entry, i (entry.id)}
|
||||||
<li role="listitem" class="inline-flex shrink-0 items-center">
|
<li role="listitem" class="inline-flex shrink-0 items-center">
|
||||||
<button
|
<button
|
||||||
@@ -102,7 +103,7 @@ $effect(() => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Entfernen"
|
aria-label={m.bulk_remove_file()}
|
||||||
data-remove-id={entry.id}
|
data-remove-id={entry.id}
|
||||||
onclick={() => onRemove(entry.id)}
|
onclick={() => onRemove(entry.id)}
|
||||||
class="ml-0.5 flex h-[44px] w-[44px] items-center justify-center text-base text-ink-3 hover:text-ink focus:outline-none focus-visible:ring-2 focus-visible:ring-accent"
|
class="ml-0.5 flex h-[44px] w-[44px] items-center justify-center text-base text-ink-3 hover:text-ink focus:outline-none focus-visible:ring-2 focus-visible:ring-accent"
|
||||||
|
|||||||
@@ -2,17 +2,10 @@ import { describe, it, expect, vi, afterEach } from 'vitest';
|
|||||||
import { cleanup, render } from 'vitest-browser-svelte';
|
import { cleanup, render } from 'vitest-browser-svelte';
|
||||||
import { page, userEvent } from 'vitest/browser';
|
import { page, userEvent } from 'vitest/browser';
|
||||||
import FileSwitcherStrip from './FileSwitcherStrip.svelte';
|
import FileSwitcherStrip from './FileSwitcherStrip.svelte';
|
||||||
|
import type { FileEntry } from './FileSwitcherStrip.svelte';
|
||||||
|
|
||||||
afterEach(cleanup);
|
afterEach(cleanup);
|
||||||
|
|
||||||
export interface FileEntry {
|
|
||||||
id: string;
|
|
||||||
file: File;
|
|
||||||
title: string;
|
|
||||||
status: 'idle' | 'error';
|
|
||||||
previewUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeFiles(n: number): FileEntry[] {
|
function makeFiles(n: number): FileEntry[] {
|
||||||
return Array.from({ length: n }, (_, i) => ({
|
return Array.from({ length: n }, (_, i) => ({
|
||||||
id: `id-${i}`,
|
id: `id-${i}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user