Fix experimental update channel and toast

This commit is contained in:
webadderall
2026-08-31 18:50:39 +10:00
parent 27f683c592
commit ffbc0adde9
33 changed files with 695 additions and 463 deletions
+2 -3
View File
@@ -15,7 +15,6 @@ import { loadAllCustomFonts } from "./lib/customFonts";
export default function App() {
const [windowType, setWindowType] = useState("");
const { t } = useI18n();
const isMacOS = /mac/i.test(navigator.platform);
const appIconSrc = "/app-icons/recordly-128.png";
useEffect(() => {
@@ -28,7 +27,7 @@ export default function App() {
type === "hud-overlay" ||
type === "source-selector" ||
type === "countdown" ||
(type === "update-toast" && isMacOS)
type === "update-toast"
) {
document.body.style.background = "transparent";
document.documentElement.style.background = "transparent";
@@ -49,7 +48,7 @@ export default function App() {
loadAllCustomFonts().catch((error) => {
console.error("Failed to load custom fonts:", error);
});
}, [isMacOS]);
}, []);
useEffect(() => {
document.title =
@@ -0,0 +1,174 @@
.window {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 8px;
box-sizing: border-box;
background: transparent;
}
.card {
display: flex;
align-items: flex-start;
gap: 12px;
width: 100%;
padding: 14px;
border: 1px solid hsl(var(--border));
border-radius: 8px;
background: hsl(var(--background));
box-shadow: none;
color: hsl(var(--foreground));
}
.icon {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 32px;
width: 32px;
height: 32px;
border-radius: 7px;
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
.iconError {
background: hsl(var(--destructive) / 0.14);
color: hsl(var(--destructive));
}
.content {
min-width: 0;
flex: 1;
}
.headingRow {
display: flex;
align-items: center;
gap: 6px;
min-height: 19px;
}
.headingRow h1 {
margin: 0;
font-size: 13px;
font-weight: 600;
line-height: 1.35;
letter-spacing: 0;
}
.content > p {
margin: 4px 0 0;
color: hsl(var(--muted-foreground));
font-size: 12px;
line-height: 1.45;
}
.version,
.preview {
padding: 2px 6px;
border-radius: 999px;
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
font-size: 10px;
font-weight: 600;
line-height: 1;
}
.preview {
background: hsl(var(--primary) / 0.12);
color: hsl(var(--primary));
text-transform: uppercase;
letter-spacing: 0.04em;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 12px;
}
.primaryButton,
.secondaryButton {
height: 30px;
padding: 0 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition:
background 140ms ease,
border-color 140ms ease,
transform 140ms ease;
}
.primaryButton:active,
.secondaryButton:active {
transform: translateY(1px);
}
.primaryButton {
border: 1px solid hsl(var(--primary));
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.primaryButton:hover {
background: hsl(var(--primary) / 0.9);
}
.secondaryButton {
border: 1px solid hsl(var(--input));
background: hsl(var(--background));
color: hsl(var(--muted-foreground));
}
.secondaryButton:hover {
background: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
.progressBlock {
margin-top: 12px;
}
.progressTrack {
height: 5px;
overflow: hidden;
border-radius: 999px;
background: hsl(var(--muted));
}
.progressFill {
height: 100%;
border-radius: inherit;
background: hsl(var(--primary));
transition: width 220ms ease;
}
.progressMeta {
display: flex;
justify-content: space-between;
gap: 12px;
margin-top: 6px;
color: hsl(var(--muted-foreground));
font-size: 10px;
}
.progressMeta strong {
color: hsl(var(--foreground));
font-weight: 600;
}
.spin {
animation: spin 900ms linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
+103 -300
View File
@@ -1,10 +1,12 @@
import {
WarningCircle as AlertCircle,
DownloadSimple as Download,
Spinner as LoaderCircle,
Rocket,
ArrowClockwiseIcon,
CheckCircleIcon,
DownloadSimpleIcon,
WarningCircleIcon,
} from "@phosphor-icons/react";
import { useEffect, useState } from "react";
import { useI18n } from "@/contexts/I18nContext";
import styles from "./UpdateToastWindow.module.css";
type UpdateToastPayload = {
version: string;
@@ -12,385 +14,186 @@ type UpdateToastPayload = {
phase: "available" | "downloading" | "ready" | "error";
delayMs: number;
isPreview?: boolean;
isExperimental?: boolean;
progressPercent?: number;
transferredBytes?: number;
totalBytes?: number;
remainingBytes?: number;
bytesPerSecond?: number;
primaryAction?: "install-and-restart" | "retry-check";
};
const DEFAULT_REMINDER_DELAY_MS = 3 * 60 * 60 * 1000;
const REMINDER_OPTIONS = [
{ label: "1 hour", value: 1 * 60 * 60 * 1000 },
{ label: "3 hours", value: 3 * 60 * 60 * 1000 },
{ label: "Tomorrow", value: 24 * 60 * 60 * 1000 },
{ label: "3 days", value: 3 * 24 * 60 * 60 * 1000 },
];
function formatBytes(value: number | undefined) {
if (value === undefined || !Number.isFinite(value) || value <= 0) {
return null;
}
const megabytes = value / (1024 * 1024);
if (megabytes >= 1024) {
return `${(megabytes / 1024).toFixed(1)} GB`;
}
return `${megabytes.toFixed(megabytes >= 100 ? 0 : 1)} MB`;
return megabytes >= 1024
? `${(megabytes / 1024).toFixed(1)} GB`
: `${megabytes.toFixed(megabytes >= 100 ? 0 : 1)} MB`;
}
function getToastTitle(payload: UpdateToastPayload) {
if (payload.isPreview) {
return "Update Prompt Preview";
}
type Translate = ReturnType<typeof useI18n>["t"];
function getTitle(payload: UpdateToastPayload, t: Translate) {
switch (payload.phase) {
case "available":
return `Recordly ${payload.version} is available`;
return payload.isExperimental
? t(
"launch.updateToast.experimentalAvailableTitle",
"Experimental update available",
)
: t("launch.updateToast.availableTitle", "Update available");
case "downloading":
return `Installing Recordly ${payload.version}`;
return t("launch.updateToast.downloadingTitle", "Downloading your update");
case "ready":
return `Recordly ${payload.version} is ready`;
return t("launch.updateToast.readyTitle", "Ready to restart");
case "error":
return payload.primaryAction === "retry-check"
? "Could not check for updates"
: `Recordly ${payload.version} needs attention`;
? t("launch.updateToast.checkErrorTitle", "Couldn’t check for updates")
: t("launch.updateToast.downloadErrorTitle", "Couldn’t download the update");
}
}
function getPrimaryButtonLabel(payload: UpdateToastPayload) {
return payload.primaryAction === "retry-check" ? "Try Again" : "Install & Restart";
function getDetail(payload: UpdateToastPayload, t: Translate) {
if (payload.phase === "available" && payload.isExperimental) {
return t(
"launch.updateToast.experimentalDescription",
"You've opted into experimental updates so you have the choice to test the latest update of Recordly before it's widely available.",
);
}
return payload.detail;
}
function getPhaseIcon(payload: UpdateToastPayload) {
function getPrimaryLabel(payload: UpdateToastPayload, t: Translate) {
if (payload.primaryAction === "retry-check") {
return t("launch.updateToast.tryAgain", "Try again");
}
return payload.phase === "ready"
? t("launch.updateToast.restartToUpdate", "Restart to update")
: t("launch.updateToast.updateNow", "Update now");
}
function PhaseIcon({ payload }: { payload: UpdateToastPayload }) {
switch (payload.phase) {
case "available":
return <Download size={20} />;
return <DownloadSimpleIcon size={20} weight="bold" />;
case "downloading":
return <LoaderCircle size={20} className="animate-spin" />;
return <ArrowClockwiseIcon size={20} weight="bold" className={styles.spin} />;
case "ready":
return <Rocket size={20} />;
return <CheckCircleIcon size={20} weight="fill" />;
case "error":
return <AlertCircle size={20} />;
return <WarningCircleIcon size={20} weight="fill" />;
}
}
export function UpdateToastWindow() {
const [payload, setPayload] = useState<UpdateToastPayload | null>(null);
const [reminderDelayMs, setReminderDelayMs] = useState(DEFAULT_REMINDER_DELAY_MS);
const { t } = useI18n();
useEffect(() => {
let mounted = true;
let pollTimer: ReturnType<typeof setInterval> | null = null;
void window.electronAPI.getCurrentUpdateToastPayload().then((nextPayload) => {
if (mounted) {
setPayload(nextPayload);
}
});
pollTimer = setInterval(() => {
const refresh = () => {
void window.electronAPI.getCurrentUpdateToastPayload().then((nextPayload) => {
if (mounted) {
setPayload(nextPayload);
}
if (mounted) setPayload(nextPayload);
});
}, 750);
};
const dispose = window.electronAPI.onUpdateToastStateChanged((nextPayload) => {
setPayload(nextPayload);
});
refresh();
const pollTimer = setInterval(refresh, 750);
const dispose = window.electronAPI.onUpdateToastStateChanged(setPayload);
return () => {
mounted = false;
if (pollTimer) {
clearInterval(pollTimer);
}
clearInterval(pollTimer);
dispose();
};
}, []);
useEffect(() => {
if (!payload) {
return;
}
setReminderDelayMs(payload.delayMs || DEFAULT_REMINDER_DELAY_MS);
}, [payload]);
const normalizedProgress = Math.max(
0,
Math.min(100, Math.round(payload?.progressPercent ?? 0)),
);
const downloadedLabel = formatBytes(payload?.transferredBytes);
const totalLabel = formatBytes(payload?.totalBytes);
const remainingLabel = formatBytes(payload?.remainingBytes);
const speedLabel = formatBytes(payload?.bytesPerSecond);
const phaseStats: Array<{ label: string; value: string }> = [];
if (payload?.phase === "downloading") {
if (downloadedLabel && totalLabel) {
phaseStats.push({ label: "Downloaded", value: `${downloadedLabel} / ${totalLabel}` });
} else if (downloadedLabel) {
phaseStats.push({ label: "Downloaded", value: downloadedLabel });
}
if (remainingLabel) {
phaseStats.push({ label: "Left", value: remainingLabel });
}
if (speedLabel) {
phaseStats.push({ label: "Speed", value: `${speedLabel}/s` });
}
if (!payload) {
return <div className={styles.window} />;
}
const isMacOS = /mac/i.test(navigator.platform);
const wrapperStyle = {
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
padding: 10,
boxSizing: "border-box",
background: isMacOS ? "transparent" : "#0b1220",
} as const;
const cardStyle = {
width: "100%",
maxWidth: 440,
display: "flex",
gap: 14,
alignItems: "flex-start",
padding: "18px 18px 16px",
borderRadius: 24,
background:
"linear-gradient(180deg, rgba(12, 19, 34, 0.98) 0%, rgba(10, 17, 30, 0.98) 100%)",
border: "1px solid rgba(37, 99, 235, 0.24)",
boxShadow: "0 20px 48px rgba(2, 6, 23, 0.5), inset 0 1px 0 rgba(148, 163, 184, 0.08)",
color: "#ffffff",
fontFamily: "var(--app-font-sans)",
} as const;
const iconBoxStyle = {
width: 42,
height: 42,
minWidth: 42,
borderRadius: 16,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "rgba(37, 99, 235, 0.16)",
color: "#60a5fa",
boxShadow: "inset 0 0 0 1px rgba(37, 99, 235, 0.18)",
} as const;
const titleStyle = {
fontSize: 15,
fontWeight: 700,
lineHeight: 1.25,
margin: 0,
color: "#f8fafc",
} as const;
const secondaryTextStyle = {
color: "rgba(226, 232, 240, 0.78)",
fontSize: 13,
lineHeight: 1.5,
margin: "6px 0 0 0",
} as const;
const subtleButtonStyle = {
height: 38,
borderRadius: 12,
padding: "0 14px",
border: "1px solid rgba(148, 163, 184, 0.16)",
background: "rgba(15, 23, 42, 0.72)",
color: "#e2e8f0",
fontSize: 13,
fontWeight: 600,
cursor: "pointer",
transition: "all 0.15s ease",
} as const;
const primaryButtonStyle = {
...subtleButtonStyle,
border: "none",
background: "linear-gradient(180deg, #3b82f6 0%, #2563eb 100%)",
color: "#ffffff",
boxShadow: "0 12px 24px rgba(37, 99, 235, 0.26)",
} as const;
const selectStyle = {
height: 38,
borderRadius: 12,
padding: "0 34px 0 12px",
border: "1px solid rgba(37, 99, 235, 0.22)",
background:
"linear-gradient(180deg, rgba(18, 29, 51, 0.96) 0%, rgba(12, 22, 42, 0.96) 100%)",
color: "#dbeafe",
fontSize: 13,
fontWeight: 600,
outline: "none",
boxShadow: "inset 0 0 0 1px rgba(37, 99, 235, 0.06)",
cursor: "pointer",
} as const;
const progress = Math.max(0, Math.min(100, Math.round(payload.progressPercent ?? 0)));
const transferred = formatBytes(payload.transferredBytes);
const total = formatBytes(payload.totalBytes);
const speed = formatBytes(payload.bytesPerSecond);
const progressDetail = [
transferred && total ? `${transferred} of ${total}` : transferred,
speed ? `${speed}/s` : null,
]
.filter(Boolean)
.join(" · ");
const handlePrimaryAction = async () => {
if (!payload || payload.phase === "downloading") {
return;
}
if (payload.phase === "downloading") return;
if (payload.primaryAction === "retry-check") {
await window.electronAPI.checkForAppUpdates();
return;
}
if (payload.phase === "ready") {
await window.electronAPI.installDownloadedUpdate();
return;
}
await window.electronAPI.downloadAvailableUpdate(true);
};
const handleLater = async () => {
if (!payload) {
return;
}
const handleNotNow = async () => {
if (payload.isPreview) {
await window.electronAPI.dismissUpdateToast();
return;
}
await window.electronAPI.deferDownloadedUpdate(reminderDelayMs);
await window.electronAPI.deferDownloadedUpdate(payload.delayMs);
};
if (!payload) {
return <div style={wrapperStyle} />;
}
return (
<div style={wrapperStyle}>
<div style={cardStyle}>
<div style={iconBoxStyle}>{getPhaseIcon(payload)}</div>
<div style={{ minWidth: 0, flex: 1 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<p style={titleStyle}>{getToastTitle(payload)}</p>
<div className={`${styles.window} launch-theme`}>
<section className={styles.card} aria-live="polite" aria-label="Recordly update">
<div className={`${styles.icon} ${payload.phase === "error" ? styles.iconError : ""}`}>
<PhaseIcon payload={payload} />
</div>
<div className={styles.content}>
<div className={styles.headingRow}>
<h1>{getTitle(payload, t)}</h1>
<span className={styles.version}>v{payload.version.replace(/^v/, "")}</span>
{payload.isExperimental ? (
<span className={styles.preview}>
{t("launch.updateToast.experimentalBadge", "Experimental")}
</span>
) : null}
{payload.isPreview ? (
<span
style={{
borderRadius: 999,
padding: "2px 8px",
fontSize: 10,
fontWeight: 700,
letterSpacing: "0.18em",
textTransform: "uppercase",
color: "#93c5fd",
background: "rgba(37, 99, 235, 0.14)",
border: "1px solid rgba(37, 99, 235, 0.18)",
}}
>
Dev
<span className={styles.preview}>
{t("launch.updateToast.previewBadge", "Preview")}
</span>
) : null}
</div>
<p style={secondaryTextStyle}>{payload.detail}</p>
<p>{getDetail(payload, t)}</p>
{payload.phase === "downloading" ? (
<div style={{ marginTop: 14 }}>
<div
style={{
height: 10,
overflow: "hidden",
borderRadius: 999,
background: "rgba(148, 163, 184, 0.14)",
}}
>
<div
style={{
height: "100%",
width: `${normalizedProgress}%`,
borderRadius: 999,
background:
"linear-gradient(90deg, #60a5fa 0%, #2563eb 45%, #1d4ed8 100%)",
boxShadow: "0 0 22px rgba(37, 99, 235, 0.38)",
}}
/>
<div className={styles.progressBlock}>
<div className={styles.progressTrack}>
<div className={styles.progressFill} style={{ width: `${progress}%` }} />
</div>
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: 8,
marginTop: 10,
}}
>
<span
style={{
fontSize: 12,
fontWeight: 700,
color: "#dbeafe",
}}
>
{normalizedProgress}% complete
</span>
{phaseStats.map((stat) => (
<span
key={stat.label}
style={{
fontSize: 11,
fontWeight: 600,
color: "rgba(191, 219, 254, 0.9)",
background: "rgba(37, 99, 235, 0.12)",
borderRadius: 999,
padding: "4px 8px",
border: "1px solid rgba(37, 99, 235, 0.16)",
}}
>
{stat.label}: {stat.value}
</span>
))}
<div className={styles.progressMeta}>
<strong>{progress}%</strong>
{progressDetail ? <span>{progressDetail}</span> : null}
</div>
</div>
) : null}
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: 10,
marginTop: 14,
alignItems: "center",
}}
>
{payload.phase !== "downloading" ? (
<>
<button
type="button"
onClick={handlePrimaryAction}
style={primaryButtonStyle}
>
{getPrimaryButtonLabel(payload)}
</button>
<select
value={String(reminderDelayMs)}
onChange={(event) => {
setReminderDelayMs(Number.parseInt(event.target.value, 10));
}}
style={selectStyle}
>
{REMINDER_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<button
type="button"
onClick={handleLater}
style={subtleButtonStyle}
>
Later
</button>
</>
) : null}
</div>
) : (
<div className={styles.actions}>
<button type="button" className={styles.secondaryButton} onClick={handleNotNow}>
{t("launch.updateToast.notNow", "Not now")}
</button>
<button type="button" className={styles.primaryButton} onClick={handlePrimaryAction}>
{getPrimaryLabel(payload, t)}
</button>
</div>
)}
</div>
</div>
</section>
</div>
);
}
@@ -2566,7 +2566,7 @@ export function SettingsPanel({
<div className="mt-0.5 text-[10px] text-muted-foreground/70">
{tSettings(
"updates.experimentalDescription",
"Receive first-line test builds published as prereleases. These may be less stable.",
"This is the front line of user testing - highly experimental so expect bugs",
)}
</div>
</div>
+15
View File
@@ -76,5 +76,20 @@
"microphoneDenied": "Der Zugriff auf das Mikrofon wurde verweigert. Die Aufzeichnung wird ohne Mikrofonton fortgesetzt.",
"failedToStart": "Die Aufzeichnung konnte nicht gestartet werden: {{error}}",
"failedToStartGeneric": "Die Aufnahme konnte nicht gestartet werden"
},
"updateToast": {
"availableTitle": "Update verfügbar",
"experimentalAvailableTitle": "Experimentelles Update verfügbar",
"experimentalDescription": "Sie haben experimentelle Updates aktiviert und können daher das neueste Update von Recordly testen, bevor es allgemein verfügbar ist.",
"downloadingTitle": "Update wird heruntergeladen",
"readyTitle": "Bereit zum Neustart",
"checkErrorTitle": "Updates konnten nicht geprüft werden",
"downloadErrorTitle": "Das Update konnte nicht heruntergeladen werden",
"experimentalBadge": "Experimentell",
"previewBadge": "Vorschau",
"notNow": "Nicht jetzt",
"updateNow": "Jetzt aktualisieren",
"restartToUpdate": "Zum Aktualisieren neu starten",
"tryAgain": "Erneut versuchen"
}
}
+6
View File
@@ -254,5 +254,11 @@
"micLabel": "Quellmikrofon",
"mixedLabel": "Quelle",
"deleteRegion": "Audio löschen"
},
"updates": {
"experimentalDescription": "Sie haben experimentelle Updates aktiviert und können daher das neueste Update von Recordly testen, bevor es allgemein verfügbar ist.",
"title": "Updates",
"experimental": "Experimentelle Updates",
"saveFailed": "Update-Kanal konnte nicht geändert werden."
}
}
+15
View File
@@ -76,5 +76,20 @@
"microphoneDenied": "Microphone access was denied. Recording will continue without microphone audio.",
"failedToStart": "Failed to start recording: {{error}}",
"failedToStartGeneric": "Failed to start recording"
},
"updateToast": {
"availableTitle": "Update available",
"experimentalAvailableTitle": "Experimental update available",
"experimentalDescription": "You've opted into experimental updates so you have the choice to test the latest update of Recordly before it's widely available.",
"downloadingTitle": "Downloading your update",
"readyTitle": "Ready to restart",
"checkErrorTitle": "Couldn't check for updates",
"downloadErrorTitle": "Couldn't download the update",
"experimentalBadge": "Experimental",
"previewBadge": "Preview",
"notNow": "Not now",
"updateNow": "Update now",
"restartToUpdate": "Restart to update",
"tryAgain": "Try again"
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
"updates": {
"title": "Updates",
"experimental": "Experimental updates",
"experimentalDescription": "Receive first-line test builds published as prereleases. These may be less stable.",
"experimentalDescription": "You've opted into experimental updates so you have the choice to test the latest update of Recordly before it's widely available.",
"saveFailed": "Failed to change the update channel."
},
"zoom": {
+15
View File
@@ -76,5 +76,20 @@
"microphoneDenied": "Se denegó el acceso al micrófono. La grabación continuará sin audio del micrófono.",
"failedToStart": "Error al iniciar la grabación: {{error}}",
"failedToStartGeneric": "Error al iniciar la grabación"
},
"updateToast": {
"availableTitle": "Actualización disponible",
"experimentalAvailableTitle": "Actualización experimental disponible",
"experimentalDescription": "Has activado las actualizaciones experimentales, así que puedes probar la actualización más reciente de Recordly antes de que esté disponible para todos.",
"downloadingTitle": "Descargando la actualización",
"readyTitle": "Listo para reiniciar",
"checkErrorTitle": "No se pudieron buscar actualizaciones",
"downloadErrorTitle": "No se pudo descargar la actualización",
"experimentalBadge": "Experimental",
"previewBadge": "Vista previa",
"notNow": "Ahora no",
"updateNow": "Actualizar ahora",
"restartToUpdate": "Reiniciar para actualizar",
"tryAgain": "Intentar de nuevo"
}
}
+6
View File
@@ -254,5 +254,11 @@
"micLabel": "Micrófono",
"mixedLabel": "Fuente",
"deleteRegion": "Eliminar audio"
},
"updates": {
"experimentalDescription": "Has activado las actualizaciones experimentales, así que puedes probar la actualización más reciente de Recordly antes de que esté disponible para todos.",
"title": "Actualizaciones",
"experimental": "Actualizaciones experimentales",
"saveFailed": "No se pudo cambiar el canal de actualización."
}
}
+15
View File
@@ -76,5 +76,20 @@
"microphoneDenied": "L’accès au microphone a été refusé. L’enregistrement continuera sans audio du microphone.",
"failedToStart": "Échec du démarrage de l’enregistrement : {{error}}",
"failedToStartGeneric": "Échec du démarrage de l’enregistrement"
},
"updateToast": {
"availableTitle": "Mise à jour disponible",
"experimentalAvailableTitle": "Mise à jour expérimentale disponible",
"experimentalDescription": "Vous avez activé les mises à jour expérimentales, vous pouvez donc tester la dernière mise à jour de Recordly avant sa disponibilité générale.",
"downloadingTitle": "Téléchargement de la mise à jour",
"readyTitle": "Prêt à redémarrer",
"checkErrorTitle": "Impossible de vérifier les mises à jour",
"downloadErrorTitle": "Impossible de télécharger la mise à jour",
"experimentalBadge": "Expérimental",
"previewBadge": "Aperçu",
"notNow": "Plus tard",
"updateNow": "Mettre à jour",
"restartToUpdate": "Redémarrer pour mettre à jour",
"tryAgain": "Réessayer"
}
}
+6
View File
@@ -254,5 +254,11 @@
"micLabel": "Microphone",
"mixedLabel": "Source",
"deleteRegion": "Supprimer la zone audio"
},
"updates": {
"experimentalDescription": "Vous avez activé les mises à jour expérimentales, vous pouvez donc tester la dernière mise à jour de Recordly avant sa disponibilité générale.",
"title": "Mises à jour",
"experimental": "Mises à jour expérimentales",
"saveFailed": "Impossible de changer le canal de mise à jour."
}
}
+15
View File
@@ -76,5 +76,20 @@
"microphoneDenied": "L'accesso al microfono è stato negato. La registrazione continuerà senza audio del microfono.",
"failedToStart": "Avvio della registrazione non riuscito: {{error}}",
"failedToStartGeneric": "Avvio della registrazione non riuscito"
},
"updateToast": {
"availableTitle": "Aggiornamento disponibile",
"experimentalAvailableTitle": "Aggiornamento sperimentale disponibile",
"experimentalDescription": "Hai attivato gli aggiornamenti sperimentali, quindi puoi scegliere di provare l'ultimo aggiornamento di Recordly prima che sia disponibile per tutti.",
"downloadingTitle": "Download dell'aggiornamento",
"readyTitle": "Pronto per il riavvio",
"checkErrorTitle": "Impossibile verificare gli aggiornamenti",
"downloadErrorTitle": "Impossibile scaricare l'aggiornamento",
"experimentalBadge": "Sperimentale",
"previewBadge": "Anteprima",
"notNow": "Non ora",
"updateNow": "Aggiorna ora",
"restartToUpdate": "Riavvia per aggiornare",
"tryAgain": "Riprova"
}
}
+6
View File
@@ -254,5 +254,11 @@
"micLabel": "Sorgente microfono",
"mixedLabel": "Sorgente",
"deleteRegion": "Elimina audio"
},
"updates": {
"experimentalDescription": "Hai attivato gli aggiornamenti sperimentali, quindi puoi scegliere di provare l'ultimo aggiornamento di Recordly prima che sia disponibile per tutti.",
"title": "Aggiornamenti",
"experimental": "Aggiornamenti sperimentali",
"saveFailed": "Impossibile cambiare il canale di aggiornamento."
}
}
+15
View File
@@ -76,5 +76,20 @@
"microphoneDenied": "마이크 접근이 거부되었습니다. 마이크 오디오 없이 녹화를 계속합니다.",
"failedToStart": "녹화 시작에 실패했습니다: {{error}}",
"failedToStartGeneric": "녹화 시작에 실패했습니다"
},
"updateToast": {
"availableTitle": "업데이트 사용 가능",
"experimentalAvailableTitle": "실험적 업데이트 사용 가능",
"experimentalDescription": "실험적 업데이트를 선택했으므로 Recordly의 최신 업데이트가 널리 제공되기 전에 먼저 테스트해 볼 수 있습니다.",
"downloadingTitle": "업데이트 다운로드 중",
"readyTitle": "다시 시작할 준비 완료",
"checkErrorTitle": "업데이트를 확인할 수 없습니다",
"downloadErrorTitle": "업데이트를 다운로드할 수 없습니다",
"experimentalBadge": "실험적",
"previewBadge": "미리보기",
"notNow": "나중에",
"updateNow": "지금 업데이트",
"restartToUpdate": "다시 시작하여 업데이트",
"tryAgain": "다시 시도"
}
}
+6
View File
@@ -254,5 +254,11 @@
"micLabel": "마이크 소스",
"mixedLabel": "소스",
"deleteRegion": "오디오 삭제"
},
"updates": {
"experimentalDescription": "실험적 업데이트를 선택했으므로 Recordly의 최신 업데이트가 널리 제공되기 전에 먼저 테스트해 볼 수 있습니다.",
"title": "업데이트",
"experimental": "실험적 업데이트",
"saveFailed": "업데이트 채널을 변경하지 못했습니다."
}
}
+15
View File
@@ -76,5 +76,20 @@
"microphoneDenied": "Microfoontoegang is geweigerd. De opname gaat verder zonder microfoonaudio.",
"failedToStart": "Opname starten mislukt: {{error}}",
"failedToStartGeneric": "Opname starten mislukt"
},
"updateToast": {
"availableTitle": "Update beschikbaar",
"experimentalAvailableTitle": "Experimentele update beschikbaar",
"experimentalDescription": "Je hebt experimentele updates ingeschakeld, dus je kunt de nieuwste update van Recordly testen voordat die breed beschikbaar is.",
"downloadingTitle": "Update downloaden",
"readyTitle": "Klaar om opnieuw te starten",
"checkErrorTitle": "Kan niet controleren op updates",
"downloadErrorTitle": "Kan de update niet downloaden",
"experimentalBadge": "Experimenteel",
"previewBadge": "Preview",
"notNow": "Niet nu",
"updateNow": "Nu updaten",
"restartToUpdate": "Opnieuw starten om te updaten",
"tryAgain": "Opnieuw proberen"
}
}
+6
View File
@@ -254,5 +254,11 @@
"micLabel": "Microfoon",
"mixedLabel": "Bron",
"deleteRegion": "Audio verwijderen"
},
"updates": {
"experimentalDescription": "Je hebt experimentele updates ingeschakeld, dus je kunt de nieuwste update van Recordly testen voordat die breed beschikbaar is.",
"title": "Updates",
"experimental": "Experimentele updates",
"saveFailed": "Kan het updatekanaal niet wijzigen."
}
}
+15
View File
@@ -76,5 +76,20 @@
"microphoneDenied": "O acesso ao microfone foi negado. A gravação continuará sem áudio do microfone.",
"failedToStart": "Falha ao iniciar gravação: {{error}}",
"failedToStartGeneric": "Falha ao iniciar gravação"
},
"updateToast": {
"availableTitle": "Atualização disponível",
"experimentalAvailableTitle": "Atualização experimental disponível",
"experimentalDescription": "Você ativou as atualizações experimentais, então pode escolher testar a atualização mais recente do Recordly antes que ela fique amplamente disponível.",
"downloadingTitle": "Baixando sua atualização",
"readyTitle": "Pronto para reiniciar",
"checkErrorTitle": "Não foi possível verificar atualizações",
"downloadErrorTitle": "Não foi possível baixar a atualização",
"experimentalBadge": "Experimental",
"previewBadge": "Prévia",
"notNow": "Agora não",
"updateNow": "Atualizar agora",
"restartToUpdate": "Reiniciar para atualizar",
"tryAgain": "Tentar novamente"
}
}
+6
View File
@@ -254,5 +254,11 @@
"micLabel": "Microfone",
"mixedLabel": "Fonte",
"deleteRegion": "Excluir áudio"
},
"updates": {
"experimentalDescription": "Você ativou as atualizações experimentais, então pode escolher testar a atualização mais recente do Recordly antes que ela fique amplamente disponível.",
"title": "Atualizações",
"experimental": "Atualizações experimentais",
"saveFailed": "Falha ao alterar o canal de atualização."
}
}
+15
View File
@@ -76,5 +76,20 @@
"microphoneDenied": "Нет доступа к микрофону. Запись продолжится без вашего голоса.",
"failedToStart": "Не удалось начать запись: {{error}}",
"failedToStartGeneric": "Не удалось начать запись"
},
"updateToast": {
"availableTitle": "Доступно обновление",
"experimentalAvailableTitle": "Доступно экспериментальное обновление",
"experimentalDescription": "Вы включили экспериментальные обновления, поэтому можете протестировать последнее обновление Recordly до его широкого выпуска.",
"downloadingTitle": "Загрузка обновления",
"readyTitle": "Готово к перезапуску",
"checkErrorTitle": "Не удалось проверить обновления",
"downloadErrorTitle": "Не удалось загрузить обновление",
"experimentalBadge": "Экспериментальное",
"previewBadge": "Предпросмотр",
"notNow": "Не сейчас",
"updateNow": "Обновить сейчас",
"restartToUpdate": "Перезапустить для обновления",
"tryAgain": "Повторить"
}
}
+6
View File
@@ -254,5 +254,11 @@
"micLabel": "Микрофон",
"mixedLabel": "Источник",
"deleteRegion": "Удалить аудио"
},
"updates": {
"title": "Обновления",
"experimental": "Экспериментальные обновления",
"saveFailed": "Не удалось изменить канал обновлений.",
"experimentalDescription": "Вы включили экспериментальные обновления, поэтому можете протестировать последнее обновление Recordly до его широкого выпуска."
}
}
+15
View File
@@ -76,5 +76,20 @@
"microphoneDenied": "麦克风访问被拒绝。将继续录制但不包含麦克风音频。",
"failedToStart": "录制启动失败:{{error}}",
"failedToStartGeneric": "录制启动失败"
},
"updateToast": {
"availableTitle": "有可用更新",
"experimentalAvailableTitle": "有可用的实验性更新",
"experimentalDescription": "你已选择接收实验性更新,因此可以在 Recordly 最新更新广泛发布之前选择先行测试。",
"downloadingTitle": "正在下载更新",
"readyTitle": "已准备好重新启动",
"checkErrorTitle": "无法检查更新",
"downloadErrorTitle": "无法下载更新",
"experimentalBadge": "实验性",
"previewBadge": "预览",
"notNow": "暂不",
"updateNow": "立即更新",
"restartToUpdate": "重新启动以更新",
"tryAgain": "重试"
}
}
+6
View File
@@ -254,5 +254,11 @@
"micLabel": "麦克风",
"mixedLabel": "来源",
"deleteRegion": "删除音频"
},
"updates": {
"experimentalDescription": "你已选择接收实验性更新,因此可以在 Recordly 最新更新广泛发布之前选择先行测试。",
"title": "更新",
"experimental": "实验性更新",
"saveFailed": "无法更改更新频道。"
}
}
+15
View File
@@ -76,5 +76,20 @@
"microphoneDenied": "麥克風存取遭拒,將在沒有麥克風音訊的情況下繼續錄製。",
"failedToStart": "無法開始錄製:{{error}}",
"failedToStartGeneric": "無法開始錄製"
},
"updateToast": {
"availableTitle": "有可用更新",
"experimentalAvailableTitle": "有可用的實驗性更新",
"experimentalDescription": "你已選擇接收實驗性更新,因此可以在 Recordly 最新更新廣泛推出之前選擇先行測試。",
"downloadingTitle": "正在下載更新",
"readyTitle": "已準備好重新啟動",
"checkErrorTitle": "無法檢查更新",
"downloadErrorTitle": "無法下載更新",
"experimentalBadge": "實驗性",
"previewBadge": "預覽",
"notNow": "暫不",
"updateNow": "立即更新",
"restartToUpdate": "重新啟動以更新",
"tryAgain": "重試"
}
}
+6
View File
@@ -254,5 +254,11 @@
"micLabel": "麥克風源",
"mixedLabel": "源",
"deleteRegion": "刪除音訊"
},
"updates": {
"experimentalDescription": "你已選擇接收實驗性更新,因此可以在 Recordly 最新更新廣泛推出之前選擇先行測試。",
"title": "更新",
"experimental": "實驗性更新",
"saveFailed": "無法變更更新頻道。"
}
}