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()