Refactor localization handling and improve i18n support across the application

- Replaced `safeTranslate` with `translate` in various components for consistent translation handling.
- Updated Chinese (Simplified and Traditional) localization files to include new keys and improved translations for model card actions, metadata, and usage tips.
- Enhanced the ModelCard, ModelDescription, ModelMetadata, ModelModal, and ModelTags components to utilize the new translation functions.
- Improved user feedback messages for actions like copying to clipboard, saving notes, and updating tags with localized strings.
- Ensured all UI elements reflect the correct translations based on the user's language preference.
This commit is contained in:
Will Miao
2025-08-31 11:19:06 +08:00
parent 75f3764e6c
commit 59010ca431
16 changed files with 1029 additions and 208 deletions

View File

@@ -341,6 +341,66 @@
"updated": "Modellbeschreibung aktualisiert",
"updateFailed": "Fehler beim Aktualisieren der Modellbeschreibung"
}
},
"actions": {
"editModelName": "Modellname bearbeiten",
"editFileName": "Dateiname bearbeiten",
"editBaseModel": "Basismodell bearbeiten",
"viewOnCivitai": "Auf Civitai anzeigen",
"viewOnCivitaiText": "Auf Civitai anzeigen",
"viewCreatorProfile": "Ersteller-Profil anzeigen"
},
"metadata": {
"version": "Version",
"fileName": "Dateiname",
"location": "Standort",
"baseModel": "Basismodell",
"size": "Größe",
"unknown": "Unbekannt",
"usageTips": "Verwendungstipps",
"additionalNotes": "Zusätzliche Notizen",
"notesHint": "Enter zum Speichern, Shift+Enter für neue Zeile",
"addNotesPlaceholder": "Fügen Sie hier Ihre Notizen hinzu...",
"aboutThisVersion": "Über diese Version",
"validation": {
"nameTooLong": "Modellname ist auf 100 Zeichen begrenzt",
"nameEmpty": "Modellname darf nicht leer sein"
},
"messages": {
"nameUpdated": "Modellname erfolgreich aktualisiert",
"nameUpdateFailed": "Aktualisierung des Modellnamens fehlgeschlagen",
"baseModelUpdated": "Basismodell erfolgreich aktualisiert",
"baseModelUpdateFailed": "Aktualisierung des Basismodells fehlgeschlagen"
}
},
"notes": {
"saved": "Notizen erfolgreich gespeichert",
"saveFailed": "Speichern der Notizen fehlgeschlagen"
},
"usageTips": {
"addPresetParameter": "Voreingestellten Parameter hinzufügen...",
"strengthMin": "Stärke Min",
"strengthMax": "Stärke Max",
"strength": "Stärke",
"clipSkip": "Clip Skip",
"valuePlaceholder": "Wert",
"add": "Hinzufügen"
},
"tags": {
"messages": {
"updated": "Tags erfolgreich aktualisiert",
"updateFailed": "Aktualisierung der Tags fehlgeschlagen"
},
"validation": {
"maxLength": "Tag sollte 30 Zeichen nicht überschreiten",
"maxCount": "Maximal 30 Tags erlaubt",
"duplicate": "Dieser Tag existiert bereits"
}
},
"recipeTab": {
"noRecipesFound": "Keine Rezepte gefunden, die diese LoRA verwenden.",
"loadingRecipes": "Rezepte werden geladen...",
"errorLoadingRecipes": "Fehler beim Laden der Rezepte. Bitte versuchen Sie es später erneut."
}
}
},
@@ -463,5 +523,38 @@
"filter": "Modelle nach verschiedenen Kriterien filtern",
"sort": "Modelle nach verschiedenen Attributen sortieren",
"backToTop": "Zurück zum Seitenanfang scrollen"
},
"modelCard": {
"actions": {
"addToFavorites": "Zu Favoriten hinzufügen",
"removeFromFavorites": "Aus Favoriten entfernen",
"viewOnCivitai": "Auf Civitai anzeigen",
"notAvailableFromCivitai": "Nicht verfügbar auf Civitai",
"sendToWorkflow": "An ComfyUI senden (Klick: Anhängen, Shift+Klick: Ersetzen)",
"copyLoRASyntax": "LoRA-Syntax kopieren",
"checkpointNameCopied": "Checkpoint-Name kopiert",
"toggleBlur": "Unschärfe umschalten",
"show": "Anzeigen",
"openExampleImages": "Beispielbilder-Ordner öffnen"
},
"nsfw": {
"matureContent": "Erwachseneninhalte",
"xxxRated": "XXX-bewertete Inhalte",
"xRated": "X-bewertete Inhalte",
"rRated": "R-bewertete Inhalte"
},
"favorites": {
"added": "Zu Favoriten hinzugefügt",
"removed": "Aus Favoriten entfernt",
"updateFailed": "Favoriten-Status aktualisierung fehlgeschlagen"
},
"sendToWorkflow": {
"checkpointNotImplemented": "Checkpoint an Workflow senden - Funktion noch zu implementieren"
},
"exampleImages": {
"checkError": "Fehler beim Überprüfen der Beispielbilder",
"missingHash": "Fehlende Modell-Hash-Informationen.",
"noRemoteImagesAvailable": "Keine Remote-Beispielbilder für dieses Modell auf Civitai verfügbar"
}
}
}
}

View File

@@ -73,48 +73,39 @@
"korean": "한국어",
"french": "Français",
"spanish": "Español"
}
},
"modelCard": {
"actions": {
"addToFavorites": "Add to favorites",
"removeFromFavorites": "Remove from favorites",
"viewOnCivitai": "View on Civitai",
"notAvailableFromCivitai": "Not available from Civitai",
"sendToWorkflow": "Send to ComfyUI (Click: Append, Shift+Click: Replace)",
"copyLoRASyntax": "Copy LoRA Syntax",
"checkpointNameCopied": "Checkpoint name copied",
"toggleBlur": "Toggle blur",
"show": "Show",
"openExampleImages": "Open Example Images Folder"
},
"modelCard": {
"favorites": {
"added": "Added to favorites",
"removed": "Removed from favorites",
"updateFailed": "Failed to update favorite status"
},
"sendToWorkflow": {
"checkpointNotImplemented": "Send checkpoint to workflow - feature to be implemented"
},
"exampleImages": {
"checkError": "Error checking for example images",
"missingHash": "Missing model hash information."
}
"nsfw": {
"matureContent": "Mature Content",
"xxxRated": "XXX-rated Content",
"xRated": "X-rated Content",
"rRated": "R-rated Content"
},
"modelTags": {
"messages": {
"updated": "Tags updated successfully",
"updateFailed": "Failed to update tags"
},
"validation": {
"maxLength": "Tag should not exceed 30 characters",
"maxCount": "Maximum 30 tags allowed",
"duplicate": "This tag already exists"
}
"favorites": {
"added": "Added to favorites",
"removed": "Removed from favorites",
"updateFailed": "Failed to update favorite status"
},
"modelMetadata": {
"validation": {
"nameTooLong": "Model name is limited to 100 characters",
"nameEmpty": "Model name cannot be empty"
},
"messages": {
"nameUpdated": "Model name updated successfully",
"nameUpdateFailed": "Failed to update model name",
"baseModelUpdated": "Base model updated successfully",
"baseModelUpdateFailed": "Failed to update base model"
}
"sendToWorkflow": {
"checkpointNotImplemented": "Send checkpoint to workflow - feature to be implemented"
},
"recipeTab": {
"noRecipesFound": "No recipes found that use this Lora.",
"loadingRecipes": "Loading recipes...",
"errorLoadingRecipes": "Failed to load recipes. Please try again later."
"exampleImages": {
"checkError": "Error checking for example images",
"missingHash": "Missing model hash information.",
"noRemoteImagesAvailable": "No remote example images available for this model on Civitai"
}
},
"header": {
@@ -487,6 +478,50 @@
"confirmAction": "Confirm Re-link"
},
"model": {
"actions": {
"editModelName": "Edit model name",
"editFileName": "Edit file name",
"editBaseModel": "Edit base model",
"viewOnCivitai": "View on Civitai",
"viewOnCivitaiText": "View on Civitai",
"viewCreatorProfile": "View Creator Profile"
},
"metadata": {
"version": "Version",
"fileName": "File Name",
"location": "Location",
"baseModel": "Base Model",
"size": "Size",
"unknown": "Unknown",
"usageTips": "Usage Tips",
"additionalNotes": "Additional Notes",
"notesHint": "Press Enter to save, Shift+Enter for new line",
"addNotesPlaceholder": "Add your notes here...",
"aboutThisVersion": "About this version",
"validation": {
"nameTooLong": "Model name is limited to 100 characters",
"nameEmpty": "Model name cannot be empty"
},
"messages": {
"nameUpdated": "Model name updated successfully",
"nameUpdateFailed": "Failed to update model name",
"baseModelUpdated": "Base model updated successfully",
"baseModelUpdateFailed": "Failed to update base model"
}
},
"notes": {
"saved": "Notes saved successfully",
"saveFailed": "Failed to save notes"
},
"usageTips": {
"addPresetParameter": "Add preset parameter...",
"strengthMin": "Strength Min",
"strengthMax": "Strength Max",
"strength": "Strength",
"clipSkip": "Clip Skip",
"valuePlaceholder": "Value",
"add": "Add"
},
"description": {
"noDescription": "No model description available",
"failedToLoad": "Failed to load model description",
@@ -509,6 +544,22 @@
"description": "Loading model description...",
"recipes": "Loading recipes...",
"examples": "Loading examples..."
},
"tags": {
"messages": {
"updated": "Tags updated successfully",
"updateFailed": "Failed to update tags"
},
"validation": {
"maxLength": "Tag should not exceed 30 characters",
"maxCount": "Maximum 30 tags allowed",
"duplicate": "This tag already exists"
}
},
"recipeTab": {
"noRecipesFound": "No recipes found that use this Lora.",
"loadingRecipes": "Loading recipes...",
"errorLoadingRecipes": "Failed to load recipes. Please try again later."
}
}
},
@@ -622,6 +673,40 @@
"keepOne": "Keep only one version (preferably with better metadata/previews) and safely delete the others."
}
},
"uiHelpers": {
"clipboard": {
"copied": "Copied to clipboard",
"copyFailed": "Copy failed"
},
"lora": {
"syntaxCopied": "LoRA syntax copied to clipboard",
"syntaxCopiedNoTriggerWords": "LoRA syntax copied to clipboard (no trigger words found)",
"syntaxCopiedWithTriggerWords": "LoRA syntax with trigger words copied to clipboard",
"syntaxCopiedWithTriggerWordGroups": "LoRA syntax with trigger word groups copied to clipboard"
},
"workflow": {
"noSupportedNodes": "No supported target nodes found in workflow",
"communicationFailed": "Failed to communicate with ComfyUI",
"recipeReplaced": "Recipe replaced in workflow",
"recipeAdded": "Recipe added to workflow",
"loraReplaced": "LoRA replaced in workflow",
"loraAdded": "LoRA added to workflow",
"recipeFailedToSend": "Failed to send recipe to workflow",
"loraFailedToSend": "Failed to send LoRA to workflow"
},
"nodeSelector": {
"recipe": "Recipe",
"lora": "LoRA",
"replace": "Replace",
"append": "Append",
"selectTargetNode": "Select target node",
"sendToAll": "Send to All"
},
"exampleImages": {
"openingFolder": "Opening example images folder",
"failedToOpen": "Failed to open example images folder"
}
},
"tooltips": {
"refresh": "Refresh the model list",
"bulkOperations": "Select multiple models for batch operations",

View File

@@ -341,6 +341,66 @@
"updated": "Descripción del modelo actualizada",
"updateFailed": "Error al actualizar la descripción del modelo"
}
},
"actions": {
"editModelName": "Editar nombre del modelo",
"editFileName": "Editar nombre del archivo",
"editBaseModel": "Editar modelo base",
"viewOnCivitai": "Ver en Civitai",
"viewOnCivitaiText": "Ver en Civitai",
"viewCreatorProfile": "Ver perfil del creador"
},
"metadata": {
"version": "Versión",
"fileName": "Nombre del archivo",
"location": "Ubicación",
"baseModel": "Modelo base",
"size": "Tamaño",
"unknown": "Desconocido",
"usageTips": "Consejos de uso",
"additionalNotes": "Notas adicionales",
"notesHint": "Presiona Enter para guardar, Shift+Enter para nueva línea",
"addNotesPlaceholder": "Añade tus notas aquí...",
"aboutThisVersion": "Sobre esta versión",
"validation": {
"nameTooLong": "El nombre del modelo está limitado a 100 caracteres",
"nameEmpty": "El nombre del modelo no puede estar vacío"
},
"messages": {
"nameUpdated": "Nombre del modelo actualizado exitosamente",
"nameUpdateFailed": "Error al actualizar el nombre del modelo",
"baseModelUpdated": "Modelo base actualizado exitosamente",
"baseModelUpdateFailed": "Error al actualizar el modelo base"
}
},
"notes": {
"saved": "Notas guardadas exitosamente",
"saveFailed": "Error al guardar las notas"
},
"usageTips": {
"addPresetParameter": "Añadir parámetro preestablecido...",
"strengthMin": "Fuerza Mín",
"strengthMax": "Fuerza Máx",
"strength": "Fuerza",
"clipSkip": "Clip Skip",
"valuePlaceholder": "Valor",
"add": "Añadir"
},
"tags": {
"messages": {
"updated": "Etiquetas actualizadas exitosamente",
"updateFailed": "Error al actualizar las etiquetas"
},
"validation": {
"maxLength": "La etiqueta no debe exceder 30 caracteres",
"maxCount": "Máximo 30 etiquetas permitidas",
"duplicate": "Esta etiqueta ya existe"
}
},
"recipeTab": {
"noRecipesFound": "No se encontraron recetas que usen esta LoRA.",
"loadingRecipes": "Cargando recetas...",
"errorLoadingRecipes": "Error al cargar las recetas. Por favor intenta más tarde."
}
}
},
@@ -463,5 +523,38 @@
"filter": "Filtrar modelos por varios criterios",
"sort": "Ordenar modelos por diferentes atributos",
"backToTop": "Volver al inicio de la página"
},
"modelCard": {
"actions": {
"addToFavorites": "Añadir a favoritos",
"removeFromFavorites": "Quitar de favoritos",
"viewOnCivitai": "Ver en Civitai",
"notAvailableFromCivitai": "No disponible en Civitai",
"sendToWorkflow": "Enviar a ComfyUI (Clic: Adjuntar, Shift+Clic: Reemplazar)",
"copyLoRASyntax": "Copiar sintaxis LoRA",
"checkpointNameCopied": "Nombre del checkpoint copiado",
"toggleBlur": "Alternar difuminado",
"show": "Mostrar",
"openExampleImages": "Abrir carpeta de imágenes de ejemplo"
},
"nsfw": {
"matureContent": "Contenido para adultos",
"xxxRated": "Contenido XXX",
"xRated": "Contenido X",
"rRated": "Contenido R"
},
"favorites": {
"added": "Añadido a favoritos",
"removed": "Eliminado de favoritos",
"updateFailed": "Error al actualizar estado de favorito"
},
"sendToWorkflow": {
"checkpointNotImplemented": "Enviar checkpoint al flujo de trabajo - función por implementar"
},
"exampleImages": {
"checkError": "Error al verificar imágenes de ejemplo",
"missingHash": "Falta información de hash del modelo.",
"noRemoteImagesAvailable": "No hay imágenes de ejemplo remotas disponibles para este modelo en Civitai"
}
}
}
}

View File

@@ -341,6 +341,66 @@
"updated": "Description du modèle mise à jour",
"updateFailed": "Échec de la mise à jour de la description du modèle"
}
},
"actions": {
"editModelName": "Modifier le nom du modèle",
"editFileName": "Modifier le nom du fichier",
"editBaseModel": "Modifier le modèle de base",
"viewOnCivitai": "Voir sur Civitai",
"viewOnCivitaiText": "Voir sur Civitai",
"viewCreatorProfile": "Voir le profil du créateur"
},
"metadata": {
"version": "Version",
"fileName": "Nom du fichier",
"location": "Emplacement",
"baseModel": "Modèle de base",
"size": "Taille",
"unknown": "Inconnu",
"usageTips": "Conseils d'utilisation",
"additionalNotes": "Notes supplémentaires",
"notesHint": "Appuyez sur Entrée pour sauvegarder, Shift+Entrée pour nouvelle ligne",
"addNotesPlaceholder": "Ajoutez vos notes ici...",
"aboutThisVersion": "À propos de cette version",
"validation": {
"nameTooLong": "Le nom du modèle est limité à 100 caractères",
"nameEmpty": "Le nom du modèle ne peut pas être vide"
},
"messages": {
"nameUpdated": "Nom du modèle mis à jour avec succès",
"nameUpdateFailed": "Échec de la mise à jour du nom du modèle",
"baseModelUpdated": "Modèle de base mis à jour avec succès",
"baseModelUpdateFailed": "Échec de la mise à jour du modèle de base"
}
},
"notes": {
"saved": "Notes sauvegardées avec succès",
"saveFailed": "Échec de la sauvegarde des notes"
},
"usageTips": {
"addPresetParameter": "Ajouter un paramètre prédéfini...",
"strengthMin": "Force Min",
"strengthMax": "Force Max",
"strength": "Force",
"clipSkip": "Clip Skip",
"valuePlaceholder": "Valeur",
"add": "Ajouter"
},
"tags": {
"messages": {
"updated": "Étiquettes mises à jour avec succès",
"updateFailed": "Échec de la mise à jour des étiquettes"
},
"validation": {
"maxLength": "L'étiquette ne doit pas dépasser 30 caractères",
"maxCount": "Maximum 30 étiquettes autorisées",
"duplicate": "Cette étiquette existe déjà"
}
},
"recipeTab": {
"noRecipesFound": "Aucune recette trouvée utilisant cette LoRA.",
"loadingRecipes": "Chargement des recettes...",
"errorLoadingRecipes": "Échec du chargement des recettes. Veuillez réessayer plus tard."
}
}
},
@@ -463,5 +523,38 @@
"filter": "Filtrer les modèles selon divers critères",
"sort": "Trier les modèles selon différents attributs",
"backToTop": "Remonter en haut de la page"
},
"modelCard": {
"actions": {
"addToFavorites": "Ajouter aux favoris",
"removeFromFavorites": "Retirer des favoris",
"viewOnCivitai": "Voir sur Civitai",
"notAvailableFromCivitai": "Non disponible sur Civitai",
"sendToWorkflow": "Envoyer vers ComfyUI (Clic: Ajouter, Shift+Clic: Remplacer)",
"copyLoRASyntax": "Copier la syntaxe LoRA",
"checkpointNameCopied": "Nom du checkpoint copié",
"toggleBlur": "Basculer le flou",
"show": "Afficher",
"openExampleImages": "Ouvrir le dossier d'images d'exemple"
},
"nsfw": {
"matureContent": "Contenu pour adultes",
"xxxRated": "Contenu XXX",
"xRated": "Contenu X",
"rRated": "Contenu R"
},
"favorites": {
"added": "Ajouté aux favoris",
"removed": "Retiré des favoris",
"updateFailed": "Échec de la mise à jour du statut favori"
},
"sendToWorkflow": {
"checkpointNotImplemented": "Envoyer checkpoint vers workflow - fonctionnalité à implémenter"
},
"exampleImages": {
"checkError": "Erreur lors de la vérification des images d'exemple",
"missingHash": "Informations de hachage du modèle manquantes.",
"noRemoteImagesAvailable": "Aucune image d'exemple distante disponible pour ce modèle sur Civitai"
}
}
}
}

View File

@@ -341,6 +341,66 @@
"updated": "モデルの説明を更新しました",
"updateFailed": "モデルの説明の更新に失敗しました"
}
},
"actions": {
"editModelName": "モデル名を編集",
"editFileName": "ファイル名を編集",
"editBaseModel": "ベースモデルを編集",
"viewOnCivitai": "Civitaiで表示",
"viewOnCivitaiText": "Civitaiで表示",
"viewCreatorProfile": "クリエイタープロフィールを表示"
},
"metadata": {
"version": "バージョン",
"fileName": "ファイル名",
"location": "場所",
"baseModel": "ベースモデル",
"size": "サイズ",
"unknown": "不明",
"usageTips": "使用のコツ",
"additionalNotes": "追加メモ",
"notesHint": "Enterで保存、Shift+Enterで改行",
"addNotesPlaceholder": "ここにメモを追加...",
"aboutThisVersion": "このバージョンについて",
"validation": {
"nameTooLong": "モデル名は100文字以内に制限されています",
"nameEmpty": "モデル名を空にすることはできません"
},
"messages": {
"nameUpdated": "モデル名が正常に更新されました",
"nameUpdateFailed": "モデル名の更新に失敗しました",
"baseModelUpdated": "ベースモデルが正常に更新されました",
"baseModelUpdateFailed": "ベースモデルの更新に失敗しました"
}
},
"notes": {
"saved": "メモが正常に保存されました",
"saveFailed": "メモの保存に失敗しました"
},
"usageTips": {
"addPresetParameter": "プリセットパラメータを追加...",
"strengthMin": "強度最小",
"strengthMax": "強度最大",
"strength": "強度",
"clipSkip": "Clip Skip",
"valuePlaceholder": "値",
"add": "追加"
},
"tags": {
"messages": {
"updated": "タグが正常に更新されました",
"updateFailed": "タグの更新に失敗しました"
},
"validation": {
"maxLength": "タグは30文字を超えてはいけません",
"maxCount": "最大30個のタグが許可されています",
"duplicate": "このタグは既に存在します"
}
},
"recipeTab": {
"noRecipesFound": "このLoRAを使用するレシピが見つかりません。",
"loadingRecipes": "レシピを読み込み中...",
"errorLoadingRecipes": "レシピの読み込みに失敗しました。後でもう一度お試しください。"
}
}
},
@@ -463,5 +523,38 @@
"filter": "様々な条件でモデルをフィルター",
"sort": "異なる属性でモデルをソート",
"backToTop": "ページトップにスクロール"
},
"modelCard": {
"actions": {
"addToFavorites": "お気に入りに追加",
"removeFromFavorites": "お気に入りから削除",
"viewOnCivitai": "Civitaiで表示",
"notAvailableFromCivitai": "Civitaiで利用不可",
"sendToWorkflow": "ComfyUIに送信クリック追加、Shift+クリック:置換)",
"copyLoRASyntax": "LoRA構文をコピー",
"checkpointNameCopied": "チェックポイント名をコピーしました",
"toggleBlur": "ぼかしを切り替え",
"show": "表示",
"openExampleImages": "サンプル画像フォルダを開く"
},
"nsfw": {
"matureContent": "成人向けコンテンツ",
"xxxRated": "XXX指定コンテンツ",
"xRated": "X指定コンテンツ",
"rRated": "R指定コンテンツ"
},
"favorites": {
"added": "お気に入りに追加しました",
"removed": "お気に入りから削除しました",
"updateFailed": "お気に入り状態の更新に失敗しました"
},
"sendToWorkflow": {
"checkpointNotImplemented": "チェックポイントをワークフローに送信 - 機能実装予定"
},
"exampleImages": {
"checkError": "サンプル画像の確認でエラーが発生しました",
"missingHash": "モデルハッシュ情報が不足しています。",
"noRemoteImagesAvailable": "このモデルのリモートサンプル画像はCivitaiで利用できません"
}
}
}
}

View File

@@ -341,6 +341,66 @@
"updated": "모델 설명이 업데이트되었습니다",
"updateFailed": "모델 설명 업데이트에 실패했습니다"
}
},
"actions": {
"editModelName": "모델명 편집",
"editFileName": "파일명 편집",
"editBaseModel": "베이스 모델 편집",
"viewOnCivitai": "Civitai에서 보기",
"viewOnCivitaiText": "Civitai에서 보기",
"viewCreatorProfile": "제작자 프로필 보기"
},
"metadata": {
"version": "버전",
"fileName": "파일명",
"location": "위치",
"baseModel": "베이스 모델",
"size": "크기",
"unknown": "알 수 없음",
"usageTips": "사용 팁",
"additionalNotes": "추가 메모",
"notesHint": "Enter로 저장, Shift+Enter로 줄바꿈",
"addNotesPlaceholder": "여기에 메모를 추가하세요...",
"aboutThisVersion": "이 버전에 대해",
"validation": {
"nameTooLong": "모델명은 100자로 제한됩니다",
"nameEmpty": "모델명은 비워둘 수 없습니다"
},
"messages": {
"nameUpdated": "모델명이 성공적으로 업데이트됨",
"nameUpdateFailed": "모델명 업데이트 실패",
"baseModelUpdated": "베이스 모델이 성공적으로 업데이트됨",
"baseModelUpdateFailed": "베이스 모델 업데이트 실패"
}
},
"notes": {
"saved": "메모가 성공적으로 저장됨",
"saveFailed": "메모 저장 실패"
},
"usageTips": {
"addPresetParameter": "프리셋 매개변수 추가...",
"strengthMin": "강도 최소",
"strengthMax": "강도 최대",
"strength": "강도",
"clipSkip": "Clip Skip",
"valuePlaceholder": "값",
"add": "추가"
},
"tags": {
"messages": {
"updated": "태그가 성공적으로 업데이트됨",
"updateFailed": "태그 업데이트 실패"
},
"validation": {
"maxLength": "태그는 30자를 초과할 수 없습니다",
"maxCount": "최대 30개의 태그가 허용됩니다",
"duplicate": "이 태그는 이미 존재합니다"
}
},
"recipeTab": {
"noRecipesFound": "이 LoRA를 사용하는 레시피를 찾을 수 없습니다.",
"loadingRecipes": "레시피 로딩 중...",
"errorLoadingRecipes": "레시피 로딩에 실패했습니다. 나중에 다시 시도해주세요."
}
}
},
@@ -463,5 +523,38 @@
"filter": "다양한 기준으로 모델 필터링",
"sort": "다양한 속성으로 모델 정렬",
"backToTop": "페이지 맨 위로 스크롤"
},
"modelCard": {
"actions": {
"addToFavorites": "즐겨찾기에 추가",
"removeFromFavorites": "즐겨찾기에서 제거",
"viewOnCivitai": "Civitai에서 보기",
"notAvailableFromCivitai": "Civitai에서 사용할 수 없음",
"sendToWorkflow": "ComfyUI로 전송 (클릭: 추가, Shift+클릭: 교체)",
"copyLoRASyntax": "LoRA 문법 복사",
"checkpointNameCopied": "체크포인트 이름이 복사됨",
"toggleBlur": "흐림 효과 전환",
"show": "표시",
"openExampleImages": "예제 이미지 폴더 열기"
},
"nsfw": {
"matureContent": "성인 콘텐츠",
"xxxRated": "XXX 등급 콘텐츠",
"xRated": "X 등급 콘텐츠",
"rRated": "R 등급 콘텐츠"
},
"favorites": {
"added": "즐겨찾기에 추가됨",
"removed": "즐겨찾기에서 제거됨",
"updateFailed": "즐겨찾기 상태 업데이트 실패"
},
"sendToWorkflow": {
"checkpointNotImplemented": "워크플로우로 체크포인트 전송 - 기능 구현 예정"
},
"exampleImages": {
"checkError": "예제 이미지 확인 중 오류 발생",
"missingHash": "모델 해시 정보 누락.",
"noRemoteImagesAvailable": "이 모델에 대한 원격 예제 이미지가 Civitai에서 사용할 수 없습니다"
}
}
}
}

View File

@@ -341,6 +341,66 @@
"updated": "Описание модели обновлено",
"updateFailed": "Не удалось обновить описание модели"
}
},
"actions": {
"editModelName": "Редактировать имя модели",
"editFileName": "Редактировать имя файла",
"editBaseModel": "Редактировать базовую модель",
"viewOnCivitai": "Посмотреть на Civitai",
"viewOnCivitaiText": "Посмотреть на Civitai",
"viewCreatorProfile": "Посмотреть профиль создателя"
},
"metadata": {
"version": "Версия",
"fileName": "Имя файла",
"location": "Расположение",
"baseModel": "Базовая модель",
"size": "Размер",
"unknown": "Неизвестно",
"usageTips": "Советы по использованию",
"additionalNotes": "Дополнительные заметки",
"notesHint": "Enter для сохранения, Shift+Enter для новой строки",
"addNotesPlaceholder": "Добавьте свои заметки здесь...",
"aboutThisVersion": "О данной версии",
"validation": {
"nameTooLong": "Имя модели ограничено 100 символами",
"nameEmpty": "Имя модели не может быть пустым"
},
"messages": {
"nameUpdated": "Имя модели успешно обновлено",
"nameUpdateFailed": "Не удалось обновить имя модели",
"baseModelUpdated": "Базовая модель успешно обновлена",
"baseModelUpdateFailed": "Не удалось обновить базовую модель"
}
},
"notes": {
"saved": "Заметки успешно сохранены",
"saveFailed": "Не удалось сохранить заметки"
},
"usageTips": {
"addPresetParameter": "Добавить предустановленный параметр...",
"strengthMin": "Мин. сила",
"strengthMax": "Макс. сила",
"strength": "Сила",
"clipSkip": "Clip Skip",
"valuePlaceholder": "Значение",
"add": "Добавить"
},
"tags": {
"messages": {
"updated": "Теги успешно обновлены",
"updateFailed": "Не удалось обновить теги"
},
"validation": {
"maxLength": "Тег не должен превышать 30 символов",
"maxCount": "Разрешено максимум 30 тегов",
"duplicate": "Этот тег уже существует"
}
},
"recipeTab": {
"noRecipesFound": "Не найдено рецептов, использующих эту LoRA.",
"loadingRecipes": "Загрузка рецептов...",
"errorLoadingRecipes": "Не удалось загрузить рецепты. Пожалуйста, попробуйте позже."
}
}
},
@@ -463,5 +523,38 @@
"filter": "Фильтровать модели по различным критериям",
"sort": "Сортировать модели по разным атрибутам",
"backToTop": "Прокрутить обратно наверх страницы"
},
"modelCard": {
"actions": {
"addToFavorites": "Добавить в избранное",
"removeFromFavorites": "Удалить из избранного",
"viewOnCivitai": "Посмотреть на Civitai",
"notAvailableFromCivitai": "Недоступно на Civitai",
"sendToWorkflow": "Отправить в ComfyUI (Клик: Добавить, Shift+Клик: Заменить)",
"copyLoRASyntax": "Копировать синтаксис LoRA",
"checkpointNameCopied": "Имя чекпоинта скопировано",
"toggleBlur": "Переключить размытие",
"show": "Показать",
"openExampleImages": "Открыть папку с примерами изображений"
},
"nsfw": {
"matureContent": "Контент для взрослых",
"xxxRated": "XXX-контент",
"xRated": "X-контент",
"rRated": "R-контент"
},
"favorites": {
"added": "Добавлено в избранное",
"removed": "Удалено из избранного",
"updateFailed": "Не удалось обновить статус избранного"
},
"sendToWorkflow": {
"checkpointNotImplemented": "Отправка чекпоинта в рабочий процесс - функция в разработке"
},
"exampleImages": {
"checkError": "Ошибка при проверке примеров изображений",
"missingHash": "Отсутствует информация о хэше модели.",
"noRemoteImagesAvailable": "Для этой модели нет удалённых примеров изображений на Civitai"
}
}
}
}

View File

@@ -73,48 +73,39 @@
"korean": "한국어",
"french": "Français",
"spanish": "Español"
}
},
"modelCard": {
"actions": {
"addToFavorites": "添加到收藏",
"removeFromFavorites": "从收藏中移除",
"viewOnCivitai": "在 Civitai 上查看",
"notAvailableFromCivitai": "Civitai 上不可用",
"sendToWorkflow": "发送到 ComfyUI点击追加Shift+点击:替换)",
"copyLoRASyntax": "复制 LoRA 语法",
"checkpointNameCopied": "Checkpoint 名称已复制",
"toggleBlur": "切换模糊",
"show": "显示",
"openExampleImages": "打开示例图片文件夹"
},
"modelCard": {
"favorites": {
"added": "已添加到收藏",
"removed": "已从收藏中移除",
"updateFailed": "更新收藏状态失败"
},
"sendToWorkflow": {
"checkpointNotImplemented": "发送 Checkpoint 到工作流 - 功能待实现"
},
"exampleImages": {
"checkError": "检查示例图片时出错",
"missingHash": "缺少模型哈希信息。"
}
"nsfw": {
"matureContent": "成人内容",
"xxxRated": "XXX 级内容",
"xRated": "X 级内容",
"rRated": "R 级内容"
},
"modelTags": {
"messages": {
"updated": "标签更新成功",
"updateFailed": "更新标签失败"
},
"validation": {
"maxLength": "标签长度不能超过30个字符",
"maxCount": "最多允许30个标签",
"duplicate": "该标签已存在"
}
"favorites": {
"added": "已添加到收藏",
"removed": "已从收藏中移除",
"updateFailed": "更新收藏状态失败"
},
"modelMetadata": {
"validation": {
"nameTooLong": "模型名称最多100个字符",
"nameEmpty": "模型名称不能为空"
},
"messages": {
"nameUpdated": "模型名称更新成功",
"nameUpdateFailed": "更新模型名称失败",
"baseModelUpdated": "基础模型更新成功",
"baseModelUpdateFailed": "更新基础模型失败"
}
"sendToWorkflow": {
"checkpointNotImplemented": "发送 Checkpoint 到工作流 - 功能待实现"
},
"recipeTab": {
"noRecipesFound": "未找到使用此 LoRA 的配方。",
"loadingRecipes": "正在加载配方...",
"errorLoadingRecipes": "加载配方失败。请稍后重试。"
"exampleImages": {
"checkError": "检查示例图片时出错",
"missingHash": "缺少模型哈希信息。",
"noRemoteImagesAvailable": "该模型在 Civitai 上没有可用的远程示例图片"
}
},
"header": {
@@ -487,6 +478,50 @@
"confirmAction": "确认重新链接"
},
"model": {
"actions": {
"editModelName": "编辑模型名称",
"editFileName": "编辑文件名",
"editBaseModel": "编辑基础模型",
"viewOnCivitai": "在 Civitai 上查看",
"viewOnCivitaiText": "在 Civitai 上查看",
"viewCreatorProfile": "查看创作者资料"
},
"metadata": {
"version": "版本",
"fileName": "文件名",
"location": "位置",
"baseModel": "基础模型",
"size": "大小",
"unknown": "未知",
"usageTips": "使用技巧",
"additionalNotes": "附加说明",
"notesHint": "按 Enter 保存Shift+Enter 换行",
"addNotesPlaceholder": "在此添加您的说明...",
"aboutThisVersion": "关于此版本",
"validation": {
"nameTooLong": "模型名称最多100个字符",
"nameEmpty": "模型名称不能为空"
},
"messages": {
"nameUpdated": "模型名称更新成功",
"nameUpdateFailed": "更新模型名称失败",
"baseModelUpdated": "基础模型更新成功",
"baseModelUpdateFailed": "更新基础模型失败"
}
},
"notes": {
"saved": "说明保存成功",
"saveFailed": "保存说明失败"
},
"usageTips": {
"addPresetParameter": "添加预设参数...",
"strengthMin": "强度最小值",
"strengthMax": "强度最大值",
"strength": "强度",
"clipSkip": "Clip Skip",
"valuePlaceholder": "值",
"add": "添加"
},
"description": {
"noDescription": "无模型描述信息",
"failedToLoad": "加载模型描述失败",
@@ -509,6 +544,22 @@
"description": "正在加载模型描述...",
"recipes": "正在加载配方...",
"examples": "正在加载示例..."
},
"tags": {
"messages": {
"updated": "标签更新成功",
"updateFailed": "更新标签失败"
},
"validation": {
"maxLength": "标签长度不能超过30个字符",
"maxCount": "最多允许30个标签",
"duplicate": "该标签已存在"
}
},
"recipeTab": {
"noRecipesFound": "未找到使用此 LoRA 的配方。",
"loadingRecipes": "正在加载配方...",
"errorLoadingRecipes": "加载配方失败。请稍后重试。"
}
}
},

View File

@@ -74,6 +74,39 @@
"spanish": "Español"
}
},
"modelCard": {
"actions": {
"addToFavorites": "新增到收藏",
"removeFromFavorites": "從收藏中移除",
"viewOnCivitai": "在 Civitai 上檢視",
"notAvailableFromCivitai": "Civitai 上不可用",
"sendToWorkflow": "傳送到 ComfyUI點擊附加Shift+點擊:取代)",
"copyLoRASyntax": "複製 LoRA 語法",
"checkpointNameCopied": "Checkpoint 名稱已複製",
"toggleBlur": "切換模糊",
"show": "顯示",
"openExampleImages": "開啟範例圖片資料夾"
},
"nsfw": {
"matureContent": "成人內容",
"xxxRated": "XXX 級內容",
"xRated": "X 級內容",
"rRated": "R 級內容"
},
"favorites": {
"added": "已新增到收藏",
"removed": "已從收藏中移除",
"updateFailed": "更新收藏狀態失敗"
},
"sendToWorkflow": {
"checkpointNotImplemented": "傳送 Checkpoint 到工作流程 - 功能待實作"
},
"exampleImages": {
"checkError": "檢查範例圖片時出錯",
"missingHash": "缺少模型雜湊資訊。",
"noRemoteImagesAvailable": "該模型在 Civitai 上沒有可用的遠端範例圖片"
}
},
"header": {
"appTitle": "LoRA 管理器",
"navigation": {
@@ -341,6 +374,77 @@
"updated": "模型描述已更新",
"updateFailed": "更新模型描述失敗"
}
},
"actions": {
"editModelName": "編輯模型名稱",
"editFileName": "編輯檔案名稱",
"editBaseModel": "編輯基礎模型",
"viewOnCivitai": "在 Civitai 上檢視",
"viewOnCivitaiText": "在 Civitai 上檢視",
"viewCreatorProfile": "檢視創作者資料"
},
"metadata": {
"version": "版本",
"fileName": "檔案名稱",
"location": "位置",
"baseModel": "基礎模型",
"size": "大小",
"unknown": "未知",
"usageTips": "使用技巧",
"additionalNotes": "附加說明",
"notesHint": "按 Enter 儲存Shift+Enter 換行",
"addNotesPlaceholder": "在此新增您的說明...",
"aboutThisVersion": "關於此版本",
"validation": {
"nameTooLong": "模型名稱最多100個字元",
"nameEmpty": "模型名稱不能為空"
},
"messages": {
"nameUpdated": "模型名稱更新成功",
"nameUpdateFailed": "更新模型名稱失敗",
"baseModelUpdated": "基礎模型更新成功",
"baseModelUpdateFailed": "更新基礎模型失敗"
}
},
"notes": {
"saved": "說明儲存成功",
"saveFailed": "儲存說明失敗"
},
"usageTips": {
"addPresetParameter": "新增預設參數...",
"strengthMin": "強度最小值",
"strengthMax": "強度最大值",
"strength": "強度",
"clipSkip": "Clip Skip",
"valuePlaceholder": "值",
"add": "新增"
},
"tabs": {
"examples": "範例圖片",
"description": "模型描述",
"recipes": "配方"
},
"loading": {
"exampleImages": "正在載入範例圖片...",
"description": "正在載入模型描述...",
"recipes": "正在載入配方...",
"examples": "正在載入範例..."
},
"tags": {
"messages": {
"updated": "標籤更新成功",
"updateFailed": "更新標籤失敗"
},
"validation": {
"maxLength": "標籤長度不能超過30個字元",
"maxCount": "最多允許30個標籤",
"duplicate": "該標籤已存在"
}
},
"recipeTab": {
"noRecipesFound": "未找到使用此 LoRA 的配方。",
"loadingRecipes": "正在載入配方...",
"errorLoadingRecipes": "載入配方失敗。請稍後重試。"
}
}
},
@@ -464,4 +568,4 @@
"sort": "按不同屬性排序模型",
"backToTop": "捲動回頁面頂部"
}
}
}

View File

@@ -8,7 +8,7 @@ import { NSFW_LEVELS } from '../../utils/constants.js';
import { MODEL_TYPES } from '../../api/apiConfig.js';
import { getModelApiClient } from '../../api/modelApiFactory.js';
import { showDeleteModal } from '../../utils/modalUtils.js';
import { safeTranslate } from '../../utils/i18nHelpers.js';
import { translate } from '../../utils/i18nHelpers.js';
// Add global event delegation handlers
export function setupModelCardEventDelegation(modelType) {
@@ -143,15 +143,15 @@ async function toggleFavorite(card) {
});
if (newFavoriteState) {
const addedText = safeTranslate('modelCard.favorites.added', {}, 'Added to favorites');
const addedText = translate('modelCard.favorites.added', {}, 'Added to favorites');
showToast(addedText, 'success');
} else {
const removedText = safeTranslate('modelCard.favorites.removed', {}, 'Removed from favorites');
const removedText = translate('modelCard.favorites.removed', {}, 'Removed from favorites');
showToast(removedText, 'success');
}
} catch (error) {
console.error('Failed to update favorite status:', error);
const errorText = safeTranslate('modelCard.favorites.updateFailed', {}, 'Failed to update favorite status');
const errorText = translate('modelCard.favorites.updateFailed', {}, 'Failed to update favorite status');
showToast(errorText, 'error');
}
}
@@ -164,7 +164,7 @@ function handleSendToWorkflow(card, replaceMode, modelType) {
sendLoraToWorkflow(loraSyntax, replaceMode, 'lora');
} else {
// Checkpoint send functionality - to be implemented
const text = safeTranslate('modelCard.sendToWorkflow.checkpointNotImplemented', {}, 'Send checkpoint to workflow - feature to be implemented');
const text = translate('modelCard.sendToWorkflow.checkpointNotImplemented', {}, 'Send checkpoint to workflow - feature to be implemented');
showToast(text, 'info');
}
}
@@ -175,7 +175,8 @@ function handleCopyAction(card, modelType) {
} else if (modelType === MODEL_TYPES.CHECKPOINT) {
// Checkpoint copy functionality - copy checkpoint name
const checkpointName = card.dataset.file_name;
copyToClipboard(checkpointName, 'Checkpoint name copied');
const message = translate('modelCard.actions.checkpointNameCopied', {}, 'Checkpoint name copied');
copyToClipboard(checkpointName, message);
} else if (modelType === MODEL_TYPES.EMBEDDING) {
const embeddingName = card.dataset.file_name;
copyToClipboard(embeddingName, 'Embedding name copied');
@@ -200,7 +201,7 @@ async function handleExampleImagesAccess(card, modelType) {
}
} catch (error) {
console.error('Error checking for example images:', error);
const text = safeTranslate('modelCard.exampleImages.checkError', {}, 'Error checking for example images');
const text = translate('modelCard.exampleImages.checkError', {}, 'Error checking for example images');
showToast(text, 'error');
}
}
@@ -283,7 +284,7 @@ function showExampleAccessModal(card, modelType) {
// Get the model hash
const modelHash = card.dataset.sha256;
if (!modelHash) {
const text = safeTranslate('modelCard.exampleImages.missingHash', {}, 'Missing model hash information.');
const text = translate('modelCard.exampleImages.missingHash', {}, 'Missing model hash information.');
showToast(text, 'error');
return;
}
@@ -305,7 +306,8 @@ function showExampleAccessModal(card, modelType) {
};
} else {
downloadBtn.classList.add('disabled');
downloadBtn.setAttribute('title', 'No remote example images available for this model on Civitai');
const noRemoteImagesTitle = translate('modelCard.exampleImages.noRemoteImagesAvailable', {}, 'No remote example images available for this model on Civitai');
downloadBtn.setAttribute('title', noRemoteImagesTitle);
downloadBtn.onclick = null;
}
}
@@ -436,14 +438,14 @@ export function createModelCard(model, modelType) {
const previewUrl = model.preview_url || '/loras_static/images/no-preview.png';
const versionedPreviewUrl = version ? `${previewUrl}?t=${version}` : previewUrl;
// Determine NSFW warning text based on level
let nsfwText = "Mature Content";
// Determine NSFW warning text based on level with i18n support
let nsfwText = translate('modelCard.nsfw.matureContent', {}, 'Mature Content');
if (nsfwLevel >= NSFW_LEVELS.XXX) {
nsfwText = "XXX-rated Content";
nsfwText = translate('modelCard.nsfw.xxxRated', {}, 'XXX-rated Content');
} else if (nsfwLevel >= NSFW_LEVELS.X) {
nsfwText = "X-rated Content";
nsfwText = translate('modelCard.nsfw.xRated', {}, 'X-rated Content');
} else if (nsfwLevel >= NSFW_LEVELS.R) {
nsfwText = "R-rated Content";
nsfwText = translate('modelCard.nsfw.rRated', {}, 'R-rated Content');
}
// Check if autoplayOnHover is enabled for video previews
@@ -454,22 +456,36 @@ export function createModelCard(model, modelType) {
// Get favorite status from model data
const isFavorite = model.favorite === true;
// Generate action icons based on model type
// Generate action icons based on model type with i18n support
const favoriteTitle = isFavorite ?
translate('modelCard.actions.removeFromFavorites', {}, 'Remove from favorites') :
translate('modelCard.actions.addToFavorites', {}, 'Add to favorites');
const globeTitle = model.from_civitai ?
translate('modelCard.actions.viewOnCivitai', {}, 'View on Civitai') :
translate('modelCard.actions.notAvailableFromCivitai', {}, 'Not available from Civitai');
const sendTitle = translate('modelCard.actions.sendToWorkflow', {}, 'Send to ComfyUI (Click: Append, Shift+Click: Replace)');
const copyTitle = translate('modelCard.actions.copyLoRASyntax', {}, 'Copy LoRA Syntax');
const actionIcons = `
<i class="${isFavorite ? 'fas fa-star favorite-active' : 'far fa-star'}"
title="${isFavorite ? 'Remove from favorites' : 'Add to favorites'}">
title="${favoriteTitle}">
</i>
<i class="fas fa-globe"
title="${model.from_civitai ? 'View on Civitai' : 'Not available from Civitai'}"
title="${globeTitle}"
${!model.from_civitai ? 'style="opacity: 0.5; cursor: not-allowed"' : ''}>
</i>
<i class="fas fa-paper-plane"
title="Send to ComfyUI (Click: Append, Shift+Click: Replace)">
title="${sendTitle}">
</i>
<i class="fas fa-copy"
title="Copy LoRA Syntax">
title="${copyTitle}">
</i>`;
// Generate UI text with i18n support
const toggleBlurTitle = translate('modelCard.actions.toggleBlur', {}, 'Toggle blur');
const showButtonText = translate('modelCard.actions.show', {}, 'Show');
const openExampleImagesTitle = translate('modelCard.actions.openExampleImages', {}, 'Open Example Images Folder');
card.innerHTML = `
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
${isVideo ?
@@ -480,7 +496,7 @@ export function createModelCard(model, modelType) {
}
<div class="card-header">
${shouldBlur ?
`<button class="toggle-blur-btn" title="Toggle blur">
`<button class="toggle-blur-btn" title="${toggleBlurTitle}">
<i class="fas fa-eye"></i>
</button>` : ''}
<span class="base-model-label ${shouldBlur ? 'with-toggle' : ''}" title="${model.base_model}">
@@ -494,7 +510,7 @@ export function createModelCard(model, modelType) {
<div class="nsfw-overlay">
<div class="nsfw-warning">
<p>${nsfwText}</p>
<button class="show-content-btn">Show</button>
<button class="show-content-btn">${showButtonText}</button>
</div>
</div>
` : ''}
@@ -505,7 +521,7 @@ export function createModelCard(model, modelType) {
</div>
<div class="card-actions">
<i class="fas fa-folder-open"
title="Open Example Images Folder">
title="${openExampleImagesTitle}">
</i>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { showToast } from '../../utils/uiHelpers.js';
import { safeTranslate } from '../../utils/i18nHelpers.js';
import { translate } from '../../utils/i18nHelpers.js';
/**
* ModelDescription.js
@@ -63,7 +63,7 @@ async function loadModelDescription() {
const description = await getModelApiClient().fetchModelDescription(filePath);
// Update content
const noDescriptionText = safeTranslate('modals.model.description.noDescription', {}, 'No model description available');
const noDescriptionText = translate('modals.model.description.noDescription', {}, 'No model description available');
descriptionContent.innerHTML = description || `<div class="no-description">${noDescriptionText}</div>`;
descriptionContent.dataset.loaded = 'true';
@@ -72,7 +72,7 @@ async function loadModelDescription() {
} catch (error) {
console.error('Error loading model description:', error);
const failedText = safeTranslate('modals.model.description.failedToLoad', {}, 'Failed to load model description');
const failedText = translate('modals.model.description.failedToLoad', {}, 'Failed to load model description');
descriptionContent.innerHTML = `<div class="no-description">${failedText}</div>`;
} finally {
// Hide loading state
@@ -96,7 +96,7 @@ export async function setupModelDescriptionEditing(filePath) {
editBtn = document.createElement('button');
editBtn.className = 'edit-model-description-btn';
// Set title using i18n
const editTitle = safeTranslate('modals.model.description.editTitle', {}, 'Edit model description');
const editTitle = translate('modals.model.description.editTitle', {}, 'Edit model description');
editBtn.title = editTitle;
editBtn.innerHTML = '<i class="fas fa-pencil-alt"></i>';
descContainer.insertBefore(editBtn, descContent);
@@ -154,7 +154,7 @@ export async function setupModelDescriptionEditing(filePath) {
}
if (!newValue) {
this.innerHTML = originalValue;
const emptyErrorText = safeTranslate('modals.model.description.validation.cannotBeEmpty', {}, 'Description cannot be empty');
const emptyErrorText = translate('modals.model.description.validation.cannotBeEmpty', {}, 'Description cannot be empty');
showToast(emptyErrorText, 'error');
exitEditMode();
return;
@@ -163,11 +163,11 @@ export async function setupModelDescriptionEditing(filePath) {
// Save to backend
const { getModelApiClient } = await import('../../api/modelApiFactory.js');
await getModelApiClient().saveModelMetadata(filePath, { modelDescription: newValue });
const successText = safeTranslate('modals.model.description.messages.updated', {}, 'Model description updated');
const successText = translate('modals.model.description.messages.updated', {}, 'Model description updated');
showToast(successText, 'success');
} catch (err) {
this.innerHTML = originalValue;
const errorText = safeTranslate('modals.model.description.messages.updateFailed', {}, 'Failed to update model description');
const errorText = translate('modals.model.description.messages.updateFailed', {}, 'Failed to update model description');
showToast(errorText, 'error');
} finally {
exitEditMode();

View File

@@ -5,7 +5,7 @@
import { showToast } from '../../utils/uiHelpers.js';
import { BASE_MODELS } from '../../utils/constants.js';
import { getModelApiClient } from '../../api/modelApiFactory.js';
import { safeTranslate } from '../../utils/i18nHelpers.js';
import { translate } from '../../utils/i18nHelpers.js';
/**
* Set up model name editing functionality
@@ -83,7 +83,7 @@ export function setupModelNameEditing(filePath) {
sel.removeAllRanges();
sel.addRange(range);
const text = safeTranslate('modelMetadata.validation.nameTooLong', {}, 'Model name is limited to 100 characters');
const text = translate('modelMetadata.validation.nameTooLong', {}, 'Model name is limited to 100 characters');
showToast(text, 'warning');
}
});

View File

@@ -18,7 +18,7 @@ import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
import { renderTriggerWords, setupTriggerWordsEditMode } from './TriggerWords.js';
import { parsePresets, renderPresetTags } from './PresetTags.js';
import { loadRecipesForLora } from './RecipeTab.js';
import { safeTranslate } from '../../utils/i18nHelpers.js';
import { translate } from '../../utils/i18nHelpers.js';
/**
* Display the model modal with the given model data
@@ -62,9 +62,9 @@ export async function showModelModal(model, modelType) {
}
// Generate tabs based on model type
const examplesText = safeTranslate('modals.model.tabs.examples', {}, 'Examples');
const descriptionText = safeTranslate('modals.model.tabs.description', {}, 'Model Description');
const recipesText = safeTranslate('modals.model.tabs.recipes', {}, 'Recipes');
const examplesText = translate('modals.model.tabs.examples', {}, 'Examples');
const descriptionText = translate('modals.model.tabs.description', {}, 'Model Description');
const recipesText = translate('modals.model.tabs.recipes', {}, 'Recipes');
const tabsContent = modelType === 'loras' ?
`<button class="tab-btn active" data-tab="showcase">${examplesText}</button>
@@ -73,10 +73,10 @@ export async function showModelModal(model, modelType) {
`<button class="tab-btn active" data-tab="showcase">${examplesText}</button>
<button class="tab-btn" data-tab="description">${descriptionText}</button>`;
const loadingExampleImagesText = safeTranslate('modals.model.loading.exampleImages', {}, 'Loading example images...');
const loadingDescriptionText = safeTranslate('modals.model.loading.description', {}, 'Loading model description...');
const loadingRecipesText = safeTranslate('modals.model.loading.recipes', {}, 'Loading recipes...');
const loadingExamplesText = safeTranslate('modals.model.loading.examples', {}, 'Loading examples...');
const loadingExampleImagesText = translate('modals.model.loading.exampleImages', {}, 'Loading example images...');
const loadingDescriptionText = translate('modals.model.loading.description', {}, 'Loading model description...');
const loadingRecipesText = translate('modals.model.loading.recipes', {}, 'Loading recipes...');
const loadingExamplesText = translate('modals.model.loading.examples', {}, 'Loading examples...');
const tabPanesContent = modelType === 'loras' ?
`<div id="showcase-tab" class="tab-pane active">
@@ -122,19 +122,19 @@ export async function showModelModal(model, modelType) {
<header class="modal-header">
<div class="model-name-header">
<h2 class="model-name-content">${modalTitle}</h2>
<button class="edit-model-name-btn" title="Edit model name">
<button class="edit-model-name-btn" title="${translate('modals.model.actions.editModelName', {}, 'Edit model name')}">
<i class="fas fa-pencil-alt"></i>
</button>
</div>
<div class="creator-actions">
${modelWithFullData.from_civitai ? `
<div class="civitai-view" title="View on Civitai" data-action="view-civitai" data-filepath="${modelWithFullData.file_path}">
<i class="fas fa-globe"></i> View on Civitai
<div class="civitai-view" title="${translate('modals.model.actions.viewOnCivitai', {}, 'View on Civitai')}" data-action="view-civitai" data-filepath="${modelWithFullData.file_path}">
<i class="fas fa-globe"></i> ${translate('modals.model.actions.viewOnCivitaiText', {}, 'View on Civitai')}
</div>` : ''}
${modelWithFullData.civitai?.creator ? `
<div class="creator-info" data-username="${modelWithFullData.civitai.creator.username}" data-action="view-creator" title="View Creator Profile">
<div class="creator-info" data-username="${modelWithFullData.civitai.creator.username}" data-action="view-creator" title="${translate('modals.model.actions.viewCreatorProfile', {}, 'View Creator Profile')}">
${modelWithFullData.civitai.creator.image ?
`<div class="creator-avatar">
<img src="${modelWithFullData.civitai.creator.image}" alt="${modelWithFullData.civitai.creator.username}" onerror="this.onerror=null; this.src='static/icons/user-placeholder.png';">
@@ -154,48 +154,48 @@ export async function showModelModal(model, modelType) {
<div class="info-section">
<div class="info-grid">
<div class="info-item">
<label>Version</label>
<label>${translate('modals.model.metadata.version', {}, 'Version')}</label>
<span>${modelWithFullData.civitai?.name || 'N/A'}</span>
</div>
<div class="info-item">
<label>File Name</label>
<label>${translate('modals.model.metadata.fileName', {}, 'File Name')}</label>
<div class="file-name-wrapper">
<span id="file-name" class="file-name-content">${modelWithFullData.file_name || 'N/A'}</span>
<button class="edit-file-name-btn" title="Edit file name">
<button class="edit-file-name-btn" title="${translate('modals.model.actions.editFileName', {}, 'Edit file name')}">
<i class="fas fa-pencil-alt"></i>
</button>
</div>
</div>
<div class="info-item location-size">
<div class="location-wrapper">
<label>Location</label>
<label>${translate('modals.model.metadata.location', {}, 'Location')}</label>
<span class="file-path">${modelWithFullData.file_path.replace(/[^/]+$/, '') || 'N/A'}</span>
</div>
</div>
<div class="info-item base-size">
<div class="base-wrapper">
<label>Base Model</label>
<label>${translate('modals.model.metadata.baseModel', {}, 'Base Model')}</label>
<div class="base-model-display">
<span class="base-model-content">${modelWithFullData.base_model || 'Unknown'}</span>
<button class="edit-base-model-btn" title="Edit base model">
<span class="base-model-content">${modelWithFullData.base_model || translate('modals.model.metadata.unknown', {}, 'Unknown')}</span>
<button class="edit-base-model-btn" title="${translate('modals.model.actions.editBaseModel', {}, 'Edit base model')}">
<i class="fas fa-pencil-alt"></i>
</button>
</div>
</div>
<div class="size-wrapper">
<label>Size</label>
<label>${translate('modals.model.metadata.size', {}, 'Size')}</label>
<span>${formatFileSize(modelWithFullData.file_size)}</span>
</div>
</div>
${typeSpecificContent}
<div class="info-item notes">
<label>Additional Notes <i class="fas fa-info-circle notes-hint" title="Press Enter to save, Shift+Enter for new line"></i></label>
<label>${translate('modals.model.metadata.additionalNotes', {}, 'Additional Notes')} <i class="fas fa-info-circle notes-hint" title="${translate('modals.model.metadata.notesHint', {}, 'Press Enter to save, Shift+Enter for new line')}"></i></label>
<div class="editable-field">
<div class="notes-content" contenteditable="true" spellcheck="false">${modelWithFullData.notes || 'Add your notes here...'}</div>
<div class="notes-content" contenteditable="true" spellcheck="false">${modelWithFullData.notes || translate('modals.model.metadata.addNotesPlaceholder', {}, 'Add your notes here...')}</div>
</div>
</div>
<div class="info-item full-width">
<label>About this version</label>
<label>${translate('modals.model.metadata.aboutThisVersion', {}, 'About this version')}</label>
<div class="description-text">${modelWithFullData.civitai?.description || 'N/A'}</div>
</div>
</div>
@@ -259,18 +259,18 @@ export async function showModelModal(model, modelType) {
function renderLoraSpecificContent(lora, escapedWords) {
return `
<div class="info-item usage-tips">
<label>Usage Tips</label>
<label>${translate('modals.model.metadata.usageTips', {}, 'Usage Tips')}</label>
<div class="editable-field">
<div class="preset-controls">
<select id="preset-selector">
<option value="">Add preset parameter...</option>
<option value="strength_min">Strength Min</option>
<option value="strength_max">Strength Max</option>
<option value="strength">Strength</option>
<option value="clip_skip">Clip Skip</option>
<option value="">${translate('modals.model.usageTips.addPresetParameter', {}, 'Add preset parameter...')}</option>
<option value="strength_min">${translate('modals.model.usageTips.strengthMin', {}, 'Strength Min')}</option>
<option value="strength_max">${translate('modals.model.usageTips.strengthMax', {}, 'Strength Max')}</option>
<option value="strength">${translate('modals.model.usageTips.strength', {}, 'Strength')}</option>
<option value="clip_skip">${translate('modals.model.usageTips.clipSkip', {}, 'Clip Skip')}</option>
</select>
<input type="number" id="preset-value" step="0.01" placeholder="Value" style="display:none;">
<button class="add-preset-btn">Add</button>
<input type="number" id="preset-value" step="0.01" placeholder="${translate('modals.model.usageTips.valuePlaceholder', {}, 'Value')}" style="display:none;">
<button class="add-preset-btn">${translate('modals.model.usageTips.add', {}, 'Add')}</button>
</div>
<div class="preset-tags">
${renderPresetTags(parsePresets(lora.usage_tips))}
@@ -438,9 +438,11 @@ async function saveNotes(filePath) {
try {
await getModelApiClient().saveModelMetadata(filePath, { notes: content });
showToast('Notes saved successfully', 'success');
const successMessage = translate('modals.model.notes.saved', {}, 'Notes saved successfully');
showToast(successMessage, 'success');
} catch (error) {
showToast('Failed to save notes', 'error');
const errorMessage = translate('modals.model.notes.saveFailed', {}, 'Failed to save notes');
showToast(errorMessage, 'error');
}
}

View File

@@ -4,7 +4,7 @@
*/
import { showToast } from '../../utils/uiHelpers.js';
import { getModelApiClient } from '../../api/modelApiFactory.js';
import { safeTranslate } from '../../utils/i18nHelpers.js';
import { translate } from '../../utils/i18nHelpers.js';
// Preset tag suggestions
const PRESET_TAGS = [
@@ -217,10 +217,10 @@ async function saveTags() {
// Exit edit mode
editBtn.click();
showToast(safeTranslate('modelTags.messages.updated', {}, 'Tags updated successfully'), 'success');
showToast(translate('modelTags.messages.updated', {}, 'Tags updated successfully'), 'success');
} catch (error) {
console.error('Error saving tags:', error);
showToast(safeTranslate('modelTags.messages.updateFailed', {}, 'Failed to update tags'), 'error');
showToast(translate('modelTags.messages.updateFailed', {}, 'Failed to update tags'), 'error');
}
}
@@ -362,7 +362,7 @@ function addNewTag(tag) {
// Validation: Check length
if (tag.length > 30) {
const text = safeTranslate('modelTags.validation.maxLength', {}, 'Tag should not exceed 30 characters');
const text = translate('modelTags.validation.maxLength', {}, 'Tag should not exceed 30 characters');
showToast(text, 'error');
return;
}
@@ -370,7 +370,7 @@ function addNewTag(tag) {
// Validation: Check total number
const currentTags = tagsContainer.querySelectorAll('.metadata-item');
if (currentTags.length >= 30) {
const text = safeTranslate('modelTags.validation.maxCount', {}, 'Maximum 30 tags allowed');
const text = translate('modelTags.validation.maxCount', {}, 'Maximum 30 tags allowed');
showToast(text, 'error');
return;
}
@@ -378,7 +378,7 @@ function addNewTag(tag) {
// Validation: Check for duplicates
const existingTags = Array.from(currentTags).map(tag => tag.dataset.tag);
if (existingTags.includes(tag)) {
const text = safeTranslate('modelTags.validation.duplicate', {}, 'This tag already exists');
const text = translate('modelTags.validation.duplicate', {}, 'This tag already exists');
showToast(text, 'error');
return;
}

View File

@@ -22,29 +22,6 @@ export function translate(key, params = {}, fallback = null) {
return translation;
}
/**
* Safe translation function. Assumes i18n is already ready.
* @param {string} key - Translation key
* @param {Object} params - Parameters for interpolation
* @param {string} fallback - Fallback text if translation fails
* @returns {string} Translated text
*/
export function safeTranslate(key, params = {}, fallback = null) {
if (!window.i18n) {
console.warn('i18n not available');
return fallback || key;
}
const translation = window.i18n.t(key, params);
// If translation returned the key (meaning not found), use fallback
if (translation === key && fallback) {
return fallback;
}
return translation;
}
/**
* Update element text with translation
* @param {HTMLElement|string} element - Element or selector
@@ -56,7 +33,7 @@ export function updateElementText(element, key, params = {}, fallback = null) {
const el = typeof element === 'string' ? document.querySelector(element) : element;
if (!el) return;
const text = safeTranslate(key, params, fallback);
const text = translate(key, params, fallback);
el.textContent = text;
}
@@ -72,7 +49,7 @@ export function updateElementAttribute(element, attribute, key, params = {}, fal
const el = typeof element === 'string' ? document.querySelector(element) : element;
if (!el) return;
const text = safeTranslate(key, params, fallback);
const text = translate(key, params, fallback);
el.setAttribute(attribute, text);
}

View File

@@ -1,14 +1,22 @@
import { translate } from './i18nHelpers.js';
import { state, getCurrentPageState } from '../state/index.js';
import { getStorageItem, setStorageItem } from './storageHelpers.js';
import { NODE_TYPE_ICONS, DEFAULT_NODE_COLOR } from './constants.js';
/**
* Utility function to copy text to clipboard with fallback for older browsers
* @param {string} text - The text to copy to clipboard
* @param {string} successMessage - Optional success message to show in toast
* @returns {Promise<boolean>} - Promise that resolves to true if copy was successful
/**
* Utility function to copy text to clipboard with fallback for older browsers
* @param {string} text - The text to copy to clipboard
* @param {string} successMessage - Optional success message to show in toast
* @returns {Promise<boolean>} - Promise that resolves to true if copy was successful
*/
export async function copyToClipboard(text, successMessage = 'Copied to clipboard') {
export async function copyToClipboard(text, successMessage = null) {
const defaultSuccessMessage = successMessage || translate('uiHelpers.clipboard.copied', {}, 'Copied to clipboard');
try {
// Modern clipboard API
if (navigator.clipboard && window.isSecureContext) {
@@ -25,13 +33,14 @@ export async function copyToClipboard(text, successMessage = 'Copied to clipboar
document.body.removeChild(textarea);
}
if (successMessage) {
showToast(successMessage, 'success');
if (defaultSuccessMessage) {
showToast(defaultSuccessMessage, 'success');
}
return true;
} catch (err) {
console.error('Copy failed:', err);
showToast('Copy failed', 'error');
const errorMessage = translate('uiHelpers.clipboard.copyFailed', {}, 'Copy failed');
showToast(errorMessage, 'error');
return false;
}
}
@@ -294,7 +303,8 @@ export function copyLoraSyntax(card) {
const includeTriggerWords = state.global.settings.includeTriggerWords;
if (!includeTriggerWords) {
copyToClipboard(baseSyntax, "LoRA syntax copied to clipboard");
const message = translate('uiHelpers.lora.syntaxCopied', {}, 'LoRA syntax copied to clipboard');
copyToClipboard(baseSyntax, message);
return;
}
@@ -307,10 +317,8 @@ export function copyLoraSyntax(card) {
!Array.isArray(trainedWords) ||
trainedWords.length === 0
) {
copyToClipboard(
baseSyntax,
"LoRA syntax copied to clipboard (no trigger words found)"
);
const message = translate('uiHelpers.lora.syntaxCopiedNoTriggerWords', {}, 'LoRA syntax copied to clipboard (no trigger words found)');
copyToClipboard(baseSyntax, message);
return;
}
@@ -325,10 +333,8 @@ export function copyLoraSyntax(card) {
if (triggers.length > 0) {
finalSyntax = `${baseSyntax}, ${triggers.join(", ")}`;
}
copyToClipboard(
finalSyntax,
"LoRA syntax with trigger words copied to clipboard"
);
const message = translate('uiHelpers.lora.syntaxCopiedWithTriggerWords', {}, 'LoRA syntax with trigger words copied to clipboard');
copyToClipboard(finalSyntax, message);
} else {
// Multiple groups: format with separators
const groups = trainedWords
@@ -348,10 +354,8 @@ export function copyLoraSyntax(card) {
finalSyntax += `\n${"-".repeat(17)}\n${groups[i]}`;
}
}
copyToClipboard(
finalSyntax,
"LoRA syntax with trigger word groups copied to clipboard"
);
const message = translate('uiHelpers.lora.syntaxCopiedWithTriggerWordGroups', {}, 'LoRA syntax with trigger word groups copied to clipboard');
copyToClipboard(finalSyntax, message);
}
}
@@ -384,7 +388,8 @@ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntax
// Success case - check node count
if (registryData.data.node_count === 0) {
// No nodes found - show warning
showToast('No supported target nodes found in workflow', 'warning');
const message = translate('uiHelpers.workflow.noSupportedNodes', {}, 'No supported target nodes found in workflow');
showToast(message, 'warning');
return false;
} else if (registryData.data.node_count > 1) {
// Multiple nodes - show selector
@@ -397,7 +402,8 @@ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntax
}
} catch (error) {
console.error('Failed to get registry:', error);
showToast('Failed to communicate with ComfyUI', 'error');
const message = translate('uiHelpers.workflow.communicationFailed', {}, 'Failed to communicate with ComfyUI');
showToast(message, 'error');
return false;
}
}
@@ -429,18 +435,31 @@ async function sendToSpecificNode(nodeIds, loraSyntax, replaceMode, syntaxType)
if (result.success) {
// Use different toast messages based on syntax type
if (syntaxType === 'recipe') {
showToast(`Recipe ${replaceMode ? 'replaced' : 'added'} to workflow`, 'success');
const message = replaceMode ?
translate('uiHelpers.workflow.recipeReplaced', {}, 'Recipe replaced in workflow') :
translate('uiHelpers.workflow.recipeAdded', {}, 'Recipe added to workflow');
showToast(message, 'success');
} else {
showToast(`LoRA ${replaceMode ? 'replaced' : 'added'} to workflow`, 'success');
const message = replaceMode ?
translate('uiHelpers.workflow.loraReplaced', {}, 'LoRA replaced in workflow') :
translate('uiHelpers.workflow.loraAdded', {}, 'LoRA added to workflow');
showToast(message, 'success');
}
return true;
} else {
showToast(result.error || `Failed to send ${syntaxType === 'recipe' ? 'recipe' : 'LoRA'} to workflow`, 'error');
const errorMessage = result.error ||
(syntaxType === 'recipe' ?
translate('uiHelpers.workflow.recipeFailedToSend', {}, 'Failed to send recipe to workflow') :
translate('uiHelpers.workflow.loraFailedToSend', {}, 'Failed to send LoRA to workflow'));
showToast(errorMessage, 'error');
return false;
}
} catch (error) {
console.error('Failed to send to workflow:', error);
showToast(`Failed to send ${syntaxType === 'recipe' ? 'recipe' : 'LoRA'} to workflow`, 'error');
const message = syntaxType === 'recipe' ?
translate('uiHelpers.workflow.recipeFailedToSend', {}, 'Failed to send recipe to workflow') :
translate('uiHelpers.workflow.loraFailedToSend', {}, 'Failed to send LoRA to workflow');
showToast(message, 'error');
return false;
}
}
@@ -482,20 +501,26 @@ function showNodeSelector(nodes, loraSyntax, replaceMode, syntaxType) {
}).join('');
// Add header with action mode indicator
const actionType = syntaxType === 'recipe' ? 'Recipe' : 'LoRA';
const actionMode = replaceMode ? 'Replace' : 'Append';
const actionType = syntaxType === 'recipe' ?
translate('uiHelpers.nodeSelector.recipe', {}, 'Recipe') :
translate('uiHelpers.nodeSelector.lora', {}, 'LoRA');
const actionMode = replaceMode ?
translate('uiHelpers.nodeSelector.replace', {}, 'Replace') :
translate('uiHelpers.nodeSelector.append', {}, 'Append');
const selectTargetNodeText = translate('uiHelpers.nodeSelector.selectTargetNode', {}, 'Select target node');
const sendToAllText = translate('uiHelpers.nodeSelector.sendToAll', {}, 'Send to All');
selector.innerHTML = `
<div class="node-selector-header">
<span class="selector-action-type">${actionMode} ${actionType}</span>
<span class="selector-instruction">Select target node</span>
<span class="selector-instruction">${selectTargetNodeText}</span>
</div>
${nodeItems}
<div class="node-item send-all-item" data-action="send-all">
<div class="node-icon-indicator all-nodes">
<i class="fas fa-broadcast-tower"></i>
</div>
<span>Send to All</span>
<span>${sendToAllText}</span>
</div>
`;
@@ -654,15 +679,18 @@ export async function openExampleImagesFolder(modelHash) {
const result = await response.json();
if (result.success) {
showToast('Opening example images folder', 'success');
const message = translate('uiHelpers.exampleImages.openingFolder', {}, 'Opening example images folder');
showToast(message, 'success');
return true;
} else {
showToast(result.error || 'Failed to open example images folder', 'error');
const message = result.error || translate('uiHelpers.exampleImages.failedToOpen', {}, 'Failed to open example images folder');
showToast(message, 'error');
return false;
}
} catch (error) {
console.error('Failed to open example images folder:', error);
showToast('Failed to open example images folder', 'error');
const message = translate('uiHelpers.exampleImages.failedToOpen', {}, 'Failed to open example images folder');
showToast(message, 'error');
return false;
}
}