Skip to content

Commit 4f8b808

Browse files
committed
Translate the mail dialog, the Playground web server and the copy-path error
Wraps the strings #630 lists that no other open batch covers, and drops the English sentence applyDoneMessage built from a verb and a noun for a pair it has no sentence for, in favour of a generic word. Adds two en-XA journeys: a mail on its Rendered and Raw tabs, and the Playground web server stopped, starting, running and after it exits. Part of #630.
1 parent e94f507 commit 4f8b808

5 files changed

Lines changed: 112 additions & 19 deletions

File tree

‎src/renderer/components/email-modal.jsx‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Modal, TabPanel } from '@wordpress/components';
2+
import { __ } from '@wordpress/i18n';
23

34
function formatEmailDate(email) {
45
if (email.sentAt) return new Date(email.sentAt).toLocaleString();
@@ -14,19 +15,28 @@ function formatEmailDate(email) {
1415
export function EmailModal({ email, onClose }) {
1516
return (
1617
<Modal
17-
title={email.subject || 'Email'}
18+
title={email.subject || __('Email')}
1819
onRequestClose={onClose}
1920
shouldCloseOnClickOutside
2021
isFullScreen
2122
>
2223
<div className="email-view">
2324
<div className="email-view-headers">
24-
<div><strong>From:</strong> {email.from || ''}</div>
25-
<div><strong>To:</strong> {email.to || ''}</div>
26-
{email.cc ? (<div><strong>CC:</strong> {email.cc}</div>) : null}
27-
<div><strong>Date:</strong> {formatEmailDate(email)}</div>
25+
<div><strong>{__('From:')}</strong> {email.from || ''}</div>
26+
<div><strong>{__('To:')}</strong> {email.to || ''}</div>
27+
{email.cc ? (<div><strong>{__('CC:')}</strong> {email.cc}</div>) : null}
28+
<div><strong>{__('Date:')}</strong> {formatEmailDate(email)}</div>
2829
</div>
29-
<TabPanel className="email-tabs" activeClass="is-active" tabs={[{name:'rendered',title:'Rendered'},{name:'raw',title:'Raw'}]}>
30+
<TabPanel
31+
className="email-tabs"
32+
activeClass="is-active"
33+
tabs={[
34+
// translators: The tab that shows an email as a reader would see it.
35+
{ name: 'rendered', title: __('Rendered') },
36+
// translators: The tab that shows an email as it was sent, headers and all.
37+
{ name: 'raw', title: __('Raw') }
38+
]}
39+
>
3040
{(tab)=> tab.name==='rendered' ? (
3141
<div className="email-view-rendered">
3242
{email.html ? (

‎src/renderer/confirmations.cjs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,9 @@ function applyDoneMessage(verb, noun) {
154154
'Restored previous branch': __('Restored the previous branch'),
155155
'Restored saved work': __('Restored the saved work')
156156
};
157-
return sentences[`${verb} ${noun}`] || `${verb} the ${noun}`;
157+
// A pair with no sentence of its own is still confirmed, without English
158+
// words spliced into it.
159+
return sentences[`${verb} ${noun}`] || __('Done');
158160
}
159161

160162
/**

‎src/renderer/index.jsx‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
SlotFillProvider
77
} from '@wordpress/components';
88
import { Page } from '@wordpress/admin-ui';
9-
import { __, _x, setLocaleData } from '@wordpress/i18n';
9+
import { __, _x, sprintf, setLocaleData } from '@wordpress/i18n';
1010
import { addFilter } from '@wordpress/hooks';
1111
import { drawerLeft, globe } from '@wordpress/icons';
1212
import { Badge, Button as UiButton, Card as UiCard, EmptyState, IconButton, Notice, Spinner as UiSpinner, Stack, Text, VisuallyHidden } from '@wordpress/ui';
@@ -445,7 +445,8 @@ function App() {
445445
const res = await window.api.startPlaygroundWeb(
446446
({ data }) => setWebLogs((v) => v + String(data)),
447447
(url) => { const u = (url || 'http://127.0.0.1:39372/').replace(/\/$/,'/'); setWebUrl(u); setWebStarting(false); },
448-
(payload) => { setWebUrl(''); if (payload && typeof payload.code === 'number' && payload.code !== 0) setWebError(`Server exited with code ${payload.code}`); }
448+
// translators: %d: the code the server exited with, a number.
449+
(payload) => { setWebUrl(''); if (payload && typeof payload.code === 'number' && payload.code !== 0) setWebError(sprintf(__('Server exited with code %d'), payload.code)); }
449450
);
450451
if (res && res.ok && res.url) {
451452
const u = String(res.url).replace(/\/$/,'/');
@@ -549,14 +550,14 @@ function App() {
549550
<Stack direction="row" align="center" justify="flex-end" gap="sm" className="window-notice">
550551
<UiButton
551552
loading={webStarting}
552-
loadingAnnouncement="Starting the Playground web server"
553+
loadingAnnouncement={__('Starting the Playground web server')}
553554
variant={webUrl ? 'outline' : 'solid'}
554555
tone={webUrl ? 'neutral' : 'brand'}
555556
onClick={togglePlaygroundWeb}
556-
>{webUrl ? 'Stop Playground web server' : 'Start Playground web server'}</UiButton>
557+
>{webUrl ? __('Stop Playground web server') : __('Start Playground web server')}</UiButton>
557558
{webStarting || webUrl ? (
558559
<Text variant="body-sm">
559-
{webStarting ? 'Starting…' : (
560+
{webStarting ? __('Starting…') : (
560561
<a href={webUrl || 'http://127.0.0.1:39372/'} onClick={(e) => { e.preventDefault(); window.api.openExternal(webUrl || 'http://127.0.0.1:39372/'); }}>{webUrl || 'http://127.0.0.1:39372/'}</a>
561562
)}
562563
</Text>
@@ -569,13 +570,13 @@ function App() {
569570
<UiCard.Root className="window-notice">
570571
<UiCard.Content render={<Stack direction="column" gap="sm" />}>
571572
<Stack direction="row" align="center" justify="space-between" gap="sm">
572-
<Text variant="heading-md">Playground web server</Text>
573+
<Text variant="heading-md">{__('Playground web server')}</Text>
573574
<Text variant="body-sm" className="muted-label">
574-
{webStarting ? 'Starting…' : null}
575+
{webStarting ? __('Starting…') : null}
575576
{!webStarting && webUrl ? (
576577
<a href={webUrl} onClick={(e)=>{ e.preventDefault(); window.api.openExternal(webUrl); }}>{webUrl}</a>
577578
) : null}
578-
{!webStarting && !webUrl ? 'Stopped' : null}
579+
{!webStarting && !webUrl ? __('Stopped') : null}
579580
</Text>
580581
</Stack>
581582
{webError ? <Text variant="body-sm" className="error-text">{webError}</Text> : null}
@@ -861,13 +862,17 @@ function SiteRow({ sitePath, initialized, createdAt, label, projectType = null,
861862
const writePathToClipboard = useCallback(async () => {
862863
try {
863864
if (!navigator?.clipboard?.writeText) {
864-
throw new Error('Clipboard access is not available in this environment');
865+
throw new Error(__('Clipboard access is not available in this environment'));
865866
}
866867
await navigator.clipboard.writeText(sitePath);
867868
return true;
868869
} catch (err) {
869870
// eslint-disable-next-line no-alert -- see the note above confirmAnd.
870-
alert('Unable to copy path: ' + (err?.message ?? String(err)));
871+
alert(sprintf(
872+
// translators: %s: why the path could not be copied, a sentence.
873+
__('Unable to copy path: %s'),
874+
err?.message ?? String(err)
875+
));
871876
return false;
872877
}
873878
}, [sitePath]);

‎tests/e2e/journeys/i18n.spec.js‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,3 +670,79 @@ for ( const [ named, label ] of [ [ 'by its name', '60001.diff' ], [ 'with no na
670670
expect( await unwrappedInReview( dialog ) ).toEqual( [] );
671671
} );
672672
}
673+
674+
test( 'a mail the site sent is fully translatable, on its Rendered and its Raw tab', async ( { session } ) => {
675+
// One mail in the store, loaded when the server starts, which is answered
676+
// by stand-ins as in mail.spec.js. What the mail says is the site's, and
677+
// is left out of the scan.
678+
const MAIL = {
679+
id: 'e2e-reset',
680+
subject: '[Test Site] Password Reset',
681+
from: 'WordPress <wordpress@example.test>',
682+
to: 'admin@example.test',
683+
cc: 'auditor@example.test',
684+
date: '2026-08-10T09:30:00.000Z',
685+
sentAt: '2026-08-10T09:30:00.000Z',
686+
text: 'A password reset was requested.',
687+
html: '<p>Someone has requested a <strong>password reset</strong>.</p>',
688+
headers: {},
689+
raw: 'Subject: [Test Site] Password Reset\nX-Mailer: PHPMailer\n\nA password reset was requested.',
690+
};
691+
const theMails = ( text ) => [ MAIL.subject, MAIL.from, MAIL.to, MAIL.cc, MAIL.raw, 'Someone has requested a password reset.' ].some( ( part ) => part.includes( text ) ) || text.includes( '2026' );
692+
const site = await makeSite( session );
693+
const { app, page } = await session.start( { ...site.settings, [ `siteMail:${ site.dir }` ]: [ MAIL ] }, { lang: 'en-XA' } );
694+
await app.evaluate( ( { ipcMain } ) => {
695+
ipcMain.removeHandler( 'npm:run-script' );
696+
ipcMain.handle( 'npm:run-script', async () => ( { runId: 'e2e-mail' } ) );
697+
ipcMain.removeHandler( 'playground:start' );
698+
ipcMain.handle( 'playground:start', async () => ( { ok: true } ) );
699+
} );
700+
await page.getByRole( 'button', { name: pseudoLocalize( 'Start development server' ), exact: true } ).click( { timeout: 30_000 } );
701+
await page.getByRole( 'button', { name: pseudoLocalize( 'Toggle Email' ), exact: true } ).click();
702+
await page.getByRole( 'button', { name: /\[Test Site\] Password Reset$/ } ).click( { timeout: 30_000 } );
703+
704+
const dialog = page.getByRole( 'dialog', { name: MAIL.subject } );
705+
await expect( dialog.getByRole( 'tab', { name: pseudoLocalize( 'Rendered' ), exact: true } ) ).toHaveAttribute( 'aria-selected', 'true' );
706+
await expect( dialog.getByText( pseudoLocalize( 'CC:' ), { exact: true } ) ).toBeVisible();
707+
expect( ( await unwrapped( dialog ) ).filter( ( text ) => ! theMails( text ) ) ).toEqual( [] );
708+
709+
await dialog.getByRole( 'tab', { name: pseudoLocalize( 'Raw' ), exact: true } ).click();
710+
await expect( dialog.getByText( 'X-Mailer: PHPMailer' ) ).toBeVisible();
711+
expect( ( await unwrapped( dialog ) ).filter( ( text ) => ! theMails( text ) ) ).toEqual( [] );
712+
} );
713+
714+
test( 'the Playground web server is fully translatable, stopped, starting, running and after it exits', async ( { session } ) => {
715+
// The server is offered only where a build ships it, which a checkout
716+
// does not, and the page asks as it mounts: so the stand-ins are in
717+
// place before the window is. Starting is never answered; the test says
718+
// what the main process would say instead. With no site, the server is
719+
// above the middle of the window, and that is what is scanned.
720+
const URL = 'http://127.0.0.1:39372/';
721+
await session.start( undefined, { lang: 'en-XA' } );
722+
const { app, page } = await session.restart( { beforeWindow: ( launched ) => launched.evaluate( ( { ipcMain } ) => {
723+
ipcMain.removeHandler( 'playground-web:available' );
724+
ipcMain.handle( 'playground-web:available', () => true );
725+
ipcMain.removeHandler( 'playground-web:start' );
726+
ipcMain.handle( 'playground-web:start', () => new Promise( () => {} ) );
727+
} ) } );
728+
const tell = ( channel, payload ) => app.evaluate( ( { BrowserWindow }, [ to, what ] ) => {
729+
BrowserWindow.getAllWindows()[ 0 ].webContents.send( to, what );
730+
}, [ channel, payload ] );
731+
const notices = page.locator( '.page-body-notices' );
732+
const SERVER_OUTPUT = 'listening for requests';
733+
734+
await page.getByRole( 'button', { name: pseudoLocalize( 'Start Playground web server' ), exact: true } ).click( { timeout: 30_000 } );
735+
await expect( page.getByText( pseudoLocalize( 'Playground web server' ), { exact: true } ) ).toBeVisible();
736+
await expect( page.getByText( pseudoLocalize( 'Starting…' ), { exact: true } ).first() ).toBeVisible();
737+
expect( await unwrapped( notices ) ).toEqual( [] );
738+
739+
await tell( 'playground-web:log', { type: 'stdout', data: `${ SERVER_OUTPUT }\n` } );
740+
await tell( 'playground-web:url', { url: URL } );
741+
await expect( page.getByRole( 'button', { name: pseudoLocalize( 'Stop Playground web server' ), exact: true } ) ).toBeVisible();
742+
expect( ( await unwrapped( notices ) ).filter( ( text ) => text !== URL && text !== SERVER_OUTPUT ) ).toEqual( [] );
743+
744+
await tell( 'playground-web:stopped', { code: 1 } );
745+
await expect( page.getByText( pseudoLocalize( 'Stopped' ), { exact: true } ) ).toBeVisible();
746+
await expect( page.getByText( pseudoLocalize( 'Server exited with code %d' ).replace( '%d', '1' ), { exact: true } ) ).toBeVisible();
747+
expect( ( await unwrapped( notices ) ).filter( ( text ) => text !== SERVER_OUTPUT ) ).toEqual( [] );
748+
} );

‎tests/unit/confirmations.test.cjs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,8 @@ test('each thing the apply flow can have done is confirmed in a sentence of its
156156
assert.strictEqual(applyDoneMessage('Restored', 'saved work'), 'Restored the saved work');
157157
});
158158

159-
test('a pair it has no sentence for is still said, as the flow words it', () => {
160-
assert.strictEqual(applyDoneMessage('Rebuilt', 'site'), 'Rebuilt the site');
159+
test('a pair it has no sentence for is still confirmed, in a word that splices nothing in', () => {
160+
assert.strictEqual(applyDoneMessage('Rebuilt', 'site'), 'Done');
161161
});
162162

163163
test('a saved patch and edits saved before an update are confirmed by the file\'s name', () => {

0 commit comments

Comments
 (0)