i18n(briefwechsel): DistributionBar reads text + aria-label via Paraglide
Drops the hardcoded German strings ("Briefverteilung in diesem Zeitraum",
"{n} von {name}") and routes every visible + assistive-tech string
through dist_bar_aria and dist_bar_segment message keys. An English
or Spanish user now sees "from" / "de" instead of "von" both on
screen and in the aria-label their screen reader announces.
Refs #305
Fixes @leonievoss i18n concern from PR review
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -165,6 +165,8 @@
|
|||||||
"conv_hero_divider": "oder",
|
"conv_hero_divider": "oder",
|
||||||
"conv_empty_recent_label": "Zuletzt geöffnet",
|
"conv_empty_recent_label": "Zuletzt geöffnet",
|
||||||
"conv_no_party": "—",
|
"conv_no_party": "—",
|
||||||
|
"dist_bar_segment": "{count} von {name}",
|
||||||
|
"dist_bar_aria": "Briefverteilung in diesem Zeitraum: {outCount} von {senderName}, {inCount} von {receiverName}",
|
||||||
"admin_heading": "Admin Dashboard",
|
"admin_heading": "Admin Dashboard",
|
||||||
"admin_tab_users": "Benutzer",
|
"admin_tab_users": "Benutzer",
|
||||||
"admin_tab_groups": "Gruppen",
|
"admin_tab_groups": "Gruppen",
|
||||||
|
|||||||
@@ -165,6 +165,8 @@
|
|||||||
"conv_hero_divider": "or",
|
"conv_hero_divider": "or",
|
||||||
"conv_empty_recent_label": "Recently opened",
|
"conv_empty_recent_label": "Recently opened",
|
||||||
"conv_no_party": "—",
|
"conv_no_party": "—",
|
||||||
|
"dist_bar_segment": "{count} from {name}",
|
||||||
|
"dist_bar_aria": "Letter distribution in this period: {outCount} from {senderName}, {inCount} from {receiverName}",
|
||||||
"admin_heading": "Admin Dashboard",
|
"admin_heading": "Admin Dashboard",
|
||||||
"admin_tab_users": "Users",
|
"admin_tab_users": "Users",
|
||||||
"admin_tab_groups": "Groups",
|
"admin_tab_groups": "Groups",
|
||||||
|
|||||||
@@ -165,6 +165,8 @@
|
|||||||
"conv_hero_divider": "o",
|
"conv_hero_divider": "o",
|
||||||
"conv_empty_recent_label": "Recientemente abiertos",
|
"conv_empty_recent_label": "Recientemente abiertos",
|
||||||
"conv_no_party": "—",
|
"conv_no_party": "—",
|
||||||
|
"dist_bar_segment": "{count} de {name}",
|
||||||
|
"dist_bar_aria": "Distribución de cartas en este período: {outCount} de {senderName}, {inCount} de {receiverName}",
|
||||||
"admin_heading": "Panel de administración",
|
"admin_heading": "Panel de administración",
|
||||||
"admin_tab_users": "Usuarios",
|
"admin_tab_users": "Usuarios",
|
||||||
"admin_tab_groups": "Grupos",
|
"admin_tab_groups": "Grupos",
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import * as m from '$lib/paraglide/messages.js';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
outCount: number;
|
outCount: number;
|
||||||
inCount: number;
|
inCount: number;
|
||||||
@@ -12,16 +14,20 @@ const total = $derived(outCount + inCount);
|
|||||||
const outPct = $derived(total > 0 ? (outCount / total) * 100 : 0);
|
const outPct = $derived(total > 0 ? (outCount / total) * 100 : 0);
|
||||||
const shortSenderName = $derived(senderName.split(' ')[0] ?? senderName);
|
const shortSenderName = $derived(senderName.split(' ')[0] ?? senderName);
|
||||||
const shortReceiverName = $derived(receiverName.split(' ')[0] ?? receiverName);
|
const shortReceiverName = $derived(receiverName.split(' ')[0] ?? receiverName);
|
||||||
|
|
||||||
|
const ariaLabel = $derived(m.dist_bar_aria({ outCount, senderName, inCount, receiverName }));
|
||||||
|
const outSegmentText = $derived(m.dist_bar_segment({ count: outCount, name: shortSenderName }));
|
||||||
|
const inSegmentText = $derived(m.dist_bar_segment({ count: inCount, name: shortReceiverName }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-1 border-b border-line bg-muted px-[18px] py-2"
|
class="flex flex-col gap-1 border-b border-line bg-muted px-[18px] py-2"
|
||||||
role="img"
|
role="img"
|
||||||
aria-label="Briefverteilung in diesem Zeitraum: {outCount} von {senderName}, {inCount} von {receiverName}"
|
aria-label={ariaLabel}
|
||||||
>
|
>
|
||||||
<div class="flex justify-between text-sm font-bold">
|
<div class="flex justify-between text-sm font-bold">
|
||||||
<span class="inline-flex items-center gap-1 text-primary"
|
<span class="inline-flex items-center gap-1 text-primary"
|
||||||
>{outCount} von {shortSenderName}
|
>{outSegmentText}
|
||||||
<img
|
<img
|
||||||
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Long-Arrow/Long-Arrow-Right-MD.svg"
|
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Long-Arrow/Long-Arrow-Right-MD.svg"
|
||||||
alt=""
|
alt=""
|
||||||
@@ -30,7 +36,7 @@ const shortReceiverName = $derived(receiverName.split(' ')[0] ?? receiverName);
|
|||||||
/></span
|
/></span
|
||||||
>
|
>
|
||||||
<span class="inline-flex items-center gap-1 text-accent"
|
<span class="inline-flex items-center gap-1 text-accent"
|
||||||
>{inCount} von {shortReceiverName}
|
>{inSegmentText}
|
||||||
<img
|
<img
|
||||||
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Long-Arrow/Long-Arrow-Left-MD.svg"
|
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Long-Arrow/Long-Arrow-Left-MD.svg"
|
||||||
alt=""
|
alt=""
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, it, expect, afterEach } from 'vitest';
|
import { describe, it, expect, afterEach } from 'vitest';
|
||||||
import { cleanup, render } from 'vitest-browser-svelte';
|
import { cleanup, render } from 'vitest-browser-svelte';
|
||||||
|
import * as m from '$lib/paraglide/messages.js';
|
||||||
|
|
||||||
import DistributionBar from './DistributionBar.svelte';
|
import DistributionBar from './DistributionBar.svelte';
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('DistributionBar', () => {
|
describe('DistributionBar', () => {
|
||||||
it('renders both counts and short names, and the two-tone fill bar', async () => {
|
it('renders the Paraglide aria-label and visible segments', async () => {
|
||||||
render(DistributionBar, {
|
render(DistributionBar, {
|
||||||
outCount: 3,
|
outCount: 3,
|
||||||
inCount: 7,
|
inCount: 7,
|
||||||
@@ -18,11 +19,22 @@ describe('DistributionBar', () => {
|
|||||||
|
|
||||||
const container = document.querySelector('[role="img"]') as HTMLElement;
|
const container = document.querySelector('[role="img"]') as HTMLElement;
|
||||||
expect(container).toBeTruthy();
|
expect(container).toBeTruthy();
|
||||||
expect(container.getAttribute('aria-label')).toContain('3 von Hans Müller');
|
|
||||||
expect(container.getAttribute('aria-label')).toContain('7 von Anna Schmidt');
|
|
||||||
|
|
||||||
expect(container.textContent).toContain('3 von Hans');
|
// The aria-label must come from Paraglide, not a hardcoded German string,
|
||||||
expect(container.textContent).toContain('7 von Anna');
|
// so the EN / ES users aren't served "Briefverteilung in diesem Zeitraum".
|
||||||
|
const expectedAria = m.dist_bar_aria({
|
||||||
|
outCount: 3,
|
||||||
|
senderName: 'Hans Müller',
|
||||||
|
inCount: 7,
|
||||||
|
receiverName: 'Anna Schmidt'
|
||||||
|
});
|
||||||
|
expect(container.getAttribute('aria-label')).toBe(expectedAria);
|
||||||
|
|
||||||
|
// The visible "{count} from/von {name}" spans must also come from Paraglide.
|
||||||
|
const outText = m.dist_bar_segment({ count: 3, name: 'Hans' });
|
||||||
|
const inText = m.dist_bar_segment({ count: 7, name: 'Anna' });
|
||||||
|
expect(container.textContent).toContain(outText);
|
||||||
|
expect(container.textContent).toContain(inText);
|
||||||
|
|
||||||
// 3/10 → 30% / 70% split on the two segments
|
// 3/10 → 30% / 70% split on the two segments
|
||||||
const segments = container.querySelectorAll('[data-testid="dist-bar-segment"]');
|
const segments = container.querySelectorAll('[data-testid="dist-bar-segment"]');
|
||||||
@@ -40,7 +52,8 @@ describe('DistributionBar', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const container = document.querySelector('[role="img"]') as HTMLElement;
|
const container = document.querySelector('[role="img"]') as HTMLElement;
|
||||||
expect(container.textContent).toContain('1 von SingleWord');
|
const expected = m.dist_bar_segment({ count: 1, name: 'SingleWord' });
|
||||||
|
expect(container.textContent).toContain(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a zero-percent left segment when outCount is zero', async () => {
|
it('renders a zero-percent left segment when outCount is zero', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user