mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
fix(i18n): translate pet import and repo icon upload refusals by code (#35861)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { translateMain } from '../i18n/main-i18n'
|
||||
import { PET_BUNDLE_DEFAULT_LABEL, PetImportError } from '../../shared/pet-import-failure-copy'
|
||||
import { BrowserWindow, dialog } from 'electron'
|
||||
import { mkdir, readFile, rename, rm, stat } from 'node:fs/promises'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
@@ -34,7 +35,7 @@ export async function importPetBundle(
|
||||
const pickedStat = await stat(picked)
|
||||
bundleDir = pickedStat.isDirectory() ? picked : dirname(picked)
|
||||
} catch {
|
||||
throw new Error('Could not read the selected path.')
|
||||
throw new PetImportError('pathUnreadable')
|
||||
}
|
||||
|
||||
const manifestPath = join(bundleDir, 'pet.json')
|
||||
@@ -42,13 +43,13 @@ export async function importPetBundle(
|
||||
try {
|
||||
manifestStat = await stat(manifestPath)
|
||||
} catch {
|
||||
throw new Error('Bundle is missing pet.json.')
|
||||
throw new PetImportError('manifestMissing')
|
||||
}
|
||||
if (!manifestStat.isFile() || manifestStat.size > MAX_MANIFEST_BYTES) {
|
||||
throw new Error('pet.json is invalid.')
|
||||
throw new PetImportError('manifestInvalid')
|
||||
}
|
||||
if (await isSymlink(manifestPath)) {
|
||||
throw new Error('pet.json must not be a symlink.')
|
||||
throw new PetImportError('manifestSymlink')
|
||||
}
|
||||
|
||||
let manifest: ResolvedPetManifest<PetManifest>
|
||||
@@ -60,7 +61,11 @@ export async function importPetBundle(
|
||||
}
|
||||
manifest = applyCodexPetDefaults(PetManifestSchema.parse(JSON.parse(raw)))
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid pet.json: ${error instanceof Error ? error.message : 'parse error'}`)
|
||||
throw new PetImportError(
|
||||
'manifestParseFailed',
|
||||
{},
|
||||
error instanceof Error ? error.message : 'parse error'
|
||||
)
|
||||
}
|
||||
|
||||
const { sheetSrc, sheetClass } = await resolveBundleSpritesheetSource(manifest, bundleDir)
|
||||
@@ -84,11 +89,11 @@ export async function importPetBundle(
|
||||
await rename(tmpDir, destDir)
|
||||
} catch {
|
||||
await rm(tmpDir, { recursive: true, force: true }).catch(() => {})
|
||||
throw new Error('Could not save the pet bundle.')
|
||||
throw new PetImportError('bundleSaveFailed')
|
||||
}
|
||||
|
||||
const rawLabel = (manifest.displayName ?? manifest.id ?? basename(bundleDir)).trim()
|
||||
const label = rawLabel.length > 0 ? rawLabel.slice(0, 40) : 'Pet bundle'
|
||||
const label = rawLabel.length > 0 ? rawLabel.slice(0, 40) : PET_BUNDLE_DEFAULT_LABEL
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { nativeImage } from 'electron'
|
||||
import { PetImportError } from '../../shared/pet-import-failure-copy'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import type { CustomPet } from '../../shared/pet-types'
|
||||
import { MAX_BYTES } from './pet-import-size-limits'
|
||||
@@ -36,38 +37,45 @@ export async function buildBundleSprite<T extends PetManifestLike>(
|
||||
const sheetBuf = await readFile(sheetSrc)
|
||||
// Recheck the read size because the file may have grown after stat.
|
||||
if (sheetBuf.byteLength > MAX_BYTES) {
|
||||
throw new Error('Spritesheet exceeded the size limit.')
|
||||
throw new PetImportError('sheetSizeLimitExceeded')
|
||||
}
|
||||
const dims = await readSheetDimensions(sheetBuf)
|
||||
if (!dims) {
|
||||
throw new Error('Could not decode the spritesheet image.')
|
||||
throw new PetImportError('sheetUndecodable')
|
||||
}
|
||||
const { width: fw, height: fh } = manifest.frame
|
||||
if (dims.width % fw !== 0 || dims.height % fh !== 0) {
|
||||
throw new Error(
|
||||
`Spritesheet ${dims.width}×${dims.height} is not a clean multiple of frame ${fw}×${fh}.`
|
||||
)
|
||||
throw new PetImportError('sheetFrameMismatch', {
|
||||
sheetWidth: dims.width,
|
||||
sheetHeight: dims.height,
|
||||
frameWidth: fw,
|
||||
frameHeight: fh
|
||||
})
|
||||
}
|
||||
const columns = dims.width / fw
|
||||
const rows = dims.height / fh
|
||||
if (manifest.animations) {
|
||||
for (const [name, anim] of Object.entries(manifest.animations)) {
|
||||
if (anim.row >= rows) {
|
||||
throw new Error(`Animation "${name}" references row ${anim.row} but sheet has ${rows}.`)
|
||||
throw new PetImportError('animationRowOutOfRange', { name, row: anim.row, rows })
|
||||
}
|
||||
if (anim.frames > columns) {
|
||||
throw new Error(
|
||||
`Animation "${name}" has ${anim.frames} frames but sheet only has ${columns} columns.`
|
||||
)
|
||||
throw new PetImportError('animationTooManyFrames', {
|
||||
name,
|
||||
frames: anim.frames,
|
||||
columns
|
||||
})
|
||||
}
|
||||
if (anim.frameDurationsMs && anim.frameDurationsMs.length !== anim.frames) {
|
||||
throw new Error(
|
||||
`Animation "${name}" declares ${anim.frameDurationsMs.length} frame durations but ${anim.frames} frames.`
|
||||
)
|
||||
throw new PetImportError('animationDurationMismatch', {
|
||||
name,
|
||||
durations: anim.frameDurationsMs.length,
|
||||
frames: anim.frames
|
||||
})
|
||||
}
|
||||
}
|
||||
if (manifest.defaultAnimation && !manifest.animations[manifest.defaultAnimation]) {
|
||||
throw new Error(`defaultAnimation "${manifest.defaultAnimation}" not in animations.`)
|
||||
throw new PetImportError('defaultAnimationMissing', { name: manifest.defaultAnimation })
|
||||
}
|
||||
}
|
||||
sprite = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { stat } from 'node:fs/promises'
|
||||
import { PetImportError } from '../../shared/pet-import-failure-copy'
|
||||
import { isAbsolute, resolve, sep } from 'node:path'
|
||||
import { classifyFile } from './pet-image-formats'
|
||||
import { MAX_BYTES } from './pet-import-size-limits'
|
||||
@@ -16,39 +17,39 @@ export async function resolveBundleSpritesheetSource<T extends PetManifestLike>(
|
||||
isAbsolute(normalizedSpritePath) ||
|
||||
/^[a-zA-Z]:/.test(manifest.spritesheetPath)
|
||||
) {
|
||||
throw new Error('spritesheetPath must be relative to the bundle.')
|
||||
throw new PetImportError('sheetPathNotRelative')
|
||||
}
|
||||
// Accept Windows-exported bundles on macOS/Linux.
|
||||
const sheetSrc = resolve(bundleDir, normalizedSpritePath)
|
||||
const bundleResolved = resolve(bundleDir)
|
||||
if (sheetSrc === bundleResolved) {
|
||||
throw new Error('spritesheetPath must point to a file, not the bundle root.')
|
||||
throw new PetImportError('sheetPathIsRoot')
|
||||
}
|
||||
const bundleRoot = bundleResolved + sep
|
||||
// Match paths case-insensitively on Windows.
|
||||
const cmp = process.platform === 'win32' ? (s: string) => s.toLowerCase() : (s: string) => s
|
||||
if (!cmp(sheetSrc + sep).startsWith(cmp(bundleRoot))) {
|
||||
throw new Error('spritesheetPath escapes the bundle.')
|
||||
throw new PetImportError('sheetPathEscapes')
|
||||
}
|
||||
if (await isSymlink(sheetSrc)) {
|
||||
throw new Error('spritesheet must not be a symlink.')
|
||||
throw new PetImportError('sheetSymlink')
|
||||
}
|
||||
const sheetClass = classifyFile(sheetSrc)
|
||||
if (!sheetClass || sheetClass.ext === '.svg') {
|
||||
// Sprite layouts require raster dimensions.
|
||||
throw new Error('Spritesheet must be a PNG, APNG, JPG, GIF, or WebP.')
|
||||
throw new PetImportError('sheetUnsupportedType')
|
||||
}
|
||||
let sheetStat: Awaited<ReturnType<typeof stat>>
|
||||
try {
|
||||
sheetStat = await stat(sheetSrc)
|
||||
} catch {
|
||||
throw new Error('Spritesheet file not found.')
|
||||
throw new PetImportError('sheetNotFound')
|
||||
}
|
||||
if (!sheetStat.isFile()) {
|
||||
throw new Error('Spritesheet path is not a file.')
|
||||
throw new PetImportError('sheetNotAFile')
|
||||
}
|
||||
if (sheetStat.size > MAX_BYTES) {
|
||||
throw new Error(`Spritesheet is too large (${(sheetStat.size / (1024 * 1024)).toFixed(1)} MB).`)
|
||||
throw new PetImportError('sheetTooLarge', { size: (sheetStat.size / (1024 * 1024)).toFixed(1) })
|
||||
}
|
||||
return { sheetSrc, sheetClass }
|
||||
}
|
||||
|
||||
@@ -168,8 +168,11 @@ describe('registerPetHandlers', () => {
|
||||
})
|
||||
showOpenDialogMock.mockResolvedValue({ canceled: false, filePaths: [bundleDir] })
|
||||
|
||||
await expect(getHandler('pet:importPetBundle')({ sender: {} })).rejects.toThrow(
|
||||
'declares 1 frame durations but 2 frames'
|
||||
)
|
||||
await expect(getHandler('pet:importPetBundle')({ sender: {} })).resolves.toEqual({
|
||||
ok: false,
|
||||
error: 'Animation "idle" declares 1 frame durations but 2 frames.',
|
||||
errorCode: 'animationDurationMismatch',
|
||||
errorParams: { name: 'idle', durations: 1, frames: 2 }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+85
-63
@@ -1,4 +1,9 @@
|
||||
import { translateMain } from '../i18n/main-i18n'
|
||||
import {
|
||||
CUSTOM_PET_DEFAULT_LABEL,
|
||||
PetImportError,
|
||||
type PetImportFailure
|
||||
} from '../../shared/pet-import-failure-copy'
|
||||
import { BrowserWindow, dialog, ipcMain } from 'electron'
|
||||
import { copyFile, mkdir, readFile, rm, stat } from 'node:fs/promises'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
@@ -17,73 +22,90 @@ const PetFileRequestSchema = z.object({
|
||||
kind: z.enum(['image', 'bundle']).optional()
|
||||
})
|
||||
|
||||
// Known refusals return as data so the renderer can translate them by code.
|
||||
async function withPetImportFailure(
|
||||
run: () => Promise<CustomPet | null>
|
||||
): Promise<CustomPet | PetImportFailure | null> {
|
||||
try {
|
||||
return await run()
|
||||
} catch (error) {
|
||||
if (error instanceof PetImportError) {
|
||||
return error.failure
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function registerPetHandlers(): void {
|
||||
ipcMain.handle('pet:import', async (event): Promise<CustomPet | null> => {
|
||||
const senderWindow =
|
||||
BrowserWindow.fromWebContents(event.sender) ?? BrowserWindow.getFocusedWindow()
|
||||
const options: Electron.OpenDialogOptions = {
|
||||
title: translateMain('native.dialog.pickPet', 'Pick pet'),
|
||||
properties: ['openFile'],
|
||||
// APNG uses .png; its missing macOS UTI can hide other formats when listed separately.
|
||||
filters: [
|
||||
{
|
||||
name: translateMain('native.dialog.petImage', 'Pet image'),
|
||||
extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg']
|
||||
}
|
||||
]
|
||||
}
|
||||
const result = senderWindow
|
||||
? await dialog.showOpenDialog(senderWindow, options)
|
||||
: await dialog.showOpenDialog(options)
|
||||
if (result.canceled || result.filePaths.length === 0) {
|
||||
return null
|
||||
}
|
||||
const src = result.filePaths[0]
|
||||
const classified = classifyFile(src)
|
||||
if (!classified) {
|
||||
throw new Error('Unsupported file. Pick a PNG, APNG, JPG, GIF, WebP, or SVG.')
|
||||
}
|
||||
let srcStat: Awaited<ReturnType<typeof stat>>
|
||||
try {
|
||||
srcStat = await stat(src)
|
||||
} catch {
|
||||
throw new Error('Could not read the selected file.')
|
||||
}
|
||||
if (!srcStat.isFile()) {
|
||||
throw new Error('Selected path is not a file')
|
||||
}
|
||||
if (srcStat.size > MAX_BYTES) {
|
||||
throw new Error(
|
||||
`File is too large (${(srcStat.size / (1024 * 1024)).toFixed(1)} MB). Max is ${MAX_BYTES / (1024 * 1024)} MB.`
|
||||
)
|
||||
}
|
||||
ipcMain.handle('pet:import', (event) =>
|
||||
withPetImportFailure(async (): Promise<CustomPet | null> => {
|
||||
const senderWindow =
|
||||
BrowserWindow.fromWebContents(event.sender) ?? BrowserWindow.getFocusedWindow()
|
||||
const options: Electron.OpenDialogOptions = {
|
||||
title: translateMain('native.dialog.pickPet', 'Pick pet'),
|
||||
properties: ['openFile'],
|
||||
// APNG uses .png; its missing macOS UTI can hide other formats when listed separately.
|
||||
filters: [
|
||||
{
|
||||
name: translateMain('native.dialog.petImage', 'Pet image'),
|
||||
extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg']
|
||||
}
|
||||
]
|
||||
}
|
||||
const result = senderWindow
|
||||
? await dialog.showOpenDialog(senderWindow, options)
|
||||
: await dialog.showOpenDialog(options)
|
||||
if (result.canceled || result.filePaths.length === 0) {
|
||||
return null
|
||||
}
|
||||
const src = result.filePaths[0]
|
||||
const classified = classifyFile(src)
|
||||
if (!classified) {
|
||||
throw new PetImportError('unsupportedFile')
|
||||
}
|
||||
let srcStat: Awaited<ReturnType<typeof stat>>
|
||||
try {
|
||||
srcStat = await stat(src)
|
||||
} catch {
|
||||
throw new PetImportError('fileUnreadable')
|
||||
}
|
||||
if (!srcStat.isFile()) {
|
||||
throw new PetImportError('notAFile')
|
||||
}
|
||||
if (srcStat.size > MAX_BYTES) {
|
||||
throw new PetImportError('fileTooLarge', {
|
||||
size: (srcStat.size / (1024 * 1024)).toFixed(1),
|
||||
max: MAX_BYTES / (1024 * 1024)
|
||||
})
|
||||
}
|
||||
|
||||
const dir = getPetsDir()
|
||||
await mkdir(dir, { recursive: true })
|
||||
const id = randomUUID()
|
||||
// Keep the extension so pet:read can recover MIME type without another lookup.
|
||||
const fileName = `${id}${classified.ext}`
|
||||
const dest = join(dir, fileName)
|
||||
try {
|
||||
await copyFile(src, dest)
|
||||
} catch {
|
||||
await rm(dest, { force: true }).catch(() => {})
|
||||
throw new Error('Could not save the pet.')
|
||||
}
|
||||
const dir = getPetsDir()
|
||||
await mkdir(dir, { recursive: true })
|
||||
const id = randomUUID()
|
||||
// Keep the extension so pet:read can recover MIME type without another lookup.
|
||||
const fileName = `${id}${classified.ext}`
|
||||
const dest = join(dir, fileName)
|
||||
try {
|
||||
await copyFile(src, dest)
|
||||
} catch {
|
||||
await rm(dest, { force: true }).catch(() => {})
|
||||
throw new PetImportError('saveFailed')
|
||||
}
|
||||
|
||||
const rawLabel = basename(src, extname(src)).trim()
|
||||
const label = rawLabel.length > 0 ? rawLabel.slice(0, 40) : 'Custom pet'
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
fileName,
|
||||
mimeType: classified.mimeType,
|
||||
kind: 'image'
|
||||
}
|
||||
})
|
||||
const rawLabel = basename(src, extname(src)).trim()
|
||||
const label = rawLabel.length > 0 ? rawLabel.slice(0, 40) : CUSTOM_PET_DEFAULT_LABEL
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
fileName,
|
||||
mimeType: classified.mimeType,
|
||||
kind: 'image'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
ipcMain.handle('pet:importPetBundle', async (event): Promise<CustomPet | null> =>
|
||||
importPetBundle(event)
|
||||
ipcMain.handle('pet:importPetBundle', (event) =>
|
||||
withPetImportFailure(() => importPetBundle(event))
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
|
||||
+35
-34
@@ -13,6 +13,10 @@ import type {
|
||||
ShellOpenLocalPathResult
|
||||
} from '../../shared/shell-open-types'
|
||||
import { MAX_REPO_ICON_UPLOAD_BYTES } from '../../shared/repo-icon'
|
||||
import {
|
||||
repoIconPickFailure,
|
||||
type RepoIconPickResult
|
||||
} from '../../shared/repo-icon-pick-failure-copy'
|
||||
import type { Store } from '../persistence'
|
||||
import {
|
||||
EXTERNAL_EDITOR_CLI_COMMAND,
|
||||
@@ -270,41 +274,38 @@ export function registerShellHandlers(
|
||||
return result.filePaths[0]
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
'shell:pickRepoIconImage',
|
||||
async (): Promise<{ dataUrl: string; fileName: string } | null> => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: translateMain('native.dialog.repoIconImages', 'Repo icon images'),
|
||||
extensions: ['png']
|
||||
}
|
||||
]
|
||||
})
|
||||
if (result.canceled || result.filePaths.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const filePath = result.filePaths[0]
|
||||
const extension = extname(filePath).toLowerCase()
|
||||
const mimeType = REPO_ICON_IMAGE_MIME_TYPES[extension]
|
||||
if (!mimeType) {
|
||||
throw new Error('Repo icons must be PNG files.')
|
||||
}
|
||||
|
||||
const stats = await stat(filePath)
|
||||
if (stats.size > MAX_REPO_ICON_UPLOAD_BYTES) {
|
||||
throw new Error('Repo icon image must be 256KB or smaller.')
|
||||
}
|
||||
|
||||
const buffer = await readFile(filePath)
|
||||
return {
|
||||
dataUrl: `data:${mimeType};base64,${buffer.toString('base64')}`,
|
||||
fileName: basename(filePath)
|
||||
}
|
||||
ipcMain.handle('shell:pickRepoIconImage', async (): Promise<RepoIconPickResult> => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: translateMain('native.dialog.repoIconImages', 'Repo icon images'),
|
||||
extensions: ['png']
|
||||
}
|
||||
]
|
||||
})
|
||||
if (result.canceled || result.filePaths.length === 0) {
|
||||
return null
|
||||
}
|
||||
)
|
||||
|
||||
const filePath = result.filePaths[0]
|
||||
const extension = extname(filePath).toLowerCase()
|
||||
const mimeType = REPO_ICON_IMAGE_MIME_TYPES[extension]
|
||||
if (!mimeType) {
|
||||
return repoIconPickFailure('notPng')
|
||||
}
|
||||
|
||||
const stats = await stat(filePath)
|
||||
if (stats.size > MAX_REPO_ICON_UPLOAD_BYTES) {
|
||||
return repoIconPickFailure('tooLarge')
|
||||
}
|
||||
|
||||
const buffer = await readFile(filePath)
|
||||
return {
|
||||
dataUrl: `data:${mimeType};base64,${buffer.toString('base64')}`,
|
||||
fileName: basename(filePath)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('shell:pickAudio', async (): Promise<string | null> => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { CustomPet } from '../../shared/pet-types'
|
||||
import type { PetImportFailure } from '../../shared/pet-import-failure-copy'
|
||||
|
||||
export type PetApi = {
|
||||
import: () => Promise<CustomPet | null>
|
||||
importPetBundle: () => Promise<CustomPet | null>
|
||||
import: () => Promise<CustomPet | PetImportFailure | null>
|
||||
importPetBundle: () => Promise<CustomPet | PetImportFailure | null>
|
||||
read: (id: string, fileName: string, kind?: 'image' | 'bundle') => Promise<ArrayBuffer | null>
|
||||
delete: (id: string, fileName: string, kind?: 'image' | 'bundle') => Promise<void>
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ipcRenderer } from 'electron'
|
||||
import type { CustomPet } from '../../shared/pet-types'
|
||||
import type { PetImportFailure } from '../../shared/pet-import-failure-copy'
|
||||
import type { PreloadApi } from '../api-types'
|
||||
|
||||
export const petApi = {
|
||||
import: (): Promise<CustomPet | null> => ipcRenderer.invoke('pet:import'),
|
||||
importPetBundle: (): Promise<CustomPet | null> => ipcRenderer.invoke('pet:importPetBundle'),
|
||||
import: (): Promise<CustomPet | PetImportFailure | null> => ipcRenderer.invoke('pet:import'),
|
||||
importPetBundle: (): Promise<CustomPet | PetImportFailure | null> =>
|
||||
ipcRenderer.invoke('pet:importPetBundle'),
|
||||
read: (id: string, fileName: string, kind?: 'image' | 'bundle'): Promise<ArrayBuffer | null> =>
|
||||
ipcRenderer.invoke('pet:read', id, fileName, kind),
|
||||
delete: (id: string, fileName: string, kind?: 'image' | 'bundle'): Promise<void> =>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RepoIconPickResult } from '../../shared/repo-icon-pick-failure-copy'
|
||||
import type { ExecutionHostId } from '../../shared/execution-host'
|
||||
import type { ShellPathExistence } from '../../shared/path-existence-batch'
|
||||
import type {
|
||||
@@ -30,10 +31,7 @@ export type ShellApi = {
|
||||
pickAttachment: () => Promise<string | null>
|
||||
pickAttachments: () => Promise<string[]>
|
||||
pickImage: () => Promise<string | null>
|
||||
pickRepoIconImage: () => Promise<{
|
||||
dataUrl: string
|
||||
fileName: string
|
||||
} | null>
|
||||
pickRepoIconImage: () => Promise<RepoIconPickResult>
|
||||
pickAudio: () => Promise<string | null>
|
||||
pickDirectory: (args: { defaultPath?: string }) => Promise<string | null>
|
||||
copyFile: (args: { srcPath: string; destPath: string }) => Promise<void>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RepoIconPickResult } from '../../shared/repo-icon-pick-failure-copy'
|
||||
import { ipcRenderer } from 'electron'
|
||||
import type {
|
||||
ShellOpenExternalEditorRequest,
|
||||
@@ -41,7 +42,7 @@ export const shellApi = {
|
||||
|
||||
pickImage: (): Promise<string | null> => ipcRenderer.invoke('shell:pickImage'),
|
||||
|
||||
pickRepoIconImage: (): Promise<{ dataUrl: string; fileName: string } | null> =>
|
||||
pickRepoIconImage: (): Promise<RepoIconPickResult> =>
|
||||
ipcRenderer.invoke('shell:pickRepoIconImage'),
|
||||
|
||||
pickAudio: (): Promise<string | null> => ipcRenderer.invoke('shell:pickAudio'),
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { i18n } from '@/i18n/i18n'
|
||||
import es from '@/i18n/locales/es.json'
|
||||
import { PetImportError } from '../../../../shared/pet-import-failure-copy'
|
||||
import { customPetLabel, petImportFailureText } from './pet-import-failure-text'
|
||||
|
||||
const originalLanguage = i18n.language
|
||||
|
||||
afterEach(async () => {
|
||||
await i18n.changeLanguage(originalLanguage)
|
||||
})
|
||||
|
||||
describe('pet import failure text', () => {
|
||||
it('localizes a coded failure while main keeps the English error', async () => {
|
||||
i18n.addResourceBundle('es', 'translation', es)
|
||||
await i18n.changeLanguage('es')
|
||||
const { failure } = new PetImportError('fileTooLarge', { size: '12.5', max: 10 })
|
||||
expect(failure.error).toBe('File is too large (12.5 MB). Max is 10 MB.')
|
||||
expect(petImportFailureText(failure)).toBe(
|
||||
es.pet.importErrors.fileTooLarge.replace('{{size}}', '12.5').replace('{{max}}', '10')
|
||||
)
|
||||
const parse = new PetImportError('manifestParseFailed', {}, 'Unexpected token }').failure
|
||||
expect(petImportFailureText(parse)).toContain('Unexpected token }')
|
||||
})
|
||||
|
||||
it('shows main English for unknown codes or missing params', async () => {
|
||||
i18n.addResourceBundle('es', 'translation', es)
|
||||
await i18n.changeLanguage('es')
|
||||
expect(petImportFailureText({ ok: false, error: 'Newer', errorCode: 'future' })).toBe('Newer')
|
||||
expect(petImportFailureText({ ok: false, error: 'English', errorCode: 'fileTooLarge' })).toBe(
|
||||
'English'
|
||||
)
|
||||
})
|
||||
|
||||
it('translates only the stored default names', async () => {
|
||||
i18n.addResourceBundle('es', 'translation', es)
|
||||
await i18n.changeLanguage('es')
|
||||
expect(customPetLabel({ label: 'Custom pet', kind: 'image' })).toBe(es.pet.defaultLabels.image)
|
||||
expect(customPetLabel({ label: 'Custom pet' })).toBe(es.pet.defaultLabels.image)
|
||||
expect(customPetLabel({ label: 'Pet bundle', kind: 'bundle' })).toBe(
|
||||
es.pet.defaultLabels.bundle
|
||||
)
|
||||
expect(customPetLabel({ label: 'Mochi', kind: 'bundle' })).toBe('Mochi')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,156 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { CustomPet } from '../../../../shared/pet-types'
|
||||
import {
|
||||
CUSTOM_PET_DEFAULT_LABEL,
|
||||
PET_BUNDLE_DEFAULT_LABEL,
|
||||
PET_IMPORT_FAILURE_COPY as EN,
|
||||
type PetImportFailure,
|
||||
type PetImportFailureCode
|
||||
} from '../../../../shared/pet-import-failure-copy'
|
||||
|
||||
type Values = {
|
||||
text: (name: string) => string | null
|
||||
count: (name: string) => number | null
|
||||
}
|
||||
|
||||
// null = required values missing; caller shows main's English `error`.
|
||||
const COPY: Record<PetImportFailureCode, (values: Values) => string | null> = {
|
||||
unsupportedFile: () => translate('pet.importErrors.unsupportedFile', EN.unsupportedFile),
|
||||
fileUnreadable: () => translate('pet.importErrors.fileUnreadable', EN.fileUnreadable),
|
||||
notAFile: () => translate('pet.importErrors.notAFile', EN.notAFile),
|
||||
fileTooLarge: ({ text, count }) => {
|
||||
const size = text('size')
|
||||
const max = count('max')
|
||||
return size === null || max === null
|
||||
? null
|
||||
: translate('pet.importErrors.fileTooLarge', EN.fileTooLarge, { size, max })
|
||||
},
|
||||
saveFailed: () => translate('pet.importErrors.saveFailed', EN.saveFailed),
|
||||
pathUnreadable: () => translate('pet.importErrors.pathUnreadable', EN.pathUnreadable),
|
||||
manifestMissing: () => translate('pet.importErrors.manifestMissing', EN.manifestMissing),
|
||||
manifestInvalid: () => translate('pet.importErrors.manifestInvalid', EN.manifestInvalid),
|
||||
manifestSymlink: () => translate('pet.importErrors.manifestSymlink', EN.manifestSymlink),
|
||||
manifestParseFailed: ({ text }) => {
|
||||
const detail = text('detail')
|
||||
return detail === null
|
||||
? null
|
||||
: translate('pet.importErrors.manifestParseFailed', EN.manifestParseFailed, { detail })
|
||||
},
|
||||
bundleSaveFailed: () => translate('pet.importErrors.bundleSaveFailed', EN.bundleSaveFailed),
|
||||
sheetPathNotRelative: () =>
|
||||
translate('pet.importErrors.sheetPathNotRelative', EN.sheetPathNotRelative),
|
||||
sheetPathIsRoot: () => translate('pet.importErrors.sheetPathIsRoot', EN.sheetPathIsRoot),
|
||||
sheetPathEscapes: () => translate('pet.importErrors.sheetPathEscapes', EN.sheetPathEscapes),
|
||||
sheetSymlink: () => translate('pet.importErrors.sheetSymlink', EN.sheetSymlink),
|
||||
sheetUnsupportedType: () =>
|
||||
translate('pet.importErrors.sheetUnsupportedType', EN.sheetUnsupportedType),
|
||||
sheetNotFound: () => translate('pet.importErrors.sheetNotFound', EN.sheetNotFound),
|
||||
sheetNotAFile: () => translate('pet.importErrors.sheetNotAFile', EN.sheetNotAFile),
|
||||
sheetTooLarge: ({ text }) => {
|
||||
const size = text('size')
|
||||
return size === null
|
||||
? null
|
||||
: translate('pet.importErrors.sheetTooLarge', EN.sheetTooLarge, { size })
|
||||
},
|
||||
sheetSizeLimitExceeded: () =>
|
||||
translate('pet.importErrors.sheetSizeLimitExceeded', EN.sheetSizeLimitExceeded),
|
||||
sheetUndecodable: () => translate('pet.importErrors.sheetUndecodable', EN.sheetUndecodable),
|
||||
sheetFrameMismatch: ({ count }) => {
|
||||
const sheetWidth = count('sheetWidth')
|
||||
const sheetHeight = count('sheetHeight')
|
||||
const frameWidth = count('frameWidth')
|
||||
const frameHeight = count('frameHeight')
|
||||
return sheetWidth === null ||
|
||||
sheetHeight === null ||
|
||||
frameWidth === null ||
|
||||
frameHeight === null
|
||||
? null
|
||||
: translate('pet.importErrors.sheetFrameMismatch', EN.sheetFrameMismatch, {
|
||||
sheetWidth,
|
||||
sheetHeight,
|
||||
frameWidth,
|
||||
frameHeight
|
||||
})
|
||||
},
|
||||
animationRowOutOfRange: ({ text, count }) => {
|
||||
const name = text('name')
|
||||
const row = count('row')
|
||||
const rows = count('rows')
|
||||
return name === null || row === null || rows === null
|
||||
? null
|
||||
: translate('pet.importErrors.animationRowOutOfRange', EN.animationRowOutOfRange, {
|
||||
name,
|
||||
row,
|
||||
rows
|
||||
})
|
||||
},
|
||||
animationTooManyFrames: ({ text, count }) => {
|
||||
const name = text('name')
|
||||
const frames = count('frames')
|
||||
const columns = count('columns')
|
||||
return name === null || frames === null || columns === null
|
||||
? null
|
||||
: translate('pet.importErrors.animationTooManyFrames', EN.animationTooManyFrames, {
|
||||
name,
|
||||
frames,
|
||||
columns
|
||||
})
|
||||
},
|
||||
animationDurationMismatch: ({ text, count }) => {
|
||||
const name = text('name')
|
||||
const durations = count('durations')
|
||||
const frames = count('frames')
|
||||
return name === null || durations === null || frames === null
|
||||
? null
|
||||
: translate('pet.importErrors.animationDurationMismatch', EN.animationDurationMismatch, {
|
||||
name,
|
||||
durations,
|
||||
frames
|
||||
})
|
||||
},
|
||||
defaultAnimationMissing: ({ text }) => {
|
||||
const name = text('name')
|
||||
return name === null
|
||||
? null
|
||||
: translate('pet.importErrors.defaultAnimationMissing', EN.defaultAnimationMissing, {
|
||||
name
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function isPetImportFailureCode(code: unknown): code is PetImportFailureCode {
|
||||
return typeof code === 'string' && Object.hasOwn(COPY, code)
|
||||
}
|
||||
|
||||
/** Unknown or uncoded failures show main's English text. */
|
||||
export function petImportFailureText(failure: PetImportFailure): string {
|
||||
if (!isPetImportFailureCode(failure.errorCode)) {
|
||||
return failure.error
|
||||
}
|
||||
const params = failure.errorParams ?? {}
|
||||
const read = (name: string): unknown => (Object.hasOwn(params, name) ? params[name] : undefined)
|
||||
return (
|
||||
COPY[failure.errorCode]({
|
||||
text: (name) => {
|
||||
const value = name === 'detail' ? failure.errorDetail : read(name)
|
||||
return typeof value === 'string' ? value : null
|
||||
},
|
||||
count: (name) => {
|
||||
const value = read(name)
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
||||
}
|
||||
}) ?? failure.error
|
||||
)
|
||||
}
|
||||
|
||||
/** Default names are stored in English; show them in the UI language. */
|
||||
export function customPetLabel(pet: Pick<CustomPet, 'label' | 'kind'>): string {
|
||||
if (pet.kind === 'bundle') {
|
||||
return pet.label === PET_BUNDLE_DEFAULT_LABEL
|
||||
? translate('pet.defaultLabels.bundle', PET_BUNDLE_DEFAULT_LABEL)
|
||||
: pet.label
|
||||
}
|
||||
return pet.label === CUSTOM_PET_DEFAULT_LABEL
|
||||
? translate('pet.defaultLabels.image', CUSTOM_PET_DEFAULT_LABEL)
|
||||
: pet.label
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { getRepoLucideIconOptions } from '../../../repo/repo-icon'
|
||||
import { useMountedRef } from '@/hooks/useMountedRef'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { lazyWithRetry } from '@/lib/lazy-with-retry'
|
||||
import { repoIconPickFailureText } from './repo-icon-pick-failure-text'
|
||||
|
||||
const RepositoryIconEmojiPicker = lazyWithRetry(
|
||||
() =>
|
||||
@@ -46,6 +47,10 @@ export function RepositoryIconTabs({
|
||||
if (!result || !mountedRef.current) {
|
||||
return
|
||||
}
|
||||
if ('error' in result) {
|
||||
toast.error(repoIconPickFailureText(result))
|
||||
return
|
||||
}
|
||||
onSetIcon({
|
||||
type: 'image',
|
||||
src: result.dataUrl,
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import {
|
||||
REPO_ICON_PICK_FAILURE_COPY as EN,
|
||||
type RepoIconPickFailure,
|
||||
type RepoIconPickFailureCode
|
||||
} from '../../../../../../shared/repo-icon-pick-failure-copy'
|
||||
|
||||
const COPY: Record<RepoIconPickFailureCode, () => string> = {
|
||||
notPng: () => translate('settings.repository.iconPickErrors.notPng', EN.notPng),
|
||||
tooLarge: () => translate('settings.repository.iconPickErrors.tooLarge', EN.tooLarge)
|
||||
}
|
||||
|
||||
function isRepoIconPickFailureCode(code: unknown): code is RepoIconPickFailureCode {
|
||||
return typeof code === 'string' && Object.hasOwn(COPY, code)
|
||||
}
|
||||
|
||||
/** Unknown or uncoded failures show main's English text. */
|
||||
export function repoIconPickFailureText(failure: RepoIconPickFailure): string {
|
||||
return isRepoIconPickFailureCode(failure.errorCode) ? COPY[failure.errorCode]() : failure.error
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import { BUNDLED_PET, BUNDLED_PETS, findBundledPet, isBundledPetId } from '../pe
|
||||
import { PET_SIZE_MAX, PET_SIZE_MIN } from '../../../../shared/pet-types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
|
||||
import { isPetImportFailure } from '../../../../shared/pet-import-failure-copy'
|
||||
import { customPetLabel, petImportFailureText } from '../pet/pet-import-failure-text'
|
||||
|
||||
// Keep visibility separate from experimental enablement so users can dismiss the pet without revisiting Settings.
|
||||
function PetStatusSegmentInner(): React.JSX.Element {
|
||||
@@ -37,7 +39,11 @@ function PetStatusSegmentInner(): React.JSX.Element {
|
||||
const bundled = isBundledPetId(petId)
|
||||
const activeBundled = bundled ? (findBundledPet(petId) ?? BUNDLED_PET) : null
|
||||
const activeCustom = bundled ? null : customPets.find((m) => m.id === petId)
|
||||
const activeLabel = activeBundled ? activeBundled.label : (activeCustom?.label ?? 'Pet')
|
||||
const activeLabel = activeBundled
|
||||
? activeBundled.label
|
||||
: activeCustom
|
||||
? customPetLabel(activeCustom)
|
||||
: 'Pet'
|
||||
const label = petVisible ? activeLabel : `${activeLabel} hidden`
|
||||
|
||||
const handleImport = async (): Promise<void> => {
|
||||
@@ -58,6 +64,10 @@ function PetStatusSegmentInner(): React.JSX.Element {
|
||||
if (!model) {
|
||||
return
|
||||
}
|
||||
if (isPetImportFailure(model)) {
|
||||
toast.error(petImportFailureText(model))
|
||||
return
|
||||
}
|
||||
addCustomPet(model)
|
||||
if (!petVisible) {
|
||||
setPetVisible(true)
|
||||
@@ -91,6 +101,10 @@ function PetStatusSegmentInner(): React.JSX.Element {
|
||||
if (!model) {
|
||||
return
|
||||
}
|
||||
if (isPetImportFailure(model)) {
|
||||
toast.error(petImportFailureText(model))
|
||||
return
|
||||
}
|
||||
addCustomPet(model)
|
||||
if (!petVisible) {
|
||||
setPetVisible(true)
|
||||
@@ -214,14 +228,14 @@ function PetStatusSegmentInner(): React.JSX.Element {
|
||||
<span className="flex w-4 items-center justify-center">
|
||||
{selected ? <Check className="size-3.5" aria-hidden /> : null}
|
||||
</span>
|
||||
<span className="flex-1 truncate">{model.label}</span>
|
||||
<span className="flex-1 truncate">{customPetLabel(model)}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 flex size-5 items-center justify-center rounded text-muted-foreground hover:bg-destructive/15 hover:text-destructive"
|
||||
aria-label={translate(
|
||||
'auto.components.status.bar.PetStatusSegment.3668339495',
|
||||
'Remove {{value0}}',
|
||||
{ value0: model.label }
|
||||
{ value0: customPetLabel(model) }
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
|
||||
+40
@@ -1996,6 +1996,40 @@
|
||||
"menu": {
|
||||
"quitApp": "Quit {{appName}}"
|
||||
},
|
||||
"pet": {
|
||||
"defaultLabels": {
|
||||
"bundle": "Pet bundle",
|
||||
"image": "Custom pet"
|
||||
},
|
||||
"importErrors": {
|
||||
"animationDurationMismatch": "Animation \"{{name}}\" declares {{durations}} frame durations but {{frames}} frames.",
|
||||
"animationRowOutOfRange": "Animation \"{{name}}\" references row {{row}} but sheet has {{rows}}.",
|
||||
"animationTooManyFrames": "Animation \"{{name}}\" has {{frames}} frames but sheet only has {{columns}} columns.",
|
||||
"bundleSaveFailed": "Could not save the pet bundle.",
|
||||
"defaultAnimationMissing": "defaultAnimation \"{{name}}\" not in animations.",
|
||||
"fileTooLarge": "File is too large ({{size}} MB). Max is {{max}} MB.",
|
||||
"fileUnreadable": "Could not read the selected file.",
|
||||
"manifestInvalid": "pet.json is invalid.",
|
||||
"manifestMissing": "Bundle is missing pet.json.",
|
||||
"manifestParseFailed": "Invalid pet.json: {{detail}}",
|
||||
"manifestSymlink": "pet.json must not be a symlink.",
|
||||
"notAFile": "Selected path is not a file",
|
||||
"pathUnreadable": "Could not read the selected path.",
|
||||
"saveFailed": "Could not save the pet.",
|
||||
"sheetFrameMismatch": "Spritesheet {{sheetWidth}}×{{sheetHeight}} is not a clean multiple of frame {{frameWidth}}×{{frameHeight}}.",
|
||||
"sheetNotAFile": "Spritesheet path is not a file.",
|
||||
"sheetNotFound": "Spritesheet file not found.",
|
||||
"sheetPathEscapes": "spritesheetPath escapes the bundle.",
|
||||
"sheetPathIsRoot": "spritesheetPath must point to a file, not the bundle root.",
|
||||
"sheetPathNotRelative": "spritesheetPath must be relative to the bundle.",
|
||||
"sheetSizeLimitExceeded": "Spritesheet exceeded the size limit.",
|
||||
"sheetSymlink": "spritesheet must not be a symlink.",
|
||||
"sheetTooLarge": "Spritesheet is too large ({{size}} MB).",
|
||||
"sheetUndecodable": "Could not decode the spritesheet image.",
|
||||
"sheetUnsupportedType": "Spritesheet must be a PNG, APNG, JPG, GIF, or WebP.",
|
||||
"unsupportedFile": "Unsupported file. Pick a PNG, APNG, JPG, GIF, WebP, or SVG."
|
||||
}
|
||||
},
|
||||
"projectViews": {
|
||||
"error": {
|
||||
"tooLargeCount_one": "This view has {{count}} item — too large to render in Orca. Narrow the view's filter on GitHub.",
|
||||
@@ -2085,6 +2119,12 @@
|
||||
"unsupportedProtocol": "Use an http, https, socks, socks4, or socks5 proxy URL."
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"iconPickErrors": {
|
||||
"notPng": "Repo icons must be PNG files.",
|
||||
"tooLarge": "Repo icon image must be 256KB or smaller."
|
||||
}
|
||||
},
|
||||
"shortcuts": {
|
||||
"diagnostics": {
|
||||
"conflictingOverrides": "Conflicting custom shortcuts were ignored: {{actions}}.",
|
||||
|
||||
@@ -874,6 +874,12 @@
|
||||
"discardFailed": "Datei zur Prüfung konnte nicht verworfen werden",
|
||||
"deleteFailed": "Gesendete Diagnosedaten konnten nicht gelöscht werden"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"iconPickErrors": {
|
||||
"notPng": "Repository-Symbole müssen PNG-Dateien sein.",
|
||||
"tooLarge": "Das Bild für das Repository-Symbol darf höchstens 256 KB groß sein."
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -22326,5 +22332,39 @@
|
||||
"repairing": "Orca repariert gerade die Berechtigungen.",
|
||||
"repairFailed": "Orca konnte sie nicht reparieren. Meist sind für den Ordner Administratorrechte erforderlich.",
|
||||
"runCommands": "Diese Befehle in einer Eingabeaufforderung mit Administratorrechten ausführen und Orca anschließend erneut starten:"
|
||||
},
|
||||
"pet": {
|
||||
"importErrors": {
|
||||
"unsupportedFile": "Nicht unterstützte Datei. PNG, APNG, JPG, GIF, WebP oder SVG auswählen.",
|
||||
"fileUnreadable": "Die ausgewählte Datei konnte nicht gelesen werden.",
|
||||
"notAFile": "Der ausgewählte Pfad ist keine Datei",
|
||||
"fileTooLarge": "Die Datei ist zu groß ({{size}} MB). Maximal {{max}} MB.",
|
||||
"saveFailed": "Das Haustier konnte nicht gespeichert werden.",
|
||||
"pathUnreadable": "Der ausgewählte Pfad konnte nicht gelesen werden.",
|
||||
"manifestMissing": "Im Paket fehlt pet.json.",
|
||||
"manifestInvalid": "pet.json ist ungültig.",
|
||||
"manifestSymlink": "pet.json darf kein symbolischer Link sein.",
|
||||
"manifestParseFailed": "Ungültige pet.json: {{detail}}",
|
||||
"bundleSaveFailed": "Das Haustierpaket konnte nicht gespeichert werden.",
|
||||
"sheetPathNotRelative": "spritesheetPath muss relativ zum Paket sein.",
|
||||
"sheetPathIsRoot": "spritesheetPath muss auf eine Datei verweisen, nicht auf das Stammverzeichnis des Pakets.",
|
||||
"sheetPathEscapes": "spritesheetPath liegt außerhalb des Pakets.",
|
||||
"sheetSymlink": "Das Spritesheet darf kein symbolischer Link sein.",
|
||||
"sheetUnsupportedType": "Das Spritesheet muss eine PNG-, APNG-, JPG-, GIF- oder WebP-Datei sein.",
|
||||
"sheetNotFound": "Spritesheet-Datei nicht gefunden.",
|
||||
"sheetNotAFile": "Der Spritesheet-Pfad ist keine Datei.",
|
||||
"sheetTooLarge": "Das Spritesheet ist zu groß ({{size}} MB).",
|
||||
"sheetSizeLimitExceeded": "Das Spritesheet überschreitet die Größenbegrenzung.",
|
||||
"sheetUndecodable": "Das Spritesheet-Bild konnte nicht dekodiert werden.",
|
||||
"sheetFrameMismatch": "Spritesheet {{sheetWidth}}×{{sheetHeight}} ist kein ganzzahliges Vielfaches der Frame-Größe {{frameWidth}}×{{frameHeight}}.",
|
||||
"animationRowOutOfRange": "Animation „{{name}}“ verweist auf Zeile {{row}}, aber die Zeilenanzahl im Spritesheet beträgt nur {{rows}}.",
|
||||
"animationTooManyFrames": "Die Frame-Anzahl der Animation „{{name}}“ ({{frames}}) übersteigt die Spaltenanzahl des Spritesheets ({{columns}}).",
|
||||
"animationDurationMismatch": "Bei der Animation „{{name}}“ stimmt die Anzahl der angegebenen Frame-Dauern ({{durations}}) nicht mit der Frame-Anzahl ({{frames}}) überein.",
|
||||
"defaultAnimationMissing": "defaultAnimation „{{name}}“ ist nicht in animations enthalten."
|
||||
},
|
||||
"defaultLabels": {
|
||||
"image": "Benutzerdefiniertes Haustier",
|
||||
"bundle": "Haustierpaket"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -892,6 +892,12 @@
|
||||
"discardFailed": "Could not discard review file",
|
||||
"deleteFailed": "Could not delete sent diagnostics"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"iconPickErrors": {
|
||||
"notPng": "Repo icons must be PNG files.",
|
||||
"tooLarge": "Repo icon image must be 256KB or smaller."
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -22790,5 +22796,39 @@
|
||||
"repairing": "Orca is repairing the permissions now.",
|
||||
"repairFailed": "Orca could not repair them, which usually means the folder needs an administrator.",
|
||||
"runCommands": "Run these in an Administrator Command Prompt, then relaunch Orca:"
|
||||
},
|
||||
"pet": {
|
||||
"importErrors": {
|
||||
"unsupportedFile": "Unsupported file. Pick a PNG, APNG, JPG, GIF, WebP, or SVG.",
|
||||
"fileUnreadable": "Could not read the selected file.",
|
||||
"notAFile": "Selected path is not a file",
|
||||
"fileTooLarge": "File is too large ({{size}} MB). Max is {{max}} MB.",
|
||||
"saveFailed": "Could not save the pet.",
|
||||
"pathUnreadable": "Could not read the selected path.",
|
||||
"manifestMissing": "Bundle is missing pet.json.",
|
||||
"manifestInvalid": "pet.json is invalid.",
|
||||
"manifestSymlink": "pet.json must not be a symlink.",
|
||||
"manifestParseFailed": "Invalid pet.json: {{detail}}",
|
||||
"bundleSaveFailed": "Could not save the pet bundle.",
|
||||
"sheetPathNotRelative": "spritesheetPath must be relative to the bundle.",
|
||||
"sheetPathIsRoot": "spritesheetPath must point to a file, not the bundle root.",
|
||||
"sheetPathEscapes": "spritesheetPath escapes the bundle.",
|
||||
"sheetSymlink": "spritesheet must not be a symlink.",
|
||||
"sheetUnsupportedType": "Spritesheet must be a PNG, APNG, JPG, GIF, or WebP.",
|
||||
"sheetNotFound": "Spritesheet file not found.",
|
||||
"sheetNotAFile": "Spritesheet path is not a file.",
|
||||
"sheetTooLarge": "Spritesheet is too large ({{size}} MB).",
|
||||
"sheetSizeLimitExceeded": "Spritesheet exceeded the size limit.",
|
||||
"sheetUndecodable": "Could not decode the spritesheet image.",
|
||||
"sheetFrameMismatch": "Spritesheet {{sheetWidth}}×{{sheetHeight}} is not a clean multiple of frame {{frameWidth}}×{{frameHeight}}.",
|
||||
"animationRowOutOfRange": "Animation \"{{name}}\" references row {{row}} but sheet has {{rows}}.",
|
||||
"animationTooManyFrames": "Animation \"{{name}}\" has {{frames}} frames but sheet only has {{columns}} columns.",
|
||||
"animationDurationMismatch": "Animation \"{{name}}\" declares {{durations}} frame durations but {{frames}} frames.",
|
||||
"defaultAnimationMissing": "defaultAnimation \"{{name}}\" not in animations."
|
||||
},
|
||||
"defaultLabels": {
|
||||
"image": "Custom pet",
|
||||
"bundle": "Pet bundle"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,6 +478,12 @@
|
||||
"discardFailed": "No se pudo descartar el archivo de revisión",
|
||||
"deleteFailed": "No se pudieron eliminar los diagnósticos enviados"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"iconPickErrors": {
|
||||
"notPng": "Los iconos del repositorio deben ser archivos PNG.",
|
||||
"tooLarge": "La imagen del icono del repositorio debe tener un tamaño máximo de 256 KB."
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -22944,5 +22950,39 @@
|
||||
"repairing": "Orca está reparando los permisos.",
|
||||
"repairFailed": "Orca no pudo repararlos, lo que normalmente significa que la carpeta requiere permisos de administrador.",
|
||||
"runCommands": "Ejecuta estos comandos en un Símbolo del sistema abierto como administrador y luego vuelve a iniciar Orca:"
|
||||
},
|
||||
"pet": {
|
||||
"importErrors": {
|
||||
"unsupportedFile": "Archivo no compatible. Elige un PNG, APNG, JPG, GIF, WebP o SVG.",
|
||||
"fileUnreadable": "No se pudo leer el archivo seleccionado.",
|
||||
"notAFile": "La ruta seleccionada no es un archivo",
|
||||
"fileTooLarge": "El archivo es demasiado grande ({{size}} MB). El máximo es {{max}} MB.",
|
||||
"saveFailed": "No se pudo guardar la mascota.",
|
||||
"pathUnreadable": "No se pudo leer la ruta seleccionada.",
|
||||
"manifestMissing": "Falta pet.json en el bundle.",
|
||||
"manifestInvalid": "pet.json no es válido.",
|
||||
"manifestSymlink": "pet.json no puede ser un enlace simbólico.",
|
||||
"manifestParseFailed": "pet.json no válido: {{detail}}",
|
||||
"bundleSaveFailed": "No se pudo guardar el bundle de mascota.",
|
||||
"sheetPathNotRelative": "spritesheetPath debe ser una ruta relativa al bundle.",
|
||||
"sheetPathIsRoot": "spritesheetPath debe apuntar a un archivo, no a la raíz del bundle.",
|
||||
"sheetPathEscapes": "spritesheetPath apunta fuera del bundle.",
|
||||
"sheetSymlink": "La hoja de sprites no puede ser un enlace simbólico.",
|
||||
"sheetUnsupportedType": "La hoja de sprites debe ser un archivo PNG, APNG, JPG, GIF o WebP.",
|
||||
"sheetNotFound": "No se encontró el archivo de la hoja de sprites.",
|
||||
"sheetNotAFile": "La ruta de la hoja de sprites no es un archivo.",
|
||||
"sheetTooLarge": "La hoja de sprites es demasiado grande ({{size}} MB).",
|
||||
"sheetSizeLimitExceeded": "La hoja de sprites superó el límite de tamaño.",
|
||||
"sheetUndecodable": "No se pudo decodificar la imagen de la hoja de sprites.",
|
||||
"sheetFrameMismatch": "La hoja de sprites de {{sheetWidth}}×{{sheetHeight}} no es un múltiplo exacto del fotograma de {{frameWidth}}×{{frameHeight}}.",
|
||||
"animationRowOutOfRange": "La animación «{{name}}» hace referencia a la fila {{row}}, pero la hoja solo tiene {{rows}}.",
|
||||
"animationTooManyFrames": "El número de fotogramas de la animación «{{name}}» ({{frames}}) supera el número de columnas de la hoja ({{columns}}).",
|
||||
"animationDurationMismatch": "En la animación «{{name}}», el número de duraciones de fotograma declaradas ({{durations}}) no coincide con el número de fotogramas ({{frames}}).",
|
||||
"defaultAnimationMissing": "defaultAnimation «{{name}}» no está en animations."
|
||||
},
|
||||
"defaultLabels": {
|
||||
"image": "Mascota personalizada",
|
||||
"bundle": "Bundle de mascota"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,6 +698,12 @@
|
||||
"discardFailed": "Impossible d’abandonner le fichier de révision",
|
||||
"deleteFailed": "Impossible de supprimer les diagnostics envoyés"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"iconPickErrors": {
|
||||
"notPng": "Les icônes de dépôt doivent être des fichiers PNG.",
|
||||
"tooLarge": "L’image de l’icône du dépôt doit faire 256 Ko maximum."
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -22944,5 +22950,39 @@
|
||||
"repairing": "Orca corrige les autorisations.",
|
||||
"repairFailed": "Orca n'a pas pu les corriger, ce qui signifie généralement que le dossier nécessite des droits d'administrateur.",
|
||||
"runCommands": "Exécutez ces commandes dans une invite de commandes ouverte en tant qu'administrateur, puis relancez Orca :"
|
||||
},
|
||||
"pet": {
|
||||
"importErrors": {
|
||||
"unsupportedFile": "Fichier non pris en charge. Choisissez un PNG, APNG, JPG, GIF, WebP ou SVG.",
|
||||
"fileUnreadable": "Impossible de lire le fichier sélectionné.",
|
||||
"notAFile": "Le chemin sélectionné n’est pas un fichier",
|
||||
"fileTooLarge": "Le fichier est trop volumineux ({{size}} Mo). Maximum : {{max}} Mo.",
|
||||
"saveFailed": "Impossible d’enregistrer le compagnon.",
|
||||
"pathUnreadable": "Impossible de lire le chemin sélectionné.",
|
||||
"manifestMissing": "Le bundle ne contient pas pet.json.",
|
||||
"manifestInvalid": "pet.json n’est pas valide.",
|
||||
"manifestSymlink": "pet.json ne doit pas être un lien symbolique.",
|
||||
"manifestParseFailed": "pet.json non valide : {{detail}}",
|
||||
"bundleSaveFailed": "Impossible d’enregistrer le bundle de compagnon.",
|
||||
"sheetPathNotRelative": "spritesheetPath doit être relatif au bundle.",
|
||||
"sheetPathIsRoot": "spritesheetPath doit pointer vers un fichier, pas vers la racine du bundle.",
|
||||
"sheetPathEscapes": "spritesheetPath pointe en dehors du bundle.",
|
||||
"sheetSymlink": "La feuille de sprites ne doit pas être un lien symbolique.",
|
||||
"sheetUnsupportedType": "La feuille de sprites doit être un fichier PNG, APNG, JPG, GIF ou WebP.",
|
||||
"sheetNotFound": "Fichier de feuille de sprites introuvable.",
|
||||
"sheetNotAFile": "Le chemin de la feuille de sprites n’est pas un fichier.",
|
||||
"sheetTooLarge": "La feuille de sprites est trop volumineuse ({{size}} Mo).",
|
||||
"sheetSizeLimitExceeded": "La feuille de sprites a dépassé la taille limite.",
|
||||
"sheetUndecodable": "Impossible de décoder l’image de la feuille de sprites.",
|
||||
"sheetFrameMismatch": "La feuille de sprites {{sheetWidth}}×{{sheetHeight}} n’est pas un multiple exact du format d’image {{frameWidth}}×{{frameHeight}}.",
|
||||
"animationRowOutOfRange": "L’animation « {{name}} » fait référence à la ligne {{row}}, mais la feuille n’en a que {{rows}}.",
|
||||
"animationTooManyFrames": "Le nombre d’images de l’animation « {{name}} » ({{frames}}) dépasse le nombre de colonnes de la feuille ({{columns}}).",
|
||||
"animationDurationMismatch": "Pour l’animation « {{name}} », le nombre de durées d’image déclarées ({{durations}}) ne correspond pas au nombre d’images ({{frames}}).",
|
||||
"defaultAnimationMissing": "defaultAnimation « {{name}} » n’est pas dans animations."
|
||||
},
|
||||
"defaultLabels": {
|
||||
"image": "Compagnon personnalisé",
|
||||
"bundle": "Bundle de compagnon"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,6 +474,12 @@
|
||||
"discardFailed": "確認用ファイルを破棄できませんでした",
|
||||
"deleteFailed": "送信済みの診断情報を削除できませんでした"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"iconPickErrors": {
|
||||
"notPng": "リポジトリアイコンには PNG ファイルを使用してください。",
|
||||
"tooLarge": "リポジトリアイコンの画像は 256 KB 以下にしてください。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -22322,5 +22328,39 @@
|
||||
"repairing": "Orca が権限を修復しています。",
|
||||
"repairFailed": "Orca は権限を修復できませんでした。通常、フォルダーへの操作に管理者権限が必要な場合に発生します。",
|
||||
"runCommands": "管理者権限のコマンドプロンプトで次のコマンドを実行し、Orca を再起動してください:"
|
||||
},
|
||||
"pet": {
|
||||
"importErrors": {
|
||||
"unsupportedFile": "未対応のファイルです。PNG、APNG、JPG、GIF、WebP、SVG のいずれかを選択してください。",
|
||||
"fileUnreadable": "選択したファイルを読み取れませんでした。",
|
||||
"notAFile": "選択したパスはファイルではありません",
|
||||
"fileTooLarge": "ファイルが大きすぎます({{size}} MB)。上限は {{max}} MB です。",
|
||||
"saveFailed": "ペットを保存できませんでした。",
|
||||
"pathUnreadable": "選択したパスを読み取れませんでした。",
|
||||
"manifestMissing": "バンドルに pet.json がありません。",
|
||||
"manifestInvalid": "pet.json が無効です。",
|
||||
"manifestSymlink": "pet.json はシンボリックリンクにできません。",
|
||||
"manifestParseFailed": "無効な pet.json: {{detail}}",
|
||||
"bundleSaveFailed": "ペットバンドルを保存できませんでした。",
|
||||
"sheetPathNotRelative": "spritesheetPath はバンドルからの相対パスである必要があります。",
|
||||
"sheetPathIsRoot": "spritesheetPath はバンドルのルートではなく、ファイルを指す必要があります。",
|
||||
"sheetPathEscapes": "spritesheetPath がバンドルの外を指しています。",
|
||||
"sheetSymlink": "スプライトシートはシンボリックリンクにできません。",
|
||||
"sheetUnsupportedType": "スプライトシートは PNG、APNG、JPG、GIF、WebP のいずれかである必要があります。",
|
||||
"sheetNotFound": "スプライトシートのファイルが見つかりません。",
|
||||
"sheetNotAFile": "スプライトシートのパスはファイルではありません。",
|
||||
"sheetTooLarge": "スプライトシートが大きすぎます({{size}} MB)。",
|
||||
"sheetSizeLimitExceeded": "スプライトシートがサイズ上限を超えました。",
|
||||
"sheetUndecodable": "スプライトシート画像をデコードできませんでした。",
|
||||
"sheetFrameMismatch": "スプライトシートのサイズ {{sheetWidth}}×{{sheetHeight}} はフレームサイズ {{frameWidth}}×{{frameHeight}} の整数倍ではありません。",
|
||||
"animationRowOutOfRange": "アニメーション「{{name}}」は {{row}} 行目を参照していますが、シートには {{rows}} 行しかありません。",
|
||||
"animationTooManyFrames": "アニメーション「{{name}}」には {{frames}} フレームありますが、シートには {{columns}} 列しかありません。",
|
||||
"animationDurationMismatch": "アニメーション「{{name}}」で指定されたフレーム時間は {{durations}} 個ですが、フレーム数は {{frames}} です。",
|
||||
"defaultAnimationMissing": "defaultAnimation「{{name}}」が animations にありません。"
|
||||
},
|
||||
"defaultLabels": {
|
||||
"image": "カスタムペット",
|
||||
"bundle": "ペットバンドル"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,6 +474,12 @@
|
||||
"discardFailed": "검토 파일을 버리지 못했습니다",
|
||||
"deleteFailed": "전송한 진단 정보를 삭제하지 못했습니다"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"iconPickErrors": {
|
||||
"notPng": "저장소 아이콘은 PNG 파일이어야 합니다.",
|
||||
"tooLarge": "저장소 아이콘 이미지는 256 KB 이하여야 합니다."
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -22322,5 +22328,39 @@
|
||||
"repairing": "Orca가 권한을 복구하고 있습니다.",
|
||||
"repairFailed": "Orca가 권한을 복구하지 못했습니다. 일반적으로 해당 폴더에 관리자 권한이 필요하다는 뜻입니다.",
|
||||
"runCommands": "관리자 권한으로 연 명령 프롬프트에서 다음 명령을 실행한 후 Orca를 다시 실행하세요:"
|
||||
},
|
||||
"pet": {
|
||||
"importErrors": {
|
||||
"unsupportedFile": "지원하지 않는 파일입니다. PNG, APNG, JPG, GIF, WebP 또는 SVG를 선택하세요.",
|
||||
"fileUnreadable": "선택한 파일을 읽지 못했습니다.",
|
||||
"notAFile": "선택한 경로가 파일이 아닙니다",
|
||||
"fileTooLarge": "파일이 너무 큽니다({{size}} MB). 최대 크기는 {{max}} MB입니다.",
|
||||
"saveFailed": "애완동물을 저장하지 못했습니다.",
|
||||
"pathUnreadable": "선택한 경로를 읽지 못했습니다.",
|
||||
"manifestMissing": "번들에 pet.json이 없습니다.",
|
||||
"manifestInvalid": "pet.json이 유효하지 않습니다.",
|
||||
"manifestSymlink": "pet.json은 심볼릭 링크일 수 없습니다.",
|
||||
"manifestParseFailed": "유효하지 않은 pet.json: {{detail}}",
|
||||
"bundleSaveFailed": "애완동물 번들을 저장하지 못했습니다.",
|
||||
"sheetPathNotRelative": "spritesheetPath는 번들 기준 상대 경로여야 합니다.",
|
||||
"sheetPathIsRoot": "spritesheetPath는 번들 루트가 아닌 파일을 가리켜야 합니다.",
|
||||
"sheetPathEscapes": "spritesheetPath가 번들 외부를 가리킵니다.",
|
||||
"sheetSymlink": "스프라이트 시트는 심볼릭 링크일 수 없습니다.",
|
||||
"sheetUnsupportedType": "스프라이트 시트는 PNG, APNG, JPG, GIF 또는 WebP 파일이어야 합니다.",
|
||||
"sheetNotFound": "스프라이트 시트 파일을 찾을 수 없습니다.",
|
||||
"sheetNotAFile": "스프라이트 시트 경로가 파일이 아닙니다.",
|
||||
"sheetTooLarge": "스프라이트 시트가 너무 큽니다({{size}} MB).",
|
||||
"sheetSizeLimitExceeded": "스프라이트 시트가 크기 제한을 초과했습니다.",
|
||||
"sheetUndecodable": "스프라이트 시트 이미지를 디코딩하지 못했습니다.",
|
||||
"sheetFrameMismatch": "스프라이트 시트({{sheetWidth}}×{{sheetHeight}})의 크기가 프레임({{frameWidth}}×{{frameHeight}}) 크기의 정수배가 아닙니다.",
|
||||
"animationRowOutOfRange": "\"{{name}}\" 애니메이션은 {{row}}행을 참조하지만 시트에는 {{rows}}행만 있습니다.",
|
||||
"animationTooManyFrames": "애니메이션 \"{{name}}\"에 {{frames}}개의 프레임이 있지만 시트에는 {{columns}}개의 열만 있습니다.",
|
||||
"animationDurationMismatch": "애니메이션 \"{{name}}\"에 프레임 지속 시간이 {{durations}}개 지정되어 있지만 프레임은 {{frames}}개입니다.",
|
||||
"defaultAnimationMissing": "defaultAnimation으로 지정된 \"{{name}}\" 애니메이션이 animations에 없습니다."
|
||||
},
|
||||
"defaultLabels": {
|
||||
"image": "사용자 정의 애완동물",
|
||||
"bundle": "애완동물 번들"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1483,6 +1483,12 @@
|
||||
"discardFailed": "Não foi possível descartar o arquivo de revisão",
|
||||
"deleteFailed": "Não foi possível excluir os diagnósticos enviados"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"iconPickErrors": {
|
||||
"notPng": "Os ícones do repositório devem ser arquivos PNG.",
|
||||
"tooLarge": "A imagem do ícone do repositório deve ter no máximo 256 KB."
|
||||
}
|
||||
}
|
||||
},
|
||||
"auto": {
|
||||
@@ -22757,5 +22763,39 @@
|
||||
"repairing": "O Orca está corrigindo as permissões.",
|
||||
"repairFailed": "O Orca não conseguiu corrigi-las, o que geralmente significa que a pasta exige permissões de administrador.",
|
||||
"runCommands": "Execute estes comandos em um Prompt de Comando como administrador e abra o Orca novamente:"
|
||||
},
|
||||
"pet": {
|
||||
"importErrors": {
|
||||
"unsupportedFile": "Arquivo não compatível. Escolha um PNG, APNG, JPG, GIF, WebP ou SVG.",
|
||||
"fileUnreadable": "Não foi possível ler o arquivo selecionado.",
|
||||
"notAFile": "O caminho selecionado não é um arquivo",
|
||||
"fileTooLarge": "O arquivo é muito grande ({{size}} MB). O máximo é {{max}} MB.",
|
||||
"saveFailed": "Não foi possível salvar o bichinho.",
|
||||
"pathUnreadable": "Não foi possível ler o caminho selecionado.",
|
||||
"manifestMissing": "O pacote não contém pet.json.",
|
||||
"manifestInvalid": "pet.json é inválido.",
|
||||
"manifestSymlink": "pet.json não pode ser um link simbólico.",
|
||||
"manifestParseFailed": "pet.json inválido: {{detail}}",
|
||||
"bundleSaveFailed": "Não foi possível salvar o pacote do bichinho.",
|
||||
"sheetPathNotRelative": "spritesheetPath deve ser relativo ao pacote.",
|
||||
"sheetPathIsRoot": "spritesheetPath deve apontar para um arquivo, não para a raiz do pacote.",
|
||||
"sheetPathEscapes": "spritesheetPath aponta para fora do pacote.",
|
||||
"sheetSymlink": "A folha de sprites não pode ser um link simbólico.",
|
||||
"sheetUnsupportedType": "A folha de sprites deve ser um arquivo PNG, APNG, JPG, GIF ou WebP.",
|
||||
"sheetNotFound": "Arquivo da folha de sprites não encontrado.",
|
||||
"sheetNotAFile": "O caminho da folha de sprites não é um arquivo.",
|
||||
"sheetTooLarge": "A folha de sprites é muito grande ({{size}} MB).",
|
||||
"sheetSizeLimitExceeded": "A folha de sprites excedeu o limite de tamanho.",
|
||||
"sheetUndecodable": "Não foi possível decodificar a imagem da folha de sprites.",
|
||||
"sheetFrameMismatch": "A folha de sprites {{sheetWidth}}×{{sheetHeight}} não é um múltiplo exato do quadro {{frameWidth}}×{{frameHeight}}.",
|
||||
"animationRowOutOfRange": "A animação \"{{name}}\" faz referência à linha {{row}}, mas a folha tem apenas {{rows}}.",
|
||||
"animationTooManyFrames": "O número de quadros da animação \"{{name}}\" ({{frames}}) excede o número de colunas da folha ({{columns}}).",
|
||||
"animationDurationMismatch": "Na animação \"{{name}}\", o número de durações de quadro declaradas ({{durations}}) não corresponde ao número de quadros ({{frames}}).",
|
||||
"defaultAnimationMissing": "defaultAnimation \"{{name}}\" não está em animations."
|
||||
},
|
||||
"defaultLabels": {
|
||||
"image": "Bichinho personalizado",
|
||||
"bundle": "Pacote do bichinho"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -868,6 +868,12 @@
|
||||
"discardFailed": "Не удалось удалить файл для проверки",
|
||||
"deleteFailed": "Не удалось удалить отправленную диагностику"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"iconPickErrors": {
|
||||
"notPng": "Значки репозитория должны быть файлами PNG.",
|
||||
"tooLarge": "Размер изображения значка репозитория не должен превышать 256 КБ."
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -23007,5 +23013,39 @@
|
||||
"repairing": "Orca исправляет права доступа.",
|
||||
"repairFailed": "Orca не удалось их исправить. Обычно это означает, что для изменения прав доступа к папке нужны права администратора.",
|
||||
"runCommands": "Выполните эти команды в командной строке от имени администратора, затем снова запустите Orca:"
|
||||
},
|
||||
"pet": {
|
||||
"importErrors": {
|
||||
"unsupportedFile": "Неподдерживаемый файл. Выберите PNG, APNG, JPG, GIF, WebP или SVG.",
|
||||
"fileUnreadable": "Не удалось прочитать выбранный файл.",
|
||||
"notAFile": "Выбранный путь не указывает на файл",
|
||||
"fileTooLarge": "Файл слишком большой ({{size}} МБ). Максимум — {{max}} МБ.",
|
||||
"saveFailed": "Не удалось сохранить питомца.",
|
||||
"pathUnreadable": "Не удалось прочитать выбранный путь.",
|
||||
"manifestMissing": "В пакете отсутствует pet.json.",
|
||||
"manifestInvalid": "Файл pet.json некорректен.",
|
||||
"manifestSymlink": "pet.json не должен быть символической ссылкой.",
|
||||
"manifestParseFailed": "Некорректный pet.json: {{detail}}",
|
||||
"bundleSaveFailed": "Не удалось сохранить пакет питомца.",
|
||||
"sheetPathNotRelative": "spritesheetPath должен быть путём относительно пакета.",
|
||||
"sheetPathIsRoot": "spritesheetPath должен указывать на файл, а не на корневую папку пакета.",
|
||||
"sheetPathEscapes": "spritesheetPath выходит за пределы пакета.",
|
||||
"sheetSymlink": "Спрайт-лист не должен быть символической ссылкой.",
|
||||
"sheetUnsupportedType": "Спрайт-лист должен быть файлом PNG, APNG, JPG, GIF или WebP.",
|
||||
"sheetNotFound": "Файл спрайт-листа не найден.",
|
||||
"sheetNotAFile": "Путь к спрайт-листу не указывает на файл.",
|
||||
"sheetTooLarge": "Спрайт-лист слишком большой ({{size}} МБ).",
|
||||
"sheetSizeLimitExceeded": "Размер спрайт-листа превышает допустимый предел.",
|
||||
"sheetUndecodable": "Не удалось декодировать изображение спрайт-листа.",
|
||||
"sheetFrameMismatch": "Размер спрайт-листа {{sheetWidth}}×{{sheetHeight}} не кратен размеру кадра {{frameWidth}}×{{frameHeight}}.",
|
||||
"animationRowOutOfRange": "Анимация «{{name}}» ссылается на строку {{row}}, но число строк в спрайт-листе — {{rows}}.",
|
||||
"animationTooManyFrames": "В анимации «{{name}}» кадров: {{frames}}, но в спрайт-листе столбцов всего {{columns}}.",
|
||||
"animationDurationMismatch": "В анимации «{{name}}» число длительностей кадров — {{durations}}, а число кадров — {{frames}}.",
|
||||
"defaultAnimationMissing": "defaultAnimation «{{name}}» отсутствует в animations."
|
||||
},
|
||||
"defaultLabels": {
|
||||
"image": "Пользовательский питомец",
|
||||
"bundle": "Пакет питомца"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,6 +852,12 @@
|
||||
"discardFailed": "İnceleme dosyası atılamadı",
|
||||
"deleteFailed": "Gönderilen tanılama bilgileri silinemedi"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"iconPickErrors": {
|
||||
"notPng": "Repo simgeleri PNG dosyaları olmalıdır.",
|
||||
"tooLarge": "Repo simgesi görseli 256 KB veya daha küçük olmalıdır."
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -22307,5 +22313,39 @@
|
||||
"repairing": "Orca şu anda izinleri onarıyor.",
|
||||
"repairFailed": "İzinler Orca tarafından onarılamadı. Bu genellikle klasör için yönetici yetkisi gerektiği anlamına gelir.",
|
||||
"runCommands": "Yönetici olarak açılan Komut İstemi penceresinde bu komutları çalıştırın, ardından Orca'yı yeniden başlatın:"
|
||||
},
|
||||
"pet": {
|
||||
"importErrors": {
|
||||
"unsupportedFile": "Desteklenmeyen dosya. PNG, APNG, JPG, GIF, WebP veya SVG seçin.",
|
||||
"fileUnreadable": "Seçilen dosya okunamadı.",
|
||||
"notAFile": "Seçilen yol bir dosya değil",
|
||||
"fileTooLarge": "Dosya çok büyük ({{size}} MB). En fazla {{max}} MB olabilir.",
|
||||
"saveFailed": "Evcil hayvan kaydedilemedi.",
|
||||
"pathUnreadable": "Seçilen yol okunamadı.",
|
||||
"manifestMissing": "Pakette pet.json eksik.",
|
||||
"manifestInvalid": "pet.json geçersiz.",
|
||||
"manifestSymlink": "pet.json sembolik bağlantı olmamalıdır.",
|
||||
"manifestParseFailed": "Geçersiz pet.json: {{detail}}",
|
||||
"bundleSaveFailed": "Evcil hayvan paketi kaydedilemedi.",
|
||||
"sheetPathNotRelative": "spritesheetPath, pakete göre göreli bir yol olmalıdır.",
|
||||
"sheetPathIsRoot": "spritesheetPath, paket kökü yerine bir dosyayı göstermelidir.",
|
||||
"sheetPathEscapes": "spritesheetPath paket dışına çıkıyor.",
|
||||
"sheetSymlink": "Sprite sayfası sembolik bağlantı olmamalıdır.",
|
||||
"sheetUnsupportedType": "Sprite sayfası PNG, APNG, JPG, GIF veya WebP olmalıdır.",
|
||||
"sheetNotFound": "Sprite sayfası dosyası bulunamadı.",
|
||||
"sheetNotAFile": "Sprite sayfasının yolu bir dosya değil.",
|
||||
"sheetTooLarge": "Sprite sayfası çok büyük ({{size}} MB).",
|
||||
"sheetSizeLimitExceeded": "Sprite sayfası boyut sınırını aştı.",
|
||||
"sheetUndecodable": "Sprite sayfası görüntüsünün kodu çözülemedi.",
|
||||
"sheetFrameMismatch": "Sprite sayfası boyutu {{sheetWidth}}×{{sheetHeight}}, kare boyutu {{frameWidth}}×{{frameHeight}} değerinin tam katı değil.",
|
||||
"animationRowOutOfRange": "\"{{name}}\" animasyonu {{row}}. satıra başvuruyor, ancak sayfada {{rows}} satır var.",
|
||||
"animationTooManyFrames": "\"{{name}}\" animasyonunda {{frames}} kare var, ancak sayfada yalnızca {{columns}} sütun var.",
|
||||
"animationDurationMismatch": "\"{{name}}\" animasyonu {{durations}} kare süresi tanımlıyor, ancak {{frames}} kare içeriyor.",
|
||||
"defaultAnimationMissing": "defaultAnimation \"{{name}}\", animations içinde yok."
|
||||
},
|
||||
"defaultLabels": {
|
||||
"image": "Özel evcil hayvan",
|
||||
"bundle": "Evcil hayvan paketi"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -848,6 +848,12 @@
|
||||
"discardFailed": "無法丟棄檢視檔案",
|
||||
"deleteFailed": "無法刪除已傳送的診斷資訊"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"iconPickErrors": {
|
||||
"notPng": "存放庫圖示必須是 PNG 檔案。",
|
||||
"tooLarge": "存放庫圖示圖片不得超過 256 KB。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -22026,5 +22032,39 @@
|
||||
"repairing": "Orca 正在修復權限。",
|
||||
"repairFailed": "Orca 無法修復權限,這通常表示該資料夾需要系統管理員權限。",
|
||||
"runCommands": "在以系統管理員身分開啟的命令提示字元中執行以下指令,然後重新啟動 Orca:"
|
||||
},
|
||||
"pet": {
|
||||
"importErrors": {
|
||||
"unsupportedFile": "不支援此檔案。請選擇 PNG、APNG、JPG、GIF、WebP 或 SVG。",
|
||||
"fileUnreadable": "無法讀取所選檔案。",
|
||||
"notAFile": "所選路徑不是檔案",
|
||||
"fileTooLarge": "檔案過大({{size}} MB)。上限為 {{max}} MB。",
|
||||
"saveFailed": "無法儲存寵物。",
|
||||
"pathUnreadable": "無法讀取所選路徑。",
|
||||
"manifestMissing": "套件缺少 pet.json。",
|
||||
"manifestInvalid": "pet.json 無效。",
|
||||
"manifestSymlink": "pet.json 不得為符號連結。",
|
||||
"manifestParseFailed": "無效的 pet.json:{{detail}}",
|
||||
"bundleSaveFailed": "無法儲存寵物套件。",
|
||||
"sheetPathNotRelative": "spritesheetPath 必須是相對於套件的路徑。",
|
||||
"sheetPathIsRoot": "spritesheetPath 必須指向檔案,而非套件的根目錄。",
|
||||
"sheetPathEscapes": "spritesheetPath 指向套件以外的路徑。",
|
||||
"sheetSymlink": "精靈圖集不得為符號連結。",
|
||||
"sheetUnsupportedType": "精靈圖集必須是 PNG、APNG、JPG、GIF 或 WebP 檔案。",
|
||||
"sheetNotFound": "找不到精靈圖集檔案。",
|
||||
"sheetNotAFile": "精靈圖集路徑不是檔案。",
|
||||
"sheetTooLarge": "精靈圖集過大({{size}} MB)。",
|
||||
"sheetSizeLimitExceeded": "精靈圖集超過大小限制。",
|
||||
"sheetUndecodable": "無法解碼精靈圖集影像。",
|
||||
"sheetFrameMismatch": "精靈圖集尺寸 {{sheetWidth}}×{{sheetHeight}} 不是影格尺寸 {{frameWidth}}×{{frameHeight}} 的整數倍。",
|
||||
"animationRowOutOfRange": "動畫「{{name}}」參照第 {{row}} 列,但精靈圖集只有 {{rows}} 列。",
|
||||
"animationTooManyFrames": "動畫「{{name}}」有 {{frames}} 個影格,但精靈圖集只有 {{columns}} 欄。",
|
||||
"animationDurationMismatch": "動畫「{{name}}」宣告了 {{durations}} 個影格持續時間,但有 {{frames}} 個影格。",
|
||||
"defaultAnimationMissing": "defaultAnimation 「{{name}}」不在 animations 中。"
|
||||
},
|
||||
"defaultLabels": {
|
||||
"image": "自訂寵物",
|
||||
"bundle": "寵物套件"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,6 +474,12 @@
|
||||
"discardFailed": "无法丢弃查看文件",
|
||||
"deleteFailed": "无法删除已发送的诊断信息"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"iconPickErrors": {
|
||||
"notPng": "仓库图标必须是 PNG 文件。",
|
||||
"tooLarge": "仓库图标图片不得超过 256 KB。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -22323,5 +22329,39 @@
|
||||
"repairing": "Orca 正在修复权限。",
|
||||
"repairFailed": "Orca 无法修复权限,这通常意味着该文件夹需要管理员权限。",
|
||||
"runCommands": "在以管理员身份运行的命令提示符中执行以下命令,然后重新启动 Orca:"
|
||||
},
|
||||
"pet": {
|
||||
"importErrors": {
|
||||
"unsupportedFile": "不支持此文件。请选择 PNG、APNG、JPG、GIF、WebP 或 SVG。",
|
||||
"fileUnreadable": "无法读取所选文件。",
|
||||
"notAFile": "所选路径不是文件",
|
||||
"fileTooLarge": "文件过大({{size}} MB)。最大允许 {{max}} MB。",
|
||||
"saveFailed": "无法保存宠物。",
|
||||
"pathUnreadable": "无法读取所选路径。",
|
||||
"manifestMissing": "包中缺少 pet.json。",
|
||||
"manifestInvalid": "pet.json 无效。",
|
||||
"manifestSymlink": "pet.json 不能是符号链接。",
|
||||
"manifestParseFailed": "无效的 pet.json:{{detail}}",
|
||||
"bundleSaveFailed": "无法保存宠物包。",
|
||||
"sheetPathNotRelative": "spritesheetPath 必须是相对于包的路径。",
|
||||
"sheetPathIsRoot": "spritesheetPath 必须指向文件,而不是包的根目录。",
|
||||
"sheetPathEscapes": "spritesheetPath 指向包之外的路径。",
|
||||
"sheetSymlink": "精灵图不能是符号链接。",
|
||||
"sheetUnsupportedType": "精灵图必须是 PNG、APNG、JPG、GIF 或 WebP 文件。",
|
||||
"sheetNotFound": "找不到精灵图文件。",
|
||||
"sheetNotAFile": "精灵图路径不是文件。",
|
||||
"sheetTooLarge": "精灵图过大({{size}} MB)。",
|
||||
"sheetSizeLimitExceeded": "精灵图超出了大小限制。",
|
||||
"sheetUndecodable": "无法解码精灵图图像。",
|
||||
"sheetFrameMismatch": "精灵图尺寸 {{sheetWidth}}×{{sheetHeight}} 不是帧尺寸 {{frameWidth}}×{{frameHeight}} 的整数倍。",
|
||||
"animationRowOutOfRange": "动画“{{name}}”引用了第 {{row}} 行,但精灵图只有 {{rows}} 行。",
|
||||
"animationTooManyFrames": "动画“{{name}}”有 {{frames}} 帧,但精灵图只有 {{columns}} 列。",
|
||||
"animationDurationMismatch": "动画“{{name}}”声明了 {{durations}} 个帧时长,但有 {{frames}} 帧。",
|
||||
"defaultAnimationMissing": "defaultAnimation “{{name}}” 不在 animations 中。"
|
||||
},
|
||||
"defaultLabels": {
|
||||
"image": "自定义宠物",
|
||||
"bundle": "宠物包"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// One English table for pet import refusals: main builds the wire `error`, the renderer translates by code.
|
||||
export const PET_IMPORT_FAILURE_COPY = {
|
||||
unsupportedFile: 'Unsupported file. Pick a PNG, APNG, JPG, GIF, WebP, or SVG.',
|
||||
fileUnreadable: 'Could not read the selected file.',
|
||||
notAFile: 'Selected path is not a file',
|
||||
fileTooLarge: 'File is too large ({{size}} MB). Max is {{max}} MB.',
|
||||
saveFailed: 'Could not save the pet.',
|
||||
pathUnreadable: 'Could not read the selected path.',
|
||||
manifestMissing: 'Bundle is missing pet.json.',
|
||||
manifestInvalid: 'pet.json is invalid.',
|
||||
manifestSymlink: 'pet.json must not be a symlink.',
|
||||
manifestParseFailed: 'Invalid pet.json: {{detail}}',
|
||||
bundleSaveFailed: 'Could not save the pet bundle.',
|
||||
sheetPathNotRelative: 'spritesheetPath must be relative to the bundle.',
|
||||
sheetPathIsRoot: 'spritesheetPath must point to a file, not the bundle root.',
|
||||
sheetPathEscapes: 'spritesheetPath escapes the bundle.',
|
||||
sheetSymlink: 'spritesheet must not be a symlink.',
|
||||
sheetUnsupportedType: 'Spritesheet must be a PNG, APNG, JPG, GIF, or WebP.',
|
||||
sheetNotFound: 'Spritesheet file not found.',
|
||||
sheetNotAFile: 'Spritesheet path is not a file.',
|
||||
sheetTooLarge: 'Spritesheet is too large ({{size}} MB).',
|
||||
sheetSizeLimitExceeded: 'Spritesheet exceeded the size limit.',
|
||||
sheetUndecodable: 'Could not decode the spritesheet image.',
|
||||
sheetFrameMismatch:
|
||||
'Spritesheet {{sheetWidth}}×{{sheetHeight}} is not a clean multiple of frame {{frameWidth}}×{{frameHeight}}.',
|
||||
animationRowOutOfRange: 'Animation "{{name}}" references row {{row}} but sheet has {{rows}}.',
|
||||
animationTooManyFrames:
|
||||
'Animation "{{name}}" has {{frames}} frames but sheet only has {{columns}} columns.',
|
||||
animationDurationMismatch:
|
||||
'Animation "{{name}}" declares {{durations}} frame durations but {{frames}} frames.',
|
||||
defaultAnimationMissing: 'defaultAnimation "{{name}}" not in animations.'
|
||||
} as const
|
||||
|
||||
export type PetImportFailureCode = keyof typeof PET_IMPORT_FAILURE_COPY
|
||||
|
||||
export type PetImportFailureParams = Record<string, string | number>
|
||||
|
||||
/** Wire shape: `error` stays English; `errorDetail` is verbatim diagnostic text. */
|
||||
export type PetImportFailure = {
|
||||
ok: false
|
||||
error: string
|
||||
errorCode?: string
|
||||
errorParams?: PetImportFailureParams
|
||||
errorDetail?: string
|
||||
}
|
||||
|
||||
/** Stored as the label when a pet has no usable name; the renderer shows it translated. */
|
||||
export const CUSTOM_PET_DEFAULT_LABEL = 'Custom pet'
|
||||
export const PET_BUNDLE_DEFAULT_LABEL = 'Pet bundle'
|
||||
|
||||
/** Thrown inside main's import steps; the IPC handler returns it as a PetImportFailure. */
|
||||
export class PetImportError extends Error {
|
||||
readonly failure: PetImportFailure
|
||||
|
||||
constructor(code: PetImportFailureCode, params: PetImportFailureParams = {}, detail?: string) {
|
||||
const values = detail === undefined ? params : { ...params, detail }
|
||||
// One pass, so a value containing "{{detail}}" stays verbatim.
|
||||
const error = PET_IMPORT_FAILURE_COPY[code].replace(/\{\{(\w+)\}\}/g, (slot, name: string) =>
|
||||
Object.hasOwn(values, name) ? String(values[name]) : slot
|
||||
)
|
||||
super(error)
|
||||
this.failure = {
|
||||
ok: false,
|
||||
error,
|
||||
errorCode: code,
|
||||
...(Object.keys(params).length > 0 ? { errorParams: params } : {}),
|
||||
...(detail !== undefined ? { errorDetail: detail } : {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isPetImportFailure(value: unknown): value is PetImportFailure {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'ok' in value &&
|
||||
value.ok === false &&
|
||||
'error' in value &&
|
||||
typeof value.error === 'string'
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// One English table for repo icon upload refusals: main builds the wire `error`, the renderer translates by code.
|
||||
export const REPO_ICON_PICK_FAILURE_COPY = {
|
||||
notPng: 'Repo icons must be PNG files.',
|
||||
tooLarge: 'Repo icon image must be 256KB or smaller.'
|
||||
} as const
|
||||
|
||||
export type RepoIconPickFailureCode = keyof typeof REPO_ICON_PICK_FAILURE_COPY
|
||||
|
||||
/** Wire shape: `error` stays English; the renderer translates known codes. */
|
||||
export type RepoIconPickFailure = { ok: false; error: string; errorCode?: string }
|
||||
|
||||
export type RepoIconPickResult = { dataUrl: string; fileName: string } | RepoIconPickFailure | null
|
||||
|
||||
export function repoIconPickFailure(errorCode: RepoIconPickFailureCode): RepoIconPickFailure {
|
||||
return { ok: false, error: REPO_ICON_PICK_FAILURE_COPY[errorCode], errorCode }
|
||||
}
|
||||
Reference in New Issue
Block a user