Merge pull request #259 from Coffey-Labs/i18n-third-pass
Third pass: the strings libraries build and views render raw
This commit is contained in:
+15
-8
@@ -13,6 +13,7 @@
|
||||
*/
|
||||
import { addDays, startOfDay } from "./dates";
|
||||
import { formatFullDateTime } from "./datetime";
|
||||
import { plural, t } from "@/lib/i18n";
|
||||
|
||||
export const SUBMISSION_CAP = "urn:ietf:params:jmap:submission";
|
||||
|
||||
@@ -96,21 +97,27 @@ export function schedulePresets(now: Date, maxMs: number): SchedulePreset[] {
|
||||
* surfaces as a failed send rather than anything the user can act on.
|
||||
*/
|
||||
export function scheduleError(at: Date, now: Date, maxMs: number): string | null {
|
||||
const t = at.getTime();
|
||||
if (Number.isNaN(t)) return "Pick a date and time.";
|
||||
if (t < now.getTime() + MIN_LEAD_MS) return "Pick a time at least a minute from now.";
|
||||
if (maxMs > 0 && t > now.getTime() + maxMs) {
|
||||
return `This server will not hold a message longer than ${describeSpan(maxMs)}.`;
|
||||
const ms = at.getTime();
|
||||
if (Number.isNaN(ms)) return t("Pick a date and time.");
|
||||
if (ms < now.getTime() + MIN_LEAD_MS) return t("Pick a time at least a minute from now.");
|
||||
if (maxMs > 0 && ms > now.getTime() + maxMs) {
|
||||
return t("This server will not hold a message longer than {span}.", { span: describeSpan(maxMs) });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** "30 days", "7 days", "12 hours" -- for explaining the server's own limit. */
|
||||
/**
|
||||
* "30 days", "7 days", "12 hours" -- for explaining the server's own limit.
|
||||
*
|
||||
* plural() rather than `day${n === 1 ? "" : "s"}`: that suffix trick is English
|
||||
* grammar written into the code, and it produces "2 Tage" only by accident of
|
||||
* the two languages agreeing. Russian needs three forms and Japanese one.
|
||||
*/
|
||||
export function describeSpan(ms: number): string {
|
||||
const days = Math.floor(ms / 86_400_000);
|
||||
if (days >= 1) return `${days} day${days === 1 ? "" : "s"}`;
|
||||
if (days >= 1) return plural(days, { one: "{n} day", other: "{n} days" });
|
||||
const hours = Math.max(1, Math.floor(ms / 3_600_000));
|
||||
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
||||
return plural(hours, { one: "{n} hour", other: "{n} hours" });
|
||||
}
|
||||
|
||||
/** How a scheduled time reads in menus, banners and toasts. */
|
||||
|
||||
@@ -466,7 +466,7 @@ export const useCompose = create<ComposeState>((set, get) => ({
|
||||
onProgress: (loaded, total) => patchAtt(key, a.id, { progress: Math.round((loaded / total) * 100) }, set),
|
||||
})
|
||||
.then((res) => patchAtt(key, a.id, { blobId: res.blobId, progress: 100, type: res.type || a.type, size: res.size }, set))
|
||||
.catch((err) => patchAtt(key, a.id, { error: (err as Error).message || "Upload failed" }, set));
|
||||
.catch((err) => patchAtt(key, a.id, { error: (err as Error).message || translate("Upload failed") }, set));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -524,7 +524,7 @@ export const useCompose = create<ComposeState>((set, get) => ({
|
||||
const up = await client.upload(accountId, blob, { type: a.type });
|
||||
patchAtt(key, a.id, { blobId: up.blobId, progress: 100, size: up.size || a.size }, set);
|
||||
} catch (err) {
|
||||
patchAtt(key, a.id, { error: (err as Error).message || "Could not attach" }, set);
|
||||
patchAtt(key, a.id, { error: (err as Error).message || translate("Could not attach") }, set);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -568,7 +568,7 @@ export const useCompose = create<ComposeState>((set, get) => ({
|
||||
toast.success(scheduling ? translate("Send scheduled for {when}", { when: formatScheduleTime(new Date(d.sendAt!)) }) : translate("Message sent"));
|
||||
} catch (err) {
|
||||
toast.error(translate("Send failed: {error}", { error: (err as Error).message }), {
|
||||
action: { label: "Open draft", onClick: () => set((s) => ({ drafts: [...s.drafts, { ...d, sending: false, error: (err as Error).message }], activeKey: d.key })) },
|
||||
action: { label: translate("Open draft"), onClick: () => set((s) => ({ drafts: [...s.drafts, { ...d, sending: false, error: (err as Error).message }], activeKey: d.key })) },
|
||||
duration: 15000,
|
||||
});
|
||||
}
|
||||
@@ -580,7 +580,7 @@ export const useCompose = create<ComposeState>((set, get) => ({
|
||||
await doSend();
|
||||
return;
|
||||
}
|
||||
const toastId = toast.show("Sending…", { duration: delay * 1000, progress: true, action: { label: "Undo", onClick: () => get().undoSend(key) } });
|
||||
const toastId = toast.show(translate("Sending…"), { duration: delay * 1000, progress: true, action: { label: translate("Undo"), onClick: () => get().undoSend(key) } });
|
||||
const timer = window.setTimeout(() => void doSend(), delay * 1000);
|
||||
set((s) => ({ pendingSends: { ...s.pendingSends, [key]: { timer, toastId, draft: d } } }));
|
||||
},
|
||||
@@ -689,7 +689,7 @@ export async function buildEmailObject(d: Draft, opts: { forSend: boolean; mailb
|
||||
const mail = useMail.getState();
|
||||
const accountId = mail.accountId!;
|
||||
const ident = mail.identities.find((i) => i.id === d.identityId) ?? mail.identities[0];
|
||||
if (!ident) throw new Error("No sending identity available");
|
||||
if (!ident) throw new Error(translate("No sending identity available"));
|
||||
const from: EmailAddress = { name: ident.name || null, email: ident.email };
|
||||
|
||||
let html = d.format === "html" ? d.html : "";
|
||||
@@ -869,15 +869,15 @@ async function sendInternal(d: Draft, _get: () => ComposeState): Promise<void> {
|
||||
const mail = useMail.getState();
|
||||
const accountId = mail.accountId!;
|
||||
const ident = mail.identities.find((i) => i.id === d.identityId) ?? mail.identities[0];
|
||||
if (!ident) throw new Error("No sending identity available");
|
||||
if (d.attachments.some((a) => !a.blobId && !a.error)) throw new Error("Attachments are still uploading");
|
||||
if (!ident) throw new Error(translate("No sending identity available"));
|
||||
if (d.attachments.some((a) => !a.blobId && !a.error)) throw new Error(translate("Attachments are still uploading"));
|
||||
const scheduled = d.sendAt !== null && d.sendAt > Date.now();
|
||||
const scheduledId = scheduled ? await ensureScheduledMailbox() : null;
|
||||
const email = await buildEmailObject(d, { forSend: true, mailboxId: scheduledId });
|
||||
const sentId = mail.roleId("sent");
|
||||
const draftsId = mail.roleId("drafts");
|
||||
const rcpts = uniqueAddresses([...d.to, ...d.cc, ...d.bcc]).map((a) => ({ email: a.email }));
|
||||
if (!rcpts.length) throw new Error("No recipients");
|
||||
if (!rcpts.length) throw new Error(translate("No recipients"));
|
||||
const sub = buildSubmission({
|
||||
identityId: ident.id,
|
||||
fromEmail: ident.email,
|
||||
|
||||
@@ -18,7 +18,7 @@ export function ScheduleMenuItems({ maxMs, onPick, onCustom }: { maxMs: number;
|
||||
<MenuSep />
|
||||
<MenuTitle>{t("Schedule send")}</MenuTitle>
|
||||
{presets.map((p) => (
|
||||
<MenuItem key={p.id} icon={<Clock size={16} />} label={p.label} kbd={formatScheduleTime(p.at)} onClick={() => onPick(p.at)} />
|
||||
<MenuItem key={p.id} icon={<Clock size={16} />} label={t(p.label)} kbd={formatScheduleTime(p.at)} onClick={() => onPick(p.at)} />
|
||||
))}
|
||||
<MenuItem icon={<Clock size={16} />} label={t("Pick date and time…")} onClick={onCustom} />
|
||||
</>
|
||||
|
||||
@@ -337,7 +337,7 @@ export const MessageView = memo(function MessageView({ email: e, expanded, wasUn
|
||||
{e["header:List-Id:asText"] && <><dt>{translate("List")}</dt><dd>{e["header:List-Id:asText"]}</dd></>}
|
||||
<dt>{translate("Size")}</dt><dd>{formatSize(e.size)}</dd>
|
||||
{spam && <><dt>{translate("Spam filter")}</dt><dd><SpamSummary report={spam} /></dd></>}
|
||||
{receiptRequested && <><dt>{translate("Receipt")}</dt><dd>{receipt.offer ? `Requested, to ${receipt.to!.email}. Never sent automatically.` : refusalText(receipt.refusal!)}</dd></>}
|
||||
{receiptRequested && <><dt>{translate("Receipt")}</dt><dd>{receipt.offer ? translate("Requested, to {address}. Never sent automatically.", { address: receipt.to!.email }) : translate(refusalText(receipt.refusal!))}</dd></>}
|
||||
</dl>
|
||||
)}
|
||||
{receipt.offer && settings.readReceiptPolicy !== "never" && receiptDone !== "dismissed" && (
|
||||
|
||||
Reference in New Issue
Block a user