fix(app): paste clipboard images on mobile (#2793)
Native composers had no path from the device clipboard into image attachments. Route clipboard data URLs through the existing attachment persistence flow and expose the action only in the native attachment sheet.
This commit is contained in:
@@ -143,20 +143,27 @@ function browserElementWorkspaceAttachment(): Extract<
|
|||||||
|
|
||||||
function createFakePersister(): AttachmentPersister & {
|
function createFakePersister(): AttachmentPersister & {
|
||||||
blobCalls: Array<{ blob: Blob; mimeType: string; fileName: string | null }>;
|
blobCalls: Array<{ blob: Blob; mimeType: string; fileName: string | null }>;
|
||||||
|
dataUrlCalls: Array<{ dataUrl: string; mimeType: string; fileName: string | null }>;
|
||||||
fileUriCalls: Array<{ uri: string; mimeType: string; fileName: string | null }>;
|
fileUriCalls: Array<{ uri: string; mimeType: string; fileName: string | null }>;
|
||||||
deletedBatches: AttachmentMetadata[][];
|
deletedBatches: AttachmentMetadata[][];
|
||||||
} {
|
} {
|
||||||
const blobCalls: Array<{ blob: Blob; mimeType: string; fileName: string | null }> = [];
|
const blobCalls: Array<{ blob: Blob; mimeType: string; fileName: string | null }> = [];
|
||||||
|
const dataUrlCalls: Array<{ dataUrl: string; mimeType: string; fileName: string | null }> = [];
|
||||||
const fileUriCalls: Array<{ uri: string; mimeType: string; fileName: string | null }> = [];
|
const fileUriCalls: Array<{ uri: string; mimeType: string; fileName: string | null }> = [];
|
||||||
const deletedBatches: AttachmentMetadata[][] = [];
|
const deletedBatches: AttachmentMetadata[][] = [];
|
||||||
return {
|
return {
|
||||||
blobCalls,
|
blobCalls,
|
||||||
|
dataUrlCalls,
|
||||||
fileUriCalls,
|
fileUriCalls,
|
||||||
deletedBatches,
|
deletedBatches,
|
||||||
persistFromBlob: async ({ blob, mimeType, fileName }) => {
|
persistFromBlob: async ({ blob, mimeType, fileName }) => {
|
||||||
blobCalls.push({ blob, mimeType, fileName });
|
blobCalls.push({ blob, mimeType, fileName });
|
||||||
return { ...imageMetadata, id: `blob-${blobCalls.length}` };
|
return { ...imageMetadata, id: `blob-${blobCalls.length}` };
|
||||||
},
|
},
|
||||||
|
persistFromDataUrl: async ({ dataUrl, mimeType, fileName }) => {
|
||||||
|
dataUrlCalls.push({ dataUrl, mimeType, fileName });
|
||||||
|
return { ...imageMetadata, id: `data-url-${dataUrlCalls.length}` };
|
||||||
|
},
|
||||||
persistFromFileUri: async ({ uri, mimeType, fileName }) => {
|
persistFromFileUri: async ({ uri, mimeType, fileName }) => {
|
||||||
fileUriCalls.push({ uri, mimeType, fileName });
|
fileUriCalls.push({ uri, mimeType, fileName });
|
||||||
return { ...imageMetadata, id: `uri-${fileUriCalls.length}` };
|
return { ...imageMetadata, id: `uri-${fileUriCalls.length}` };
|
||||||
@@ -388,6 +395,25 @@ describe("pickAndPersistImages", () => {
|
|||||||
]);
|
]);
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("persists data_url sources via persistFromDataUrl", async () => {
|
||||||
|
const persister = createFakePersister();
|
||||||
|
const dataUrl = "data:image/png;base64,AAEC";
|
||||||
|
const result = await pickAndPersistImages({
|
||||||
|
pickImages: async () => [
|
||||||
|
{
|
||||||
|
source: { kind: "data_url", dataUrl },
|
||||||
|
mimeType: "image/png",
|
||||||
|
fileName: "clipboard.png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
persister,
|
||||||
|
});
|
||||||
|
expect(persister.dataUrlCalls).toEqual([
|
||||||
|
{ dataUrl, mimeType: "image/png", fileName: "clipboard.png" },
|
||||||
|
]);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("dispatchComposerAgentMessage", () => {
|
describe("dispatchComposerAgentMessage", () => {
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ export interface AttachmentPersister {
|
|||||||
mimeType: string;
|
mimeType: string;
|
||||||
fileName: string | null;
|
fileName: string | null;
|
||||||
}) => Promise<AttachmentMetadata>;
|
}) => Promise<AttachmentMetadata>;
|
||||||
|
persistFromDataUrl: (input: {
|
||||||
|
dataUrl: string;
|
||||||
|
mimeType: string;
|
||||||
|
fileName: string | null;
|
||||||
|
}) => Promise<AttachmentMetadata>;
|
||||||
deleteAttachments: (metadata: AttachmentMetadata[]) => Promise<void> | void;
|
deleteAttachments: (metadata: AttachmentMetadata[]) => Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +85,10 @@ export interface QueueWriter {
|
|||||||
|
|
||||||
export async function pickAndPersistImages(input: {
|
export async function pickAndPersistImages(input: {
|
||||||
pickImages: () => Promise<PickedImageAttachmentInput[] | null>;
|
pickImages: () => Promise<PickedImageAttachmentInput[] | null>;
|
||||||
persister: Pick<AttachmentPersister, "persistFromBlob" | "persistFromFileUri">;
|
persister: Pick<
|
||||||
|
AttachmentPersister,
|
||||||
|
"persistFromBlob" | "persistFromFileUri" | "persistFromDataUrl"
|
||||||
|
>;
|
||||||
}): Promise<AttachmentMetadata[]> {
|
}): Promise<AttachmentMetadata[]> {
|
||||||
const result = await input.pickImages();
|
const result = await input.pickImages();
|
||||||
if (!result?.length) return [];
|
if (!result?.length) return [];
|
||||||
@@ -95,6 +103,13 @@ export async function pickAndPersistImages(input: {
|
|||||||
fileName,
|
fileName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (picked.source.kind === "data_url") {
|
||||||
|
return await input.persister.persistFromDataUrl({
|
||||||
|
dataUrl: picked.source.dataUrl,
|
||||||
|
mimeType,
|
||||||
|
fileName,
|
||||||
|
});
|
||||||
|
}
|
||||||
return await input.persister.persistFromFileUri({
|
return await input.persister.persistFromFileUri({
|
||||||
uri: picked.source.uri,
|
uri: picked.source.uri,
|
||||||
mimeType,
|
mimeType,
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { readClipboardImage, type ClipboardImageReader } from "./clipboard-image";
|
||||||
|
|
||||||
|
function createClipboard(input: {
|
||||||
|
hasImage: boolean;
|
||||||
|
imageData?: string | null;
|
||||||
|
}): ClipboardImageReader {
|
||||||
|
return {
|
||||||
|
hasImageAsync: async () => input.hasImage,
|
||||||
|
getImageAsync: async () =>
|
||||||
|
input.imageData === undefined || input.imageData === null ? null : { data: input.imageData },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("readClipboardImage", () => {
|
||||||
|
it("returns null when the clipboard does not contain an image", async () => {
|
||||||
|
await expect(readClipboardImage(createClipboard({ hasImage: false }))).resolves.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the clipboard image as a persistable data URL", async () => {
|
||||||
|
const dataUrl = "data:image/png;base64,AAEC";
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
readClipboardImage(createClipboard({ hasImage: true, imageData: dataUrl })),
|
||||||
|
).resolves.toEqual({
|
||||||
|
source: { kind: "data_url", dataUrl },
|
||||||
|
mimeType: "image/png",
|
||||||
|
fileName: "clipboard.png",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when the clipboard image disappears before it is read", async () => {
|
||||||
|
await expect(
|
||||||
|
readClipboardImage(createClipboard({ hasImage: true, imageData: null })),
|
||||||
|
).resolves.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { PickedImageAttachmentInput } from "@/hooks/image-attachment-picker";
|
||||||
|
|
||||||
|
interface ClipboardImage {
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClipboardImageReader {
|
||||||
|
hasImageAsync: () => Promise<boolean>;
|
||||||
|
getImageAsync: (options: { format: "png" }) => Promise<ClipboardImage | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readClipboardImage(
|
||||||
|
clipboard: ClipboardImageReader,
|
||||||
|
): Promise<PickedImageAttachmentInput | null> {
|
||||||
|
if (!(await clipboard.hasImageAsync())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = await clipboard.getImageAsync({ format: "png" });
|
||||||
|
if (!image) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
source: { kind: "data_url", dataUrl: image.data },
|
||||||
|
mimeType: "image/png",
|
||||||
|
fileName: "clipboard.png",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -30,8 +30,10 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
GitPullRequest,
|
GitPullRequest,
|
||||||
Image as ImageIcon,
|
Image as ImageIcon,
|
||||||
|
ClipboardPaste,
|
||||||
Paperclip,
|
Paperclip,
|
||||||
} from "lucide-react-native";
|
} from "lucide-react-native";
|
||||||
|
import * as Clipboard from "expo-clipboard";
|
||||||
import Animated from "react-native-reanimated";
|
import Animated from "react-native-reanimated";
|
||||||
import { FOOTER_HEIGHT, MAX_CONTENT_WIDTH } from "@/constants/layout";
|
import { FOOTER_HEIGHT, MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||||
import {
|
import {
|
||||||
@@ -64,6 +66,7 @@ import {
|
|||||||
sendQueuedComposerMessageNow,
|
sendQueuedComposerMessageNow,
|
||||||
toggleGithubAttachmentFromPicker,
|
toggleGithubAttachmentFromPicker,
|
||||||
uploadFileAttachments,
|
uploadFileAttachments,
|
||||||
|
type AttachmentPersister,
|
||||||
type QueueWriter,
|
type QueueWriter,
|
||||||
type QueuedComposerMessage,
|
type QueuedComposerMessage,
|
||||||
} from "@/composer/actions";
|
} from "@/composer/actions";
|
||||||
@@ -82,6 +85,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
deleteAttachments,
|
deleteAttachments,
|
||||||
persistAttachmentFromBlob,
|
persistAttachmentFromBlob,
|
||||||
|
persistAttachmentFromDataUrl,
|
||||||
persistAttachmentFromFileUri,
|
persistAttachmentFromFileUri,
|
||||||
} from "@/attachments/service";
|
} from "@/attachments/service";
|
||||||
import { resolveAgentControlsMode } from "@/composer/agent-controls/mode";
|
import { resolveAgentControlsMode } from "@/composer/agent-controls/mode";
|
||||||
@@ -119,6 +123,7 @@ import { useCheckoutPrStatusQuery } from "@/git/use-pr-status-query";
|
|||||||
import { getForgePresentation } from "@/git/forge";
|
import { getForgePresentation } from "@/git/forge";
|
||||||
import { ForgeBrandIcon } from "@/git/forge-icon";
|
import { ForgeBrandIcon } from "@/git/forge-icon";
|
||||||
import { useComposerGithubAutoAttach } from "./github/auto-attach";
|
import { useComposerGithubAutoAttach } from "./github/auto-attach";
|
||||||
|
import { readClipboardImage } from "./clipboard-image";
|
||||||
import { resolveClientSlashCommand, type ClientSlashCommand } from "@/client-slash-commands";
|
import { resolveClientSlashCommand, type ClientSlashCommand } from "@/client-slash-commands";
|
||||||
import {
|
import {
|
||||||
appendWorkspaceFileAttachment,
|
appendWorkspaceFileAttachment,
|
||||||
@@ -130,6 +135,15 @@ import {
|
|||||||
type WorkspaceFileDragPayload,
|
type WorkspaceFileDragPayload,
|
||||||
} from "@/attachments/workspace-file-drag";
|
} from "@/attachments/workspace-file-drag";
|
||||||
|
|
||||||
|
const composerImageAttachmentPersister: Pick<
|
||||||
|
AttachmentPersister,
|
||||||
|
"persistFromBlob" | "persistFromDataUrl" | "persistFromFileUri"
|
||||||
|
> = {
|
||||||
|
persistFromBlob: persistAttachmentFromBlob,
|
||||||
|
persistFromDataUrl: persistAttachmentFromDataUrl,
|
||||||
|
persistFromFileUri: persistAttachmentFromFileUri,
|
||||||
|
};
|
||||||
|
|
||||||
type QueuedMessage = QueuedComposerMessage;
|
type QueuedMessage = QueuedComposerMessage;
|
||||||
|
|
||||||
type AttachmentListUpdater =
|
type AttachmentListUpdater =
|
||||||
@@ -1408,17 +1422,32 @@ export function Composer({
|
|||||||
const handlePickImage = useCallback(async () => {
|
const handlePickImage = useCallback(async () => {
|
||||||
const newImages = await pickAndPersistImages({
|
const newImages = await pickAndPersistImages({
|
||||||
pickImages,
|
pickImages,
|
||||||
persister: {
|
persister: composerImageAttachmentPersister,
|
||||||
persistFromBlob: ({ blob, mimeType, fileName }) =>
|
|
||||||
persistAttachmentFromBlob({ blob, mimeType, fileName }),
|
|
||||||
persistFromFileUri: ({ uri, mimeType, fileName }) =>
|
|
||||||
persistAttachmentFromFileUri({ uri, mimeType, fileName }),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (newImages.length === 0) return;
|
if (newImages.length === 0) return;
|
||||||
addImages(newImages);
|
addImages(newImages);
|
||||||
}, [addImages, pickImages]);
|
}, [addImages, pickImages]);
|
||||||
|
|
||||||
|
const handlePasteImage = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const newImages = await pickAndPersistImages({
|
||||||
|
pickImages: async () => {
|
||||||
|
const image = await readClipboardImage(Clipboard);
|
||||||
|
return image ? [image] : null;
|
||||||
|
},
|
||||||
|
persister: composerImageAttachmentPersister,
|
||||||
|
});
|
||||||
|
if (newImages.length === 0) {
|
||||||
|
toastErrorRef.current(t("composer.errors.noClipboardImage"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addImages(newImages);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Composer] Failed to paste clipboard image:", error);
|
||||||
|
toastErrorRef.current(t("composer.errors.pasteImageFailed"));
|
||||||
|
}
|
||||||
|
}, [addImages, t]);
|
||||||
|
|
||||||
const uploadPickedFiles = useCallback(
|
const uploadPickedFiles = useCallback(
|
||||||
async (files: PickedFile[]) => {
|
async (files: PickedFile[]) => {
|
||||||
if (files.length === 0) return;
|
if (files.length === 0) return;
|
||||||
@@ -1823,8 +1852,8 @@ export function Composer({
|
|||||||
[githubSearchItems, githubSearchQueryTrimmed],
|
[githubSearchItems, githubSearchQueryTrimmed],
|
||||||
);
|
);
|
||||||
|
|
||||||
const attachmentMenuItems = useMemo<AttachmentMenuItem[]>(
|
const attachmentMenuItems = useMemo<AttachmentMenuItem[]>(() => {
|
||||||
() => [
|
const items: AttachmentMenuItem[] = [
|
||||||
{
|
{
|
||||||
id: "image",
|
id: "image",
|
||||||
label: t("composer.attachments.addImage"),
|
label: t("composer.attachments.addImage"),
|
||||||
@@ -1833,6 +1862,18 @@ export function Composer({
|
|||||||
void handlePickImage();
|
void handlePickImage();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
];
|
||||||
|
if (isNative) {
|
||||||
|
items.push({
|
||||||
|
id: "paste-image",
|
||||||
|
label: t("composer.attachments.pasteImage"),
|
||||||
|
icon: <ThemedClipboardPaste size={ICON_SIZE.md} uniProps={iconForegroundMutedMapping} />,
|
||||||
|
onSelect: () => {
|
||||||
|
void handlePasteImage();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
items.push(
|
||||||
{
|
{
|
||||||
id: "github",
|
id: "github",
|
||||||
label: t("composer.attachments.addIssueOrPr", {
|
label: t("composer.attachments.addIssueOrPr", {
|
||||||
@@ -1851,9 +1892,9 @@ export function Composer({
|
|||||||
void handlePickFile();
|
void handlePickFile();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
);
|
||||||
[handlePickImage, handlePickFile, t, forgePresentation],
|
return items;
|
||||||
);
|
}, [forgePresentation, handlePasteImage, handlePickFile, handlePickImage, t]);
|
||||||
|
|
||||||
const handleToggleGithubItem = useCallback(
|
const handleToggleGithubItem = useCallback(
|
||||||
(item: ForgeSearchItem) => {
|
(item: ForgeSearchItem) => {
|
||||||
@@ -2248,6 +2289,7 @@ const ThemedCircleDot = withUnistyles(CircleDot);
|
|||||||
const ThemedAudioLines = withUnistyles(AudioLines);
|
const ThemedAudioLines = withUnistyles(AudioLines);
|
||||||
const ThemedPaperclip = withUnistyles(Paperclip);
|
const ThemedPaperclip = withUnistyles(Paperclip);
|
||||||
const ThemedImageIcon = withUnistyles(ImageIcon);
|
const ThemedImageIcon = withUnistyles(ImageIcon);
|
||||||
|
const ThemedClipboardPaste = withUnistyles(ClipboardPaste);
|
||||||
const ThemedFileText = withUnistyles(FileText);
|
const ThemedFileText = withUnistyles(FileText);
|
||||||
const iconForegroundMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
const iconForegroundMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
||||||
const iconForegroundMutedMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
const iconForegroundMutedMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { getFileNameFromPath } from "@/attachments/utils";
|
|||||||
import { i18n } from "@/i18n/i18next";
|
import { i18n } from "@/i18n/i18next";
|
||||||
import { isAbsolutePath } from "@/utils/path";
|
import { isAbsolutePath } from "@/utils/path";
|
||||||
|
|
||||||
export type PickedImageSource = { kind: "file_uri"; uri: string } | { kind: "blob"; blob: Blob };
|
export type PickedImageSource =
|
||||||
|
| { kind: "file_uri"; uri: string }
|
||||||
|
| { kind: "blob"; blob: Blob }
|
||||||
|
| { kind: "data_url"; dataUrl: string };
|
||||||
|
|
||||||
export interface PickedImageAttachmentInput {
|
export interface PickedImageAttachmentInput {
|
||||||
source: PickedImageSource;
|
source: PickedImageSource;
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export const ar: TranslationResources = {
|
|||||||
},
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
addImage: "أضف صورة",
|
addImage: "أضف صورة",
|
||||||
|
pasteImage: "لصق صورة",
|
||||||
addFile: "Upload file",
|
addFile: "Upload file",
|
||||||
addIssueOrPr: "أضف مشكلة أو PR",
|
addIssueOrPr: "أضف مشكلة أو PR",
|
||||||
addIssueOrPr_mr: "أضف مشكلة أو MR",
|
addIssueOrPr_mr: "أضف مشكلة أو MR",
|
||||||
@@ -137,6 +138,8 @@ export const ar: TranslationResources = {
|
|||||||
initialPromptRequired: "مطلوب موجه الأولي",
|
initialPromptRequired: "مطلوب موجه الأولي",
|
||||||
alreadyLoading: "جارٍ التحميل بالفعل",
|
alreadyLoading: "جارٍ التحميل بالفعل",
|
||||||
uploadFailed: "Failed to upload file",
|
uploadFailed: "Failed to upload file",
|
||||||
|
noClipboardImage: "لا توجد صورة في الحافظة",
|
||||||
|
pasteImageFailed: "تعذر لصق الصورة",
|
||||||
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
||||||
},
|
},
|
||||||
clientCommands: {
|
clientCommands: {
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ export const en = {
|
|||||||
},
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
addImage: "Add image",
|
addImage: "Add image",
|
||||||
|
pasteImage: "Paste image",
|
||||||
addFile: "Upload file",
|
addFile: "Upload file",
|
||||||
addIssueOrPr: "Add issue or PR",
|
addIssueOrPr: "Add issue or PR",
|
||||||
addIssueOrPr_mr: "Add issue or MR",
|
addIssueOrPr_mr: "Add issue or MR",
|
||||||
@@ -135,6 +136,8 @@ export const en = {
|
|||||||
initialPromptRequired: "Initial prompt is required",
|
initialPromptRequired: "Initial prompt is required",
|
||||||
alreadyLoading: "Already loading",
|
alreadyLoading: "Already loading",
|
||||||
uploadFailed: "Failed to upload file",
|
uploadFailed: "Failed to upload file",
|
||||||
|
noClipboardImage: "No image in clipboard",
|
||||||
|
pasteImageFailed: "Failed to paste image",
|
||||||
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
||||||
},
|
},
|
||||||
clientCommands: {
|
clientCommands: {
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export const es: TranslationResources = {
|
|||||||
},
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
addImage: "Agregar imagen",
|
addImage: "Agregar imagen",
|
||||||
|
pasteImage: "Pegar imagen",
|
||||||
addFile: "Upload file",
|
addFile: "Upload file",
|
||||||
addIssueOrPr: "Agregar problema oPR",
|
addIssueOrPr: "Agregar problema oPR",
|
||||||
addIssueOrPr_mr: "Agregar problema o MR",
|
addIssueOrPr_mr: "Agregar problema o MR",
|
||||||
@@ -137,6 +138,8 @@ export const es: TranslationResources = {
|
|||||||
initialPromptRequired: "Se requiere aviso inicial",
|
initialPromptRequired: "Se requiere aviso inicial",
|
||||||
alreadyLoading: "Ya cargando",
|
alreadyLoading: "Ya cargando",
|
||||||
uploadFailed: "Failed to upload file",
|
uploadFailed: "Failed to upload file",
|
||||||
|
noClipboardImage: "No hay ninguna imagen en el portapapeles",
|
||||||
|
pasteImageFailed: "No se pudo pegar la imagen",
|
||||||
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
||||||
},
|
},
|
||||||
clientCommands: {
|
clientCommands: {
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export const fr: TranslationResources = {
|
|||||||
},
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
addImage: "Ajouter une image",
|
addImage: "Ajouter une image",
|
||||||
|
pasteImage: "Coller une image",
|
||||||
addFile: "Upload file",
|
addFile: "Upload file",
|
||||||
addIssueOrPr: "Ajouter un problème ouPR",
|
addIssueOrPr: "Ajouter un problème ouPR",
|
||||||
addIssueOrPr_mr: "Ajouter un problème ou MR",
|
addIssueOrPr_mr: "Ajouter un problème ou MR",
|
||||||
@@ -139,6 +140,8 @@ export const fr: TranslationResources = {
|
|||||||
initialPromptRequired: "Une invite initiale est requise",
|
initialPromptRequired: "Une invite initiale est requise",
|
||||||
alreadyLoading: "Déjà en cours de chargement",
|
alreadyLoading: "Déjà en cours de chargement",
|
||||||
uploadFailed: "Failed to upload file",
|
uploadFailed: "Failed to upload file",
|
||||||
|
noClipboardImage: "Aucune image dans le presse-papiers",
|
||||||
|
pasteImageFailed: "Impossible de coller l’image",
|
||||||
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
||||||
},
|
},
|
||||||
clientCommands: {
|
clientCommands: {
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export const ja: TranslationResources = {
|
|||||||
},
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
addImage: "画像を追加",
|
addImage: "画像を追加",
|
||||||
|
pasteImage: "画像を貼り付け",
|
||||||
addFile: "ファイルをアップロード",
|
addFile: "ファイルをアップロード",
|
||||||
addIssueOrPr: "イシューまたはPRを追加",
|
addIssueOrPr: "イシューまたはPRを追加",
|
||||||
addIssueOrPr_mr: "イシューまたはMRを追加",
|
addIssueOrPr_mr: "イシューまたはMRを追加",
|
||||||
@@ -137,6 +138,8 @@ export const ja: TranslationResources = {
|
|||||||
initialPromptRequired: "初期プロンプトが必要です",
|
initialPromptRequired: "初期プロンプトが必要です",
|
||||||
alreadyLoading: "すでに読み込み中です",
|
alreadyLoading: "すでに読み込み中です",
|
||||||
uploadFailed: "ファイルのアップロードに失敗しました",
|
uploadFailed: "ファイルのアップロードに失敗しました",
|
||||||
|
noClipboardImage: "クリップボードに画像がありません",
|
||||||
|
pasteImageFailed: "画像を貼り付けられませんでした",
|
||||||
fileTooLarge: "{{fileName}}が大きすぎます(最大{{size}})",
|
fileTooLarge: "{{fileName}}が大きすぎます(最大{{size}})",
|
||||||
},
|
},
|
||||||
clientCommands: {
|
clientCommands: {
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export const ptBR: TranslationResources = {
|
|||||||
},
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
addImage: "Adicionar imagem",
|
addImage: "Adicionar imagem",
|
||||||
|
pasteImage: "Colar imagem",
|
||||||
addFile: "Enviar arquivo",
|
addFile: "Enviar arquivo",
|
||||||
addIssueOrPr: "Adicionar issue ou PR",
|
addIssueOrPr: "Adicionar issue ou PR",
|
||||||
addIssueOrPr_mr: "Adicionar issue ou MR",
|
addIssueOrPr_mr: "Adicionar issue ou MR",
|
||||||
@@ -137,6 +138,8 @@ export const ptBR: TranslationResources = {
|
|||||||
initialPromptRequired: "O prompt inicial é obrigatório",
|
initialPromptRequired: "O prompt inicial é obrigatório",
|
||||||
alreadyLoading: "Já está carregando",
|
alreadyLoading: "Já está carregando",
|
||||||
uploadFailed: "Falha ao enviar arquivo",
|
uploadFailed: "Falha ao enviar arquivo",
|
||||||
|
noClipboardImage: "Não há imagem na área de transferência",
|
||||||
|
pasteImageFailed: "Falha ao colar a imagem",
|
||||||
fileTooLarge: "{{fileName}} é grande demais (máximo {{size}})",
|
fileTooLarge: "{{fileName}} é grande demais (máximo {{size}})",
|
||||||
},
|
},
|
||||||
clientCommands: {
|
clientCommands: {
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export const ru: TranslationResources = {
|
|||||||
},
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
addImage: "Добавить изображение",
|
addImage: "Добавить изображение",
|
||||||
|
pasteImage: "Вставить изображение",
|
||||||
addFile: "Upload file",
|
addFile: "Upload file",
|
||||||
addIssueOrPr: "Добавить проблему или PR",
|
addIssueOrPr: "Добавить проблему или PR",
|
||||||
addIssueOrPr_mr: "Добавить проблему или MR",
|
addIssueOrPr_mr: "Добавить проблему или MR",
|
||||||
@@ -137,6 +138,8 @@ export const ru: TranslationResources = {
|
|||||||
initialPromptRequired: "Требуется начальное приглашение",
|
initialPromptRequired: "Требуется начальное приглашение",
|
||||||
alreadyLoading: "Уже загружается",
|
alreadyLoading: "Уже загружается",
|
||||||
uploadFailed: "Failed to upload file",
|
uploadFailed: "Failed to upload file",
|
||||||
|
noClipboardImage: "В буфере обмена нет изображения",
|
||||||
|
pasteImageFailed: "Не удалось вставить изображение",
|
||||||
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
||||||
},
|
},
|
||||||
clientCommands: {
|
clientCommands: {
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export const zhCN: TranslationResources = {
|
|||||||
},
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
addImage: "添加图片",
|
addImage: "添加图片",
|
||||||
|
pasteImage: "粘贴图片",
|
||||||
addFile: "Upload file",
|
addFile: "Upload file",
|
||||||
addIssueOrPr: "添加 issue 或 PR",
|
addIssueOrPr: "添加 issue 或 PR",
|
||||||
addIssueOrPr_mr: "添加 issue 或 MR",
|
addIssueOrPr_mr: "添加 issue 或 MR",
|
||||||
@@ -137,6 +138,8 @@ export const zhCN: TranslationResources = {
|
|||||||
initialPromptRequired: "初始 prompt 必填",
|
initialPromptRequired: "初始 prompt 必填",
|
||||||
alreadyLoading: "正在加载",
|
alreadyLoading: "正在加载",
|
||||||
uploadFailed: "Failed to upload file",
|
uploadFailed: "Failed to upload file",
|
||||||
|
noClipboardImage: "剪贴板中没有图片",
|
||||||
|
pasteImageFailed: "无法粘贴图片",
|
||||||
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
fileTooLarge: "{{fileName}} is too large (max {{size}})",
|
||||||
},
|
},
|
||||||
clientCommands: {
|
clientCommands: {
|
||||||
|
|||||||
Reference in New Issue
Block a user