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
11 changes: 10 additions & 1 deletion src/editor-launch.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
const path = require('path');
const { describeRefused } = require('./safe-log');
const { isActionableSite } = require('./site-registry');
const { __, sprintf } = require('@wordpress/i18n');

// Path semantics follow the platform being asked about, not the platform the
// test happens to run on: `path.isAbsolute('C:\\x')` is false under POSIX, and a
Expand Down Expand Up @@ -385,7 +386,15 @@ function awaitLaunch(child, { platform } = {}) {
child.on('close', (code) => {
settle(code === 0
? { ok: true }
: { ok: false, reason: REFUSAL_REASONS.SPAWN_FAILED, error: `the editor could not be opened (exit code ${code})` });
: {
ok: false,
reason: REFUSAL_REASONS.SPAWN_FAILED,
error: sprintf(
// translators: %s: the exit code of the command that opens the editor, such as 1. Follows "The application would not start:", so it starts in lower case.
__('the editor could not be opened (exit code %s)'),
code
)
});
});
return;
}
Expand Down
12 changes: 6 additions & 6 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const DEEP_LINK_CHANNEL = 'deep-link:ticket';
// Trac parser is reached through it rather than directly, so no handler
// here has to know which kind it is holding.
const { workItemProvider } = require('./work-item.cjs');
const { LEGACY_SITE_ERROR } = require('./renderer/legacy-site.cjs');
const { legacySiteError } = require('./renderer/legacy-site.cjs');
const { resolveCatalog, languageChoices } = require('./i18n.cjs');
const { isPseudoLocale } = require('./renderer/pseudo-locale.cjs');
const { applyLocale } = require('./renderer/locale-setup.cjs');
Expand Down Expand Up @@ -1586,7 +1586,7 @@ async function noOriginBlock(sitePath) {

async function legacySiteBlock(sitePath) {
if (!await isLegacySite(sitePath)) return null;
return { ok: false, code: 'legacy-site', error: LEGACY_SITE_ERROR };
return { ok: false, code: 'legacy-site', error: legacySiteError() };
}

/**
Expand Down Expand Up @@ -3254,7 +3254,7 @@ ipcMain.handle('sites:set-ticket', async (event, sitePath, ref, options) => with
// own work to restore, so loose edits cannot ride into it.
return {
ok: false,
error: 'There is uncommitted work on trunk — decide what happens to it before starting the ticket',
error: __('There is uncommitted work on trunk — decide what happens to it before starting the ticket'),
code: 'dirty-trunk',
canCarry: true,
files,
Expand Down Expand Up @@ -3348,11 +3348,11 @@ ipcMain.handle('branches:rebase', async (event, sitePath) => withRegisteredSite(
if (blocked) return blocked;
const { ref, meta } = await activeBranch(sitePath, { migrate: true });
if (ref === TRUNK) {
return { ok: false, code: 'on-trunk', error: 'Link a ticket first: trunk is what tickets are measured against, not a ticket.' };
return { ok: false, code: 'on-trunk', error: __('Link a ticket first: trunk is what tickets are measured against, not a ticket.') };
}
if (ticketIdFromRef(ref) === null) return { ok: false, code: 'not-a-ticket-branch', error: 'Only a ticket branch can be moved onto the current trunk.' };
if (ticketIdFromRef(ref) === null) return { ok: false, code: 'not-a-ticket-branch', error: __('Only a ticket branch can be moved onto the current trunk.') };
if (!meta || !meta.baseOid) {
return { ok: false, code: 'no-base', error: 'This ticket has no recorded starting point, so the app cannot move its work onto the current trunk.' };
return { ok: false, code: 'no-base', error: __('This ticket has no recorded starting point, so the app cannot move its work onto the current trunk.') };
}
const progress = switchProgressReporter(event, sitePath);
let result;
Expand Down
156 changes: 121 additions & 35 deletions src/renderer/changes-note.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
// to nothing is news for the buttons that would give it somewhere to go.
'use strict';

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

// Byte-identical to the confirm the dirty-update modal has always used, so
// the same action reads the same everywhere it can be triggered.
const DISCARD_CONFIRM_MESSAGE = 'Discard all local changes? This cannot be undone.';
function discardConfirmMessage() {
return __('Discard all local changes? This cannot be undone.');
}

// The branch namespaces a work item gets (#251): `ticket/` on a Core site,
// `issue/` on a Gutenberg one. Spelled out here rather than imported because
Expand All @@ -26,62 +28,139 @@ const DISCARD_CONFIRM_MESSAGE = 'Discard all local changes? This cannot be undon
const WORK_ITEM_BRANCH = /^(?:ticket|issue)\//;

/**
* The changes note, split into parts the component interleaves with its two
* link buttons, or null when there is nothing to say.
* The changes note, as one sentence with its two links marked in it, or null
* when there is nothing to say. `<review>` is the link that opens the review,
* `<discard>` the one that discards; the component fills them in with
* `createInterpolateElement`, so each sentence is whole for a translator.
*
* `changedCount` can be missing: the dirty probe may have answered before a
* count existed, and "You have changes" is still true then.
*
* The action labels move with the placement. By the buttons the modal has
* not been named yet, so the link says what it produces — a patch. In the
* The review link's words move with the placement. By the buttons the modal
* has not been named yet, so the link says what it produces — a patch. In the
* ticket card the sentence already says where the changes are going, so the
* link borrows the modal's own name, "review and submit". The ticket card
* also carries a reassurance the buttons never need: Unlink sits right
* above, and the changes must not look like they hang on it.
*
* `workItemNoun` is what the site calls its work item (#251), `ticket` unless
* told otherwise; every sentence that names it reads the noun from there.
* told otherwise; every sentence that names it is written once per kind.
*
* @param {{dirty?: boolean, changedCount?: number, tracTicket?: *, pullRequest?: Object, workItemNoun?: string}} state
* @return {{placement: 'buttons'|'ticket', lead: string, patchLabel: string,
* middle: string, discardLabel: string, end: string,
* unlinkNote?: string}|null}
* @return {{placement: 'buttons'|'ticket', sentence: string, unlinkNote?: string}|null}
*/
function changesNoteParts({ dirty, changedCount, tracTicket, pullRequest, workItemNoun = 'ticket' } = {}) {
if (!dirty) return null;
const count = Number.isInteger(changedCount) && changedCount > 0 ? changedCount : null;
const noun = count === 1 ? 'change' : 'changes';
const issue = workItemNoun === 'issue';
if (pullRequest && Number.isInteger(pullRequest.number)) {
const hasReturnDestination = typeof pullRequest.returnTo === 'string' && pullRequest.returnTo.length > 0;
const returnsToTicket = hasReturnDestination ? WORK_ITEM_BRANCH.test(pullRequest.returnTo) : Boolean(tracTicket);
return {
placement: returnsToTicket ? 'ticket' : 'buttons',
lead: `You have ${count === null ? '' : `${count} `}${noun} on top of PR #${pullRequest.number}. You can `,
patchLabel: 'review them',
middle: ' or ',
discardLabel: 'discard your changes',
end: `. They stay with this pull request's local copy when you revert this PR.`
};
return { placement: returnsToTicket ? 'ticket' : 'buttons', sentence: pullRequestChangesSentence({ count, number: pullRequest.number }) };
}
if (tracTicket) {
return {
placement: 'ticket',
lead: `You have ${count === null ? '' : `${count} `}unsubmitted ${noun} for ${workItemNoun} #${tracTicket}. You can `,
patchLabel: 'review and submit',
middle: ' or ',
discardLabel: 'discard your changes',
end: '.',
unlinkNote: `Unlinking this ${workItemNoun} doesn\'t affect your local changes for this ${workItemNoun} — they remain attached to it in this site, ready for when you link it again.`
sentence: workItemChangesSentence({ count, issue, number: tracTicket }),
unlinkNote: issue
? __("Unlinking this issue doesn't affect your local changes for this issue — they remain attached to it in this site, ready for when you link it again.")
: __("Unlinking this ticket doesn't affect your local changes for this ticket — they remain attached to it in this site, ready for when you link it again.")
};
}
return {
placement: 'buttons',
lead: `You have ${count === null ? '' : `${count} `}${noun} not assigned to any ${workItemNoun}. You can `,
patchLabel: 'create and save a patch',
middle: ' or ',
discardLabel: 'discard your changes',
end: '.'
};
return { placement: 'buttons', sentence: unassignedChangesSentence({ count, issue }) };
}

// The note's sentence for changes on top of a checked-out pull request.
function pullRequestChangesSentence({ count, number }) {
if (count === null) {
return sprintf(
// translators: %d: a pull request number. <review> and <discard> are links.
__("You have changes on top of PR #%d. You can <review>review them</review> or <discard>discard your changes</discard>. They stay with this pull request's local copy when you revert this PR."),
number
);
}
return sprintf(
// translators: 1: how many files have changes. 2: a pull request number. <review> and <discard> are links.
_n(
"You have %1$d change on top of PR #%2$d. You can <review>review them</review> or <discard>discard your changes</discard>. They stay with this pull request's local copy when you revert this PR.",
"You have %1$d changes on top of PR #%2$d. You can <review>review them</review> or <discard>discard your changes</discard>. They stay with this pull request's local copy when you revert this PR.",
count
),
count,
number
);
}

// The note's sentence for changes on a linked ticket or issue.
function workItemChangesSentence({ count, issue, number }) {
if (count === null && issue) {
return sprintf(
// translators: %s: a GitHub issue number. <review> and <discard> are links.
__('You have unsubmitted changes for issue #%s. You can <review>review and submit</review> or <discard>discard your changes</discard>.'),
number
);
}
if (count === null) {
return sprintf(
// translators: %s: a Trac ticket number. <review> and <discard> are links.
__('You have unsubmitted changes for ticket #%s. You can <review>review and submit</review> or <discard>discard your changes</discard>.'),
number
);
}
if (issue) {
return sprintf(
// translators: 1: how many files have changes. 2: a GitHub issue number. <review> and <discard> are links.
_n(
'You have %1$d unsubmitted change for issue #%2$s. You can <review>review and submit</review> or <discard>discard your changes</discard>.',
'You have %1$d unsubmitted changes for issue #%2$s. You can <review>review and submit</review> or <discard>discard your changes</discard>.',
count
),
count,
number
);
}
return sprintf(
// translators: 1: how many files have changes. 2: a Trac ticket number. <review> and <discard> are links.
_n(
'You have %1$d unsubmitted change for ticket #%2$s. You can <review>review and submit</review> or <discard>discard your changes</discard>.',
'You have %1$d unsubmitted changes for ticket #%2$s. You can <review>review and submit</review> or <discard>discard your changes</discard>.',
count
),
count,
number
);
}

// The note's sentence for changes that belong to nothing yet.
function unassignedChangesSentence({ count, issue }) {
if (count === null && issue) {
// translators: <review> and <discard> are links.
return __('You have changes not assigned to any issue. You can <review>create and save a patch</review> or <discard>discard your changes</discard>.');
}
if (count === null) {
// translators: <review> and <discard> are links.
return __('You have changes not assigned to any ticket. You can <review>create and save a patch</review> or <discard>discard your changes</discard>.');
}
if (issue) {
return sprintf(
// translators: %d: how many files have changes. <review> and <discard> are links.
_n(
'You have %d change not assigned to any issue. You can <review>create and save a patch</review> or <discard>discard your changes</discard>.',
'You have %d changes not assigned to any issue. You can <review>create and save a patch</review> or <discard>discard your changes</discard>.',
count
),
count
);
}
return sprintf(
// translators: %d: how many files have changes. <review> and <discard> are links.
_n(
'You have %d change not assigned to any ticket. You can <review>create and save a patch</review> or <discard>discard your changes</discard>.',
'You have %d changes not assigned to any ticket. You can <review>create and save a patch</review> or <discard>discard your changes</discard>.',
count
),
count
);
}

/**
Expand All @@ -104,7 +183,14 @@ function discardOutcome(res) {
? { ok: true, dirty: res.dirty, changedCount: res.changedCount }
: { ok: true };
}
return { ok: false, message: `Failed to discard changes: ${res && res.error ? res.error : 'Unknown error'}` };
return {
ok: false,
message: sprintf(
// translators: %s: why the discard failed, usually Git's own message in English.
__('Failed to discard changes: %s'),
res && res.error ? res.error : __('Unknown error')
)
};
}

/**
Expand Down Expand Up @@ -224,4 +310,4 @@ function patchReviewContext({ pullRequest, tracTicket, workItemNoun = 'ticket' }
};
}

module.exports = { patchReviewContext, changesNoteParts, discardOutcome, applyFeedbackAfterDiscard, noteAfterDiscard, noteAfterProbe, discardBlocked, discardDisabledReason, DISCARD_CONFIRM_MESSAGE };
module.exports = { patchReviewContext, changesNoteParts, discardOutcome, applyFeedbackAfterDiscard, noteAfterDiscard, noteAfterProbe, discardBlocked, discardDisabledReason, discardConfirmMessage };
7 changes: 4 additions & 3 deletions src/renderer/components/discard-changes-link.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { ReasonedButton } from './reasoned-button.jsx';

// One discard action, wherever it is offered. Keeping the disabled rendering
// here means the ticket note cannot lose the explanation while the review
// modal keeps it (or vice versa).
export function DiscardChangesLink({ label, onClick, reason }) {
return <ReasonedButton variant="link" isDestructive onClick={onClick} reason={reason}>{label}</ReasonedButton>;
// modal keeps it (or vice versa). Its words are `label`, or, inside a
// sentence, the ones the sentence marks for it.
export function DiscardChangesLink({ label, children, onClick, reason }) {
return <ReasonedButton variant="link" isDestructive onClick={onClick} reason={reason}>{children ?? label}</ReasonedButton>;
}
30 changes: 23 additions & 7 deletions src/renderer/deep-link-notice.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
// turns an address someone else chose into something the contributor did.
'use strict';

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

/**
* The banner for a ticket that arrived from a link, or null when none has.
*
Expand Down Expand Up @@ -56,25 +58,39 @@ function deepLinkNotice({ ticket = null, siteLabel = '', currentTicket = null, p
if (siteLabel && provider !== 'trac') {
return {
state: 'unsupported',
title: `Ticket #${ticket} cannot be linked to ${siteLabel}.`,
body: 'A Trac ticket belongs on a WordPress Core site. Open one and the app will offer the ticket there.',
title: sprintf(
// translators: 1: a Trac ticket number. 2: the name of the site that is open.
__('Ticket #%1$s cannot be linked to %2$s.'),
ticket,
siteLabel
),
body: __('A Trac ticket belongs on a WordPress Core site. Open one and the app will offer the ticket there.'),
confirmLabel: null
};
}

if (siteLabel) {
return {
state: 'confirm',
title: `Link ticket #${ticket} to ${siteLabel}?`,
body: 'The ticket number came from a link. Linking it parks whatever the site is on now and checks out this ticket’s branch, so the app asks first.',
confirmLabel: 'Link ticket'
title: sprintf(
// translators: 1: a Trac ticket number. 2: the name of the site that is open.
__('Link ticket #%1$s to %2$s?'),
ticket,
siteLabel
),
body: __('The ticket number came from a link. Linking it parks whatever the site is on now and checks out this ticket’s branch, so the app asks first.'),
confirmLabel: __('Link ticket')
};
}

return {
state: 'no-sites',
title: `Ticket #${ticket} is ready to link.`,
body: 'There is no site to work on it in yet. Create one from the sidebar and the app will offer this ticket when it is ready.',
title: sprintf(
// translators: %s: a Trac ticket number.
__('Ticket #%s is ready to link.'),
ticket
),
body: __('There is no site to work on it in yet. Create one from the sidebar and the app will offer this ticket when it is ready.'),
confirmLabel: null
};
}
Expand Down
Loading
Loading