Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1744,13 +1744,17 @@ async function appliedPatchSubmissionRefusal(sitePath) {
const appliedPatch = (ref === TRUNK || !meta ? site : meta).appliedPatch;
if (!appliedPatch) return null;

const label = typeof appliedPatch.label === 'string' && appliedPatch.label.trim()
? appliedPatch.label.trim()
: 'The patch you applied';
const label = typeof appliedPatch.label === 'string' ? appliedPatch.label.trim() : '';
return {
ok: false,
reason: 'applied-patch',
error: `${label} is applied. Revert it before submitting this checkout as your own work.`
error: label
? sprintf(
// translators: %s: the name of the patch or pull request, such as 60001.diff or PR #6717.
__('%s is applied. Revert it before submitting this checkout as your own work.'),
label
)
: __('The patch you applied is applied. Revert it before submitting this checkout as your own work.')
};
}

Expand Down Expand Up @@ -4157,7 +4161,8 @@ ipcMain.handle('playground-web:start', async () => {
];
const webDir = webDirCandidates.find((p) => { try { return fs.existsSync(p); } catch { return false; } });
if (!webDir) {
return { ok: false, error: 'local-playground-web directory not found.' };
// translators: %s: the name of the folder the web server serves, local-playground-web.
return { ok: false, error: sprintf(__('%s directory not found.'), 'local-playground-web') };
}

const runnerPath = path.join(__dirname, 'playground-web-runner.js');
Expand Down Expand Up @@ -4205,7 +4210,7 @@ ipcMain.handle('playground-web:start', async () => {
if (stillPending) {
clearTimeout(timeoutId);
if (probeIntervalId) clearInterval(probeIntervalId);
try { pendingResolve({ ok: false, error: 'Server exited before becoming ready' }); } catch {}
try { pendingResolve({ ok: false, error: __('Server exited before becoming ready') }); } catch {}
pendingResolve = null;
}
broadcastToAll('playground-web:stopped', { code });
Expand Down Expand Up @@ -4237,7 +4242,7 @@ ipcMain.handle('playground-web:start', async () => {
probeIntervalId = setInterval(probe, 600);
timeoutId = setTimeout(() => {
if (!resolved && typeof pendingResolve === 'function') {
pendingResolve({ ok: false, error: 'Timed out starting web server' });
pendingResolve({ ok: false, error: __('Timed out starting web server') });
pendingResolve = null;
}
if (probeIntervalId) clearInterval(probeIntervalId);
Expand Down
9 changes: 3 additions & 6 deletions src/project-type.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,9 @@ const PROJECT_TYPES = {
// The local branch a work item gets its own namespace under. Core's
// is `ticket/`, unchanged since #108, so no existing site moves.
branchPrefix: 'ticket/',
// What the app calls it outside its card, in the next step's sentence
// and in the refusal to open a pull request with none linked, and
// where a newcomer goes to find one. The card's own words are in
// renderer/ticket-card.cjs.
label: 'Trac ticket',
// Where a newcomer goes to find one. What the app calls it is in
// renderer/ticket-card.cjs and the sentences that name it, one per
// kind of work item, so that each is translated whole.
browseUrl: 'https://core.trac.wordpress.org/tickets/good-first-bugs'
},

Expand Down Expand Up @@ -244,7 +242,6 @@ const PROJECT_TYPES = {
// their own client sees the noun the upstream uses, and a site
// cannot end up with two namespaces meaning the same thing.
branchPrefix: 'issue/',
label: 'GitHub issue',
browseUrl: 'https://github.com/WordPress/gutenberg/issues?q=is%3Aissue+is%3Aopen+label%3A%22Good+First+Issue%22'
},

Expand Down
17 changes: 12 additions & 5 deletions src/renderer/apply-card.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// banner's and the steps' words are still English until their own batches.
'use strict';

const { __, _n, sprintf } = require('@wordpress/i18n');
const { __, _n, _x, sprintf } = require('@wordpress/i18n');

/**
* The card's own words. A project that takes patch files (Core) is offered
Expand Down Expand Up @@ -177,9 +177,12 @@ function applyStepRows(steps = [], states = []) {
* the breakdown's headline says the same as the error in counts, which is the
* part that decides whether the patch is worth rescuing, so it replaces the
* sentence; without one the sentence is the whole story, and is given the
* full stop it may lack. A patch file is applied all or nothing, so its
* failure also says that nothing was changed; a pull request's does not,
* since a checkout that fails part-way can leave the site on another branch.
* full stop it may lack: Git's own line and a thrown error have none. The
* full stop is the translator's, since not every language ends a sentence
* with one, and a sentence already ended in any script's mark keeps it. A
* patch file is applied all or nothing, so its failure also says that nothing
* was changed; a pull request's does not, since a checkout that fails part-way
* can leave the site on another branch.
*
* @param {Object} root0
* @param {string} [root0.error] What went wrong, as it was reported.
Expand All @@ -189,7 +192,11 @@ function applyStepRows(steps = [], states = []) {
*/
function applyFailureWords({ error = '', conflict = null, kind = '' } = {}) {
const sentence = String(error || '').trim();
const headline = (conflict && conflict.headline) || (/[.!?]$/.test(sentence) || !sentence ? sentence : `${sentence}.`);
const headline = (conflict && conflict.headline) || (/[\p{Sentence_Terminal}…]$/u.test(sentence) || !sentence ? sentence : sprintf(
// translators: %s: a sentence that did not end in a full stop. Use the mark your language ends a sentence with, or none.
_x('%s.', 'ends a sentence that lacks a full stop'),
sentence
));
return {
headline,
untouched: kind === 'patch' ? __('The checkout was not changed.') : ''
Expand Down
5 changes: 5 additions & 0 deletions src/renderer/hooks/use-site-terminal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,11 @@ export function useSiteTerminal({ allowedScripts, runInstall, runScript, killCur
}, [handleTerminalData]);

useEffect(() => {
// xterm's own words, read by a screen reader: its input's label, and what
// it says when output comes too fast to read out. Set here, once the
// locale has arrived, and before the terminal draws its input.
Terminal.strings.promptLabel = __('Terminal input');
Terminal.strings.tooMuchOutput = __('Too much output to announce, navigate to rows manually to read');
const term = new Terminal({
rows: 12,
cursorBlink: true,
Expand Down
8 changes: 3 additions & 5 deletions src/work-item.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@
* had to change to gain the other.
*
* The split with project-type.cjs: the registry holds what is true of one
* target (`workItem.label`, which the refusal to open a pull request with
* none linked names it by, and `browseUrl`, where
* a newcomer finds one), this module holds what is true of
* one kind of work item (how to parse a reference, where it lives, whether it
* takes attachments). A second Trac-based target would reuse this provider
* target (`browseUrl`, where a newcomer finds one), this module holds what is
* true of one kind of work item (how to parse a reference, where it lives,
* whether it takes attachments). A second Trac-based target would reuse this provider
* and bring its own registry entry. What the work-item card itself says is in
* renderer/ticket-card.cjs, by kind of work item, since #557: its sentences
* are translated whole, which a label handed in from here could not be.
Expand Down
12 changes: 6 additions & 6 deletions tests/e2e/journeys/i18n.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -955,8 +955,8 @@ test( 'the Terminal is fully translatable: what it prints, a command it does not
expect( await printed() ).toEqual( [] );

// The help asked for, and a command it does not know. The terminal's
// input is named by xterm, which is not ours to translate.
const input = page.getByRole( 'textbox', { name: 'Terminal input' } );
// input is named by xterm, in the words the app gives it.
const input = page.getByRole( 'textbox', { name: pseudoLocalize( 'Terminal input' ), exact: true } );
await input.pressSequentially( 'help', { delay: 10 } );
await input.press( 'Enter' );
await input.pressSequentially( 'frobnicate', { delay: 10 } );
Expand Down Expand Up @@ -1072,14 +1072,14 @@ test( 'the apply card is fully translatable when a patch or a pull request will

// A pull request address from somewhere else, refused before anything is
// asked of GitHub. The card gives a sentence a full stop when it ends
// without one, which a bracketed one always does: that stop is the card's
// (#630 drops it), and the sentence before it is checked whole.
// without one, which a bracketed one always does, and that full stop is
// translated too (#630): the whole line is one translated string.
await card.getByRole( 'tab', { name: pseudoLocalize( 'Pull request' ), exact: true } ).click();
await card.getByLabel( pseudoLocalize( 'Pull request URL or number' ), { exact: true } ).fill( 'https://gitlab.com/WordPress/wordpress-develop/pull/7' );
await card.getByRole( 'button', { name: pseudoLocalize( 'Apply PR' ), exact: true } ).click();
const elsewhere = `${ pseudoLocalize( 'Only github.com pull requests are supported.' ) }.`;
const elsewhere = pseudoLocalize( '%s.' ).replace( '%s', pseudoLocalize( 'Only github.com pull requests are supported.' ) );
await expect( card.getByText( elsewhere, { exact: true } ) ).toBeVisible();
expect( ( await inCard() ).filter( ( text ) => text !== elsewhere ) ).toEqual( [] );
expect( await inCard() ).toEqual( [] );

// A pull request's preview, read from a stand-in for its fetch: what it
// changes, and that dependencies will be installed.
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/apply-card.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,16 @@ test('a failed patch file says what went wrong, ended as a sentence, and that no
assert.equal(applyFailureWords({ error: ' Why? ', kind: 'patch' }).headline, 'Why?');
});

test('the full stop a sentence is given is the translator\'s, and one ended in another script\'s mark keeps it', (t) => {
const i18n = require('@wordpress/i18n');
t.after(() => i18n.resetLocaleData());
i18n.setLocaleData({ 'ends a sentence that lacks a full stop\u0004%s.': ['%s。'] });
assert.equal(applyFailureWords({ error: 'git: patch does not apply', kind: 'patch' }).headline, 'git: patch does not apply。');
assert.equal(applyFailureWords({ error: 'パッチを適用できません。', kind: 'patch' }).headline, 'パッチを適用できません。');
assert.equal(applyFailureWords({ error: 'هل تريد المتابعة؟', kind: 'pr' }).headline, 'هل تريد المتابعة؟');
assert.equal(applyFailureWords({ error: 'पैच लागू नहीं हुआ।', kind: 'patch' }).headline, 'पैच लागू नहीं हुआ।');
});

test('a breakdown\'s headline replaces the sentence it says in counts', () => {
assert.equal(applyFailureWords({ error: 'The patch does not fit', conflict: { headline: '2 of 5 changes could not be applied.' }, kind: 'patch' }).headline, '2 of 5 changes could not be applied.');
// A breakdown with no headline of its own leaves the sentence.
Expand Down
51 changes: 51 additions & 0 deletions tests/unit/ipc-wiring.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1742,6 +1742,28 @@ test('git:save-patch refuses submission destinations while a patch is applied, b
assert.equal(main.calls.showSaveDialog.length, 1, 'an unattributed backup remains available');
});

// The refusal is shown in the patch pane as it is, so it is said whole in the
// locale main applied, with the patch's name put into it, or a sentence of its
// own for a patch with no name on record (#630).
test('git:save-patch words its refusal in the locale main applied, with and without the patch\'s name (#630)', async (t) => {
const { addFilter, removeFilter } = require('@wordpress/hooks');
const { pseudoLocalize } = require('../../src/renderer/pseudo-locale.cjs');
addFilter('i18n.gettext', 'test/pseudo-locale', (text) => pseudoLocalize(text));
t.after(() => removeFilter('i18n.gettext', 'test/pseudo-locale'));
const said = [];
for (const appliedPatch of [{ label: 'PR #6717', text: 'STORED' }, { text: 'STORED' }]) {
const dir = await fixtureRepo(t);
const settings = fakeSettingsStore({ sites: [dir], siteMeta: { [dir]: { tracTicket: 62281, appliedPatch } } });
const main = loadMain({ stubs: { ...silentLogging(), ...settings.stubs } });
said.push((await main.invoke('git:save-patch', dir, { handoff: true })).error);
}

assert.deepEqual(said, [
pseudoLocalize('%s is applied. Revert it before submitting this checkout as your own work.').replace('%s', 'PR #6717'),
pseudoLocalize('The patch you applied is applied. Revert it before submitting this checkout as your own work.')
]);
});

// --- provenance:* -> src/wporg-handle.cjs + src/patch-provenance.cjs (#166) ---

// The handle becomes a filename and a line in a file other people read, so it
Expand Down Expand Up @@ -3032,6 +3054,35 @@ test('playground:start words a server that died before its URL in the locale mai
assert.equal(res.error, pseudoLocalize('Server exited with code %1$s (signal %2$s) before reporting a URL').replace('%1$s', '1').replace('%2$s', 'SIGTERM'));
});

// Where the web server cannot start, the window shows main's sentence as it
// is, so it is said in the locale main applied: with no folder to serve, and
// with a server that exits before it is ready (#630).
test('playground-web:start words its failures in the locale main applied (#630)', async (t) => {
const { addFilter, removeFilter } = require('@wordpress/hooks');
const { pseudoLocalize } = require('../../src/renderer/pseudo-locale.cjs');
addFilter('i18n.gettext', 'test/pseudo-locale', (text) => pseudoLocalize(text));
t.after(() => removeFilter('i18n.gettext', 'test/pseudo-locale'));

const noFolder = loadMain({ stubs: { ...silentLogging(), ...noWebProbe(), 'fs': { existsSync: () => false } } });
const missing = await noFolder.invoke('playground-web:start');
assert.deepEqual(missing, { ok: false, error: pseudoLocalize('%s directory not found.').replace('%s', 'local-playground-web') });

const cp = stubbedSpawn();
const main = loadMain({
stubs: {
...silentLogging(),
...noWebProbe(),
...webDirOnDisk(),
'child_process': { spawn: cp.spawn },
'./kill-tree': { killChildTree: () => {} }
}
});
const pending = main.invoke('playground-web:start');
await waitForSpawnCount(cp, 1);
cp.children[0].emit('close', 1, null);
assert.deepEqual(await pending, { ok: false, error: pseudoLocalize('Server exited before becoming ready') });
});

test('playground-web:stop ends the web server tree rather than signalling the child', async (t) => {
const cp = stubbedSpawn();
const killChildTree = spy();
Expand Down
Loading