diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index a742262a16..397476a5a6 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -619,56 +619,174 @@
         "addNodeToolTip": "Add Node (Shift+A, Space)",
         "animatedEdges": "Animated Edges",
         "animatedEdgesHelp": "Animate selected edges and edges connected to selected nodes",
+        "boolean": "Booleans",
+        "booleanCollection": "Boolean Collection",
+        "booleanCollectionDescription": "A collection of booleans.",
+        "booleanDescription": "Booleans are true or false.",
+        "booleanPolymorphic": "Boolean Polymorphic",
+        "booleanPolymorphicDescription": "A collection of booleans.",
         "cannotConnectInputToInput": "Cannot connect input to input",
         "cannotConnectOutputToOutput": "Cannot connect output to output",
         "cannotConnectToSelf": "Cannot connect to self",
+        "clipField": "Clip",
+        "clipFieldDescription": "Tokenizer and text_encoder submodels.",
+        "collection": "Collection",
+        "collectionDescription": "TODO",
+        "collectionItem": "Collection Item",
+        "collectionItemDescription": "TODO",
         "colorCodeEdges": "Color-Code Edges",
         "colorCodeEdgesHelp": "Color-code edges according to their connected fields",
+        "colorCollectionDescription": "A collection of colors.",
+        "colorField": "Color",
+        "colorFieldDescription": "A RGBA color.",
+        "colorPolymorphic": "Color Polymorphic",
+        "colorPolymorphicDescription": "A collection of colors.",
+        "conditioningCollection": "Conditioning Collection",
+        "conditioningCollectionDescription": "Conditioning may be passed between nodes.",
+        "conditioningField": "Conditioning",
+        "conditioningFieldDescription": "Conditioning may be passed between nodes.",
+        "conditioningPolymorphic": "Conditioning Polymorphic",
+        "conditioningPolymorphicDescription": "Conditioning may be passed between nodes.",
         "connectionWouldCreateCycle": "Connection would create a cycle",
+        "controlCollection": "Control Collection",
+        "controlCollectionDescription": "Control info passed between nodes.",
+        "controlField": "Control",
+        "controlFieldDescription": "Control info passed between nodes.",
         "currentImage": "Current Image",
         "currentImageDescription": "Displays the current image in the Node Editor",
+        "denoiseMaskField": "Denoise Mask",
+        "denoiseMaskFieldDescription": "Denoise Mask may be passed between nodes",
+        "doesNotExist": "does not exist",
         "downloadWorkflow": "Download Workflow JSON",
+        "edge": "Edge",
+        "enum": "Enum",
+        "enumDescription": "Enums are values that may be one of a number of options.",
+        "executionStateCompleted": "Completed",
+        "executionStateError": "Error",
+        "executionStateInProgress": "In Progress",
         "fieldTypesMustMatch": "Field types must match",
         "fitViewportNodes": "Fit View",
+        "float": "Float",
+        "floatCollection": "Float Collection",
+        "floatCollectionDescription": "A collection of floats.",
+        "floatDescription": "Floats are numbers with a decimal point.",
+        "floatPolymorphic": "Float Polymorphic",
+        "floatPolymorphicDescription": "A collection of floats.",
         "fullyContainNodes": "Fully Contain Nodes to Select",
         "fullyContainNodesHelp": "Nodes must be fully inside the selection box to be selected",
         "hideGraphNodes": "Hide Graph Overlay",
         "hideLegendNodes": "Hide Field Type Legend",
         "hideMinimapnodes": "Hide MiniMap",
+        "imageCollection": "Image Collection",
+        "imageCollectionDescription": "A collection of images.",
+        "imageField": "Image",
+        "imageFieldDescription": "Images may be passed between nodes.",
+        "imagePolymorphic": "Image Polymorphic",
+        "imagePolymorphicDescription": "A collection of images.",
+        "inputFields": "Input Feilds",
         "inputMayOnlyHaveOneConnection": "Input may only have one connection",
+        "inputNode": "Input Node",
+        "integer": "Integer",
+        "integerCollection": "Integer Collection",
+        "integerCollectionDescription": "A collection of integers.",
+        "integerDescription": "Integers are whole numbers, without a decimal point.",
+        "integerPolymorphic": "Integer Polymorphic",
+        "integerPolymorphicDescription": "A collection of integers.",
+        "invalidOutputSchema": "Invalid output schema",
+        "latentsCollection": "Latents Collection",
+        "latentsCollectionDescription": "Latents may be passed between nodes.",
+        "latentsField": "Latents",
+        "latentsFieldDescription": "Latents may be passed between nodes.",
+        "latentsPolymorphic": "Latents Polymorphic",
+        "latentsPolymorphicDescription": "Latents may be passed between nodes.",
         "loadingNodes": "Loading Nodes...",
         "loadWorkflow": "Load Workflow",
+        "loRAModelField": "LoRA",
+        "loRAModelFieldDescription": "TODO",
+        "mainModelField": "Model",
+        "mainModelFieldDescription": "TODO",
+        "maybeIncompatible": "May be Incompatible With Installed",
+        "mismatchedVersion": "Has Mismatched Version",
+        "missingCanvaInitImage": "Missing canvas init image",
+        "missingCanvaInitMaskImages": "Missing canvas init and mask images",
+        "missingTemplate": "Missing Template",
         "noConnectionData": "No connection data",
         "noConnectionInProgress": "No connection in progress",
+        "node": "Node",
         "nodeOutputs": "Node Outputs",
         "nodeSearch": "Search for nodes",
         "nodeTemplate": "Node Template",
+        "nodeType": "Node Type",
         "noFieldsLinearview": "No fields added to Linear View",
         "noFieldType": "No field type",
+        "noImageFoundState": "No initial image found in state",
         "noMatchingNodes": "No matching nodes",
         "noNodeSelected": "No node selected",
         "noOpacity": "Node Opacity",
         "noOutputRecorded": "No outputs recorded",
+        "noOutputSchemaName": "No output schema name found in ref object",
         "notes": "Notes",
         "notesDescription": "Add notes about your workflow",
+        "oNNXModelField": "ONNX Model",
+        "oNNXModelFieldDescription": "ONNX model field.",
+        "outputFields": "Output Feilds",
+        "outputNode": "Output node",
+        "outputSchemaNotFound": "Output schema not found",
         "pickOne": "Pick One",
+        "problemReadingMetadata": "Problem reading metadata from image",
+        "problemReadingWorkflow": "Problem reading workflow from image",
         "problemSettingTitle": "Problem Setting Title",
         "reloadNodeTemplates": "Reload Node Templates",
         "removeLinearView": "Remove from Linear View",
         "resetWorkflow": "Reset Workflow",
         "resetWorkflowDesc": "Are you sure you want to reset this workflow?",
         "resetWorkflowDesc2": "Resetting the workflow will clear all nodes, edges and workflow details.",
+        "scheduler": "Scheduler",
+        "schedulerDescription": "TODO",
+        "sDXLMainModelField": "SDXL Model",
+        "sDXLMainModelFieldDescription": "SDXL model field.",
+        "sDXLRefinerModelField": "Refiner Model",
+        "sDXLRefinerModelFieldDescription": "TODO",
         "showGraphNodes": "Show Graph Overlay",
         "showLegendNodes": "Show Field Type Legend",
         "showMinimapnodes": "Show MiniMap",
+        "skipped": "Skipped",
+        "skippedReservedInput": "Skipped reserved input field",
+        "skippedReservedOutput": "Skipped reserved output field",
+        "skippingInputNoTemplate": "Skipping input field with no template",
+        "skippingReservedFieldType": "Skipping reserved field type",
+        "skippingUnknownInputType": "Skipping unknown input field type",
+        "skippingUnknownOutputType": "Skipping unknown output field type",
         "snapToGrid": "Snap to Grid",
         "snapToGridHelp": "Snap nodes to grid when moved",
+        "sourceNode": "Source node",
+        "string": "String",
+        "stringCollection": "String Collection",
+        "stringCollectionDescription": "A collection of strings.",
+        "stringDescription": "Strings are text.",
+        "stringPolymorphic": "String Polymorphic",
+        "stringPolymorphicDescription": "A collection of strings.",
         "unableToLoadWorkflow": "Unable to Validate Workflow",
+        "unableToParseEdge": "Unable to parse edge",
+        "unableToParseNode": "Unable to parse node",
         "unableToValidateWorkflow": "Unable to Validate Workflow",
+        "uNetField": "UNet",
+        "uNetFieldDescription": "UNet submodel.",
+        "unhandledInputProperty": "Unhandled input property",
+        "unhandledOutputProperty": "Unhandled output property",
         "unknownField": "Unknown Field",
+        "unknownNode": "Unknown Node",
+        "unknownTemplate": "Unknown Template",
         "unkownInvocation": "Unknown Invocation type",
+        "updateApp": "Update App",
+        "vaeField": "Vae",
+        "vaeFieldDescription": "Vae submodel.",
+        "vaeModelField": "VAE",
+        "vaeModelFieldDescription": "TODO",
         "validateConnections": "Validate Connections and Graph",
         "validateConnectionsHelp": "Prevent invalid connections from being made, and invalid graphs from being invoked",
+        "version": "Version",
+        "versionUnknown": " Version Unknown",
         "workflow": "Workflow",
         "workflowAuthor": "Author",
         "workflowContact": "Contact",
@@ -680,15 +798,7 @@
         "workflowValidation": "Workflow Validation Error",
         "workflowVersion": "Version",
         "zoomInNodes": "Zoom In",
-        "zoomOutNodes": "Zoom Out",
-        "executionStateError": "Error",
-        "executionStateCompleted": "Completed",
-        "executionStateInProgress": "In Progress",
-        "versionUnknown": " Version Unknown",
-        "unknownNode": "Unknown Node",
-        "version": "Version",
-        "updateApp": "Update App",
-        "unknownTemplate": "Unknown Template"
+        "zoomOutNodes": "Zoom Out"
     },
     "parameters": {
         "aspectRatio": "Ratio",
@@ -853,8 +963,14 @@
         "useSlidersForAll": "Use Sliders For All Options"
     },
     "toast": {
+        "addedToBoard": "Added to board",
+        "baseModelChangedCleared": "Base model changed, cleared",
         "canceled": "Processing Canceled",
+        "canvasCopiedClipboard": "Canvas Copied to Clipboard",
+        "canvasDownloaded": "Canvas Downloaded",
         "canvasMerged": "Canvas Merged",
+        "canvasSavedGallery": "Canvas Saved to Gallery",
+        "canvasSentControlnetAssets": "Canvas Sent to ControlNet & Assets",
         "connected": "Connected to Server",
         "disconnected": "Disconnected from Server",
         "downloadImageStarted": "Image Download Started",
@@ -863,10 +979,18 @@
         "imageLinkCopied": "Image Link Copied",
         "imageNotLoaded": "No Image Loaded",
         "imageNotLoadedDesc": "Could not find image",
+        "imageSaved": "Image Saved",
         "imageSavedToGallery": "Image Saved to Gallery",
+        "imageSavingFailed": "Image Saving Failed",
+        "imageUploaded": "Image Uploaded",
+        "imageUploadFailed": "Image Upload Failed",
+        "incompatibleSubmodel": "incompatible submodel",
         "initialImageNotSet": "Initial Image Not Set",
         "initialImageNotSetDesc": "Could not load initial image",
         "initialImageSet": "Initial Image Set",
+        "loadedWithWarnings": "Workflow Loaded with Warnings",
+        "maskSavedAssets": "Mask Saved to Assets",
+        "maskSentControlnetAssets": "Mask Sent to ControlNet & Assets",
         "metadataLoadFailed": "Failed to load metadata",
         "modelAdded": "Model Added: {{modelName}}",
         "modelAddedSimple": "Model Added",
@@ -887,8 +1011,20 @@
         "parametersNotSet": "Parameters Not Set",
         "parametersNotSetDesc": "No metadata found for this image.",
         "parametersSet": "Parameters Set",
+        "problemCopyingCanvas": "Problem Copying Canvas",
+        "problemCopyingCanvasDesc": "Unable to export base layer",
         "problemCopyingImage": "Unable to Copy Image",
         "problemCopyingImageLink": "Unable to Copy Image Link",
+        "problemDownloadingCanvas": "Problem Downloading Canvas",
+        "problemDownloadingCanvasDesc": "Unable to export base layer",
+        "problemImportingMask": "Problem Importing Mask",
+        "problemImportingMaskDesc": "Unable to export mask",
+        "problemMergingCanvas": "Problem Merging Canvas",
+        "problemMergingCanvasDesc": "Unable to export base layer",
+        "problemSavingCanvas": "Problem Saving Canvas",
+        "problemSavingCanvasDesc": "Unable to export base layer",
+        "problemSavingMask": "Problem Saving Mask",
+        "problemSavingMaskDesc": "Unable to export mask",
         "promptNotSet": "Prompt Not Set",
         "promptNotSetDesc": "Could not find prompt for this image.",
         "promptSet": "Prompt Set",
@@ -898,11 +1034,16 @@
         "sentToImageToImage": "Sent To Image To Image",
         "sentToUnifiedCanvas": "Sent to Unified Canvas",
         "serverError": "Server Error",
+        "setCanvasInitialImage": "Set as canvas initial image",
+        "setControlImage": "Set as control image",
+        "setInitialImage": "Set as initial image",
+        "setNodeField": "Set as node field",
         "tempFoldersEmptied": "Temp Folder Emptied",
         "uploadFailed": "Upload failed",
         "uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image",
         "uploadFailedUnableToLoadDesc": "Unable to load file",
-        "upscalingFailed": "Upscaling Failed"
+        "upscalingFailed": "Upscaling Failed",
+        "workflowLoaded": "Workflow Loaded"
     },
     "tooltip": {
         "feature": {
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts
index 9e66d1bdb8..c328aceedf 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts
@@ -4,6 +4,7 @@ import { $logger } from 'app/logging/logger';
 import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
 import { addToast } from 'features/system/store/systemSlice';
 import { copyBlobToClipboard } from 'features/canvas/util/copyBlobToClipboard';
+import { t } from 'i18next';
 
 export const addCanvasCopiedToClipboardListener = () => {
   startAppListening({
@@ -20,8 +21,8 @@ export const addCanvasCopiedToClipboardListener = () => {
         moduleLog.error('Problem getting base layer blob');
         dispatch(
           addToast({
-            title: 'Problem Copying Canvas',
-            description: 'Unable to export base layer',
+            title: t('toast.problemCopyingCanvas'),
+            description: t('toast.problemCopyingCanvasDesc'),
             status: 'error',
           })
         );
@@ -32,7 +33,7 @@ export const addCanvasCopiedToClipboardListener = () => {
 
       dispatch(
         addToast({
-          title: 'Canvas Copied to Clipboard',
+          title: t('toast.canvasCopiedClipboard'),
           status: 'success',
         })
       );
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts
index b101d00541..23faf4a356 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts
@@ -4,6 +4,7 @@ import { $logger } from 'app/logging/logger';
 import { downloadBlob } from 'features/canvas/util/downloadBlob';
 import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
 import { addToast } from 'features/system/store/systemSlice';
+import { t } from 'i18next';
 
 export const addCanvasDownloadedAsImageListener = () => {
   startAppListening({
@@ -20,8 +21,8 @@ export const addCanvasDownloadedAsImageListener = () => {
         moduleLog.error('Problem getting base layer blob');
         dispatch(
           addToast({
-            title: 'Problem Downloading Canvas',
-            description: 'Unable to export base layer',
+            title: t('toast.problemDownloadingCanvas'),
+            description: t('toast.problemDownloadingCanvasDesc'),
             status: 'error',
           })
         );
@@ -29,7 +30,9 @@ export const addCanvasDownloadedAsImageListener = () => {
       }
 
       downloadBlob(blob, 'canvas.png');
-      dispatch(addToast({ title: 'Canvas Downloaded', status: 'success' }));
+      dispatch(
+        addToast({ title: t('toast.canvasDownloaded'), status: 'success' })
+      );
     },
   });
 };
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts
index fb411a6e25..5181df134f 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts
@@ -5,6 +5,7 @@ import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlic
 import { addToast } from 'features/system/store/systemSlice';
 import { imagesApi } from 'services/api/endpoints/images';
 import { startAppListening } from '..';
+import { t } from 'i18next';
 
 export const addCanvasImageToControlNetListener = () => {
   startAppListening({
@@ -19,8 +20,8 @@ export const addCanvasImageToControlNetListener = () => {
         log.error('Problem getting base layer blob');
         dispatch(
           addToast({
-            title: 'Problem Saving Canvas',
-            description: 'Unable to export base layer',
+            title: t('toast.problemSavingCanvas'),
+            description: t('toast.problemSavingCanvasDesc'),
             status: 'error',
           })
         );
@@ -40,7 +41,7 @@ export const addCanvasImageToControlNetListener = () => {
           crop_visible: true,
           postUploadAction: {
             type: 'TOAST',
-            toastOptions: { title: 'Canvas Sent to ControlNet & Assets' },
+            toastOptions: { title: t('toast.canvasSentControlnetAssets') },
           },
         })
       ).unwrap();
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts
index e701b93352..f814d94f3a 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts
@@ -4,6 +4,7 @@ import { getCanvasData } from 'features/canvas/util/getCanvasData';
 import { addToast } from 'features/system/store/systemSlice';
 import { imagesApi } from 'services/api/endpoints/images';
 import { startAppListening } from '..';
+import { t } from 'i18next';
 
 export const addCanvasMaskSavedToGalleryListener = () => {
   startAppListening({
@@ -30,8 +31,8 @@ export const addCanvasMaskSavedToGalleryListener = () => {
         log.error('Problem getting mask layer blob');
         dispatch(
           addToast({
-            title: 'Problem Saving Mask',
-            description: 'Unable to export mask',
+            title: t('toast.problemSavingMask'),
+            description: t('toast.problemSavingMaskDesc'),
             status: 'error',
           })
         );
@@ -51,7 +52,7 @@ export const addCanvasMaskSavedToGalleryListener = () => {
           crop_visible: true,
           postUploadAction: {
             type: 'TOAST',
-            toastOptions: { title: 'Mask Saved to Assets' },
+            toastOptions: { title: t('toast.maskSavedAssets') },
           },
         })
       );
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts
index 6c97259f02..671c7f63e4 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts
@@ -5,6 +5,7 @@ import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlic
 import { addToast } from 'features/system/store/systemSlice';
 import { imagesApi } from 'services/api/endpoints/images';
 import { startAppListening } from '..';
+import { t } from 'i18next';
 
 export const addCanvasMaskToControlNetListener = () => {
   startAppListening({
@@ -31,8 +32,8 @@ export const addCanvasMaskToControlNetListener = () => {
         log.error('Problem getting mask layer blob');
         dispatch(
           addToast({
-            title: 'Problem Importing Mask',
-            description: 'Unable to export mask',
+            title: t('toast.problemImportingMask'),
+            description: t('toast.problemImportingMaskDesc'),
             status: 'error',
           })
         );
@@ -52,7 +53,7 @@ export const addCanvasMaskToControlNetListener = () => {
           crop_visible: true,
           postUploadAction: {
             type: 'TOAST',
-            toastOptions: { title: 'Mask Sent to ControlNet & Assets' },
+            toastOptions: { title: t('toast.maskSentControlnetAssets') },
           },
         })
       ).unwrap();
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts
index 21c506242d..62f7b60036 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts
@@ -6,6 +6,7 @@ import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
 import { addToast } from 'features/system/store/systemSlice';
 import { imagesApi } from 'services/api/endpoints/images';
 import { startAppListening } from '..';
+import { t } from 'i18next';
 
 export const addCanvasMergedListener = () => {
   startAppListening({
@@ -20,8 +21,8 @@ export const addCanvasMergedListener = () => {
         moduleLog.error('Problem getting base layer blob');
         dispatch(
           addToast({
-            title: 'Problem Merging Canvas',
-            description: 'Unable to export base layer',
+            title: t('toast.problemMergingCanvas'),
+            description: t('toast.problemMergingCanvasDesc'),
             status: 'error',
           })
         );
@@ -34,8 +35,8 @@ export const addCanvasMergedListener = () => {
         moduleLog.error('Problem getting canvas base layer');
         dispatch(
           addToast({
-            title: 'Problem Merging Canvas',
-            description: 'Unable to export base layer',
+            title: t('toast.problemMergingCanvas'),
+            description: t('toast.problemMergingCanvasDesc'),
             status: 'error',
           })
         );
@@ -55,7 +56,7 @@ export const addCanvasMergedListener = () => {
           is_intermediate: true,
           postUploadAction: {
             type: 'TOAST',
-            toastOptions: { title: 'Canvas Merged' },
+            toastOptions: { title: t('toast.canvasMerged') },
           },
         })
       ).unwrap();
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts
index dbadb72a52..0bb8ad8550 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts
@@ -4,6 +4,7 @@ import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
 import { addToast } from 'features/system/store/systemSlice';
 import { imagesApi } from 'services/api/endpoints/images';
 import { startAppListening } from '..';
+import { t } from 'i18next';
 
 export const addCanvasSavedToGalleryListener = () => {
   startAppListening({
@@ -18,8 +19,8 @@ export const addCanvasSavedToGalleryListener = () => {
         log.error('Problem getting base layer blob');
         dispatch(
           addToast({
-            title: 'Problem Saving Canvas',
-            description: 'Unable to export base layer',
+            title: t('toast.problemSavingCanvas'),
+            description: t('toast.problemSavingCanvasDesc'),
             status: 'error',
           })
         );
@@ -39,7 +40,7 @@ export const addCanvasSavedToGalleryListener = () => {
           crop_visible: true,
           postUploadAction: {
             type: 'TOAST',
-            toastOptions: { title: 'Canvas Saved to Gallery' },
+            toastOptions: { title: t('toast.canvasSavedGallery') },
           },
         })
       );
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
index 0c55908748..2cc406469b 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
@@ -9,9 +9,10 @@ import { omit } from 'lodash-es';
 import { boardsApi } from 'services/api/endpoints/boards';
 import { startAppListening } from '..';
 import { imagesApi } from '../../../../../services/api/endpoints/images';
+import { t } from 'i18next';
 
 const DEFAULT_UPLOADED_TOAST: UseToastOptions = {
-  title: 'Image Uploaded',
+  title: t('toast.imageUploaded'),
   status: 'success',
 };
 
@@ -57,8 +58,8 @@ export const addImageUploadedFulfilledListener = () => {
           // Fall back to just the board id if we can't find the board for some reason
           const board = data?.find((b) => b.board_id === autoAddBoardId);
           const description = board
-            ? `Added to board ${board.board_name}`
-            : `Added to board ${autoAddBoardId}`;
+            ? `${t('toast.addedToBoard')} ${board.board_name}`
+            : `${t('toast.addedToBoard')} ${autoAddBoardId}`;
 
           dispatch(
             addToast({
@@ -75,7 +76,7 @@ export const addImageUploadedFulfilledListener = () => {
         dispatch(
           addToast({
             ...DEFAULT_UPLOADED_TOAST,
-            description: 'Set as canvas initial image',
+            description: t('toast.setCanvasInitialImage'),
           })
         );
         return;
@@ -92,7 +93,7 @@ export const addImageUploadedFulfilledListener = () => {
         dispatch(
           addToast({
             ...DEFAULT_UPLOADED_TOAST,
-            description: 'Set as control image',
+            description: t('toast.setControlImage'),
           })
         );
         return;
@@ -103,7 +104,7 @@ export const addImageUploadedFulfilledListener = () => {
         dispatch(
           addToast({
             ...DEFAULT_UPLOADED_TOAST,
-            description: 'Set as initial image',
+            description: t('toast.setInitialImage'),
           })
         );
         return;
@@ -117,7 +118,7 @@ export const addImageUploadedFulfilledListener = () => {
         dispatch(
           addToast({
             ...DEFAULT_UPLOADED_TOAST,
-            description: `Set as node field ${fieldName}`,
+            description: `${t('toast.setNodeField')} ${fieldName}`,
           })
         );
         return;
@@ -140,7 +141,7 @@ export const addImageUploadedRejectedListener = () => {
       log.error({ ...sanitizedData }, 'Image upload failed');
       dispatch(
         addToast({
-          title: 'Image Upload Failed',
+          title: t('toast.imageUploadFailed'),
           description: action.error.message,
           status: 'error',
         })
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts
index 240890f043..eb25b22293 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts
@@ -14,6 +14,7 @@ import { addToast } from 'features/system/store/systemSlice';
 import { makeToast } from 'features/system/util/makeToast';
 import { forEach } from 'lodash-es';
 import { startAppListening } from '..';
+import { t } from 'i18next';
 
 export const addModelSelectedListener = () => {
   startAppListening({
@@ -67,7 +68,9 @@ export const addModelSelectedListener = () => {
           dispatch(
             addToast(
               makeToast({
-                title: `Base model changed, cleared ${modelsCleared} incompatible submodel${
+                title: `${t(
+                  'toast.baseModelChangedCleared'
+                )} ${modelsCleared} ${t('toast.incompatibleSubmodel')}${
                   modelsCleared === 1 ? '' : 's'
                 }`,
                 status: 'warning',
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts
index b14e18ea63..c00cf78beb 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts
@@ -2,6 +2,7 @@ import { stagingAreaImageSaved } from 'features/canvas/store/actions';
 import { addToast } from 'features/system/store/systemSlice';
 import { imagesApi } from 'services/api/endpoints/images';
 import { startAppListening } from '..';
+import { t } from 'i18next';
 
 export const addStagingAreaImageSavedListener = () => {
   startAppListening({
@@ -28,11 +29,11 @@ export const addStagingAreaImageSavedListener = () => {
             })
           );
         }
-        dispatch(addToast({ title: 'Image Saved', status: 'success' }));
+        dispatch(addToast({ title: t('toast.imageSaved'), status: 'success' }));
       } catch (error) {
         dispatch(
           addToast({
-            title: 'Image Saving Failed',
+            title: t('toast.imageSavingFailed'),
             description: (error as Error)?.message,
             status: 'error',
           })
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoaded.ts
index c447720941..de697a70e5 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoaded.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoaded.ts
@@ -7,6 +7,7 @@ import { addToast } from 'features/system/store/systemSlice';
 import { makeToast } from 'features/system/util/makeToast';
 import { setActiveTab } from 'features/ui/store/uiSlice';
 import { startAppListening } from '..';
+import { t } from 'i18next';
 
 export const addWorkflowLoadedListener = () => {
   startAppListening({
@@ -27,7 +28,7 @@ export const addWorkflowLoadedListener = () => {
         dispatch(
           addToast(
             makeToast({
-              title: 'Workflow Loaded',
+              title: t('toast.workflowLoaded'),
               status: 'success',
             })
           )
@@ -36,7 +37,7 @@ export const addWorkflowLoadedListener = () => {
         dispatch(
           addToast(
             makeToast({
-              title: 'Workflow Loaded with Warnings',
+              title: t('toast.loadedWithWarnings'),
               status: 'warning',
             })
           )
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx
index c1124477e2..4f1bd39b8c 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx
@@ -1,8 +1,9 @@
-import { CoreMetadata } from 'features/nodes/types/types';
+import { CoreMetadata, LoRAMetadataItem } from 'features/nodes/types/types';
 import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
 import { memo, useCallback } from 'react';
-import ImageMetadataItem from './ImageMetadataItem';
 import { useTranslation } from 'react-i18next';
+import { isValidLoRAModel } from '../../../parameters/types/parameterSchemas';
+import ImageMetadataItem from './ImageMetadataItem';
 
 type Props = {
   metadata?: CoreMetadata;
@@ -24,6 +25,7 @@ const ImageMetadataActions = (props: Props) => {
     recallWidth,
     recallHeight,
     recallStrength,
+    recallLoRA,
   } = useRecallParameters();
 
   const handleRecallPositivePrompt = useCallback(() => {
@@ -66,6 +68,13 @@ const ImageMetadataActions = (props: Props) => {
     recallStrength(metadata?.strength);
   }, [metadata?.strength, recallStrength]);
 
+  const handleRecallLoRA = useCallback(
+    (lora: LoRAMetadataItem) => {
+      recallLoRA(lora);
+    },
+    [recallLoRA]
+  );
+
   if (!metadata || Object.keys(metadata).length === 0) {
     return null;
   }
@@ -130,20 +139,6 @@ const ImageMetadataActions = (props: Props) => {
           onClick={handleRecallHeight}
         />
       )}
-      {/* {metadata.threshold !== undefined && (
-          <MetadataItem
-            label={t('metadata.threshold')}
-            value={metadata.threshold}
-            onClick={() => dispatch(setThreshold(Number(metadata.threshold)))}
-          />
-        )}
-        {metadata.perlin !== undefined && (
-          <MetadataItem
-            label={t('metadata.perlin')}
-            value={metadata.perlin}
-            onClick={() => dispatch(setPerlin(Number(metadata.perlin)))}
-          />
-        )} */}
       {metadata.scheduler && (
         <ImageMetadataItem
           label={t('metadata.scheduler')}
@@ -165,40 +160,6 @@ const ImageMetadataActions = (props: Props) => {
           onClick={handleRecallCfgScale}
         />
       )}
-      {/* {metadata.variations && metadata.variations.length > 0 && (
-          <MetadataItem
-            label="{t('metadata.variations')}
-            value={seedWeightsToString(metadata.variations)}
-            onClick={() =>
-              dispatch(
-                setSeedWeights(seedWeightsToString(metadata.variations))
-              )
-            }
-          />
-        )}
-        {metadata.seamless && (
-          <MetadataItem
-            label={t('metadata.seamless')}
-            value={metadata.seamless}
-            onClick={() => dispatch(setSeamless(metadata.seamless))}
-          />
-        )}
-        {metadata.hires_fix && (
-          <MetadataItem
-            label={t('metadata.hiresFix')}
-            value={metadata.hires_fix}
-            onClick={() => dispatch(setHiresFix(metadata.hires_fix))}
-          />
-        )} */}
-
-      {/* {init_image_path && (
-          <MetadataItem
-            label={t('metadata.initImage')}
-            value={init_image_path}
-            isLink
-            onClick={() => dispatch(setInitialImage(init_image_path))}
-          />
-        )} */}
       {metadata.strength && (
         <ImageMetadataItem
           label={t('metadata.strength')}
@@ -206,13 +167,19 @@ const ImageMetadataActions = (props: Props) => {
           onClick={handleRecallStrength}
         />
       )}
-      {/* {metadata.fit && (
-          <MetadataItem
-            label={t('metadata.fit')}
-            value={metadata.fit}
-            onClick={() => dispatch(setShouldFitToWidthHeight(metadata.fit))}
-          />
-        )} */}
+      {metadata.loras &&
+        metadata.loras.map((lora, index) => {
+          if (isValidLoRAModel(lora.lora)) {
+            return (
+              <ImageMetadataItem
+                key={index}
+                label="LoRA"
+                value={`${lora.lora.model_name} - ${lora.weight}`}
+                onClick={() => handleRecallLoRA(lora)}
+              />
+            );
+          }
+        })}
     </>
   );
 };
diff --git a/invokeai/frontend/web/src/features/lora/store/loraSlice.ts b/invokeai/frontend/web/src/features/lora/store/loraSlice.ts
index 10a1671933..bbe019b1c3 100644
--- a/invokeai/frontend/web/src/features/lora/store/loraSlice.ts
+++ b/invokeai/frontend/web/src/features/lora/store/loraSlice.ts
@@ -27,6 +27,13 @@ export const loraSlice = createSlice({
       const { model_name, id, base_model } = action.payload;
       state.loras[id] = { id, model_name, base_model, ...defaultLoRAConfig };
     },
+    loraRecalled: (
+      state,
+      action: PayloadAction<LoRAModelConfigEntity & { weight: number }>
+    ) => {
+      const { model_name, id, base_model, weight } = action.payload;
+      state.loras[id] = { id, model_name, base_model, weight };
+    },
     loraRemoved: (state, action: PayloadAction<string>) => {
       const id = action.payload;
       delete state.loras[id];
@@ -62,6 +69,7 @@ export const {
   loraWeightChanged,
   loraWeightReset,
   lorasCleared,
+  loraRecalled,
 } = loraSlice.actions;
 
 export default loraSlice.reducer;
diff --git a/invokeai/frontend/web/src/features/nodes/types/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts
index a12c1fbddc..0ab10db159 100644
--- a/invokeai/frontend/web/src/features/nodes/types/constants.ts
+++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts
@@ -1,4 +1,5 @@
 import { FieldType, FieldUIConfig } from './types';
+import { t } from 'i18next';
 
 export const HANDLE_TOOLTIP_OPEN_DELAY = 500;
 export const COLOR_TOKEN_VALUE = 500;
@@ -102,73 +103,73 @@ export const isPolymorphicItemType = (
 export const FIELDS: Record<FieldType, FieldUIConfig> = {
   boolean: {
     color: 'green.500',
-    description: 'Booleans are true or false.',
-    title: 'Boolean',
+    description: t('nodes.booleanDescription'),
+    title: t('nodes.boolean'),
   },
   BooleanCollection: {
     color: 'green.500',
-    description: 'A collection of booleans.',
-    title: 'Boolean Collection',
+    description: t('nodes.booleanCollectionDescription'),
+    title: t('nodes.booleanCollection'),
   },
   BooleanPolymorphic: {
     color: 'green.500',
-    description: 'A collection of booleans.',
-    title: 'Boolean Polymorphic',
+    description: t('nodes.booleanPolymorphicDescription'),
+    title: t('nodes.booleanPolymorphic'),
   },
   ClipField: {
     color: 'green.500',
-    description: 'Tokenizer and text_encoder submodels.',
-    title: 'Clip',
+    description: t('nodes.clipFieldDescription'),
+    title: t('nodes.clipField'),
   },
   Collection: {
     color: 'base.500',
-    description: 'TODO',
-    title: 'Collection',
+    description: t('nodes.collectionDescription'),
+    title: t('nodes.collection'),
   },
   CollectionItem: {
     color: 'base.500',
-    description: 'TODO',
-    title: 'Collection Item',
+    description: t('nodes.collectionItemDescription'),
+    title: t('nodes.collectionItem'),
   },
   ColorCollection: {
     color: 'pink.300',
-    description: 'A collection of colors.',
-    title: 'Color Collection',
+    description: t('nodes.colorCollectionDescription'),
+    title: t('nodes.colorCollection'),
   },
   ColorField: {
     color: 'pink.300',
-    description: 'A RGBA color.',
-    title: 'Color',
+    description: t('nodes.colorFieldDescription'),
+    title: t('nodes.colorField'),
   },
   ColorPolymorphic: {
     color: 'pink.300',
-    description: 'A collection of colors.',
-    title: 'Color Polymorphic',
+    description: t('nodes.colorPolymorphicDescription'),
+    title: t('nodes.colorPolymorphic'),
   },
   ConditioningCollection: {
     color: 'cyan.500',
-    description: 'Conditioning may be passed between nodes.',
-    title: 'Conditioning Collection',
+    description: t('nodes.conditioningCollectionDescription'),
+    title: t('nodes.conditioningCollection'),
   },
   ConditioningField: {
     color: 'cyan.500',
-    description: 'Conditioning may be passed between nodes.',
-    title: 'Conditioning',
+    description: t('nodes.conditioningFieldDescription'),
+    title: t('nodes.conditioningField'),
   },
   ConditioningPolymorphic: {
     color: 'cyan.500',
-    description: 'Conditioning may be passed between nodes.',
-    title: 'Conditioning Polymorphic',
+    description: t('nodes.conditioningPolymorphicDescription'),
+    title: t('nodes.conditioningPolymorphic'),
   },
   ControlCollection: {
     color: 'teal.500',
-    description: 'Control info passed between nodes.',
-    title: 'Control Collection',
+    description: t('nodes.controlCollectionDescription'),
+    title: t('nodes.controlCollection'),
   },
   ControlField: {
     color: 'teal.500',
-    description: 'Control info passed between nodes.',
-    title: 'Control',
+    description: t('nodes.controlFieldDescription'),
+    title: t('nodes.controlField'),
   },
   ControlNetModelField: {
     color: 'teal.500',
@@ -182,132 +183,132 @@ export const FIELDS: Record<FieldType, FieldUIConfig> = {
   },
   DenoiseMaskField: {
     color: 'blue.300',
-    description: 'Denoise Mask may be passed between nodes',
-    title: 'Denoise Mask',
+    description: t('nodes.denoiseMaskFieldDescription'),
+    title: t('nodes.denoiseMaskField'),
   },
   enum: {
     color: 'blue.500',
-    description: 'Enums are values that may be one of a number of options.',
-    title: 'Enum',
+    description: t('nodes.enumDescription'),
+    title: t('nodes.enum'),
   },
   float: {
     color: 'orange.500',
-    description: 'Floats are numbers with a decimal point.',
-    title: 'Float',
+    description: t('nodes.floatDescription'),
+    title: t('nodes.float'),
   },
   FloatCollection: {
     color: 'orange.500',
-    description: 'A collection of floats.',
-    title: 'Float Collection',
+    description: t('nodes.floatCollectionDescription'),
+    title: t('nodes.floatCollection'),
   },
   FloatPolymorphic: {
     color: 'orange.500',
-    description: 'A collection of floats.',
-    title: 'Float Polymorphic',
+    description: t('nodes.floatPolymorphicDescription'),
+    title: t('nodes.floatPolymorphic'),
   },
   ImageCollection: {
     color: 'purple.500',
-    description: 'A collection of images.',
-    title: 'Image Collection',
+    description: t('nodes.imageCollectionDescription'),
+    title: t('nodes.imageCollection'),
   },
   ImageField: {
     color: 'purple.500',
-    description: 'Images may be passed between nodes.',
-    title: 'Image',
+    description: t('nodes.imageFieldDescription'),
+    title: t('nodes.imageField'),
   },
   ImagePolymorphic: {
     color: 'purple.500',
-    description: 'A collection of images.',
-    title: 'Image Polymorphic',
+    description: t('nodes.imagePolymorphicDescription'),
+    title: t('nodes.imagePolymorphic'),
   },
   integer: {
     color: 'red.500',
-    description: 'Integers are whole numbers, without a decimal point.',
-    title: 'Integer',
+    description: t('nodes.integerDescription'),
+    title: t('nodes.integer'),
   },
   IntegerCollection: {
     color: 'red.500',
-    description: 'A collection of integers.',
-    title: 'Integer Collection',
+    description: t('nodes.integerCollectionDescription'),
+    title: t('nodes.integerCollection'),
   },
   IntegerPolymorphic: {
     color: 'red.500',
-    description: 'A collection of integers.',
-    title: 'Integer Polymorphic',
+    description: t('nodes.integerPolymorphicDescription'),
+    title: t('nodes.integerPolymorphic'),
   },
   LatentsCollection: {
     color: 'pink.500',
-    description: 'Latents may be passed between nodes.',
-    title: 'Latents Collection',
+    description: t('nodes.latentsCollectionDescription'),
+    title: t('nodes.latentsCollection'),
   },
   LatentsField: {
     color: 'pink.500',
-    description: 'Latents may be passed between nodes.',
-    title: 'Latents',
+    description: t('nodes.latentsFieldDescription'),
+    title: t('nodes.latentsField'),
   },
   LatentsPolymorphic: {
     color: 'pink.500',
-    description: 'Latents may be passed between nodes.',
-    title: 'Latents Polymorphic',
+    description: t('nodes.latentsPolymorphicDescription'),
+    title: t('nodes.latentsPolymorphic'),
   },
   LoRAModelField: {
     color: 'teal.500',
-    description: 'TODO',
-    title: 'LoRA',
+    description: t('nodes.loRAModelFieldDescription'),
+    title: t('nodes.loRAModelField'),
   },
   MainModelField: {
     color: 'teal.500',
-    description: 'TODO',
-    title: 'Model',
+    description: t('nodes.mainModelFieldDescription'),
+    title: t('nodes.mainModelField'),
   },
   ONNXModelField: {
     color: 'teal.500',
-    description: 'ONNX model field.',
-    title: 'ONNX Model',
+    description: t('nodes.oNNXModelFieldDescription'),
+    title: t('nodes.oNNXModelField'),
   },
   Scheduler: {
     color: 'base.500',
-    description: 'TODO',
-    title: 'Scheduler',
+    description: t('nodes.schedulerDescription'),
+    title: t('nodes.scheduler'),
   },
   SDXLMainModelField: {
     color: 'teal.500',
-    description: 'SDXL model field.',
-    title: 'SDXL Model',
+    description: t('nodes.sDXLMainModelFieldDescription'),
+    title: t('nodes.sDXLMainModelField'),
   },
   SDXLRefinerModelField: {
     color: 'teal.500',
-    description: 'TODO',
-    title: 'Refiner Model',
+    description: t('nodes.sDXLRefinerModelFieldDescription'),
+    title: t('nodes.sDXLRefinerModelField'),
   },
   string: {
     color: 'yellow.500',
-    description: 'Strings are text.',
-    title: 'String',
+    description: t('nodes.stringDescription'),
+    title: t('nodes.string'),
   },
   StringCollection: {
     color: 'yellow.500',
-    description: 'A collection of strings.',
-    title: 'String Collection',
+    description: t('nodes.stringCollectionDescription'),
+    title: t('nodes.stringCollection'),
   },
   StringPolymorphic: {
     color: 'yellow.500',
-    description: 'A collection of strings.',
-    title: 'String Polymorphic',
+    description: t('nodes.stringPolymorphicDescription'),
+    title: t('nodes.stringPolymorphic'),
   },
   UNetField: {
     color: 'red.500',
-    description: 'UNet submodel.',
-    title: 'UNet',
+    description: t('nodes.uNetFieldDescription'),
+    title: t('nodes.uNetField'),
   },
   VaeField: {
     color: 'blue.500',
-    description: 'Vae submodel.',
-    title: 'Vae',
+    description: t('nodes.vaeFieldDescription'),
+    title: t('nodes.vaeField'),
   },
   VaeModelField: {
     color: 'teal.500',
-    description: 'TODO',
-    title: 'VAE',
+    description: t('nodes.vaeModelFieldDescription'),
+    title: t('nodes.vaeModelField'),
   },
 };
diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts
index b1ad6a7b96..c92a41391c 100644
--- a/invokeai/frontend/web/src/features/nodes/types/types.ts
+++ b/invokeai/frontend/web/src/features/nodes/types/types.ts
@@ -20,6 +20,7 @@ import {
 import { O } from 'ts-toolbelt';
 import { JsonObject } from 'type-fest';
 import { z } from 'zod';
+import i18n from 'i18next';
 
 export type NonNullableGraph = O.Required<Graph, 'nodes' | 'edges'>;
 
@@ -1056,6 +1057,13 @@ export const isInvocationFieldSchema = (
 
 export type InvocationEdgeExtra = { type: 'default' | 'collapsed' };
 
+const zLoRAMetadataItem = z.object({
+  lora: zLoRAModelField.deepPartial(),
+  weight: z.number(),
+});
+
+export type LoRAMetadataItem = z.infer<typeof zLoRAMetadataItem>;
+
 export const zCoreMetadata = z
   .object({
     app_version: z.string().nullish(),
@@ -1075,14 +1083,7 @@ export const zCoreMetadata = z
       .union([zMainModel.deepPartial(), zOnnxModel.deepPartial()])
       .nullish(),
     controlnets: z.array(zControlField.deepPartial()).nullish(),
-    loras: z
-      .array(
-        z.object({
-          lora: zLoRAModelField.deepPartial(),
-          weight: z.number(),
-        })
-      )
-      .nullish(),
+    loras: z.array(zLoRAMetadataItem).nullish(),
     vae: zVaeModelField.nullish(),
     strength: z.number().nullish(),
     init_image: z.string().nullish(),
@@ -1258,23 +1259,35 @@ export const zValidatedWorkflow = zWorkflow.transform((workflow) => {
     const targetNode = keyedNodes[edge.target];
     const issues: string[] = [];
     if (!sourceNode) {
-      issues.push(`Output node ${edge.source} does not exist`);
+      issues.push(
+        `${i18n.t('nodes.outputNode')} ${edge.source} ${i18n.t(
+          'nodes.doesNotExist'
+        )}`
+      );
     } else if (
       edge.type === 'default' &&
       !(edge.sourceHandle in sourceNode.data.outputs)
     ) {
       issues.push(
-        `Output field "${edge.source}.${edge.sourceHandle}" does not exist`
+        `${i18n.t('nodes.outputField')}"${edge.source}.${
+          edge.sourceHandle
+        }" ${i18n.t('nodes.doesNotExist')}`
       );
     }
     if (!targetNode) {
-      issues.push(`Input node ${edge.target} does not exist`);
+      issues.push(
+        `${i18n.t('nodes.inputNode')} ${edge.target} ${i18n.t(
+          'nodes.doesNotExist'
+        )}`
+      );
     } else if (
       edge.type === 'default' &&
       !(edge.targetHandle in targetNode.data.inputs)
     ) {
       issues.push(
-        `Input field "${edge.target}.${edge.targetHandle}" does not exist`
+        `${i18n.t('nodes.inputField')} "${edge.target}.${
+          edge.targetHandle
+        }" ${i18n.t('nodes.doesNotExist')}`
       );
     }
     if (issues.length) {
@@ -1282,7 +1295,9 @@ export const zValidatedWorkflow = zWorkflow.transform((workflow) => {
       const src = edge.type === 'default' ? edge.sourceHandle : edge.source;
       const tgt = edge.type === 'default' ? edge.targetHandle : edge.target;
       warnings.push({
-        message: `Edge "${src} -> ${tgt}" skipped`,
+        message: `${i18n.t('nodes.edge')} "${src} -> ${tgt}" ${i18n.t(
+          'nodes.skipped'
+        )}`,
         issues,
         data: edge,
       });
diff --git a/invokeai/frontend/web/src/features/nodes/util/buildWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/buildWorkflow.ts
index b0ade42a9f..43ee75b735 100644
--- a/invokeai/frontend/web/src/features/nodes/util/buildWorkflow.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/buildWorkflow.ts
@@ -3,6 +3,7 @@ import { NodesState } from '../store/types';
 import { Workflow, zWorkflowEdge, zWorkflowNode } from '../types/types';
 import { fromZodError } from 'zod-validation-error';
 import { parseify } from 'common/util/serialize';
+import i18n from 'i18next';
 
 export const buildWorkflow = (nodesState: NodesState): Workflow => {
   const { workflow: workflowMeta, nodes, edges } = nodesState;
@@ -20,7 +21,7 @@ export const buildWorkflow = (nodesState: NodesState): Workflow => {
       const result = zWorkflowNode.safeParse(node);
       if (!result.success) {
         const { message } = fromZodError(result.error, {
-          prefix: 'Unable to parse node',
+          prefix: i18n.t('nodes.unableToParseNode'),
         });
         logger('nodes').warn({ node: parseify(node) }, message);
         return;
@@ -32,7 +33,7 @@ export const buildWorkflow = (nodesState: NodesState): Workflow => {
     const result = zWorkflowEdge.safeParse(edge);
     if (!result.success) {
       const { message } = fromZodError(result.error, {
-        prefix: 'Unable to parse edge',
+        prefix: i18n.t('nodes.unableToParseEdge'),
       });
       logger('nodes').warn({ edge: parseify(edge) }, message);
       return;
diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts
index 58a7726410..bfedc03de4 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts
@@ -79,8 +79,8 @@ export const buildCanvasInpaintGraph = (
   } = state.generation;
 
   if (!model) {
-    log.error('No model found in state');
-    throw new Error('No model found in state');
+    log.error('No Image found in state');
+    throw new Error('No Image found in state');
   }
 
   // The bounding box determines width and height, not the width and height params
diff --git a/invokeai/frontend/web/src/features/nodes/util/validateWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/validateWorkflow.ts
index a3085d516b..14b90fc731 100644
--- a/invokeai/frontend/web/src/features/nodes/util/validateWorkflow.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/validateWorkflow.ts
@@ -7,6 +7,7 @@ import {
   isWorkflowInvocationNode,
 } from '../types/types';
 import { parseify } from 'common/util/serialize';
+import i18n from 'i18next';
 
 export const validateWorkflow = (
   workflow: Workflow,
@@ -25,8 +26,14 @@ export const validateWorkflow = (
     const nodeTemplate = nodeTemplates[node.data.type];
     if (!nodeTemplate) {
       errors.push({
-        message: `Node "${node.data.type}" skipped`,
-        issues: [`Node type "${node.data.type}" does not exist`],
+        message: `${i18n.t('nodes.node')} "${node.data.type}" ${i18n.t(
+          'nodes.skipped'
+        )}`,
+        issues: [
+          `${i18n.t('nodes.nodeType')}"${node.data.type}" ${i18n.t(
+            'nodes.doesNotExist'
+          )}`,
+        ],
         data: node,
       });
       return;
@@ -38,9 +45,13 @@ export const validateWorkflow = (
       compareVersions(nodeTemplate.version, node.data.version) !== 0
     ) {
       errors.push({
-        message: `Node "${node.data.type}" has mismatched version`,
+        message: `${i18n.t('nodes.node')} "${node.data.type}" ${i18n.t(
+          'nodes.mismatchedVersion'
+        )}`,
         issues: [
-          `Node "${node.data.type}" v${node.data.version} may be incompatible with installed v${nodeTemplate.version}`,
+          `${i18n.t('nodes.node')} "${node.data.type}" v${
+            node.data.version
+          } ${i18n.t('nodes.maybeIncompatible')} v${nodeTemplate.version}`,
         ],
         data: { node, nodeTemplate: parseify(nodeTemplate) },
       });
@@ -52,33 +63,49 @@ export const validateWorkflow = (
     const targetNode = keyedNodes[edge.target];
     const issues: string[] = [];
     if (!sourceNode) {
-      issues.push(`Output node ${edge.source} does not exist`);
+      issues.push(
+        `${i18n.t('nodes.outputNode')} ${edge.source} ${i18n.t(
+          'nodes.doesNotExist'
+        )}`
+      );
     } else if (
       edge.type === 'default' &&
       !(edge.sourceHandle in sourceNode.data.outputs)
     ) {
       issues.push(
-        `Output field "${edge.source}.${edge.sourceHandle}" does not exist`
+        `${i18n.t('nodes.outputNodes')} "${edge.source}.${
+          edge.sourceHandle
+        }" ${i18n.t('nodes.doesNotExist')}`
       );
     }
     if (!targetNode) {
-      issues.push(`Input node ${edge.target} does not exist`);
+      issues.push(
+        `${i18n.t('nodes.inputNode')} ${edge.target} ${i18n.t(
+          'nodes.doesNotExist'
+        )}`
+      );
     } else if (
       edge.type === 'default' &&
       !(edge.targetHandle in targetNode.data.inputs)
     ) {
       issues.push(
-        `Input field "${edge.target}.${edge.targetHandle}" does not exist`
+        `${i18n.t('nodes.inputFeilds')} "${edge.target}.${
+          edge.targetHandle
+        }" ${i18n.t('nodes.doesNotExist')}`
       );
     }
     if (!nodeTemplates[sourceNode?.data.type ?? '__UNKNOWN_NODE_TYPE__']) {
       issues.push(
-        `Source node "${edge.source}" missing template "${sourceNode?.data.type}"`
+        `${i18n.t('nodes.sourceNode')} "${edge.source}" ${i18n.t(
+          'nodes.missingTemplate'
+        )} "${sourceNode?.data.type}"`
       );
     }
     if (!nodeTemplates[targetNode?.data.type ?? '__UNKNOWN_NODE_TYPE__']) {
       issues.push(
-        `Source node "${edge.target}" missing template "${targetNode?.data.type}"`
+        `${i18n.t('nodes.sourceNode')}"${edge.target}" ${i18n.t(
+          'nodes.missingTemplate'
+        )} "${targetNode?.data.type}"`
       );
     }
     if (issues.length) {
diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts
index 203ff2cb1b..bc850df0d0 100644
--- a/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts
+++ b/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts
@@ -1,6 +1,8 @@
+import { createSelector } from '@reduxjs/toolkit';
 import { useAppToaster } from 'app/components/Toaster';
-import { useAppDispatch } from 'app/store/storeHooks';
-import { CoreMetadata } from 'features/nodes/types/types';
+import { stateSelector } from 'app/store/store';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { CoreMetadata, LoRAMetadataItem } from 'features/nodes/types/types';
 import {
   refinerModelChanged,
   setNegativeStylePromptSDXL,
@@ -15,6 +17,11 @@ import {
 import { useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 import { ImageDTO } from 'services/api/types';
+import {
+  loraModelsAdapter,
+  useGetLoRAModelsQuery,
+} from '../../../services/api/endpoints/models';
+import { loraRecalled } from '../../lora/store/loraSlice';
 import { initialImageSelected, modelSelected } from '../store/actions';
 import {
   setCfgScale,
@@ -30,6 +37,7 @@ import {
 import {
   isValidCfgScale,
   isValidHeight,
+  isValidLoRAModel,
   isValidMainModel,
   isValidNegativePrompt,
   isValidPositivePrompt,
@@ -46,10 +54,16 @@ import {
   isValidWidth,
 } from '../types/parameterSchemas';
 
+const selector = createSelector(stateSelector, ({ generation }) => {
+  const { model } = generation;
+  return { model };
+});
+
 export const useRecallParameters = () => {
   const dispatch = useAppDispatch();
   const toaster = useAppToaster();
   const { t } = useTranslation();
+  const { model } = useAppSelector(selector);
 
   const parameterSetToast = useCallback(() => {
     toaster({
@@ -60,14 +74,18 @@ export const useRecallParameters = () => {
     });
   }, [t, toaster]);
 
-  const parameterNotSetToast = useCallback(() => {
-    toaster({
-      title: t('toast.parameterNotSet'),
-      status: 'warning',
-      duration: 2500,
-      isClosable: true,
-    });
-  }, [t, toaster]);
+  const parameterNotSetToast = useCallback(
+    (description?: string) => {
+      toaster({
+        title: t('toast.parameterNotSet'),
+        description,
+        status: 'warning',
+        duration: 2500,
+        isClosable: true,
+      });
+    },
+    [t, toaster]
+  );
 
   const allParameterSetToast = useCallback(() => {
     toaster({
@@ -78,14 +96,18 @@ export const useRecallParameters = () => {
     });
   }, [t, toaster]);
 
-  const allParameterNotSetToast = useCallback(() => {
-    toaster({
-      title: t('toast.parametersNotSet'),
-      status: 'warning',
-      duration: 2500,
-      isClosable: true,
-    });
-  }, [t, toaster]);
+  const allParameterNotSetToast = useCallback(
+    (description?: string) => {
+      toaster({
+        title: t('toast.parametersNotSet'),
+        status: 'warning',
+        description,
+        duration: 2500,
+        isClosable: true,
+      });
+    },
+    [t, toaster]
+  );
 
   /**
    * Recall both prompts with toast
@@ -307,6 +329,67 @@ export const useRecallParameters = () => {
     [dispatch, parameterSetToast, parameterNotSetToast]
   );
 
+  /**
+   * Recall LoRA with toast
+   */
+
+  const { loras } = useGetLoRAModelsQuery(undefined, {
+    selectFromResult: (result) => ({
+      loras: result.data
+        ? loraModelsAdapter.getSelectors().selectAll(result.data)
+        : [],
+    }),
+  });
+
+  const prepareLoRAMetadataItem = useCallback(
+    (loraMetadataItem: LoRAMetadataItem) => {
+      if (!isValidLoRAModel(loraMetadataItem.lora)) {
+        return { lora: null, error: 'Invalid LoRA model' };
+      }
+
+      const { base_model, model_name } = loraMetadataItem.lora;
+
+      const matchingLoRA = loras.find(
+        (l) => l.base_model === base_model && l.model_name === model_name
+      );
+
+      if (!matchingLoRA) {
+        return { lora: null, error: 'LoRA model is not installed' };
+      }
+
+      const isCompatibleBaseModel =
+        matchingLoRA?.base_model === model?.base_model;
+
+      if (!isCompatibleBaseModel) {
+        return {
+          lora: null,
+          error: 'LoRA incompatible with currently-selected model',
+        };
+      }
+
+      return { lora: matchingLoRA, error: null };
+    },
+    [loras, model?.base_model]
+  );
+
+  const recallLoRA = useCallback(
+    (loraMetadataItem: LoRAMetadataItem) => {
+      const result = prepareLoRAMetadataItem(loraMetadataItem);
+
+      if (!result.lora) {
+        parameterNotSetToast(result.error);
+        return;
+      }
+
+      dispatch(
+        loraRecalled({ ...result.lora, weight: loraMetadataItem.weight })
+      );
+
+      parameterSetToast();
+    },
+    [prepareLoRAMetadataItem, dispatch, parameterSetToast, parameterNotSetToast]
+  );
+
   /*
    * Sets image as initial image with toast
    */
@@ -344,6 +427,7 @@ export const useRecallParameters = () => {
         refiner_positive_aesthetic_score,
         refiner_negative_aesthetic_score,
         refiner_start,
+        loras,
       } = metadata;
 
       if (isValidCfgScale(cfg_scale)) {
@@ -425,9 +509,21 @@ export const useRecallParameters = () => {
         dispatch(setRefinerStart(refiner_start));
       }
 
+      loras?.forEach((lora) => {
+        const result = prepareLoRAMetadataItem(lora);
+        if (result.lora) {
+          dispatch(loraRecalled({ ...result.lora, weight: lora.weight }));
+        }
+      });
+
       allParameterSetToast();
     },
-    [allParameterNotSetToast, allParameterSetToast, dispatch]
+    [
+      allParameterNotSetToast,
+      allParameterSetToast,
+      dispatch,
+      prepareLoRAMetadataItem,
+    ]
   );
 
   return {
@@ -444,6 +540,7 @@ export const useRecallParameters = () => {
     recallWidth,
     recallHeight,
     recallStrength,
+    recallLoRA,
     recallAllParameters,
     sendToImageToImage,
   };
diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts
index 9be8bd13f6..9db7762344 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/models.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts
@@ -128,7 +128,7 @@ export const mainModelsAdapter = createEntityAdapter<MainModelConfigEntity>({
 const onnxModelsAdapter = createEntityAdapter<OnnxModelConfigEntity>({
   sortComparer: (a, b) => a.model_name.localeCompare(b.model_name),
 });
-const loraModelsAdapter = createEntityAdapter<LoRAModelConfigEntity>({
+export const loraModelsAdapter = createEntityAdapter<LoRAModelConfigEntity>({
   sortComparer: (a, b) => a.model_name.localeCompare(b.model_name),
 });
 export const controlNetModelsAdapter =
diff --git a/pyproject.toml b/pyproject.toml
index 63ac3b7c12..2ea5455c3d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -198,6 +198,13 @@ output = "coverage/index.xml"
 max-line-length = 120
 ignore = ["E203", "E266", "E501", "W503"]
 select = ["B", "C", "E", "F", "W", "T4"]
+exclude = [
+  ".git",
+  "__pycache__",
+  "build",
+  "dist",
+  "invokeai/frontend/web/node_modules/"
+]
 
 [tool.black]
 line-length = 120