mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
fix: copy remote preview images without download fallback
This commit is contained in:
1 parent
485c6b30dc
commit
2c419048a2
26 files changed
+198
-66
No files matched your search
@@ -49,6 +49,12 @@ describe('ImagePreview', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi
|
||||
.fn()
|
||||
.mockImplementation(async () => new Response(new Blob(['image'], { type: 'image/png' }))),
|
||||
)
|
||||
|
||||
if (!navigator.clipboard) {
|
||||
Object.defineProperty(globalThis.navigator, 'clipboard', {
|
||||
@@ -75,7 +81,7 @@ describe('ImagePreview', () => {
|
||||
})
|
||||
|
||||
globalThis.ClipboardItem = class {
|
||||
constructor(public readonly data: Record<string, Blob>) {}
|
||||
constructor(public readonly data: Record<string, Blob | Promise<Blob>>) {}
|
||||
} as unknown as typeof ClipboardItem
|
||||
vi.spyOn(window, 'open').mockImplementation((...args: Parameters<Window['open']>) => {
|
||||
return mocks.windowOpen(...args)
|
||||
@@ -85,6 +91,7 @@ describe('ImagePreview', () => {
|
||||
afterEach(() => {
|
||||
globalThis.ClipboardItem = originalClipboardItem
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
@@ -289,21 +296,18 @@ describe('ImagePreview', () => {
|
||||
})
|
||||
|
||||
describe('Action Buttons', () => {
|
||||
it('should open valid url in new tab', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<ImagePreview
|
||||
url="https://example.com/image.png"
|
||||
title="Preview Image"
|
||||
onCancel={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
it.each(['https://example.com/image.png', '/image-proxy/github/example'])(
|
||||
'should open valid url %s in new tab',
|
||||
async (url) => {
|
||||
const user = userEvent.setup()
|
||||
render(<ImagePreview url={url} title="Preview Image" onCancel={vi.fn()} />)
|
||||
|
||||
const openInTabButton = getOpenInTabButton()
|
||||
await user.click(openInTabButton)
|
||||
const openInTabButton = getOpenInTabButton()
|
||||
await user.click(openInTabButton)
|
||||
|
||||
expect(mocks.windowOpen).toHaveBeenCalledWith('https://example.com/image.png', '_blank')
|
||||
})
|
||||
expect(mocks.windowOpen).toHaveBeenCalledWith(url, '_blank')
|
||||
},
|
||||
)
|
||||
|
||||
it('should open data image by writing to popup window document', async () => {
|
||||
const user = userEvent.setup()
|
||||
@@ -336,7 +340,7 @@ describe('ImagePreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should fall back to download and show info toast when clipboard copy fails', async () => {
|
||||
it('keeps the preview open and reports clipboard denial without downloading or navigating', async () => {
|
||||
const user = userEvent.setup()
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
mocks.clipboardWrite.mockRejectedValue(new Error('copy failed'))
|
||||
@@ -346,21 +350,116 @@ describe('ImagePreview', () => {
|
||||
const copyButton = getCopyButton()
|
||||
await user.click(copyButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.downloadUrl).toHaveBeenCalledWith({
|
||||
url: dataImage,
|
||||
fileName: 'Preview Image.png',
|
||||
})
|
||||
})
|
||||
expect(mocks.notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'info',
|
||||
await waitFor(() =>
|
||||
expect(mocks.notify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'common.operation.imageCopyFailed',
|
||||
}),
|
||||
)
|
||||
expect(mocks.downloadUrl).not.toHaveBeenCalled()
|
||||
expect(mocks.windowOpen).not.toHaveBeenCalled()
|
||||
expect(getOverlay()).toBeInTheDocument()
|
||||
expect(consoleErrorSpy).toHaveBeenCalled()
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('copies a remote PNG and starts the clipboard write before the image fetch resolves', async () => {
|
||||
const user = userEvent.setup()
|
||||
const png = new Blob(['png bytes'], { type: 'image/png' })
|
||||
let resolveFetch!: (response: Response) => void
|
||||
const fetchMock = vi.fn(
|
||||
() =>
|
||||
new Promise<Response>((resolve) => {
|
||||
resolveFetch = resolve
|
||||
}),
|
||||
)
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
let copiedBlob: Blob | undefined
|
||||
mocks.clipboardWrite.mockImplementation(async ([item]) => {
|
||||
copiedBlob = await (item as unknown as { data: Record<string, Promise<Blob>> }).data[
|
||||
'image/png'
|
||||
]
|
||||
})
|
||||
render(
|
||||
<ImagePreview
|
||||
url="https://example.com/image.png"
|
||||
title="Preview Image"
|
||||
onCancel={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(getCopyButton())
|
||||
|
||||
expect(mocks.clipboardWrite).toHaveBeenCalledTimes(1)
|
||||
expect(copiedBlob).toBeUndefined()
|
||||
resolveFetch(new Response(png))
|
||||
await waitFor(() =>
|
||||
expect(mocks.notify).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' })),
|
||||
)
|
||||
expect(copiedBlob?.type).toBe('image/png')
|
||||
expect(await copiedBlob?.text()).toBe('png bytes')
|
||||
expect(mocks.downloadUrl).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('converts a JPEG to PNG instead of labelling JPEG bytes as PNG', async () => {
|
||||
const user = userEvent.setup()
|
||||
const jpeg = new Blob(['jpeg bytes'], { type: 'image/jpeg' })
|
||||
const png = new Blob(['encoded png'], { type: 'image/png' })
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(jpeg)))
|
||||
const bitmap = { width: 12, height: 8, close: vi.fn() }
|
||||
vi.stubGlobal('createImageBitmap', vi.fn().mockResolvedValue(bitmap))
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
|
||||
drawImage: vi.fn(),
|
||||
} as unknown as CanvasRenderingContext2D)
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'toBlob').mockImplementation((callback) =>
|
||||
callback(png),
|
||||
)
|
||||
let copiedBlob: Blob | undefined
|
||||
mocks.clipboardWrite.mockImplementation(async ([item]) => {
|
||||
copiedBlob = await (item as unknown as { data: Record<string, Promise<Blob>> }).data[
|
||||
'image/png'
|
||||
]
|
||||
})
|
||||
render(
|
||||
<ImagePreview
|
||||
url="https://example.com/image.jpg"
|
||||
title="Preview Image"
|
||||
onCancel={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(getCopyButton())
|
||||
|
||||
await waitFor(() => expect(copiedBlob).toBe(png))
|
||||
expect(bitmap.close).toHaveBeenCalled()
|
||||
expect(mocks.downloadUrl).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports an image fetch failure without leaving the preview', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new TypeError('Failed to fetch')))
|
||||
mocks.clipboardWrite.mockImplementation(async ([item]) => {
|
||||
await (item as unknown as { data: Record<string, Promise<Blob>> }).data['image/png']
|
||||
})
|
||||
render(
|
||||
<ImagePreview
|
||||
url="https://example.com/image.png"
|
||||
title="Preview Image"
|
||||
onCancel={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(getCopyButton())
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mocks.notify).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' })),
|
||||
)
|
||||
expect(mocks.downloadUrl).not.toHaveBeenCalled()
|
||||
expect(mocks.windowOpen).not.toHaveBeenCalled()
|
||||
expect(getOverlay()).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should copy image and show success toast', async () => {
|
||||
const user = userEvent.setup()
|
||||
mocks.clipboardWrite.mockResolvedValue()
|
||||
@@ -379,24 +478,21 @@ describe('ImagePreview', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should call download action for valid url', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<ImagePreview
|
||||
url="https://example.com/image.png"
|
||||
title="Preview Image"
|
||||
onCancel={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
const downloadButton = getDownloadButton()
|
||||
await user.click(downloadButton)
|
||||
it.each(['https://example.com/image.png', '/image-proxy/github/example'])(
|
||||
'should download valid url %s',
|
||||
async (url) => {
|
||||
const user = userEvent.setup()
|
||||
render(<ImagePreview url={url} title="Preview Image" onCancel={vi.fn()} />)
|
||||
const downloadButton = getDownloadButton()
|
||||
await user.click(downloadButton)
|
||||
|
||||
expect(mocks.downloadUrl).toHaveBeenCalledWith({
|
||||
url: 'https://example.com/image.png',
|
||||
fileName: 'Preview Image',
|
||||
target: '_blank',
|
||||
})
|
||||
})
|
||||
expect(mocks.downloadUrl).toHaveBeenCalledWith({
|
||||
url,
|
||||
fileName: 'Preview Image',
|
||||
target: '_blank',
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
it('should show error toast for invalid download url', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
@@ -25,6 +25,31 @@ const isBase64 = (str: string): boolean => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchImageAsPng = async (url: string): Promise<Blob> => {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) throw new Error('Unable to load image')
|
||||
const blob = await response.blob()
|
||||
if (blob.type === 'image/png') return blob
|
||||
|
||||
const bitmap = await createImageBitmap(blob)
|
||||
try {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = bitmap.width
|
||||
canvas.height = bitmap.height
|
||||
const context = canvas.getContext('2d')
|
||||
if (!context) throw new Error('Unable to convert image')
|
||||
context.drawImage(bitmap, 0, 0)
|
||||
return await new Promise<Blob>((resolve, reject) => {
|
||||
canvas.toBlob((png) => {
|
||||
if (png) resolve(png)
|
||||
else reject(new Error('Unable to encode image'))
|
||||
}, 'image/png')
|
||||
})
|
||||
} finally {
|
||||
bitmap.close()
|
||||
}
|
||||
}
|
||||
|
||||
const ImagePreview: FC<ImagePreviewProps> = ({ url, title, onCancel, onPrev, onNext }) => {
|
||||
const { t } = useTranslation()
|
||||
const [scale, setScale] = useState(1)
|
||||
@@ -36,7 +61,7 @@ const ImagePreview: FC<ImagePreviewProps> = ({ url, title, onCancel, onPrev, onN
|
||||
|
||||
const openInNewTab = () => {
|
||||
// Open in a new window, considering the case when the page is inside an iframe
|
||||
if (url.startsWith('http') || url.startsWith('https')) {
|
||||
if (url.startsWith('http') || url.startsWith('/')) {
|
||||
window.open(url, '_blank')
|
||||
} else if (url.startsWith('data:image')) {
|
||||
// Base64 image
|
||||
@@ -49,7 +74,7 @@ const ImagePreview: FC<ImagePreviewProps> = ({ url, title, onCancel, onPrev, onN
|
||||
|
||||
const downloadImage = () => {
|
||||
// Open in a new window, considering the case when the page is inside an iframe
|
||||
if (url.startsWith('http') || url.startsWith('https') || url.startsWith('data:image')) {
|
||||
if (url.startsWith('http') || url.startsWith('/') || url.startsWith('data:image')) {
|
||||
downloadUrl({ url, fileName: title, target: '_blank' })
|
||||
return
|
||||
}
|
||||
@@ -69,31 +94,19 @@ const ImagePreview: FC<ImagePreviewProps> = ({ url, title, onCancel, onPrev, onN
|
||||
})
|
||||
}
|
||||
|
||||
const imageBase64ToBlob = (base64: string, type = 'image/png'): Blob => {
|
||||
const byteCharacters = atob(base64)
|
||||
const byteArrays = []
|
||||
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
|
||||
const slice = byteCharacters.slice(offset, offset + 512)
|
||||
const byteNumbers = Array.from({ length: slice.length })
|
||||
for (let i = 0; i < slice.length; i++) byteNumbers[i] = slice.charCodeAt(i)
|
||||
|
||||
const byteArray = new Uint8Array(byteNumbers as any)
|
||||
byteArrays.push(byteArray)
|
||||
}
|
||||
|
||||
return new Blob(byteArrays, { type })
|
||||
}
|
||||
|
||||
const imageCopy = useCallback(() => {
|
||||
const shareImage = async () => {
|
||||
try {
|
||||
const base64Data = url.split(',')[1]
|
||||
const blob = imageBase64ToBlob(base64Data!, 'image/png')
|
||||
if (!navigator.clipboard?.write || typeof ClipboardItem === 'undefined')
|
||||
throw new Error('Image clipboard is unavailable')
|
||||
|
||||
const png = fetchImageAsPng(isBase64(url) ? `data:image/png;base64,${url}` : url)
|
||||
// Handle a rejected fetch even if clipboard permission is denied before
|
||||
// the browser consumes the item. Keep write in the click gesture for Safari.
|
||||
void png.catch(noop)
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
[blob.type]: blob,
|
||||
'image/png': png,
|
||||
}),
|
||||
])
|
||||
setIsCopied(true)
|
||||
@@ -102,13 +115,12 @@ const ImagePreview: FC<ImagePreviewProps> = ({ url, title, onCancel, onPrev, onN
|
||||
} catch (err) {
|
||||
console.error('Failed to copy image:', err)
|
||||
|
||||
downloadUrl({ url, fileName: `${title}.png` })
|
||||
|
||||
toast.info(t(($) => $['operation.imageDownloaded'], { ns: 'common' }))
|
||||
setIsCopied(false)
|
||||
toast.error(t(($) => $['operation.imageCopyFailed'], { ns: 'common' }))
|
||||
}
|
||||
}
|
||||
shareImage()
|
||||
}, [t, title, url])
|
||||
}, [t, url])
|
||||
|
||||
const handleWheel = useCallback((e: React.WheelEvent<HTMLDivElement>) => {
|
||||
if (e.deltaY < 0) zoomIn()
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "ملء تلقائي",
|
||||
"operation.format": "تنسيق",
|
||||
"operation.imageCopied": "تم نسخ الصورة",
|
||||
"operation.imageCopyFailed": "تعذّر نسخ الصورة. يرجى تنزيلها بدلاً من ذلك.",
|
||||
"operation.imageDownloaded": "تم تنزيل الصورة",
|
||||
"operation.in": "في",
|
||||
"operation.learnMore": "تعرف على المزيد",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "Automatisch ausfüllen",
|
||||
"operation.format": "Format",
|
||||
"operation.imageCopied": "Kopiertes Bild",
|
||||
"operation.imageCopyFailed": "Das Bild konnte nicht kopiert werden. Bitte laden Sie es stattdessen herunter.",
|
||||
"operation.imageDownloaded": "Bild heruntergeladen",
|
||||
"operation.in": "in",
|
||||
"operation.learnMore": "Mehr erfahren",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "Autofill",
|
||||
"operation.format": "Format",
|
||||
"operation.imageCopied": "Image copied",
|
||||
"operation.imageCopyFailed": "Could not copy the image. Please download it instead.",
|
||||
"operation.imageDownloaded": "Image downloaded",
|
||||
"operation.in": "in",
|
||||
"operation.learnMore": "Learn more",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "Autocompletar",
|
||||
"operation.format": "Formato",
|
||||
"operation.imageCopied": "Imagen copiada",
|
||||
"operation.imageCopyFailed": "No se pudo copiar la imagen. Descárgala en su lugar.",
|
||||
"operation.imageDownloaded": "Imagen descargada",
|
||||
"operation.in": "en",
|
||||
"operation.learnMore": "Aprender más",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "پر کردن خودکار",
|
||||
"operation.format": "قالب",
|
||||
"operation.imageCopied": "تصویر کپی شده",
|
||||
"operation.imageCopyFailed": "تصویر کپی نشد. لطفاً بهجای آن تصویر را دانلود کنید.",
|
||||
"operation.imageDownloaded": "تصویر دانلود شد",
|
||||
"operation.in": "در",
|
||||
"operation.learnMore": "اطلاعات بیشتر",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "Remplissage auto",
|
||||
"operation.format": "Format",
|
||||
"operation.imageCopied": "Image copied",
|
||||
"operation.imageCopyFailed": "Impossible de copier l’image. Veuillez la télécharger à la place.",
|
||||
"operation.imageDownloaded": "Image téléchargée",
|
||||
"operation.in": "dans",
|
||||
"operation.learnMore": "En savoir plus",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "ऑटोफिल",
|
||||
"operation.format": "फॉर्मेट",
|
||||
"operation.imageCopied": "कॉपी की गई छवि",
|
||||
"operation.imageCopyFailed": "इमेज कॉपी नहीं हो सकी। कृपया इसे डाउनलोड करें।",
|
||||
"operation.imageDownloaded": "छवि डाउनलोड की गई",
|
||||
"operation.in": "में",
|
||||
"operation.learnMore": "अधिक जानें",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "Isi otomatis",
|
||||
"operation.format": "Format",
|
||||
"operation.imageCopied": "Gambar yang disalin",
|
||||
"operation.imageCopyFailed": "Gambar tidak dapat disalin. Silakan unduh sebagai gantinya.",
|
||||
"operation.imageDownloaded": "Gambar diunduh",
|
||||
"operation.in": "di",
|
||||
"operation.learnMore": "Pelajari lebih lanjut",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "Compilazione automatica",
|
||||
"operation.format": "Formato",
|
||||
"operation.imageCopied": "Immagine copiata",
|
||||
"operation.imageCopyFailed": "Impossibile copiare l’immagine. Scaricala invece.",
|
||||
"operation.imageDownloaded": "Immagine scaricata",
|
||||
"operation.in": "in",
|
||||
"operation.learnMore": "Scopri di più",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "自動入力",
|
||||
"operation.format": "フォーマット",
|
||||
"operation.imageCopied": "コピーした画像",
|
||||
"operation.imageCopyFailed": "画像をコピーできませんでした。代わりにダウンロードしてください。",
|
||||
"operation.imageDownloaded": "画像がダウンロードされました",
|
||||
"operation.in": "中",
|
||||
"operation.learnMore": "詳細はこちら",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "자동 입력",
|
||||
"operation.format": "형식",
|
||||
"operation.imageCopied": "복사된 이미지",
|
||||
"operation.imageCopyFailed": "이미지를 복사할 수 없습니다. 대신 다운로드해 주세요.",
|
||||
"operation.imageDownloaded": "이미지 다운로드됨",
|
||||
"operation.in": "안으로",
|
||||
"operation.learnMore": "자세히 알아보기",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "ຕື່ມຂໍ້ມູນອັດຕະໂນມັດ",
|
||||
"operation.format": "ຈັດຮູບແບບ",
|
||||
"operation.imageCopied": "ຄັດລອກຮູບແລ້ວ",
|
||||
"operation.imageCopyFailed": "ບໍ່ສາມາດສຳເນົາຮູບພາບໄດ້. ກະລຸນາດາວໂຫຼດຮູບພາບແທນ.",
|
||||
"operation.imageDownloaded": "ດາວໂຫຼດຮູບແລ້ວ",
|
||||
"operation.in": "ໃນ",
|
||||
"operation.learnMore": "ຮຽນຮູ້ເພີ່ມເຕີມ",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "Automatisch invullen",
|
||||
"operation.format": "Format",
|
||||
"operation.imageCopied": "Image copied",
|
||||
"operation.imageCopyFailed": "De afbeelding kon niet worden gekopieerd. Download deze in plaats daarvan.",
|
||||
"operation.imageDownloaded": "Image downloaded",
|
||||
"operation.in": "in",
|
||||
"operation.learnMore": "Meer informatie",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "Autouzupełnianie",
|
||||
"operation.format": "Format",
|
||||
"operation.imageCopied": "Skopiowany obraz",
|
||||
"operation.imageCopyFailed": "Nie można skopiować obrazu. Zamiast tego pobierz go.",
|
||||
"operation.imageDownloaded": "Obraz pobrany",
|
||||
"operation.in": "w",
|
||||
"operation.learnMore": "Dowiedz się więcej",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "Preenchimento automático",
|
||||
"operation.format": "Formato",
|
||||
"operation.imageCopied": "Imagem copiada",
|
||||
"operation.imageCopyFailed": "Não foi possível copiar a imagem. Baixe-a em vez disso.",
|
||||
"operation.imageDownloaded": "Imagem baixada",
|
||||
"operation.in": "em",
|
||||
"operation.learnMore": "Saiba mais",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "Completare automată",
|
||||
"operation.format": "Format",
|
||||
"operation.imageCopied": "Imagine copiată",
|
||||
"operation.imageCopyFailed": "Imaginea nu a putut fi copiată. Descărcați-o în schimb.",
|
||||
"operation.imageDownloaded": "Imagine descărcată",
|
||||
"operation.in": "în",
|
||||
"operation.learnMore": "Află mai multe",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "Автозаполнение",
|
||||
"operation.format": "Формат",
|
||||
"operation.imageCopied": "Скопированное изображение",
|
||||
"operation.imageCopyFailed": "Не удалось скопировать изображение. Попробуйте скачать его.",
|
||||
"operation.imageDownloaded": "Изображение загружено",
|
||||
"operation.in": "в",
|
||||
"operation.learnMore": "Узнать больше",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "Samodejno izpolni",
|
||||
"operation.format": "Format",
|
||||
"operation.imageCopied": "Kopirana slika",
|
||||
"operation.imageCopyFailed": "Slike ni bilo mogoče kopirati. Namesto tega jo prenesite.",
|
||||
"operation.imageDownloaded": "Slika prenesena",
|
||||
"operation.in": "v",
|
||||
"operation.learnMore": "Izvedi več",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "กรอกอัตโนมัติ",
|
||||
"operation.format": "รูปแบบ",
|
||||
"operation.imageCopied": "ภาพที่คัดลอก",
|
||||
"operation.imageCopyFailed": "ไม่สามารถคัดลอกรูปภาพได้ โปรดดาวน์โหลดรูปภาพแทน",
|
||||
"operation.imageDownloaded": "ดาวน์โหลดรูปภาพแล้ว",
|
||||
"operation.in": "ใน",
|
||||
"operation.learnMore": "ศึกษาเพิ่มเติม",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "Otomatik doldur",
|
||||
"operation.format": "Format",
|
||||
"operation.imageCopied": "Kopyalanan görüntü",
|
||||
"operation.imageCopyFailed": "Görüntü kopyalanamadı. Lütfen bunun yerine indirin.",
|
||||
"operation.imageDownloaded": "Resim indirildi",
|
||||
"operation.in": "içinde",
|
||||
"operation.learnMore": "Daha fazla bilgi",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "Автозаповнення",
|
||||
"operation.format": "Формат",
|
||||
"operation.imageCopied": "Скопійоване зображення",
|
||||
"operation.imageCopyFailed": "Не вдалося скопіювати зображення. Спробуйте завантажити його.",
|
||||
"operation.imageDownloaded": "Зображення завантажено",
|
||||
"operation.in": "В",
|
||||
"operation.learnMore": "Дізнатися більше",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "Tự động điền",
|
||||
"operation.format": "Định dạng",
|
||||
"operation.imageCopied": "Hình ảnh sao chép",
|
||||
"operation.imageCopyFailed": "Không thể sao chép hình ảnh. Vui lòng tải ảnh xuống.",
|
||||
"operation.imageDownloaded": "Hình ảnh đã được tải xuống",
|
||||
"operation.in": "trong",
|
||||
"operation.learnMore": "Tìm hiểu thêm",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "一键填入",
|
||||
"operation.format": "格式化",
|
||||
"operation.imageCopied": "图片已复制",
|
||||
"operation.imageCopyFailed": "无法复制图片,请尝试下载图片。",
|
||||
"operation.imageDownloaded": "图片已下载",
|
||||
"operation.in": "在",
|
||||
"operation.learnMore": "了解更多",
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"operation.fill": "一鍵填入",
|
||||
"operation.format": "格式",
|
||||
"operation.imageCopied": "複製的圖片",
|
||||
"operation.imageCopyFailed": "無法複製圖片,請嘗試下載圖片。",
|
||||
"operation.imageDownloaded": "圖片已下載",
|
||||
"operation.in": "在",
|
||||
"operation.learnMore": "瞭解更多",
|
||||
|
||||
Reference in new issue
Block a user