diff --git a/.github/workflows/frontend-checks.yml b/.github/workflows/frontend-checks.yml
index a80302f6f83..43bbfdec0ac 100644
--- a/.github/workflows/frontend-checks.yml
+++ b/.github/workflows/frontend-checks.yml
@@ -83,20 +83,3 @@ jobs:
if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
run: 'pnpm lint:knip'
shell: bash
-
- - name: translations
- if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
- run: |
- echo "Checking for unused translations..."
- if ! pnpm translations:check; then
- echo ""
- echo "ā Translation file contains unused keys!"
- echo ""
- echo "To fix this, run:"
- echo " pnpm translations:clean"
- echo " pnpm fix"
- echo ""
- echo "Then commit the changes."
- exit 1
- fi
- shell: bash
diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json
index 48eea55f71e..34c979ed9b3 100644
--- a/invokeai/frontend/web/package.json
+++ b/invokeai/frontend/web/package.json
@@ -36,9 +36,7 @@
"build-storybook": "storybook build",
"test": "vitest",
"test:ui": "vitest --coverage --ui",
- "test:no-watch": "vitest --no-watch",
- "translations:check": "node scripts/clean-translations.js public/locales/en.json --mode check",
- "translations:clean": "node scripts/clean-translations.js public/locales/en.json --mode clean"
+ "test:no-watch": "vitest --no-watch"
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.7.4",
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index e07b0025144..974c2125606 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -35,6 +35,8 @@
"deletedPrivateBoardsCannotbeRestored": "Deleted boards and images cannot be restored. Selecting 'Delete Board Only' will move images to a private uncategorized state for the image's creator.",
"uncategorizedImages": "Uncategorized Images",
"deleteAllUncategorizedImages": "Delete All Uncategorized Images",
+ "deletedImagesCannotBeRestored": "Deleted images cannot be restored.",
+ "hideBoards": "Hide Boards",
"loading": "Loading...",
"locateInGalery": "Locate in Gallery",
"menuItemAutoAdd": "Auto-add to this Board",
@@ -53,6 +55,7 @@
"topMessage": "This selection contains images used in the following features:",
"unarchiveBoard": "Unarchive Board",
"uncategorized": "Uncategorized",
+ "viewBoards": "View Boards",
"downloadBoard": "Download Board",
"imagesWithCount_one": "{{count}} image",
"imagesWithCount_other": "{{count}} images",
@@ -132,9 +135,11 @@
"folder": "Folder",
"format": "format",
"githubLabel": "Github",
+ "goTo": "Go to",
"hotkeysLabel": "Hotkeys",
"loadingImage": "Loading Image",
"loadingModel": "Loading Model",
+ "imageFailedToLoad": "Unable to Load Image",
"img2img": "Image To Image",
"inpaint": "inpaint",
"input": "Input",
@@ -143,11 +148,13 @@
"linear": "Linear",
"load": "Load",
"loading": "Loading",
+ "localSystem": "Local System",
"learnMore": "Learn More",
"modelManager": "Model Manager",
"noMatches": "No matches",
"noOptions": "No options",
"nodes": "Workflows",
+ "notInstalled": "Not $t(common.installed)",
"openInNewTab": "Open in New Tab",
"openInViewer": "Open in Viewer",
"orderBy": "Order By",
@@ -171,6 +178,8 @@
"upload": "Upload",
"updated": "Updated",
"created": "Created",
+ "prevPage": "Previous Page",
+ "nextPage": "Next Page",
"unknownError": "Unknown Error",
"red": "Red",
"green": "Green",
@@ -197,6 +206,7 @@
"min": "Min",
"max": "Max",
"values": "Values",
+ "resetToDefaults": "Reset to Defaults",
"seed": "Seed",
"combinatorial": "Combinatorial",
"layout": "Layout",
@@ -212,6 +222,8 @@
},
"hrf": {
"hrf": "High Resolution Fix",
+ "enableHrf": "Enable High Resolution Fix",
+ "upscaleMethod": "Upscale Method",
"metadata": {
"enabled": "High Resolution Fix Enabled",
"strength": "High Resolution Fix Strength",
@@ -226,7 +238,10 @@
"expandCurrentPrompt": "Expand Current Prompt",
"uploadImageForPromptGeneration": "Upload Image for Prompt Generation",
"expandingPrompt": "Expanding prompt...",
+ "resultTitle": "Prompt Expansion Complete",
+ "resultSubtitle": "Choose how to handle the expanded prompt:",
"replace": "Replace",
+ "insert": "Insert",
"discard": "Discard"
},
"queue": {
@@ -365,14 +380,18 @@
"image": "image",
"imagesTab": "Images you've created and saved within Invoke.",
"imagesSettings": "Gallery Images Settings",
+ "jump": "Jump",
"loading": "Loading",
"newestFirst": "Newest First",
"oldestFirst": "Oldest First",
"sortDirection": "Sort Direction",
"showStarredImagesFirst": "Show Starred Images First",
"noImageSelected": "No Image Selected",
+ "noVideoSelected": "No Video Selected",
+ "noImagesInGallery": "No Images to Display",
"starImage": "Star",
"unstarImage": "Unstar",
+ "unableToLoad": "Unable to load Gallery",
"deleteSelection": "Delete Selection",
"downloadSelection": "Download Selection",
"bulkDownloadRequested": "Preparing Download",
@@ -386,6 +405,7 @@
"selectAllOnPage": "Select All On Page",
"showArchivedBoards": "Show Archived Boards",
"selectForCompare": "Select for Compare",
+ "selectAnImageToCompare": "Select an Image to Compare",
"slider": "Slider",
"sideBySide": "Side-by-Side",
"hover": "Hover",
@@ -396,6 +416,8 @@
"compareHelp2": "Press M to cycle through comparison modes.",
"compareHelp3": "Press C to swap the compared images.",
"compareHelp4": "Press Z or Esc to exit.",
+ "openViewer": "Open Viewer",
+ "closeViewer": "Close Viewer",
"move": "Move",
"useForPromptGeneration": "Use for Prompt Generation",
"videos": "Videos",
@@ -793,8 +815,10 @@
"noMetaData": "No metadata found",
"noRecallParameters": "No parameters to recall found",
"parameterSet": "Parameter {{parameter}} set",
+ "parsingFailed": "Parsing Failed",
"positivePrompt": "Positive Prompt",
"recallParameters": "Recall Parameters",
+ "recallParameter": "Recall {{label}}",
"scheduler": "Scheduler",
"seamlessXAxis": "Seamless X Axis",
"seamlessYAxis": "Seamless Y Axis",
@@ -849,6 +873,7 @@
"description": "Description",
"edit": "Edit",
"fileSize": "File Size",
+ "filterModels": "Filter models",
"fluxRedux": "FLUX Redux",
"height": "Height",
"huggingFace": "HuggingFace",
@@ -886,6 +911,7 @@
"installBundle": "Install Bundle",
"installBundleMsg1": "Are you sure you want to install the {{bundleName}} bundle?",
"installBundleMsg2": "This bundle will install the following {{count}} models:",
+ "ipAdapters": "IP Adapters",
"learnMoreAboutSupportedModels": "Learn more about the models we support",
"load": "Load",
"localOnly": "local only",
@@ -922,6 +948,7 @@
"prune": "Prune",
"pruneTooltip": "Prune finished imports from queue",
"relatedModels": "Related Models",
+ "showOnlyRelatedModels": "Related",
"repo_id": "Repo ID",
"repoVariant": "Repo Variant",
"scanFolder": "Scan Folder",
@@ -939,6 +966,7 @@
"starterBundles": "Starter Bundles",
"starterBundleHelpText": "Easily install all models needed to get started with a base model, including a main model, controlnets, IP adapters, and more. Selecting a bundle will skip any models that you already have installed.",
"starterModels": "Starter Models",
+ "starterModelsInModelManager": "Starter Models can be found in Model Manager",
"bundleAlreadyInstalled": "Bundle already installed",
"bundleAlreadyInstalledDesc": "All models in the {{bundleName}} bundle are already installed.",
"launchpadTab": "Launchpad",
@@ -951,8 +979,12 @@
"scanFolderDescription": "Scan a local folder to automatically detect and install models.",
"recommendedModels": "Recommended Models",
"exploreStarter": "Or browse all available starter models",
+ "quickStart": "Quick Start Bundles",
"bundleDescription": "Each bundle includes essential models for each model family and curated base models to get started.",
- "sdxl": "SDXL"
+ "browseAll": "Or browse all available models:",
+ "stableDiffusion15": "Stable Diffusion 1.5",
+ "sdxl": "SDXL",
+ "fluxDev": "FLUX.1 dev"
},
"controlLora": "Control LoRA",
"llavaOnevision": "LLaVA OneVision",
@@ -983,10 +1015,12 @@
"addLora": "Add LoRA",
"concepts": "Concepts",
"loading": "loading",
+ "noMatchingLoRAs": "No matching LoRAs",
"noMatchingModels": "No matching Models",
"noModelsAvailable": "No models available",
"lora": "LoRA",
"selectModel": "Select a Model",
+ "noLoRAsInstalled": "No LoRAs installed",
"noRefinerModelsInstalled": "No SDXL Refiner models installed",
"defaultVAE": "Default VAE",
"noCompatibleLoRAs": "No Compatible LoRAs"
@@ -1004,12 +1038,14 @@
"generatorNRandomValues_one": "{{count}} random value",
"generatorNRandomValues_other": "{{count}} random values",
"generatorNoValues": "empty",
+ "generatorLoading": "loading",
"generatorLoadFromFile": "Load from File",
"generatorImagesFromBoard": "Images from Board",
"dynamicPromptsRandom": "Dynamic Prompts (Random)",
"dynamicPromptsCombinatorial": "Dynamic Prompts (Combinatorial)",
"addNode": "Add Node",
"addNodeToolTip": "Add Node (Shift+A, Space)",
+ "addLinearView": "Add to Linear View",
"animatedEdges": "Animated Edges",
"animatedEdgesHelp": "Animate selected edges and edges connected to selected nodes",
"boolean": "Booleans",
@@ -1049,6 +1085,7 @@
"fullyContainNodesHelp": "Nodes must be fully inside the selection box to be selected",
"showEdgeLabels": "Show Edge Labels",
"showEdgeLabelsHelp": "Show labels on edges, indicating the connected nodes",
+ "hideLegendNodes": "Hide Field Type Legend",
"hideMinimapnodes": "Hide MiniMap",
"inputMayOnlyHaveOneConnection": "Input may only have one connection",
"integer": "Integer",
@@ -1059,6 +1096,7 @@
"noMatchingWorkflows": "No Matching Workflows",
"noWorkflow": "No Workflow",
"unableToUpdateNode": "Node update failed: node {{node}} of type {{type}} (may require deleting and recreating)",
+ "mismatchedVersion": "Invalid node: node {{node}} of type {{type}} has mismatched version (try updating?)",
"missingTemplate": "Invalid node: node {{node}} of type {{type}} missing template (not installed?)",
"sourceNodeDoesNotExist": "Invalid edge: source/output node {{node}} does not exist",
"targetNodeDoesNotExist": "Invalid edge: target/input node {{node}} does not exist",
@@ -1073,6 +1111,7 @@
"nodeTemplate": "Node Template",
"nodeType": "Node Type",
"nodeName": "Node Name",
+ "noFieldsLinearview": "No fields added to Linear View",
"noFieldsViewMode": "This workflow has no selected fields to display. View the full workflow to configure values.",
"workflowHelpText": "Need Help? Check out our guide to Getting Started with Workflows.",
"noNodeSelected": "No node selected",
@@ -1085,6 +1124,8 @@
"problemSettingTitle": "Problem Setting Title",
"resetToDefaultValue": "Reset to default value",
"reloadNodeTemplates": "Reload Node Templates",
+ "removeLinearView": "Remove from Linear View",
+ "reorderLinearView": "Reorder Linear View",
"newWorkflow": "New Workflow",
"newWorkflowDesc": "Create a new workflow?",
"newWorkflowDesc2": "Your current workflow has unsaved changes.",
@@ -1094,10 +1135,12 @@
"clearWorkflowDesc": "Clear this workflow and start a new one?",
"clearWorkflowDesc2": "Your current workflow has unsaved changes.",
"scheduler": "Scheduler",
+ "showLegendNodes": "Show Field Type Legend",
"showMinimapnodes": "Show MiniMap",
"snapToGrid": "Snap to Grid",
"snapToGridHelp": "Snap nodes to grid when moved",
"string": "String",
+ "unableToLoadWorkflow": "Unable to Load Workflow",
"unableToValidateWorkflow": "Unable to Validate Workflow",
"unknownErrorValidatingWorkflow": "Unknown error validating workflow",
"inputFieldTypeParseError": "Unable to parse type of input field {{node}}.{{field}} ({{message}})",
@@ -1112,12 +1155,15 @@
"unknownFieldType": "$t(nodes.unknownField) type: {{type}}",
"unknownNode": "Unknown Node",
"unknownNodeType": "Unknown node type",
+ "unknownTemplate": "Unknown Template",
+ "unknownInput": "Unknown input: {{name}}",
"missingField_withName": "Missing field \"{{name}}\"",
"unexpectedField_withName": "Unexpected field \"{{name}}\"",
"unknownField_withName": "Unknown field \"{{name}}\"",
"unknownFieldEditWorkflowToFix_withName": "Workflow contains an unknown field \"{{name}}\".\nEdit the workflow to fix the issue.",
"updateNode": "Update Node",
"updateApp": "Update App",
+ "loadingTemplates": "Loading {{name}}",
"updateAllNodes": "Update Nodes",
"allNodesUpdated": "All Nodes Updated",
"unableToUpdateNodes_one": "Unable to update {{count}} node",
@@ -1127,6 +1173,7 @@
"viewMode": "Use in Linear View",
"unableToGetWorkflowVersion": "Unable to get workflow schema version",
"version": "Version",
+ "versionUnknown": " Version Unknown",
"workflow": "Workflow",
"graph": "Graph",
"noGraph": "No Graph",
@@ -1150,6 +1197,9 @@
"modelAccessError": "Unable to find model {{key}}, resetting to default",
"saveToGallery": "Save To Gallery",
"addItem": "Add Item",
+ "generateValues": "Generate Values",
+ "floatRangeGenerator": "Float Range Generator",
+ "integerRangeGenerator": "Integer Range Generator",
"layout": {
"autoLayout": "Auto Layout",
"layeringStrategy": "Layering Strategy",
@@ -1188,6 +1238,7 @@
"copyImage": "Copy Image",
"denoisingStrength": "Denoising Strength",
"disabledNoRasterContent": "Disabled (No Raster Content)",
+ "downloadImage": "Download Image",
"general": "General",
"guidance": "Guidance",
"height": "Height",
@@ -1208,6 +1259,7 @@
"missingFieldTemplate": "Missing field template",
"missingInputForField": "missing input",
"missingNodeTemplate": "Missing node template",
+ "emptyBatches": "empty batches",
"batchNodeNotConnected": "Batch node not connected: {{label}}",
"batchNodeEmptyCollection": "Some batch nodes have empty collections",
"collectionEmpty": "empty collection",
@@ -1227,6 +1279,10 @@
"noT5EncoderModelSelected": "No T5 Encoder model selected for FLUX generation",
"noFLUXVAEModelSelected": "No VAE model selected for FLUX generation",
"noCLIPEmbedModelSelected": "No CLIP Embed model selected for FLUX generation",
+ "fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), bbox width is {{width}}",
+ "fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), bbox height is {{height}}",
+ "fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), scaled bbox width is {{width}}",
+ "fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), scaled bbox height is {{height}}",
"modelIncompatibleBboxWidth": "Bbox width is {{width}} but {{model}} requires multiple of {{multiple}}",
"modelIncompatibleBboxHeight": "Bbox height is {{height}} but {{model}} requires multiple of {{multiple}}",
"modelIncompatibleScaledBboxWidth": "Scaled bbox width is {{width}} but {{model}} requires multiple of {{multiple}}",
@@ -1266,6 +1322,7 @@
"sendToCanvas": "Send To Canvas",
"sendToUpscale": "Send To Upscale",
"sendToVideo": "Send To Video",
+ "showOptionsPanel": "Show Side Panel (O or T)",
"shuffle": "Shuffle Seed",
"steps": "Steps",
"strength": "Strength",
@@ -1303,15 +1360,20 @@
"perPromptLabel": "Seed per Image",
"perPromptDesc": "Use a different seed for each image"
},
- "loading": "Generating Dynamic Prompts..."
+ "loading": "Generating Dynamic Prompts...",
+ "promptsToGenerate": "Prompts to Generate"
},
"sdxl": {
"cfgScale": "CFG Scale",
+ "concatPromptStyle": "Linking Prompt & Style",
+ "freePromptStyle": "Manual Style Prompting",
"denoisingStrength": "Denoising Strength",
"loading": "Loading...",
"negAestheticScore": "Negative Aesthetic Score",
+ "negStylePrompt": "Negative Style Prompt",
"noModelsAvailable": "No models available",
"posAestheticScore": "Positive Aesthetic Score",
+ "posStylePrompt": "Positive Style Prompt",
"refiner": "Refiner",
"refinermodel": "Refiner Model",
"refinerStart": "Refiner Start",
@@ -1331,6 +1393,8 @@
"informationalPopoversDisabledDesc": "Informational popovers have been disabled. Enable them in Settings.",
"enableModelDescriptions": "Enable Model Descriptions in Dropdowns",
"enableHighlightFocusedRegions": "Highlight Focused Regions",
+ "modelDescriptionsDisabled": "Model Descriptions in Dropdowns Disabled",
+ "modelDescriptionsDisabledDesc": "Model descriptions in dropdowns have been disabled. Enable them in Settings.",
"enableInvisibleWatermark": "Enable Invisible Watermark",
"enableNSFWChecker": "Enable NSFW Checker",
"general": "General",
@@ -1369,11 +1433,16 @@
"unableToLoadImageMetadata": "Unable to Load Image Metadata",
"unableToLoadStylePreset": "Unable to Load Style Preset",
"stylePresetLoaded": "Style Preset Loaded",
+ "imageNotLoadedDesc": "Could not find image",
+ "imageSaved": "Image Saved",
+ "imageSavingFailed": "Image Saving Failed",
"imageUploaded": "Image Uploaded",
"imageUploadFailed": "Image Upload Failed",
"importFailed": "Import Failed",
"importSuccessful": "Import Successful",
+ "invalidUpload": "Invalid Upload",
"layerCopiedToClipboard": "Layer Copied to Clipboard",
+ "layerSavedToAssets": "Layer Saved to Assets",
"loadedWithWarnings": "Workflow Loaded with Warnings",
"modelAddedSimple": "Model Added to Queue",
"modelImportCanceled": "Model Import Canceled",
@@ -1393,15 +1462,21 @@
"problemCopyingLayer": "Unable to Copy Layer",
"problemSavingLayer": "Unable to Save Layer",
"problemDownloadingImage": "Unable to Download Image",
+ "noRasterLayers": "No Raster Layers Found",
+ "noRasterLayersDesc": "Create at least one raster layer to export to PSD",
+ "noActiveRasterLayers": "No Active Raster Layers",
+ "noActiveRasterLayersDesc": "Enable at least one raster layer to export to PSD",
"noVisibleRasterLayers": "No Visible Raster Layers",
"noVisibleRasterLayersDesc": "Enable at least one raster layer to export to PSD",
"invalidCanvasDimensions": "Invalid Canvas Dimensions",
"canvasTooLarge": "Canvas Too Large",
"canvasTooLargeDesc": "Canvas dimensions exceed the maximum allowed size for PSD export. Reduce the total width and height of the canvas of the canvas and try again.",
+ "failedToProcessLayers": "Failed to Process Layers",
"psdExportSuccess": "PSD Export Complete",
"psdExportSuccessDesc": "Successfully exported {{count}} layers to PSD file",
"problemExportingPSD": "Problem Exporting PSD",
"canvasManagerNotAvailable": "Canvas Manager Not Available",
+ "noValidLayerAdapters": "No Valid Layer Adapters Found",
"pasteSuccess": "Pasted to {{destination}}",
"pasteFailed": "Paste Failed",
"prunedQueue": "Pruned Queue",
@@ -1409,9 +1484,13 @@
"sentToUpscale": "Sent to Upscale",
"serverError": "Server Error",
"sessionRef": "Session: {{sessionId}}",
+ "setControlImage": "Set as control image",
+ "setNodeField": "Set as node field",
"somethingWentWrong": "Something Went Wrong",
"uploadFailed": "Upload failed",
"imagesWillBeAddedTo": "Uploaded images will be added to board {{boardName}}'s assets.",
+ "uploadFailedInvalidUploadDesc_withCount_one": "Must be maximum of 1 PNG, JPEG or WEBP image.",
+ "uploadFailedInvalidUploadDesc_withCount_other": "Must be maximum of {{count}} PNG, JPEG or WEBP images.",
"uploadFailedInvalidUploadDesc": "Must be PNG, JPEG or WEBP images.",
"workflowLoaded": "Workflow Loaded",
"problemRetrievingWorkflow": "Problem Retrieving Workflow",
@@ -1427,13 +1506,19 @@
"problemUnpublishingWorkflow": "Problem Unpublishing Workflow",
"problemUnpublishingWorkflowDescription": "There was a problem unpublishing the workflow. Please try again.",
"workflowUnpublished": "Workflow Unpublished",
+ "sentToCanvas": "Sent to Canvas",
+ "sentToUpscale": "Sent to Upscale",
"promptGenerationStarted": "Prompt generation started",
"uploadAndPromptGenerationFailed": "Failed to upload image and generate prompt",
"promptExpansionFailed": "We ran into an issue. Please try prompt expansion again.",
"maskInverted": "Mask Inverted",
"maskInvertFailed": "Failed to Invert Mask",
"noVisibleMasks": "No Visible Masks",
- "noVisibleMasksDesc": "Create or enable at least one inpaint mask to invert"
+ "noVisibleMasksDesc": "Create or enable at least one inpaint mask to invert",
+ "noInpaintMaskSelected": "No Inpaint Mask Selected",
+ "noInpaintMaskSelectedDesc": "Select an inpaint mask to invert",
+ "invalidBbox": "Invalid Bounding Box",
+ "invalidBboxDesc": "The bounding box has no valid dimensions"
},
"popovers": {
"clipSkip": {
@@ -1830,15 +1915,21 @@
},
"workflows": {
"chooseWorkflowFromLibrary": "Choose Workflow from Library",
+ "defaultWorkflows": "Default Workflows",
+ "userWorkflows": "User Workflows",
+ "projectWorkflows": "Project Workflows",
"ascending": "Ascending",
"created": "Created",
"descending": "Descending",
"workflows": "Workflows",
"workflowLibrary": "Workflow Library",
"loadMore": "Load More",
+ "allLoaded": "All Workflows Loaded",
"searchPlaceholder": "Search by name, description or tags",
+ "filterByTags": "Filter by Tags",
"yourWorkflows": "Your Workflows",
"recentlyOpened": "Recently Opened",
+ "noRecentWorkflows": "No Recent Workflows",
"private": "Private",
"shared": "Shared",
"published": "Published",
@@ -1846,6 +1937,7 @@
"deselectAll": "Deselect All",
"recommended": "Recommended For You",
"opened": "Opened",
+ "openWorkflow": "Open Workflow",
"updated": "Updated",
"uploadWorkflow": "Load from File",
"deleteWorkflow": "Delete Workflow",
@@ -1860,7 +1952,11 @@
"workflowSaved": "Workflow Saved",
"name": "Name",
"noWorkflows": "No Workflows",
+ "problemLoading": "Problem Loading Workflows",
"loading": "Loading Workflows",
+ "noDescription": "No description",
+ "searchWorkflows": "Search Workflows",
+ "clearWorkflowSearchFilter": "Clear Workflow Search Filter",
"workflowName": "Workflow Name",
"newWorkflowCreated": "New Workflow Created",
"workflowCleared": "Workflow Cleared",
@@ -1875,6 +1971,7 @@
"copyShareLink": "Copy Share Link",
"copyShareLinkForWorkflow": "Copy Share Link for Workflow",
"delete": "Delete",
+ "openLibrary": "Open Library",
"workflowThumbnail": "Workflow Thumbnail",
"saveChanges": "Save Changes",
"emptyStringPlaceholder": "",
@@ -1909,10 +2006,12 @@
"addOption": "Add Option",
"resetOptions": "Reset Options",
"both": "Both",
+ "emptyRootPlaceholderViewMode": "Click Edit to start building a form for this workflow.",
"emptyRootPlaceholderEditMode": "Drag a form element or node field here to get started.",
"containerPlaceholder": "Empty Container",
"headingPlaceholder": "Empty Heading",
"textPlaceholder": "Empty Text",
+ "workflowBuilderAlphaWarning": "The workflow builder is currently in alpha. There may be breaking changes before the stable release.",
"minimum": "Minimum",
"maximum": "Maximum",
"publish": "Publish",
@@ -2002,6 +2101,7 @@
"autoNegative": "Auto Negative",
"enableAutoNegative": "Enable Auto Negative",
"disableAutoNegative": "Disable Auto Negative",
+ "deletePrompt": "Delete Prompt",
"deleteReferenceImage": "Delete Reference Image",
"showHUD": "Show HUD",
"rectangle": "Rectangle",
@@ -2014,6 +2114,7 @@
"addControlLayer": "Add $t(controlLayers.controlLayer)",
"addInpaintMask": "Add $t(controlLayers.inpaintMask)",
"addRegionalGuidance": "Add $t(controlLayers.regionalGuidance)",
+ "addGlobalReferenceImage": "Add $t(controlLayers.globalReferenceImage)",
"addDenoiseLimit": "Add $t(controlLayers.denoiseLimit)",
"rasterLayer": "Raster Layer",
"controlLayer": "Control Layer",
@@ -2021,6 +2122,7 @@
"invertMask": "Invert Mask",
"regionalGuidance": "Regional Guidance",
"referenceImageRegional": "Reference Image (Regional)",
+ "referenceImageGlobal": "Reference Image (Global)",
"asRasterLayer": "As $t(controlLayers.rasterLayer)",
"asRasterLayerResize": "As $t(controlLayers.rasterLayer) (Resize)",
"asControlLayer": "As $t(controlLayers.controlLayer)",
@@ -2030,18 +2132,43 @@
"useAsReferenceImage": "Use as Reference Image",
"regionalReferenceImage": "Regional Reference Image",
"globalReferenceImage": "Global Reference Image",
+ "sendingToCanvas": "Staging Generations on Canvas",
+ "sendingToGallery": "Sending Generations to Gallery",
+ "sendToGallery": "Send To Gallery",
+ "sendToGalleryDesc": "Pressing Invoke generates and saves a unique image to your gallery.",
"sendToCanvas": "Send To Canvas",
"newLayerFromImage": "New Layer from Image",
"newCanvasFromImage": "New Canvas from Image",
+ "newImg2ImgCanvasFromImage": "New Img2Img from Image",
"copyToClipboard": "Copy to Clipboard",
+ "sendToCanvasDesc": "Pressing Invoke stages your work in progress on the canvas.",
+ "viewProgressInViewer": "View progress and outputs in the Image Viewer.",
+ "viewProgressOnCanvas": "View progress and stage outputs on the Canvas.",
+ "rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)",
"rasterLayer_withCount_other": "Raster Layers",
+ "controlLayer_withCount_one": "$t(controlLayers.controlLayer)",
"controlLayer_withCount_other": "Control Layers",
+ "inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)",
"inpaintMask_withCount_other": "Inpaint Masks",
"regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)",
"regionalGuidance_withCount_other": "Regional Guidance",
+ "globalReferenceImage_withCount_one": "$t(controlLayers.globalReferenceImage)",
+ "globalReferenceImage_withCount_other": "Global Reference Images",
"opacity": "Opacity",
+ "regionalGuidance_withCount_hidden": "Regional Guidance ({{count}} hidden)",
+ "controlLayers_withCount_hidden": "Control Layers ({{count}} hidden)",
+ "rasterLayers_withCount_hidden": "Raster Layers ({{count}} hidden)",
+ "globalReferenceImages_withCount_hidden": "Global Reference Images ({{count}} hidden)",
+ "inpaintMasks_withCount_hidden": "Inpaint Masks ({{count}} hidden)",
+ "regionalGuidance_withCount_visible": "Regional Guidance ({{count}})",
+ "controlLayers_withCount_visible": "Control Layers ({{count}})",
+ "rasterLayers_withCount_visible": "Raster Layers ({{count}})",
+ "globalReferenceImages_withCount_visible": "Global Reference Images ({{count}})",
+ "inpaintMasks_withCount_visible": "Inpaint Masks ({{count}})",
"layer_one": "Layer",
"layer_other": "Layers",
+ "layer_withCount_one": "Layer ({{count}})",
+ "layer_withCount_other": "Layers ({{count}})",
"convertRasterLayerTo": "Convert $t(controlLayers.rasterLayer) To",
"convertControlLayerTo": "Convert $t(controlLayers.controlLayer) To",
"convertInpaintMaskTo": "Convert $t(controlLayers.inpaintMask) To",
@@ -2061,6 +2188,7 @@
"pasteToBboxDesc": "New Layer (in Bbox)",
"pasteToCanvas": "Canvas",
"pasteToCanvasDesc": "New Layer (in Canvas)",
+ "pastedTo": "Pasted to {{destination}}",
"transparency": "Transparency",
"enableTransparencyEffect": "Enable Transparency Effect",
"disableTransparencyEffect": "Disable Transparency Effect",
@@ -2073,6 +2201,7 @@
"locked": "Locked",
"unlocked": "Unlocked",
"deleteSelected": "Delete Selected",
+ "stagingOnCanvas": "Staging images on",
"replaceLayer": "Replace Layer",
"pullBboxIntoLayer": "Pull Bbox into Layer",
"pullBboxIntoReferenceImage": "Pull Bbox into Reference Image",
@@ -2082,11 +2211,17 @@
"negativePrompt": "Negative Prompt",
"beginEndStepPercentShort": "Begin/End %",
"weight": "Weight",
+ "newGallerySession": "New Gallery Session",
+ "newGallerySessionDesc": "This will clear the canvas and all settings except for your model selection. Generations will be sent to the gallery.",
+ "newCanvasSession": "New Canvas Session",
+ "newCanvasSessionDesc": "This will clear the canvas and all settings except for your model selection. Generations will be staged on the canvas.",
"resetCanvasLayers": "Reset Canvas Layers",
"resetGenerationSettings": "Reset Generation Settings",
+ "replaceCurrent": "Replace Current",
"controlLayerEmptyState": "Upload an image, drag an image from the gallery onto this layer, pull the bounding box into this layer, or draw on the canvas to get started.",
"referenceImageEmptyStateWithCanvasOptions": "Upload an image, drag an image from the gallery onto this Reference Image or pull the bounding box into this Reference Image to get started.",
"referenceImageEmptyState": "Upload an image or drag an image from the gallery onto this Reference Image to get started.",
+ "uploadOrDragAnImage": "Drag an image from the gallery or upload an image.",
"imageNoise": "Image Noise",
"denoiseLimit": "Denoise Limit",
"warnings": {
@@ -2382,7 +2517,9 @@
"showResultsOff": "Hiding Results"
},
"autoSwitch": {
- "off": "Off"
+ "off": "Off",
+ "switchOnStart": "On Start",
+ "switchOnFinish": "On Finish"
}
},
"upscaling": {
@@ -2488,6 +2625,10 @@
"canvasTitle": "Edit and refine on Canvas.",
"generateTitle": "Generate images from text prompts.",
"videoTitle": "Generate videos from text prompts.",
+ "video": {
+ "startingFrameCalloutTitle": "Add a Starting Frame",
+ "startingFrameCalloutDesc": "Add an image to control the first frame of your video."
+ },
"addStartingFrame": {
"title": "Add a Starting Frame",
"description": "Add an image to control the first frame of your video."
@@ -2566,6 +2707,10 @@
}
}
},
+ "video": {
+ "noVideoSelected": "No video selected",
+ "selectFromGallery": "Select a video from the gallery to play"
+ },
"system": {
"enableLogging": "Enable Logging",
"logLevel": {
@@ -2607,7 +2752,8 @@
"Workflows: Add a Shuffle button to number input fields"
],
"readReleaseNotes": "Read Release Notes",
- "watchRecentReleaseVideos": "Watch Recent Release Videos"
+ "watchRecentReleaseVideos": "Watch Recent Release Videos",
+ "watchUiUpdatesOverview": "Watch UI Updates Overview"
},
"supportVideos": {
"supportVideos": "Support Videos",
diff --git a/invokeai/frontend/web/scripts/clean-translations.js b/invokeai/frontend/web/scripts/clean-translations.js
deleted file mode 100644
index 35729f0599b..00000000000
--- a/invokeai/frontend/web/scripts/clean-translations.js
+++ /dev/null
@@ -1,283 +0,0 @@
-#!/usr/bin/env node
-/* eslint-disable no-console */
-
-import fs from 'fs';
-import path from 'path';
-import { fileURLToPath } from 'url';
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-
-class TranslationCleaner {
- constructor(srcDir) {
- this.srcDir = srcDir;
- this.fileCache = new Map();
- this.duplicateKeys = [];
- }
-
- getKeys(obj, currentPath = '', keys = []) {
- for (const key in obj) {
- const newPath = currentPath ? `${currentPath}.${key}` : key;
- const value = obj[key];
-
- if (typeof value === 'object' && value !== null) {
- this.getKeys(value, newPath, keys);
- } else {
- // Include all keys - we'll handle pluralization in searchCodebase
- keys.push(newPath);
- }
- }
- return keys;
- }
-
- searchCodebase(key) {
- // Known i18next pluralization suffixes
- const pluralizationSuffixes = ['_one', '_other', '_zero', '_few', '_many', '_two'];
-
- // Check if this is a pluralized key
- const lastPart = key.split('.').pop();
- const isPluralizedKey = pluralizationSuffixes.some((suffix) => lastPart.endsWith(suffix));
-
- // If it's a pluralized key, also check for the base key (without suffix)
- let keysToCheck = [key];
- if (isPluralizedKey) {
- // Extract the base key by removing the pluralization suffix
- const suffixMatch = pluralizationSuffixes.find((suffix) => lastPart.endsWith(suffix));
- if (suffixMatch) {
- const basePart = lastPart.slice(0, -suffixMatch.length);
- const keyParts = key.split('.');
- keyParts[keyParts.length - 1] = basePart;
- const baseKey = keyParts.join('.');
- keysToCheck.push(baseKey);
- }
- }
-
- const searchDir = (dir) => {
- const files = fs.readdirSync(dir);
-
- for (const file of files) {
- const fullPath = path.join(dir, file);
- const stat = fs.statSync(fullPath);
-
- if (stat.isDirectory() && !file.startsWith('.') && file !== 'node_modules') {
- if (searchDir(fullPath)) {
- return true;
- }
- } else if (file.endsWith('.ts') || file.endsWith('.tsx')) {
- let content;
- if (this.fileCache.has(fullPath)) {
- content = this.fileCache.get(fullPath);
- } else {
- content = fs.readFileSync(fullPath, 'utf8');
- this.fileCache.set(fullPath, content);
- }
-
- // Check each variant of the key
- for (const keyToCheck of keysToCheck) {
- // Escape special regex characters in the key
- const escapedKey = keyToCheck.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-
- // Check for the whole key surrounded by quotes
- if (new RegExp(`['"\`]${escapedKey}['"\`]`).test(content)) {
- return true;
- }
-
- // Check for the last part of the key (stem) with quotes
- const stem = keyToCheck.split('.').pop();
- const escapedStem = stem.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- if (new RegExp(`${escapedStem}['"\`]`).test(content)) {
- return true;
- }
- }
- }
- }
- return false;
- };
-
- return searchDir(this.srcDir);
- }
-
- removeKey(obj, keyPath) {
- const path = keyPath.split('.');
- const lastKey = path.pop();
-
- let current = obj;
- for (const key of path) {
- current = current[key];
- }
-
- delete current[lastKey];
- }
-
- removeEmptyObjects(obj) {
- for (const key in obj) {
- const value = obj[key];
- if (typeof value === 'object' && value !== null) {
- this.removeEmptyObjects(value);
- if (Object.keys(value).length === 0) {
- delete obj[key];
- }
- }
- }
- return obj;
- }
-
- detectDuplicates(obj, path = '') {
- const seenKeys = new Set();
- const duplicates = [];
-
- for (const key in obj) {
- const fullPath = path ? `${path}.${key}` : key;
-
- if (seenKeys.has(key)) {
- duplicates.push(fullPath);
- } else {
- seenKeys.add(key);
- }
-
- const value = obj[key];
- if (typeof value === 'object' && value !== null) {
- const subDuplicates = this.detectDuplicates(value, fullPath);
- duplicates.push(...subDuplicates);
- }
- }
-
- return duplicates;
- }
-
- clean(translations) {
- const keys = this.getKeys(translations);
- const removedKeys = [];
-
- console.log(`Checking ${keys.length} translation keys...`);
-
- for (const key of keys) {
- if (!this.searchCodebase(key)) {
- this.removeKey(translations, key);
- removedKeys.push(key);
- }
- }
-
- if (removedKeys.length > 0) {
- console.log(`\nFound ${removedKeys.length} unused keys:`);
- removedKeys.forEach((key) => console.log(` - ${key}`));
- } else {
- console.log('No unused keys found');
- }
-
- // Remove empty objects left after key removal
- this.removeEmptyObjects(translations);
-
- return { translations, removedCount: removedKeys.length, removedKeys };
- }
-
- check(translations) {
- const copyTranslations = JSON.parse(JSON.stringify(translations));
- const { removedCount, removedKeys } = this.clean(copyTranslations);
- return { isClean: removedCount === 0, removedKeys };
- }
-}
-
-function parseArgs() {
- const args = process.argv.slice(2);
- const options = {
- input: null,
- srcDir: null,
- mode: 'check',
- output: null,
- };
-
- for (let i = 0; i < args.length; i++) {
- const arg = args[i];
-
- if (arg === '--input' && i + 1 < args.length) {
- options.input = args[++i];
- } else if (arg === '--src-dir' && i + 1 < args.length) {
- options.srcDir = args[++i];
- } else if (arg === '--mode' && i + 1 < args.length) {
- options.mode = args[++i];
- } else if (arg === '--output' && i + 1 < args.length) {
- options.output = args[++i];
- } else if (!arg.startsWith('--') && !options.input) {
- options.input = arg;
- }
- }
-
- if (!options.input) {
- console.error('Error: Input file path is required');
- console.error(
- 'Usage: node clean-translations.js [--mode check|clean] [--src-dir ] [--output ]'
- );
- process.exit(1);
- }
-
- // Set defaults
- if (!options.srcDir) {
- // Default to ../src relative to the script location
- options.srcDir = path.join(__dirname, '..', 'src');
- }
-
- if (!options.output) {
- options.output = options.input;
- }
-
- // Resolve paths to absolute
- options.input = path.resolve(options.input);
- options.srcDir = path.resolve(options.srcDir);
- options.output = path.resolve(options.output);
-
- return options;
-}
-
-function main() {
- const options = parseArgs();
-
- console.log(`Mode: ${options.mode}`);
- console.log(`Input: ${options.input}`);
- console.log(`Source directory: ${options.srcDir}`);
-
- if (!fs.existsSync(options.input)) {
- console.error(`Error: Input file not found: ${options.input}`);
- process.exit(1);
- }
-
- if (!fs.existsSync(options.srcDir)) {
- console.error(`Error: Source directory not found: ${options.srcDir}`);
- process.exit(1);
- }
-
- const cleaner = new TranslationCleaner(options.srcDir);
- let jsonString = fs.readFileSync(options.input, 'utf8');
-
- const translations = JSON.parse(jsonString);
-
- if (options.mode === 'check') {
- console.log('Checking for unused translations...');
- const { isClean } = cleaner.check(translations);
-
- if (isClean) {
- console.log('ā All translations are in use');
- process.exit(0);
- } else {
- console.error('\nā Found unused translations. Run with --mode clean to remove them.');
- process.exit(1);
- }
- } else if (options.mode === 'clean') {
- console.log('Cleaning unused translations...');
- const { translations: cleanedTranslations } = cleaner.clean(translations);
-
- console.log(`\nWriting cleaned translations to: ${options.output}`);
- fs.writeFileSync(options.output, `${JSON.stringify(cleanedTranslations, null, 2)}\n`);
- console.log('ā Translations cleaned successfully');
- console.log('\nNext step: Run `pnpm fix` to ensure proper formatting');
- process.exit(0);
- } else {
- console.error(`Error: Invalid mode '${options.mode}'. Use 'check' or 'clean'.`);
- process.exit(1);
- }
-}
-
-main().catch((err) => {
- console.error('Error:', err);
- process.exit(1);
-});
diff --git a/invokeai/frontend/web/scripts/clean_translations.py b/invokeai/frontend/web/scripts/clean_translations.py
new file mode 100644
index 00000000000..a422747ef5c
--- /dev/null
+++ b/invokeai/frontend/web/scripts/clean_translations.py
@@ -0,0 +1,89 @@
+# Cleans translations by removing unused keys
+# Usage: python clean_translations.py
+# Note: Must be run from invokeai/frontend/web/scripts directory
+#
+# After running the script, open `en.json` and check for empty objects (`{}`) and remove them manually.
+# Also, the script does not handle keys with underscores. They need to be checked manually.
+
+import json
+import os
+import re
+from typing import TypeAlias, Union
+
+from tqdm import tqdm
+
+RecursiveDict: TypeAlias = dict[str, Union["RecursiveDict", str]]
+
+
+class TranslationCleaner:
+ file_cache: dict[str, str] = {}
+
+ def _get_keys(self, obj: RecursiveDict, current_path: str = "", keys: list[str] | None = None):
+ if keys is None:
+ keys = []
+ for key in obj:
+ new_path = f"{current_path}.{key}" if current_path else key
+ next_ = obj[key]
+ if isinstance(next_, dict):
+ self._get_keys(next_, new_path, keys)
+ elif "_" in key:
+ # This typically means its a pluralized key
+ continue
+ else:
+ keys.append(new_path)
+ return keys
+
+ def _search_codebase(self, key: str):
+ for root, _dirs, files in os.walk("../src"):
+ for file in files:
+ if file.endswith(".ts") or file.endswith(".tsx"):
+ full_path = os.path.join(root, file)
+ if full_path in self.file_cache:
+ content = self.file_cache[full_path]
+ else:
+ with open(full_path, "r") as f:
+ content = f.read()
+ self.file_cache[full_path] = content
+
+ # match the whole key, surrounding by quotes
+ if re.search(r"['\"`]" + re.escape(key) + r"['\"`]", self.file_cache[full_path]):
+ return True
+ # math the stem of the key, with quotes at the end
+ if re.search(re.escape(key.split(".")[-1]) + r"['\"`]", self.file_cache[full_path]):
+ return True
+ return False
+
+ def _remove_key(self, obj: RecursiveDict, key: str):
+ path = key.split(".")
+ last_key = path[-1]
+ for k in path[:-1]:
+ obj = obj[k]
+ del obj[last_key]
+
+ def clean(self, obj: RecursiveDict) -> RecursiveDict:
+ keys = self._get_keys(obj)
+ pbar = tqdm(keys, desc="Checking keys")
+ for key in pbar:
+ if not self._search_codebase(key):
+ self._remove_key(obj, key)
+ return obj
+
+
+def main():
+ try:
+ with open("../public/locales/en.json", "r") as f:
+ data = json.load(f)
+ except FileNotFoundError as e:
+ raise FileNotFoundError(
+ "Unable to find en.json file - must be run from invokeai/frontend/web/scripts directory"
+ ) from e
+
+ cleaner = TranslationCleaner()
+ cleaned_data = cleaner.clean(data)
+
+ with open("../public/locales/en.json", "w") as f:
+ json.dump(cleaned_data, f, indent=4)
+
+
+if __name__ == "__main__":
+ main()