diff --git a/src/components/monitor/monitor.jsx b/src/components/monitor/monitor.jsx
index edd0fb0439a39ab0ccc080c67aa6cd068e603a3a..54b1ebe8a6cb6e3a20b78dadfc8fe7fd3a477bb7 100644
--- a/src/components/monitor/monitor.jsx
+++ b/src/components/monitor/monitor.jsx
@@ -7,7 +7,7 @@ const styles = require('./monitor.css');
 const categories = {
     data: '#FF8C1A',
     sensing: '#5CB1D6',
-    sounds: '#CF63CF',
+    sound: '#CF63CF',
     looks: '#9966FF',
     motion: '#4C97FF'
 };
diff --git a/src/lib/monitor-adapter.js b/src/lib/monitor-adapter.js
new file mode 100644
index 0000000000000000000000000000000000000000..8dcc802a48e9414062431e6ca9137f8d2eef6cde
--- /dev/null
+++ b/src/lib/monitor-adapter.js
@@ -0,0 +1,26 @@
+/**
+ * Convert monitors from VM format to what the GUI needs to render.
+ * - Convert opcode to a label and a category
+ * - Add missing XY position data if needed
+ */
+const OpcodeLabels = require('../lib/opcode-labels.js');
+
+const PADDING = 5;
+const MONITOR_HEIGHT = 23;
+
+const isUndefined = a => typeof a === 'undefined';
+
+module.exports = function ({id, opcode, params, value, x, y}, monitorIndex) {
+    let {label, category, labelFn} = OpcodeLabels(opcode);
+
+    // Use labelFn if provided for dynamic labelling (e.g. variables)
+    if (!isUndefined(labelFn)) label = labelFn(params);
+
+    // Simple layout if x or y are undefined
+    // @todo scratch2 has a more complex layout behavior we may want to adopt
+    // @todo e.g. this does not work well when monitors have already been moved
+    if (isUndefined(x)) x = PADDING;
+    if (isUndefined(y)) y = PADDING + (monitorIndex * (PADDING + MONITOR_HEIGHT));
+
+    return {id, label, category, value, x, y};
+};
diff --git a/src/lib/opcode-labels.js b/src/lib/opcode-labels.js
new file mode 100644
index 0000000000000000000000000000000000000000..bebe14743630de643343e9b88318b88fdf312b07
--- /dev/null
+++ b/src/lib/opcode-labels.js
@@ -0,0 +1,75 @@
+const opcodeMap = {
+    // Motion
+    motion_direction: {
+        category: 'motion',
+        label: 'direction'
+    },
+    motion_xposition: {
+        category: 'motion',
+        label: 'x position'
+    },
+    motion_yposition: {
+        category: 'motion',
+        label: 'y position'
+    },
+
+    // Looks
+    looks_size: {
+        category: 'looks',
+        label: 'size'
+    },
+    looks_costumeorder: {
+        category: 'looks',
+        label: 'costume #'
+    },
+    looks_backdroporder: {
+        category: 'looks',
+        label: 'backdrop #'
+    },
+    looks_backdropname: {
+        category: 'looks',
+        label: 'backdrop name'
+    },
+
+    // Data
+    data_variable: {
+        category: 'data',
+        labelFn: params => params.VARIABLE
+    },
+
+    // Sound
+    sound_volume: {
+        category: 'sound',
+        label: 'volume'
+    },
+    sound_tempo: {
+        category: 'sound',
+        label: 'tempo'
+    },
+
+    // Sensing
+    sensing_loudness: {
+        category: 'sensing',
+        label: 'loudness'
+    },
+    sensing_of: {
+        category: 'sensing',
+        labelFn: params => `${params.PROPERTY} of ${params.OBJECT}`
+    },
+    sensing_current: {
+        category: 'sensing',
+        labelFn: params => params.CURRENTMENU.toLowerCase()
+    },
+    sensing_timer: {
+        category: 'sensing',
+        label: 'timer'
+    }
+};
+
+module.exports = function (opcode) {
+    if (opcode in opcodeMap) return opcodeMap[opcode];
+    return {
+        category: 'data',
+        label: opcode
+    };
+};
diff --git a/src/reducers/monitors.js b/src/reducers/monitors.js
index e8b4fa947eee4862f24be5ea8727caef6793fe5c..18e9529f4ad417645101a5cc85ed60b6b8adecf0 100644
--- a/src/reducers/monitors.js
+++ b/src/reducers/monitors.js
@@ -1,3 +1,5 @@
+const monitorAdapter = require('../lib/monitor-adapter.js');
+
 const UPDATE_MONITORS = 'scratch-gui/monitors/UPDATE_MONITORS';
 
 const initialState = [];
@@ -6,7 +8,7 @@ const reducer = function (state, action) {
     if (typeof state === 'undefined') state = initialState;
     switch (action.type) {
     case UPDATE_MONITORS:
-        return [...action.monitors];
+        return action.monitors.map(monitorAdapter);
     default:
         return state;
     }