多国语言

This commit is contained in:
Yeomai committed 2026-09-11 22:34:08 +08:00
1 parent 63cd0b3246
commit f0694431e3
30 files changed
+2343 -189

No files matched your search

@@ -0,0 +1,79 @@
{
"manifest": {
"app": {
"name": "SpineViewer"
}
},
"viewer": {
"title": "Spine-Vorschau",
"skin": "Skin",
"play": "▶ Abspielen",
"pause": "⏸ Pause",
"static": "▮ Statisch",
"staticLabel": "Statisch",
"playPauseTitle": "Abspielen / Pause (Leertaste)",
"loop": "Schleife",
"speed": "Geschwindigkeit",
"pma": "Prämultipliziert",
"pmaTitle": "Für Texturen, die mit Premultiplied Alpha (PMA) exportiert wurden",
"background": "Hintergrund",
"backgroundTitle": "50×50-Schachbrett, das mit der Leinwand verschoben und skaliert wird und an Blend-Modi teilnimmt",
"thumbnail": "🖼 Vorschaubild",
"thumbnailTitle": "Aus dem aktuellen Frame ein 400×400-Vorschaubild erzeugen",
"versionTitle": "Spine-Version",
"reloadTitle": "Ordner neu laden",
"animationList": "Animationsliste",
"animations": "Animationen",
"errorTitle": "Vorschau nicht möglich",
"durationPlaceholder": "Dauer --",
"keyframesLegend": "Blaue Marken: Keyframes",
"keyframesLegendTitle": "Eine blaue Marke zeigt eine Zeit, zu der mindestens eine Bone-, Slot- oder Constraint-Eigenschaft einen Keyframe hat",
"keyframesHint": "Blaue Marken kennzeichnen Keyframes; zum Anzeigen von Zeit und Frame den Zeiger darüber halten",
"timePlaceholder": "0.00 / 0.00 s · 0 / 0 Frames",
"staticPoseTitle": "Statische Pose · Frame 1",
"keyframeTitle": "Keyframe · {time} · Frame {frame}",
"timelineStatic": "Statisch · 1 Frame",
"timelineDuration": "Dauer {time} · {frames} Frames",
"readoutStatic": "Statisch · 1 / 1 Frames",
"readout": "{time} / {total} · {frame} / {totalFrames} Frames",
"rulerStatic": "0 s (statisch)",
"seconds": "{value} s",
"imagePreview": "Bildvorschau (keine passenden Spine-Ressourcen gefunden)",
"generating": "Eagle-Vorschaubild wird erzeugt und aktualisiert…",
"thumbUpdated": "Eagle-Vorschaubild aktualisiert · {format} {size}KB",
"thumbWrittenNoRefresh": "Vorschaubild geschrieben, automatische Aktualisierung fehlgeschlagen: {error}",
"recentered": "Leinwand wieder zentriert",
"pmaOn": "Prämultipliziertes Alpha an",
"pmaOff": "Prämultipliziertes Alpha aus",
"pmaRemembered": "{state} · für dieses Asset gespeichert",
"pmaSaveFailed": "Umgeschaltet, aber Speichern der Einstellung fehlgeschlagen: {error}",
"badgeFailed": "Spine-Badge konnte nicht geladen werden",
"runtimeFailed": "Spine-Laufzeit kann nicht geladen werden: {file}",
"compoundName": "Eagle hat die zusammengesetzte Dateiendung noch nicht synchronisiert; Bibliothek erneut öffnen",
"txtRegistered": "Dieses Element ist noch als TXT registriert. Vorschaubild in Eagle neu erzeugen und Bibliothek erneut öffnen",
"apiHttp": "Eagle-API meldete HTTP {status}",
"noSkeletonForThumb": "Kein Skelett geladen; Vorschaubild kann nicht erzeugt werden",
"noItemId": "Eagle-Item-ID lässt sich aus dem Pfad nicht ermitteln",
"thumbnailFailed": "Vorschaubild konnte nicht erzeugt werden: {error}",
"imageLoadFailed": "Bild konnte nicht geladen werden",
"settingsNotReady": "Asset-Einstellungen sind noch nicht bereit",
"noSkeletonFile": "Skelettdatei nicht gefunden",
"noSkeletonFileMsg": "Zur ausgewählten Datei wurde keine passende Spine-Skelettdatei (.skel / .skel.bytes / .json) gefunden.\n\nBitte den kompletten Spine-Export in Eagle importieren.",
"noAtlas": "Atlas nicht gefunden",
"noAtlasMsg": "Die passende .atlas- / .atlas.txt-Datei wurde nicht gefunden.",
"unsupportedVersion": "Nicht unterstützte Version",
"unsupportedVersionMsg": "Unbekannte Spine-Version: {version}\nUnterstützte Versionen: {list}",
"unknownVersion": "unbekannt",
"runtimeLoadFailed": "Laufzeit konnte nicht geladen werden",
"atlasLoadFailed": "Atlas konnte nicht geladen werden",
"emptySkeleton": "Leere Skelettdaten",
"emptySkeletonMsg": "Es konnten keine gültigen Skelettdaten gelesen werden.",
"missingFile": "Datei fehlt",
"missingFileMsg": "Es wurde kein Dateipfad-Parameter übergeben.",
"fileNotExist": "Datei existiert nicht",
"loadFailed": "Laden fehlgeschlagen",
"notSpineText": "Diese Datei ist keine Spine-Ressource im Format .atlas.txt oder .skel.bytes.",
"truncated256": "\n\n(nur die ersten 256 KB werden angezeigt)",
"truncated1m": "\n\n(große Datei; nur das erste 1 MB wird angezeigt)"
}
}
@@ -0,0 +1,79 @@
{
"manifest": {
"app": {
"name": "SpineViewer"
}
},
"viewer": {
"title": "Spine Preview",
"skin": "Skin",
"play": "▶ Play",
"pause": "⏸ Pause",
"static": "▮ Still",
"staticLabel": "Still",
"playPauseTitle": "Play / pause (Space)",
"loop": "Loop",
"speed": "Speed",
"pma": "Premultiplied",
"pmaTitle": "For textures exported with Premultiplied Alpha (PMA)",
"background": "Background",
"backgroundTitle": "50×50 checkerboard that pans and zooms with the canvas and participates in blend modes",
"thumbnail": "🖼 Set thumbnail",
"thumbnailTitle": "Generate a 400×400 thumbnail from the current frame",
"versionTitle": "Spine version",
"reloadTitle": "Reload folder",
"animationList": "Animation list",
"animations": "Animations",
"errorTitle": "Cannot preview",
"durationPlaceholder": "Duration --",
"keyframesLegend": "Blue ticks: keyframes",
"keyframesLegendTitle": "A blue tick marks a time where at least one bone, slot or constraint property has a keyframe",
"keyframesHint": "Blue ticks mark animation keyframes; hover a tick to see its time and frame",
"timePlaceholder": "0.00 / 0.00 s · 0 / 0 frames",
"staticPoseTitle": "Still pose · frame 1",
"keyframeTitle": "Keyframe · {time} · frame {frame}",
"timelineStatic": "Still · 1 frame",
"timelineDuration": "Duration {time} · {frames} frames",
"readoutStatic": "Still · 1 / 1 frames",
"readout": "{time} / {total} · {frame} / {totalFrames} frames",
"rulerStatic": "0 s (still)",
"seconds": "{value} s",
"imagePreview": "Image preview (no matching Spine assets found)",
"generating": "Generating and updating the Eagle thumbnail…",
"thumbUpdated": "Eagle thumbnail updated · {format} {size}KB",
"thumbWrittenNoRefresh": "Thumbnail written, but auto-refresh failed: {error}",
"recentered": "Canvas re-centered",
"pmaOn": "Premultiplied alpha on",
"pmaOff": "Premultiplied alpha off",
"pmaRemembered": "{state} · saved for this asset",
"pmaSaveFailed": "Toggled, but saving the setting failed: {error}",
"badgeFailed": "Failed to load the Spine badge",
"runtimeFailed": "Cannot load the Spine runtime: {file}",
"compoundName": "Eagle has not synced the compound extension yet; please reopen the library",
"txtRegistered": "This item is still registered as TXT. Regenerate the thumbnail in Eagle and reopen the library",
"apiHttp": "Eagle API returned HTTP {status}",
"noSkeletonForThumb": "No skeleton loaded; cannot generate a thumbnail",
"noItemId": "Cannot determine the Eagle item ID from the asset path",
"thumbnailFailed": "Failed to generate the thumbnail: {error}",
"imageLoadFailed": "Failed to load the image",
"settingsNotReady": "Asset settings are not ready",
"noSkeletonFile": "Skeleton file not found",
"noSkeletonFileMsg": "Could not locate the Spine skeleton file (.skel / .skel.bytes / .json) that matches the selected file.\n\nPlease import the whole Spine export into Eagle.",
"noAtlas": "Atlas not found",
"noAtlasMsg": "Could not locate the matching .atlas / .atlas.txt file.",
"unsupportedVersion": "Unsupported version",
"unsupportedVersionMsg": "Unrecognized Spine version: {version}\nSupported versions: {list}",
"unknownVersion": "unknown",
"runtimeLoadFailed": "Failed to load the runtime",
"atlasLoadFailed": "Failed to load the atlas",
"emptySkeleton": "Empty skeleton data",
"emptySkeletonMsg": "No valid skeleton data could be read.",
"missingFile": "Missing file",
"missingFileMsg": "No file path parameter was provided.",
"fileNotExist": "File not found",
"loadFailed": "Failed to load",
"notSpineText": "This file is not a .atlas.txt or .skel.bytes Spine asset.",
"truncated256": "\n\n(only the first 256 KB is shown)",
"truncated1m": "\n\n(large file, only the first 1 MB is shown)"
}
}
@@ -0,0 +1,79 @@
{
"manifest": {
"app": {
"name": "SpineViewer"
}
},
"viewer": {
"title": "Vista previa de Spine",
"skin": "Piel",
"play": "▶ Reproducir",
"pause": "⏸ Pausa",
"static": "▮ Estático",
"staticLabel": "Estático",
"playPauseTitle": "Reproducir / pausar (espacio)",
"loop": "Bucle",
"speed": "Velocidad",
"pma": "Premultiplicado",
"pmaTitle": "Para texturas exportadas con Premultiplied Alpha (PMA)",
"background": "Fondo",
"backgroundTitle": "Fondo de cuadros de 50×50 que se desplaza y escala con el lienzo y participa en los modos de fusión",
"thumbnail": "🖼 Crear miniatura",
"thumbnailTitle": "Crear una miniatura de 400×400 con el fotograma actual",
"versionTitle": "Versión de Spine",
"reloadTitle": "Recargar carpeta",
"animationList": "Lista de animaciones",
"animations": "Animaciones",
"errorTitle": "No se puede previsualizar",
"durationPlaceholder": "Duración --",
"keyframesLegend": "Marcas azules: fotogramas clave",
"keyframesLegendTitle": "Una marca azul señala un instante en el que al menos una propiedad de hueso, ranura o restricción tiene un fotograma clave",
"keyframesHint": "Las marcas azules indican fotogramas clave; pasa el cursor para ver el tiempo y el fotograma",
"timePlaceholder": "0.00 / 0.00 s · 0 / 0 fotogramas",
"staticPoseTitle": "Pose estática · fotograma 1",
"keyframeTitle": "Fotograma clave · {time} · fotograma {frame}",
"timelineStatic": "Estático · 1 fotograma",
"timelineDuration": "Duración {time} · {frames} fotogramas",
"readoutStatic": "Estático · 1 / 1 fotogramas",
"readout": "{time} / {total} · {frame} / {totalFrames} fotogramas",
"rulerStatic": "0 s (estático)",
"seconds": "{value} s",
"imagePreview": "Vista previa de imagen (no se encontraron recursos Spine correspondientes)",
"generating": "Generando y actualizando la miniatura de Eagle…",
"thumbUpdated": "Miniatura de Eagle actualizada · {format} {size}KB",
"thumbWrittenNoRefresh": "Miniatura escrita, pero falló la actualización automática: {error}",
"recentered": "Lienzo centrado de nuevo",
"pmaOn": "Alfa premultiplicado activado",
"pmaOff": "Alfa premultiplicado desactivado",
"pmaRemembered": "{state} · guardado para este recurso",
"pmaSaveFailed": "Se cambió, pero no se pudo guardar la configuración: {error}",
"badgeFailed": "No se pudo cargar el distintivo de Spine",
"runtimeFailed": "No se puede cargar el entorno de ejecución de Spine: {file}",
"compoundName": "Eagle aún no ha sincronizado la extensión compuesta; vuelve a abrir la biblioteca",
"txtRegistered": "Este elemento sigue registrado como TXT. Vuelve a generar la miniatura en Eagle y reabre la biblioteca",
"apiHttp": "La API de Eagle devolvió HTTP {status}",
"noSkeletonForThumb": "No hay esqueleto cargado; no se puede generar la miniatura",
"noItemId": "No se puede determinar el ID del elemento de Eagle a partir de la ruta",
"thumbnailFailed": "No se pudo generar la miniatura: {error}",
"imageLoadFailed": "No se pudo cargar la imagen",
"settingsNotReady": "La configuración del recurso aún no está lista",
"noSkeletonFile": "No se encontró el archivo de esqueleto",
"noSkeletonFileMsg": "No se encontró el archivo de esqueleto de Spine (.skel / .skel.bytes / .json) correspondiente al archivo seleccionado.\n\nImporta el conjunto completo de exportación de Spine en Eagle.",
"noAtlas": "No se encontró el Atlas",
"noAtlasMsg": "No se encontró el archivo .atlas / .atlas.txt correspondiente.",
"unsupportedVersion": "Versión no compatible",
"unsupportedVersionMsg": "Versión de Spine no reconocida: {version}\nVersiones compatibles: {list}",
"unknownVersion": "desconocida",
"runtimeLoadFailed": "Error al cargar el entorno de ejecución",
"atlasLoadFailed": "Error al cargar el atlas",
"emptySkeleton": "Datos de esqueleto vacíos",
"emptySkeletonMsg": "No se pudieron leer datos de esqueleto válidos.",
"missingFile": "Falta el archivo",
"missingFileMsg": "No se proporcionó el parámetro de ruta del archivo.",
"fileNotExist": "El archivo no existe",
"loadFailed": "Error al cargar",
"notSpineText": "Este archivo no es un recurso Spine en formato .atlas.txt o .skel.bytes.",
"truncated256": "\n\n(solo se muestran los primeros 256 KB)",
"truncated1m": "\n\n(archivo grande; solo se muestra el primer 1 MB)"
}
}
@@ -0,0 +1,79 @@
{
"manifest": {
"app": {
"name": "SpineViewer"
}
},
"viewer": {
"title": "Spine プレビュー",
"skin": "スキン",
"play": "▶ 再生",
"pause": "⏸ 一時停止",
"static": "▮ 静止",
"staticLabel": "静止",
"playPauseTitle": "再生 / 一時停止(スペース)",
"loop": "ループ",
"speed": "速度",
"pma": "乗算済み",
"pmaTitle": "Premultiplied Alpha(PMA)で書き出したテクスチャ用",
"background": "背景",
"backgroundTitle": "50×50 のチェッカー背景。キャンバスの移動・拡大に追従し、ブレンドモードにも参加します",
"thumbnail": "🖼 サムネイル生成",
"thumbnailTitle": "現在のフレームから 400×400 のサムネイルを生成",
"versionTitle": "Spine バージョン",
"reloadTitle": "フォルダーを再読み込み",
"animationList": "アニメーション一覧",
"animations": "アニメーション",
"errorTitle": "プレビューできません",
"durationPlaceholder": "長さ --",
"keyframesLegend": "青い線:キーフレーム",
"keyframesLegendTitle": "青い線は、いずれかのボーン・スロット・コンストレイントのプロパティにキーフレームがある位置を示します",
"keyframesHint": "青い線はアニメーションのキーフレームです。線にカーソルを合わせると時間とフレームが表示されます",
"timePlaceholder": "0.00 / 0.00 秒 · 0 / 0 フレーム",
"staticPoseTitle": "静止ポーズ · 1 フレーム目",
"keyframeTitle": "キーフレーム · {time} · {frame} フレーム目",
"timelineStatic": "静止 · 1 フレーム",
"timelineDuration": "長さ {time} · {frames} フレーム",
"readoutStatic": "静止 · 1 / 1 フレーム",
"readout": "{time} / {total} · {frame} / {totalFrames} フレーム",
"rulerStatic": "0 秒(静止)",
"seconds": "{value} 秒",
"imagePreview": "画像プレビュー(対応する Spine リソースが見つかりません)",
"generating": "Eagle のサムネイルを生成・更新しています…",
"thumbUpdated": "Eagle のサムネイルを更新しました · {format} {size}KB",
"thumbWrittenNoRefresh": "サムネイルを書き込みましたが、自動更新に失敗しました:{error}",
"recentered": "キャンバスを中央に戻しました",
"pmaOn": "乗算済みアルファ:オン",
"pmaOff": "乗算済みアルファ:オフ",
"pmaRemembered": "{state} · この素材の設定として保存しました",
"pmaSaveFailed": "切り替えましたが、設定の保存に失敗しました:{error}",
"badgeFailed": "Spine バッジの読み込みに失敗しました",
"runtimeFailed": "Spine ランタイムを読み込めません:{file}",
"compoundName": "Eagle が複合拡張子を同期していません。ライブラリを開き直してください",
"txtRegistered": "この項目はまだ TXT として登録されています。Eagle でサムネイルを再生成し、ライブラリを開き直してください",
"apiHttp": "Eagle API が HTTP {status} を返しました",
"noSkeletonForThumb": "骨格が読み込まれていないため、サムネイルを生成できません",
"noItemId": "素材パスから Eagle の項目 ID を特定できません",
"thumbnailFailed": "サムネイルの生成に失敗しました:{error}",
"imageLoadFailed": "画像の読み込みに失敗しました",
"settingsNotReady": "素材の設定がまだ準備できていません",
"noSkeletonFile": "骨格ファイルが見つかりません",
"noSkeletonFileMsg": "選択したファイルに対応する Spine 骨格ファイル(.skel / .skel.bytes / .json)が見つかりませんでした。\n\n同じ Spine 書き出し一式を Eagle に読み込んでください。",
"noAtlas": "Atlas が見つかりません",
"noAtlasMsg": "対応する .atlas / .atlas.txt ファイルが見つかりませんでした。",
"unsupportedVersion": "未対応のバージョン",
"unsupportedVersionMsg": "認識できない Spine バージョン:{version}\n対応バージョン:{list}",
"unknownVersion": "不明",
"runtimeLoadFailed": "ランタイムの読み込みに失敗しました",
"atlasLoadFailed": "Atlas の読み込みに失敗しました",
"emptySkeleton": "骨格データが空です",
"emptySkeletonMsg": "有効な骨格データを読み取れませんでした。",
"missingFile": "ファイルがありません",
"missingFileMsg": "ファイルパスのパラメーターが指定されていません。",
"fileNotExist": "ファイルが存在しません",
"loadFailed": "読み込みに失敗しました",
"notSpineText": "このファイルは .atlas.txt または .skel.bytes 形式の Spine リソースではありません。",
"truncated256": "\n\n(先頭 256 KB のみ表示)",
"truncated1m": "\n\n(ファイルが大きいため、先頭 1 MB のみ表示)"
}
}
@@ -0,0 +1,79 @@
{
"manifest": {
"app": {
"name": "SpineViewer"
}
},
"viewer": {
"title": "Spine 미리보기",
"skin": "스킨",
"play": "▶ 재생",
"pause": "⏸ 일시정지",
"static": "▮ 정지",
"staticLabel": "정지",
"playPauseTitle": "재생 / 일시정지(스페이스)",
"loop": "반복",
"speed": "속도",
"pma": "미리 곱함",
"pmaTitle": "Premultiplied Alpha(PMA)로 내보낸 텍스처용",
"background": "배경",
"backgroundTitle": "50×50 체커 배경. 캔버스 이동·확대를 따라가며 블렌드 모드에도 참여합니다",
"thumbnail": "🖼 썸네일 생성",
"thumbnailTitle": "현재 프레임으로 400×400 썸네일 생성",
"versionTitle": "Spine 버전",
"reloadTitle": "폴더 다시 불러오기",
"animationList": "애니메이션 목록",
"animations": "애니메이션",
"errorTitle": "미리볼 수 없음",
"durationPlaceholder": "길이 --",
"keyframesLegend": "파란 선: 키프레임",
"keyframesLegendTitle": "파란 선은 하나 이상의 본·슬롯·컨스트레인트 속성에 키프레임이 있는 시점을 나타냅니다",
"keyframesHint": "파란 선은 애니메이션 키프레임입니다. 선에 마우스를 올리면 시간과 프레임이 표시됩니다",
"timePlaceholder": "0.00 / 0.00 초 · 0 / 0 프레임",
"staticPoseTitle": "정지 자세 · 1 프레임",
"keyframeTitle": "키프레임 · {time} · {frame} 프레임",
"timelineStatic": "정지 · 1 프레임",
"timelineDuration": "길이 {time} · {frames} 프레임",
"readoutStatic": "정지 · 1 / 1 프레임",
"readout": "{time} / {total} · {frame} / {totalFrames} 프레임",
"rulerStatic": "0초 (정지)",
"seconds": "{value}초",
"imagePreview": "이미지 미리보기 (함께 있는 Spine 리소스를 찾지 못했습니다)",
"generating": "Eagle 썸네일을 생성하고 갱신하는 중…",
"thumbUpdated": "Eagle 썸네일을 갱신했습니다 · {format} {size}KB",
"thumbWrittenNoRefresh": "썸네일을 저장했지만 자동 새로고침에 실패했습니다: {error}",
"recentered": "캔버스를 가운데로 되돌렸습니다",
"pmaOn": "미리 곱한 알파 켜짐",
"pmaOff": "미리 곱한 알파 꺼짐",
"pmaRemembered": "{state} · 이 에셋의 설정으로 저장했습니다",
"pmaSaveFailed": "전환했지만 설정 저장에 실패했습니다: {error}",
"badgeFailed": "Spine 배지를 불러오지 못했습니다",
"runtimeFailed": "Spine 런타임을 불러올 수 없습니다: {file}",
"compoundName": "Eagle이 복합 확장자를 아직 동기화하지 않았습니다. 라이브러리를 다시 열어 주세요",
"txtRegistered": "이 항목은 아직 TXT로 등록되어 있습니다. Eagle에서 썸네일을 다시 생성하고 라이브러리를 다시 열어 주세요",
"apiHttp": "Eagle API가 HTTP {status}를 반환했습니다",
"noSkeletonForThumb": "스켈레톤이 로드되지 않아 썸네일을 생성할 수 없습니다",
"noItemId": "에셋 경로에서 Eagle 항목 ID를 찾을 수 없습니다",
"thumbnailFailed": "썸네일 생성 실패: {error}",
"imageLoadFailed": "이미지를 불러오지 못했습니다",
"settingsNotReady": "에셋 설정이 아직 준비되지 않았습니다",
"noSkeletonFile": "스켈레톤 파일을 찾을 수 없음",
"noSkeletonFileMsg": "선택한 파일과 짝을 이루는 Spine 스켈레톤 파일(.skel / .skel.bytes / .json)을 찾지 못했습니다.\n\n같은 Spine 내보내기 세트를 Eagle에 함께 가져와 주세요.",
"noAtlas": "Atlas를 찾을 수 없음",
"noAtlasMsg": "짝을 이루는 .atlas / .atlas.txt 파일을 찾지 못했습니다.",
"unsupportedVersion": "지원하지 않는 버전",
"unsupportedVersionMsg": "인식할 수 없는 Spine 버전: {version}\n지원 버전: {list}",
"unknownVersion": "알 수 없음",
"runtimeLoadFailed": "런타임 로드 실패",
"atlasLoadFailed": "Atlas 로드 실패",
"emptySkeleton": "스켈레톤 데이터가 비어 있음",
"emptySkeletonMsg": "유효한 스켈레톤 데이터를 읽지 못했습니다.",
"missingFile": "파일 없음",
"missingFileMsg": "파일 경로 매개변수가 전달되지 않았습니다.",
"fileNotExist": "파일이 존재하지 않음",
"loadFailed": "불러오기 실패",
"notSpineText": "이 파일은 .atlas.txt 또는 .skel.bytes 형식의 Spine 리소스가 아닙니다.",
"truncated256": "\n\n(앞부분 256 KB만 표시)",
"truncated1m": "\n\n(파일이 커서 앞부분 1 MB만 표시)"
}
}
@@ -0,0 +1,79 @@
{
"manifest": {
"app": {
"name": "SpineViewer"
}
},
"viewer": {
"title": "Просмотр Spine",
"skin": "Скин",
"play": "▶ Воспроизвести",
"pause": "⏸ Пауза",
"static": "▮ Статично",
"staticLabel": "Статично",
"playPauseTitle": "Воспроизведение / пауза (пробел)",
"loop": "Повтор",
"speed": "Скорость",
"pma": "Премультипликация",
"pmaTitle": "Для текстур, экспортированных с Premultiplied Alpha (PMA)",
"background": "Фон",
"backgroundTitle": "Шахматный фон 50×50: следует за сдвигом и масштабом холста и участвует в режимах смешивания",
"thumbnail": "🖼 Создать миниатюру",
"thumbnailTitle": "Создать миниатюру 400×400 из текущего кадра",
"versionTitle": "Версия Spine",
"reloadTitle": "Перезагрузить папку",
"animationList": "Список анимаций",
"animations": "Анимации",
"errorTitle": "Просмотр невозможен",
"durationPlaceholder": "Длительность --",
"keyframesLegend": "Синие метки: ключевые кадры",
"keyframesLegendTitle": "Синяя метка отмечает момент, в котором есть ключевой кадр хотя бы у одного свойства кости, слота или ограничения",
"keyframesHint": "Синие метки обозначают ключевые кадры; наведите курсор, чтобы увидеть время и кадр",
"timePlaceholder": "0.00 / 0.00 с · 0 / 0 кадров",
"staticPoseTitle": "Статичная поза · кадр 1",
"keyframeTitle": "Ключевой кадр · {time} · кадр {frame}",
"timelineStatic": "Статично · 1 кадр",
"timelineDuration": "Длительность {time} · {frames} кадров",
"readoutStatic": "Статично · 1 / 1 кадров",
"readout": "{time} / {total} · {frame} / {totalFrames} кадров",
"rulerStatic": "0 с (статично)",
"seconds": "{value} с",
"imagePreview": "Просмотр изображения (подходящие ресурсы Spine не найдены)",
"generating": "Создание и обновление миниатюры Eagle…",
"thumbUpdated": "Миниатюра Eagle обновлена · {format} {size}KB",
"thumbWrittenNoRefresh": "Миниатюра записана, но автообновление не удалось: {error}",
"recentered": "Холст возвращён в центр",
"pmaOn": "Премультипликация включена",
"pmaOff": "Премультипликация выключена",
"pmaRemembered": "{state} · сохранено для этого ресурса",
"pmaSaveFailed": "Переключено, но сохранить настройку не удалось: {error}",
"badgeFailed": "Не удалось загрузить значок Spine",
"runtimeFailed": "Не удалось загрузить среду выполнения Spine: {file}",
"compoundName": "Eagle ещё не синхронизировал составное расширение. Откройте библиотеку заново",
"txtRegistered": "Этот элемент всё ещё зарегистрирован как TXT. Пересоздайте миниатюру в Eagle и откройте библиотеку заново",
"apiHttp": "Eagle API вернул HTTP {status}",
"noSkeletonForThumb": "Скелет не загружен — невозможно создать миниатюру",
"noItemId": "Не удалось определить ID элемента Eagle по пути к ресурсу",
"thumbnailFailed": "Не удалось создать миниатюру: {error}",
"imageLoadFailed": "Не удалось загрузить изображение",
"settingsNotReady": "Настройки ресурса ещё не готовы",
"noSkeletonFile": "Файл скелета не найден",
"noSkeletonFileMsg": "Не удалось найти файл скелета Spine (.skel / .skel.bytes / .json), соответствующий выбранному файлу.\n\nИмпортируйте весь набор экспорта Spine в Eagle.",
"noAtlas": "Atlas не найден",
"noAtlasMsg": "Не удалось найти соответствующий файл .atlas / .atlas.txt.",
"unsupportedVersion": "Неподдерживаемая версия",
"unsupportedVersionMsg": "Неизвестная версия Spine: {version}\nПоддерживаемые версии: {list}",
"unknownVersion": "неизвестно",
"runtimeLoadFailed": "Не удалось загрузить среду выполнения",
"atlasLoadFailed": "Не удалось загрузить atlas",
"emptySkeleton": "Пустые данные скелета",
"emptySkeletonMsg": "Не удалось прочитать корректные данные скелета.",
"missingFile": "Файл отсутствует",
"missingFileMsg": "Параметр пути к файлу не передан.",
"fileNotExist": "Файл не существует",
"loadFailed": "Не удалось загрузить",
"notSpineText": "Этот файл не является ресурсом Spine в формате .atlas.txt или .skel.bytes.",
"truncated256": "\n\n(показаны только первые 256 КБ)",
"truncated1m": "\n\n(файл большой, показан только первый 1 МБ)"
}
}
@@ -0,0 +1,79 @@
{
"manifest": {
"app": {
"name": "SpineViewer"
}
},
"viewer": {
"title": "Spine 预览",
"skin": "皮肤",
"play": "▶ 播放",
"pause": "⏸ 暂停",
"static": "▮ 静帧",
"staticLabel": "静帧",
"playPauseTitle": "播放/暂停 (空格)",
"loop": "循环",
"speed": "速度",
"pma": "预乘",
"pmaTitle": "用于以 Premultiplied Alpha(PMA)方式导出的贴图",
"background": "背景",
"backgroundTitle": "50×50 方格背景,随画布移动和缩放,并参与混合模式",
"thumbnail": "🖼 生成缩略图",
"thumbnailTitle": "使用当前帧生成 400×400 缩略图",
"versionTitle": "Spine 版本",
"reloadTitle": "重新加载目录",
"animationList": "动画列表",
"animations": "动画",
"errorTitle": "无法预览",
"durationPlaceholder": "时长 --",
"keyframesLegend": "蓝色短线:关键帧",
"keyframesLegendTitle": "蓝色短线表示动画中至少一个骨骼、插槽或约束属性在该时刻有关键帧",
"keyframesHint": "蓝色短线表示动画关键帧;悬停短线可查看时间和帧号",
"timePlaceholder": "0.00 / 0.00 秒 · 0 / 0 帧",
"staticPoseTitle": "静帧姿势 · 第 1 帧",
"keyframeTitle": "关键帧 · {time} · 第 {frame} 帧",
"timelineStatic": "静帧 · 1 帧",
"timelineDuration": "时长 {time} · {frames} 帧",
"readoutStatic": "静帧 · 1 / 1 帧",
"readout": "{time} / {total} · {frame} / {totalFrames} 帧",
"rulerStatic": "0 秒(静帧)",
"seconds": "{value} 秒",
"imagePreview": "图片预览(未检测到配套 Spine 资源)",
"generating": "正在生成并更新 Eagle 缩略图…",
"thumbUpdated": "Eagle 缩略图已更新 · {format} {size}KB",
"thumbWrittenNoRefresh": "缩略图已写入;自动刷新失败:{error}",
"recentered": "已回到画布中心",
"pmaOn": "预乘已开启",
"pmaOff": "预乘已关闭",
"pmaRemembered": "{state} · 已记住此素材的设置",
"pmaSaveFailed": "预乘已切换,但设置保存失败:{error}",
"badgeFailed": "Spine badge 加载失败",
"runtimeFailed": "无法加载 Spine 运行时:{file}",
"compoundName": "Eagle 尚未同步复合后缀名称,请重新打开素材库",
"txtRegistered": "此条目仍按 TXT 登记,请在 Eagle 重新生成缩略图并重新打开素材库",
"apiHttp": "Eagle API 返回 HTTP {status}",
"noSkeletonForThumb": "尚未加载骨骼,无法生成缩略图",
"noItemId": "无法从素材路径识别 Eagle 项目 ID",
"thumbnailFailed": "生成缩略图失败: {error}",
"imageLoadFailed": "图片加载失败",
"settingsNotReady": "素材设置尚未就绪",
"noSkeletonFile": "未找到骨骼文件",
"noSkeletonFileMsg": "未能定位到与所选文件配套的 Spine 骨骼文件(.skel / .skel.bytes / .json)。\n\n请将同一套 Spine 导出文件导入 Eagle。",
"noAtlas": "未找到 Atlas",
"noAtlasMsg": "未能定位到配套的 .atlas / .atlas.txt 文件。",
"unsupportedVersion": "版本不支持",
"unsupportedVersionMsg": "无法识别的 Spine 版本:{version}\n支持的版本:{list}",
"unknownVersion": "未知",
"runtimeLoadFailed": "运行时加载失败",
"atlasLoadFailed": "Atlas 加载失败",
"emptySkeleton": "骨骼数据为空",
"emptySkeletonMsg": "未能读取到有效的骨骼数据。",
"missingFile": "缺少文件",
"missingFileMsg": "未传入文件路径参数。",
"fileNotExist": "文件不存在",
"loadFailed": "加载失败",
"notSpineText": "此文件不是 .atlas.txt 或 .skel.bytes 格式的 Spine 资源。",
"truncated256": "\n\n(仅显示前 256 KB)",
"truncated1m": "\n\n(文件较大,仅显示前 1 MB)"
}
}
@@ -0,0 +1,79 @@
{
"manifest": {
"app": {
"name": "SpineViewer"
}
},
"viewer": {
"title": "Spine 預覽",
"skin": "外觀",
"play": "▶ 播放",
"pause": "⏸ 暫停",
"static": "▮ 靜態",
"staticLabel": "靜態",
"playPauseTitle": "播放/暫停(空白鍵)",
"loop": "循環",
"speed": "速度",
"pma": "預乘",
"pmaTitle": "用於以 Premultiplied Alpha(PMA)方式匯出的貼圖",
"background": "背景",
"backgroundTitle": "50×50 方格背景,隨畫布移動和縮放,並參與混合模式",
"thumbnail": "🖼 產生縮圖",
"thumbnailTitle": "使用目前影格產生 400×400 縮圖",
"versionTitle": "Spine 版本",
"reloadTitle": "重新載入目錄",
"animationList": "動畫清單",
"animations": "動畫",
"errorTitle": "無法預覽",
"durationPlaceholder": "時長 --",
"keyframesLegend": "藍色短線:關鍵影格",
"keyframesLegendTitle": "藍色短線表示動畫中至少一個骨骼、插槽或約束屬性在該時刻有關鍵影格",
"keyframesHint": "藍色短線表示動畫關鍵影格;將游標停在短線上可查看時間與影格",
"timePlaceholder": "0.00 / 0.00 秒 · 0 / 0 影格",
"staticPoseTitle": "靜態姿勢 · 第 1 影格",
"keyframeTitle": "關鍵影格 · {time} · 第 {frame} 影格",
"timelineStatic": "靜態 · 1 影格",
"timelineDuration": "時長 {time} · {frames} 影格",
"readoutStatic": "靜態 · 1 / 1 影格",
"readout": "{time} / {total} · {frame} / {totalFrames} 影格",
"rulerStatic": "0 秒(靜態)",
"seconds": "{value} 秒",
"imagePreview": "圖片預覽(未偵測到配套的 Spine 資源)",
"generating": "正在產生並更新 Eagle 縮圖…",
"thumbUpdated": "Eagle 縮圖已更新 · {format} {size}KB",
"thumbWrittenNoRefresh": "縮圖已寫入;自動重新整理失敗:{error}",
"recentered": "已回到畫布中心",
"pmaOn": "預乘已開啟",
"pmaOff": "預乘已關閉",
"pmaRemembered": "{state} · 已記住此素材的設定",
"pmaSaveFailed": "預乘已切換,但設定儲存失敗:{error}",
"badgeFailed": "Spine badge 載入失敗",
"runtimeFailed": "無法載入 Spine 執行環境:{file}",
"compoundName": "Eagle 尚未同步複合副檔名,請重新開啟素材庫",
"txtRegistered": "此項目仍以 TXT 登錄,請在 Eagle 重新產生縮圖並重新開啟素材庫",
"apiHttp": "Eagle API 回傳 HTTP {status}",
"noSkeletonForThumb": "尚未載入骨骼,無法產生縮圖",
"noItemId": "無法從素材路徑辨識 Eagle 項目 ID",
"thumbnailFailed": "產生縮圖失敗:{error}",
"imageLoadFailed": "圖片載入失敗",
"settingsNotReady": "素材設定尚未就緒",
"noSkeletonFile": "找不到骨骼檔案",
"noSkeletonFileMsg": "找不到與所選檔案搭配的 Spine 骨骼檔案(.skel / .skel.bytes / .json)。\n\n請將同一套 Spine 匯出檔案匯入 Eagle。",
"noAtlas": "找不到 Atlas",
"noAtlasMsg": "找不到搭配的 .atlas / .atlas.txt 檔案。",
"unsupportedVersion": "不支援的版本",
"unsupportedVersionMsg": "無法辨識的 Spine 版本:{version}\n支援的版本:{list}",
"unknownVersion": "未知",
"runtimeLoadFailed": "執行環境載入失敗",
"atlasLoadFailed": "Atlas 載入失敗",
"emptySkeleton": "骨骼資料為空",
"emptySkeletonMsg": "無法讀取到有效的骨骼資料。",
"missingFile": "缺少檔案",
"missingFileMsg": "未傳入檔案路徑參數。",
"fileNotExist": "檔案不存在",
"loadFailed": "載入失敗",
"notSpineText": "此檔案不是 .atlas.txt 或 .skel.bytes 格式的 Spine 資源。",
"truncated256": "\n\n(僅顯示前 256 KB)",
"truncated1m": "\n\n(檔案較大,僅顯示前 1 MB)"
}
}
+12 -1
View File
@@ -3,7 +3,7 @@
"version": "1.1.0",
"platform": "all",
"arch": "all",
"name": "SpineViewer",
"name": "{{manifest.app.name}}",
"logo": "/logo-spineviewer.png",
"keywords": [
"spine",
@@ -12,6 +12,17 @@
"skel",
"atlas"
],
"fallbackLanguage": "zh_CN",
"languages": [
"en",
"zh_CN",
"zh_TW",
"ja_JP",
"ko_KR",
"ru_RU",
"es_ES",
"de_DE"
],
"devTools": false,
"preview": {
"skel,skel.bytes,bytes,json,atlas,atlas.txt,txt": {
+137 -62
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spine 预览</title>
<title data-i18n="viewer.title">Spine 预览</title>
<style>
:root {
--bg: #1e1e1e;
@@ -197,30 +197,30 @@
<body>
<div id="app">
<div id="toolbar">
<label>皮肤
<label><span data-i18n="viewer.skin">皮肤</span>
<select id="skinSelect" disabled></select>
</label>
<button id="playBtn" title="播放/暂停 (空格)">⏸ 暂停</button>
<label><input type="checkbox" id="loopCheck" checked> 循环</label>
<label>速度
<button id="playBtn" data-i18n-title="viewer.playPauseTitle" title="播放/暂停 (空格)">⏸ 暂停</button>
<label><input type="checkbox" id="loopCheck" checked> <span data-i18n="viewer.loop">循环</span></label>
<label><span data-i18n="viewer.speed">速度</span>
<input type="range" id="speedRange" min="0" max="3" step="0.05" value="1">
<span id="speedVal">1.0x</span>
</label>
<label title="用于以 Premultiplied Alpha(PMA)方式导出的贴图">
<input type="checkbox" id="pmaCheck" disabled> 预乘
<label data-i18n-title="viewer.pmaTitle" title="用于以 Premultiplied Alpha(PMA)方式导出的贴图">
<input type="checkbox" id="pmaCheck" disabled> <span data-i18n="viewer.pma">预乘</span>
</label>
<label title="50×50 方格背景,随画布移动和缩放,并参与混合模式">
<input type="checkbox" id="backgroundCheck"> 背景
<label data-i18n-title="viewer.backgroundTitle" title="50×50 方格背景,随画布移动和缩放,并参与混合模式">
<input type="checkbox" id="backgroundCheck"> <span data-i18n="viewer.background">背景</span>
</label>
<div class="spacer"></div>
<button id="thumbBtn" title="使用当前帧生成 400×400 缩略图">🖼 生成缩略图</button>
<label id="verLabel" style="color: var(--text-dim);" title="Spine 版本"></label>
<button id="reloadBtn" title="重新加载目录">↻</button>
<button id="thumbBtn" data-i18n="viewer.thumbnail" data-i18n-title="viewer.thumbnailTitle" title="使用当前帧生成 400×400 缩略图">🖼 生成缩略图</button>
<label id="verLabel" style="color: var(--text-dim);" data-i18n-title="viewer.versionTitle" title="Spine 版本"></label>
<button id="reloadBtn" title="重新加载目录" data-i18n-title="viewer.reloadTitle">↻</button>
</div>
<div id="stage">
<aside id="animationPanel" aria-label="动画列表">
<div id="animationPanelTitle">动画</div>
<div id="animationList" role="listbox" aria-label="动画"></div>
<aside id="animationPanel" data-i18n-aria-label="viewer.animationList" aria-label="动画列表">
<div id="animationPanelTitle" data-i18n="viewer.animations">动画</div>
<div id="animationList" role="listbox" data-i18n-aria-label="viewer.animations" aria-label="动画"></div>
</aside>
<div id="viewport">
<canvas id="canvas"></canvas>
@@ -228,7 +228,7 @@
<div id="loading"><div class="spinner"></div></div>
<div id="overlay" class="hidden">
<div class="box">
<div class="title" id="errorTitle">无法预览</div>
<div class="title" id="errorTitle" data-i18n="viewer.errorTitle">无法预览</div>
<div class="msg" id="errorMsg"></div>
</div>
</div>
@@ -236,15 +236,15 @@
</div>
<div id="timelineBar">
<div id="timelineLabels">
<span class="tl-label" id="durationLabel">时长 --</span>
<span id="keyframeLegend" title="蓝色短线表示动画中至少一个骨骼、插槽或约束属性在该时刻有关键帧">蓝色短线:关键帧</span>
<span class="tl-label" id="durationLabel" data-i18n="viewer.durationPlaceholder">时长 --</span>
<span id="keyframeLegend" data-i18n="viewer.keyframesLegend" data-i18n-title="viewer.keyframesLegendTitle" title="蓝色短线表示动画中至少一个骨骼、插槽或约束属性在该时刻有关键帧">蓝色短线:关键帧</span>
</div>
<div class="tl-wrap">
<div id="timeRuler"></div>
<input type="range" id="timeline" min="0" max="1" step="0.001" value="0" disabled>
<div id="keyframes"></div>
</div>
<span id="timeLabel">0.00 / 0.00 秒 · 0 / 0 帧</span>
<span id="timeLabel" data-i18n="viewer.timePlaceholder">0.00 / 0.00 秒 · 0 / 0 帧</span>
</div>
</div>
@@ -263,7 +263,7 @@
try {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = () => reject(new Error('Spine badge 加载失败'));
image.onerror = () => reject(new Error(t('viewer.badgeFailed')));
image.src = spineBadge.dataUrl();
} catch (e) {
reject(e);
@@ -300,6 +300,73 @@
const lang = urlParams.get('lang');
if (theme === 'light') document.documentElement.setAttribute('data-theme', 'light');
// ---------- 多国语系 ----------
// 文案与 manifest.json 的 languages / fallbackLanguage 共用同一份 _locales/<语言>.json。
// 格式扩展插件的查看器页不会注入 Eagle Plugin API,因此这里直接读取语言文件。
const I18N_LANGUAGES = ['en', 'zh_CN', 'zh_TW', 'ja_JP', 'ko_KR', 'ru_RU', 'es_ES', 'de_DE'];
const I18N_FALLBACK = 'zh_CN';
function normalizeLocale(raw) {
if (!raw) return null;
const tag = String(raw).trim().toLowerCase().replace(/-/g, '_');
if (tag === 'zh_tw' || tag === 'zh_hk' || tag === 'zh_hant' || tag === 'zh_mo') return 'zh_TW';
for (const code of I18N_LANGUAGES) {
if (code.toLowerCase() === tag) return code;
}
const base = tag.split('_')[0];
if (base === 'zh') return 'zh_CN';
for (const code of I18N_LANGUAGES) {
if (code.toLowerCase().split('_')[0] === base) return code;
}
return null;
}
function readLocaleFile(code) {
try {
const file = path.join(__dirname, '..', '_locales', code + '.json');
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
return (data && typeof data === 'object') ? data : {};
} catch (e) {
return {};
}
}
// 按 "viewer.skin" 这样的点号路径取字符串,与官方 i18next 的用法一致
function lookupMessage(table, key) {
let node = table;
for (const part of String(key).split('.')) {
if (!node || typeof node !== 'object') return undefined;
node = node[part];
}
return typeof node === 'string' ? node : undefined;
}
const fallbackMessages = readLocaleFile(I18N_FALLBACK);
const locale = normalizeLocale(lang) || I18N_FALLBACK;
const messages = locale === I18N_FALLBACK ? fallbackMessages : readLocaleFile(locale);
document.documentElement.lang = locale.replace('_', '-');
log('界面语言: ' + locale + ' (lang=' + (lang || '未提供') + ')');
// t('viewer.skin', { name: '...' }):缺失的键回退到 fallbackLanguage
function t(key, vars) {
let text = lookupMessage(messages, key);
if (text === undefined) text = lookupMessage(fallbackMessages, key);
if (text === undefined) return key;
if (vars) {
text = text.replace(/\{(\w+)\}/g, (whole, name) => (Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : whole));
}
return text;
}
// 应用静态文案:data-i18n / data-i18n-title / data-i18n-aria-label
function applyStaticI18n(root) {
const scope = root || document;
for (const el of scope.querySelectorAll('[data-i18n]')) el.textContent = t(el.dataset.i18n);
for (const el of scope.querySelectorAll('[data-i18n-title]')) el.title = t(el.dataset.i18nTitle);
for (const el of scope.querySelectorAll('[data-i18n-aria-label]')) el.setAttribute('aria-label', t(el.dataset.i18nAriaLabel));
}
applyStaticI18n();
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
@@ -312,7 +379,7 @@
const durationLabel = document.getElementById('durationLabel');
const timeRuler = document.getElementById('timeRuler');
const keyframesEl = document.getElementById('keyframes');
keyframesEl.title = '蓝色短线表示动画关键帧;悬停短线可查看时间和帧号';
keyframesEl.title = t('viewer.keyframesHint');
const appEl = document.getElementById('app');
const toolbarEl = document.getElementById('toolbar');
const timelineBarEl = document.getElementById('timelineBar');
@@ -381,7 +448,7 @@
try { require('../js/spine-legacy-binary.js').install(spine, dir); resolve(); }
catch (e) { reject(e); }
};
s.onerror = () => reject(new Error('无法加载 Spine 运行时:lib/' + dir + '/spine-canvas.js'));
s.onerror = () => reject(new Error(t('viewer.runtimeFailed', { file: 'lib/' + dir + '/spine-canvas.js' })));
document.head.appendChild(s);
});
}
@@ -529,8 +596,8 @@
const response = await fetch('http://localhost:41595/api/item/info?id=' + encodeURIComponent(itemId));
const info = await response.json();
if (!response.ok || info.status !== 'success' || info.data.name + '.' + info.data.ext !== path.basename(filePath))
throw new Error('Eagle 尚未同步复合后缀名称,请重新打开素材库');
if (info.data.ext === 'txt') throw new Error('此条目仍按 TXT 登记,请在 Eagle 重新生成缩略图并重新打开素材库');
throw new Error(t('viewer.compoundName'));
if (info.data.ext === 'txt') throw new Error(t('viewer.txtRegistered'));
}
const response = await fetch('http://localhost:41595/api/item/setCustomThumbnail', {
method: 'POST',
@@ -541,16 +608,16 @@
try { result = await response.json(); } catch (e) { /* use HTTP status below */ }
if (!response.ok || (result && result.status && result.status !== 'success')) {
const reason = result && (result.message || result.error);
throw new Error(reason || ('Eagle API 返回 HTTP ' + response.status));
throw new Error(reason || t('viewer.apiHttp', { status: response.status }));
}
return result;
}
async function generateThumbnail() {
if (!skeleton || !renderer) { setStatus('尚未加载骨骼,无法生成缩略图'); return; }
if (!skeleton || !renderer) { setStatus(t('viewer.noSkeletonForThumb')); return; }
const thumbBtn = document.getElementById('thumbBtn');
thumbBtn.disabled = true;
setStatus('正在生成并更新 Eagle 缩略图…', Infinity);
setStatus(t('viewer.generating'), Infinity);
try {
// 暂停动画,冻结当前帧:先确保当前动画时间已被应用到骨骼
playing = false;
@@ -599,25 +666,25 @@
const outPath = path.join(path.dirname(filePath), stem + '_thumbnail.png');
const tempPath = path.join(path.dirname(filePath), '.' + stem + '-thumbnail-' + Date.now() + (isWebp ? '.webp' : '.png'));
try {
if (!itemId) throw new Error('无法从素材路径识别 Eagle 项目 ID');
if (!itemId) throw new Error(t('viewer.noItemId'));
fs.writeFileSync(tempPath, buf);
await setEagleCustomThumbnail(itemId, tempPath);
setStatus('Eagle 缩略图已更新 · ' + (isWebp ? 'WebP' : 'PNG') + ' ' + (buf.length / 1024).toFixed(1) + 'KB', 3500);
setStatus(t('viewer.thumbUpdated', { format: isWebp ? 'WebP' : 'PNG', size: (buf.length / 1024).toFixed(1) }), 3500);
log('通过 Eagle API 更新缩略图: item=' + itemId + ' (' + buf.length + 'B)');
} catch (e) {
// 兼容旧版 Eagle:保留原先的磁盘写入方案,但明确提示可能需要手动刷新列表。
try {
fs.writeFileSync(outPath, buf);
setStatus('缩略图已写入;自动刷新失败:' + ((e && e.message) || String(e)), 6000);
setStatus(t('viewer.thumbWrittenNoRefresh', { error: (e && e.message) || String(e) }), 6000);
log('Eagle API 更新失败,已回退写入: ' + ((e && e.message) || String(e)));
} catch (e2) {
setStatus('生成缩略图失败: ' + ((e2 && e2.message) || String(e2)), 6000);
setStatus(t('viewer.thumbnailFailed', { error: (e2 && e2.message) || String(e2) }), 6000);
}
} finally {
try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) { /* ignore */ }
}
} catch (e) {
setStatus('生成缩略图失败: ' + ((e && e.message) || String(e)), 6000);
setStatus(t('viewer.thumbnailFailed', { error: (e && e.message) || String(e) }), 6000);
log('生成当前帧缩略图失败: ' + ((e && e.stack) || e));
} finally {
thumbBtn.disabled = false;
@@ -642,7 +709,7 @@
if (entry) {
const currentTime = getTrackAnimationTime(entry);
if (now >= statusHoldUntil) {
setStatus(animName(entry.animation ? entry.animation.name : '') + ' · ' + (isStaticAnimation ? '静帧' : formatSeconds(currentTime)));
setStatus(animName(entry.animation ? entry.animation.name : '') + ' · ' + (isStaticAnimation ? t('viewer.staticLabel') : formatSeconds(currentTime)));
}
// 同步时间轴(若用户未在拖动)。兼容 3.x 的 time 与 4.x 的 trackTime/getAnimationTime。
if (!seeking) {
@@ -667,8 +734,8 @@
function formatSeconds(seconds) {
const value = Math.max(0, Number(seconds) || 0);
if (value < 10) return value.toFixed(2) + ' 秒';
if (value < 60) return value.toFixed(1) + ' 秒';
if (value < 10) return t('viewer.seconds', { value: value.toFixed(2) });
if (value < 60) return t('viewer.seconds', { value: value.toFixed(1) });
const minutes = Math.floor(value / 60);
return minutes + ':' + (value % 60).toFixed(1).padStart(4, '0');
}
@@ -726,11 +793,16 @@
function updateTimeReadout(time) {
if (isStaticAnimation) {
timeLabel.textContent = '静帧 · 1 / 1 帧';
timeLabel.textContent = t('viewer.readoutStatic');
return;
}
const current = Math.max(0, Math.min(Number(time) || 0, duration));
timeLabel.textContent = formatSeconds(current) + ' / ' + formatSeconds(duration) + ' · ' + frameOf(current) + ' / ' + frameOf(duration) + ' 帧';
timeLabel.textContent = t('viewer.readout', {
time: formatSeconds(current),
total: formatSeconds(duration),
frame: frameOf(current),
totalFrames: frameOf(duration)
});
}
function niceTickInterval(totalSeconds) {
@@ -746,7 +818,7 @@
const label = document.createElement('span');
label.className = 'tick-label first';
label.style.left = '0%';
label.textContent = '0 秒(静帧)';
label.textContent = t('viewer.rulerStatic');
timeRuler.appendChild(label);
return;
}
@@ -859,7 +931,7 @@
timeline.step = String(isStaticAnimation ? 1 : Math.min(1 / FPS, Math.max(duration / 1000, 0.001)));
timeline.value = '0';
timeline.disabled = !anim || isStaticAnimation;
durationLabel.textContent = isStaticAnimation ? '静帧 · 1 帧' : ('时长 ' + formatSeconds(duration) + ' · ' + frameOf(duration) + ' 帧');
durationLabel.textContent = isStaticAnimation ? t('viewer.timelineStatic') : t('viewer.timelineDuration', { time: formatSeconds(duration), frames: frameOf(duration) });
updateTimeReadout(0);
renderTimeRuler();
renderKeyframeMarkers(anim);
@@ -873,7 +945,7 @@
if (isStaticAnimation) {
const span = document.createElement('span');
span.style.left = '0%';
span.title = '静帧姿势 · 第 1 帧';
span.title = t('viewer.staticPoseTitle');
span.setAttribute('aria-label', span.title);
keyframesEl.appendChild(span);
return;
@@ -886,7 +958,7 @@
const t = sorted[i];
const span = document.createElement('span');
span.style.left = (duration > 0 ? (t / duration) * 100 : 0) + '%';
span.title = '关键帧 · ' + formatSeconds(t) + ' · 第 ' + frameOf(t) + ' 帧';
span.title = t('viewer.keyframeTitle', { time: formatSeconds(t), frame: frameOf(t) });
span.setAttribute('aria-label', span.title);
keyframesEl.appendChild(span);
}
@@ -939,12 +1011,12 @@
const scale = Math.min(cw / img.width, ch / img.height);
const w = img.width * scale, h = img.height * scale;
ctx.drawImage(img, (cw - w) / 2, (ch - h) / 2, w, h);
setStatus('图片预览(未检测到配套 Spine 资源)');
setStatus(t('viewer.imagePreview'));
};
img.onerror = () => showError('图片加载失败', file);
img.onerror = () => showError(t('viewer.imageLoadFailed'), file);
img.src = 'data:' + mime + ';base64,' + data.toString('base64');
} catch (e) {
showError('图片加载失败', (e && e.message) || String(e));
showError(t('viewer.imageLoadFailed'), (e && e.message) || String(e));
}
}
@@ -953,14 +1025,14 @@
if (!utils.extOf(path.basename(filePath))) {
// Registering txt/bytes is required by Eagle's extension dispatch.
// Ordinary text remains readable and never starts Spine discovery.
let message = '此文件不是 .atlas.txt 或 .skel.bytes 格式的 Spine 资源。';
let message = t('viewer.notSpineText');
if (/\.txt$/i.test(filePath)) {
const fd = fs.openSync(filePath, 'r');
try {
const buffer = Buffer.alloc(256 * 1024);
const count = fs.readSync(fd, buffer, 0, buffer.length, 0);
message = buffer.subarray(0, count).toString('utf8');
if (fs.fstatSync(fd).size > count) message += '\n\n(仅显示前 256 KB)';
if (fs.fstatSync(fd).size > count) message += t('viewer.truncated256');
} finally { fs.closeSync(fd); }
}
showTextPreview(path.basename(filePath), message);
@@ -978,7 +1050,7 @@
let text = buffer.subarray(0, count).toString('utf8');
if (stat.size <= maxPreviewBytes) {
try { text = JSON.stringify(JSON.parse(text), null, 2); } catch (e) { /* 保留 JSONC/原始文本 */ }
} else text += '\n\n(文件较大,仅显示前 1 MB)';
} else text += t('viewer.truncated1m');
showTextPreview(path.basename(filePath), text);
return;
}
@@ -986,9 +1058,9 @@
if (!assets.skeletonPath || !assets.atlasPath) {
if (!assets.skeletonPath) {
showError('未找到骨骼文件', '未能定位到与所选文件配套的 Spine 骨骼文件(.skel / .skel.bytes / .json)。\n\n请将同一套 Spine 导出文件导入 Eagle。');
showError(t('viewer.noSkeletonFile'), t('viewer.noSkeletonFileMsg'));
} else {
showError('未找到 Atlas', '未能定位到配套的 .atlas / .atlas.txt 文件。');
showError(t('viewer.noAtlas'), t('viewer.noAtlasMsg'));
}
return;
}
@@ -1005,7 +1077,10 @@
activeVersion = version;
const runtimeDir = utils.chooseRuntimeDir(version, assets.isBinary);
if (!runtimeDir) {
showError('版本不支持', '无法识别的 Spine 版本:' + (version || '未知') + '\n支持的版本:3.4 / 3.5 / 3.6 / 3.7 / 3.8 / 4.0 / 4.1 / 4.2 / 4.3');
showError(t('viewer.unsupportedVersion'), t('viewer.unsupportedVersionMsg', {
version: version || t('viewer.unknownVersion'),
list: '3.4 / 3.5 / 3.6 / 3.7 / 3.8 / 4.0 / 4.1 / 4.2 / 4.3'
}));
return;
}
activeRuntimeDir = runtimeDir;
@@ -1014,7 +1089,7 @@
try {
await loadRuntime(runtimeDir);
} catch (e) {
showError('运行时加载失败', e.message);
showError(t('viewer.runtimeLoadFailed'), e.message);
return;
}
@@ -1036,7 +1111,7 @@
document.getElementById('pmaCheck').checked = premultipliedAlpha;
atlas = await buildBrowserAtlas(spine, atlasText, assets);
} catch (e) {
showError('Atlas 加载失败', (e && e.message) || String(e));
showError(t('viewer.atlasLoadFailed'), (e && e.message) || String(e));
return;
}
@@ -1053,7 +1128,7 @@
}
if (!skeletonData2 || !skeletonData2.bones || skeletonData2.bones.length === 0) {
showError('骨骼数据为空', '未能读取到有效的骨骼数据。');
showError(t('viewer.emptySkeleton'), t('viewer.emptySkeletonMsg'));
return;
}
@@ -1101,11 +1176,11 @@
const btn = document.getElementById('playBtn');
btn.disabled = isStaticAnimation;
if (isStaticAnimation) {
btn.textContent = '▮ 静帧';
btn.textContent = t('viewer.static');
btn.classList.remove('active');
return;
}
btn.textContent = playing ? '⏸ 暂停' : '▶ 播放';
btn.textContent = playing ? t('viewer.pause') : t('viewer.play');
btn.classList.toggle('active', !playing);
}
@@ -1140,11 +1215,11 @@
if (renderer) renderer.premultipliedAlpha = premultipliedAlpha;
draw();
try {
if (!previewSettings) throw new Error('素材设置尚未就绪');
if (!previewSettings) throw new Error(t('viewer.settingsNotReady'));
previewSettings.write(premultipliedAlpha);
setStatus((premultipliedAlpha ? '预乘已开启' : '预乘已关闭') + ' · 已记住此素材的设置', 1800);
setStatus(t('viewer.pmaRemembered', { state: premultipliedAlpha ? t('viewer.pmaOn') : t('viewer.pmaOff') }), 1800);
} catch (err) {
setStatus('预乘已切换,但设置保存失败:' + err.message, 5000);
setStatus(t('viewer.pmaSaveFailed', { error: err.message }), 5000);
}
});
document.getElementById('reloadBtn').addEventListener('click', () => {
@@ -1195,7 +1270,7 @@
panX = 0; panY = 0; zoom = 1;
fitToCanvas();
draw();
setStatus('已回到画布中心');
setStatus(t('viewer.recentered'));
}
});
canvas.addEventListener('pointermove', (e) => {
@@ -1229,12 +1304,12 @@
// ---------- 启动 ----------
if (!filePath) {
showError('缺少文件', '未传入文件路径参数。');
showError(t('viewer.missingFile'), t('viewer.missingFileMsg'));
} else if (!fs.existsSync(filePath)) {
showError('文件不存在', filePath);
showError(t('viewer.fileNotExist'), filePath);
} else {
loadAssets().catch((err) => {
showError('加载失败', (err && err.message) ? err.message : String(err));
showError(t('viewer.loadFailed'), (err && err.message) ? err.message : String(err));
});
}
</script>
@@ -0,0 +1,79 @@
{
"manifest": {
"app": {
"name": "SpineViewer"
}
},
"viewer": {
"title": "Spine-Vorschau",
"skin": "Skin",
"play": "▶ Abspielen",
"pause": "⏸ Pause",
"static": "▮ Statisch",
"staticLabel": "Statisch",
"playPauseTitle": "Abspielen / Pause (Leertaste)",
"loop": "Schleife",
"speed": "Geschwindigkeit",
"pma": "Prämultipliziert",
"pmaTitle": "Für Texturen, die mit Premultiplied Alpha (PMA) exportiert wurden",
"background": "Hintergrund",
"backgroundTitle": "50×50-Schachbrett, das mit der Leinwand verschoben und skaliert wird und an Blend-Modi teilnimmt",
"thumbnail": "🖼 Vorschaubild",
"thumbnailTitle": "Aus dem aktuellen Frame ein 400×400-Vorschaubild erzeugen",
"versionTitle": "Spine-Version",
"reloadTitle": "Ordner neu laden",
"animationList": "Animationsliste",
"animations": "Animationen",
"errorTitle": "Vorschau nicht möglich",
"durationPlaceholder": "Dauer --",
"keyframesLegend": "Blaue Marken: Keyframes",
"keyframesLegendTitle": "Eine blaue Marke zeigt eine Zeit, zu der mindestens eine Bone-, Slot- oder Constraint-Eigenschaft einen Keyframe hat",
"keyframesHint": "Blaue Marken kennzeichnen Keyframes; zum Anzeigen von Zeit und Frame den Zeiger darüber halten",
"timePlaceholder": "0.00 / 0.00 s · 0 / 0 Frames",
"staticPoseTitle": "Statische Pose · Frame 1",
"keyframeTitle": "Keyframe · {time} · Frame {frame}",
"timelineStatic": "Statisch · 1 Frame",
"timelineDuration": "Dauer {time} · {frames} Frames",
"readoutStatic": "Statisch · 1 / 1 Frames",
"readout": "{time} / {total} · {frame} / {totalFrames} Frames",
"rulerStatic": "0 s (statisch)",
"seconds": "{value} s",
"imagePreview": "Bildvorschau (keine passenden Spine-Ressourcen gefunden)",
"generating": "Eagle-Vorschaubild wird erzeugt und aktualisiert…",
"thumbUpdated": "Eagle-Vorschaubild aktualisiert · {format} {size}KB",
"thumbWrittenNoRefresh": "Vorschaubild geschrieben, automatische Aktualisierung fehlgeschlagen: {error}",
"recentered": "Leinwand wieder zentriert",
"pmaOn": "Prämultipliziertes Alpha an",
"pmaOff": "Prämultipliziertes Alpha aus",
"pmaRemembered": "{state} · für dieses Asset gespeichert",
"pmaSaveFailed": "Umgeschaltet, aber Speichern der Einstellung fehlgeschlagen: {error}",
"badgeFailed": "Spine-Badge konnte nicht geladen werden",
"runtimeFailed": "Spine-Laufzeit kann nicht geladen werden: {file}",
"compoundName": "Eagle hat die zusammengesetzte Dateiendung noch nicht synchronisiert; Bibliothek erneut öffnen",
"txtRegistered": "Dieses Element ist noch als TXT registriert. Vorschaubild in Eagle neu erzeugen und Bibliothek erneut öffnen",
"apiHttp": "Eagle-API meldete HTTP {status}",
"noSkeletonForThumb": "Kein Skelett geladen; Vorschaubild kann nicht erzeugt werden",
"noItemId": "Eagle-Item-ID lässt sich aus dem Pfad nicht ermitteln",
"thumbnailFailed": "Vorschaubild konnte nicht erzeugt werden: {error}",
"imageLoadFailed": "Bild konnte nicht geladen werden",
"settingsNotReady": "Asset-Einstellungen sind noch nicht bereit",
"noSkeletonFile": "Skelettdatei nicht gefunden",
"noSkeletonFileMsg": "Zur ausgewählten Datei wurde keine passende Spine-Skelettdatei (.skel / .skel.bytes / .json) gefunden.\n\nBitte den kompletten Spine-Export in Eagle importieren.",
"noAtlas": "Atlas nicht gefunden",
"noAtlasMsg": "Die passende .atlas- / .atlas.txt-Datei wurde nicht gefunden.",
"unsupportedVersion": "Nicht unterstützte Version",
"unsupportedVersionMsg": "Unbekannte Spine-Version: {version}\nUnterstützte Versionen: {list}",
"unknownVersion": "unbekannt",
"runtimeLoadFailed": "Laufzeit konnte nicht geladen werden",
"atlasLoadFailed": "Atlas konnte nicht geladen werden",
"emptySkeleton": "Leere Skelettdaten",
"emptySkeletonMsg": "Es konnten keine gültigen Skelettdaten gelesen werden.",
"missingFile": "Datei fehlt",
"missingFileMsg": "Es wurde kein Dateipfad-Parameter übergeben.",
"fileNotExist": "Datei existiert nicht",
"loadFailed": "Laden fehlgeschlagen",
"notSpineText": "Diese Datei ist keine Spine-Ressource im Format .atlas.txt oder .skel.bytes.",
"truncated256": "\n\n(nur die ersten 256 KB werden angezeigt)",
"truncated1m": "\n\n(große Datei; nur das erste 1 MB wird angezeigt)"
}
}
@@ -0,0 +1,79 @@
{
"manifest": {
"app": {
"name": "SpineViewer"
}
},
"viewer": {
"title": "Spine Preview",
"skin": "Skin",
"play": "▶ Play",
"pause": "⏸ Pause",
"static": "▮ Still",
"staticLabel": "Still",
"playPauseTitle": "Play / pause (Space)",
"loop": "Loop",
"speed": "Speed",
"pma": "Premultiplied",
"pmaTitle": "For textures exported with Premultiplied Alpha (PMA)",
"background": "Background",
"backgroundTitle": "50×50 checkerboard that pans and zooms with the canvas and participates in blend modes",
"thumbnail": "🖼 Set thumbnail",
"thumbnailTitle": "Generate a 400×400 thumbnail from the current frame",
"versionTitle": "Spine version",
"reloadTitle": "Reload folder",
"animationList": "Animation list",
"animations": "Animations",
"errorTitle": "Cannot preview",
"durationPlaceholder": "Duration --",
"keyframesLegend": "Blue ticks: keyframes",
"keyframesLegendTitle": "A blue tick marks a time where at least one bone, slot or constraint property has a keyframe",
"keyframesHint": "Blue ticks mark animation keyframes; hover a tick to see its time and frame",
"timePlaceholder": "0.00 / 0.00 s · 0 / 0 frames",
"staticPoseTitle": "Still pose · frame 1",
"keyframeTitle": "Keyframe · {time} · frame {frame}",
"timelineStatic": "Still · 1 frame",
"timelineDuration": "Duration {time} · {frames} frames",
"readoutStatic": "Still · 1 / 1 frames",
"readout": "{time} / {total} · {frame} / {totalFrames} frames",
"rulerStatic": "0 s (still)",
"seconds": "{value} s",
"imagePreview": "Image preview (no matching Spine assets found)",
"generating": "Generating and updating the Eagle thumbnail…",
"thumbUpdated": "Eagle thumbnail updated · {format} {size}KB",
"thumbWrittenNoRefresh": "Thumbnail written, but auto-refresh failed: {error}",
"recentered": "Canvas re-centered",
"pmaOn": "Premultiplied alpha on",
"pmaOff": "Premultiplied alpha off",
"pmaRemembered": "{state} · saved for this asset",
"pmaSaveFailed": "Toggled, but saving the setting failed: {error}",
"badgeFailed": "Failed to load the Spine badge",
"runtimeFailed": "Cannot load the Spine runtime: {file}",
"compoundName": "Eagle has not synced the compound extension yet; please reopen the library",
"txtRegistered": "This item is still registered as TXT. Regenerate the thumbnail in Eagle and reopen the library",
"apiHttp": "Eagle API returned HTTP {status}",
"noSkeletonForThumb": "No skeleton loaded; cannot generate a thumbnail",
"noItemId": "Cannot determine the Eagle item ID from the asset path",
"thumbnailFailed": "Failed to generate the thumbnail: {error}",
"imageLoadFailed": "Failed to load the image",
"settingsNotReady": "Asset settings are not ready",
"noSkeletonFile": "Skeleton file not found",
"noSkeletonFileMsg": "Could not locate the Spine skeleton file (.skel / .skel.bytes / .json) that matches the selected file.\n\nPlease import the whole Spine export into Eagle.",
"noAtlas": "Atlas not found",
"noAtlasMsg": "Could not locate the matching .atlas / .atlas.txt file.",
"unsupportedVersion": "Unsupported version",
"unsupportedVersionMsg": "Unrecognized Spine version: {version}\nSupported versions: {list}",
"unknownVersion": "unknown",
"runtimeLoadFailed": "Failed to load the runtime",
"atlasLoadFailed": "Failed to load the atlas",
"emptySkeleton": "Empty skeleton data",
"emptySkeletonMsg": "No valid skeleton data could be read.",
"missingFile": "Missing file",
"missingFileMsg": "No file path parameter was provided.",
"fileNotExist": "File not found",
"loadFailed": "Failed to load",
"notSpineText": "This file is not a .atlas.txt or .skel.bytes Spine asset.",
"truncated256": "\n\n(only the first 256 KB is shown)",
"truncated1m": "\n\n(large file, only the first 1 MB is shown)"
}
}
@@ -0,0 +1,79 @@
{
"manifest": {
"app": {
"name": "SpineViewer"
}
},
"viewer": {
"title": "Vista previa de Spine",
"skin": "Piel",
"play": "▶ Reproducir",
"pause": "⏸ Pausa",
"static": "▮ Estático",
"staticLabel": "Estático",
"playPauseTitle": "Reproducir / pausar (espacio)",
"loop": "Bucle",
"speed": "Velocidad",
"pma": "Premultiplicado",
"pmaTitle": "Para texturas exportadas con Premultiplied Alpha (PMA)",
"background": "Fondo",
"backgroundTitle": "Fondo de cuadros de 50×50 que se desplaza y escala con el lienzo y participa en los modos de fusión",
"thumbnail": "🖼 Crear miniatura",
"thumbnailTitle": "Crear una miniatura de 400×400 con el fotograma actual",
"versionTitle": "Versión de Spine",
"reloadTitle": "Recargar carpeta",
"animationList": "Lista de animaciones",
"animations": "Animaciones",
"errorTitle": "No se puede previsualizar",
"durationPlaceholder": "Duración --",
"keyframesLegend": "Marcas azules: fotogramas clave",
"keyframesLegendTitle": "Una marca azul señala un instante en el que al menos una propiedad de hueso, ranura o restricción tiene un fotograma clave",
"keyframesHint": "Las marcas azules indican fotogramas clave; pasa el cursor para ver el tiempo y el fotograma",
"timePlaceholder": "0.00 / 0.00 s · 0 / 0 fotogramas",
"staticPoseTitle": "Pose estática · fotograma 1",
"keyframeTitle": "Fotograma clave · {time} · fotograma {frame}",
"timelineStatic": "Estático · 1 fotograma",
"timelineDuration": "Duración {time} · {frames} fotogramas",
"readoutStatic": "Estático · 1 / 1 fotogramas",
"readout": "{time} / {total} · {frame} / {totalFrames} fotogramas",
"rulerStatic": "0 s (estático)",
"seconds": "{value} s",
"imagePreview": "Vista previa de imagen (no se encontraron recursos Spine correspondientes)",
"generating": "Generando y actualizando la miniatura de Eagle…",
"thumbUpdated": "Miniatura de Eagle actualizada · {format} {size}KB",
"thumbWrittenNoRefresh": "Miniatura escrita, pero falló la actualización automática: {error}",
"recentered": "Lienzo centrado de nuevo",
"pmaOn": "Alfa premultiplicado activado",
"pmaOff": "Alfa premultiplicado desactivado",
"pmaRemembered": "{state} · guardado para este recurso",
"pmaSaveFailed": "Se cambió, pero no se pudo guardar la configuración: {error}",
"badgeFailed": "No se pudo cargar el distintivo de Spine",
"runtimeFailed": "No se puede cargar el entorno de ejecución de Spine: {file}",
"compoundName": "Eagle aún no ha sincronizado la extensión compuesta; vuelve a abrir la biblioteca",
"txtRegistered": "Este elemento sigue registrado como TXT. Vuelve a generar la miniatura en Eagle y reabre la biblioteca",
"apiHttp": "La API de Eagle devolvió HTTP {status}",
"noSkeletonForThumb": "No hay esqueleto cargado; no se puede generar la miniatura",
"noItemId": "No se puede determinar el ID del elemento de Eagle a partir de la ruta",
"thumbnailFailed": "No se pudo generar la miniatura: {error}",
"imageLoadFailed": "No se pudo cargar la imagen",
"settingsNotReady": "La configuración del recurso aún no está lista",
"noSkeletonFile": "No se encontró el archivo de esqueleto",
"noSkeletonFileMsg": "No se encontró el archivo de esqueleto de Spine (.skel / .skel.bytes / .json) correspondiente al archivo seleccionado.\n\nImporta el conjunto completo de exportación de Spine en Eagle.",
"noAtlas": "No se encontró el Atlas",
"noAtlasMsg": "No se encontró el archivo .atlas / .atlas.txt correspondiente.",
"unsupportedVersion": "Versión no compatible",
"unsupportedVersionMsg": "Versión de Spine no reconocida: {version}\nVersiones compatibles: {list}",
"unknownVersion": "desconocida",
"runtimeLoadFailed": "Error al cargar el entorno de ejecución",
"atlasLoadFailed": "Error al cargar el atlas",
"emptySkeleton": "Datos de esqueleto vacíos",
"emptySkeletonMsg": "No se pudieron leer datos de esqueleto válidos.",
"missingFile": "Falta el archivo",
"missingFileMsg": "No se proporcionó el parámetro de ruta del archivo.",
"fileNotExist": "El archivo no existe",
"loadFailed": "Error al cargar",
"notSpineText": "Este archivo no es un recurso Spine en formato .atlas.txt o .skel.bytes.",
"truncated256": "\n\n(solo se muestran los primeros 256 KB)",
"truncated1m": "\n\n(archivo grande; solo se muestra el primer 1 MB)"
}
}
@@ -0,0 +1,79 @@
{
"manifest": {
"app": {
"name": "SpineViewer"
}
},
"viewer": {
"title": "Spine プレビュー",
"skin": "スキン",
"play": "▶ 再生",
"pause": "⏸ 一時停止",
"static": "▮ 静止",
"staticLabel": "静止",
"playPauseTitle": "再生 / 一時停止(スペース)",
"loop": "ループ",
"speed": "速度",
"pma": "乗算済み",
"pmaTitle": "Premultiplied Alpha(PMA)で書き出したテクスチャ用",
"background": "背景",
"backgroundTitle": "50×50 のチェッカー背景。キャンバスの移動・拡大に追従し、ブレンドモードにも参加します",
"thumbnail": "🖼 サムネイル生成",
"thumbnailTitle": "現在のフレームから 400×400 のサムネイルを生成",
"versionTitle": "Spine バージョン",
"reloadTitle": "フォルダーを再読み込み",
"animationList": "アニメーション一覧",
"animations": "アニメーション",
"errorTitle": "プレビューできません",
"durationPlaceholder": "長さ --",
"keyframesLegend": "青い線:キーフレーム",
"keyframesLegendTitle": "青い線は、いずれかのボーン・スロット・コンストレイントのプロパティにキーフレームがある位置を示します",
"keyframesHint": "青い線はアニメーションのキーフレームです。線にカーソルを合わせると時間とフレームが表示されます",
"timePlaceholder": "0.00 / 0.00 秒 · 0 / 0 フレーム",
"staticPoseTitle": "静止ポーズ · 1 フレーム目",
"keyframeTitle": "キーフレーム · {time} · {frame} フレーム目",
"timelineStatic": "静止 · 1 フレーム",
"timelineDuration": "長さ {time} · {frames} フレーム",
"readoutStatic": "静止 · 1 / 1 フレーム",
"readout": "{time} / {total} · {frame} / {totalFrames} フレーム",
"rulerStatic": "0 秒(静止)",
"seconds": "{value} 秒",
"imagePreview": "画像プレビュー(対応する Spine リソースが見つかりません)",
"generating": "Eagle のサムネイルを生成・更新しています…",
"thumbUpdated": "Eagle のサムネイルを更新しました · {format} {size}KB",
"thumbWrittenNoRefresh": "サムネイルを書き込みましたが、自動更新に失敗しました:{error}",
"recentered": "キャンバスを中央に戻しました",
"pmaOn": "乗算済みアルファ:オン",
"pmaOff": "乗算済みアルファ:オフ",
"pmaRemembered": "{state} · この素材の設定として保存しました",
"pmaSaveFailed": "切り替えましたが、設定の保存に失敗しました:{error}",
"badgeFailed": "Spine バッジの読み込みに失敗しました",
"runtimeFailed": "Spine ランタイムを読み込めません:{file}",
"compoundName": "Eagle が複合拡張子を同期していません。ライブラリを開き直してください",
"txtRegistered": "この項目はまだ TXT として登録されています。Eagle でサムネイルを再生成し、ライブラリを開き直してください",
"apiHttp": "Eagle API が HTTP {status} を返しました",
"noSkeletonForThumb": "骨格が読み込まれていないため、サムネイルを生成できません",
"noItemId": "素材パスから Eagle の項目 ID を特定できません",
"thumbnailFailed": "サムネイルの生成に失敗しました:{error}",
"imageLoadFailed": "画像の読み込みに失敗しました",
"settingsNotReady": "素材の設定がまだ準備できていません",
"noSkeletonFile": "骨格ファイルが見つかりません",
"noSkeletonFileMsg": "選択したファイルに対応する Spine 骨格ファイル(.skel / .skel.bytes / .json)が見つかりませんでした。\n\n同じ Spine 書き出し一式を Eagle に読み込んでください。",
"noAtlas": "Atlas が見つかりません",
"noAtlasMsg": "対応する .atlas / .atlas.txt ファイルが見つかりませんでした。",
"unsupportedVersion": "未対応のバージョン",
"unsupportedVersionMsg": "認識できない Spine バージョン:{version}\n対応バージョン:{list}",
"unknownVersion": "不明",
"runtimeLoadFailed": "ランタイムの読み込みに失敗しました",
"atlasLoadFailed": "Atlas の読み込みに失敗しました",
"emptySkeleton": "骨格データが空です",
"emptySkeletonMsg": "有効な骨格データを読み取れませんでした。",
"missingFile": "ファイルがありません",
"missingFileMsg": "ファイルパスのパラメーターが指定されていません。",
"fileNotExist": "ファイルが存在しません",
"loadFailed": "読み込みに失敗しました",
"notSpineText": "このファイルは .atlas.txt または .skel.bytes 形式の Spine リソースではありません。",
"truncated256": "\n\n(先頭 256 KB のみ表示)",
"truncated1m": "\n\n(ファイルが大きいため、先頭 1 MB のみ表示)"
}
}
@@ -0,0 +1,79 @@
{
"manifest": {
"app": {
"name": "SpineViewer"
}
},
"viewer": {
"title": "Spine 미리보기",
"skin": "스킨",
"play": "▶ 재생",
"pause": "⏸ 일시정지",
"static": "▮ 정지",
"staticLabel": "정지",
"playPauseTitle": "재생 / 일시정지(스페이스)",
"loop": "반복",
"speed": "속도",
"pma": "미리 곱함",
"pmaTitle": "Premultiplied Alpha(PMA)로 내보낸 텍스처용",
"background": "배경",
"backgroundTitle": "50×50 체커 배경. 캔버스 이동·확대를 따라가며 블렌드 모드에도 참여합니다",
"thumbnail": "🖼 썸네일 생성",
"thumbnailTitle": "현재 프레임으로 400×400 썸네일 생성",
"versionTitle": "Spine 버전",
"reloadTitle": "폴더 다시 불러오기",
"animationList": "애니메이션 목록",
"animations": "애니메이션",
"errorTitle": "미리볼 수 없음",
"durationPlaceholder": "길이 --",
"keyframesLegend": "파란 선: 키프레임",
"keyframesLegendTitle": "파란 선은 하나 이상의 본·슬롯·컨스트레인트 속성에 키프레임이 있는 시점을 나타냅니다",
"keyframesHint": "파란 선은 애니메이션 키프레임입니다. 선에 마우스를 올리면 시간과 프레임이 표시됩니다",
"timePlaceholder": "0.00 / 0.00 초 · 0 / 0 프레임",
"staticPoseTitle": "정지 자세 · 1 프레임",
"keyframeTitle": "키프레임 · {time} · {frame} 프레임",
"timelineStatic": "정지 · 1 프레임",
"timelineDuration": "길이 {time} · {frames} 프레임",
"readoutStatic": "정지 · 1 / 1 프레임",
"readout": "{time} / {total} · {frame} / {totalFrames} 프레임",
"rulerStatic": "0초 (정지)",
"seconds": "{value}초",
"imagePreview": "이미지 미리보기 (함께 있는 Spine 리소스를 찾지 못했습니다)",
"generating": "Eagle 썸네일을 생성하고 갱신하는 중…",
"thumbUpdated": "Eagle 썸네일을 갱신했습니다 · {format} {size}KB",
"thumbWrittenNoRefresh": "썸네일을 저장했지만 자동 새로고침에 실패했습니다: {error}",
"recentered": "캔버스를 가운데로 되돌렸습니다",
"pmaOn": "미리 곱한 알파 켜짐",
"pmaOff": "미리 곱한 알파 꺼짐",
"pmaRemembered": "{state} · 이 에셋의 설정으로 저장했습니다",
"pmaSaveFailed": "전환했지만 설정 저장에 실패했습니다: {error}",
"badgeFailed": "Spine 배지를 불러오지 못했습니다",
"runtimeFailed": "Spine 런타임을 불러올 수 없습니다: {file}",
"compoundName": "Eagle이 복합 확장자를 아직 동기화하지 않았습니다. 라이브러리를 다시 열어 주세요",
"txtRegistered": "이 항목은 아직 TXT로 등록되어 있습니다. Eagle에서 썸네일을 다시 생성하고 라이브러리를 다시 열어 주세요",
"apiHttp": "Eagle API가 HTTP {status}를 반환했습니다",
"noSkeletonForThumb": "스켈레톤이 로드되지 않아 썸네일을 생성할 수 없습니다",
"noItemId": "에셋 경로에서 Eagle 항목 ID를 찾을 수 없습니다",
"thumbnailFailed": "썸네일 생성 실패: {error}",
"imageLoadFailed": "이미지를 불러오지 못했습니다",
"settingsNotReady": "에셋 설정이 아직 준비되지 않았습니다",
"noSkeletonFile": "스켈레톤 파일을 찾을 수 없음",
"noSkeletonFileMsg": "선택한 파일과 짝을 이루는 Spine 스켈레톤 파일(.skel / .skel.bytes / .json)을 찾지 못했습니다.\n\n같은 Spine 내보내기 세트를 Eagle에 함께 가져와 주세요.",
"noAtlas": "Atlas를 찾을 수 없음",
"noAtlasMsg": "짝을 이루는 .atlas / .atlas.txt 파일을 찾지 못했습니다.",
"unsupportedVersion": "지원하지 않는 버전",
"unsupportedVersionMsg": "인식할 수 없는 Spine 버전: {version}\n지원 버전: {list}",
"unknownVersion": "알 수 없음",
"runtimeLoadFailed": "런타임 로드 실패",
"atlasLoadFailed": "Atlas 로드 실패",
"emptySkeleton": "스켈레톤 데이터가 비어 있음",
"emptySkeletonMsg": "유효한 스켈레톤 데이터를 읽지 못했습니다.",
"missingFile": "파일 없음",
"missingFileMsg": "파일 경로 매개변수가 전달되지 않았습니다.",
"fileNotExist": "파일이 존재하지 않음",
"loadFailed": "불러오기 실패",
"notSpineText": "이 파일은 .atlas.txt 또는 .skel.bytes 형식의 Spine 리소스가 아닙니다.",
"truncated256": "\n\n(앞부분 256 KB만 표시)",
"truncated1m": "\n\n(파일이 커서 앞부분 1 MB만 표시)"
}
}
@@ -0,0 +1,79 @@
{
"manifest": {
"app": {
"name": "SpineViewer"
}
},
"viewer": {
"title": "Просмотр Spine",
"skin": "Скин",
"play": "▶ Воспроизвести",
"pause": "⏸ Пауза",
"static": "▮ Статично",
"staticLabel": "Статично",
"playPauseTitle": "Воспроизведение / пауза (пробел)",
"loop": "Повтор",
"speed": "Скорость",
"pma": "Премультипликация",
"pmaTitle": "Для текстур, экспортированных с Premultiplied Alpha (PMA)",
"background": "Фон",
"backgroundTitle": "Шахматный фон 50×50: следует за сдвигом и масштабом холста и участвует в режимах смешивания",
"thumbnail": "🖼 Создать миниатюру",
"thumbnailTitle": "Создать миниатюру 400×400 из текущего кадра",
"versionTitle": "Версия Spine",
"reloadTitle": "Перезагрузить папку",
"animationList": "Список анимаций",
"animations": "Анимации",
"errorTitle": "Просмотр невозможен",
"durationPlaceholder": "Длительность --",
"keyframesLegend": "Синие метки: ключевые кадры",
"keyframesLegendTitle": "Синяя метка отмечает момент, в котором есть ключевой кадр хотя бы у одного свойства кости, слота или ограничения",
"keyframesHint": "Синие метки обозначают ключевые кадры; наведите курсор, чтобы увидеть время и кадр",
"timePlaceholder": "0.00 / 0.00 с · 0 / 0 кадров",
"staticPoseTitle": "Статичная поза · кадр 1",
"keyframeTitle": "Ключевой кадр · {time} · кадр {frame}",
"timelineStatic": "Статично · 1 кадр",
"timelineDuration": "Длительность {time} · {frames} кадров",
"readoutStatic": "Статично · 1 / 1 кадров",
"readout": "{time} / {total} · {frame} / {totalFrames} кадров",
"rulerStatic": "0 с (статично)",
"seconds": "{value} с",
"imagePreview": "Просмотр изображения (подходящие ресурсы Spine не найдены)",
"generating": "Создание и обновление миниатюры Eagle…",
"thumbUpdated": "Миниатюра Eagle обновлена · {format} {size}KB",
"thumbWrittenNoRefresh": "Миниатюра записана, но автообновление не удалось: {error}",
"recentered": "Холст возвращён в центр",
"pmaOn": "Премультипликация включена",
"pmaOff": "Премультипликация выключена",
"pmaRemembered": "{state} · сохранено для этого ресурса",
"pmaSaveFailed": "Переключено, но сохранить настройку не удалось: {error}",
"badgeFailed": "Не удалось загрузить значок Spine",
"runtimeFailed": "Не удалось загрузить среду выполнения Spine: {file}",
"compoundName": "Eagle ещё не синхронизировал составное расширение. Откройте библиотеку заново",
"txtRegistered": "Этот элемент всё ещё зарегистрирован как TXT. Пересоздайте миниатюру в Eagle и откройте библиотеку заново",
"apiHttp": "Eagle API вернул HTTP {status}",
"noSkeletonForThumb": "Скелет не загружен — невозможно создать миниатюру",
"noItemId": "Не удалось определить ID элемента Eagle по пути к ресурсу",
"thumbnailFailed": "Не удалось создать миниатюру: {error}",
"imageLoadFailed": "Не удалось загрузить изображение",
"settingsNotReady": "Настройки ресурса ещё не готовы",
"noSkeletonFile": "Файл скелета не найден",
"noSkeletonFileMsg": "Не удалось найти файл скелета Spine (.skel / .skel.bytes / .json), соответствующий выбранному файлу.\n\nИмпортируйте весь набор экспорта Spine в Eagle.",
"noAtlas": "Atlas не найден",
"noAtlasMsg": "Не удалось найти соответствующий файл .atlas / .atlas.txt.",
"unsupportedVersion": "Неподдерживаемая версия",
"unsupportedVersionMsg": "Неизвестная версия Spine: {version}\nПоддерживаемые версии: {list}",
"unknownVersion": "неизвестно",
"runtimeLoadFailed": "Не удалось загрузить среду выполнения",
"atlasLoadFailed": "Не удалось загрузить atlas",
"emptySkeleton": "Пустые данные скелета",
"emptySkeletonMsg": "Не удалось прочитать корректные данные скелета.",
"missingFile": "Файл отсутствует",
"missingFileMsg": "Параметр пути к файлу не передан.",
"fileNotExist": "Файл не существует",
"loadFailed": "Не удалось загрузить",
"notSpineText": "Этот файл не является ресурсом Spine в формате .atlas.txt или .skel.bytes.",
"truncated256": "\n\n(показаны только первые 256 КБ)",
"truncated1m": "\n\n(файл большой, показан только первый 1 МБ)"
}
}
@@ -0,0 +1,79 @@
{
"manifest": {
"app": {
"name": "SpineViewer"
}
},
"viewer": {
"title": "Spine 预览",
"skin": "皮肤",
"play": "▶ 播放",
"pause": "⏸ 暂停",
"static": "▮ 静帧",
"staticLabel": "静帧",
"playPauseTitle": "播放/暂停 (空格)",
"loop": "循环",
"speed": "速度",
"pma": "预乘",
"pmaTitle": "用于以 Premultiplied Alpha(PMA)方式导出的贴图",
"background": "背景",
"backgroundTitle": "50×50 方格背景,随画布移动和缩放,并参与混合模式",
"thumbnail": "🖼 生成缩略图",
"thumbnailTitle": "使用当前帧生成 400×400 缩略图",
"versionTitle": "Spine 版本",
"reloadTitle": "重新加载目录",
"animationList": "动画列表",
"animations": "动画",
"errorTitle": "无法预览",
"durationPlaceholder": "时长 --",
"keyframesLegend": "蓝色短线:关键帧",
"keyframesLegendTitle": "蓝色短线表示动画中至少一个骨骼、插槽或约束属性在该时刻有关键帧",
"keyframesHint": "蓝色短线表示动画关键帧;悬停短线可查看时间和帧号",
"timePlaceholder": "0.00 / 0.00 秒 · 0 / 0 帧",
"staticPoseTitle": "静帧姿势 · 第 1 帧",
"keyframeTitle": "关键帧 · {time} · 第 {frame} 帧",
"timelineStatic": "静帧 · 1 帧",
"timelineDuration": "时长 {time} · {frames} 帧",
"readoutStatic": "静帧 · 1 / 1 帧",
"readout": "{time} / {total} · {frame} / {totalFrames} 帧",
"rulerStatic": "0 秒(静帧)",
"seconds": "{value} 秒",
"imagePreview": "图片预览(未检测到配套 Spine 资源)",
"generating": "正在生成并更新 Eagle 缩略图…",
"thumbUpdated": "Eagle 缩略图已更新 · {format} {size}KB",
"thumbWrittenNoRefresh": "缩略图已写入;自动刷新失败:{error}",
"recentered": "已回到画布中心",
"pmaOn": "预乘已开启",
"pmaOff": "预乘已关闭",
"pmaRemembered": "{state} · 已记住此素材的设置",
"pmaSaveFailed": "预乘已切换,但设置保存失败:{error}",
"badgeFailed": "Spine badge 加载失败",
"runtimeFailed": "无法加载 Spine 运行时:{file}",
"compoundName": "Eagle 尚未同步复合后缀名称,请重新打开素材库",
"txtRegistered": "此条目仍按 TXT 登记,请在 Eagle 重新生成缩略图并重新打开素材库",
"apiHttp": "Eagle API 返回 HTTP {status}",
"noSkeletonForThumb": "尚未加载骨骼,无法生成缩略图",
"noItemId": "无法从素材路径识别 Eagle 项目 ID",
"thumbnailFailed": "生成缩略图失败: {error}",
"imageLoadFailed": "图片加载失败",
"settingsNotReady": "素材设置尚未就绪",
"noSkeletonFile": "未找到骨骼文件",
"noSkeletonFileMsg": "未能定位到与所选文件配套的 Spine 骨骼文件(.skel / .skel.bytes / .json)。\n\n请将同一套 Spine 导出文件导入 Eagle。",
"noAtlas": "未找到 Atlas",
"noAtlasMsg": "未能定位到配套的 .atlas / .atlas.txt 文件。",
"unsupportedVersion": "版本不支持",
"unsupportedVersionMsg": "无法识别的 Spine 版本:{version}\n支持的版本:{list}",
"unknownVersion": "未知",
"runtimeLoadFailed": "运行时加载失败",
"atlasLoadFailed": "Atlas 加载失败",
"emptySkeleton": "骨骼数据为空",
"emptySkeletonMsg": "未能读取到有效的骨骼数据。",
"missingFile": "缺少文件",
"missingFileMsg": "未传入文件路径参数。",
"fileNotExist": "文件不存在",
"loadFailed": "加载失败",
"notSpineText": "此文件不是 .atlas.txt 或 .skel.bytes 格式的 Spine 资源。",
"truncated256": "\n\n(仅显示前 256 KB)",
"truncated1m": "\n\n(文件较大,仅显示前 1 MB)"
}
}
@@ -0,0 +1,79 @@
{
"manifest": {
"app": {
"name": "SpineViewer"
}
},
"viewer": {
"title": "Spine 預覽",
"skin": "外觀",
"play": "▶ 播放",
"pause": "⏸ 暫停",
"static": "▮ 靜態",
"staticLabel": "靜態",
"playPauseTitle": "播放/暫停(空白鍵)",
"loop": "循環",
"speed": "速度",
"pma": "預乘",
"pmaTitle": "用於以 Premultiplied Alpha(PMA)方式匯出的貼圖",
"background": "背景",
"backgroundTitle": "50×50 方格背景,隨畫布移動和縮放,並參與混合模式",
"thumbnail": "🖼 產生縮圖",
"thumbnailTitle": "使用目前影格產生 400×400 縮圖",
"versionTitle": "Spine 版本",
"reloadTitle": "重新載入目錄",
"animationList": "動畫清單",
"animations": "動畫",
"errorTitle": "無法預覽",
"durationPlaceholder": "時長 --",
"keyframesLegend": "藍色短線:關鍵影格",
"keyframesLegendTitle": "藍色短線表示動畫中至少一個骨骼、插槽或約束屬性在該時刻有關鍵影格",
"keyframesHint": "藍色短線表示動畫關鍵影格;將游標停在短線上可查看時間與影格",
"timePlaceholder": "0.00 / 0.00 秒 · 0 / 0 影格",
"staticPoseTitle": "靜態姿勢 · 第 1 影格",
"keyframeTitle": "關鍵影格 · {time} · 第 {frame} 影格",
"timelineStatic": "靜態 · 1 影格",
"timelineDuration": "時長 {time} · {frames} 影格",
"readoutStatic": "靜態 · 1 / 1 影格",
"readout": "{time} / {total} · {frame} / {totalFrames} 影格",
"rulerStatic": "0 秒(靜態)",
"seconds": "{value} 秒",
"imagePreview": "圖片預覽(未偵測到配套的 Spine 資源)",
"generating": "正在產生並更新 Eagle 縮圖…",
"thumbUpdated": "Eagle 縮圖已更新 · {format} {size}KB",
"thumbWrittenNoRefresh": "縮圖已寫入;自動重新整理失敗:{error}",
"recentered": "已回到畫布中心",
"pmaOn": "預乘已開啟",
"pmaOff": "預乘已關閉",
"pmaRemembered": "{state} · 已記住此素材的設定",
"pmaSaveFailed": "預乘已切換,但設定儲存失敗:{error}",
"badgeFailed": "Spine badge 載入失敗",
"runtimeFailed": "無法載入 Spine 執行環境:{file}",
"compoundName": "Eagle 尚未同步複合副檔名,請重新開啟素材庫",
"txtRegistered": "此項目仍以 TXT 登錄,請在 Eagle 重新產生縮圖並重新開啟素材庫",
"apiHttp": "Eagle API 回傳 HTTP {status}",
"noSkeletonForThumb": "尚未載入骨骼,無法產生縮圖",
"noItemId": "無法從素材路徑辨識 Eagle 項目 ID",
"thumbnailFailed": "產生縮圖失敗:{error}",
"imageLoadFailed": "圖片載入失敗",
"settingsNotReady": "素材設定尚未就緒",
"noSkeletonFile": "找不到骨骼檔案",
"noSkeletonFileMsg": "找不到與所選檔案搭配的 Spine 骨骼檔案(.skel / .skel.bytes / .json)。\n\n請將同一套 Spine 匯出檔案匯入 Eagle。",
"noAtlas": "找不到 Atlas",
"noAtlasMsg": "找不到搭配的 .atlas / .atlas.txt 檔案。",
"unsupportedVersion": "不支援的版本",
"unsupportedVersionMsg": "無法辨識的 Spine 版本:{version}\n支援的版本:{list}",
"unknownVersion": "未知",
"runtimeLoadFailed": "執行環境載入失敗",
"atlasLoadFailed": "Atlas 載入失敗",
"emptySkeleton": "骨骼資料為空",
"emptySkeletonMsg": "無法讀取到有效的骨骼資料。",
"missingFile": "缺少檔案",
"missingFileMsg": "未傳入檔案路徑參數。",
"fileNotExist": "檔案不存在",
"loadFailed": "載入失敗",
"notSpineText": "此檔案不是 .atlas.txt 或 .skel.bytes 格式的 Spine 資源。",
"truncated256": "\n\n(僅顯示前 256 KB)",
"truncated1m": "\n\n(檔案較大,僅顯示前 1 MB)"
}
}
+12 -1
View File
@@ -3,7 +3,7 @@
"version": "1.1.0",
"platform": "win",
"arch": "x64",
"name": "SpineViewer",
"name": "{{manifest.app.name}}",
"logo": "/logo-spineviewer.png",
"keywords": [
"spine",
@@ -12,6 +12,17 @@
"skel",
"atlas"
],
"fallbackLanguage": "zh_CN",
"languages": [
"en",
"zh_CN",
"zh_TW",
"ja_JP",
"ko_KR",
"ru_RU",
"es_ES",
"de_DE"
],
"devTools": false,
"preview": {
"skel,skel.bytes,bytes,json,atlas,atlas.txt,txt": {
+137 -62
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spine 预览</title>
<title data-i18n="viewer.title">Spine 预览</title>
<style>
:root {
--bg: #1e1e1e;
@@ -197,30 +197,30 @@
<body>
<div id="app">
<div id="toolbar">
<label>皮肤
<label><span data-i18n="viewer.skin">皮肤</span>
<select id="skinSelect" disabled></select>
</label>
<button id="playBtn" title="播放/暂停 (空格)">⏸ 暂停</button>
<label><input type="checkbox" id="loopCheck" checked> 循环</label>
<label>速度
<button id="playBtn" data-i18n-title="viewer.playPauseTitle" title="播放/暂停 (空格)">⏸ 暂停</button>
<label><input type="checkbox" id="loopCheck" checked> <span data-i18n="viewer.loop">循环</span></label>
<label><span data-i18n="viewer.speed">速度</span>
<input type="range" id="speedRange" min="0" max="3" step="0.05" value="1">
<span id="speedVal">1.0x</span>
</label>
<label title="用于以 Premultiplied Alpha(PMA)方式导出的贴图">
<input type="checkbox" id="pmaCheck" disabled> 预乘
<label data-i18n-title="viewer.pmaTitle" title="用于以 Premultiplied Alpha(PMA)方式导出的贴图">
<input type="checkbox" id="pmaCheck" disabled> <span data-i18n="viewer.pma">预乘</span>
</label>
<label title="50×50 方格背景,随画布移动和缩放,并参与混合模式">
<input type="checkbox" id="backgroundCheck"> 背景
<label data-i18n-title="viewer.backgroundTitle" title="50×50 方格背景,随画布移动和缩放,并参与混合模式">
<input type="checkbox" id="backgroundCheck"> <span data-i18n="viewer.background">背景</span>
</label>
<div class="spacer"></div>
<button id="thumbBtn" title="使用当前帧生成 400×400 缩略图">🖼 生成缩略图</button>
<label id="verLabel" style="color: var(--text-dim);" title="Spine 版本"></label>
<button id="reloadBtn" title="重新加载目录">↻</button>
<button id="thumbBtn" data-i18n="viewer.thumbnail" data-i18n-title="viewer.thumbnailTitle" title="使用当前帧生成 400×400 缩略图">🖼 生成缩略图</button>
<label id="verLabel" style="color: var(--text-dim);" data-i18n-title="viewer.versionTitle" title="Spine 版本"></label>
<button id="reloadBtn" title="重新加载目录" data-i18n-title="viewer.reloadTitle">↻</button>
</div>
<div id="stage">
<aside id="animationPanel" aria-label="动画列表">
<div id="animationPanelTitle">动画</div>
<div id="animationList" role="listbox" aria-label="动画"></div>
<aside id="animationPanel" data-i18n-aria-label="viewer.animationList" aria-label="动画列表">
<div id="animationPanelTitle" data-i18n="viewer.animations">动画</div>
<div id="animationList" role="listbox" data-i18n-aria-label="viewer.animations" aria-label="动画"></div>
</aside>
<div id="viewport">
<canvas id="canvas"></canvas>
@@ -228,7 +228,7 @@
<div id="loading"><div class="spinner"></div></div>
<div id="overlay" class="hidden">
<div class="box">
<div class="title" id="errorTitle">无法预览</div>
<div class="title" id="errorTitle" data-i18n="viewer.errorTitle">无法预览</div>
<div class="msg" id="errorMsg"></div>
</div>
</div>
@@ -236,15 +236,15 @@
</div>
<div id="timelineBar">
<div id="timelineLabels">
<span class="tl-label" id="durationLabel">时长 --</span>
<span id="keyframeLegend" title="蓝色短线表示动画中至少一个骨骼、插槽或约束属性在该时刻有关键帧">蓝色短线:关键帧</span>
<span class="tl-label" id="durationLabel" data-i18n="viewer.durationPlaceholder">时长 --</span>
<span id="keyframeLegend" data-i18n="viewer.keyframesLegend" data-i18n-title="viewer.keyframesLegendTitle" title="蓝色短线表示动画中至少一个骨骼、插槽或约束属性在该时刻有关键帧">蓝色短线:关键帧</span>
</div>
<div class="tl-wrap">
<div id="timeRuler"></div>
<input type="range" id="timeline" min="0" max="1" step="0.001" value="0" disabled>
<div id="keyframes"></div>
</div>
<span id="timeLabel">0.00 / 0.00 秒 · 0 / 0 帧</span>
<span id="timeLabel" data-i18n="viewer.timePlaceholder">0.00 / 0.00 秒 · 0 / 0 帧</span>
</div>
</div>
@@ -263,7 +263,7 @@
try {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = () => reject(new Error('Spine badge 加载失败'));
image.onerror = () => reject(new Error(t('viewer.badgeFailed')));
image.src = spineBadge.dataUrl();
} catch (e) {
reject(e);
@@ -300,6 +300,73 @@
const lang = urlParams.get('lang');
if (theme === 'light') document.documentElement.setAttribute('data-theme', 'light');
// ---------- 多国语系 ----------
// 文案与 manifest.json 的 languages / fallbackLanguage 共用同一份 _locales/<语言>.json。
// 格式扩展插件的查看器页不会注入 Eagle Plugin API,因此这里直接读取语言文件。
const I18N_LANGUAGES = ['en', 'zh_CN', 'zh_TW', 'ja_JP', 'ko_KR', 'ru_RU', 'es_ES', 'de_DE'];
const I18N_FALLBACK = 'zh_CN';
function normalizeLocale(raw) {
if (!raw) return null;
const tag = String(raw).trim().toLowerCase().replace(/-/g, '_');
if (tag === 'zh_tw' || tag === 'zh_hk' || tag === 'zh_hant' || tag === 'zh_mo') return 'zh_TW';
for (const code of I18N_LANGUAGES) {
if (code.toLowerCase() === tag) return code;
}
const base = tag.split('_')[0];
if (base === 'zh') return 'zh_CN';
for (const code of I18N_LANGUAGES) {
if (code.toLowerCase().split('_')[0] === base) return code;
}
return null;
}
function readLocaleFile(code) {
try {
const file = path.join(__dirname, '..', '_locales', code + '.json');
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
return (data && typeof data === 'object') ? data : {};
} catch (e) {
return {};
}
}
// 按 "viewer.skin" 这样的点号路径取字符串,与官方 i18next 的用法一致
function lookupMessage(table, key) {
let node = table;
for (const part of String(key).split('.')) {
if (!node || typeof node !== 'object') return undefined;
node = node[part];
}
return typeof node === 'string' ? node : undefined;
}
const fallbackMessages = readLocaleFile(I18N_FALLBACK);
const locale = normalizeLocale(lang) || I18N_FALLBACK;
const messages = locale === I18N_FALLBACK ? fallbackMessages : readLocaleFile(locale);
document.documentElement.lang = locale.replace('_', '-');
log('界面语言: ' + locale + ' (lang=' + (lang || '未提供') + ')');
// t('viewer.skin', { name: '...' }):缺失的键回退到 fallbackLanguage
function t(key, vars) {
let text = lookupMessage(messages, key);
if (text === undefined) text = lookupMessage(fallbackMessages, key);
if (text === undefined) return key;
if (vars) {
text = text.replace(/\{(\w+)\}/g, (whole, name) => (Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : whole));
}
return text;
}
// 应用静态文案:data-i18n / data-i18n-title / data-i18n-aria-label
function applyStaticI18n(root) {
const scope = root || document;
for (const el of scope.querySelectorAll('[data-i18n]')) el.textContent = t(el.dataset.i18n);
for (const el of scope.querySelectorAll('[data-i18n-title]')) el.title = t(el.dataset.i18nTitle);
for (const el of scope.querySelectorAll('[data-i18n-aria-label]')) el.setAttribute('aria-label', t(el.dataset.i18nAriaLabel));
}
applyStaticI18n();
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
@@ -312,7 +379,7 @@
const durationLabel = document.getElementById('durationLabel');
const timeRuler = document.getElementById('timeRuler');
const keyframesEl = document.getElementById('keyframes');
keyframesEl.title = '蓝色短线表示动画关键帧;悬停短线可查看时间和帧号';
keyframesEl.title = t('viewer.keyframesHint');
const appEl = document.getElementById('app');
const toolbarEl = document.getElementById('toolbar');
const timelineBarEl = document.getElementById('timelineBar');
@@ -381,7 +448,7 @@
try { require('../js/spine-legacy-binary.js').install(spine, dir); resolve(); }
catch (e) { reject(e); }
};
s.onerror = () => reject(new Error('无法加载 Spine 运行时:lib/' + dir + '/spine-canvas.js'));
s.onerror = () => reject(new Error(t('viewer.runtimeFailed', { file: 'lib/' + dir + '/spine-canvas.js' })));
document.head.appendChild(s);
});
}
@@ -529,8 +596,8 @@
const response = await fetch('http://localhost:41595/api/item/info?id=' + encodeURIComponent(itemId));
const info = await response.json();
if (!response.ok || info.status !== 'success' || info.data.name + '.' + info.data.ext !== path.basename(filePath))
throw new Error('Eagle 尚未同步复合后缀名称,请重新打开素材库');
if (info.data.ext === 'txt') throw new Error('此条目仍按 TXT 登记,请在 Eagle 重新生成缩略图并重新打开素材库');
throw new Error(t('viewer.compoundName'));
if (info.data.ext === 'txt') throw new Error(t('viewer.txtRegistered'));
}
const response = await fetch('http://localhost:41595/api/item/setCustomThumbnail', {
method: 'POST',
@@ -541,16 +608,16 @@
try { result = await response.json(); } catch (e) { /* use HTTP status below */ }
if (!response.ok || (result && result.status && result.status !== 'success')) {
const reason = result && (result.message || result.error);
throw new Error(reason || ('Eagle API 返回 HTTP ' + response.status));
throw new Error(reason || t('viewer.apiHttp', { status: response.status }));
}
return result;
}
async function generateThumbnail() {
if (!skeleton || !renderer) { setStatus('尚未加载骨骼,无法生成缩略图'); return; }
if (!skeleton || !renderer) { setStatus(t('viewer.noSkeletonForThumb')); return; }
const thumbBtn = document.getElementById('thumbBtn');
thumbBtn.disabled = true;
setStatus('正在生成并更新 Eagle 缩略图…', Infinity);
setStatus(t('viewer.generating'), Infinity);
try {
// 暂停动画,冻结当前帧:先确保当前动画时间已被应用到骨骼
playing = false;
@@ -599,25 +666,25 @@
const outPath = path.join(path.dirname(filePath), stem + '_thumbnail.png');
const tempPath = path.join(path.dirname(filePath), '.' + stem + '-thumbnail-' + Date.now() + (isWebp ? '.webp' : '.png'));
try {
if (!itemId) throw new Error('无法从素材路径识别 Eagle 项目 ID');
if (!itemId) throw new Error(t('viewer.noItemId'));
fs.writeFileSync(tempPath, buf);
await setEagleCustomThumbnail(itemId, tempPath);
setStatus('Eagle 缩略图已更新 · ' + (isWebp ? 'WebP' : 'PNG') + ' ' + (buf.length / 1024).toFixed(1) + 'KB', 3500);
setStatus(t('viewer.thumbUpdated', { format: isWebp ? 'WebP' : 'PNG', size: (buf.length / 1024).toFixed(1) }), 3500);
log('通过 Eagle API 更新缩略图: item=' + itemId + ' (' + buf.length + 'B)');
} catch (e) {
// 兼容旧版 Eagle:保留原先的磁盘写入方案,但明确提示可能需要手动刷新列表。
try {
fs.writeFileSync(outPath, buf);
setStatus('缩略图已写入;自动刷新失败:' + ((e && e.message) || String(e)), 6000);
setStatus(t('viewer.thumbWrittenNoRefresh', { error: (e && e.message) || String(e) }), 6000);
log('Eagle API 更新失败,已回退写入: ' + ((e && e.message) || String(e)));
} catch (e2) {
setStatus('生成缩略图失败: ' + ((e2 && e2.message) || String(e2)), 6000);
setStatus(t('viewer.thumbnailFailed', { error: (e2 && e2.message) || String(e2) }), 6000);
}
} finally {
try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) { /* ignore */ }
}
} catch (e) {
setStatus('生成缩略图失败: ' + ((e && e.message) || String(e)), 6000);
setStatus(t('viewer.thumbnailFailed', { error: (e && e.message) || String(e) }), 6000);
log('生成当前帧缩略图失败: ' + ((e && e.stack) || e));
} finally {
thumbBtn.disabled = false;
@@ -642,7 +709,7 @@
if (entry) {
const currentTime = getTrackAnimationTime(entry);
if (now >= statusHoldUntil) {
setStatus(animName(entry.animation ? entry.animation.name : '') + ' · ' + (isStaticAnimation ? '静帧' : formatSeconds(currentTime)));
setStatus(animName(entry.animation ? entry.animation.name : '') + ' · ' + (isStaticAnimation ? t('viewer.staticLabel') : formatSeconds(currentTime)));
}
// 同步时间轴(若用户未在拖动)。兼容 3.x 的 time 与 4.x 的 trackTime/getAnimationTime。
if (!seeking) {
@@ -667,8 +734,8 @@
function formatSeconds(seconds) {
const value = Math.max(0, Number(seconds) || 0);
if (value < 10) return value.toFixed(2) + ' 秒';
if (value < 60) return value.toFixed(1) + ' 秒';
if (value < 10) return t('viewer.seconds', { value: value.toFixed(2) });
if (value < 60) return t('viewer.seconds', { value: value.toFixed(1) });
const minutes = Math.floor(value / 60);
return minutes + ':' + (value % 60).toFixed(1).padStart(4, '0');
}
@@ -726,11 +793,16 @@
function updateTimeReadout(time) {
if (isStaticAnimation) {
timeLabel.textContent = '静帧 · 1 / 1 帧';
timeLabel.textContent = t('viewer.readoutStatic');
return;
}
const current = Math.max(0, Math.min(Number(time) || 0, duration));
timeLabel.textContent = formatSeconds(current) + ' / ' + formatSeconds(duration) + ' · ' + frameOf(current) + ' / ' + frameOf(duration) + ' 帧';
timeLabel.textContent = t('viewer.readout', {
time: formatSeconds(current),
total: formatSeconds(duration),
frame: frameOf(current),
totalFrames: frameOf(duration)
});
}
function niceTickInterval(totalSeconds) {
@@ -746,7 +818,7 @@
const label = document.createElement('span');
label.className = 'tick-label first';
label.style.left = '0%';
label.textContent = '0 秒(静帧)';
label.textContent = t('viewer.rulerStatic');
timeRuler.appendChild(label);
return;
}
@@ -859,7 +931,7 @@
timeline.step = String(isStaticAnimation ? 1 : Math.min(1 / FPS, Math.max(duration / 1000, 0.001)));
timeline.value = '0';
timeline.disabled = !anim || isStaticAnimation;
durationLabel.textContent = isStaticAnimation ? '静帧 · 1 帧' : ('时长 ' + formatSeconds(duration) + ' · ' + frameOf(duration) + ' 帧');
durationLabel.textContent = isStaticAnimation ? t('viewer.timelineStatic') : t('viewer.timelineDuration', { time: formatSeconds(duration), frames: frameOf(duration) });
updateTimeReadout(0);
renderTimeRuler();
renderKeyframeMarkers(anim);
@@ -873,7 +945,7 @@
if (isStaticAnimation) {
const span = document.createElement('span');
span.style.left = '0%';
span.title = '静帧姿势 · 第 1 帧';
span.title = t('viewer.staticPoseTitle');
span.setAttribute('aria-label', span.title);
keyframesEl.appendChild(span);
return;
@@ -886,7 +958,7 @@
const t = sorted[i];
const span = document.createElement('span');
span.style.left = (duration > 0 ? (t / duration) * 100 : 0) + '%';
span.title = '关键帧 · ' + formatSeconds(t) + ' · 第 ' + frameOf(t) + ' 帧';
span.title = t('viewer.keyframeTitle', { time: formatSeconds(t), frame: frameOf(t) });
span.setAttribute('aria-label', span.title);
keyframesEl.appendChild(span);
}
@@ -939,12 +1011,12 @@
const scale = Math.min(cw / img.width, ch / img.height);
const w = img.width * scale, h = img.height * scale;
ctx.drawImage(img, (cw - w) / 2, (ch - h) / 2, w, h);
setStatus('图片预览(未检测到配套 Spine 资源)');
setStatus(t('viewer.imagePreview'));
};
img.onerror = () => showError('图片加载失败', file);
img.onerror = () => showError(t('viewer.imageLoadFailed'), file);
img.src = 'data:' + mime + ';base64,' + data.toString('base64');
} catch (e) {
showError('图片加载失败', (e && e.message) || String(e));
showError(t('viewer.imageLoadFailed'), (e && e.message) || String(e));
}
}
@@ -953,14 +1025,14 @@
if (!utils.extOf(path.basename(filePath))) {
// Registering txt/bytes is required by Eagle's extension dispatch.
// Ordinary text remains readable and never starts Spine discovery.
let message = '此文件不是 .atlas.txt 或 .skel.bytes 格式的 Spine 资源。';
let message = t('viewer.notSpineText');
if (/\.txt$/i.test(filePath)) {
const fd = fs.openSync(filePath, 'r');
try {
const buffer = Buffer.alloc(256 * 1024);
const count = fs.readSync(fd, buffer, 0, buffer.length, 0);
message = buffer.subarray(0, count).toString('utf8');
if (fs.fstatSync(fd).size > count) message += '\n\n(仅显示前 256 KB)';
if (fs.fstatSync(fd).size > count) message += t('viewer.truncated256');
} finally { fs.closeSync(fd); }
}
showTextPreview(path.basename(filePath), message);
@@ -978,7 +1050,7 @@
let text = buffer.subarray(0, count).toString('utf8');
if (stat.size <= maxPreviewBytes) {
try { text = JSON.stringify(JSON.parse(text), null, 2); } catch (e) { /* 保留 JSONC/原始文本 */ }
} else text += '\n\n(文件较大,仅显示前 1 MB)';
} else text += t('viewer.truncated1m');
showTextPreview(path.basename(filePath), text);
return;
}
@@ -986,9 +1058,9 @@
if (!assets.skeletonPath || !assets.atlasPath) {
if (!assets.skeletonPath) {
showError('未找到骨骼文件', '未能定位到与所选文件配套的 Spine 骨骼文件(.skel / .skel.bytes / .json)。\n\n请将同一套 Spine 导出文件导入 Eagle。');
showError(t('viewer.noSkeletonFile'), t('viewer.noSkeletonFileMsg'));
} else {
showError('未找到 Atlas', '未能定位到配套的 .atlas / .atlas.txt 文件。');
showError(t('viewer.noAtlas'), t('viewer.noAtlasMsg'));
}
return;
}
@@ -1005,7 +1077,10 @@
activeVersion = version;
const runtimeDir = utils.chooseRuntimeDir(version, assets.isBinary);
if (!runtimeDir) {
showError('版本不支持', '无法识别的 Spine 版本:' + (version || '未知') + '\n支持的版本:3.4 / 3.5 / 3.6 / 3.7 / 3.8 / 4.0 / 4.1 / 4.2 / 4.3');
showError(t('viewer.unsupportedVersion'), t('viewer.unsupportedVersionMsg', {
version: version || t('viewer.unknownVersion'),
list: '3.4 / 3.5 / 3.6 / 3.7 / 3.8 / 4.0 / 4.1 / 4.2 / 4.3'
}));
return;
}
activeRuntimeDir = runtimeDir;
@@ -1014,7 +1089,7 @@
try {
await loadRuntime(runtimeDir);
} catch (e) {
showError('运行时加载失败', e.message);
showError(t('viewer.runtimeLoadFailed'), e.message);
return;
}
@@ -1036,7 +1111,7 @@
document.getElementById('pmaCheck').checked = premultipliedAlpha;
atlas = await buildBrowserAtlas(spine, atlasText, assets);
} catch (e) {
showError('Atlas 加载失败', (e && e.message) || String(e));
showError(t('viewer.atlasLoadFailed'), (e && e.message) || String(e));
return;
}
@@ -1053,7 +1128,7 @@
}
if (!skeletonData2 || !skeletonData2.bones || skeletonData2.bones.length === 0) {
showError('骨骼数据为空', '未能读取到有效的骨骼数据。');
showError(t('viewer.emptySkeleton'), t('viewer.emptySkeletonMsg'));
return;
}
@@ -1101,11 +1176,11 @@
const btn = document.getElementById('playBtn');
btn.disabled = isStaticAnimation;
if (isStaticAnimation) {
btn.textContent = '▮ 静帧';
btn.textContent = t('viewer.static');
btn.classList.remove('active');
return;
}
btn.textContent = playing ? '⏸ 暂停' : '▶ 播放';
btn.textContent = playing ? t('viewer.pause') : t('viewer.play');
btn.classList.toggle('active', !playing);
}
@@ -1140,11 +1215,11 @@
if (renderer) renderer.premultipliedAlpha = premultipliedAlpha;
draw();
try {
if (!previewSettings) throw new Error('素材设置尚未就绪');
if (!previewSettings) throw new Error(t('viewer.settingsNotReady'));
previewSettings.write(premultipliedAlpha);
setStatus((premultipliedAlpha ? '预乘已开启' : '预乘已关闭') + ' · 已记住此素材的设置', 1800);
setStatus(t('viewer.pmaRemembered', { state: premultipliedAlpha ? t('viewer.pmaOn') : t('viewer.pmaOff') }), 1800);
} catch (err) {
setStatus('预乘已切换,但设置保存失败:' + err.message, 5000);
setStatus(t('viewer.pmaSaveFailed', { error: err.message }), 5000);
}
});
document.getElementById('reloadBtn').addEventListener('click', () => {
@@ -1195,7 +1270,7 @@
panX = 0; panY = 0; zoom = 1;
fitToCanvas();
draw();
setStatus('已回到画布中心');
setStatus(t('viewer.recentered'));
}
});
canvas.addEventListener('pointermove', (e) => {
@@ -1229,12 +1304,12 @@
// ---------- 启动 ----------
if (!filePath) {
showError('缺少文件', '未传入文件路径参数。');
showError(t('viewer.missingFile'), t('viewer.missingFileMsg'));
} else if (!fs.existsSync(filePath)) {
showError('文件不存在', filePath);
showError(t('viewer.fileNotExist'), filePath);
} else {
loadAssets().catch((err) => {
showError('加载失败', (err && err.message) ? err.message : String(err));
showError(t('viewer.loadFailed'), (err && err.message) ? err.message : String(err));
});
}
</script>