Studio: localize the prompt queue view and align its editor shortcut (#11117)

* Studio: localize the prompt queue view and align its editor shortcut

The queue controls added in #10980 render hardcoded English while
Settings > Chat > Composer ships translated Queue and Steer labels in
all 12 locales, so a Japanese user reads 方向を変更 in Settings and
"Steer" on the button next to it.

- Route the queue list, reorder announcements, Suspense fallback and the
  composer send/queue labels through useT, and add 39 promptQueue keys
  to every locale, reusing each catalog's existing Queue/Steer wording.
- Save a queued-prompt edit with the configured send shortcut instead of
  a hardcoded Cmd/Ctrl+Enter. This reuses composerSubmitIntent, which
  already covers the IME, repeat and Shift+Enter guards.
- Keep the parked-send release synchronous and mark it, so thread.tsx
  stops reporting one more react-hooks/set-state-in-effect than before.

Adds tests/studio/playwright_queue_localization.py, which fails on the
current code for both behaviours and passes here.

* Studio: trim the queueing tooltip and tidy the queue controls

- Shorten the queueing option tooltip in every locale. It now reads in
  two lines instead of four.
- Drop the queue editor radius from 24px to 17.6px and move the resize
  grip out to the corner, stopping short of the radius so it stays
  visible.
- Render the resume icon at 14px and centre its glyph. Also removes the
  stray leading space that pushed the Resume queue label out of line
  with the other menu items.
- Use the standard arrow for the composer send button when follow-ups
  steer, instead of a separate corner arrow.

* Studio: keep the queued-prompt draft when an IME consumes Escape

Hoisting the Escape branch above the composition guard meant Escape
pressed to close an IME candidate window cancelled the edit and dropped
the draft. Check the native composition state first, as the handler did
before, and cover it in the queue regression test.

* Studio: gate the queue editor on composition state and localize Send

- Track composition start and end in the queued-prompt editor. A
  candidate-confirming Enter can arrive with isComposing false and no
  key code 229, and composerSubmitIntent assumes the caller already
  guards that, so Enter as Save could commit the pre-edit text. Mirrors
  the composer's own gate.
- Use the localized send label for the composer send buttons. aria-label
  overrides the tooltip text, so screen readers still read English while
  the visible tooltip was translated.

* Studio: recover the queue editor from a stuck IME composition

An IME that never sends compositionend left composingRef set, and every
later Enter took the gate's early return, so the send shortcut could no
longer save the edit. Arm a watchdog whenever composing is set, reset on
blur, and do not re-arm from the gated Enter, since a candidate-confirming
Enter follows its composition immediately.
This commit is contained in:
Michael Han 2026-09-16 08:16:36 -07:00 • committed by GitHub
parent cf335fa470
commit 9b1f931744
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 981 additions and 81 deletions

View file

@ -3,6 +3,7 @@
import { lazy, Suspense, type ComponentProps } from "react";
import type { PromptQueueList as QueueList } from "./prompt-queue-list";
import { useT } from "@/i18n";
const List = lazy(() =>
import("./prompt-queue-list").then((module) => ({
@ -11,11 +12,12 @@ const List = lazy(() =>
);
export function PromptQueueList(props: ComponentProps<typeof QueueList>) {
const t = useT();
return (
<Suspense
fallback={
<div role="status" className="px-5 py-2 text-sm text-muted-foreground">
Loading queued prompts
{t("promptQueue.loading")}
</div>
}
>

View file

@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import { useEffect, useId, useRef, useState } from "react";
import { useCallback, useEffect, useId, useRef, useState } from "react";
import {
CornerDownRightIcon,
GripVerticalIcon,
@ -29,13 +29,18 @@ import { usePromptQueueReorder } from "./use-prompt-queue-reorder";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { QueueResumeIcon } from "@/components/assistant-ui/queue-resume-icon";
import {
composerSubmitIntent,
type PromptQueueUIEntry,
type PromptQueueUIItem,
useChatPreferencesStore,
} from "@/features/chat";
import { copyToClipboard } from "@/lib/copy-to-clipboard";
import { useT } from "@/i18n";
import { cn } from "@/lib/utils";
// Matches the composer's IME watchdog.
const IME_STUCK_TIMEOUT_MS = 2500;
type PromptQueueListProps = {
entry: PromptQueueUIEntry;
items: PromptQueueUIItem[];
@ -56,12 +61,16 @@ export function PromptQueueList({
onSteer,
onResume,
}: PromptQueueListProps) {
const t = useT();
const followUpBehavior = useChatPreferencesStore((s) => s.followUpBehavior);
const setFollowUpBehavior = useChatPreferencesStore((s) => s.setFollowUpBehavior);
const sendShortcut = useChatPreferencesStore((s) => s.sendShortcut);
const [editingId, setEditingId] = useState<string | null>(null);
const [draft, setDraft] = useState("");
const [announcement, setAnnouncement] = useState("");
const inputRef = useRef<HTMLTextAreaElement>(null);
const composingRef = useRef(false);
const composingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const editFromMenuRef = useRef(false);
const instructionsId = useId();
const editingItem = items.find(
@ -90,7 +99,28 @@ export function PromptQueueList({
inputRef.current?.select();
}, [activeEditingId]);
// Some IMEs never send compositionend, which would wedge the Enter gate
// below. Drop the flag after a quiet spell, as the composer's watchdog does.
const setComposing = useCallback((next: boolean) => {
composingRef.current = next;
if (composingTimerRef.current) clearTimeout(composingTimerRef.current);
composingTimerRef.current = next
? setTimeout(() => {
composingTimerRef.current = null;
composingRef.current = false;
}, IME_STUCK_TIMEOUT_MS)
: null;
}, []);
useEffect(
() => () => {
if (composingTimerRef.current) clearTimeout(composingTimerRef.current);
},
[],
);
function startEditing(item: PromptQueueUIItem) {
setComposing(false);
setDraft(item.prompt);
setEditingId(item.id);
}
@ -113,12 +143,10 @@ export function PromptQueueList({
function saveEditing() {
if (!editingItem || !draft.trim()) return;
if (onEdit(editingItem.id, draft)) {
setAnnouncement("Queued prompt updated.");
setAnnouncement(t("promptQueue.announceUpdated"));
finishEditing();
} else {
setAnnouncement(
"This prompt can no longer be edited because the queue changed.",
);
setAnnouncement(t("promptQueue.announceEditFailed"));
}
}
@ -126,16 +154,18 @@ export function PromptQueueList({
<div
ref={listRef}
className="relative z-0 mx-3 mb-[-8px] max-h-[28dvh] overflow-y-auto rounded-t-[20px] border border-border/60 bg-background px-1.5 pt-1 pb-3 text-muted-foreground sm:mx-5 sm:px-2 dark:bg-[color-mix(in_srgb,var(--card)_50%,var(--background))] [&_button]:border-0 [&_button]:shadow-none [&_.aui-button-icon:focus-visible]:bg-accent"
aria-label={`Prompt queue, ${entry.current} of ${entry.total}`}
aria-label={t("promptQueue.regionLabel", {
current: entry.current,
total: entry.total,
})}
>
<p id={instructionsId} className="sr-only">
Drag the handle to reorder. With the handle focused, use Up or Down to
move one position, or Home or End to move to the front or end.
{t("promptQueue.reorderInstructions")}
</p>
<div role="status" aria-live="polite" className="sr-only">
{announcement}
</div>
<div role="list" aria-label="Queued prompts">
<div role="list" aria-label={t("promptQueue.listLabel")}>
{items.map((item, index) => {
const isEditing = editingItem?.id === item.id;
const position = index + 1;
@ -152,7 +182,11 @@ export function PromptQueueList({
role="listitem"
data-queue-item-id={item.id}
data-queue-dragging={draggingId === item.id || undefined}
aria-label={`Queued prompt ${position} of ${items.length}: ${item.prompt}`}
aria-label={t("promptQueue.itemLabel", {
position,
total: items.length,
prompt: item.prompt,
})}
className={cn(
"group relative rounded-lg transition-colors",
draggingId && "will-change-transform",
@ -161,40 +195,80 @@ export function PromptQueueList({
>
{isEditing ? (
<div className="group/queue-editor flex flex-wrap items-center justify-end gap-2 px-1 py-2">
<div className="w-full overflow-hidden rounded-3xl border-0 bg-muted/40 p-3 has-[:focus-visible]:bg-muted dark:bg-card dark:has-[:focus-visible]:bg-accent">
<div className="w-full overflow-hidden rounded-lg border-0 bg-muted/40 p-3 has-[:focus-visible]:bg-muted dark:bg-card dark:has-[:focus-visible]:bg-accent">
<textarea
ref={inputRef}
value={draft}
rows={2}
onChange={(event) => setDraft(event.currentTarget.value)}
onCompositionStart={() => setComposing(true)}
onCompositionUpdate={() => setComposing(true)}
onCompositionEnd={() => setComposing(false)}
// Blur commits or cancels any composition first, so it is
// a safe unconditional reset.
onBlur={() => setComposing(false)}
onKeyDown={(event) => {
// The candidate window owns this key, Escape included:
// cancelling here would discard the draft instead.
if (
event.nativeEvent.isComposing ||
event.nativeEvent.keyCode === 229 ||
event.repeat
) return;
event.nativeEvent.keyCode === 229
) {
setComposing(true);
return;
}
if (composingRef.current) {
// Candidate-confirming Enter can arrive as
// non-composing; keep it gated. It follows the
// composition immediately, so the gate is not
// re-armed here and a stuck flag times out.
if (event.key === "Enter") {
if (!event.shiftKey) event.preventDefault();
return;
}
// Any other key means the composition really ended.
setComposing(false);
}
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
finishEditing();
return;
}
// Same chord as the composer send. The helper owns the
// repeat and Shift+Enter guards.
if (
event.key === "Enter" &&
(event.metaKey || event.ctrlKey)
composerSubmitIntent(
{
key: event.key,
metaKey: event.metaKey,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
altKey: event.altKey,
repeat: event.repeat,
isComposing: event.nativeEvent.isComposing,
keyCode: event.nativeEvent.keyCode,
},
sendShortcut,
)
) {
event.preventDefault();
event.stopPropagation();
saveEditing();
} else if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
finishEditing();
}
}}
className="block max-h-36 min-h-12 w-full resize-y border-0 bg-transparent px-1 text-sm text-foreground outline-none"
aria-label={`Edit queued prompt ${position}`}
// Negative margins move the resize grip out to the corner,
// stopping short of the radius so it is not clipped; the
// matching padding keeps the text where it was.
className="-mr-1.5 -mb-1.5 block max-h-36 min-h-12 w-[calc(100%+0.375rem)] resize-y border-0 bg-transparent pt-0 pr-1.5 pb-1.5 pl-1 text-sm text-foreground outline-none"
aria-label={t("promptQueue.editLabel", { position })}
/>
</div>
<span
aria-hidden="true"
className="invisible mr-auto text-xs font-medium text-foreground group-has-[textarea:focus-visible]/queue-editor:visible"
>
Editing message
{t("promptQueue.editingHint")}
</span>
<Button
type="button"
@ -203,7 +277,7 @@ export function PromptQueueList({
className="focus-visible:bg-accent"
onClick={finishEditing}
>
Cancel
{t("promptQueue.cancel")}
</Button>
<Button
type="button"
@ -212,15 +286,18 @@ export function PromptQueueList({
disabled={!draft.trim()}
onClick={saveEditing}
>
Save
{t("promptQueue.save")}
</Button>
</div>
) : (
<div className="flex min-h-11 items-center gap-1 sm:min-h-12">
<TooltipIconButton
type="button"
tooltip="Drag to reorder"
aria-label={`Reorder queued prompt ${position} of ${items.length}`}
tooltip={t("promptQueue.dragTooltip")}
aria-label={t("promptQueue.reorderLabel", {
position,
total: items.length,
})}
aria-describedby={instructionsId}
className="h-8 w-4 shrink-0 touch-none cursor-grab text-muted-foreground/50 hover:bg-transparent hover:text-muted-foreground/50 active:cursor-grabbing pointer-coarse:h-11 pointer-coarse:w-8 dark:hover:bg-transparent"
disabled={!canMove}
@ -258,35 +335,37 @@ export function PromptQueueList({
</span>
{index === 0 && entry.paused && (
<span className="hidden shrink-0 px-1 text-xs text-muted-foreground sm:inline">
Paused
{t("promptQueue.paused")}
</span>
)}
<TooltipIconButton
type="button"
tooltip="Interrupt the response and send this prompt next"
aria-label={`Steer with queued prompt ${position}`}
tooltip={t("promptQueue.steerTooltip")}
aria-label={t("promptQueue.steerLabel", { position })}
disabled={!item.canEdit || !item.canRemove}
className="h-8 w-auto shrink-0 gap-1.5 px-2 font-normal text-muted-foreground hover:text-foreground pointer-coarse:h-11"
onClick={() => {
setAnnouncement(
onSteer(item.id)
? "This prompt will steer the response next."
: "This prompt could not steer the response. Check the queue and try again.",
t(
onSteer(item.id)
? "promptQueue.announceSteered"
: "promptQueue.announceSteerFailed",
),
);
}}
>
<CornerDownRightIcon className="size-4" />
<span>Steer</span>
<span>{t("promptQueue.steer")}</span>
</TooltipIconButton>
<TooltipIconButton
type="button"
tooltip="Remove from queue"
aria-label={`Remove queued prompt ${position}`}
tooltip={t("promptQueue.removeTooltip")}
aria-label={t("promptQueue.removeLabel", { position })}
disabled={!item.canRemove}
className="size-8 shrink-0 text-muted-foreground hover:text-destructive pointer-coarse:size-11"
onClick={() => {
if (onRemove(item.id))
setAnnouncement("Prompt removed from queue.");
setAnnouncement(t("promptQueue.announceRemoved"));
}}
>
<HugeiconsIcon icon={Delete02Icon} strokeWidth={1.8} />
@ -295,9 +374,9 @@ export function PromptQueueList({
trigger={(triggerRef) => (
<TooltipIconButton
ref={triggerRef}
tooltip="More options"
tooltip={t("promptQueue.moreTooltip")}
data-queue-menu
aria-label={`More options for queued prompt ${position}`}
aria-label={t("promptQueue.moreLabel", { position })}
className="size-8 shrink-0 text-muted-foreground hover:text-foreground data-[state=open]:bg-accent data-[state=open]:text-foreground pointer-coarse:size-11"
>
<MoreHorizontalIcon className="size-4" />
@ -323,22 +402,24 @@ export function PromptQueueList({
startEditing(item);
}}
>
<HugeiconsIcon icon={Edit03Icon} strokeWidth={1.8} /> Edit
message
<HugeiconsIcon icon={Edit03Icon} strokeWidth={1.8} />
{t("promptQueue.editItem")}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
void copyToClipboard(item.prompt).then((copied) =>
setAnnouncement(
copied
? "Prompt copied."
: "Could not copy this prompt. Try again.",
t(
copied
? "promptQueue.announceCopied"
: "promptQueue.announceCopyFailed",
),
),
);
}}
>
<HugeiconsIcon icon={Copy01Icon} strokeWidth={1.8} /> Copy
message
<HugeiconsIcon icon={Copy01Icon} strokeWidth={1.8} />
{t("promptQueue.copyItem")}
</DropdownMenuItem>
<DropdownMenuSeparator />
<Tooltip delayDuration={300} disableHoverableContent={true}>
@ -348,14 +429,20 @@ export function PromptQueueList({
const behavior = followUpBehavior === "queue" ? "steer" : "queue";
setFollowUpBehavior(behavior);
setAnnouncement(
behavior === "queue"
? "New follow-ups will queue after the current response."
: "New follow-ups will steer the current response.",
t(
behavior === "queue"
? "promptQueue.announceQueueingOn"
: "promptQueue.announceQueueingOff",
),
);
}}
>
<ListEndIcon />
{followUpBehavior === "queue" ? "Turn off queueing" : "Turn on queueing"}
{t(
followUpBehavior === "queue"
? "promptQueue.turnOffQueueing"
: "promptQueue.turnOnQueueing",
)}
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent
@ -365,16 +452,18 @@ export function PromptQueueList({
collisionPadding={8}
className="prompt-queue-option-tooltip pointer-events-none max-w-[min(20rem,calc(100vw-1rem))]"
>
{followUpBehavior === "queue"
? "New messages sent during a response will stop that response and run next."
: "New messages sent during a response will wait in line and run in order."}{" "}
Existing queued prompts are kept; this does not pause or
resume the queue.
{t(
followUpBehavior === "queue"
? "promptQueue.queueingOffHint"
: "promptQueue.queueingOnHint",
)}{" "}
{t("promptQueue.queueingHintShared")}
</TooltipContent>
</Tooltip>
{entry.paused && (
<DropdownMenuItem onSelect={onResume}>
<QueueResumeIcon className="size-4" /> Resume queue
<QueueResumeIcon className="size-3.5" />
{t("promptQueue.resume")}
</DropdownMenuItem>
)}
</NonModalDropdownMenu>

View file

@ -10,8 +10,10 @@ export function QueueResumeIcon({ className = "size-6" }: { className?: string }
className={className}
aria-hidden="true"
>
{/* Centres the glyph: its own box sits 0.15 right of the viewBox middle. */}
<path
fillRule="evenodd"
transform="translate(-0.15 0)"
d="M4.5 5.653c0-1.427 1.529-2.33 2.779-1.643l11.54 6.347c1.295.712 1.295 2.573 0 3.286L7.28 19.99c-1.25.687-2.779-.217-2.779-1.643V5.653Z"
clipRule="evenodd"
/>

View file

@ -323,7 +323,6 @@ import {
ChevronRightIcon,
Columns2Icon,
SlidersHorizontalIcon,
CornerUpRightIcon,
FastForwardIcon,
GitBranchIcon,
GlobeIcon,
@ -4569,6 +4568,9 @@ const Composer: FC<{
return;
}
clearStoredDraft();
// Stays synchronous: deferring lets the run state above go stale, and the
// send is then refused after the wait toast is already gone.
// eslint-disable-next-line react-hooks/set-state-in-effect -- see above
sendReservedComposer();
}
}, [
@ -6695,11 +6697,20 @@ const ComposerRightControls: FC<{
menuSide,
queueThreadIds,
}) => {
const t = useT();
const followUpBehavior = useChatPreferencesStore((s) => s.followUpBehavior);
const sendShortcut = useChatPreferencesStore((s) => s.sendShortcut);
const shortcutLabels = composerShortcutLabels(sendShortcut, isMacPlatform());
const followUpLabel = followUpBehavior === "queue" ? "Queue message" : "Steer response";
const followUpTooltip = `${followUpLabel} (${shortcutLabels.send}) · ${shortcutLabels.opposite} for the opposite`;
const followUpLabel = t(
followUpBehavior === "queue"
? "promptQueue.queueButton"
: "promptQueue.steerButton",
);
const followUpTooltip = t("promptQueue.followUpTooltip", {
action: followUpLabel,
send: shortcutLabels.send,
opposite: shortcutLabels.opposite,
});
const queueEntry = usePromptQueueUI((s) =>
findPromptQueueEntry(s, queueThreadIds),
);
@ -6788,7 +6799,9 @@ const ComposerRightControls: FC<{
<ComposerPrimitive.Send asChild={true}>
<TooltipIconButton
tooltip={
pendingSend ? "Waiting for documents…" : `Send message (${shortcutLabels.send})`
pendingSend
? "Waiting for documents…"
: t("promptQueue.sendTooltip", { shortcut: shortcutLabels.send })
}
side="bottom"
type="submit"
@ -6799,7 +6812,7 @@ const ComposerRightControls: FC<{
disabled={disabled || pendingSend}
onClick={(event) => onSendClick?.(event)}
className="aui-composer-send ml-1.5 size-9 rounded-full"
aria-label="Send message"
aria-label={t("promptQueue.sendLabel")}
>
{pendingSend ? (
<Spinner className="size-[18px]" />
@ -6847,11 +6860,7 @@ const ComposerRightControls: FC<{
className="aui-composer-send ml-1.5 size-9 rounded-full"
aria-label={followUpLabel}
>
{followUpBehavior === "steer" ? (
<CornerUpRightIcon className="size-[21px] stroke-2" />
) : (
<ArrowUpIcon className="unsloth-send-icon aui-composer-send-icon size-[21px] stroke-2" />
)}
<ArrowUpIcon className="unsloth-send-icon aui-composer-send-icon size-[21px] stroke-2" />
</TooltipIconButton>
)}
</AuiIf>
@ -6902,11 +6911,7 @@ const ComposerRightControls: FC<{
className="aui-composer-send size-9 rounded-full"
aria-label={followUpLabel}
>
{followUpBehavior === "steer" ? (
<CornerUpRightIcon className="size-[21px] stroke-2" />
) : (
<ArrowUpIcon className="unsloth-send-icon aui-composer-send-icon size-[21px] stroke-2" />
)}
<ArrowUpIcon className="unsloth-send-icon aui-composer-send-icon size-[21px] stroke-2" />
</TooltipIconButton>
)}
</div>

View file

@ -11,6 +11,7 @@ import {
} from "react";
import { useReducedMotionConfig } from "motion/react";
import type { PromptQueueUIItem } from "@/features/chat";
import { useT } from "@/i18n";
const SETTLE_MS = 180;
const EASING = "cubic-bezier(0.2, 0.8, 0.2, 1)";
@ -51,6 +52,7 @@ export function usePromptQueueReorder(
onMove: (id: string, targetId: string) => boolean,
announce: (message: string) => void,
) {
const t = useT();
const listRef = useRef<HTMLDivElement>(null);
const dragRef = useRef<Drag | null>(null);
const animations = useRef<Animation[]>([]);
@ -106,12 +108,12 @@ export function usePromptQueueReorder(
pendingPositions.current ??= capturePositions();
if (onMove(id, targetId)) {
const position = items.findIndex((item) => item.id === targetId) + 1;
announce(`Prompt moved to position ${position} of ${items.length}.`);
announce(
t("promptQueue.announceMoved", { position, total: items.length }),
);
} else {
if (!draggingId) pendingPositions.current = null;
announce(
"The queue changed before this prompt could be moved. Try again.",
);
announce(t("promptQueue.announceMoveFailed"));
}
}
@ -120,9 +122,7 @@ export function usePromptQueueReorder(
if (drag && (drag.signature !== signature || disabled)) {
clearDrag(drag);
setDraggingId(null);
announce(
"The queue changed. Drag again to reorder the remaining prompts.",
);
announce(t("promptQueue.announceDragReset"));
}
const previous = pendingPositions.current;
pendingPositions.current = null;
@ -155,6 +155,7 @@ export function usePromptQueueReorder(
draggingId,
clearDrag,
stopAnimations,
t,
]);
useEffect(() => {

View file

@ -2901,14 +2901,17 @@ export function SharedComposer({
</Button>
) : (
<TooltipIconButton
tooltip={sendUnavailableReason ?? `Send message (${shortcutLabels.send})`}
tooltip={
sendUnavailableReason ??
t("promptQueue.sendTooltip", { shortcut: shortcutLabels.send })
}
side="bottom"
variant="default"
size="icon"
className="ml-1.5 size-9 rounded-full"
onClick={send}
disabled={!canSend}
aria-label="Send message"
aria-label={t("promptQueue.sendLabel")}
>
<ArrowUpIcon className="unsloth-send-icon size-[22px] stroke-2" />
</TooltipIconButton>

View file

@ -20,6 +20,52 @@ export const ar = {
settings: "إعدادات كتابة الرسائل",
preview: "معاينة منسقة",
},
promptQueue: {
loading: "جارٍ تحميل الرسائل في الطابور",
listLabel: "الرسائل في الطابور",
regionLabel: "طابور الرسائل، {current} من {total}",
itemLabel: "الرسالة {position} من {total} في الطابور: {prompt}",
reorderInstructions: "اسحب المقبض لإعادة الترتيب. عند تركيز المقبض، استخدم السهم لأعلى أو لأسفل للتحريك موضعًا واحدًا، أو Home أو End للانتقال إلى البداية أو النهاية.",
dragTooltip: "اسحب لإعادة الترتيب",
reorderLabel: "إعادة ترتيب الرسالة {position} من {total} في الطابور",
paused: "متوقف مؤقتًا",
steer: "توجيه",
steerTooltip: "إيقاف الرد وإرسال هذه الرسالة بعده مباشرة",
steerLabel: "التوجيه بالرسالة {position} في الطابور",
removeTooltip: "إزالة من الطابور",
removeLabel: "إزالة الرسالة {position} من الطابور",
moreTooltip: "خيارات أخرى",
moreLabel: "خيارات أخرى للرسالة {position} في الطابور",
editItem: "تعديل الرسالة",
copyItem: "نسخ الرسالة",
editLabel: "تعديل الرسالة {position} في الطابور",
cancel: "إلغاء",
save: "حفظ",
turnOffQueueing: "إيقاف الطابور",
turnOnQueueing: "تشغيل الطابور",
resume: "استئناف الطابور",
queueButton: "إضافة الرسالة للطابور",
steerButton: "توجيه الرد",
sendTooltip: "إرسال الرسالة ({shortcut})",
sendLabel: "إرسال الرسالة",
followUpTooltip: "{action} ({send}) · {opposite} للإجراء المعاكس",
announceUpdated: "تم تحديث الرسالة في الطابور.",
announceEditFailed: "لم يعد بالإمكان تعديل هذه الرسالة لأن الطابور تغيّر.",
announceRemoved: "تمت إزالة الرسالة من الطابور.",
announceSteered: "ستوجّه هذه الرسالة الرد التالي.",
announceSteerFailed: "تعذّر توجيه الرد بهذه الرسالة. راجع الطابور وحاول مرة أخرى.",
announceCopied: "تم نسخ الرسالة.",
announceCopyFailed: "تعذّر نسخ هذه الرسالة. حاول مرة أخرى.",
announceQueueingOn: "ستُضاف رسائل المتابعة الجديدة إلى الطابور بعد الرد الحالي.",
announceQueueingOff: "ستوجّه رسائل المتابعة الجديدة الرد الحالي.",
announceMoved: "تم نقل الرسالة إلى الموضع {position} من {total}.",
announceMoveFailed: "تغيّر الطابور قبل التمكن من نقل هذه الرسالة. حاول مرة أخرى.",
announceDragReset: "تغيّر الطابور. اسحب مرة أخرى لإعادة ترتيب الرسائل المتبقية.",
editingHint: "جارٍ تعديل الرسالة",
queueingOffHint: "الرسائل الجديدة توقف الرد الحالي وتُنفَّذ بعده.",
queueingOnHint: "الرسائل الجديدة تنتظر دورها وتُنفَّذ بالترتيب.",
queueingHintShared: "تبقى الرسائل الموجودة في الطابور.",
},
picker: {
onDevice: "على الجهاز",
huggingFace: "Hugging Face",

View file

@ -20,6 +20,52 @@ export const de = {
settings: "Eingabeeinstellungen",
preview: "Formatierte Vorschau",
},
promptQueue: {
loading: "Eingereihte Nachrichten werden geladen",
listLabel: "Eingereihte Nachrichten",
regionLabel: "Nachrichtenwarteschlange, {current} von {total}",
itemLabel: "Eingereihte Nachricht {position} von {total}: {prompt}",
reorderInstructions: "Ziehe den Griff, um die Reihenfolge zu ändern. Wenn der Griff fokussiert ist, verschiebst du mit Auf oder Ab um eine Position und mit Pos1 oder Ende an den Anfang oder das Ende.",
dragTooltip: "Zum Umsortieren ziehen",
reorderLabel: "Eingereihte Nachricht {position} von {total} umsortieren",
paused: "Pausiert",
steer: "Umlenken",
steerTooltip: "Die Antwort unterbrechen und diese Nachricht als Nächstes senden",
steerLabel: "Mit eingereihter Nachricht {position} umlenken",
removeTooltip: "Aus der Warteschlange entfernen",
removeLabel: "Eingereihte Nachricht {position} entfernen",
moreTooltip: "Weitere Optionen",
moreLabel: "Weitere Optionen für eingereihte Nachricht {position}",
editItem: "Nachricht bearbeiten",
copyItem: "Nachricht kopieren",
editLabel: "Eingereihte Nachricht {position} bearbeiten",
cancel: "Abbrechen",
save: "Speichern",
turnOffQueueing: "Einreihen ausschalten",
turnOnQueueing: "Einreihen einschalten",
resume: "Warteschlange fortsetzen",
queueButton: "Nachricht einreihen",
steerButton: "Antwort umlenken",
sendTooltip: "Nachricht senden ({shortcut})",
sendLabel: "Nachricht senden",
followUpTooltip: "{action} ({send}) · {opposite} für das Gegenteil",
announceUpdated: "Eingereihte Nachricht aktualisiert.",
announceEditFailed: "Diese Nachricht kann nicht mehr bearbeitet werden, weil sich die Warteschlange geändert hat.",
announceRemoved: "Nachricht aus der Warteschlange entfernt.",
announceSteered: "Diese Nachricht lenkt die Antwort als Nächstes um.",
announceSteerFailed: "Diese Nachricht konnte die Antwort nicht umlenken. Prüfe die Warteschlange und versuche es erneut.",
announceCopied: "Nachricht kopiert.",
announceCopyFailed: "Diese Nachricht konnte nicht kopiert werden. Versuche es erneut.",
announceQueueingOn: "Neue Folgenachrichten werden nach der aktuellen Antwort eingereiht.",
announceQueueingOff: "Neue Folgenachrichten lenken die aktuelle Antwort um.",
announceMoved: "Nachricht an Position {position} von {total} verschoben.",
announceMoveFailed: "Die Warteschlange hat sich geändert, bevor diese Nachricht verschoben werden konnte. Versuche es erneut.",
announceDragReset: "Die Warteschlange hat sich geändert. Ziehe erneut, um die übrigen Nachrichten umzusortieren.",
editingHint: "Nachricht wird bearbeitet",
queueingOffHint: "Neue Nachrichten stoppen die aktuelle Antwort und laufen als Nächstes.",
queueingOnHint: "Neue Nachrichten warten und laufen der Reihe nach.",
queueingHintShared: "Eingereihte Nachrichten bleiben erhalten.",
},
picker: {
onDevice: "Auf dem Gerät",
huggingFace: "Hugging Face",

View file

@ -17,6 +17,57 @@ export const en = {
settings: "Composer settings",
preview: "Formatted preview",
},
promptQueue: {
loading: "Loading queued prompts",
listLabel: "Queued prompts",
regionLabel: "Prompt queue, {current} of {total}",
itemLabel: "Queued prompt {position} of {total}: {prompt}",
reorderInstructions:
"Drag the handle to reorder. With the handle focused, use Up or Down to move one position, or Home or End to move to the front or end.",
dragTooltip: "Drag to reorder",
reorderLabel: "Reorder queued prompt {position} of {total}",
paused: "Paused",
steer: "Steer",
steerTooltip: "Interrupt the response and send this prompt next",
steerLabel: "Steer with queued prompt {position}",
removeTooltip: "Remove from queue",
removeLabel: "Remove queued prompt {position}",
moreTooltip: "More options",
moreLabel: "More options for queued prompt {position}",
editItem: "Edit message",
copyItem: "Copy message",
editLabel: "Edit queued prompt {position}",
cancel: "Cancel",
save: "Save",
turnOffQueueing: "Turn off queueing",
turnOnQueueing: "Turn on queueing",
resume: "Resume queue",
queueButton: "Queue message",
steerButton: "Steer response",
sendTooltip: "Send message ({shortcut})",
sendLabel: "Send message",
followUpTooltip: "{action} ({send}) · {opposite} for the opposite",
announceUpdated: "Queued prompt updated.",
announceEditFailed:
"This prompt can no longer be edited because the queue changed.",
announceRemoved: "Prompt removed from queue.",
announceSteered: "This prompt will steer the response next.",
announceSteerFailed:
"This prompt could not steer the response. Check the queue and try again.",
announceCopied: "Prompt copied.",
announceCopyFailed: "Could not copy this prompt. Try again.",
announceQueueingOn: "New follow-ups will queue after the current response.",
announceQueueingOff: "New follow-ups will steer the current response.",
announceMoved: "Prompt moved to position {position} of {total}.",
announceMoveFailed:
"The queue changed before this prompt could be moved. Try again.",
announceDragReset:
"The queue changed. Drag again to reorder the remaining prompts.",
editingHint: "Editing message",
queueingOffHint: "New messages stop the current response and run next.",
queueingOnHint: "New messages wait their turn and run in order.",
queueingHintShared: "Queued prompts are kept.",
},
picker: {
onDevice: "On Device",
huggingFace: "Hugging Face",

View file

@ -20,6 +20,52 @@ export const es = {
settings: "Ajustes del redactor",
preview: "Vista previa con formato",
},
promptQueue: {
loading: "Cargando mensajes en cola",
listLabel: "Mensajes en cola",
regionLabel: "Cola de mensajes, {current} de {total}",
itemLabel: "Mensaje en cola {position} de {total}: {prompt}",
reorderInstructions: "Arrastra el controlador para reordenar. Con el controlador enfocado, usa Arriba o Abajo para mover una posición, o Inicio o Fin para moverlo al principio o al final.",
dragTooltip: "Arrastra para reordenar",
reorderLabel: "Reordenar el mensaje en cola {position} de {total}",
paused: "En pausa",
steer: "Redirigir",
steerTooltip: "Interrumpir la respuesta y enviar este mensaje a continuación",
steerLabel: "Redirigir con el mensaje en cola {position}",
removeTooltip: "Quitar de la cola",
removeLabel: "Quitar el mensaje en cola {position}",
moreTooltip: "Más opciones",
moreLabel: "Más opciones para el mensaje en cola {position}",
editItem: "Editar mensaje",
copyItem: "Copiar mensaje",
editLabel: "Editar el mensaje en cola {position}",
cancel: "Cancelar",
save: "Guardar",
turnOffQueueing: "Desactivar la cola",
turnOnQueueing: "Activar la cola",
resume: "Reanudar la cola",
queueButton: "Poner el mensaje en cola",
steerButton: "Redirigir la respuesta",
sendTooltip: "Enviar mensaje ({shortcut})",
sendLabel: "Enviar mensaje",
followUpTooltip: "{action} ({send}) · {opposite} para lo contrario",
announceUpdated: "Mensaje en cola actualizado.",
announceEditFailed: "Este mensaje ya no se puede editar porque la cola cambió.",
announceRemoved: "Mensaje quitado de la cola.",
announceSteered: "Este mensaje redirigirá la respuesta a continuación.",
announceSteerFailed: "Este mensaje no pudo redirigir la respuesta. Revisa la cola e inténtalo de nuevo.",
announceCopied: "Mensaje copiado.",
announceCopyFailed: "No se pudo copiar este mensaje. Inténtalo de nuevo.",
announceQueueingOn: "Los nuevos mensajes de seguimiento se pondrán en cola después de la respuesta actual.",
announceQueueingOff: "Los nuevos mensajes de seguimiento redirigirán la respuesta actual.",
announceMoved: "Mensaje movido a la posición {position} de {total}.",
announceMoveFailed: "La cola cambió antes de poder mover este mensaje. Inténtalo de nuevo.",
announceDragReset: "La cola cambió. Arrastra de nuevo para reordenar los mensajes restantes.",
editingHint: "Editando el mensaje",
queueingOffHint: "Los mensajes nuevos detienen la respuesta actual y se ejecutan a continuación.",
queueingOnHint: "Los mensajes nuevos esperan su turno y se ejecutan en orden.",
queueingHintShared: "Los mensajes en cola se conservan.",
},
picker: {
onDevice: "En el dispositivo",
huggingFace: "Hugging Face",

View file

@ -20,6 +20,52 @@ export const fr = {
settings: "Paramètres de saisie",
preview: "Aperçu mis en forme",
},
promptQueue: {
loading: "Chargement des messages en file d’attente",
listLabel: "Messages en file d’attente",
regionLabel: "File d’attente des messages, {current} sur {total}",
itemLabel: "Message en file d’attente {position} sur {total} : {prompt}",
reorderInstructions: "Faites glisser la poignée pour réorganiser. Lorsque la poignée a le focus, utilisez Haut ou Bas pour déplacer d’une position, ou Début ou Fin pour aller au début ou à la fin.",
dragTooltip: "Faire glisser pour réorganiser",
reorderLabel: "Réorganiser le message en file d’attente {position} sur {total}",
paused: "En pause",
steer: "Réorienter",
steerTooltip: "Interrompre la réponse et envoyer ce message en priorité",
steerLabel: "Réorienter avec le message en file d’attente {position}",
removeTooltip: "Retirer de la file d’attente",
removeLabel: "Retirer le message en file d’attente {position}",
moreTooltip: "Plus d’options",
moreLabel: "Plus d’options pour le message en file d’attente {position}",
editItem: "Modifier le message",
copyItem: "Copier le message",
editLabel: "Modifier le message en file d’attente {position}",
cancel: "Annuler",
save: "Enregistrer",
turnOffQueueing: "Désactiver la file d’attente",
turnOnQueueing: "Activer la file d’attente",
resume: "Reprendre la file d’attente",
queueButton: "Mettre le message en file d’attente",
steerButton: "Réorienter la réponse",
sendTooltip: "Envoyer le message ({shortcut})",
sendLabel: "Envoyer le message",
followUpTooltip: "{action} ({send}) · {opposite} pour l’inverse",
announceUpdated: "Message en file d’attente mis à jour.",
announceEditFailed: "Ce message ne peut plus être modifié car la file d’attente a changé.",
announceRemoved: "Message retiré de la file d’attente.",
announceSteered: "Ce message réorientera la réponse en priorité.",
announceSteerFailed: "Ce message n’a pas pu réorienter la réponse. Vérifiez la file d’attente et réessayez.",
announceCopied: "Message copié.",
announceCopyFailed: "Impossible de copier ce message. Réessayez.",
announceQueueingOn: "Les nouveaux messages de suivi seront mis en file d’attente après la réponse en cours.",
announceQueueingOff: "Les nouveaux messages de suivi réorienteront la réponse en cours.",
announceMoved: "Message déplacé à la position {position} sur {total}.",
announceMoveFailed: "La file d’attente a changé avant que ce message puisse être déplacé. Réessayez.",
announceDragReset: "La file d’attente a changé. Faites glisser à nouveau pour réorganiser les messages restants.",
editingHint: "Modification du message",
queueingOffHint: "Les nouveaux messages arrêtent la réponse en cours et sont exécutés ensuite.",
queueingOnHint: "Les nouveaux messages attendent leur tour et sont exécutés dans l’ordre.",
queueingHintShared: "Les messages en file d’attente sont conservés.",
},
picker: {
onDevice: "Sur l'appareil",
huggingFace: "Hugging Face",

View file

@ -20,6 +20,52 @@ export const hi = {
settings: "संदेश इनपुट की सेटिंग",
preview: "फ़ॉर्मैट किया हुआ पूर्वावलोकन",
},
promptQueue: {
loading: "कतार के संदेश लोड हो रहे हैं",
listLabel: "कतार के संदेश",
regionLabel: "संदेश कतार, {total} में से {current}",
itemLabel: "कतार का संदेश {total} में से {position}: {prompt}",
reorderInstructions: "क्रम बदलने के लिए हैंडल को खींचें। हैंडल पर फ़ोकस होने पर ऊपर या नीचे तीर से एक स्थान खिसकाएँ, या Home या End से शुरुआत या अंत में ले जाएँ।",
dragTooltip: "क्रम बदलने के लिए खींचें",
reorderLabel: "कतार के संदेश {total} में से {position} का क्रम बदलें",
paused: "रुका हुआ",
steer: "दिशा बदलें",
steerTooltip: "उत्तर रोककर यह संदेश अगला भेजें",
steerLabel: "कतार के संदेश {position} से दिशा बदलें",
removeTooltip: "कतार से हटाएँ",
removeLabel: "कतार का संदेश {position} हटाएँ",
moreTooltip: "और विकल्प",
moreLabel: "कतार के संदेश {position} के लिए और विकल्प",
editItem: "संदेश संपादित करें",
copyItem: "संदेश कॉपी करें",
editLabel: "कतार का संदेश {position} संपादित करें",
cancel: "रद्द करें",
save: "सहेजें",
turnOffQueueing: "कतार बंद करें",
turnOnQueueing: "कतार चालू करें",
resume: "कतार फिर से शुरू करें",
queueButton: "संदेश कतार में जोड़ें",
steerButton: "उत्तर की दिशा बदलें",
sendTooltip: "संदेश भेजें ({shortcut})",
sendLabel: "संदेश भेजें",
followUpTooltip: "{action} ({send}) · विपरीत के लिए {opposite}",
announceUpdated: "कतार का संदेश अपडेट हो गया।",
announceEditFailed: "कतार बदल जाने के कारण यह संदेश अब संपादित नहीं किया जा सकता।",
announceRemoved: "संदेश कतार से हटा दिया गया।",
announceSteered: "यह संदेश अगला उत्तर की दिशा बदलेगा।",
announceSteerFailed: "यह संदेश उत्तर की दिशा नहीं बदल सका। कतार जाँचें और फिर कोशिश करें।",
announceCopied: "संदेश कॉपी हो गया।",
announceCopyFailed: "यह संदेश कॉपी नहीं हो सका। फिर कोशिश करें।",
announceQueueingOn: "नए अगले संदेश मौजूदा उत्तर के बाद कतार में जुड़ेंगे।",
announceQueueingOff: "नए अगले संदेश मौजूदा उत्तर की दिशा बदलेंगे।",
announceMoved: "संदेश {total} में से {position} स्थान पर ले जाया गया।",
announceMoveFailed: "इस संदेश को खिसकाने से पहले कतार बदल गई। फिर कोशिश करें।",
announceDragReset: "कतार बदल गई। बाकी संदेशों का क्रम बदलने के लिए फिर से खींचें।",
editingHint: "संदेश संपादित हो रहा है",
queueingOffHint: "नए संदेश मौजूदा उत्तर रोककर अगले चलते हैं।",
queueingOnHint: "नए संदेश अपनी बारी का इंतज़ार कर क्रम से चलते हैं।",
queueingHintShared: "कतार के संदेश बने रहते हैं।",
},
picker: {
onDevice: "डिवाइस पर",
huggingFace: "Hugging Face",

View file

@ -17,6 +17,52 @@ export const it = {
settings: "Impostazioni di composizione",
preview: "Anteprima formattata",
},
promptQueue: {
loading: "Caricamento dei messaggi in coda",
listLabel: "Messaggi in coda",
regionLabel: "Coda dei messaggi, {current} di {total}",
itemLabel: "Messaggio in coda {position} di {total}: {prompt}",
reorderInstructions: "Trascina la maniglia per riordinare. Con la maniglia attiva, usa Su o Giù per spostare di una posizione, oppure Home o Fine per andare all’inizio o alla fine.",
dragTooltip: "Trascina per riordinare",
reorderLabel: "Riordina il messaggio in coda {position} di {total}",
paused: "In pausa",
steer: "Riorienta",
steerTooltip: "Interrompi la risposta e invia subito dopo questo messaggio",
steerLabel: "Riorienta con il messaggio in coda {position}",
removeTooltip: "Rimuovi dalla coda",
removeLabel: "Rimuovi il messaggio in coda {position}",
moreTooltip: "Altre opzioni",
moreLabel: "Altre opzioni per il messaggio in coda {position}",
editItem: "Modifica messaggio",
copyItem: "Copia messaggio",
editLabel: "Modifica il messaggio in coda {position}",
cancel: "Annulla",
save: "Salva",
turnOffQueueing: "Disattiva la coda",
turnOnQueueing: "Attiva la coda",
resume: "Riprendi la coda",
queueButton: "Metti il messaggio in coda",
steerButton: "Riorienta la risposta",
sendTooltip: "Invia messaggio ({shortcut})",
sendLabel: "Invia messaggio",
followUpTooltip: "{action} ({send}) · {opposite} per l’opposto",
announceUpdated: "Messaggio in coda aggiornato.",
announceEditFailed: "Questo messaggio non può più essere modificato perché la coda è cambiata.",
announceRemoved: "Messaggio rimosso dalla coda.",
announceSteered: "Questo messaggio riorienterà la risposta subito dopo.",
announceSteerFailed: "Non è stato possibile riorientare la risposta con questo messaggio. Controlla la coda e riprova.",
announceCopied: "Messaggio copiato.",
announceCopyFailed: "Non è stato possibile copiare questo messaggio. Riprova.",
announceQueueingOn: "I nuovi messaggi successivi verranno messi in coda dopo la risposta corrente.",
announceQueueingOff: "I nuovi messaggi successivi riorienteranno la risposta corrente.",
announceMoved: "Messaggio spostato alla posizione {position} di {total}.",
announceMoveFailed: "La coda è cambiata prima che questo messaggio potesse essere spostato. Riprova.",
announceDragReset: "La coda è cambiata. Trascina di nuovo per riordinare i messaggi rimanenti.",
editingHint: "Modifica del messaggio",
queueingOffHint: "I nuovi messaggi interrompono la risposta corrente e vengono eseguiti subito dopo.",
queueingOnHint: "I nuovi messaggi attendono il turno e vengono eseguiti in ordine.",
queueingHintShared: "I messaggi in coda vengono mantenuti.",
},
common: {
cancel: "Annulla",
close: "Chiudi",

View file

@ -21,6 +21,52 @@ export const ja = {
settings: "入力欄の設定",
preview: "書式付きプレビュー",
},
promptQueue: {
loading: "キューのメッセージを読み込んでいます",
listLabel: "キューのメッセージ",
regionLabel: "メッセージキュー、{total} 件中 {current} 件目",
itemLabel: "キューのメッセージ {total} 件中 {position} 件目: {prompt}",
reorderInstructions: "ハンドルをドラッグすると並べ替えできます。ハンドルにフォーカスがある状態で、上下キーで 1 つ移動、Home または End で先頭または末尾に移動します。",
dragTooltip: "ドラッグして並べ替え",
reorderLabel: "キューのメッセージ {total} 件中 {position} 件目を並べ替え",
paused: "一時停止中",
steer: "方向を変更",
steerTooltip: "現在の応答を中断し、このメッセージを次に送信します",
steerLabel: "キューのメッセージ {position} で方向を変更",
removeTooltip: "キューから削除",
removeLabel: "キューのメッセージ {position} を削除",
moreTooltip: "その他の操作",
moreLabel: "キューのメッセージ {position} のその他の操作",
editItem: "メッセージを編集",
copyItem: "メッセージをコピー",
editLabel: "キューのメッセージ {position} を編集",
cancel: "キャンセル",
save: "保存",
turnOffQueueing: "キューをオフにする",
turnOnQueueing: "キューをオンにする",
resume: "キューを再開",
queueButton: "メッセージをキューに追加",
steerButton: "応答の方向を変更",
sendTooltip: "メッセージを送信({shortcut})",
sendLabel: "メッセージを送信",
followUpTooltip: "{action}({send})・{opposite} で逆の動作",
announceUpdated: "キューのメッセージを更新しました。",
announceEditFailed: "キューが変更されたため、このメッセージは編集できません。",
announceRemoved: "メッセージをキューから削除しました。",
announceSteered: "このメッセージで次の応答の方向を変更します。",
announceSteerFailed: "このメッセージで方向を変更できませんでした。キューを確認して再試行してください。",
announceCopied: "メッセージをコピーしました。",
announceCopyFailed: "このメッセージをコピーできませんでした。再試行してください。",
announceQueueingOn: "新しい追加メッセージは現在の応答の後にキューへ追加されます。",
announceQueueingOff: "新しい追加メッセージは現在の応答の方向を変更します。",
announceMoved: "メッセージを {total} 件中 {position} 番目に移動しました。",
announceMoveFailed: "移動前にキューが変更されました。再試行してください。",
announceDragReset: "キューが変更されました。残りのメッセージをもう一度ドラッグして並べ替えてください。",
editingHint: "メッセージを編集中",
queueingOffHint: "新しいメッセージは現在の応答を停止して次に実行されます。",
queueingOnHint: "新しいメッセージは順番待ちになり、順に実行されます。",
queueingHintShared: "キュー内のメッセージは保持されます。",
},
picker: {
onDevice: "デバイス上",
huggingFace: "Hugging Face",

View file

@ -20,6 +20,52 @@ export const ko = {
settings: "메시지 입력 설정",
preview: "서식 미리보기",
},
promptQueue: {
loading: "대기열 메시지를 불러오는 중",
listLabel: "대기열 메시지",
regionLabel: "메시지 대기열, {total}개 중 {current}번째",
itemLabel: "대기열 메시지 {total}개 중 {position}번째: {prompt}",
reorderInstructions: "핸들을 드래그해 순서를 바꿉니다. 핸들에 포커스가 있을 때 위아래 화살표로 한 칸 이동하고, Home 또는 End로 맨 앞이나 맨 뒤로 이동합니다.",
dragTooltip: "드래그해서 순서 변경",
reorderLabel: "대기열 메시지 {total}개 중 {position}번째 순서 변경",
paused: "일시 중지됨",
steer: "방향 변경",
steerTooltip: "현재 응답을 중단하고 이 메시지를 다음에 전송합니다",
steerLabel: "대기열 메시지 {position}(으)로 방향 변경",
removeTooltip: "대기열에서 제거",
removeLabel: "대기열 메시지 {position} 제거",
moreTooltip: "옵션 더 보기",
moreLabel: "대기열 메시지 {position}의 옵션 더 보기",
editItem: "메시지 편집",
copyItem: "메시지 복사",
editLabel: "대기열 메시지 {position} 편집",
cancel: "취소",
save: "저장",
turnOffQueueing: "대기열 사용 안 함",
turnOnQueueing: "대기열 사용",
resume: "대기열 재개",
queueButton: "메시지 대기열에 추가",
steerButton: "응답 방향 변경",
sendTooltip: "메시지 전송({shortcut})",
sendLabel: "메시지 전송",
followUpTooltip: "{action}({send}) · {opposite}(으)로 반대 동작",
announceUpdated: "대기열 메시지를 업데이트했습니다.",
announceEditFailed: "대기열이 변경되어 이 메시지를 더 이상 편집할 수 없습니다.",
announceRemoved: "메시지를 대기열에서 제거했습니다.",
announceSteered: "이 메시지로 다음 응답의 방향을 변경합니다.",
announceSteerFailed: "이 메시지로 방향을 변경하지 못했습니다. 대기열을 확인한 뒤 다시 시도하세요.",
announceCopied: "메시지를 복사했습니다.",
announceCopyFailed: "이 메시지를 복사하지 못했습니다. 다시 시도하세요.",
announceQueueingOn: "새 후속 메시지는 현재 응답 뒤에 대기합니다.",
announceQueueingOff: "새 후속 메시지는 현재 응답의 방향을 변경합니다.",
announceMoved: "메시지를 {total}개 중 {position}번째로 옮겼습니다.",
announceMoveFailed: "옮기기 전에 대기열이 변경되었습니다. 다시 시도하세요.",
announceDragReset: "대기열이 변경되었습니다. 남은 메시지를 다시 드래그해 순서를 바꾸세요.",
editingHint: "메시지 편집 중",
queueingOffHint: "새 메시지가 현재 응답을 중지하고 다음에 실행됩니다.",
queueingOnHint: "새 메시지는 순서를 기다렸다가 차례로 실행됩니다.",
queueingHintShared: "대기 중인 메시지는 유지됩니다.",
},
picker: {
onDevice: "기기",
huggingFace: "Hugging Face",

View file

@ -20,6 +20,52 @@ export const ptBR = {
settings: "Configurações de composição",
preview: "Prévia formatada",
},
promptQueue: {
loading: "Carregando mensagens na fila",
listLabel: "Mensagens na fila",
regionLabel: "Fila de mensagens, {current} de {total}",
itemLabel: "Mensagem na fila {position} de {total}: {prompt}",
reorderInstructions: "Arraste a alça para reordenar. Com a alça em foco, use Para cima ou Para baixo para mover uma posição, ou Home ou End para ir ao início ou ao fim.",
dragTooltip: "Arraste para reordenar",
reorderLabel: "Reordenar a mensagem na fila {position} de {total}",
paused: "Pausada",
steer: "Redirecionar",
steerTooltip: "Interromper a resposta e enviar esta mensagem em seguida",
steerLabel: "Redirecionar com a mensagem na fila {position}",
removeTooltip: "Remover da fila",
removeLabel: "Remover a mensagem na fila {position}",
moreTooltip: "Mais opções",
moreLabel: "Mais opções para a mensagem na fila {position}",
editItem: "Editar mensagem",
copyItem: "Copiar mensagem",
editLabel: "Editar a mensagem na fila {position}",
cancel: "Cancelar",
save: "Salvar",
turnOffQueueing: "Desativar o enfileiramento",
turnOnQueueing: "Ativar o enfileiramento",
resume: "Retomar a fila",
queueButton: "Enfileirar mensagem",
steerButton: "Redirecionar a resposta",
sendTooltip: "Enviar mensagem ({shortcut})",
sendLabel: "Enviar mensagem",
followUpTooltip: "{action} ({send}) · {opposite} para o oposto",
announceUpdated: "Mensagem na fila atualizada.",
announceEditFailed: "Esta mensagem não pode mais ser editada porque a fila mudou.",
announceRemoved: "Mensagem removida da fila.",
announceSteered: "Esta mensagem vai redirecionar a resposta em seguida.",
announceSteerFailed: "Não foi possível redirecionar a resposta com esta mensagem. Verifique a fila e tente de novo.",
announceCopied: "Mensagem copiada.",
announceCopyFailed: "Não foi possível copiar esta mensagem. Tente de novo.",
announceQueueingOn: "Novas mensagens de acompanhamento entrarão na fila depois da resposta atual.",
announceQueueingOff: "Novas mensagens de acompanhamento vão redirecionar a resposta atual.",
announceMoved: "Mensagem movida para a posição {position} de {total}.",
announceMoveFailed: "A fila mudou antes que esta mensagem pudesse ser movida. Tente de novo.",
announceDragReset: "A fila mudou. Arraste de novo para reordenar as mensagens restantes.",
editingHint: "Editando a mensagem",
queueingOffHint: "Mensagens novas interrompem a resposta atual e são executadas em seguida.",
queueingOnHint: "Mensagens novas esperam a vez e são executadas em ordem.",
queueingHintShared: "As mensagens na fila são mantidas.",
},
picker: {
onDevice: "No dispositivo",
huggingFace: "Hugging Face",

View file

@ -20,6 +20,52 @@ export const ru = {
settings: "Настройки ввода",
preview: "Предпросмотр форматирования",
},
promptQueue: {
loading: "Загрузка сообщений из очереди",
listLabel: "Сообщения в очереди",
regionLabel: "Очередь сообщений, {current} из {total}",
itemLabel: "Сообщение в очереди {position} из {total}: {prompt}",
reorderInstructions: "Перетащите маркер, чтобы изменить порядок. Когда маркер в фокусе, используйте стрелки вверх или вниз для перемещения на одну позицию либо Home или End для перехода в начало или конец.",
dragTooltip: "Перетащите, чтобы изменить порядок",
reorderLabel: "Изменить порядок сообщения в очереди {position} из {total}",
paused: "Приостановлено",
steer: "Перенаправить",
steerTooltip: "Прервать ответ и отправить это сообщение следующим",
steerLabel: "Перенаправить сообщением из очереди {position}",
removeTooltip: "Убрать из очереди",
removeLabel: "Убрать сообщение из очереди {position}",
moreTooltip: "Дополнительно",
moreLabel: "Дополнительные действия для сообщения в очереди {position}",
editItem: "Изменить сообщение",
copyItem: "Скопировать сообщение",
editLabel: "Изменить сообщение в очереди {position}",
cancel: "Отмена",
save: "Сохранить",
turnOffQueueing: "Отключить очередь",
turnOnQueueing: "Включить очередь",
resume: "Возобновить очередь",
queueButton: "Поставить сообщение в очередь",
steerButton: "Перенаправить ответ",
sendTooltip: "Отправить сообщение ({shortcut})",
sendLabel: "Отправить сообщение",
followUpTooltip: "{action} ({send}) · {opposite} для обратного действия",
announceUpdated: "Сообщение в очереди обновлено.",
announceEditFailed: "Это сообщение больше нельзя изменить, потому что очередь изменилась.",
announceRemoved: "Сообщение убрано из очереди.",
announceSteered: "Это сообщение перенаправит ответ следующим.",
announceSteerFailed: "Не удалось перенаправить ответ этим сообщением. Проверьте очередь и повторите попытку.",
announceCopied: "Сообщение скопировано.",
announceCopyFailed: "Не удалось скопировать это сообщение. Повторите попытку.",
announceQueueingOn: "Новые последующие сообщения встанут в очередь после текущего ответа.",
announceQueueingOff: "Новые последующие сообщения перенаправят текущий ответ.",
announceMoved: "Сообщение перемещено на позицию {position} из {total}.",
announceMoveFailed: "Очередь изменилась до того, как сообщение удалось переместить. Повторите попытку.",
announceDragReset: "Очередь изменилась. Перетащите снова, чтобы изменить порядок оставшихся сообщений.",
editingHint: "Изменение сообщения",
queueingOffHint: "Новые сообщения останавливают текущий ответ и выполняются следующими.",
queueingOnHint: "Новые сообщения ждут очереди и выполняются по порядку.",
queueingHintShared: "Сообщения в очереди сохраняются.",
},
picker: {
onDevice: "На устройстве",
huggingFace: "Hugging Face",

View file

@ -20,6 +20,52 @@ export const zhCN = {
settings: "输入设置",
preview: "格式化预览",
},
promptQueue: {
loading: "正在加载排队的消息",
listLabel: "排队的消息",
regionLabel: "消息队列,第 {current} 条,共 {total} 条",
itemLabel: "排队消息 {position}/{total}:{prompt}",
reorderInstructions: "拖动手柄可重新排序。手柄获得焦点时,按上下方向键可移动一个位置,按 Home 或 End 可移到开头或末尾。",
dragTooltip: "拖动以重新排序",
reorderLabel: "重新排序排队消息 {position}/{total}",
paused: "已暂停",
steer: "调整方向",
steerTooltip: "中断当前回复并优先发送此消息",
steerLabel: "用排队消息 {position} 调整方向",
removeTooltip: "从队列中移除",
removeLabel: "移除排队消息 {position}",
moreTooltip: "更多选项",
moreLabel: "排队消息 {position} 的更多选项",
editItem: "编辑消息",
copyItem: "复制消息",
editLabel: "编辑排队消息 {position}",
cancel: "取消",
save: "保存",
turnOffQueueing: "关闭排队",
turnOnQueueing: "开启排队",
resume: "恢复队列",
queueButton: "将消息排队",
steerButton: "调整回复方向",
sendTooltip: "发送消息({shortcut})",
sendLabel: "发送消息",
followUpTooltip: "{action}({send})· {opposite} 执行相反操作",
announceUpdated: "排队消息已更新。",
announceEditFailed: "队列已变更,无法再编辑此消息。",
announceRemoved: "消息已从队列中移除。",
announceSteered: "此消息将用于调整下一步回复方向。",
announceSteerFailed: "无法用此消息调整回复方向。请检查队列后重试。",
announceCopied: "消息已复制。",
announceCopyFailed: "无法复制此消息。请重试。",
announceQueueingOn: "新的后续消息将排在当前回复之后。",
announceQueueingOff: "新的后续消息将调整当前回复的方向。",
announceMoved: "消息已移至第 {position} 位,共 {total} 位。",
announceMoveFailed: "移动前队列已发生变更。请重试。",
announceDragReset: "队列已变更。请重新拖动以排序其余消息。",
editingHint: "正在编辑消息",
queueingOffHint: "新消息会停止当前回复并优先执行。",
queueingOnHint: "新消息会排队并按顺序执行。",
queueingHintShared: "已排队的消息会保留。",
},
picker: {
onDevice: "本机",
huggingFace: "Hugging Face",

View file

@ -0,0 +1,241 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Queue view localization and the editor's send-shortcut chord; no backend needed.
Run: python tests/studio/playwright_queue_localization.py
PW_ENGINE=webkit selects WebKit.
"""
import os
from playwright.sync_api import expect, sync_playwright
from _playwright_robust import start_vite, stop_process, wait_for_smoke_page
PAGE = "/smoke-prompt-queue-actions.html"
ENTRY = "/smoke-prompt-queue-actions-main.tsx"
# The queue view must not fall back to English while Settings shows the
# translated Queue/Steer wording from the same catalog.
JA = {
"steer": "方向を変更",
"edit": "メッセージを編集",
"copy": "メッセージをコピー",
"queueing": "キューをオフにする",
"removed": "メッセージをキューから削除しました。",
}
def seed(page, base, *, locale = "en", shortcut = "enter"):
page.goto(base + PAGE)
page.evaluate(
"""([locale, shortcut]) => {
localStorage.setItem("unsloth_locale", locale);
localStorage.setItem(
"unsloth_chat_preferences",
JSON.stringify({ state: { sendShortcut: shortcut }, version: 0 }),
);
}""",
[locale, shortcut],
)
page.reload()
page.get_by_role("button", name = "Reset fixture", exact = True).wait_for(
state = "visible", timeout = 60_000
)
def open_editor(page, more_name, edit_name):
page.get_by_role("button", name = more_name, exact = True).click()
page.get_by_role("menuitem", name = edit_name, exact = True).click()
editor = page.locator('textarea[aria-label^="Edit queued prompt"]')
expect(editor).to_be_visible()
return editor
def check_localized(page, base):
seed(page, base, locale = "ja")
# setLocale loads the catalog lazily; the harness never calls initializeLocale.
page.evaluate(
"async () => { const m = await import('/src/i18n/index.ts'); await m.setLocale('ja'); }"
)
row = page.locator("[data-queue-item-id]").first
expect(row).to_contain_text(JA["steer"])
expect(page.locator('[aria-label^="キューのメッセージ 1"]').first).to_be_visible()
page.get_by_role("button", name = "キューのメッセージ 1 のその他の操作", exact = True).click()
for name in (JA["edit"], JA["copy"], JA["queueing"]):
expect(page.get_by_role("menuitem", name = name, exact = True)).to_be_visible()
page.keyboard.press("Escape")
page.get_by_role("button", name = "キューのメッセージ 2 を削除", exact = True).click()
expect(page.locator('div[role="status"][aria-live="polite"]')).to_have_text(JA["removed"])
print("PASS: queue view, menu and announcements follow the selected locale", flush = True)
def check_editor_shortcut(page, base):
# Default "Enter" sends in the composer, so it saves here; Shift+Enter is a newline.
seed(page, base, shortcut = "enter")
editor = open_editor(page, "More options for queued prompt 1", "Edit message")
editor.fill("shift-stays-open")
editor.press("Shift+Enter")
expect(editor).to_be_visible()
expect(page.locator("[data-queue-item-id]").first).to_have_attribute(
"aria-label", "Queued prompt 1 of 3: First prompt"
)
editor.fill("saved-with-enter")
editor.press("Enter")
expect(editor).to_have_count(0)
expect(page.locator("[data-queue-item-id]").first).to_contain_text("saved-with-enter")
expect(page.get_by_label("Composer submissions", exact = True)).to_have_text("0")
# "mod-enter" moves the chord: plain Enter becomes a newline.
seed(page, base, shortcut = "mod-enter")
editor = open_editor(page, "More options for queued prompt 1", "Edit message")
editor.fill("plain-enter-stays-open")
editor.press("Enter")
expect(editor).to_be_visible()
# The row renders the open editor, so read the committed prompt off the label.
expect(page.locator("[data-queue-item-id]").first).to_have_attribute(
"aria-label", "Queued prompt 1 of 3: First prompt"
)
editor.fill("saved-with-mod-enter")
editor.press("ControlOrMeta+Enter")
expect(editor).to_have_count(0)
expect(page.locator("[data-queue-item-id]").first).to_contain_text("saved-with-mod-enter")
expect(page.get_by_label("Composer submissions", exact = True)).to_have_text("0")
print("PASS: the queued-prompt editor follows the send-shortcut preference", flush = True)
def check_escape_during_ime(page, base):
# An IME consumes Escape to close its candidate window and reports the key
# as composing, so the editor must keep the draft rather than cancel.
seed(page, base)
editor = open_editor(page, "More options for queued prompt 1", "Edit message")
editor.fill("draft-survives-ime-escape")
for init in ('{isComposing: true}', '{keyCode: 229}'):
page.evaluate(
f"""() => {{
const ta = document.querySelector('textarea[aria-label^="Edit queued prompt"]');
ta.dispatchEvent(new KeyboardEvent('keydown', Object.assign(
{{key: 'Escape', bubbles: true, cancelable: true}}, {init})));
}}"""
)
expect(editor).to_be_visible()
expect(editor).to_have_value("draft-survives-ime-escape")
# Escape with no composition still cancels.
editor.press("Escape")
expect(editor).to_have_count(0)
expect(page.locator("[data-queue-item-id]").first).to_have_attribute(
"aria-label", "Queued prompt 1 of 3: First prompt"
)
print("PASS: Escape keeps the draft while an IME is composing", flush = True)
def check_candidate_confirming_enter(page, base):
# A candidate-confirming Enter can arrive with isComposing false and no key
# code 229, so per-event flags alone would save the pre-edit text.
seed(page, base, shortcut = "enter")
editor = open_editor(page, "More options for queued prompt 1", "Edit message")
editor.fill("composition-in-progress")
page.evaluate(
"""() => {
const ta = document.querySelector('textarea[aria-label^="Edit queued prompt"]');
ta.dispatchEvent(new CompositionEvent('compositionstart', {bubbles: true}));
ta.dispatchEvent(new KeyboardEvent('keydown',
{key: 'Enter', bubbles: true, cancelable: true}));
}"""
)
expect(editor).to_be_visible()
expect(page.locator("[data-queue-item-id]").first).to_have_attribute(
"aria-label", "Queued prompt 1 of 3: First prompt"
)
# After the composition ends the same Enter saves.
page.evaluate(
"""() => {
const ta = document.querySelector('textarea[aria-label^="Edit queued prompt"]');
ta.dispatchEvent(new CompositionEvent('compositionend', {bubbles: true}));
}"""
)
editor.press("Enter")
expect(editor).to_have_count(0)
expect(page.locator("[data-queue-item-id]").first).to_contain_text(
"composition-in-progress"
)
print("PASS: a candidate-confirming Enter does not save the edit", flush = True)
def start_composition(page):
page.evaluate(
"""() => {
const ta = document.querySelector('textarea[aria-label^="Edit queued prompt"]');
ta.dispatchEvent(new CompositionEvent('compositionstart', {bubbles: true}));
}"""
)
def check_stuck_composition_recovers(page, base):
# Some IMEs never send compositionend. The gate must not wedge Enter.
seed(page, base, shortcut = "enter")
editor = open_editor(page, "More options for queued prompt 1", "Edit message")
editor.fill("recovers-after-timeout")
start_composition(page)
editor.press("Enter")
expect(editor).to_be_visible()
# The watchdog drops the flag, so the next Enter saves.
page.wait_for_timeout(2800)
editor.press("Enter")
expect(editor).to_have_count(0)
expect(page.locator("[data-queue-item-id]").first).to_contain_text(
"recovers-after-timeout"
)
# Blur is the other reset point.
editor = open_editor(page, "More options for queued prompt 2", "Edit message")
editor.fill("recovers-after-blur")
start_composition(page)
editor.blur()
editor.focus()
editor.press("Enter")
expect(editor).to_have_count(0)
expect(page.locator("[data-queue-item-id]").nth(1)).to_contain_text(
"recovers-after-blur"
)
print("PASS: a stuck composition recovers on timeout and on blur", flush = True)
def main():
server = None
try:
base = os.environ.get("PW_BASE_URL", "").rstrip("/")
if not base:
port = int(os.environ.get("PW_PORT", "5424"))
server = start_vite(port)
base = f"http://127.0.0.1:{port}"
wait_for_smoke_page(base + PAGE, ENTRY, proc = server)
with sync_playwright() as pw:
options = {"headless": True}
if os.environ.get("PW_EXECUTABLE"):
options["executable_path"] = os.environ["PW_EXECUTABLE"]
if os.environ.get("PW_CHANNEL"):
options["channel"] = os.environ["PW_CHANNEL"]
browser = getattr(pw, os.environ.get("PW_ENGINE", "chromium")).launch(**options)
print(
f"Browser: {os.environ.get('PW_CHANNEL', os.environ.get('PW_ENGINE', 'chromium'))} {browser.version}",
flush = True,
)
try:
page = browser.new_page(viewport = {"width": 1100, "height": 850})
errors = []
page.on("pageerror", lambda error: errors.append(str(error)))
check_localized(page, base)
check_editor_shortcut(page, base)
check_escape_during_ime(page, base)
check_candidate_confirming_enter(page, base)
check_stuck_composition_recovers(page, base)
assert not errors, errors
finally:
browser.close()
finally:
if server:
stop_process(server)
if __name__ == "__main__":
main()