From 59010ca431ea3301cfe9366403f3e0009078ea5a Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Sun, 31 Aug 2025 11:19:06 +0800 Subject: [PATCH] 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. --- locales/de.json | 95 ++++++++++- locales/en.json | 161 +++++++++++++----- locales/es.json | 95 ++++++++++- locales/fr.json | 95 ++++++++++- locales/ja.json | 95 ++++++++++- locales/ko.json | 95 ++++++++++- locales/ru.json | 95 ++++++++++- locales/zh-CN.json | 127 +++++++++----- locales/zh-TW.json | 106 +++++++++++- static/js/components/shared/ModelCard.js | 60 ++++--- .../js/components/shared/ModelDescription.js | 14 +- static/js/components/shared/ModelMetadata.js | 4 +- static/js/components/shared/ModelModal.js | 68 ++++---- static/js/components/shared/ModelTags.js | 12 +- static/js/utils/i18nHelpers.js | 27 +-- static/js/utils/uiHelpers.js | 88 ++++++---- 16 files changed, 1029 insertions(+), 208 deletions(-) diff --git a/locales/de.json b/locales/de.json index a7503fd7..be1b16de 100644 --- a/locales/de.json +++ b/locales/de.json @@ -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" + } } -} +} \ No newline at end of file diff --git a/locales/en.json b/locales/en.json index 6f2818cd..5935f785 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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", diff --git a/locales/es.json b/locales/es.json index 7e27dfa7..2553f491 100644 --- a/locales/es.json +++ b/locales/es.json @@ -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" + } } -} +} \ No newline at end of file diff --git a/locales/fr.json b/locales/fr.json index 8fe572b0..ef63ffc5 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -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" + } } -} +} \ No newline at end of file diff --git a/locales/ja.json b/locales/ja.json index 11626949..058c1fd3 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -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で利用できません" + } } -} +} \ No newline at end of file diff --git a/locales/ko.json b/locales/ko.json index aed19d78..625a6dab 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -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에서 사용할 수 없습니다" + } } -} +} \ No newline at end of file diff --git a/locales/ru.json b/locales/ru.json index c756111e..f1a37a29 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -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" + } } -} +} \ No newline at end of file diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 49d4533e..63718e4f 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -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": "加载配方失败。请稍后重试。" } } }, diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 601c5d76..4719325d 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -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": "捲動回頁面頂部" } -} +} \ No newline at end of file diff --git a/static/js/components/shared/ModelCard.js b/static/js/components/shared/ModelCard.js index c0951eac..6525f4bd 100644 --- a/static/js/components/shared/ModelCard.js +++ b/static/js/components/shared/ModelCard.js @@ -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 = ` + title="${favoriteTitle}"> + title="${sendTitle}"> + title="${copyTitle}"> `; + // 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 = `
diff --git a/static/js/components/shared/ModelDescription.js b/static/js/components/shared/ModelDescription.js index 3299e75d..5a7a1066 100644 --- a/static/js/components/shared/ModelDescription.js +++ b/static/js/components/shared/ModelDescription.js @@ -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 || `