diff --git a/Procfile b/Procfile
index 513dddcb384864aa9a295fbea8eed87846cc39fa..91d50767bbcd82317e579cfc7f256cbb1e40e19c 100644
--- a/Procfile
+++ b/Procfile
@@ -1 +1 @@
-web: ./bin/start
+web: HEROKU=true ./bin/start
diff --git a/app.json b/app.json
index c7ec46163075e4825d494649d4da7dc4907f6007..c9d6bcaf1133565221299241229c6f516b849b92 100644
--- a/app.json
+++ b/app.json
@@ -5,7 +5,8 @@
     "business intelligence",
     "analytics",
     "dashboard",
-    "charting"
+    "charting",
+    "metabase"
   ],
   "website": "http://www.metabase.com/",
   "repository": "https://github.com/metabase/metabase",
diff --git a/bin/docker/build_image.sh b/bin/docker/build_image.sh
index 83cb7b72900389661e15296be60a39145646a0af..5224a2184da3c054adb6be378b93dd9c0c5fd9d6 100755
--- a/bin/docker/build_image.sh
+++ b/bin/docker/build_image.sh
@@ -1,4 +1,6 @@
-#!/bin/bash
+#! /usr/bin/env bash
+
+set -eu
 
 BASEDIR=$(dirname $0)
 PROJECT_ROOT="$BASEDIR/../.."
@@ -26,8 +28,8 @@ if [ "$4" == "--latest" ]; then
     LATEST="YES"
 fi
 
-if [ "$PUBLISH" == "YES" ] && [ -z "$DOCKERHUB_EMAIL" -o -z "$DOCKERHUB_USERNAME" -o -z "$DOCKERHUB_PASSWORD" ]; then
-    echo "In order to publish an image to Dockerhub you must set \$DOCKERHUB_EMAIL, \$DOCKERHUB_USERNAME and \$DOCKERHUB_PASSWORD before running."
+if [ "$PUBLISH" == "YES" ] && [ -z "$DOCKERHUB_USERNAME" -o -z "$DOCKERHUB_PASSWORD" ]; then
+    echo "In order to publish an image to Dockerhub you must set \$DOCKERHUB_USERNAME and \$DOCKERHUB_PASSWORD before running."
     exit 1
 fi
 
@@ -77,7 +79,7 @@ if [ "$PUBLISH" == "YES" ]; then
     echo "Publishing image ${DOCKER_IMAGE} to Dockerhub"
 
     # make sure that we are logged into dockerhub
-    docker login --email="${DOCKERHUB_EMAIL}" --username="${DOCKERHUB_USERNAME}" --password="${DOCKERHUB_PASSWORD}"
+    docker login --username="${DOCKERHUB_USERNAME}" --password="${DOCKERHUB_PASSWORD}"
 
     # push the built image to dockerhub
     docker push ${DOCKER_IMAGE}
@@ -99,4 +101,3 @@ fi
 rm -f ${BASEDIR}/metabase.jar
 
 echo "Done"
-
diff --git a/bin/start b/bin/start
index 8a4a9baeedce050edc7d936160657a3f7909323f..191b96872fe7293e9b4b317ac5f6816832f353bb 100755
--- a/bin/start
+++ b/bin/start
@@ -82,9 +82,29 @@ if [ ! -z "$RDS_HOSTNAME" ]; then
     export MB_DB_PORT=$RDS_PORT
 fi
 
-JAVA_OPTS="${JAVA_OPTS} -XX:+IgnoreUnrecognizedVMOptions"
-JAVA_OPTS="${JAVA_OPTS} -Dfile.encoding=UTF-8"
-JAVA_OPTS="${JAVA_OPTS} --add-opens=java.base/java.net=ALL-UNNAMED"
-JAVA_OPTS="${JAVA_OPTS} --add-modules=java.xml.bind"
+# Determine whether we're on Heroku on a free, hobby, or 1x dyno.
+#
+# We set $HEROKU in the Procfile; we know we're on a baby dyno if the process limit is 256 per user.
+#
+# On a baby dyno we need to override the $JAVA_OPTS and give it a slightly lower memory limit because Heroku tends to think
+# we can use more memory than we actually can. It defaults to giving us 300m but that still ends up going over the 512MB
+# limit for the dyno. Set a few other additional options to minimize memory usage as well.
+if [ -n "$HEROKU" ] && [ `ulimit -u` = 256 ]; then
+    JAVA_OPTS="$JAVA_OPTS -Xmx248m"                        # This seems to be the right amount that prevents the dyno from going over the quota
+    JAVA_OPTS="$JAVA_OPTS -XX:-UseGCOverheadLimit"         # Disable limit to amount of time spent in GC. Better slow than not working at all
+    JAVA_OPTS="$JAVA_OPTS -XX:+UseConcMarkSweepGC"         # ConcMarkSweepGC seems to cause less OOM issues in my testing on low-mem Heroku envs
+    JAVA_OPTS="$JAVA_OPTS -XX:+CMSClassUnloadingEnabled"   # Not 100% sure this does anything in Java 8 but if it does, we want to enable it
+    JAVA_OPTS="$JAVA_OPTS -XX:+UseCompressedOops"          # Use 32-bit pointers. Reduces memory usage and GC events
+    JAVA_OPTS="$JAVA_OPTS -XX:+UseCompressedClassPointers" # Same as above. See also http://blog.leneghan.com/2012/03/reducing-java-memory-usage-and-garbage.html
+fi
+
+# Other Java options
+JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions"           # Don't barf if we see an option we don't understand (e.g. Java 9 option on Java 7/8)
+JAVA_OPTS="$JAVA_OPTS -Djava.awt.headless=true"                   # don't try to start AWT. Not sure this does anything but better safe than wasting memory
+JAVA_OPTS="$JAVA_OPTS -Dfile.encoding=UTF-8"                      # Use UTF-8
+JAVA_OPTS="$JAVA_OPTS --add-opens=java.base/java.net=ALL-UNNAMED" # Allow dynamically adding JARs to classpath (Java 9)
+JAVA_OPTS="$JAVA_OPTS --add-modules=java.xml.bind"                # Enable access to java.xml.bind module (Java 9)
+
+echo "Using these JAVA_OPTS: ${JAVA_OPTS}"
 
 exec java $JAVA_OPTS -jar ./target/uberjar/metabase.jar
diff --git a/bin/version b/bin/version
index 5b4d28e51845666f980e88ddaa0cd15a50508e05..cd6f9f6b43f126579811e612f3027e3b8b26b020 100755
--- a/bin/version
+++ b/bin/version
@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 
-VERSION="v0.27.0-snapshot"
+VERSION="v0.28.0-snapshot"
 
 # dynamically pull more interesting stuff from latest git commit
 HASH=$(git show-ref --head --hash=7 head)            # first 7 letters of hash should be enough; that's what GitHub uses
diff --git a/docs/administration-guide/13-embedding.md b/docs/administration-guide/13-embedding.md
index 950d545bfe4fe5f025463a146b2144371080f840..92f6caf1196eb9e6046ef3a8da33cfa1cdea0ccf 100644
--- a/docs/administration-guide/13-embedding.md
+++ b/docs/administration-guide/13-embedding.md
@@ -30,9 +30,9 @@ You can also see all questions and dashboards that have been marked as "Embeddab
 
 Once you've enabled the embedding feature on your Metabase instance, you should then go to the individual questions and dashboards you wish to embed to set them up for embedding.
 
-### Embedding Charts and Dashboards
+### Embedding charts and dashboards
 
-To mark a given question or dashboard, click on the sharing icon
+To make a question or dashboard embeddable, click the sharing icon on it:
 
 ![Share icon](images/embedding/02-share-icon.png)
 
@@ -51,12 +51,12 @@ Importantly, you will need to hit "Publish" when you first set up a  chart or da
 We provide code samples for common front end template languages as well as some common back-end web frameworks and languages. You may also use these as starting points for writing your own versions in other platforms.
 
 
-### Embedding Charts and Dashboards with locked parameters
+### Embedding charts and dashboards with locked parameters
 If you wish to have a parameter locked down to prevent your embedding application's end users from seeing other users' data, you can mark parameters as "Locked."Once a parameter is marked as Locked, it is not displayed as a filter widget, and must be set by the embedding application's server code.
 
 ![Locked parameters](images/embedding/06-locked.png)
 
-### Resizing Dashboards to fit their content
+### Resizing dashboards to fit their content
 Dashboards are a fixed aspect ratio, so if you'd like to ensure they're automatically sized vertically to fit their contents you can use the [iFrame Resizer](https://github.com/davidjbradshaw/iframe-resizer) script. Metabase serves a copy for convenience:
 ```
 <script src="http://metabase.example.com/app/iframeResizer.js"></script>
@@ -65,3 +65,7 @@ Dashboards are a fixed aspect ratio, so if you'd like to ensure they're automati
 
 ### Reference applications
 To see concrete examples of how to embed Metabase in applications under a number of common frameworks, check out our [reference implementations](https://github.com/metabase/embedding-reference-apps) on Github.
+
+
+## Premium embedding
+If you'd like to embed Metabase dashboards or charts in your application without the "Powered by Metabase" attribution, you can purchase premium embedding from the Metabase store. [Find out more here](https://store.metabase.com/product/embedding).
diff --git a/docs/api-documentation.md b/docs/api-documentation.md
index 2b303f31424308f677dd11b22e406cc1e4ad53a1..d8b3c001d721cbd0b7f54f227771308836850390 100644
--- a/docs/api-documentation.md
+++ b/docs/api-documentation.md
@@ -127,17 +127,20 @@ Delete a `Card`.
 
 ## `GET /api/card/`
 
-Get all the `Cards`. Option filter param `f` can be used to change the set of Cards that are returned; default is `all`,
-   but other options include `mine`, `fav`, `database`, `table`, `recent`, `popular`, and `archived`. See corresponding implementation
-   functions above for the specific behavior of each filter option. :card_index:
+Get all the `Cards`. Option filter param `f` can be used to change the set of Cards that are returned; default is
+  `all`, but other options include `mine`, `fav`, `database`, `table`, `recent`, `popular`, and `archived`. See
+  corresponding implementation functions above for the specific behavior of each filter option. :card_index:
 
-   Optionally filter cards by LABEL or COLLECTION slug. (COLLECTION can be a blank string, to signify cards with *no collection* should be returned.)
+  Optionally filter cards by LABEL or COLLECTION slug. (COLLECTION can be a blank string, to signify cards with *no
+  collection* should be returned.)
 
-   NOTES:
+  NOTES:
 
-   *  Filtering by LABEL is considered *deprecated*, as `Labels` will be removed from an upcoming version of Metabase in favor of `Collections`.
-   *  LABEL and COLLECTION params are mutually exclusive; if both are specified, LABEL will be ignored and Cards will only be filtered by their `Collection`.
-   *  If no `Collection` exists with the slug COLLECTION, this endpoint will return a 404.
+  *  Filtering by LABEL is considered *deprecated*, as `Labels` will be removed from an upcoming version of Metabase
+     in favor of `Collections`.
+  *  LABEL and COLLECTION params are mutually exclusive; if both are specified, LABEL will be ignored and Cards will
+     only be filtered by their `Collection`.
+  *  If no `Collection` exists with the slug COLLECTION, this endpoint will return a 404.
 
 ##### PARAMS:
 
@@ -161,7 +164,8 @@ Get `Card` with ID.
 
 ## `GET /api/card/embeddable`
 
-Fetch a list of Cards where `enable_embedding` is `true`. The cards can be embedded using the embedding endpoints and a signed JWT.
+Fetch a list of Cards where `enable_embedding` is `true`. The cards can be embedded using the embedding endpoints
+  and a signed JWT.
 
 You must be a superuser to do this.
 
@@ -219,9 +223,9 @@ Update the set of `Labels` that apply to a `Card`.
 
 ## `POST /api/card/:card-id/public_link`
 
-Generate publically-accessible links for this Card. Returns UUID to be used in public links.
-   (If this Card has already been shared, it will return the existing public link rather than creating a new one.)
-   Public sharing must be enabled.
+Generate publically-accessible links for this Card. Returns UUID to be used in public links. (If this Card has
+  already been shared, it will return the existing public link rather than creating a new one.)  Public sharing must
+  be enabled.
 
 You must be a superuser to do this.
 
@@ -245,7 +249,8 @@ Run the query associated with a Card.
 
 ## `POST /api/card/:card-id/query/:export-format`
 
-Run the query associated with a Card, and return its results as a file in the specified format. Note that this expects the parameters as serialized JSON in the 'parameters' parameter
+Run the query associated with a Card, and return its results as a file in the specified format. Note that this
+  expects the parameters as serialized JSON in the 'parameters' parameter
 
 ##### PARAMS:
 
@@ -258,8 +263,8 @@ Run the query associated with a Card, and return its results as a file in the sp
 
 ## `POST /api/card/collections`
 
-Bulk update endpoint for Card Collections. Move a set of `Cards` with CARD_IDS into a `Collection` with COLLECTION_ID,
-   or remove them from any Collections by passing a `null` COLLECTION_ID.
+Bulk update endpoint for Card Collections. Move a set of `Cards` with CARD_IDS into a `Collection` with
+  COLLECTION_ID, or remove them from any Collections by passing a `null` COLLECTION_ID.
 
 ##### PARAMS:
 
diff --git a/docs/users-guide/14-x-rays.md b/docs/users-guide/14-x-rays.md
index afa722830f1e1e2b1a6c896be1b653e09d856be3..2c9a64fca48924675011fd246e31ce6e67b423a9 100644
--- a/docs/users-guide/14-x-rays.md
+++ b/docs/users-guide/14-x-rays.md
@@ -33,7 +33,9 @@ X-rays can be a somewhat costly or slow operation for your database to run, so b
 
 ![X-ray fidelity](images/x-ray-fidelity.png)
 
-### Comparing a segment
+Administrators can also set the maximum allowed fidelity for x-rays in the Admin Panel. Note that the `Extended` setting is required for time series x-rays to work. Admins can even turn x-rays off entirely, but that makes Simon cry. No one likes it when Simon cries.
+
+### Comparing segments
 
 Segments are a subset of a larger table or list, so one thing you can do when viewing an x-ray of a segment is compare it to its "parent" table. For example, if I have a segment called "Californians," which is a subset of the "People" table, I can click on the button that says "Compare to all People" to see a comparison report:
 
@@ -44,3 +46,13 @@ The comparison report shows how many rows there are in the segment versus the pa
 ![Comparison report](images/x-ray-comparison.png)
 
 An example for where this can be especially useful is a scenario where you've defined many different segments for your users or customers, like "Repeat Customers," "Users between 18 and 35," or "Female customers in Kalamazoo who dislike cheese." You can open up the x-ray for any of these segments, and then compare them to the larger Users or Customers table to see if there are any interesting patterns or differences.
+
+## Automated insights
+Metabase hasn't quite achieved self-awareness, but it has gotten smarter recently. It will now show you relevant insights about your data at the top of x-rays about time series or numeric fields, provided there's something insightful to say.
+
+![Insights](./images/insights.png)
+
+Insights include things like whether or not your data has an overall trend, has uncharacteristic spikes or dips, or if it follows a similar pattern at regular intervals.
+
+## Need help?
+If you still have questions about x-rays or comparisons, you can head over to our [discussion forum](http://discourse.metabase.com/). See you there!
diff --git a/docs/users-guide/15-alerts.md b/docs/users-guide/15-alerts.md
new file mode 100644
index 0000000000000000000000000000000000000000..28d96a19ef2b67cf15d5ce2cfaace91da949bdcc
--- /dev/null
+++ b/docs/users-guide/15-alerts.md
@@ -0,0 +1,71 @@
+
+## Getting alerts about questions
+Whether you're keeping track of revenue, users, or negative reviews, there are often times when you want to be alerted about something. Metabase has a few different kinds of alerts you can set up, and you can choose to be notified via email or Slack.
+
+### Getting alerts
+To start using alerts, someone on your team who's an administrator will need to make sure that [email integration](../administrator-guide/02-setting-up-email.md) is set up first.
+
+### Types of alerts
+There are three kinds of things you can get alerted about in Metabase:
+1. When a time series crosses a goal line.
+2. When a progress bar reaches or goes below its goal.
+3. When any other kind of question returns a result.
+
+We'll go through these one by one.
+
+### Goal line alerts
+This kind of alert is useful when you're doing things like tracking daily active users and you want to know when you reach a certain number of them, or when you're tracking orders per week and you want to know whenever that number ever goes below a certain threshold.
+
+To start, you'll need a line, area, or bar chart displaying a number over time. (If you need help with that, check out the page on [asking questions](04-asking-questions).)
+
+Now we need to set up a goal line. To do that, open up the visualization settings by clicking the gear icon next to the dropdown where you chose your chart type. Then click on the Display tab, and turn on the "Show goal" setting. Choose a value for your goal and click Done.
+
+Save your question, then click on the menu button in the top right of the screen and click on "Get alerts about this."
+
+![Get alerts](./images/alerts/get-alerts-about-this.png)
+
+This is where you'll get to choose a few things:
+- Whether you want to be alerted when the time series goes above the goal line or when it goes below it.
+- Whether you only wanted to be alerted every time this happens or only the first time.
+- How often you want Metabase to check to see if the goal line has been crossed.
+
+![Goal line alert options](./images/alerts/goal-line-options.png)
+
+Click Done, and your alert will be all set up! You'll get an email confirmation, too. If you need to edit or unsubscribe from the alert you set up, just open up that same menu. It'll say "Alerts are on," so just click that, and you'll see the Edit and Unsubscribe buttons. This is also where you'll see alerts about this question that administrators might have added you to.
+
+![Edit menu](./images/alerts/edit-menu.png)
+
+### Progress bar alerts
+Setting up this kind of alert is really similar to setting up a goal line alert. First, create a question that returns a single number as its result, then choose the Progress Bar chart type from the Visualization menu. Click the gear to select a goal value, click Done, then save your question.
+
+Next, open up the menu in the top-right, click "Get alerts about this," and you'll see that same screen of options for when you want to get alerts about this progress bar.
+
+### Results alerts
+Lastly, you can get an alert when one of your saved questions returns any result. This kind of alert is the most useful if you have a question that doesn't *usually* return any results, but you just want to know when it *does*. For example, you might have a table called `Reviews`, and you want to know any time a customer leaves a bad review, which you consider to be anything below three stars. To set up an alert for this situation, you'd go and create a raw data question (i.e., a question that returns a list of reviews), and add a filter to only include results with one or two stars.
+
+![Bad reviews](./images/alerts/bad-reviews.png)
+
+You probably don't want to be alerted about all the bad reviews you've *ever* gotten, but just recent ones, you'd probably also add a filter to only include results from yesterday or today, depending on how often you want to check for these bad reviews. At this point, when you check the results of this question, it probably won't return any results, which is a good thing.
+
+![No results](./images/alerts/no-results.png)
+
+Save the question, the click on "get alerts about this" from the menu in the top-right of the screen, and select how often you want Metabase to check this question for results. That's it!
+
+### Adding additional recipients to your alerts
+If you're an administrator of your Metabase instance, you'll be able to see and edit every alert on all saved questions. You'll also see some additional options to add recipients to alerts, which look like this:
+
+![Recipients](./images/alerts/recipients.png)
+
+Just like with [Pulses](10-pulses.md), you can add any Metabase user, email address, or even a Slack channel as a recipient of an alert. Admins can add or remove recipients on any alert, even ones that they did not create themselves.
+
+Here's more information about [setting up email integration](../administration-guide/02-setting-up-email.md) and [setting up Slack integration](../administration-guide/09-setting-up-slack.md).
+
+### Stopping alerts
+There are a few ways alerts can be stopped:
+- Regular users can unsubscribe from any alert that they're a recipient of.
+- Admins can edit any alert and delete it entirely. This can't be undone, so be careful!
+- If a saved question that has an alert on it gets edited in such a way that the alert doesn't make sense anymore, the alert will get deleted. For example, if a saved question with a goal line alert on it gets edited, and the goal line is removed entirely, that alert will get deleted.
+- If a question gets archived, any alerts on it will be deleted.
+
+## That’s it!
+If you still have questions about using alerts, you can head over to our [discussion forum](http://discourse.metabase.com/). See you there!
diff --git a/docs/users-guide/images/alerts/bad-reviews.png b/docs/users-guide/images/alerts/bad-reviews.png
new file mode 100644
index 0000000000000000000000000000000000000000..c3fa0c784f72e990f4f3e92179bdc2edb82d515b
Binary files /dev/null and b/docs/users-guide/images/alerts/bad-reviews.png differ
diff --git a/docs/users-guide/images/alerts/edit-menu.png b/docs/users-guide/images/alerts/edit-menu.png
new file mode 100644
index 0000000000000000000000000000000000000000..f9d5c9a3bd7060cca4a552fcd6e55e7c40105487
Binary files /dev/null and b/docs/users-guide/images/alerts/edit-menu.png differ
diff --git a/docs/users-guide/images/alerts/get-alerts-about-this.png b/docs/users-guide/images/alerts/get-alerts-about-this.png
new file mode 100644
index 0000000000000000000000000000000000000000..fa4e722a21476f6e04d011ea8d58c2ef1157ccea
Binary files /dev/null and b/docs/users-guide/images/alerts/get-alerts-about-this.png differ
diff --git a/docs/users-guide/images/alerts/goal-line-options.png b/docs/users-guide/images/alerts/goal-line-options.png
new file mode 100644
index 0000000000000000000000000000000000000000..dbc901da87e62464599327e16579cefbe6db21b1
Binary files /dev/null and b/docs/users-guide/images/alerts/goal-line-options.png differ
diff --git a/docs/users-guide/images/alerts/no-results.png b/docs/users-guide/images/alerts/no-results.png
new file mode 100644
index 0000000000000000000000000000000000000000..15a34e4c206841bd9c483469f7cf832c795e30d9
Binary files /dev/null and b/docs/users-guide/images/alerts/no-results.png differ
diff --git a/docs/users-guide/images/alerts/recipients.png b/docs/users-guide/images/alerts/recipients.png
new file mode 100644
index 0000000000000000000000000000000000000000..cd68acf9a51d6865c7d1acabbaf67b0ddf1fdfe5
Binary files /dev/null and b/docs/users-guide/images/alerts/recipients.png differ
diff --git a/docs/users-guide/images/insights.png b/docs/users-guide/images/insights.png
new file mode 100644
index 0000000000000000000000000000000000000000..a7dd4dc24ed3715334f44b013281912d555c58f9
Binary files /dev/null and b/docs/users-guide/images/insights.png differ
diff --git a/docs/users-guide/start.md b/docs/users-guide/start.md
index 8a57fe4c703d586f3918ab43455a835deea6f230..37cc3ea6441293378eea4f0e8fd8d879c6664ee9 100644
--- a/docs/users-guide/start.md
+++ b/docs/users-guide/start.md
@@ -16,5 +16,6 @@
 *   [Some helpful tips on building your data model](12-data-model-reference.md)
 *   [Creating SQL Templates](13-sql-parameters.md)
 *   [Viewing X-ray reports](14-x-rays.md)
+*   [Getting alerts](15-alerts.md)
 
 Let's get started with an overview of [What Metabase does](01-what-is-metabase.md).
diff --git a/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLevel.jsx b/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLevel.jsx
index 4c7d5d069be77f8e4413f977d34c38f0f9d7793a..84a9c743dc539eaaf6daa6b39f39c061bed7367b 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLevel.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLevel.jsx
@@ -26,7 +26,7 @@ const PremiumTokenInput = ({ token, onChangeSetting }) =>
 const PremiumExplanation = ({ showEnterScreen }) =>
     <div>
         <h2>Premium embedding</h2>
-        <p className="mt1">Premium embedding lets you disable "Powered by Metabase" on your embeded dashboards and questions.</p>
+        <p className="mt1">Premium embedding lets you disable "Powered by Metabase" on your embedded dashboards and questions.</p>
         <div className="mt2 mb3">
             <a className="link mx1" href={PREMIUM_EMBEDDING_STORE_URL} target="_blank">
                 Buy a token
diff --git a/frontend/src/metabase/admin/settings/components/widgets/PremiumEmbeddingWidget.jsx b/frontend/src/metabase/admin/settings/components/widgets/PremiumEmbeddingWidget.jsx
deleted file mode 100644
index 2394a20a3404bf0e0ec664dca7f523c6ada93d76..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/admin/settings/components/widgets/PremiumEmbeddingWidget.jsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React, { Component } from "react";
-
-import MetabaseSettings from "metabase/lib/settings";
-
-import SettingInput from "./SettingInput.jsx";
-
-export default class PremiumEmbeddingWidget extends Component {
-    render() {
-        return (
-            <div>
-                <SettingInput {...this.props} />
-                <h3 className="mt4 mb4">
-                    Getting your very own premium embedding token
-                </h3>
-                <a href={MetabaseSettings.metastoreUrl()} target="_blank">
-                    Visit the MetaStore
-                </a>
-            </div>
-        );
-    }
-}
diff --git a/frontend/src/metabase/components/ChannelSetupMessage.jsx b/frontend/src/metabase/components/ChannelSetupMessage.jsx
index 96ad7087777cbec1d12c0c001ae16c44cf558867..78bd045a7637856c8f9e177a84817df5fc84061d 100644
--- a/frontend/src/metabase/components/ChannelSetupMessage.jsx
+++ b/frontend/src/metabase/components/ChannelSetupMessage.jsx
@@ -2,6 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
+import { t } from 'c-3po';
 
 import Settings from "metabase/lib/settings";
 
@@ -22,7 +23,7 @@ export default class ChannelSetupMessage extends Component {
             content = (
                 <div>
                     {channels.map(c =>
-                        <Link to={"/admin/settings/"+c.toLowerCase()} key={c.toLowerCase()} className="Button Button--primary mr1" target={window.OSX ? null : "_blank"}>Configure {c}</Link>
+                        <Link to={"/admin/settings/"+c.toLowerCase()} key={c.toLowerCase()} className="Button Button--primary mr1" target={window.OSX ? null : "_blank"}>{t`Configure`} {c}</Link>
                     )}
                 </div>
             );
@@ -31,7 +32,7 @@ export default class ChannelSetupMessage extends Component {
             let adminEmail = Settings.get("admin_email");
             content = (
                 <div className="mb1">
-                    <h4 className="text-grey-4">Your admin's email address:</h4>
+                    <h4 className="text-grey-4">{t`Your admin's email address`}:</h4>
                     <a className="h2 link no-decoration" href={"mailto:"+adminEmail}>{adminEmail}</a>
                 </div>
             );
diff --git a/frontend/src/metabase/components/DownloadButton.jsx b/frontend/src/metabase/components/DownloadButton.jsx
index e6d45c3871ef037dde4163b0de33369eb3dca6f5..49641cf6e42e4c46bc1422e4e31a4ca59b57d8d2 100644
--- a/frontend/src/metabase/components/DownloadButton.jsx
+++ b/frontend/src/metabase/components/DownloadButton.jsx
@@ -5,7 +5,7 @@ import Button from "metabase/components/Button.jsx";
 
 const DownloadButton = ({ className, style, children, method, url, params, extensions, ...props }) =>
     <form className={className} style={style} method={method} action={url}>
-        { Object.entries(params).map(([name, value]) =>
+        { params && Object.entries(params).map(([name, value]) =>
             <input key={name} type="hidden" name={name} value={value} />
         )}
         <Button
diff --git a/frontend/src/metabase/home/components/Activity.jsx b/frontend/src/metabase/home/components/Activity.jsx
index 4e385298dda6d0128f1e341a97dae15efbf75d59..7f0b16aa68c950eaf531fb1143767b461302c294 100644
--- a/frontend/src/metabase/home/components/Activity.jsx
+++ b/frontend/src/metabase/home/components/Activity.jsx
@@ -86,6 +86,44 @@ export default class Activity extends Component {
         };
 
         switch (item.topic) {
+            case "alert-create":
+                if(item.model_exists) {
+                    description.summary = (
+                        <span>
+                            {t`created an alert about - `}
+                            <Link to={Urls.modelToUrl(item.model, item.model_id)}
+                                  data-metabase-event={"Activity Feed;Header Clicked;Database -> " + item.topic}
+                                  className="link text-dark"
+                            >
+                                  {item.details.name}
+                            </Link>
+                        </span>
+                    );
+                } else {
+                    description.summary = (
+                        <span>{t`created an alert about - `}<span className="text-dark">{item.details.name}</span></span>
+                    );
+                }
+                break;
+            case "alert-delete":
+                if(item.model_exists) {
+                    description.summary = (
+                        <span>
+                            {t`deleted an alert about - `}
+                            <Link to={Urls.modelToUrl(item.model, item.model_id)}
+                                  data-metabase-event={"Activity Feed;Header Clicked;Database -> " + item.topic}
+                                  className="link text-dark"
+                            >
+                                  {item.details.name}
+                            </Link>
+                        </span>
+                    );
+                } else {
+                    description.summary = (
+                        <span>{t`deleted an alert about- `}<span className="text-dark">{item.details.name}</span></span>
+                    );
+                }
+                break;
             case "card-create":
             case "card-update":
                 if(item.table) {
diff --git a/frontend/src/metabase/lib/utils.js b/frontend/src/metabase/lib/utils.js
index db040ab0709a5adaf25a3dcab1aac7eebb2537e4..64ada075d02b4100276d2c3c8386f3180146236e 100644
--- a/frontend/src/metabase/lib/utils.js
+++ b/frontend/src/metabase/lib/utils.js
@@ -37,6 +37,7 @@ var MetabaseUtils = {
     },
 
     isEmpty: function(str) {
+        if (str != null) str = String(str); // make sure 'str' is actually a string
         return (str == null || 0 === str.length || str.match(/^\s+$/) != null);
     },
 
@@ -64,7 +65,7 @@ var MetabaseUtils = {
     },
 
     isJWT(string) {
-        return typeof string === "string" && /^[A-Za-z0-9]+\.[A-Za-z0-9]+\.[A-Za-z0-9_-]+$/.test(string);
+        return typeof string === "string" && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(string);
     },
 
     validEmail: function(email) {
diff --git a/frontend/src/metabase/pulse/components/CardPicker.jsx b/frontend/src/metabase/pulse/components/CardPicker.jsx
index ae4aadbe879448e064dfea9e2140ed4d8b693e08..035aa08c6900a614b8897b4871697b27a8f5ef88 100644
--- a/frontend/src/metabase/pulse/components/CardPicker.jsx
+++ b/frontend/src/metabase/pulse/components/CardPicker.jsx
@@ -2,6 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
+import { t } from 'c-3po';
 
 import Icon from "metabase/components/Icon.jsx";
 import Popover from "metabase/components/Popover.jsx";
@@ -58,11 +59,11 @@ export default class CardPicker extends Component {
         let error;
         try {
             if (Query.isBareRows(card.dataset_query.query)) {
-                error = "Raw data cannot be included in pulses";
+                error = t`Raw data cannot be included in pulses`;
             }
         } catch (e) {}
         if (card.display === "pin_map" || card.display === "state" || card.display === "country") {
-            error = "Maps cannot be included in pulses";
+            error = t`Maps cannot be included in pulses`;
         }
 
         if (error) {
@@ -104,7 +105,7 @@ export default class CardPicker extends Component {
             .sortBy("name")
             // add "Everything else" as the last option for cards without a
             // collection
-            .concat([{ id: null, name: "Everything else"}])
+            .concat([{ id: null, name: t`Everything else`}])
             .value();
 
         let visibleCardList;
@@ -128,7 +129,7 @@ export default class CardPicker extends Component {
                 <input
                     ref="input"
                     className="input no-focus full text-bold"
-                    placeholder="Type a question name to filter"
+                    placeholder={t`Type a question name to filter`}
                     value={this.inputValue}
                     onFocus={this.onInputFocus}
                     onBlur={this.onInputBlur}
diff --git a/frontend/src/metabase/pulse/components/PulseEdit.jsx b/frontend/src/metabase/pulse/components/PulseEdit.jsx
index 1fd2baa6afe6876c45ee179f182de2a376181dc9..0ab0acfd2d862bbd793ad7f287953d1fe444eff6 100644
--- a/frontend/src/metabase/pulse/components/PulseEdit.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEdit.jsx
@@ -2,6 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
+import { t, jt } from 'c-3po';
 
 import PulseEditName from "./PulseEditName.jsx";
 import PulseEditCards from "./PulseEditCards.jsx";
@@ -82,11 +83,11 @@ export default class PulseEdit extends Component {
     getConfirmItems() {
         return this.props.pulse.channels.map(c =>
             c.channel_type === "email" ?
-                <span>This pulse will no longer be emailed to <strong>{c.recipients.length} {inflect("address", c.recipients.length)}</strong> <strong>{c.schedule_type}</strong>.</span>
+                <span>{jt`This pulse will no longer be emailed to ${<strong>{c.recipients.length} {inflect("address", c.recipients.length)}</strong>} ${<strong>{c.schedule_type}</strong>}`}.</span>
             : c.channel_type === "slack" ?
-                <span>Slack channel <strong>{c.details && c.details.channel}</strong> will no longer get this pulse <strong>{c.schedule_type}</strong>.</span>
+                <span>{jt`Slack channel ${<strong>{c.details && c.details.channel}</strong>} will no longer get this pulse ${<strong>{c.schedule_type}</strong>}`}.</span>
             :
-                <span>Channel <strong>{c.channel_type}</strong> will no longer receive this pulse <strong>{c.schedule_type}</strong>.</span>
+                <span>{jt`Channel ${<strong>{c.channel_type}</strong>} will no longer receive this pulse ${<strong>{c.schedule_type}</strong>}`}.</span>
         );
     }
 
@@ -96,11 +97,11 @@ export default class PulseEdit extends Component {
         return (
             <div className="PulseEdit">
                 <div className="PulseEdit-header flex align-center border-bottom py3">
-                    <h1>{pulse && pulse.id != null ? "Edit" : "New"} pulse</h1>
+                    <h1>{pulse && pulse.id != null ? t`Edit pulse` : t`New pulse`}</h1>
                     <ModalWithTrigger
                         ref="pulseInfo"
                         className="Modal WhatsAPulseModal"
-                        triggerElement="What's a Pulse?"
+                        triggerElement={t`What's a Pulse?`}
                         triggerClasses="text-brand text-bold flex-align-right"
                     >
                         <ModalContent
@@ -108,7 +109,7 @@ export default class PulseEdit extends Component {
                         >
                             <div className="mx4 mb4">
                                 <WhatsAPulse
-                                    button={<button className="Button Button--primary" onClick={() => this.refs.pulseInfo.close()}>Got it</button>}
+                                    button={<button className="Button Button--primary" onClick={() => this.refs.pulseInfo.close()}>{t`Got it`}</button>}
                                 />
                             </div>
                         </ModalContent>
@@ -124,15 +125,15 @@ export default class PulseEdit extends Component {
                     <PulseEditSkip {...this.props} setPulse={this.setPulse} />
                     { pulse && pulse.id != null &&
                         <div className="DangerZone mb2 p3 rounded bordered relative">
-                            <h3 className="text-error absolute top bg-white px1" style={{ marginTop: "-12px" }}>Danger Zone</h3>
+                            <h3 className="text-error absolute top bg-white px1" style={{ marginTop: "-12px" }}>{t`Danger Zone`}</h3>
                             <div className="ml1">
-                                <h4 className="text-bold mb1">Delete this pulse</h4>
+                                <h4 className="text-bold mb1">{t`Delete this pulse`}</h4>
                                 <div className="flex">
-                                    <p className="h4 pr2">Stop delivery and delete this pulse. There's no undo, so be careful.</p>
+                                    <p className="h4 pr2">{t`Stop delivery and delete this pulse. There's no undo, so be careful.`}</p>
                                     <ModalWithTrigger
                                         ref={"deleteModal"+pulse.id}
                                         triggerClasses="Button Button--danger flex-align-right flex-no-shrink"
-                                        triggerElement="Delete this Pulse"
+                                        triggerElement={t`Delete this Pulse`}
                                     >
                                         <DeleteModalWithConfirm
                                             objectType="pulse"
@@ -151,12 +152,12 @@ export default class PulseEdit extends Component {
                     <ActionButton
                         actionFn={this.save}
                         className={cx("Button Button--primary", { "disabled": !isValid })}
-                        normalText={pulse.id != null ? "Save changes" : "Create pulse"}
-                        activeText="Saving…"
-                        failedText="Save failed"
-                        successText="Saved"
+                        normalText={pulse.id != null ? t`Save changes` : t`Create pulse`}
+                        activeText={t`Saving…`}
+                        failedText={t`Save failed`}
+                        successText={t`Saved`}
                     />
-                  <Link to="/pulse" className="Button ml2">Cancel</Link>
+                  <Link to="/pulse" className="Button ml2">{t`Cancel`}</Link>
                 </div>
             </div>
         );
diff --git a/frontend/src/metabase/pulse/components/PulseEditCards.jsx b/frontend/src/metabase/pulse/components/PulseEditCards.jsx
index 251650c16e5091cee436774decd5f0ccc27b9c6d..521bd45c37347b1492d51f88c93ba1b91889e974 100644
--- a/frontend/src/metabase/pulse/components/PulseEditCards.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditCards.jsx
@@ -1,6 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
+import { t } from 'c-3po';
 
 import CardPicker from "./CardPicker.jsx";
 import PulseCardPreview from "./PulseCardPreview.jsx";
@@ -52,21 +53,21 @@ export default class PulseEditCards extends Component {
         if (cardPreview) {
             if (cardPreview.pulse_card_type === "bar" && cardPreview.row_count > 10) {
                 warnings.push({
-                    head: "Heads up",
-                    body: "This is a large table and we'll have to crop it to use it in a pulse. The max size that can be displayed is 2 columns and 10 rows."
+                    head: t`Heads up`,
+                    body: t`This is a large table and we'll have to crop it to use it in a pulse. The max size that can be displayed is 2 columns and 10 rows.`
                 });
             }
             if (cardPreview.pulse_card_type == null) {
                 warnings.push({
-                    head: "Heads up",
-                    body: "We are unable to display this card in a pulse"
+                    head: t`Heads up`,
+                    body: t`We are unable to display this card in a pulse`
                 });
             }
         }
         if (showSoftLimitWarning) {
             warnings.push({
-                head: "Looks like this pulse is getting big",
-                body: "We recommend keeping pulses small and focused to help keep them digestable and useful to the whole team."
+                head: t`Looks like this pulse is getting big`,
+                body: t`We recommend keeping pulses small and focused to help keep them digestable and useful to the whole team.`
             });
         }
         return warnings;
@@ -100,7 +101,7 @@ export default class PulseEditCards extends Component {
         return (
             <div className="py1">
                 <h2>Pick your data</h2>
-                <p className="mt1 h4 text-bold text-grey-3">Choose questions you'd like to send in this pulse.</p>
+                <p className="mt1 h4 text-bold text-grey-3">{t`Choose questions you'd like to send in this pulse`}.</p>
                 <ol className="my3">
                     {cards && pulseCards.map((card, index) =>
                         <li key={index} className="my1">
diff --git a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
index 963eb4086266621de89c5168b6c810e0f719ec25..02628f181e134c4b8f6bf7fc60d71f88b4a8f852 100644
--- a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
@@ -3,6 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import _ from "underscore";
 import { assoc, assocIn } from "icepick";
+import { t } from 'c-3po';
 
 import RecipientPicker from "./RecipientPicker.jsx";
 
@@ -25,8 +26,8 @@ export const CHANNEL_ICONS = {
 };
 
 const CHANNEL_NOUN_PLURAL = {
-    "email": "Emails",
-    "slack": "Slack messages"
+    "email": t`Emails`,
+    "slack": t`Slack messages`
 };
 
 export default class PulseEditChannels extends Component {
@@ -197,8 +198,8 @@ export default class PulseEditChannels extends Component {
                     <SchedulePicker
                         schedule={_.pick(channel, "schedule_day", "schedule_frame", "schedule_hour", "schedule_type") }
                         scheduleOptions={channelSpec.schedules}
-                        textBeforeInterval="Sent"
-                        textBeforeSendTime={`${CHANNEL_NOUN_PLURAL[channelSpec && channelSpec.type] || "Messages"} will be sent at `}
+                        textBeforeInterval={t`Sent`}
+                        textBeforeSendTime={t`${CHANNEL_NOUN_PLURAL[channelSpec && channelSpec.type] || t`Messages`} will be sent at`}
                         onScheduleChange={this.onChannelScheduleChange.bind(this, index)}
                     />
                 }
@@ -208,11 +209,11 @@ export default class PulseEditChannels extends Component {
                             actionFn={this.onTestPulseChannel.bind(this, channel)}
                             className={cx("Button", { disabled: !isValid })}
                             normalText={channelSpec.type === "email" ?
-                                "Send email now" :
-                                "Send to  " + channelSpec.name + " now"}
-                            activeText="Sending…"
-                            failedText="Sending failed"
-                            successText={ this.willPulseSkip() ?  "Didn’t send because the pulse has no results." : "Pulse sent"}
+                                t`Send email now` :
+                                t`Send to ${channelSpec.name} now`}
+                            activeText={t`Sending…`}
+                            failedText={t`Sending failed`}
+                            successText={ this.willPulseSkip() ?  t`Didn’t send because the pulse has no results.` : t`Pulse sent`}
                             forceActiveStyle={ this.willPulseSkip() }
                         />
                     </div>
@@ -237,7 +238,7 @@ export default class PulseEditChannels extends Component {
                     <ul className="bg-grey-0 px3">{channels}</ul>
                 : channels.length > 0 && !channelSpec.configured ?
                     <div className="p4 text-centered">
-                        <h3 className="mb2">{channelSpec.name} needs to be set up by an administrator.</h3>
+                        <h3 className="mb2">{t`${channelSpec.name} needs to be set up by an administrator.`}</h3>
                         <ChannelSetupMessage user={user} channels={[channelSpec.name]} />
                     </div>
                 : null
@@ -250,8 +251,8 @@ export default class PulseEditChannels extends Component {
         let { formInput } = this.props;
         // Default to show the default channels until full formInput is loaded
         let channels = formInput.channels || {
-            email: { name: "Email", type: "email" },
-            slack: { name: "Slack", type: "slack" }
+            email: { name: t`Email`, type: "email" },
+            slack: { name: t`Slack`, type: "slack" }
         };
         return (
             <ul className="bordered rounded">
diff --git a/frontend/src/metabase/pulse/components/PulseEditName.jsx b/frontend/src/metabase/pulse/components/PulseEditName.jsx
index 15c06e88c6846db840cc2e13453eeffaed777c54..6a9e29b61e6af8f4d44fad56f2ec4984f87a35cc 100644
--- a/frontend/src/metabase/pulse/components/PulseEditName.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditName.jsx
@@ -1,5 +1,6 @@
 import React, { Component } from "react";
 import ReactDOM from "react-dom";
+import { t } from 'c-3po';
 
 import _ from "underscore";
 import cx from "classnames";
@@ -32,7 +33,7 @@ export default class PulseEditName extends Component {
         return (
             <div className="py1">
                 <h2>Name your pulse</h2>
-                <p className="mt1 h4 text-bold text-grey-3">Give your pulse a name to help others understand what it's about.</p>
+                <p className="mt1 h4 text-bold text-grey-3">{t`Give your pulse a name to help others understand what it's about`}.</p>
                 <div className="my3">
                     <input
                         ref="name"
@@ -41,7 +42,7 @@ export default class PulseEditName extends Component {
                         value={pulse.name || ""}
                         onChange={this.setName}
                         onBlur={this.refs.name && this.validate}
-                        placeholder="Important metrics"
+                        placeholder={t`Important metrics`}
                         autoFocus
                     />
                 </div>
diff --git a/frontend/src/metabase/pulse/components/PulseEditSkip.jsx b/frontend/src/metabase/pulse/components/PulseEditSkip.jsx
index 21b99a6cd4578a4486325be134be7991cd65d17d..a60386d0658c014b085a772b10d8d2011c2b7426 100644
--- a/frontend/src/metabase/pulse/components/PulseEditSkip.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditSkip.jsx
@@ -1,5 +1,6 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
+import { t } from 'c-3po';
 
 import Toggle from "metabase/components/Toggle.jsx";
 
@@ -18,8 +19,8 @@ export default class PulseEditSkip extends Component {
         const { pulse } = this.props;
         return (
             <div className="py1">
-                <h2>Skip if no results</h2>
-                <p className="mt1 h4 text-bold text-grey-3">Skip a scheduled Pulse if none of its questions have any results.</p>
+                <h2>{t`Skip if no results`}</h2>
+                <p className="mt1 h4 text-bold text-grey-3">{t`Skip a scheduled Pulse if none of its questions have any results`}.</p>
                 <div className="my3">
                     <Toggle value={pulse.skip_if_empty || false} onChange={this.toggle} />
                 </div>
diff --git a/frontend/src/metabase/pulse/components/PulseList.jsx b/frontend/src/metabase/pulse/components/PulseList.jsx
index 3dc1fc8c0aebbaf6055bcdf4e4884e554bfefc2e..41f9f0d2fa397c9f090b53ce8ad47d0f21f558b0 100644
--- a/frontend/src/metabase/pulse/components/PulseList.jsx
+++ b/frontend/src/metabase/pulse/components/PulseList.jsx
@@ -1,4 +1,5 @@
 import React, { Component } from "react";
+import { t } from 'c-3po';
 
 import PulseListItem from "./PulseListItem.jsx";
 import WhatsAPulse from "./WhatsAPulse.jsx";
@@ -42,8 +43,8 @@ export default class PulseList extends Component {
             <div className="PulseList pt3">
                 <div className="border-bottom mb2">
                     <div className="wrapper wrapper--trim flex align-center mb2">
-                        <h1>Pulses</h1>
-                        <a onClick={this.create} className="PulseButton Button flex-align-right">Create a pulse</a>
+                        <h1>{t`Pulses`}</h1>
+                        <a onClick={this.create} className="PulseButton Button flex-align-right">{t`Create a pulse`}</a>
                     </div>
                 </div>
                 <LoadingAndErrorWrapper loading={!pulses}>
@@ -64,7 +65,7 @@ export default class PulseList extends Component {
                 :
                     <div className="mt4 ml-auto mr-auto">
                         <WhatsAPulse
-                            button={<a onClick={this.create} className="Button Button--primary">Create a pulse</a>}
+                            button={<a onClick={this.create} className="Button Button--primary">{t`Create a pulse`}</a>}
                         />
                     </div>
                 }
diff --git a/frontend/src/metabase/pulse/components/PulseListChannel.jsx b/frontend/src/metabase/pulse/components/PulseListChannel.jsx
index 3a8e3841eecb712134a39a73e6a1afa35faf9c98..3bba5509bb2b06664ca61be0d01dc2890bf456fd 100644
--- a/frontend/src/metabase/pulse/components/PulseListChannel.jsx
+++ b/frontend/src/metabase/pulse/components/PulseListChannel.jsx
@@ -1,6 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
+import { t } from 'c-3po';
 
 import Icon from "metabase/components/Icon.jsx";
 
@@ -52,12 +53,12 @@ export default class PulseListChannel extends Component {
 
         if (channel.channel_type === "email") {
             channelIcon = "mail";
-            channelVerb = "Emailed";
+            channelVerb = t`Emailed`;
         } else if (channel.channel_type === "slack") {
             channelIcon = "slack";
-            channelVerb = "Slack'd";
+            channelVerb = t`Slack'd`;
             // Address #5799 where `details` object is missing for some reason
-            channelTarget = channel.details ? channel.details.channel : "No channel";
+            channelTarget = channel.details ? channel.details.channel : t`No channel`;
         }
 
         return (
@@ -66,7 +67,7 @@ export default class PulseListChannel extends Component {
                 <span>
                     {channelVerb + " "}
                     <strong>{channelSchedule}</strong>
-                    {channelTarget && <span>{" to "}<strong>{channelTarget}</strong></span>}
+                    {channelTarget && <span>{" " + t`to` + " "}<strong>{channelTarget}</strong></span>}
                 </span>
             </div>
         );
@@ -80,7 +81,6 @@ export default class PulseListChannel extends Component {
         if (subscribable) {
             subscribed = _.any(channel.recipients, r => r.id === user.id);
         }
-
         return (
             <div className="py2 flex align-center">
                 { this.renderChannelSchedule() }
@@ -88,13 +88,13 @@ export default class PulseListChannel extends Component {
                     <div className="flex-align-right">
                         { subscribed ?
                             <div className="flex align-center rounded bg-green text-white text-bold">
-                                <div className="pl2">You get this {channel.channel_type}</div>
+                                <div className="pl2">{t`You get this ${channel.channel_type}`}</div>
                                 <Icon className="p2 text-grey-1 text-white-hover cursor-pointer" name="close" size={12} onClick={this.unsubscribe}/>
                             </div>
                         : !pulse.read_only ?
                             <div className="flex align-center rounded bordered bg-white text-default text-bold cursor-pointer" onClick={this.subscribe}>
                                 <Icon className="p2" name="add" size={12}/>
-                                <div className="pr2">Get this {channel.channel_type}</div>
+                                <div className="pr2">{t`Get this ${channel.channel_type}`}</div>
                             </div>
                         : null }
                     </div>
diff --git a/frontend/src/metabase/pulse/components/PulseListItem.jsx b/frontend/src/metabase/pulse/components/PulseListItem.jsx
index 116ab53f43942d34a8824f12755f3d95cd4d2ce7..a157c3b8642f96c51514b98bc66df0c018b8c095 100644
--- a/frontend/src/metabase/pulse/components/PulseListItem.jsx
+++ b/frontend/src/metabase/pulse/components/PulseListItem.jsx
@@ -3,6 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import { Link } from "react-router";
+import { t } from 'c-3po';
 
 import cx from "classnames";
 
@@ -37,7 +38,7 @@ export default class PulseListItem extends Component {
                     </div>
                     { !pulse.read_only &&
                         <div className="flex-align-right">
-                            <Link to={"/pulse/" + pulse.id} className="PulseEditButton PulseButton Button no-decoration text-bold">Edit</Link>
+                            <Link to={"/pulse/" + pulse.id} className="PulseEditButton PulseButton Button no-decoration text-bold">{t`Edit`}</Link>
                         </div>
                     }
                 </div>
diff --git a/frontend/src/metabase/pulse/components/RecipientPicker.jsx b/frontend/src/metabase/pulse/components/RecipientPicker.jsx
index df3c5cefb9291603c13c32d2bfbb6dc13cf286b2..bfd2acc0d4127dccb5ca320d96f94b7e1f5425b0 100644
--- a/frontend/src/metabase/pulse/components/RecipientPicker.jsx
+++ b/frontend/src/metabase/pulse/components/RecipientPicker.jsx
@@ -1,32 +1,42 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import ReactDOM from "react-dom";
+import { findDOMNode } from "react-dom";
+import _ from "underscore";
+import cx from "classnames";
+import { t } from "c-3po";
 
-import Icon from "metabase/components/Icon.jsx";
-import Popover from "metabase/components/Popover.jsx";
-import UserAvatar from "metabase/components/UserAvatar.jsx";
+import OnClickOutsideWrapper from 'metabase/components/OnClickOutsideWrapper';
+import Icon from "metabase/components/Icon";
+import Input from "metabase/components/Input";
+import Popover from "metabase/components/Popover";
+import UserAvatar from "metabase/components/UserAvatar";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
-import { KEYCODE_ESCAPE, KEYCODE_COMMA, KEYCODE_TAB, KEYCODE_UP, KEYCODE_DOWN, KEYCODE_BACKSPACE } from "metabase/lib/keyboard";
 
-import _ from "underscore";
-import cx from "classnames";
+import {
+    KEYCODE_ESCAPE,
+    KEYCODE_ENTER,
+    KEYCODE_COMMA,
+    KEYCODE_TAB,
+    KEYCODE_UP,
+    KEYCODE_DOWN,
+    KEYCODE_BACKSPACE
+} from "metabase/lib/keyboard";
+
 
 const VALID_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
 
 export default class RecipientPicker extends Component {
-    constructor(props, context) {
-        super(props, context);
+    constructor(props) {
+        super(props);
 
         this.state = {
             inputValue: "",
             filteredUsers: [],
-            selectedUser: null,
+            selectedUserID: null,
             focused: props.recipients.length === 0
         };
-
-        _.bindAll(this, "onMouseDownCapture", "onInputChange", "onInputKeyDown", "onInputFocus", "onInputBlur");
     }
 
     // TODO: use recipientTypes to limit the type of recipient that can be added
@@ -44,8 +54,12 @@ export default class RecipientPicker extends Component {
     };
 
     setInputValue(inputValue) {
-        let { users, recipients } = this.props;
-        let { selectedUser } = this.state;
+        const { users, recipients } = this.props;
+        const searchString = inputValue.toLowerCase()
+
+        let { selectedUserID } = this.state;
+        let filteredUsers = [];
+
 
         let recipientsById = {};
         for (let recipient of recipients) {
@@ -54,54 +68,72 @@ export default class RecipientPicker extends Component {
             }
         }
 
-        let filteredUsers = [];
+
         if (inputValue) {
             // case insensitive search of name or email
-            let inputValueLower = inputValue.toLowerCase()
             filteredUsers = users.filter(user =>
+                // filter out users who have already been selected
                 !(user.id in recipientsById) &&
-                (user.common_name.toLowerCase().indexOf(inputValueLower) >= 0 || user.email.toLowerCase().indexOf(inputValueLower) >= 0)
+                (
+                    user.common_name.toLowerCase().indexOf(searchString) >= 0 ||
+                    user.email.toLowerCase().indexOf(searchString) >= 0
+                )
             );
         }
 
-        if (selectedUser == null || !_.find(filteredUsers, (u) => u.id === selectedUser)) {
+
+        if (selectedUserID == null || !_.find(filteredUsers, (user) => user.id === selectedUserID)) {
+            // if there are results based on the user's typing...
             if (filteredUsers.length > 0) {
-                selectedUser = filteredUsers[0].id;
+                // select the first user in the list and set the ID to that
+                selectedUserID = filteredUsers[0].id;
             } else {
-                selectedUser = null;
+                selectedUserID = null;
             }
         }
 
-        this.setState({ inputValue, filteredUsers, selectedUser });
+        this.setState({
+            inputValue,
+            filteredUsers,
+            selectedUserID
+        });
     }
 
-    onInputChange(e) {
-        this.setInputValue(e.target.value);
+    onInputChange = ({ target }) => {
+        this.setInputValue(target.value);
     }
 
-    onInputKeyDown(e) {
+    // capture events on the input to allow for convenient keyboard shortcuts
+    onInputKeyDown = (event) => {
+        const keyCode = event.keyCode
+
+        const { filteredUsers, selectedUserID } = this.state
+
         // enter, tab, comma
-        if (e.keyCode === KEYCODE_ESCAPE || e.keyCode === KEYCODE_TAB || e.keyCode === KEYCODE_COMMA) {
+        if (keyCode === KEYCODE_ESCAPE || keyCode === KEYCODE_TAB || keyCode === KEYCODE_COMMA || keyCode === KEYCODE_ENTER) {
             this.addCurrentRecipient();
         }
+
         // up arrow
-        else if (e.keyCode === KEYCODE_UP) {
-            e.preventDefault();
-            let index = _.findIndex(this.state.filteredUsers, (u) => u.id === this.state.selectedUser);
+        else if (event.keyCode === KEYCODE_UP) {
+            event.preventDefault();
+            let index = _.findIndex(filteredUsers, (u) => u.id === selectedUserID);
             if (index > 0) {
-                this.setState({ selectedUser: this.state.filteredUsers[index - 1].id });
+                this.setState({ selectedUserID: filteredUsers[index - 1].id });
             }
         }
+
         // down arrow
-        else if (e.keyCode === KEYCODE_DOWN) {
-            e.preventDefault();
-            let index = _.findIndex(this.state.filteredUsers, (u) => u.id === this.state.selectedUser);
-            if (index >= 0 && index < this.state.filteredUsers.length - 1) {
-                this.setState({ selectedUser: this.state.filteredUsers[index + 1].id });
+        else if (keyCode === KEYCODE_DOWN) {
+            event.preventDefault();
+            let index = _.findIndex(filteredUsers, (u) => u.id === selectedUserID);
+            if (index >= 0 && index < filteredUsers.length - 1) {
+                this.setState({ selectedUserID: filteredUsers[index + 1].id });
             }
         }
+
         // backspace
-        else if (e.keyCode === KEYCODE_BACKSPACE) {
+        else if (keyCode === KEYCODE_BACKSPACE) {
             let { recipients } = this.props;
             if (!this.state.inputValue && recipients.length > 0) {
                 this.removeRecipient(recipients[recipients.length - 1])
@@ -109,17 +141,17 @@ export default class RecipientPicker extends Component {
         }
     }
 
-    onInputFocus(e) {
+    onInputFocus = () => {
         this.setState({ focused: true });
     }
 
-    onInputBlur(e) {
+    onInputBlur = () => {
         this.addCurrentRecipient();
         this.setState({ focused: false });
     }
 
-    onMouseDownCapture(e) {
-        let input = ReactDOM.findDOMNode(this.refs.input);
+    onMouseDownCapture = (e) => {
+        let input = findDOMNode(this.refs.input);
         input.focus();
         // prevents clicks from blurring input while still allowing text selection:
         if (input !== e.target) {
@@ -128,8 +160,8 @@ export default class RecipientPicker extends Component {
     }
 
     addCurrentRecipient() {
-        let input = ReactDOM.findDOMNode(this.refs.input);
-        let user = _.find(this.state.filteredUsers, (u) => u.id === this.state.selectedUser);
+        let input = findDOMNode(this.refs.input);
+        let user = _.find(this.state.filteredUsers, (u) => u.id === this.state.selectedUserID);
         if (user) {
             this.addRecipient(user);
         } else if (VALID_EMAIL_REGEX.test(input.value)) {
@@ -137,72 +169,101 @@ export default class RecipientPicker extends Component {
         }
     }
 
-    addRecipient(recipient) {
+    addRecipient = (recipient) => {
+        const { recipients } = this.props
+
         // recipient is a user object, or plain object containing "email" key
-        this.props.onRecipientsChange(this.props.recipients.concat(recipient));
+        this.props.onRecipientsChange(
+            // return the list of recipients with the new user added
+            recipients.concat(recipient)
+        );
+        // reset the input value
         this.setInputValue("");
 
-        MetabaseAnalytics.trackEvent((this.props.isNewPulse) ? "PulseCreate" : "PulseEdit", "AddRecipient", (recipient.id) ? "user" : "email");
+        MetabaseAnalytics.trackEvent(
+            (this.props.isNewPulse) ? "PulseCreate" : "PulseEdit",
+            "AddRecipient",
+            (recipient.id) ? "user" : "email"
+        );
     }
 
     removeRecipient(recipient) {
-        this.props.onRecipientsChange(this.props.recipients.filter(r => recipient.id != null ? recipient.id !== r.id : recipient.email !== r.email));
+        const { recipients, onRecipientsChange } = this.props
+        onRecipientsChange(
+            recipients.filter(r =>
+                recipient.id != null
+                    ? recipient.id !== r.id
+                    : recipient.email !== r.email
+            )
+        );
 
-        MetabaseAnalytics.trackEvent((this.props.isNewPulse) ? "PulseCreate" : "PulseEdit", "RemoveRecipient", (recipient.id) ? "user" : "email");
+        MetabaseAnalytics.trackEvent(
+            (this.props.isNewPulse) ? "PulseCreate" : "PulseEdit",
+            "RemoveRecipient",
+            (recipient.id) ? "user" : "email"
+        );
     }
 
     render() {
-        let { filteredUsers, selectedUser } = this.state;
-        let { recipients } = this.props;
+        const { filteredUsers, inputValue, focused, selectedUserID } = this.state;
+        const { recipients } = this.props;
 
         return (
-            <ul className={cx("px1 pb1 bordered rounded flex flex-wrap bg-white", { "input--focus": this.state.focused })} onMouseDownCapture={this.onMouseDownCapture}>
-                {recipients.map((recipient, index) =>
-                    <li key={index} className="mr1 py1 pl1 mt1 rounded bg-grey-1">
-                        <span className="h4 text-bold">{recipient.common_name || recipient.email}</span>
-                        <a className="text-grey-2 text-grey-4-hover px1" onClick={this.removeRecipient.bind(this, recipient)}>
-                            <Icon name="close" className="" size={12} />
-                        </a>
+            <OnClickOutsideWrapper handleDismissal={() => {
+                this.setState({ focused: false });
+            }}>
+                <ul className={cx("px1 pb1 bordered rounded flex flex-wrap bg-white", { "input--focus": this.state.focused })} onMouseDownCapture={this.onMouseDownCapture}>
+                    {recipients.map((recipient, index) =>
+                        <li key={recipient.id} className="mr1 py1 pl1 mt1 rounded bg-grey-1">
+                            <span className="h4 text-bold">{recipient.common_name || recipient.email}</span>
+                            <a
+                                className="text-grey-2 text-grey-4-hover px1"
+                                onClick={() => this.removeRecipient(recipient)}
+                            >
+                                <Icon name="close" className="" size={12} />
+                            </a>
+                        </li>
+                    )}
+                    <li className="flex-full mr1 py1 pl1 mt1 bg-white" style={{ "minWidth": " 100px" }}>
+                        <Input
+                            ref="input"
+                            className="full h4 text-bold text-default no-focus borderless"
+                            placeholder={recipients.length === 0 ? t`Enter email addresses you'd like this data to go to` : null}
+                            value={inputValue}
+                            autoFocus={focused}
+                            onKeyDown={this.onInputKeyDown}
+                            onChange={this.onInputChange}
+                            onFocus={this.onInputFocus}
+                            onBlurChange={this.onInputBlur}
+                        />
+                        <Popover
+                            isOpen={filteredUsers.length > 0}
+                            hasArrow={false}
+                            tetherOptions={{
+                                attachment: "top left",
+                                targetAttachment: "bottom left",
+                                targetOffset: "10 0"
+                            }}
+                        >
+                            <ul className="py1">
+                                {filteredUsers.map(user =>
+                                    <li
+                                        key={user.id}
+                                        className={cx(
+                                            "py1 px2 flex align-center text-bold bg-brand-hover text-white-hover", {
+                                            "bg-grey-1": user.id === selectedUserID
+                                        })}
+                                        onClick={() => this.addRecipient(user)}
+                                    >
+                                        <span className="text-white"><UserAvatar user={user} /></span>
+                                        <span className="ml1 h4">{user.common_name}</span>
+                                    </li>
+                                )}
+                            </ul>
+                        </Popover>
                     </li>
-                )}
-                <li className="flex-full mr1 py1 pl1 mt1 bg-white" style={{ "minWidth": " 100px" }}>
-                    <input
-                        ref="input"
-                        type="text"
-                        className="full h4 text-bold text-default no-focus borderless"
-                        placeholder={recipients.length === 0 ? "Enter email addresses you'd like this data to go to" : null}
-                        value={this.state.inputValue}
-                        autoFocus={this.state.focused}
-                        onKeyDown={this.onInputKeyDown}
-                        onChange={this.onInputChange}
-                        onFocus={this.onInputFocus}
-                        onBlur={this.onInputBlur}
-                    />
-                    <Popover
-                        isOpen={filteredUsers.length > 0}
-                        hasArrow={false}
-                        tetherOptions={{
-                            attachment: "top left",
-                            targetAttachment: "bottom left",
-                            targetOffset: "10 0"
-                        }}
-                    >
-                        <ul className="py1">
-                            {filteredUsers.map(user =>
-                                <li
-                                    className={cx("py1 px2 flex align-center text-bold bg-brand-hover text-white-hover", {
-                                        "bg-grey-1": user.id === selectedUser
-                                    })}
-                                    onClick={this.addRecipient.bind(this, user)}
-                                >
-                                    <span className="text-white"><UserAvatar user={user} /></span>
-                                    <span className="ml1 h4">{user.common_name}</span>
-                                </li>
-                            )}
-                        </ul>
-                    </Popover>
-                </li>
-            </ul>
+                </ul>
+            </OnClickOutsideWrapper>
         );
     }
 }
diff --git a/frontend/src/metabase/pulse/components/WhatsAPulse.jsx b/frontend/src/metabase/pulse/components/WhatsAPulse.jsx
index 85ec9f8cef1816e9d139f6badfba3db75d329b4a..d0f934b92624b4022c15432069a242cb23677b50 100644
--- a/frontend/src/metabase/pulse/components/WhatsAPulse.jsx
+++ b/frontend/src/metabase/pulse/components/WhatsAPulse.jsx
@@ -1,6 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
+import { t } from 'c-3po';
 
 import RetinaImage from "react-retina-image";
 
@@ -12,7 +13,7 @@ export default class WhatsAPulse extends Component {
         return (
             <div className="flex flex-column align-center px4">
                 <h2 className="my4 text-brand">
-                    Help everyone on your team stay in sync with your data.
+                    {t`Help everyone on your team stay in sync with your data.`}
                 </h2>
                 <div className="mx4">
                     <RetinaImage
@@ -22,7 +23,7 @@ export default class WhatsAPulse extends Component {
                     />
                 </div>
                 <div className="h3 my3 text-centered text-grey-2 text-bold" style={{maxWidth: "500px"}}>
-                    Pulses let you send data from Metabase to email or Slack on the schedule of your choice.
+                    {t`Pulses let you send data from Metabase to email or Slack on the schedule of your choice.`}
                 </div>
                 {this.props.button}
             </div>
diff --git a/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx b/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx
index 9eee01e0ffad70d72af93922a58fec286df78558..7af40eee6edc6a720149187a58e9667478d8fc74 100644
--- a/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx
+++ b/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx
@@ -1,7 +1,7 @@
 /* @flow weak */
 
 import React, { Component } from "react";
-
+import { t } from "c-3po";
 import DatePicker
     from "metabase/query_builder/components/filters/pickers/DatePicker";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
@@ -103,16 +103,16 @@ export default class TimeseriesFilterWidget extends Component {
                 currentFilter
             ).join(" - ");
             if (currentFilter[0] === ">") {
-                currentDescription = "After " + currentDescription;
+                currentDescription = t`After ${currentDescription}`;
             } else if (currentFilter[0] === "<") {
-                currentDescription = "Before " + currentDescription;
+                currentDescription = t`Before ${currentDescription}`;
             } else if (currentFilter[0] === "IS_NULL") {
-                currentDescription = "Is Empty";
+                currentDescription = t`Is Empty`;
             } else if (currentFilter[0] === "NOT_NULL") {
-                currentDescription = "Not Empty";
+                currentDescription = t`Not Empty`;
             }
         } else {
-            currentDescription = "All Time";
+            currentDescription = t`All Time`;
         }
 
         return (
diff --git a/frontend/src/metabase/qb/components/actions/CommonMetricsAction.jsx b/frontend/src/metabase/qb/components/actions/CommonMetricsAction.jsx
index 2f31a967b20b6022bead33e9d73a43e17ee1448c..ff70068d6ef28cda14488291b414f26a3961a40b 100644
--- a/frontend/src/metabase/qb/components/actions/CommonMetricsAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/CommonMetricsAction.jsx
@@ -1,7 +1,7 @@
 /* @flow */
 
 import React from "react";
-
+import { jt } from "c-3po";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 
 import type {
@@ -18,7 +18,7 @@ export default ({ question }: ClickActionProps): ClickAction[] => {
     const activeMetrics = query.table().metrics.filter(m => m.isActive());
     return activeMetrics.slice(0, 5).map(metric => ({
         name: "common-metric",
-        title: <span>View <strong>{metric.name}</strong></span>,
+        title: <span>{jt`View ${<strong>{metric.name}</strong>}`}</span>,
         question: () => question.summarize(["METRIC", metric.id])
     }));
 };
diff --git a/frontend/src/metabase/qb/components/actions/CompoundQueryAction.jsx b/frontend/src/metabase/qb/components/actions/CompoundQueryAction.jsx
index cd5d4ae3c4bed780c56b5e2c092d5b6689ffdd61..4cbc98cc04f17b0513c012f8e261744e64b0bafe 100644
--- a/frontend/src/metabase/qb/components/actions/CompoundQueryAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/CompoundQueryAction.jsx
@@ -4,13 +4,14 @@ import type {
     ClickAction,
     ClickActionProps
 } from "metabase/meta/types/Visualization";
+import { t } from "c-3po";
 
 export default ({ question }: ClickActionProps): ClickAction[] => {
     if (question.id()) {
         return [
             {
                 name: "nest-query",
-                title: "Analyze the results of this Query",
+                title: t`Analyze the results of this Query`,
                 icon: "table",
                 question: () => question.composeThisQuery()
             }
diff --git a/frontend/src/metabase/qb/components/actions/CountByTimeAction.jsx b/frontend/src/metabase/qb/components/actions/CountByTimeAction.jsx
index bc43bc313fa562432a3a4d8461af85d98e3a47dd..61ef0b22fbb83b38122eb76486153b8e44a2b7b8 100644
--- a/frontend/src/metabase/qb/components/actions/CountByTimeAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/CountByTimeAction.jsx
@@ -1,7 +1,7 @@
 /* @flow */
 
 import React from "react";
-
+import { t } from "c-3po";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 
 import type {
@@ -26,7 +26,7 @@ export default ({ question }: ClickActionProps): ClickAction[] => {
         {
             name: "count-by-time",
             section: "sum",
-            title: <span>Count of rows by time</span>,
+            title: <span>{t`Count of rows by time`}</span>,
             icon: "line",
             question: () =>
                 question
diff --git a/frontend/src/metabase/qb/components/actions/PivotByAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByAction.jsx
index 20f3d8e7b2c14efba23b59a5a6a2a6861c4b0d49..2fad6e9a44f0e82be638b0382ae6064601e51214 100644
--- a/frontend/src/metabase/qb/components/actions/PivotByAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/PivotByAction.jsx
@@ -1,7 +1,7 @@
 /* @flow */
 
 import React from "react";
-
+import { jt } from "c-3po";
 import BreakoutPopover from "metabase/qb/components/gui/BreakoutPopover";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 
@@ -42,7 +42,6 @@ export default (name: string, icon: string, fieldFilter: FieldFilter) =>
         if (breakoutOptions.count === 0) {
             return [];
         }
-
         return [
             {
                 name: "pivot-by-" + name.toLowerCase(),
@@ -50,11 +49,11 @@ export default (name: string, icon: string, fieldFilter: FieldFilter) =>
                 title: clicked
                     ? name
                     : <span>
-                          Break out by
-                          {" "}
-                          <span className="text-dark">
-                              {name.toLowerCase()}
-                          </span>
+                          {
+                              jt`Break out by ${<span className="text-dark">
+                                      {name.toLowerCase()}
+                                  </span>}`
+                          }
                       </span>,
                 icon: icon,
                 // eslint-disable-next-line react/display-name
diff --git a/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx
index a7b811801fd68a06dab6d27736107fb536417d21..f2ba6b0a071f91c7b766a070a85c77f04324b5db 100644
--- a/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx
@@ -3,9 +3,10 @@
 import { isCategory, isAddress } from "metabase/lib/schema_metadata";
 
 import PivotByAction from "./PivotByAction";
+import { t } from "c-3po";
 
 export default PivotByAction(
-    "Category",
+    t`Category`,
     "label",
     field => isCategory(field) && !isAddress(field)
 );
diff --git a/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx
index 0b4f241ba1da0478052bbd065c13e90f81d93751..db26fa1cc774bcc755294d0eefe5f4b898875f54 100644
--- a/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx
@@ -3,5 +3,7 @@
 import { isAddress } from "metabase/lib/schema_metadata";
 
 import PivotByAction from "./PivotByAction";
+import { t } from "c-3po";
 
-export default PivotByAction("Location", "location", field => isAddress(field));
+export default PivotByAction(t`Location`, "location", field =>
+    isAddress(field));
diff --git a/frontend/src/metabase/qb/components/actions/PivotByTimeAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByTimeAction.jsx
index c70764e3f0468469ee2420b014d57842076c2941..ef36a663de1ae50ac77a30abd77562d44e777b56 100644
--- a/frontend/src/metabase/qb/components/actions/PivotByTimeAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/PivotByTimeAction.jsx
@@ -3,5 +3,6 @@
 import { isDate } from "metabase/lib/schema_metadata";
 
 import PivotByAction from "./PivotByAction";
+import { t } from "c-3po";
 
-export default PivotByAction("Time", "clock", field => isDate(field));
+export default PivotByAction(t`Time`, "clock", field => isDate(field));
diff --git a/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx b/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx
index d94f101415c3fcb061cce4845d64e6270a1dc0dc..7c4389008172ba1dfe3a3a5cd4b95a097a6e20d2 100644
--- a/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx
@@ -1,7 +1,7 @@
 /* @flow */
 
 import React from "react";
-
+import { t } from "c-3po";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 import AggregationPopover from "metabase/qb/components/gui/AggregationPopover";
 
@@ -33,7 +33,7 @@ export default ({ question }: ClickActionProps): ClickAction[] => {
     return [
         {
             name: "summarize",
-            title: "Summarize this segment",
+            title: t`Summarize this segment`,
             icon: "sum",
             // eslint-disable-next-line react/display-name
             popover: (
diff --git a/frontend/src/metabase/qb/components/actions/UnderlyingDataAction.jsx b/frontend/src/metabase/qb/components/actions/UnderlyingDataAction.jsx
index dd99f8a87994c07e7857af088d8696cbbf92702f..cbe11b750b0ca361e15f1be9d2768ea49166d2a1 100644
--- a/frontend/src/metabase/qb/components/actions/UnderlyingDataAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/UnderlyingDataAction.jsx
@@ -4,13 +4,14 @@ import type {
     ClickAction,
     ClickActionProps
 } from "metabase/meta/types/Visualization";
+import { t } from "c-3po";
 
 export default ({ question }: ClickActionProps): ClickAction[] => {
     if (question.display() !== "table" && question.display() !== "scalar") {
         return [
             {
                 name: "underlying-data",
-                title: "View this as a table",
+                title: t`View this as a table`,
                 icon: "table",
                 question: () => question.toUnderlyingData()
             }
diff --git a/frontend/src/metabase/qb/components/actions/UnderlyingRecordsAction.jsx b/frontend/src/metabase/qb/components/actions/UnderlyingRecordsAction.jsx
index bf99842c1971cfbc97093e2a822d374bc427b326..65d005d9da95932d1f13172db0217dee3abe5b79 100644
--- a/frontend/src/metabase/qb/components/actions/UnderlyingRecordsAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/UnderlyingRecordsAction.jsx
@@ -3,7 +3,7 @@
 import React from "react";
 
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
-
+import { jt } from "c-3po";
 import type {
     ClickAction,
     ClickActionProps
@@ -14,19 +14,16 @@ export default ({ question }: ClickActionProps): ClickAction[] => {
     if (!(query instanceof StructuredQuery) || query.isBareRows()) {
         return [];
     }
-
     return [
         {
             name: "underlying-records",
             title: (
                 <span>
-                    View the underlying
-                    {" "}
-                    <span className="text-dark">
-                        {query.table().display_name}
-                    </span>
-                    {" "}
-                    records
+                    {
+                        jt`View the underlying ${<span className="text-dark">
+                                {query.table().display_name}
+                            </span>} records`
+                    }
                 </span>
             ),
             icon: "table2",
diff --git a/frontend/src/metabase/qb/components/actions/XRayCard.jsx b/frontend/src/metabase/qb/components/actions/XRayCard.jsx
index 36cd5da501a839c48a677f0ecdd9ab4c19dd3675..7876b8e000eab6c23041b55d6882653c30486e8d 100644
--- a/frontend/src/metabase/qb/components/actions/XRayCard.jsx
+++ b/frontend/src/metabase/qb/components/actions/XRayCard.jsx
@@ -4,6 +4,7 @@ import type {
     ClickAction,
     ClickActionProps
 } from "metabase/meta/types/Visualization";
+import { t } from "c-3po";
 
 export default ({ question, settings }: ClickActionProps): ClickAction[] => {
     // currently time series xrays require the maximum fidelity
@@ -15,7 +16,7 @@ export default ({ question, settings }: ClickActionProps): ClickAction[] => {
         return [
             {
                 name: "xray-card",
-                title: "X-ray this question",
+                title: t`X-ray this question`,
                 icon: "beaker",
                 url: () => `/xray/card/${question.card().id}/extended`
             }
diff --git a/frontend/src/metabase/qb/components/actions/XRaySegment.jsx b/frontend/src/metabase/qb/components/actions/XRaySegment.jsx
index 39eec07735dd22596df0f89421d2de49ee724cc4..f5ee7270455159150831395c6b5e3aa641d624a6 100644
--- a/frontend/src/metabase/qb/components/actions/XRaySegment.jsx
+++ b/frontend/src/metabase/qb/components/actions/XRaySegment.jsx
@@ -6,6 +6,7 @@ import type {
 } from "metabase/meta/types/Visualization";
 
 import { isSegmentFilter } from "metabase/lib/query/filter";
+import { t } from "c-3po";
 
 export default ({ question, settings }: ClickActionProps): ClickAction[] => {
     if (question.card().id && settings["enable_xrays"]) {
@@ -16,9 +17,10 @@ export default ({ question, settings }: ClickActionProps): ClickAction[] => {
             .map(filter => {
                 const id = filter[1];
                 const segment = question.metadata().segments[id];
+                const xraysegmentname = segment && segment.name;
                 return {
                     name: "xray-segment",
-                    title: `X-ray ${segment && segment.name}`,
+                    title: t`X-ray ${xraysegmentname}`,
                     icon: "beaker",
                     url: () => `/xray/segment/${id}/approximate`
                 };
diff --git a/frontend/src/metabase/qb/components/drill/CountByColumnDrill.js b/frontend/src/metabase/qb/components/drill/CountByColumnDrill.js
index 1238a4f115bbe02fe1a2f7ff55dcb043f76777a7..e9c783c7cc3da8d3111d593b9f248209cc46c1c3 100644
--- a/frontend/src/metabase/qb/components/drill/CountByColumnDrill.js
+++ b/frontend/src/metabase/qb/components/drill/CountByColumnDrill.js
@@ -1,7 +1,7 @@
 /* @flow */
 
 import React from "react";
-
+import { t } from "c-3po";
 import { getFieldRefFromColumn } from "metabase/qb/lib/actions";
 import { isCategory } from "metabase/lib/schema_metadata";
 
@@ -26,7 +26,7 @@ export default ({ question, clicked }: ClickActionProps): ClickAction[] => {
         {
             name: "count-by-column",
             section: "distribution",
-            title: <span>Distribution</span>,
+            title: <span>{t`Distribution`}</span>,
             question: () =>
                 question
                     .summarize(["count"])
diff --git a/frontend/src/metabase/qb/components/drill/ObjectDetailDrill.jsx b/frontend/src/metabase/qb/components/drill/ObjectDetailDrill.jsx
index 6b6496657cbe5eddba94c5fd82c77733ea6a4ce7..9b634982ed08a781337b44e332c5e884cb323f0d 100644
--- a/frontend/src/metabase/qb/components/drill/ObjectDetailDrill.jsx
+++ b/frontend/src/metabase/qb/components/drill/ObjectDetailDrill.jsx
@@ -1,7 +1,7 @@
 /* @flow */
 
 import { isFK, isPK } from "metabase/lib/schema_metadata";
-
+import { t } from "c-3po";
 import type {
     ClickAction,
     ClickActionProps
@@ -35,7 +35,7 @@ export default ({ question, clicked }: ClickActionProps): ClickAction[] => {
         {
             name: "object-detail",
             section: "details",
-            title: "View details",
+            title: t`View details`,
             default: true,
             question: () => question.drillPK(field, clicked && clicked.value)
         }
diff --git a/frontend/src/metabase/qb/components/drill/QuickFilterDrill.jsx b/frontend/src/metabase/qb/components/drill/QuickFilterDrill.jsx
index 86e374dc1fff803beaf5640ef64edd227aaf03c1..65d4ced0396c348aefd132890ff79c2ae3915cee 100644
--- a/frontend/src/metabase/qb/components/drill/QuickFilterDrill.jsx
+++ b/frontend/src/metabase/qb/components/drill/QuickFilterDrill.jsx
@@ -1,7 +1,7 @@
 /* @flow */
 
 import React from "react";
-
+import { jt } from "c-3po";
 import { TYPE, isa, isFK, isPK } from "metabase/lib/types";
 import { singularize, pluralize, stripId } from "metabase/lib/formatting";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
@@ -51,12 +51,9 @@ export default ({ question, clicked }: ClickActionProps): ClickAction[] => {
                 section: "filter",
                 title: (
                     <span>
-                        View this
-                        {" "}
-                        {singularize(stripId(column.display_name))}
-                        's
-                        {" "}
-                        {pluralize(query.table().display_name)}
+                        {
+                            jt`View this ${singularize(stripId(column.display_name))}'s ${pluralize(query.table().display_name)}`
+                        }
                     </span>
                 ),
                 question: () => question.filter("=", column, value)
diff --git a/frontend/src/metabase/qb/components/drill/SortAction.jsx b/frontend/src/metabase/qb/components/drill/SortAction.jsx
index 75a287d116e74b2345b6e41780b396c92fb233b1..17557cdc1d2943740094920dd49ab92a23973b38 100644
--- a/frontend/src/metabase/qb/components/drill/SortAction.jsx
+++ b/frontend/src/metabase/qb/components/drill/SortAction.jsx
@@ -2,7 +2,7 @@
 
 import Query from "metabase/lib/query";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
-
+import { t } from "c-3po";
 import type {
     ClickAction,
     ClickActionProps
@@ -42,7 +42,7 @@ export default ({ question, clicked }: ClickActionProps): ClickAction[] => {
         actions.push({
             name: "sort-ascending",
             section: "sort",
-            title: "Ascending",
+            title: t`Ascending`,
             question: () =>
                 query.replaceSort([fieldRef, "ascending"]).question()
         });
@@ -55,7 +55,7 @@ export default ({ question, clicked }: ClickActionProps): ClickAction[] => {
         actions.push({
             name: "sort-descending",
             section: "sort",
-            title: "Descending",
+            title: t`Descending`,
             question: () =>
                 query.replaceSort([fieldRef, "descending"]).question()
         });
diff --git a/frontend/src/metabase/qb/components/drill/SummarizeColumnByTimeDrill.js b/frontend/src/metabase/qb/components/drill/SummarizeColumnByTimeDrill.js
index ad39e438eb97309af30287a115b334dccf6389b1..d667a167c12c6e48507f65b6511a8edc2a53566d 100644
--- a/frontend/src/metabase/qb/components/drill/SummarizeColumnByTimeDrill.js
+++ b/frontend/src/metabase/qb/components/drill/SummarizeColumnByTimeDrill.js
@@ -1,7 +1,7 @@
 /* @flow */
 
 import React from "react";
-
+import { t } from "c-3po";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 import { getFieldRefFromColumn } from "metabase/qb/lib/actions";
 import {
@@ -37,7 +37,7 @@ export default ({ question, clicked }: ClickActionProps): ClickAction[] => {
         .map(aggregator => ({
             name: "summarize-by-time",
             section: "sum",
-            title: <span>{capitalize(aggregator.short)} by time</span>,
+            title: <span>{capitalize(aggregator.short)} {t`by time`}</span>,
             question: () =>
                 question
                     .summarize(
diff --git a/frontend/src/metabase/qb/components/drill/SummarizeColumnDrill.js b/frontend/src/metabase/qb/components/drill/SummarizeColumnDrill.js
index 921ee52ab544bb5d35476198d517865f16385e80..3a6d299f3681cbee637badbc5ffe9d62f182114d 100644
--- a/frontend/src/metabase/qb/components/drill/SummarizeColumnDrill.js
+++ b/frontend/src/metabase/qb/components/drill/SummarizeColumnDrill.js
@@ -5,7 +5,7 @@ import {
     getAggregator,
     isCompatibleAggregatorForField
 } from "metabase/lib/schema_metadata";
-
+import { t } from "c-3po";
 import type {
     ClickAction,
     ClickActionProps
@@ -14,23 +14,23 @@ import type {
 const AGGREGATIONS = {
     sum: {
         section: "sum",
-        title: "Sum"
+        title: t`Sum`
     },
     avg: {
         section: "averages",
-        title: "Avg"
+        title: t`Avg`
     },
     min: {
         section: "averages",
-        title: "Min"
+        title: t`Min`
     },
     max: {
         section: "averages",
-        title: "Max"
+        title: t`Max`
     },
     distinct: {
         section: "averages",
-        title: "Distincts"
+        title: t`Distincts`
     }
 };
 
diff --git a/frontend/src/metabase/qb/components/drill/UnderlyingRecordsDrill.jsx b/frontend/src/metabase/qb/components/drill/UnderlyingRecordsDrill.jsx
index fb773c0e4327f4eb9e58dfcbd05ccdcae5447704..d397757ad326f32f6a7f4b910e2e9022e9864c29 100644
--- a/frontend/src/metabase/qb/components/drill/UnderlyingRecordsDrill.jsx
+++ b/frontend/src/metabase/qb/components/drill/UnderlyingRecordsDrill.jsx
@@ -3,7 +3,7 @@
 import { inflect } from "metabase/lib/formatting";
 
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
-
+import { t } from "c-3po";
 import type {
     ClickAction,
     ClickActionProps
@@ -27,10 +27,7 @@ export default ({ question, clicked }: ClickActionProps): ClickAction[] => {
         {
             name: "underlying-records",
             section: "records",
-            title: "View " +
-                inflect("these", count, "this", "these") +
-                " " +
-                inflect(query.table().display_name, count),
+            title: t`View ${inflect(t`these`, count, t`this`, t`these`)} ${inflect(query.table().display_name, count)}`,
             question: () => question.drillUnderlyingRecords(dimensions)
         }
     ];
diff --git a/frontend/src/metabase/qb/components/drill/ZoomDrill.jsx b/frontend/src/metabase/qb/components/drill/ZoomDrill.jsx
index 7c6d5d2365ee1a173168ba5c7136fec330b230b4..086baf00b597a7fee0336a08c1f48bfcbd2820f2 100644
--- a/frontend/src/metabase/qb/components/drill/ZoomDrill.jsx
+++ b/frontend/src/metabase/qb/components/drill/ZoomDrill.jsx
@@ -6,6 +6,7 @@ import type {
     ClickAction,
     ClickActionProps
 } from "metabase/meta/types/Visualization";
+import { t } from "c-3po";
 
 export default (
     { question, clicked, settings }: ClickActionProps
@@ -20,7 +21,7 @@ export default (
         {
             name: "timeseries-zoom",
             section: "zoom",
-            title: "Zoom in",
+            title: t`Zoom in`,
             question: () => question.pivot(drilldown.breakouts, dimensions)
         }
     ];
diff --git a/frontend/src/metabase/query_builder/components/AggregationPopover.jsx b/frontend/src/metabase/query_builder/components/AggregationPopover.jsx
index ee35d9a3028eebefe70af4b84b1f237aed86d1fb..2033e463e24b4596e68925474ad0ccbd3815d8c9 100644
--- a/frontend/src/metabase/query_builder/components/AggregationPopover.jsx
+++ b/frontend/src/metabase/query_builder/components/AggregationPopover.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import ReactDOM from "react-dom";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import AccordianList from "metabase/components/AccordianList.jsx";
 import FieldList from './FieldList.jsx';
 import QueryDefinitionTooltip from "./QueryDefinitionTooltip.jsx";
@@ -16,8 +16,8 @@ import _ from "underscore";
 
 import ExpressionEditorTextfield from "./expressions/ExpressionEditorTextfield.jsx"
 
-const CUSTOM_SECTION_NAME = "Custom Expression";
-const METRICS_SECTION_NAME = "Common Metrics";
+const CUSTOM_SECTION_NAME = t`Custom Expression`;
+const METRICS_SECTION_NAME = t`Common Metrics`;
 
 export default class AggregationPopover extends Component {
     constructor(props, context) {
@@ -181,7 +181,7 @@ export default class AggregationPopover extends Component {
             if (tableMetadata.db.features.indexOf("expression-aggregations") >= 0) {
                 sections.push({
                     name: CUSTOM_SECTION_NAME,
-                    icon: "staroutline"
+                    icon: "sum"
                 });
             }
         }
@@ -229,10 +229,10 @@ export default class AggregationPopover extends Component {
                                     NamedClause.setName(aggregation, e.target.value) :
                                     aggregation
                             })}
-                            placeholder="Name (optional)"
+                            placeholder={t`Name (optional)`}
                         />
                         <Button className="full" primary disabled={this.state.error} onClick={() => this.commitAggregation(this.state.aggregation)}>
-                            Done
+                            {t`Done`}
                         </Button>
                     </div>
                 </div>
diff --git a/frontend/src/metabase/query_builder/components/AggregationWidget.jsx b/frontend/src/metabase/query_builder/components/AggregationWidget.jsx
index 22545608ef25c95fc4aa82ff043475ba2bc199f8..4d0b69a4bc24b856abdef9003a9f9d27058d351c 100644
--- a/frontend/src/metabase/query_builder/components/AggregationWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/AggregationWidget.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import AggregationPopover from "./AggregationPopover.jsx";
 import FieldName from './FieldName.jsx';
 import Clearable from './Clearable.jsx';
@@ -56,7 +56,7 @@ export default class AggregationWidget extends Component {
                 <span className="flex align-center">
                     { selectedAggregation.name.replace(" of ...", "") }
                     { fieldId &&
-                        <span style={{paddingRight: "4px", paddingLeft: "4px"}} className="text-bold">of</span>
+                        <span style={{paddingRight: "4px", paddingLeft: "4px"}} className="text-bold">{t`of`}</span>
                     }
                     { fieldId &&
                         <FieldName
@@ -133,7 +133,7 @@ export default class AggregationWidget extends Component {
                             <div id="Query-section-aggregation" onClick={this.open} className="Query-section Query-section-aggregation cursor-pointer">
                                 <span className="View-section-aggregation QueryOption py1 mx1">
                                     { aggregationName == null ?
-                                        "Choose an aggregation"
+                                        t`Choose an aggregation`
                                     : name ?
                                         name
                                     :
diff --git a/frontend/src/metabase/query_builder/components/DataSelector.jsx b/frontend/src/metabase/query_builder/components/DataSelector.jsx
index 5906937577fc8112c7ddd95be10c16895ef1d1d4..67e568ace0813818f734bddbfa382b2ee51fccac 100644
--- a/frontend/src/metabase/query_builder/components/DataSelector.jsx
+++ b/frontend/src/metabase/query_builder/components/DataSelector.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import Icon from "metabase/components/Icon.jsx";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
 import AccordianList from "metabase/components/AccordianList.jsx";
@@ -292,7 +292,7 @@ export default class DataSelector extends Component {
                             <h3 className="text-default">{header}</h3>
                         </div>
                     </div>
-                    <div className="p4 text-centered">No tables found in this database.</div>
+                    <div className="p4 text-centered">{t`No tables found in this database.`}</div>
                 </section>
             );
         } else {
@@ -322,8 +322,8 @@ export default class DataSelector extends Component {
                     />
                     { isSavedQuestionList && (
                         <div className="bg-slate-extra-light p2 text-centered border-top">
-                            Is a question missing?
-                            <a href="http://metabase.com/docs/latest/users-guide/04-asking-questions.html#source-data" className="block link">Learn more about nested queries</a>
+                            {t`Is a question missing?`}
+                            <a href="http://metabase.com/docs/latest/users-guide/04-asking-questions.html#source-data" className="block link">{t`Learn more about nested queries`}</a>
                         </div>
                     )}
                 </div>
@@ -338,7 +338,7 @@ export default class DataSelector extends Component {
             <span className="flex align-center">
                 <span className="flex align-center text-slate cursor-pointer" onClick={this.onBack}>
                     <Icon name="chevronleft" size={18} />
-                    <span className="ml1">Segments</span>
+                    <span className="ml1">{t`Segments`}</span>
                 </span>
             </span>
         );
@@ -351,7 +351,7 @@ export default class DataSelector extends Component {
                             <h3 className="text-default">{header}</h3>
                         </div>
                     </div>
-                    <div className="p4 text-centered">No segments were found.</div>
+                    <div className="p4 text-centered">{t`No segments were found.`}</div>
                 </section>
             );
         }
@@ -374,7 +374,7 @@ export default class DataSelector extends Component {
                 maxHeight={maxHeight}
                 sections={sections}
                 searchable
-                searchPlaceholder="Find a segment"
+                searchPlaceholder={t`Find a segment`}
                 onChange={this.onChangeSegment}
                 itemIsSelected={(item) => item.segment ? item.segment.id === this.getSegmentId() : false}
                 itemIsClickable={(item) => item.segment && !item.disabled}
@@ -401,19 +401,19 @@ export default class DataSelector extends Component {
             } else if (segment) {
                 content = <span className="text-grey no-decoration">{segment.name}</span>;
             } else {
-                content = <span className="text-grey-4 no-decoration">Pick a segment or table</span>;
+                content = <span className="text-grey-4 no-decoration">{t`Pick a segment or table`}</span>;
             }
         } else if (this.props.includeTables) {
             if (table) {
                 content = <span className="text-grey no-decoration">{table.display_name || table.name}</span>;
             } else {
-                content = <span className="text-grey-4 no-decoration">Select a table</span>;
+                content = <span className="text-grey-4 no-decoration">{t`Select a table`}</span>;
             }
         } else {
             if (database) {
                 content = <span className="text-grey no-decoration">{database.name}</span>;
             } else {
-                content = <span className="text-grey-4 no-decoration">Select a database</span>;
+                content = <span className="text-grey-4 no-decoration">t`Select a database`}</span>;
             }
         }
 
diff --git a/frontend/src/metabase/query_builder/components/ExpandableString.jsx b/frontend/src/metabase/query_builder/components/ExpandableString.jsx
index cc147b27489598dd8772871435402d53d4d521f1..6fcc7fc66bf2355b347364f67d166420e49f2bd9 100644
--- a/frontend/src/metabase/query_builder/components/ExpandableString.jsx
+++ b/frontend/src/metabase/query_builder/components/ExpandableString.jsx
@@ -1,5 +1,5 @@
 import React, { Component } from "react";
-
+import { t } from 'c-3po';
 import Humanize from "humanize-plus";
 
 export default class ExpandableString extends Component {
@@ -35,9 +35,9 @@ export default class ExpandableString extends Component {
         var truncated = Humanize.truncate(this.props.str || "", 140);
 
         if (this.state.expanded) {
-            return (<span>{this.props.str} <span className="block mt1 link" onClick={this.toggleExpansion}>View less</span></span>);
+            return (<span>{this.props.str} <span className="block mt1 link" onClick={this.toggleExpansion}>{t`View less`}</span></span>);
         } else if (truncated !== this.props.str) {
-            return (<span>{truncated} <span className="block mt1 link" onClick={this.toggleExpansion}>View more</span></span>);
+            return (<span>{truncated} <span className="block mt1 link" onClick={this.toggleExpansion}>{t`View more`}</span></span>);
         } else {
             return (<span>{this.props.str}</span>);
         }
diff --git a/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx b/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
index 540cb431d6708613ec0be4419a0d0ca4ea4724d7..d44cdd935392bdaab299226dc3ed911a86534651 100644
--- a/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
+++ b/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
@@ -4,7 +4,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import _ from "underscore";
 import cx from "classnames";
-
+import { t } from 'c-3po';
 import AddClauseButton from "./AddClauseButton.jsx";
 import Expressions from "./expressions/Expressions.jsx";
 import ExpressionWidget from './expressions/ExpressionWidget.jsx';
@@ -101,7 +101,7 @@ export default class ExtendedOptions extends Component {
 
             if (query.canAddSort()) {
                 addSortButton = (
-                    <AddClauseButton text="Pick a field to sort by" onClick={() => {
+                    <AddClauseButton text={t`Pick a field to sort by`} onClick={() => {
                         // $FlowFixMe: shouldn't be adding a sort with null field
                         query.addSort([null, "ascending"]).update(setDatasetQuery)
                     }} />
@@ -112,7 +112,7 @@ export default class ExtendedOptions extends Component {
         if ((sortList && sortList.length > 0) || addSortButton) {
             return (
                 <div className="pb3">
-                    <div className="pb1 h6 text-uppercase text-grey-3 text-bold">Sort</div>
+                    <div className="pb1 h6 text-uppercase text-grey-3 text-bold">{t`Sort`}</div>
                     {sortList}
                     {addSortButton}
                 </div>
@@ -168,7 +168,7 @@ export default class ExtendedOptions extends Component {
 
                     { features.limit &&
                         <div>
-                            <div className="mb1 h6 text-uppercase text-grey-3 text-bold">Row limit</div>
+                            <div className="mb1 h6 text-uppercase text-grey-3 text-bold">{t`Row limit`}</div>
                             <LimitWidget limit={query.limit()} onChange={this.setLimit} />
                         </div>
                     }
diff --git a/frontend/src/metabase/query_builder/components/FieldName.jsx b/frontend/src/metabase/query_builder/components/FieldName.jsx
index bd508a5974cca61a76842f627a220f3d3e2541d1..1e2684c2e1c1053566cd90768cee482046aed325 100644
--- a/frontend/src/metabase/query_builder/components/FieldName.jsx
+++ b/frontend/src/metabase/query_builder/components/FieldName.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import Clearable from "./Clearable.jsx";
 
 import Query from "metabase/lib/query";
@@ -56,10 +56,10 @@ export default class FieldName extends Component {
             else if (Query.isLocalField(field) && Query.isFieldLiteral(field[1])) {
                 parts.push(<span key="field">{this.displayNameForFieldLiteral(tableMetadata, field[1])}</span>);
             } else {
-                parts.push(<span key="field">Unknown Field</span>);
+                parts.push(<span key="field">{t`Unknown Field`}</span>);
             }
         } else {
-            parts.push(<span key="field" className={"text-grey-2"}>field</span>)
+            parts.push(<span key="field" className={"text-grey-2"}>{t`field`}</span>)
         }
 
         return (
diff --git a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
index a2aa04a59b7ad813ca5ca46cf30d9b67bc2faa91..1b8b134f0d8d9725cf5d2df8c4ee2ca8af37cd07 100644
--- a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
+++ b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
@@ -3,7 +3,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
-
+import { t } from 'c-3po';
 import AggregationWidget_LEGACY from './AggregationWidget.jsx';
 import BreakoutWidget_LEGACY from './BreakoutWidget.jsx';
 import DataSelector from './DataSelector.jsx';
@@ -138,11 +138,11 @@ export default class GuiQueryEditor extends Component {
             }
 
             if (query.canAddFilter()) {
-                addFilterButton = this.renderAdd((filterList ? null : "Add filters to narrow your answer"), null, "addFilterTarget");
+                addFilterButton = this.renderAdd((filterList ? null : t`Add filters to narrow your answer`), null, "addFilterTarget");
             }
         } else {
             enabled = false;
-            addFilterButton = this.renderAdd("Add filters to narrow your answer", null, "addFilterTarget");
+            addFilterButton = this.renderAdd(t`Add filters to narrow your answer`, null, "addFilterTarget");
         }
 
         return (
@@ -208,7 +208,7 @@ export default class GuiQueryEditor extends Component {
                 );
                 if (aggregations[index + 1] != null && aggregations[index + 1].length > 0) {
                     aggregationList.push(
-                        <span key={"and"+index} className="text-bold">and</span>
+                        <span key={"and"+index} className="text-bold">{t`and`}</span>
                     );
                 }
             }
@@ -217,7 +217,7 @@ export default class GuiQueryEditor extends Component {
             // TODO: move this into AggregationWidget?
             return (
                 <div className="Query-section Query-section-aggregation disabled">
-                    <a className="QueryOption p1 flex align-center">Raw data</a>
+                    <a className="QueryOption p1 flex align-center">{t`Raw data`}</a>
                 </div>
             );
         }
@@ -255,13 +255,13 @@ export default class GuiQueryEditor extends Component {
                     breakout={breakout}
                     query={query}
                     updateQuery={setDatasetQuery}
-                    addButton={this.renderAdd(i === 0 ? "Add a grouping" : null)}
+                    addButton={this.renderAdd(i === 0 ? t`Add a grouping` : null)}
                 />
             );
 
             if (breakouts[i + 1] != null) {
                 breakoutList.push(
-                    <span key={"and"+i} className="text-bold">and</span>
+                    <span key={"and"+i} className="text-bold">{t`and`}</span>
                 );
             }
         }
@@ -278,7 +278,7 @@ export default class GuiQueryEditor extends Component {
         const tableMetadata = query.tableMetadata();
         return (
             <div className={"GuiBuilder-section GuiBuilder-data flex align-center arrow-right"}>
-                <span className="GuiBuilder-section-label Query-label">Data</span>
+                <span className="GuiBuilder-section-label Query-label">{t`Data`}</span>
                 { this.props.features.data ?
                     <DataSelector
                         ref="dataSection"
@@ -306,7 +306,7 @@ export default class GuiQueryEditor extends Component {
 
         return (
             <div className="GuiBuilder-section GuiBuilder-filtered-by flex align-center" ref="filterSection">
-                <span className="GuiBuilder-section-label Query-label">Filtered by</span>
+                <span className="GuiBuilder-section-label Query-label">{t`Filtered by`}</span>
                 {this.renderFilters()}
             </div>
         );
@@ -320,7 +320,7 @@ export default class GuiQueryEditor extends Component {
 
         return (
             <div className="GuiBuilder-section GuiBuilder-view flex align-center px1 pr2" ref="viewSection">
-                <span className="GuiBuilder-section-label Query-label">View</span>
+                <span className="GuiBuilder-section-label Query-label">{t`View`}</span>
                 {this.renderAggregation()}
             </div>
         );
@@ -334,7 +334,7 @@ export default class GuiQueryEditor extends Component {
 
         return (
             <div className="GuiBuilder-section GuiBuilder-groupedBy flex align-center px1" ref="viewSection">
-                <span className="GuiBuilder-section-label Query-label">Grouped By</span>
+                <span className="GuiBuilder-section-label Query-label">{t`Grouped By`}</span>
                 {this.renderBreakouts()}
             </div>
         );
diff --git a/frontend/src/metabase/query_builder/components/LimitWidget.jsx b/frontend/src/metabase/query_builder/components/LimitWidget.jsx
index a85f0069c96e2ef60abe9556295a36dea70c8b21..f6f40be07cc7a99a26fa00a67d4312d16fa8c635 100644
--- a/frontend/src/metabase/query_builder/components/LimitWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/LimitWidget.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
-
+import { t } from 'c-3po';
 
 export default class LimitWidget extends Component {
 
@@ -19,7 +19,7 @@ export default class LimitWidget extends Component {
             <ul className="Button-group Button-group--blue">
                 {this.props.options.map(count =>
                     <li key={count || "None"} className={cx("Button", { "Button--active":  count === this.props.limit })} onClick={() => this.props.onChange(count)}>
-                        {count || "None"}
+                        {count || t`None`}
                     </li>
                 )}
             </ul>
diff --git a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
index 67a1fee77ab4a38988a06c1f8294c81af397f326..16a8e653f2887a6d45e05f75f62c2590c61c0caa 100644
--- a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
+++ b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
@@ -23,7 +23,7 @@ import 'ace/snippets/mysql';
 import 'ace/snippets/pgsql';
 import 'ace/snippets/sqlserver';
 import 'ace/snippets/json';
-
+import { t } from 'c-3po';
 
 import { SQLBehaviour } from "metabase/lib/ace/sql_behaviour";
 
@@ -267,7 +267,7 @@ export default class NativeQueryEditor extends Component {
             if (databases.length > 1 && (database == null || _.any(databases, (db) => db.id === database.id))) {
                 dataSelectors.push(
                     <div key="db_selector" className="GuiBuilder-section GuiBuilder-data flex align-center">
-                        <span className="GuiBuilder-section-label Query-label">Database</span>
+                        <span className="GuiBuilder-section-label Query-label">{t`Database`}</span>
                         <DataSelector
                             databases={databases}
                             datasetQuery={query.datasetQuery()}
@@ -287,7 +287,7 @@ export default class NativeQueryEditor extends Component {
 
                 dataSelectors.push(
                     <div key="table_selector" className="GuiBuilder-section GuiBuilder-data flex align-center">
-                        <span className="GuiBuilder-section-label Query-label">Table</span>
+                        <span className="GuiBuilder-section-label Query-label">{t`Table`}</span>
                         <DataSelector
                             ref="dataSection"
                             includeTables={true}
@@ -306,17 +306,17 @@ export default class NativeQueryEditor extends Component {
                 );
             }
         } else {
-            dataSelectors = <span className="p2 text-grey-4">{`This question is written in ${query.nativeQueryLanguage()}.`}</span>;
+            dataSelectors = <span className="p2 text-grey-4">{t`This question is written in ${query.nativeQueryLanguage()}.`}</span>;
         }
 
         let editorClasses, toggleEditorText, toggleEditorIcon;
         if (this.state.showEditor) {
             editorClasses = "";
-            toggleEditorText = query.hasWritePermission() ? "Hide Editor" : "Hide Query";
+            toggleEditorText = query.hasWritePermission() ? t`Hide Editor` : t`Hide Query`;
             toggleEditorIcon = "contract";
         } else {
             editorClasses = "hide";
-            toggleEditorText = query.hasWritePermission() ? "Open Editor" : "Show Query";
+            toggleEditorText = query.hasWritePermission() ? t`Open Editor` : t`Show Query`;
             toggleEditorIcon = "expand";
         }
 
diff --git a/frontend/src/metabase/query_builder/components/QueryDefinitionTooltip.jsx b/frontend/src/metabase/query_builder/components/QueryDefinitionTooltip.jsx
index d7fa933a7dfee7e152f3e9c82340cab0264f0329..6214ff691f9edfbb398ba934150fa65190206f7c 100644
--- a/frontend/src/metabase/query_builder/components/QueryDefinitionTooltip.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryDefinitionTooltip.jsx
@@ -6,7 +6,7 @@ import AggregationWidget from "./AggregationWidget.jsx";
 import FieldSet from "metabase/components/FieldSet.jsx";
 
 import Query from "metabase/lib/query";
-
+import { t } from 'c-3po';
 
 export default class QueryDefinitionTooltip extends Component {
 
@@ -23,14 +23,14 @@ export default class QueryDefinitionTooltip extends Component {
             <div className="p2" style={{width: 250}}>
                 <div>
                     { type && type === "metric" && !object.is_active ?
-                        "This metric has been retired.  It's no longer available for use."
+                        t`This metric has been retired.  It's no longer available for use.`
                     :
                         object.description
                     }
                 </div>
                 { object.definition &&
                     <div className="mt2">
-                        <FieldSet legend="Definition" className="border-light">
+                        <FieldSet legend={t`Definition`} className="border-light">
                             <div className="TooltipFilterList">
                                 { Query.getAggregations(object.definition).map(aggregation =>
                                     <AggregationWidget
diff --git a/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx b/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
index 8dac1828f84217e29bdd2d2ed95ea3e8665b5985..359e48ec1d18fd343d72a456163a5db2bac11300 100644
--- a/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
@@ -1,6 +1,10 @@
 import React from "react";
 import PropTypes from "prop-types";
 
+import { t } from 'c-3po';
+import { parse as urlParse } from "url";
+import querystring from "querystring";
+
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
 import Icon from "metabase/components/Icon.jsx";
 import DownloadButton from "metabase/components/DownloadButton.jsx";
@@ -18,24 +22,24 @@ const EXPORT_FORMATS = ["csv", "xlsx", "json"];
 const QueryDownloadWidget = ({ className, card, result, uuid, token }) =>
     <PopoverWithTrigger
         triggerElement={
-            <Tooltip tooltip="Download full results">
-                <Icon title="Download this data" name="downarrow" size={16} />
+            <Tooltip tooltip={t`Download full results`}>
+                <Icon title={t`Download this data`} name="downarrow" size={16} />
             </Tooltip>
         }
         triggerClasses={cx(className, "text-brand-hover")}
     >
         <div className="p2" style={{ maxWidth: 320 }}>
-            <h4>Download full results</h4>
+            <h4>{t`Download full results`}</h4>
             { result.data.rows_truncated != null &&
-                <FieldSet className="my2 text-gold border-gold" legend="Warning">
-                    <div className="my1">Your answer has a large number of rows so it could take awhile to download.</div>
-                    <div>The maximum download size is 1 million rows.</div>
+                <FieldSet className="my2 text-gold border-gold" legend={t`Warning`}>
+                    <div className="my1">{t`Your answer has a large number of rows so it could take a while to download.`}</div>
+                    <div>{t`The maximum download size is 1 million rows.`}</div>
                 </FieldSet>
             }
             <div className="flex flex-row mt2">
                 {EXPORT_FORMATS.map(type =>
                     uuid ?
-                        <PublicQueryButton key={type} type={type} uuid={uuid} className="mr1 text-uppercase text-default" />
+                        <PublicQueryButton key={type} type={type} uuid={uuid} result={result} className="mr1 text-uppercase text-default" />
                     : token ?
                         <EmbedQueryButton key={type} type={type} token={token} className="mr1 text-uppercase text-default" />
                     : card && card.id ?
@@ -69,25 +73,38 @@ const SavedQueryButton = ({ className, type, result: { json_query }, card }) =>
         {type}
     </DownloadButton>
 
-const PublicQueryButton = ({ className, type, uuid }) =>
+const PublicQueryButton = ({ className, type, uuid, result: { json_query }}) =>
     <DownloadButton
         className={className}
         method="GET"
         url={Urls.publicCard(uuid, type)}
+        params={{ parameters: JSON.stringify(json_query.parameters) }}
         extensions={[type]}
     >
         {type}
     </DownloadButton>
 
-const EmbedQueryButton = ({ className, type, token }) =>
-    <DownloadButton
-        className={className}
-        method="GET"
-        url={Urls.embedCard(token, type)}
-        extensions={[type]}
-    >
-        {type}
-    </DownloadButton>
+
+const EmbedQueryButton = ({ className, type, token }) => {
+    // Parse the query string part of the URL (e.g. the `?key=value` part) into an object. We need to pass them this
+    // way to the `DownloadButton` because it's a form which means we need to insert a hidden `<input>` for each param
+    // we want to pass along. For whatever wacky reason the /api/embed endpoint expect params like ?key=value instead
+    // of like ?params=<json-encoded-params-array> like the other endpoints do.
+    const query  = urlParse(window.location.href).query; // get the part of the URL that looks like key=value
+    const params = query && querystring.parse(query);    // expand them out into a map
+
+    return (
+        <DownloadButton
+            className={className}
+            method="GET"
+            url={Urls.embedCard(token, type)}
+            params={params}
+            extensions={[type]}
+        >
+            {type}
+        </DownloadButton>
+    );
+}
 
 QueryDownloadWidget.propTypes = {
     className: PropTypes.string,
diff --git a/frontend/src/metabase/query_builder/components/QueryHeader.jsx b/frontend/src/metabase/query_builder/components/QueryHeader.jsx
index 82550ab31df4c265be0ba0792acac0ccd49774cc..7961a6fe603136bb9f911a5361e29a0c37ab22bf 100644
--- a/frontend/src/metabase/query_builder/components/QueryHeader.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryHeader.jsx
@@ -2,7 +2,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
 import { connect } from "react-redux";
-
+import { t } from 'c-3po';
 import QueryModeButton from "./QueryModeButton.jsx";
 
 import ActionButton from 'metabase/components/ActionButton.jsx';
@@ -281,14 +281,14 @@ export default class QueryHeader extends Component {
                         >
                             <span>
                                 <Icon name='check' size={12} />
-                                <span className="ml1">Saved</span>
+                                <span className="ml1">{t`Saved`}</span>
                             </span>
                         </button>
                     ]);
                 } else {
                     // edit button
                     buttonSections.push([
-                        <Tooltip key="edit" tooltip="Edit question">
+                        <Tooltip key="edit" tooltip={t`Edit question`}>
                             <a className="cursor-pointer text-brand-hover" onClick={this.onBeginEditing}>
                                 <Icon name="pencil" size={16} />
                             </a>
@@ -303,17 +303,17 @@ export default class QueryHeader extends Component {
                         key="save"
                         actionFn={() => this.onSave(this.props.card, false)}
                         className="cursor-pointer text-brand-hover bg-white text-grey-4 text-uppercase"
-                        normalText="SAVE CHANGES"
-                        activeText="Saving…"
-                        failedText="Save failed"
-                        successText="Saved"
+                        normalText={t`SAVE CHANGES`}
+                        activeText={t`Saving…`}
+                        failedText={t`Save failed`}
+                        successText={t`Saved`}
                     />
                 ]);
 
                 // cancel button
                 buttonSections.push([
                     <a key="cancel" className="cursor-pointer text-brand-hover text-grey-4 text-uppercase" onClick={this.onCancel}>
-                        CANCEL
+                        {t`CANCEL`}
                     </a>
                 ]);
 
@@ -328,7 +328,7 @@ export default class QueryHeader extends Component {
                         key="move"
                         full
                         triggerElement={
-                            <Tooltip tooltip="Move question">
+                            <Tooltip tooltip={t`Move question`}>
                                 <Icon name="move" />
                             </Tooltip>
                         }
@@ -353,7 +353,7 @@ export default class QueryHeader extends Component {
                 'text-brand-hover': !this.props.uiControls.isShowingTemplateTagsEditor
             });
             buttonSections.push([
-                <Tooltip key="parameterEdititor" tooltip="Variables">
+                <Tooltip key="parameterEdititor" tooltip={t`Variables`}>
                     <a className={parametersButtonClasses}>
                         <Icon name="variable" size={16} onClick={this.props.toggleTemplateTagsEditor}></Icon>
                     </a>
@@ -365,7 +365,7 @@ export default class QueryHeader extends Component {
         if (!isNew && !isEditing) {
             // simply adding an existing saved card to a dashboard, so show the modal to do so
             buttonSections.push([
-                <Tooltip key="addtodash" tooltip="Add to dashboard">
+                <Tooltip key="addtodash" tooltip={t`Add to dashboard`}>
                     <span data-metabase-event={"QueryBuilder;AddToDash Modal;normal"} className="cursor-pointer text-brand-hover" onClick={() => this.setState({ modal: "add-to-dashboard" })}>
                         <Icon name="addtodash" size={ICON_SIZE} />
                     </span>
@@ -374,7 +374,7 @@ export default class QueryHeader extends Component {
         } else if (isNew && isDirty) {
             // this is a new card, so we need the user to save first then they can add to dash
             buttonSections.push([
-                <Tooltip key="addtodashsave" tooltip="Add to dashboard">
+                <Tooltip key="addtodashsave" tooltip={t`Add to dashboard`}>
                     <ModalWithTrigger
                         ref="addToDashSaveModal"
                         triggerClasses="h4 text-brand-hover text-uppercase"
@@ -403,7 +403,7 @@ export default class QueryHeader extends Component {
         // history icon on saved cards
         if (!isNew) {
             buttonSections.push([
-                <Tooltip key="history" tooltip="Revision history">
+                <Tooltip key="history" tooltip={t`Revision history`}>
                     <ModalWithTrigger
                         ref="cardHistory"
                         triggerElement={<span className="text-brand-hover"><Icon name="history" size={18} /></span>}
@@ -446,7 +446,7 @@ export default class QueryHeader extends Component {
             'text-brand-hover': !this.state.isShowingDataReference
         });
         buttonSections.push([
-            <Tooltip key="dataReference" tooltip="Learn about your data">
+            <Tooltip key="dataReference" tooltip={t`Learn about your data`}>
                 <a className={dataReferenceButtonClasses}>
                     <Icon name='reference' size={ICON_SIZE} onClick={this.onToggleDataReference}></Icon>
                 </a>
@@ -514,9 +514,9 @@ export default class QueryHeader extends Component {
             <div className="relative">
                 <HeaderBar
                     isEditing={this.props.isEditing}
-                    name={this.props.isNew ? "New question" : this.props.card.name}
+                    name={this.props.isNew ? t`New question` : this.props.card.name}
                     description={this.props.card ? this.props.card.description : null}
-                    breadcrumb={(!this.props.card.id && this.props.originalCard) ? (<span className="pl2">started from <a className="link" onClick={this.onFollowBreadcrumb}>{this.props.originalCard.name}</a></span>) : null }
+                    breadcrumb={(!this.props.card.id && this.props.originalCard) ? (<span className="pl2">{t`started from`} <a className="link" onClick={this.onFollowBreadcrumb}>{this.props.originalCard.name}</a></span>) : null }
                     buttons={this.getHeaderButtons()}
                     setItemAttributeFn={this.props.onSetCardAttribute}
                     badge={this.props.card.collection &&
diff --git a/frontend/src/metabase/query_builder/components/QueryModeButton.jsx b/frontend/src/metabase/query_builder/components/QueryModeButton.jsx
index a01a7bdca6edfc255041b0eaa26aec10f35c8883..26a45ea9f9755c453403637b87343ec7141fe9cf 100644
--- a/frontend/src/metabase/query_builder/components/QueryModeButton.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryModeButton.jsx
@@ -7,7 +7,7 @@ import { getEngineNativeType, formatJsonQuery } from "metabase/lib/engine";
 import Icon from "metabase/components/Icon.jsx";
 import Modal from "metabase/components/Modal.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
-
+import { t } from 'c-3po';
 
 export default class QueryModeButton extends Component {
 
@@ -38,17 +38,17 @@ export default class QueryModeButton extends Component {
         var targetType = (mode === "query") ? "native" : "query";
 
         const engine = tableMetadata && tableMetadata.db.engine;
-        const nativeQueryName = getEngineNativeType(engine) === "sql" ? "SQL" : "native query";
+        const nativeQueryName = getEngineNativeType(engine) === "sql" ? t`SQL` : t`native query`;
 
         // maybe switch up the icon based on mode?
         let onClick = null;
-        let tooltip = "Not Supported";
+        let tooltip = t`Not Supported`;
         if (mode === "query" && allowQueryToNative) {
             onClick = nativeForm ? () => this.setState({isOpen: true}) : () => onSetMode("native");
-            tooltip = nativeForm ? `View the ${nativeQueryName}` : `Switch to ${nativeQueryName}`;
+            tooltip = nativeForm ? t`View the ${nativeQueryName}` : t`Switch to ${nativeQueryName}`;
         } else if (mode === "native" && allowNativeToQuery) {
             onClick = () => onSetMode("query");
-            tooltip = "Switch to Builder";
+            tooltip = t`Switch to Builder`;
         }
 
         return (
@@ -62,7 +62,7 @@ export default class QueryModeButton extends Component {
                 <Modal medium isOpen={this.state.isOpen} onClose={() => this.setState({isOpen: false})}>
                     <div className="p4">
                         <div className="mb3 flex flex-row flex-full align-center justify-between">
-                            <h2>{capitalize(nativeQueryName)} for this question</h2>
+                            <h2>{t`${capitalize(nativeQueryName)} for this question`}</h2>
                             <span className="cursor-pointer" onClick={() => this.setState({isOpen: false})}><Icon name="close" size={16} /></span>
                         </div>
 
@@ -79,7 +79,7 @@ export default class QueryModeButton extends Component {
                             <a className="Button Button--primary" onClick={() => {
                                 onSetMode(targetType);
                                 this.setState({isOpen: false});
-                            }}>Convert this question to {nativeQueryName}</a>
+                            }}>{t`Convert this question to ${nativeQueryName}`}</a>
                         </div>
                     </div>
                 </Modal>
diff --git a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
index fb46c7fac5f2e49255777c2388e7b4c631e0eb3d..f7e38151264517fed2b7896f5acf6c2cf69558fd 100644
--- a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
@@ -2,7 +2,7 @@
 
 import React, { Component } from "react";
 import { Link } from "react-router";
-
+import { t, jt } from 'c-3po';
 import LoadingSpinner from 'metabase/components/LoadingSpinner.jsx';
 import Tooltip from "metabase/components/Tooltip";
 import Icon from "metabase/components/Icon";
@@ -105,7 +105,7 @@ export default class QueryVisualization extends Component {
 
         let runButtonTooltip;
         if (!isResultDirty && result && result.cached && result.average_execution_time > REFRESH_TOOLTIP_THRESHOLD) {
-            runButtonTooltip = `This question will take approximately ${duration(result.average_execution_time)} to refresh`;
+            runButtonTooltip = t`This question will take approximately ${duration(result.average_execution_time)} to refresh`;
         }
 
         const messages = [];
@@ -114,7 +114,7 @@ export default class QueryVisualization extends Component {
                 icon: "clock",
                 message: (
                     <div>
-                        Updated {moment(result.updated_at).fromNow()}
+                        {t`Updated ${moment(result.updated_at).fromNow()}`}
                     </div>
                 )
             })
@@ -125,9 +125,7 @@ export default class QueryVisualization extends Component {
                 message: (
                     // class name is included for the sake of making targeting the element in tests easier
                     <div className="ShownRowCount">
-                        { result.data.rows_truncated != null ? ("Showing first ") : ("Showing ")}
-                        <strong>{formatNumber(result.row_count)}</strong>
-                        { " " + inflect("row", result.data.rows.length) }
+                        {jt`${ result.data.rows_truncated != null ? (t`Showing first`) : (t`Showing`)} ${<strong>{formatNumber(result.row_count)}</strong>} ${inflect("row", result.data.rows.length)}`}
                     </div>
                 )
             })
@@ -232,7 +230,7 @@ export default class QueryVisualization extends Component {
                 { isRunning && (
                     <div className="Loading spread flex flex-column layout-centered text-brand z2">
                         <LoadingSpinner />
-                        <h2 className="Loading-message text-brand text-uppercase my3">Doing science...</h2>
+                        <h2 className="Loading-message text-brand text-uppercase my3">{t`Doing science`}...</h2>
                     </div>
                 )}
                 <div className={visualizationClasses}>
@@ -245,6 +243,6 @@ export default class QueryVisualization extends Component {
 
 export const VisualizationEmptyState = ({showTutorialLink}) =>
     <div className="flex full layout-centered text-grey-1 flex-column">
-        <h1>If you give me some data I can show you something cool. Run a Query!</h1>
-        { showTutorialLink && <Link to={Urls.question(null, "?tutorial")} className="link cursor-pointer my2">How do I use this thing?</Link> }
+        <h1>{t`If you give me some data I can show you something cool. Run a Query!`}</h1>
+        { showTutorialLink && <Link to={Urls.question(null, "?tutorial")} className="link cursor-pointer my2">{t`How do I use this thing?`}</Link> }
     </div>;
diff --git a/frontend/src/metabase/query_builder/components/RunButton.jsx b/frontend/src/metabase/query_builder/components/RunButton.jsx
index 42853ee10ee46309da3ae86aa4a342803d2a4441..579e96c1be69958b0831fa8aa3115deaa74f989f 100644
--- a/frontend/src/metabase/query_builder/components/RunButton.jsx
+++ b/frontend/src/metabase/query_builder/components/RunButton.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import Icon from "metabase/components/Icon.jsx";
 
 import cx from "classnames";
@@ -18,11 +18,11 @@ export default class RunButton extends Component {
         let { isRunnable, isRunning, isDirty, onRun, onCancel } = this.props;
         let buttonText = null;
         if (isRunning) {
-            buttonText = <div className="flex align-center"><Icon className="mr1" name="close" />Cancel</div>;
+            buttonText = <div className="flex align-center"><Icon className="mr1" name="close" />{t`Cancel`}</div>;
         } else if (isRunnable && isDirty) {
-            buttonText = "Get Answer";
+            buttonText = t`Get Answer`;
         } else if (isRunnable && !isDirty) {
-            buttonText = <div className="flex align-center"><Icon className="mr1" name="refresh" />Refresh</div>;
+            buttonText = <div className="flex align-center"><Icon className="mr1" name="refresh" />{t`Refresh`}</div>;
         }
         let actionFn = isRunning ? onCancel : onRun;
         let classes = cx("Button Button--medium circular RunButton ml-auto mr-auto block", {
diff --git a/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx b/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx
index a96780e8789ef7856e65e810d87337ff5cc024b6..7a7cad744561fd3e98ce6b6cdfb456e011892522 100644
--- a/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx
+++ b/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 
 import Modal from "metabase/components/Modal.jsx";
-
+import { t } from 'c-3po';
 
 export default class SavedQuestionIntroModal extends Component {
 
@@ -10,13 +10,13 @@ export default class SavedQuestionIntroModal extends Component {
             <Modal small isOpen={this.props.isShowingNewbModal}>
                 <div className="Modal-content Modal-content--small NewForm">
                     <div className="Modal-header Form-header">
-                        <h2 className="pb2 text-dark">It's okay to play around with saved questions</h2>
+                        <h2 className="pb2 text-dark">{t`It's okay to play around with saved questions`}</h2>
 
-                        <div className="pb1 text-grey-4">You won't make any permanent changes to a saved question unless you click the edit icon in the top-right.</div>
+                        <div className="pb1 text-grey-4">{t`You won't make any permanent changes to a saved question unless you click the edit icon in the top-right.`}</div>
                     </div>
 
                     <div className="Form-actions flex justify-center py1">
-                        <button data-metabase-event={"QueryBuilder;IntroModal"} className="Button Button--primary" onClick={() => this.props.onClose()}>Okay</button>
+                        <button data-metabase-event={"QueryBuilder;IntroModal"} className="Button Button--primary" onClick={() => this.props.onClose()}>{t`Okay`}</button>
                     </div>
                 </div>
             </Modal>
diff --git a/frontend/src/metabase/query_builder/components/SearchBar.jsx b/frontend/src/metabase/query_builder/components/SearchBar.jsx
index abca8cf77a553d7451d85c4abab220305fa03c55..4a99c146d12b198bd5f8b9783ed995cabff76a0b 100644
--- a/frontend/src/metabase/query_builder/components/SearchBar.jsx
+++ b/frontend/src/metabase/query_builder/components/SearchBar.jsx
@@ -1,6 +1,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
+import { t } from 'c-3po';
 
 export default class SearchBar extends React.Component {
     constructor(props, context) {
@@ -19,7 +20,7 @@ export default class SearchBar extends React.Component {
 
     render() {
         return (
-            <input className="SearchBar" type="text" ref="filterTextInput" value={this.props.filter} placeholder="Search for" onChange={this.handleInputChange}/>
+            <input className="SearchBar" type="text" ref="filterTextInput" value={this.props.filter} placeholder={t`Search for`} onChange={this.handleInputChange}/>
         );
     }
 }
diff --git a/frontend/src/metabase/query_builder/components/SelectionModule.jsx b/frontend/src/metabase/query_builder/components/SelectionModule.jsx
index 3224055dd7097aabaa793cbe254834b892fb1e8a..7273413707ca625449b11cec3c11e03a54fdac14 100644
--- a/frontend/src/metabase/query_builder/components/SelectionModule.jsx
+++ b/frontend/src/metabase/query_builder/components/SelectionModule.jsx
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
 import Popover from "metabase/components/Popover.jsx";
 import Icon from "metabase/components/Icon.jsx";
 import SearchBar from './SearchBar.jsx';
-
+import { t } from 'c-3po';
 import _ from "underscore";
 import cx from "classnames";
 
@@ -145,7 +145,7 @@ export default class SelectionModule extends Component {
                         <Icon name="chevrondown" size={12} />
                         <div>
                             <div className="SelectionModule-display">
-                                {this.props.expandedTitle || "Advanced..."}
+                                {this.props.expandedTitle || t`Advanced...`}
                             </div>
                         </div>
                     </li>
@@ -154,7 +154,7 @@ export default class SelectionModule extends Component {
 
             return items;
         } else {
-            return "Sorry. Something went wrong.";
+            return t`Sorry. Something went wrong.`;
         }
     }
 
diff --git a/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx b/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx
index 462c94134b36f94079b7323aa464cdc66a2ecabd..580e4bb713cedf71a987e503daad6647a1c459d1 100644
--- a/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx
+++ b/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx
@@ -2,7 +2,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 
 import { parseFieldBucketing, formatBucketing } from "metabase/lib/query_time";
-
+import { t } from 'c-3po';
 import cx from "classnames";
 
 const BUCKETINGS = [
@@ -37,7 +37,7 @@ export default class TimeGroupingPopover extends Component {
     };
 
     static defaultProps = {
-        title: "Group time by",
+        title: t`Group time by`,
         groupingOptions: [
             // "default",
             "minute",
diff --git a/frontend/src/metabase/query_builder/components/VisualizationError.jsx b/frontend/src/metabase/query_builder/components/VisualizationError.jsx
index 587ca14eee21cdbf8e8e831d1d9551b09d29c230..9dccff47adb05031e3938b790815f653917b4d81 100644
--- a/frontend/src/metabase/query_builder/components/VisualizationError.jsx
+++ b/frontend/src/metabase/query_builder/components/VisualizationError.jsx
@@ -2,7 +2,7 @@
 
 import React, { Component } from 'react';
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import MetabaseSettings from "metabase/lib/settings";
 import VisualizationErrorMessage from './VisualizationErrorMessage';
 
@@ -40,15 +40,15 @@ class VisualizationError extends Component {
           if (duration > 15*1000) {
               return <VisualizationErrorMessage
                         type="timeout"
-                        title="Your question took too long"
-                        message="We didn't get an answer back from your database in time, so we had to stop. You can try again in a minute, or if the problem persists, you can email an admin to let them know."
+                        title={t`Your question took too long`}
+                        message={t`We didn't get an answer back from your database in time, so we had to stop. You can try again in a minute, or if the problem persists, you can email an admin to let them know.`}
                         action={<EmailAdmin />}
                     />
           } else {
               return <VisualizationErrorMessage
                         type="serverError"
-                        title="We're experiencing server issues"
-                        message="Try refreshing the page after waiting a minute or two. If the problem persists we'd recommend you contact an admin."
+                        title={t`We're experiencing server issues`}
+                        message={t`Try refreshing the page after waiting a minute or two. If the problem persists we'd recommend you contact an admin.`}
                         action={<EmailAdmin />}
                     />
           }
@@ -69,13 +69,13 @@ class VisualizationError extends Component {
               <div className="QueryError2 flex full justify-center">
                   <div className="QueryError-image QueryError-image--queryError mr4" />
                   <div className="QueryError2-details">
-                      <h1 className="text-bold">There was a problem with your question</h1>
-                      <p className="QueryError-messageText">Most of the time this is caused by an invalid selection or bad input value.  Double check your inputs and retry your query.</p>
+                      <h1 className="text-bold">{t`There was a problem with your question`}</h1>
+                      <p className="QueryError-messageText">{t`Most of the time this is caused by an invalid selection or bad input value. Double check your inputs and retry your query.`}</p>
                       <div className="pt2">
-                          <a onClick={() => this.setState({ showError: true })} className="link cursor-pointer">Show error details</a>
+                          <a onClick={() => this.setState({ showError: true })} className="link cursor-pointer">{t`Show error details`}</a>
                       </div>
                       <div style={{ display: this.state.showError? 'inherit': 'none'}} className="pt3 text-left">
-                          <h2>Here's the full error message</h2>
+                          <h2>{t`Here's the full error message`}</h2>
                           <div style={{fontFamily: "monospace"}} className="QueryError2-detailBody bordered rounded bg-grey-0 text-bold p2 mt1">{error}</div>
                       </div>
                   </div>
diff --git a/frontend/src/metabase/query_builder/components/VisualizationResult.jsx b/frontend/src/metabase/query_builder/components/VisualizationResult.jsx
index 9c7bf3222356932d31bac0a0178c8c5a8e112ef5..a359275c421af01e90aa68803b600746a6b6c9d8 100644
--- a/frontend/src/metabase/query_builder/components/VisualizationResult.jsx
+++ b/frontend/src/metabase/query_builder/components/VisualizationResult.jsx
@@ -47,14 +47,16 @@ export default class VisualizationResult extends Component {
                 <VisualizationErrorMessage
                     type='noRows'
                     title='No results!'
-                    message='This may be the answer you’re looking for. If not, try removing or changing your filters to make them less specific.'
+                    message={t`This may be the answer you’re looking for. If not, try removing or changing your filters to make them less specific.`}
                     action={
                         <div>
-                            { supportsRowsPresentAlert && !isDirty && <p>
-                                You can also <a className="link" onClick={this.showCreateAlertModal}>get an alert</a> when there are any results.
-                            </p> }
+                            { supportsRowsPresentAlert && !isDirty && (
+                                <p>
+                                    {jt`You can also ${<a className="link" onClick={this.showCreateAlertModal}>get an alert</a>} when there are any results.`}
+                                </p>
+                                )}
                             <button className="Button" onClick={() => window.history.back() }>
-                                Back to last run
+                                {t`Back to last run`}
                             </button>
                         </div>
                     }
@@ -76,14 +78,16 @@ export default class VisualizationResult extends Component {
                 data: results[index] && results[index].data
             }));
 
-            return <Visualization
-                series={series}
-                onChangeCardAndRun={navigateToNewCardInsideQB}
-                isEditing={true}
-                card={question.card()}
-                // Table:
-                {...props}
-            />
+            return (
+                <Visualization
+                    series={series}
+                    onChangeCardAndRun={navigateToNewCardInsideQB}
+                    isEditing={true}
+                    card={question.card()}
+                    // Table:
+                    {...props}
+                />
+            )
         }
     }
 }
diff --git a/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx b/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx
index 83136b629dec99a2d2a84a73acc30912538ff80f..da1f76b73e816d5c49b05630256c6bd41053441f 100644
--- a/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx
+++ b/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx
@@ -1,6 +1,6 @@
 import React from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import Icon from "metabase/components/Icon.jsx";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
 import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
@@ -52,7 +52,7 @@ export default class VisualizationSettings extends React.Component {
                     className="GuiBuilder-section-label pl0 Query-label"
                     style={{ marginLeft: 4 }}
                 >
-                    Visualization
+                    {t`Visualization`}
                 </span>
                 <PopoverWithTrigger
                     id="VisualizationPopover"
diff --git a/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx b/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx
index 180439c363556a19563d91b96b65dc694bd942b8..7da4f03cb7e31d3e36a6b4eeb466c590c9ae7960 100644
--- a/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx
@@ -1,7 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import MainPane from './MainPane.jsx';
 import TablePane from './TablePane.jsx';
 import FieldPane from './FieldPane.jsx';
@@ -72,7 +72,7 @@ export default class DataReference extends Component {
             backButton = (
                 <a className="flex align-center mb2 text-default text-brand-hover no-decoration" onClick={this.back}>
                     <Icon name="chevronleft" size={18} />
-                    <span className="text-uppercase">Back</span>
+                    <span className="text-uppercase">{t`Back`}</span>
                 </a>
             )
         }
diff --git a/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx b/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx
index 6ebc166666bf52eb0fb1ba1d885313227a8c587a..fe18456d42bfc7394569e5d00ed94c47f8e23082 100644
--- a/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx
@@ -1,18 +1,18 @@
 /* eslint "react/prop-types": "warn" */
 import React from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import cx from "classnames";
 
 const DetailPane = ({ name, description, usefulQuestions, useForCurrentQuestion, extra }) =>
     <div>
         <h1>{name}</h1>
         <p className={cx({ "text-grey-3": !description })}>
-            {description || "No description set."}
+            {description || t`No description set.`}
         </p>
         { useForCurrentQuestion && useForCurrentQuestion.length > 0 ?
             <div className="py1">
-                <p className="text-bold">Use for current question</p>
+                <p className="text-bold">{t`Use for current question`}</p>
                 <ul className="my2">
                 {useForCurrentQuestion.map((item, index) =>
                     <li className="mt1" key={index}>
@@ -24,7 +24,7 @@ const DetailPane = ({ name, description, usefulQuestions, useForCurrentQuestion,
         : null }
         { usefulQuestions && usefulQuestions.length > 0 ?
             <div className="py1">
-                <p className="text-bold">Potentially useful questions</p>
+                <p className="text-bold">{t`Potentially useful questions`}</p>
                 <ul>
                 {usefulQuestions.map((item, index) =>
                     <li className="border-row-divider" key={index}>
diff --git a/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx b/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx
index f8b045b0f5aceb165459f11b072aca9424ec3368..7fed60cb8fadcd05d6446e6ff18e516b9ebd085b 100644
--- a/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx
@@ -1,7 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import DetailPane from "./DetailPane.jsx";
 import QueryButton from "metabase/components/QueryButton.jsx";
 import UseForButton from "./UseForButton.jsx";
@@ -147,15 +147,15 @@ export default class FieldPane extends Component {
 
             // current field must be a valid breakout option for this table AND cannot already be in the breakout clause of our query
             if (validBreakout && !_.some(query.breakouts(), this.isBreakoutWithCurrentField)) {
-                useForCurrentQuestion.push(<UseForButton title={"Group by " + name} onClick={this.groupBy} />);
+                useForCurrentQuestion.push(<UseForButton title={t`Group by ${name}`} onClick={this.groupBy} />);
             }
         }
 
         if (isSummable(field)) {
-            usefulQuestions.push(<QueryButton icon="number" text={"Sum of all values of " + fieldName} onClick={this.setQuerySum} />);
+            usefulQuestions.push(<QueryButton icon="number" text={t`Sum of all values of ${fieldName}`} onClick={this.setQuerySum} />);
         }
-        usefulQuestions.push(<QueryButton icon="table" text={"All distinct values of " + fieldName} onClick={this.setQueryDistinct} />);
-        let queryCountGroupedByText = "Number of " + inflection.pluralize(tableName) + " grouped by " + fieldName;
+        usefulQuestions.push(<QueryButton icon="table" text={t`All distinct values of ${fieldName}`} onClick={this.setQueryDistinct} />);
+        let queryCountGroupedByText = t`Number of ${inflection.pluralize(tableName)} grouped by ${fieldName}`;
         if (validBreakout) {
             usefulQuestions.push(<QueryButton icon="bar" text={queryCountGroupedByText} onClick={this.setQueryCountGroupedBy.bind(null, "bar")} />);
             usefulQuestions.push(<QueryButton icon="pie" text={queryCountGroupedByText} onClick={this.setQueryCountGroupedBy.bind(null, "pie")} />);
diff --git a/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx b/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx
index 7963e969fe1f3420b53b59ea02643890de9c43dd..33a7680733fc73be2997d2e99d4f65ce0801a8e7 100644
--- a/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx
@@ -1,7 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import { isQueryable } from "metabase/lib/table";
 
 import inflection from "inflection";
@@ -9,8 +9,8 @@ import cx from "classnames";
 
 const MainPane = ({ databases, show }) =>
     <div>
-        <h1>Data Reference</h1>
-        <p>Learn more about your data structure to ask more useful questions.</p>
+        <h1>{t`Data Reference`}</h1>
+        <p>{t`Learn more about your data structure to ask more useful questions`}.</p>
         <ul>
             {databases && databases.filter(db => db.tables && db.tables.length > 0).map(database =>
                 <li key={database.id}>
diff --git a/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx b/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx
index 8c90c9edf7b790b4c7a874e60635ab4577ee3479..f86cf3e5897a301346b9278083db2d70e896077f 100644
--- a/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import { connect } from "react-redux";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import DetailPane from "./DetailPane.jsx";
 import QueryButton from "metabase/components/QueryButton.jsx";
 import QueryDefinition from "./QueryDefinition.jsx";
@@ -54,7 +54,7 @@ export default class MetricPane extends Component {
             card.dataset_query = createQuery("query", table.db_id, table.id);
             return card;
         } else {
-            throw new Error("Could not find the table metadata prior to creating a new question")
+            throw new Error(t`Could not find the table metadata prior to creating a new question`)
         }
     }
 
@@ -72,7 +72,7 @@ export default class MetricPane extends Component {
         let useForCurrentQuestion = [];
         let usefulQuestions = [];
 
-        usefulQuestions.push(<QueryButton icon="number" text={"See " + metricName} onClick={this.setQueryMetric} />);
+        usefulQuestions.push(<QueryButton icon="number" text={t`See ${metricName}`} onClick={this.setQueryMetric} />);
 
         return (
             <DetailPane
@@ -82,7 +82,7 @@ export default class MetricPane extends Component {
                 usefulQuestions={usefulQuestions}
                 extra={metadata &&
                     <div>
-                        <p className="text-bold">Metric Definition</p>
+                        <p className="text-bold">{t`Metric Definition`}</p>
                         <QueryDefinition object={metric} tableMetadata={metadata.tables[metric.table_id]} />
                     </div>
                 }
diff --git a/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx b/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx
index cc2c21af640cf089c7f9d9cd80646a99ce8c8480..d7bb3e26e1f12f294b486d26711848e31569ea23 100644
--- a/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-
+import { t } from 'c-3po';
 import { fetchTableMetadata } from "metabase/redux/metadata";
 import { getMetadata } from "metabase/selectors/metadata";
 
@@ -75,7 +75,7 @@ export default class SegmentPane extends Component {
             card.dataset_query = createQuery("query", table.db_id, table.id);
             return card;
         } else {
-            throw new Error("Could not find the table metadata prior to creating a new question")
+            throw new Error(t`Could not find the table metadata prior to creating a new question`)
         }
     }
     setQueryFilteredBy() {
@@ -106,11 +106,11 @@ export default class SegmentPane extends Component {
             query.tableId() === segment.table_id &&
             !_.findWhere(query.filters(), {[0]: "SEGMENT", [1]: segment.id})) {
 
-            useForCurrentQuestion.push(<UseForButton title={"Filter by " + segmentName} onClick={this.filterBy} />);
+            useForCurrentQuestion.push(<UseForButton title={t`Filter by ${segmentName}`} onClick={this.filterBy} />);
         }
 
-        usefulQuestions.push(<QueryButton icon="number" text={"Number of " + segmentName} onClick={this.setQueryCountFilteredBy} />);
-        usefulQuestions.push(<QueryButton icon="table" text={"See all " + segmentName} onClick={this.setQueryFilteredBy} />);
+        usefulQuestions.push(<QueryButton icon="number" text={t`Number of ${segmentName}`} onClick={this.setQueryCountFilteredBy} />);
+        usefulQuestions.push(<QueryButton icon="table" text={t`See all ${segmentName}`} onClick={this.setQueryFilteredBy} />);
 
         return (
             <DetailPane
@@ -120,7 +120,7 @@ export default class SegmentPane extends Component {
                 usefulQuestions={usefulQuestions}
                 extra={metadata &&
                 <div>
-                    <p className="text-bold">Segment Definition</p>
+                    <p className="text-bold">{t`Segment Definition`}</p>
                     <QueryDefinition object={segment} tableMetadata={metadata.tables[segment.table_id]} />
                 </div>
                 }
diff --git a/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx b/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx
index 16104b9997b4f15f81d946675026aba56a0b56b1..cc3277642638417747b9cf0a7336feb7d9a5930b 100644
--- a/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx
@@ -1,7 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import QueryButton from "metabase/components/QueryButton.jsx";
 import { createCard } from "metabase/lib/card";
 import { createQuery } from "metabase/lib/query";
@@ -41,7 +41,7 @@ export default class TablePane extends Component {
             });
         }).catch((error) => {
             this.setState({
-                error: "An error occurred loading the table"
+                error: t`An error occurred loading the table`
             });
         });
     }
@@ -61,7 +61,7 @@ export default class TablePane extends Component {
         if (table) {
             var queryButton;
             if (table.rows != null) {
-                var text = `See the raw data for ${table.display_name}`
+                var text = t`See the raw data for ${table.display_name}`
                 queryButton = (<QueryButton className="border-bottom border-top mb3" icon="table" text={text} onClick={this.setQueryAllRows} />);
             }
             var panes = {
@@ -108,7 +108,7 @@ export default class TablePane extends Component {
             } else
 
             var descriptionClasses = cx({ "text-grey-3": !table.description });
-            var description = (<p className={descriptionClasses}>{table.description || "No description set."}</p>);
+            var description = (<p className={descriptionClasses}>{table.description || t`No description set.`}</p>);
 
             return (
                 <div>
@@ -154,7 +154,7 @@ const ExpandableItemList = Expandable(({ name, type, show, items, isExpanded, on
                     {item.name}
                 </ListItem>
             ) }
-            { !isExpanded && <ListItem onClick={onExpand}>More...</ListItem>}
+            { !isExpanded && <ListItem onClick={onExpand}>{t`More`}...</ListItem>}
         </ul>
     </div>
 );
diff --git a/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx b/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx
index 3d9a6ac0238d83db8e092fd1027e9c8ac0c8dde5..f4807b81382007ba404729861dc2855f1df2bb69 100644
--- a/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx
+++ b/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx
@@ -2,7 +2,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import S from "./ExpressionEditorTextfield.css";
-
+import { t } from 'c-3po';
 import _ from "underscore";
 import cx from "classnames";
 
@@ -171,7 +171,7 @@ export default class ExpressionEditorTextfield extends Component {
         } else if (this.state.expressionErrorMessage) {
             this.props.onError(this.state.expressionErrorMessage);
         } else {
-            this.props.onError({ message: "Invalid expression" });
+            this.props.onError({ message: t`Invalid expression` });
         }
     }
 
@@ -239,7 +239,7 @@ export default class ExpressionEditorTextfield extends Component {
 
     render() {
         let errorMessage = this.state.expressionErrorMessage;
-        if (errorMessage && !errorMessage.length) errorMessage = 'unknown error';
+        if (errorMessage && !errorMessage.length) errorMessage = t`unknown error`;
 
         const { placeholder } = this.props;
         const { suggestions } = this.state;
diff --git a/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx b/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx
index 6ce36a79a693edd9cc033d9baf4edd47ae1d316f..7a85da155a7d21a1f7796d4515af6aae8863dcea 100644
--- a/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
 import PropTypes from "prop-types";
 import cx from "classnames";
 import _ from 'underscore';
-
+import { t } from 'c-3po';
 import ExpressionEditorTextfield from "./ExpressionEditorTextfield.jsx";
 import { isExpression } from "metabase/lib/expressions";
 
@@ -45,7 +45,7 @@ export default class ExpressionWidget extends Component {
         return (
             <div style={{maxWidth: "600px"}}>
                 <div className="p2">
-                    <div className="h5 text-uppercase text-grey-3 text-bold">Field formula</div>
+                    <div className="h5 text-uppercase text-grey-3 text-bold">{t`Field formula`}</div>
                     <div>
                         <ExpressionEditorTextfield
                             expression={expression}
@@ -54,19 +54,18 @@ export default class ExpressionWidget extends Component {
                             onError={(errorMessage) => this.setState({error: errorMessage})}
                         />
                       <p className="h5 text-grey-5">
-                            Think of this as being kind of like writing a formula in a spreadsheet program: you can use numbers, fields in this table,
-                            mathematical symbols like +, and some functions.  So you could type something like Subtotal &minus; Cost.
-                            &nbsp;<a className="link" target="_blank" href="http://www.metabase.com/docs/latest/users-guide/04-asking-questions.html#creating-a-custom-field">Learn more</a>
+                            {t`Think of this as being kind of like writing a formula in a spreadsheet program: you can use numbers, fields in this table, mathematical symbols like +, and some functions. So you could type something like Subtotal &minus; Cost.`}
+                            &nbsp;<a className="link" target="_blank" href="http://www.metabase.com/docs/latest/users-guide/04-asking-questions.html#creating-a-custom-field">{t`Learn more`}</a>
                         </p>
                     </div>
 
-                    <div className="mt3 h5 text-uppercase text-grey-3 text-bold">Give it a name</div>
+                    <div className="mt3 h5 text-uppercase text-grey-3 text-bold">{t`Give it a name`}</div>
                     <div>
                         <input
                             className="my1 input block full"
                             type="text"
                             value={this.state.name}
-                            placeholder="Something nice and descriptive"
+                            placeholder={t`Something nice and descriptive`}
                             onChange={(event) => this.setState({name: event.target.value})}
                         />
                     </div>
@@ -74,17 +73,17 @@ export default class ExpressionWidget extends Component {
 
                 <div className="mt2 p2 border-top flex flex-row align-center justify-between">
                     <div className="ml-auto">
-                        <button className="Button" onClick={() => this.props.onCancel()}>Cancel</button>
+                        <button className="Button" onClick={() => this.props.onCancel()}>{t`Cancel`}</button>
                           <button
                               className={cx("Button ml2", {"Button--primary": this.isValid()})}
                               onClick={() => this.props.onSetExpression(this.state.name, this.state.expression)}
                               disabled={!this.isValid()}>
-                                {this.props.expression ? "Update" : "Done"}
+                                {this.props.expression ? t`Update` : t`Done`}
                           </button>
                     </div>
                     <div>
                         {this.props.expression ?
-                         <a className="pr2 text-warning link" onClick={() => this.props.onRemoveExpression(this.props.name)}>Remove</a>
+                         <a className="pr2 text-warning link" onClick={() => this.props.onRemoveExpression(this.props.name)}>{t`Remove`}</a>
                          : null }
                     </div>
                 </div>
diff --git a/frontend/src/metabase/query_builder/components/expressions/Expressions.jsx b/frontend/src/metabase/query_builder/components/expressions/Expressions.jsx
index a65797a093eb9964c20136ebb8d773b71fe3b586..0806049ac0a87e38794d3d99f99f7c9778814048 100644
--- a/frontend/src/metabase/query_builder/components/expressions/Expressions.jsx
+++ b/frontend/src/metabase/query_builder/components/expressions/Expressions.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import _ from "underscore";
-
+import { t } from 'c-3po';
 import Icon from "metabase/components/Icon.jsx";
 import IconBorder from "metabase/components/IconBorder.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
@@ -46,7 +46,7 @@ export default class Expressions extends Component {
                         <IconBorder borderRadius="3px">
                             <Icon name="add" size={14} />
                         </IconBorder>
-                        <span className="ml1">Add a custom field</span>
+                        <span className="ml1">{t`Add a custom field`}</span>
                     </a>
             </div>
         );
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterList.jsx b/frontend/src/metabase/query_builder/components/filters/FilterList.jsx
index e09fda0b303597d6e9c2b476ae96bcfac0e5ded1..40067a808a13874428b460911d818f3b58494411 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterList.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterList.jsx
@@ -2,7 +2,7 @@
 
 import React, { Component } from "react";
 import { findDOMNode } from 'react-dom';
-
+import { t } from 'c-3po';
 import FilterWidget from './FilterWidget.jsx';
 
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
@@ -59,7 +59,7 @@ export default class FilterList extends Component {
                 {filters.map((filter, index) =>
                     <FilterWidget
                         key={index}
-                        placeholder="Item"
+                        placeholder={t`Item`}
                         // TODO: update widgets that are still passing tableMetadata instead of query
                         query={query || {
                             table: () => tableMetadata,
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
index 1ee9880f007018f037ba96a5f14054c57d495640..ef7c15238d11feed0e822599ccbd4ec5750c44bc 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
@@ -4,7 +4,7 @@ import React, { Component } from "react";
 
 import FieldList from "../FieldList.jsx";
 import OperatorSelector from "./OperatorSelector.jsx";
-
+import { t } from 'c-3po';
 import DatePicker from "./pickers/DatePicker.jsx";
 import NumberPicker from "./pickers/NumberPicker.jsx";
 import SelectPicker from "./pickers/SelectPicker.jsx";
@@ -228,7 +228,7 @@ export default class FilterPopover extends Component {
                     />
                 );
             }
-            return <span>not implemented {operatorField.type} {operator.multi ? "true" : "false"}</span>;
+            return <span>{t`not implemented ${operatorField.type}`} {operator.multi ? t`true` : t`false`}</span>;
         });
     }
 
@@ -296,7 +296,7 @@ export default class FilterPopover extends Component {
                             className={cx("Button Button--purple full", { "disabled": !this.isValid() })}
                             onClick={() => this.commitFilter(this.state.filter)}
                         >
-                            {!this.props.filter ? "Add filter" : "Update filter"}
+                            {!this.props.filter ? t`Add filter` : t`Update filter`}
                         </button>
                     </div>
                 </div>
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx b/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx
index aabfa530cb6576c0f59b71d989ed9f3e77a8deed..74aea4289252ee59941ca031a094443ce8516036 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx
@@ -1,7 +1,7 @@
 /* @flow */
 
 import React, { Component } from "react";
-
+import { t } from 'c-3po';
 import Icon from "metabase/components/Icon.jsx";
 import FieldName from '../FieldName.jsx';
 import Popover from "metabase/components/Popover.jsx";
@@ -112,7 +112,7 @@ export default class FilterWidget extends Component {
             <div onClick={this.open}>
                 <div className="flex align-center" style={{padding: "0.5em", paddingTop: "0.3em", paddingBottom: "0.3em", paddingLeft: 0}}>
                     <div className="Filter-section Filter-section-field">
-                        <span className="QueryOption">Matches</span>
+                        <span className="QueryOption">{t`Matches`}</span>
                     </div>
                 </div>
                 <div className="flex align-center flex-wrap">
diff --git a/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx b/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx
index 4ac57c31ce1902806dc6521817ed0c5a770cf3a7..4470dd5c902eed3c79db32f89f4ec54178fb86e8 100644
--- a/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx
@@ -5,7 +5,7 @@ import ReactDOM from "react-dom";
 import PropTypes from "prop-types";
 import cx from "classnames";
 import _ from "underscore";
-
+import { t } from 'c-3po';
 import {forceRedraw} from "metabase/lib/dom";
 
 import Icon from "metabase/components/Icon.jsx";
@@ -76,7 +76,7 @@ export default class OperatorSelector extends Component {
                 { !expanded && expandedOperators.length > 0 ?
                     <div className="text-grey-3 text-purple-hover transition-color cursor-pointer" onClick={this.expandOperators}>
                         <Icon className="px1" name="chevrondown" size={14} />
-                        More Options
+                        {t`More Options`}
                     </div>
                 : null }
             </div>
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
index 4e40abafd66f67be41932b61dd54533d5354ca22..cde4e9e73fbb955f9cc3ab9fd143601af243046e 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
@@ -2,7 +2,7 @@
 
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import SpecificDatePicker from "./SpecificDatePicker";
 import RelativeDatePicker, { DATE_PERIODS, UnitPicker } from "./RelativeDatePicker";
 import DateOperatorSelector from "../DateOperatorSelector";
@@ -159,52 +159,52 @@ export type Operator = {
 }
 
 const ALL_TIME_OPERATOR = {
-    name: "All Time",
+    name: t`All Time`,
     init: () => null,
     test: (op) => op === null
 }
 
 export const DATE_OPERATORS: Operator[] = [
     {
-        name: "Previous",
+        name: t`Previous`,
         init: (filter) => ["time-interval", getDateTimeField(filter[1]), -getIntervals(filter), getUnit(filter)],
         // $FlowFixMe
         test: ([op, field, value]) => mbqlEq(op, "time-interval") && value < 0 || Object.is(value, -0),
         widget: PreviousPicker,
     },
     {
-        name: "Next",
+        name: t`Next`,
         init: (filter) => ["time-interval", getDateTimeField(filter[1]), getIntervals(filter), getUnit(filter)],
         // $FlowFixMe
         test: ([op, field, value]) => mbqlEq(op, "time-interval") && value >= 0,
         widget: NextPicker,
     },
     {
-        name: "Current",
+        name: t`Current`,
         init: (filter) => ["time-interval", getDateTimeField(filter[1]), "current", getUnit(filter)],
         test: ([op, field, value]) => mbqlEq(op, "time-interval") && value === "current",
         widget: CurrentPicker,
     },
     {
-        name: "Before",
+        name: t`Before`,
         init: (filter) =>  ["<", ...getDateTimeFieldAndValues(filter, 1)],
         test: ([op]) => op === "<",
         widget: SingleDatePicker,
     },
     {
-        name: "After",
+        name: t`After`,
         init: (filter) => [">", ...getDateTimeFieldAndValues(filter, 1)],
         test: ([op]) => op === ">",
         widget: SingleDatePicker,
     },
     {
-        name: "On",
+        name: t`On`,
         init: (filter) => ["=", ...getDateTimeFieldAndValues(filter, 1)],
         test: ([op]) => op === "=",
         widget: SingleDatePicker,
     },
     {
-        name: "Between",
+        name: t`Between`,
         init: (filter) => ["BETWEEN", ...getDateTimeFieldAndValues(filter, 2)],
         test: ([op]) => mbqlEq(op, "between"),
         widget: MultiDatePicker,
@@ -214,12 +214,12 @@ export const DATE_OPERATORS: Operator[] = [
 
 export const EMPTINESS_OPERATORS: Operator[] = [
     {
-        name: "Is Empty",
+        name: t`Is Empty`,
         init: (filter) => ["IS_NULL", getDateTimeField(filter[1])],
         test: ([op]) => op === "IS_NULL"
     },
     {
-        name: "Not Empty",
+        name: t`Not Empty`,
         init: (filter) => ["NOT_NULL", getDateTimeField(filter[1])],
         test: ([op]) => op === "NOT_NULL"
     }
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx
index 763d79577fcde28a10b4a10eb99c488a58213cda..808ae9da9557ed78e30a2da8a172b3bc42ba3c6e 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx
@@ -2,7 +2,7 @@
 
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import TextPicker from "./TextPicker.jsx";
 
 type Props = {
@@ -44,7 +44,7 @@ export default class NumberPicker extends Component {
     };
 
     static defaultProps = {
-        placeholder: "Enter desired number"
+        placeholder: t`Enter desired number`
     };
 
     _validate(values: Array<number|null>) {
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/SelectPicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/SelectPicker.jsx
index 44392273b7d1f1b45aabe06e82ec0038018ea4bd..7bb8ca60cf224e35e796bc0e49c337d2cdefa96b 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/SelectPicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/SelectPicker.jsx
@@ -2,7 +2,7 @@
 
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import CheckBox from 'metabase/components/CheckBox.jsx';
 import ListSearchField from "metabase/components/ListSearchField.jsx";
 
@@ -80,7 +80,7 @@ export default class SelectPicker extends Component {
 
     nameForOption(option: SelectOption) {
         if (option.name === "") {
-            return "Empty";
+            return t`Empty`;
         } else if (typeof option.name === "string") {
             return option.name;
         } else {
@@ -114,7 +114,7 @@ export default class SelectPicker extends Component {
                       <ListSearchField
                           onChange={this.updateSearchText}
                           searchText={this.state.searchText}
-                          placeholder="Find a value"
+                          placeholder={t`Find a value`}
                           autoFocus={true}
                       />
                   </div>
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
index 6d7b4204f5107aa6a0495cc5008f7321d088ded3..dbca125f17d187e477b8759e562b213c9e763d4f 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
@@ -2,7 +2,7 @@
 
 import React, { Component } from 'react';
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import Calendar from "metabase/components/Calendar";
 import Input from "metabase/components/Input";
 import Icon from "metabase/components/Icon";
@@ -107,7 +107,7 @@ export default class SpecificDatePicker extends Component {
                         <div className="border-right border-bottom border-top p2">
                             <Tooltip
                                 tooltip={
-                                    showCalendar ? "Hide calendar" : "Show calendar"
+                                    showCalendar ? t`Hide calendar` : t`Show calendar`
                                 }
                                 children={
                                     <Icon
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/TextPicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/TextPicker.jsx
index 1009b1f28827daba841c867c9265382b977b2594..e4362ed15928f076dda246f0d0ec04ad096a92f5 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/TextPicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/TextPicker.jsx
@@ -3,7 +3,7 @@
 import React, {Component} from "react";
 import PropTypes from "prop-types";
 import AutosizeTextarea from 'react-textarea-autosize';
-
+import { t } from 'c-3po';
 import cx from "classnames";
 import _ from "underscore";
 
@@ -35,7 +35,7 @@ export default class TextPicker extends Component {
 
     static defaultProps = {
         validations: [],
-        placeholder: "Enter desired text"
+        placeholder: t`Enter desired text`
     };
 
     constructor(props: Props) {
@@ -89,7 +89,7 @@ export default class TextPicker extends Component {
 
                 { multi ?
                     <div className="p1 text-small">
-                        You can enter multiple values separated by commas
+                        {t`You can enter multiple values separated by commas`}
                     </div>
                     : null }
             </div>
diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx
index 481a9b1945598bbe8845faf17eaa0b7a0b184203..66abc7d4a0bf23316911d8730e722d97b696cae7 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx
@@ -1,5 +1,5 @@
 import React from "react";
-
+import { t, jt } from 'c-3po';
 import Code from "metabase/components/Code.jsx";
 
 const EXAMPLES = {
@@ -59,7 +59,7 @@ const TagExample = ({ datasetQuery, setDatasetQuery }) =>
                     data-metabase-event="QueryBuilder;Template Tag Example Query Used"
                     onClick={() => setDatasetQuery(datasetQuery, true) }
                 >
-                    Try it
+                    {t`Try it`}
                 </div>
             )}
         </p>
@@ -77,51 +77,39 @@ const TagEditorHelp = ({ setDatasetQuery, sampleDatasetId }) => {
     }
     return (
         <div>
-            <h4>What's this for?</h4>
+            <h4>{t`What's this for?`}</h4>
             <p>
-                Variables in native queries let you dynamically replace values in your
-                queries using filter widgets or through the URL.
+                {t`Variables in native queries let you dynamically replace values in your queries using filter widgets or through the URL.`}
             </p>
 
-            <h4 className="pt2">Variables</h4>
+            <h4 className="pt2">{t`Variables`}</h4>
             <p>
-                <Code>{"{{variable_name}}"}</Code> creates a variable in this SQL template called "variable_name".
-                Variables can be given types in the side panel, which changes their behavior. All
-                variable types other than "Field Filter" will automatically cause a filter widget to be placed on this
-                question; with Field Filters, this is optional. When this filter widget is filled in, that value replaces the variable in the SQL
-                template.
+                {jt`${<Code>{"{{variable_name}}"}</Code>} creates a variable in this SQL template called "variable_name". Variables can be given types in the side panel, which changes their behavior. All variable types other than "Field Filter" will automatically cause a filter widget to be placed on this question; with Field Filters, this is optional. When this filter widget is filled in, that value replaces the variable in the SQL template.`}
             </p>
             <TagExample datasetQuery={EXAMPLES.variable} setDatasetQuery={setQueryWithSampleDatasetId} />
 
-            <h4 className="pt2">Field Filters</h4>
+            <h4 className="pt2">{t`Field Filters`}</h4>
             <p>
-                Giving a variable the "Field Filter" type allows you to link SQL cards to dashboard
-                filter widgets or use more types of filter widgets on your SQL question. A Field Filter variable
-                inserts SQL similar to that generated by the GUI query builder when adding filters on existing columns.
+                {t`Giving a variable the "Field Filter" type allows you to link SQL cards to dashboard filter widgets or use more types of filter widgets on your SQL question. A Field Filter variable inserts SQL similar to that generated by the GUI query builder when adding filters on existing columns.`}
             </p>
             <p>
-                When adding a Field Filter variable, you'll need to map it to a specific field. You can then choose to display
-                a filter widget on your question, but even if you don't, you can now map your Field Filter variable to a dashboard filter
-                when adding this question to a dashboard. Field Filters should be used inside of a "WHERE" clause.
+                {t`When adding a Field Filter variable, you'll need to map it to a specific field. You can then choose to display a filter widget on your question, but even if you don't, you can now map your Field Filter variable to a dashboard filter when adding this question to a dashboard. Field Filters should be used inside of a "WHERE" clause.`}
             </p>
             <TagExample datasetQuery={EXAMPLES.dimension} />
 
-            <h4 className="pt2">Optional Clauses</h4>
+            <h4 className="pt2">{t`Optional Clauses`}</h4>
             <p>
-                <Code>{"[[brackets around a {{variable}}]]"}</Code> create an optional clause in the
-                template. If "variable" is set, then the entire clause is placed into the template.
-                If not, then the entire clause is ignored.
+                {jt`brackets around a ${<Code>{"[[{{variable}}]]"}</Code>} create an optional clause in the template. If "variable" is set, then the entire clause is placed into the template. If not, then the entire clause is ignored.`}
             </p>
             <TagExample datasetQuery={EXAMPLES.optional} setDatasetQuery={setQueryWithSampleDatasetId} />
 
             <p>
-                To use multiple optional clauses you can include at least one non-optional WHERE clause
-                followed by optional clauses starting with "AND".
+                {t`To use multiple optional clauses you can include at least one non-optional WHERE clause followed by optional clauses starting with "AND".`}
             </p>
             <TagExample datasetQuery={EXAMPLES.multipleOptional} setDatasetQuery={setQueryWithSampleDatasetId} />
 
             <p className="pt2 link">
-                <a href="http://www.metabase.com/docs/latest/users-guide/start" target="_blank" data-metabase-event="QueryBuilder;Template Tag Documentation Click">Read the full documentation</a>
+                <a href="http://www.metabase.com/docs/latest/users-guide/start" target="_blank" data-metabase-event="QueryBuilder;Template Tag Documentation Click">{t`Read the full documentation`}</a>
             </p>
         </div>
     )
diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
index 21ffde903a4af6e63e00e7cd8d77ce05ea640b80..dcf16ebc92ea7eda9506c4f9a3c60683bbd934c9 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
@@ -1,7 +1,7 @@
 /* @flow weak */
 
 import React, { Component } from "react";
-
+import { t } from 'c-3po';
 import Toggle from "metabase/components/Toggle.jsx";
 import Input from "metabase/components/Input.jsx";
 import Select, { Option } from "metabase/components/Select.jsx";
@@ -100,7 +100,7 @@ export default class TagEditorParam extends Component {
                 <h3 className="pb2">{tag.name}</h3>
 
                 <div className="pb1">
-                    <h5 className="pb1 text-normal">Filter label</h5>
+                    <h5 className="pb1 text-normal">{t`Filter label`}</h5>
                     <Input
                         type="text"
                         value={tag.display_name}
@@ -110,25 +110,25 @@ export default class TagEditorParam extends Component {
                 </div>
 
                 <div className="pb1">
-                    <h5 className="pb1 text-normal">Variable type</h5>
+                    <h5 className="pb1 text-normal">{t`Variable type`}</h5>
                     <Select
                         className="border-med bg-white block"
                         value={tag.type}
                         onChange={(e) => this.setType(e.target.value)}
                         isInitiallyOpen={!tag.type}
-                        placeholder="Select…"
+                        placeholder={t`Select…`}
                         height={300}
                     >
-                        <Option value="text">Text</Option>
-                        <Option value="number">Number</Option>
-                        <Option value="date">Date</Option>
-                        <Option value="dimension">Field Filter</Option>
+                        <Option value="text">{t`Text`}</Option>
+                        <Option value="number">{t`Number`}</Option>
+                        <Option value="date">{t`Date`}</Option>
+                        <Option value="dimension">{t`Field Filter`}</Option>
                     </Select>
                 </div>
 
                 { tag.type === "dimension" &&
                     <div className="pb1">
-                        <h5 className="pb1 text-normal">Field to map to</h5>
+                        <h5 className="pb1 text-normal">{t`Field to map to`}</h5>
                         <Select
                             className="border-med bg-white block"
                             value={Array.isArray(tag.dimension) ? tag.dimension[1] : null}
@@ -136,7 +136,7 @@ export default class TagEditorParam extends Component {
                             searchProp="name"
                             searchCaseInsensitive
                             isInitiallyOpen={!tag.dimension}
-                            placeholder="Select…"
+                            placeholder={t`Select…`}
                             rowHeight={60}
                             width={280}
                         >
@@ -155,13 +155,13 @@ export default class TagEditorParam extends Component {
 
                 { widgetOptions && widgetOptions.length > 0 &&
                     <div className="pb1">
-                        <h5 className="pb1 text-normal">Filter widget type</h5>
+                        <h5 className="pb1 text-normal">{t`Filter widget type`}</h5>
                         <Select
                             className="border-med bg-white block"
                             value={tag.widget_type}
                             onChange={(e) => this.setParameterAttribute("widget_type", e.target.value)}
                             isInitiallyOpen={!tag.widget_type}
-                            placeholder="Select…"
+                            placeholder={t`Select…`}
                         >
                             {[{ name: "None", type: undefined }].concat(widgetOptions).map(widgetOption =>
                                 <Option key={widgetOption.type} value={widgetOption.type}>
@@ -174,14 +174,14 @@ export default class TagEditorParam extends Component {
 
                 { tag.type !== "dimension" &&
                     <div className="flex align-center pb1">
-                        <h5 className="text-normal mr1">Required?</h5>
+                        <h5 className="text-normal mr1">{t`Required?`}</h5>
                         <Toggle value={tag.required} onChange={(value) => this.setRequired(value)} />
                     </div>
                 }
 
                 { ((tag.type !== "dimension" && tag.required) || (tag.type === "dimension" || tag.widget_type)) &&
                     <div className="pb1">
-                        <h5 className="pb1 text-normal">Default filter widget value</h5>
+                        <h5 className="pb1 text-normal">{t`Default filter widget value`}</h5>
                         <ParameterValueWidget
                             parameter={{
                                 type: tag.widget_type || (tag.type === "date" ? "date/single" : null)
diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
index 666862a904d47617f9158438de63524f954237c0..9e4e378ab2a8ef81b60358c3322ecf8b5dcf4205 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
@@ -6,7 +6,7 @@ import Icon from "metabase/components/Icon.jsx";
 import TagEditorParam from "./TagEditorParam.jsx";
 import TagEditorHelp from "./TagEditorHelp.jsx";
 import MetabaseAnalytics from "metabase/lib/analytics";
-
+import { t } from 'c-3po';
 import cx from "classnames";
 
 import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
@@ -65,7 +65,7 @@ export default class TagEditorSidebar extends Component {
             <div className="DataReference-container p3 full-height scroll-y">
                 <div className="DataReference-header flex align-center mb2">
                     <h2 className="text-default">
-                        Variables
+                        {t`Variables`}
                     </h2>
                     <a className="flex-align-right text-default text-brand-hover no-decoration" onClick={() => this.props.onClose()}>
                         <Icon name="close" size={18} />
@@ -73,8 +73,8 @@ export default class TagEditorSidebar extends Component {
                 </div>
                 <div className="DataReference-content">
                     <div className="Button-group Button-group--brand text-uppercase mb2">
-                        <a className={cx("Button Button--small", { "Button--active": section === "settings" , "disabled": tags.length === 0 })} onClick={() => this.setSection("settings")}>Settings</a>
-                        <a className={cx("Button Button--small", { "Button--active": section === "help" })} onClick={() => this.setSection("help")}>Help</a>
+                        <a className={cx("Button Button--small", { "Button--active": section === "settings" , "disabled": tags.length === 0 })} onClick={() => this.setSection("settings")}>{t`Settings`}</a>
+                        <a className={cx("Button Button--small", { "Button--active": section === "help" })} onClick={() => this.setSection("help")}>{t`Help`}</a>
                     </div>
                     { section === "settings" ?
                         <SettingsPane tags={tags} onUpdate={this.props.updateTemplateTag} databaseFields={this.props.databaseFields}/>
diff --git a/frontend/src/metabase/query_builder/containers/ArchiveQuestionModal.jsx b/frontend/src/metabase/query_builder/containers/ArchiveQuestionModal.jsx
index 4d523580d3bdacd110a5560423122123ee80119d..f3cbfd8effd9a4036f9f31ce9cf8e5b2d8aa6212 100644
--- a/frontend/src/metabase/query_builder/containers/ArchiveQuestionModal.jsx
+++ b/frontend/src/metabase/query_builder/containers/ArchiveQuestionModal.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from "react"
 import { connect } from "react-redux"
-
+import { t } from 'c-3po';
 import Button from "metabase/components/Button"
 import Icon from "metabase/components/Icon"
 import ModalWithTrigger from "metabase/components/ModalWithTrigger"
@@ -37,19 +37,19 @@ class ArchiveQuestionModal extends Component {
             <ModalWithTrigger
                 ref="archiveModal"
                 triggerElement={
-                    <Tooltip key="archive" tooltip="Archive">
+                    <Tooltip key="archive" tooltip={t`Archive`}>
                         <span className="text-brand-hover">
                             <Icon name="archive" size={16} />
                         </span>
                     </Tooltip>
                 }
-                title="Archive this question?"
+                title={t`Archive this question?`}
                 footer={[
-                    <Button key='cancel' onClick={this.onClose}>Cancel</Button>,
-                    <Button key='archive' warning onClick={this.onArchive}>Archive</Button>
+                    <Button key='cancel' onClick={this.onClose}>{t`Cancel`}</Button>,
+                    <Button key='archive' warning onClick={this.onArchive}>{t`Archive`}</Button>
                 ]}
             >
-                <div className="px4 pb4">This question will be removed from any dashboards or pulses using it.</div>
+                <div className="px4 pb4">{t`This question will be removed from any dashboards or pulses using it.`}</div>
             </ModalWithTrigger>
         )
     }
diff --git a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
index 23a04ece6dbc4179302ae4322fb864bc7007d2e7..a76ae538e5f31b6ebff7c3223d73c72392d05ef0 100644
--- a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
+++ b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
@@ -3,7 +3,7 @@
 import React, { Component } from "react";
 import ReactDOM from "react-dom";
 import { connect } from "react-redux";
-
+import { t } from 'c-3po';
 import cx from "classnames";
 import _ from "underscore";
 
@@ -133,7 +133,7 @@ const mapDispatchToProps = {
 
 
 @connect(mapStateToProps, mapDispatchToProps)
-@title(({ card }) => (card && card.name) || "Question")
+@title(({ card }) => (card && card.name) || t`Question`)
 export default class QueryBuilder extends Component {
     forceUpdateDebounced: () => void;
 
diff --git a/frontend/src/metabase/questions/components/ActionHeader.jsx b/frontend/src/metabase/questions/components/ActionHeader.jsx
index ad1c6ea6e22d1860b20fda498867c40a959da470..7f550909ac44d2cb734c62c3e772c0af00d85f11 100644
--- a/frontend/src/metabase/questions/components/ActionHeader.jsx
+++ b/frontend/src/metabase/questions/components/ActionHeader.jsx
@@ -2,7 +2,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 import S from "./ActionHeader.css";
-
+import { t } from 'c-3po';
 import StackedCheckBox from "metabase/components/StackedCheckBox.jsx";
 import Icon from "metabase/components/Icon.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
@@ -13,7 +13,7 @@ import LabelPopover from "../containers/LabelPopover.jsx";
 
 const ActionHeader = ({ visibleCount, selectedCount, allAreSelected, sectionIsArchive, setAllSelected, setArchived, labels }) =>
     <div className={S.actionHeader}>
-        <Tooltip tooltip={"Select all " + visibleCount} isEnabled={!allAreSelected}>
+        <Tooltip tooltip={t`Select all ${visibleCount}`} isEnabled={!allAreSelected}>
             <span className="ml1">
                 <StackedCheckBox
                     checked={allAreSelected}
@@ -24,7 +24,7 @@ const ActionHeader = ({ visibleCount, selectedCount, allAreSelected, sectionIsAr
             </span>
         </Tooltip>
         <span className={S.selectedCount}>
-            {selectedCount} selected
+            {t`${selectedCount} selected`}
         </span>
         <span className="flex align-center flex-align-right">
             { !sectionIsArchive && labels.length > 0 ?
@@ -32,7 +32,7 @@ const ActionHeader = ({ visibleCount, selectedCount, allAreSelected, sectionIsAr
                     triggerElement={
                         <span className={S.actionButton}>
                             <Icon name="label" />
-                            Labels
+                            {t`Labels`}
                             <Icon name="chevrondown" size={12} />
                         </span>
                     }
@@ -45,7 +45,7 @@ const ActionHeader = ({ visibleCount, selectedCount, allAreSelected, sectionIsAr
                 triggerElement={
                     <span className={S.actionButton} >
                         <Icon name="move" className="mr1" />
-                        Move
+                        {t`Move`}
                     </span>
                 }
             >
@@ -53,7 +53,7 @@ const ActionHeader = ({ visibleCount, selectedCount, allAreSelected, sectionIsAr
             </ModalWithTrigger>
             <span className={S.actionButton} onClick={() => setArchived(undefined, !sectionIsArchive, true)}>
                 <Icon name={ sectionIsArchive ? "unarchive" : "archive" }  className="mr1" />
-                { sectionIsArchive ? "Unarchive" : "Archive" }
+                { sectionIsArchive ? t`Unarchive` : t`Archive` }
             </span>
         </span>
     </div>
diff --git a/frontend/src/metabase/questions/components/CollectionButtons.jsx b/frontend/src/metabase/questions/components/CollectionButtons.jsx
index 28bd7451991b48acbdf930d4d1a3f086f48245a9..a445baf6e600d1844e84e0c24b89554fe79ea637 100644
--- a/frontend/src/metabase/questions/components/CollectionButtons.jsx
+++ b/frontend/src/metabase/questions/components/CollectionButtons.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import { Link } from "react-router";
 import cx from "classnames";
-
+import { t } from 'c-3po';
 import Icon from "metabase/components/Icon";
 import ArchiveCollectionWidget from "../containers/ArchiveCollectionWidget";
 
@@ -47,7 +47,7 @@ class CollectionButton extends Component {
                     { isAdmin &&
                         <div className="absolute top right mt2 mr2 hover-child">
                             <Link to={"/collections/permissions?collectionId=" + id} className="mr1">
-                                <Icon name="lockoutline" tooltip="Set collection permissions" />
+                                <Icon name="lockoutline" tooltip={t`Set collection permissions`} />
                             </Link>
                             <ArchiveCollectionWidget collectionId={id} />
                         </div>
@@ -90,7 +90,7 @@ const NewCollectionButton = ({ push }) =>
                 />
             </div>
         </div>
-        <h3>New collection</h3>
+        <h3>{t`New collection`}</h3>
     </div>
 
 export default CollectionButtons;
diff --git a/frontend/src/metabase/questions/components/ExpandingSearchField.jsx b/frontend/src/metabase/questions/components/ExpandingSearchField.jsx
index a2e7f4485eb216c3421f5d0f31ecfd3ee0f95fb7..5ea409e4f3ef52365f8843142cbd2efba05eaa80 100644
--- a/frontend/src/metabase/questions/components/ExpandingSearchField.jsx
+++ b/frontend/src/metabase/questions/components/ExpandingSearchField.jsx
@@ -3,7 +3,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
-
+import { t } from 'c-3po';
 import cx from "classnames";
 import { Motion, spring } from "react-motion";
 
@@ -88,7 +88,7 @@ export default class ExpandingSearchField extends Component {
                         <input
                             ref={(search) => this.searchInput = search}
                             className="input borderless text-bold"
-                            placeholder="Search for a question"
+                            placeholder={t`Search for a question`}
                             style={Object.assign({}, interpolatingStyle, { fontSize: '1em'})}
                             onFocus={() => this.setState({ active: true })}
                             onBlur={() => this.setState({ active: false })}
diff --git a/frontend/src/metabase/questions/components/Item.jsx b/frontend/src/metabase/questions/components/Item.jsx
index 28266b96e969c28aba247a1c0a91e4aaf7081fd7..4c5b88a31142b2f43ce3aad70f7f8b0c2af7f2f6 100644
--- a/frontend/src/metabase/questions/components/Item.jsx
+++ b/frontend/src/metabase/questions/components/Item.jsx
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
 import { Link } from "react-router";
 import cx from "classnames";
 import pure from "recompose/pure";
-
+import { t } from 'c-3po';
 import S from "./List.css";
 
 import Icon from "metabase/components/Icon.jsx";
@@ -72,7 +72,7 @@ const Item = ({
                     <ModalWithTrigger
                         full
                         triggerElement={
-                            <Tooltip tooltip="Move to a collection">
+                            <Tooltip tooltip={t`Move to a collection`}>
                                 <Icon
                                     className="text-light-blue cursor-pointer text-brand-hover transition-color mx2"
                                     name="move"
@@ -86,7 +86,7 @@ const Item = ({
                             initialCollectionId={collection && collection.id}
                         />
                     </ModalWithTrigger>
-                    <Tooltip tooltip={archived ? "Unarchive" : "Archive"}>
+                    <Tooltip tooltip={archived ? t`Unarchive` : t`Archive`}>
                         <Icon
                             className="text-light-blue cursor-pointer text-brand-hover transition-color"
                             name={ archived ? "unarchive" : "archive"}
@@ -129,7 +129,7 @@ const ItemBody = pure(({ entity, id, name, description, labels, favorite, collec
                 <CollectionBadge collection={collection} />
             }
             { favorite != null && setFavorited &&
-                <Tooltip tooltip={favorite ? "Unfavorite" : "Favorite"}>
+                <Tooltip tooltip={favorite ? t`Unfavorite` : t`Favorite`}>
                     <Icon
                         className={cx(
                             "flex cursor-pointer",
@@ -145,7 +145,7 @@ const ItemBody = pure(({ entity, id, name, description, labels, favorite, collec
             <Labels labels={labels} />
         </div>
         <div className={cx({ 'text-slate': description }, { 'text-light-blue': !description })}>
-            {description ? description : "No description yet"}
+            {description ? description : t`No description yet`}
         </div>
     </div>
 );
@@ -161,7 +161,7 @@ ItemBody.propTypes = {
 const ItemCreated = pure(({ created, by }) =>
     (created || by) ?
         <div className={S.itemSubtitle}>
-            {"Created" + (created ? ` ${created}` : ``) + (by ? ` by ${by}` : ``)}
+            {t`Created` + (created ? ` ${created}` : ``) + (by ? t` by ${by}` : ``)}
         </div>
     :
         null
diff --git a/frontend/src/metabase/questions/components/LabelIconPicker.jsx b/frontend/src/metabase/questions/components/LabelIconPicker.jsx
index fe58e7058ac4253ac63d81465fc181aa7cecd813..67494cbc8ae454303d6574c5d312dca2cd4660a7 100644
--- a/frontend/src/metabase/questions/components/LabelIconPicker.jsx
+++ b/frontend/src/metabase/questions/components/LabelIconPicker.jsx
@@ -1,7 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import S from "./LabelIconPicker.css";
 
 import Icon from "metabase/components/Icon.jsx";
@@ -40,7 +40,7 @@ function pushIcons(icons) {
 
 // Colors
 const ALL_COLORS = [].concat(...[colors.saturated, colors.normal, colors.desaturated].map(o => Object.values(o)));
-pushHeader("Colors");
+pushHeader(t`Colors`);
 pushIcons(ALL_COLORS);
 
 // Emoji
diff --git a/frontend/src/metabase/questions/components/LabelPicker.jsx b/frontend/src/metabase/questions/components/LabelPicker.jsx
index f835089581eaac5ac9a132f28971fca534f79f68..c71f84f34bd0bc4f76e916d795e274b405e97733 100644
--- a/frontend/src/metabase/questions/components/LabelPicker.jsx
+++ b/frontend/src/metabase/questions/components/LabelPicker.jsx
@@ -2,7 +2,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
-
+import { t } from 'c-3po';
 import S from "./LabelPicker.css";
 
 import LabelIcon from "metabase/components/LabelIcon.jsx";
@@ -15,9 +15,9 @@ const LabelPicker = ({ labels, count, item, setLabeled }) =>
     <div className={S.picker}>
         <div className={S.heading}>
         { count > 1 ?
-            "Apply labels to " + count + " questions"
+            t`Apply labels to ${count} questions`
         :
-            "Label as"
+            t`Label as`
         }
         </div>
         <ul className={S.options}>
@@ -49,8 +49,8 @@ const LabelPicker = ({ labels, count, item, setLabeled }) =>
             }) }
         </ul>
         <div className={S.footer}>
-            <Link className="link" to="/labels">Add and edit labels</Link>
-            <Tooltip tooltip="In an upcoming release, Labels will be removed in favor of Collections.">
+            <Link className="link" to="/labels">{t`Add and edit labels`}</Link>
+            <Tooltip tooltip={t`In an upcoming release, Labels will be removed in favor of Collections.`}>
                 <Icon name="warning2" className="text-error float-right" />
             </Tooltip>
         </div>
diff --git a/frontend/src/metabase/questions/containers/AddToDashboard.jsx b/frontend/src/metabase/questions/containers/AddToDashboard.jsx
index 5472e7188002a117816f11b9ecd01594f79da69f..7e480e79e447168d22e2c1b75b6d89025d1a0172 100644
--- a/frontend/src/metabase/questions/containers/AddToDashboard.jsx
+++ b/frontend/src/metabase/questions/containers/AddToDashboard.jsx
@@ -1,5 +1,5 @@
 import React, { Component } from "react";
-
+import { t } from 'c-3po';
 import Button from "metabase/components/Button.jsx";
 import ModalContent from "metabase/components/ModalContent.jsx";
 import Icon from "metabase/components/Icon.jsx";
@@ -65,7 +65,7 @@ export default class AddToDashboard extends Component {
                                     <li
                                         className="text-brand-hover flex align-center border-bottom cursor-pointer py1 md-py2"
                                         onClick={() => this.setState({
-                                            collection: { name: "Everything else" },
+                                            collection: { name: t`Everything else` },
                                             query: { collection: "" }
                                         })}
                                     >
@@ -93,7 +93,7 @@ export default class AddToDashboard extends Component {
         const { query, collection } = this.state;
         return (
             <ModalContent
-                title="Pick a question to add"
+                title={t`Pick a question to add`}
                 className="px4 mb4 scroll-y"
                 onClose={() => this.props.onClose()}
             >
@@ -117,10 +117,10 @@ export default class AddToDashboard extends Component {
                         <div className="ml-auto flex align-center">
                             <h5>Sort by</h5>
                             <Button borderless>
-                                Last modified
+                                {t`Last modified`}
                             </Button>
                             <Button borderless>
-                                Alphabetical order
+                                {t`Alphabetical order`}
                             </Button>
                         </div>
                     }
diff --git a/frontend/src/metabase/questions/containers/ArchiveCollectionWidget.jsx b/frontend/src/metabase/questions/containers/ArchiveCollectionWidget.jsx
index c988729ce759c81ccecbdc23e97fc7ee32af44ca..96c59f30f62c9cf1b626829996e07d338ba7982c 100644
--- a/frontend/src/metabase/questions/containers/ArchiveCollectionWidget.jsx
+++ b/frontend/src/metabase/questions/containers/ArchiveCollectionWidget.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import { connect } from "react-redux";
-
+import { t } from 'c-3po';
 import ModalWithTrigger from "metabase/components/ModalWithTrigger";
 import Button from "metabase/components/Button";
 import Icon from "metabase/components/Icon";
@@ -42,17 +42,17 @@ export default class ArchiveCollectionWidget extends Component {
                 {...this.props}
                 ref="modal"
                 triggerElement={
-                    <Tooltip tooltip="Archive collection">
+                    <Tooltip tooltip={t`Archive collection`}>
                         <Icon size={18} name="archive" />
                     </Tooltip>
                 }
-                title="Archive this collection?"
+                title={t`Archive this collection?`}
                 footer={[
-                    <Button onClick={this._onClose}>Cancel</Button>,
-                    <Button warning onClick={this._onArchive}>Archive</Button>
+                    <Button onClick={this._onClose}>{t`Cancel`}</Button>,
+                    <Button warning onClick={this._onArchive}>{t`Archive`}</Button>
                 ]}
             >
-                <div className="px4 pb4">The saved questions in this collection will also be archived.</div>
+                <div className="px4 pb4">{t`The saved questions in this collection will also be archived.`}</div>
             </ModalWithTrigger>
         );
     }
diff --git a/frontend/src/metabase/questions/containers/CollectionEditorForm.jsx b/frontend/src/metabase/questions/containers/CollectionEditorForm.jsx
index 73b18e3d00bba08dbb75e43004f0edc8436fdb57..2324348c7f732e2ff2e7a710ec27aa7c90716cd2 100644
--- a/frontend/src/metabase/questions/containers/CollectionEditorForm.jsx
+++ b/frontend/src/metabase/questions/containers/CollectionEditorForm.jsx
@@ -1,5 +1,5 @@
 import React, { Component } from "react";
-
+import { t } from 'c-3po';
 import Button from "metabase/components/Button";
 import ColorPicker from "metabase/components/ColorPicker";
 import FormField from "metabase/components/FormField";
@@ -16,12 +16,12 @@ const formConfig = {
     validate: (values) => {
         const errors = {};
         if (!values.name) {
-            errors.name = "Name is required";
+            errors.name = t`Name is required`;
         } else if (values.name.length > 100) {
-            errors.name = "Name must be 100 characters or less";
+            errors.name = t`Name must be 100 characters or less`;
         }
         if (!values.color) {
-            errors.color = "Color is required";
+            errors.color = t`Color is required`;
         }
         return errors;
     },
@@ -34,10 +34,10 @@ const formConfig = {
 }
 
 export const getFormTitle = ({ id, name }) =>
-    id.value ? name.value : "New collection"
+    id.value ? name.value : t`New collection`
 
 export const getActionText = ({ id }) =>
-    id.value ? "Update": "Create"
+    id.value ? t`Update`: t`Create`
 
 
 export const CollectionEditorFormActions = ({ handleSubmit, invalid, onClose, fields}) =>
@@ -70,28 +70,28 @@ export class CollectionEditorForm extends Component {
             >
                 <div className="NewForm ml-auto mr-auto mt4 pt2" style={{ width: 540 }}>
                     <FormField
-                        displayName="Name"
+                        displayName={t`Name`}
                         {...fields.name}
                     >
                         <Input
                             className="Form-input full"
-                            placeholder="My new fantastic collection"
+                            placeholder={t`My new fantastic collection`}
                             autoFocus
                             {...fields.name}
                         />
                     </FormField>
                     <FormField
-                        displayName="Description"
+                        displayName={t`Description`}
                         {...fields.description}
                     >
                         <textarea
                             className="Form-input full"
-                            placeholder="It's optional but oh, so helpful"
+                            placeholder={t`It's optional but oh, so helpful`}
                             {...fields.description}
                         />
                     </FormField>
                     <FormField
-                        displayName="Color"
+                        displayName={t`Color`}
                         {...fields.color}
                     >
                         <ColorPicker {...fields.color} />
diff --git a/frontend/src/metabase/questions/containers/CollectionPage.jsx b/frontend/src/metabase/questions/containers/CollectionPage.jsx
index a90a2e56b37da7546510f01931d46f172c5e020d..7620c7c10bde4ce252da3e1bcbb6c3af985dbebc 100644
--- a/frontend/src/metabase/questions/containers/CollectionPage.jsx
+++ b/frontend/src/metabase/questions/containers/CollectionPage.jsx
@@ -2,7 +2,7 @@ import React, { Component } from "react";
 import { connect } from "react-redux";
 import { push, replace, goBack } from "react-router-redux";
 import title from "metabase/hoc/Title";
-
+import { t } from 'c-3po';
 import Icon from "metabase/components/Icon";
 import HeaderWithBack from "metabase/components/HeaderWithBack";
 
@@ -50,14 +50,14 @@ export default class CollectionPage extends Component {
                     <div className="ml-auto">
                         <CollectionActions>
                             { canEdit && <ArchiveCollectionWidget collectionId={this.props.collection.id} onArchived={this.props.goToQuestions}/> }
-                            { canEdit && <Icon size={18} name="pencil" tooltip="Edit collection" onClick={() => this.props.editCollection(this.props.collection.id)} /> }
-                            { canEdit && <Icon size={18} name="lock" tooltip="Set permissions" onClick={() => this.props.editPermissions(this.props.collection.id)} /> }
+                            { canEdit && <Icon size={18} name="pencil" tooltip={t`Edit collection`} onClick={() => this.props.editCollection(this.props.collection.id)} /> }
+                            { canEdit && <Icon size={18} name="lock" tooltip={t`Set permissions`} onClick={() => this.props.editPermissions(this.props.collection.id)} /> }
                         </CollectionActions>
                     </div>
                 </div>
                 <div className="mt4">
                     <EntityList
-                        defaultEmptyState="No questions have been added to this collection yet."
+                        defaultEmptyState={t`No questions have been added to this collection yet.`}
                         entityType="cards"
                         entityQuery={{ f: "all", collection: params.collectionSlug, ...location.query }}
                         // use replace when changing sections so back button still takes you back to collections page
diff --git a/frontend/src/metabase/questions/containers/EditLabels.jsx b/frontend/src/metabase/questions/containers/EditLabels.jsx
index 065e38a06d2a68848f67b4a4c361d48faf58d395..fc56745bfce92748d0ac68f363daea0e62e4cc39 100644
--- a/frontend/src/metabase/questions/containers/EditLabels.jsx
+++ b/frontend/src/metabase/questions/containers/EditLabels.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-
+import { t } from 'c-3po';
 import S from "./EditLabels.css";
 
 import Confirm from "metabase/components/Confirm.jsx";
@@ -56,13 +56,13 @@ export default class EditLabels extends Component {
         return (
             <div className={S.editor} style={style}>
                 <div className="wrapper wrapper--trim">
-                    <div className={S.header}>Add and edit labels</div>
+                    <div className={S.header}>{t`Add and edit labels`}</div>
                     <div className="bordered border-error rounded p2 mb2">
-                        <h3 className="text-error mb1">Heads up!</h3>
-                        <div>In an upcoming release, Labels will be removed in favor of Collections.</div>
+                        <h3 className="text-error mb1">{t`Heads up!`}</h3>
+                        <div>{t`In an upcoming release, Labels will be removed in favor of Collections.`}</div>
                     </div>
                 </div>
-                <LabelEditorForm onSubmit={saveLabel} initialValues={{ icon: colors.normal.blue, name: "" }} submitButtonText={"Create Label"} className="wrapper wrapper--trim"/>
+                <LabelEditorForm onSubmit={saveLabel} initialValues={{ icon: colors.normal.blue, name: "" }} submitButtonText={t`Create Label`} className="wrapper wrapper--trim"/>
                 <LoadingAndErrorWrapper loading={labelsLoading} error={labelsError} noBackground noWrapper>
                 { () => labels.length > 0 ?
                     <div className="wrapper wrapper--trim">
@@ -70,15 +70,15 @@ export default class EditLabels extends Component {
                         { labels.map(label =>
                             editingLabelId === label.id ?
                                 <li key={label.id} className={S.labelEditing}>
-                                    <LabelEditorForm formKey={String(label.id)} className="flex-full" onSubmit={saveLabel} initialValues={label} submitButtonText={"Update Label"}/>
-                                    <a className={" text-grey-1 text-grey-4-hover ml2"} onClick={() => editLabel(null)}>Cancel</a>
+                                    <LabelEditorForm formKey={String(label.id)} className="flex-full" onSubmit={saveLabel} initialValues={label} submitButtonText={t`Update Label`}/>
+                                    <a className={" text-grey-1 text-grey-4-hover ml2"} onClick={() => editLabel(null)}>{t`Cancel`}</a>
                                 </li>
                             :
                                 <li key={label.id} className={S.label}>
                                     <LabelIcon icon={label.icon} size={28} />
                                     <span className={S.name}>{label.name}</span>
-                                    <a className={S.edit} onClick={() => editLabel(label.id)}>Edit</a>
-                                    <Confirm title={`Delete label "${label.name}"`} action={() => deleteLabel(label.id)}>
+                                    <a className={S.edit} onClick={() => editLabel(label.id)}>{t`Edit`}</a>
+                                    <Confirm title={t`Delete label "${label.name}"`} action={() => deleteLabel(label.id)}>
                                         <Icon className={S.delete + " text-grey-1 text-grey-4-hover"} name="close" size={14} />
                                     </Confirm>
                                 </li>
@@ -87,7 +87,7 @@ export default class EditLabels extends Component {
                     </div>
                 :
                     <div className="full-height full flex-full flex align-center justify-center">
-                        <EmptyState message="Create labels to group and manage questions." icon="label" />
+                        <EmptyState message={t`Create labels to group and manage questions.`} icon="label" />
                     </div>
                 }
                 </LoadingAndErrorWrapper>
diff --git a/frontend/src/metabase/questions/containers/EntityList.jsx b/frontend/src/metabase/questions/containers/EntityList.jsx
index 3ab7f641146b1201f483b335c4a319494ffd3c1a..f42ce4b667bebe67fb1ad5a4b1039b9cbfa4c229 100644
--- a/frontend/src/metabase/questions/containers/EntityList.jsx
+++ b/frontend/src/metabase/questions/containers/EntityList.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import { connect } from "react-redux";
-
+import { t } from 'c-3po';
 import EmptyState from "metabase/components/EmptyState";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
 import ListFilterWidget from "metabase/components/ListFilterWidget"
@@ -15,7 +15,6 @@ import SearchHeader from "metabase/components/SearchHeader";
 import ActionHeader from "../components/ActionHeader";
 
 import _ from "underscore";
-import { t } from 'c-3po';
 
 import { loadEntities, setSearchText, setItemSelected, setAllSelected, setArchived } from "../questions";
 import { loadLabels } from "../labels";
diff --git a/frontend/src/metabase/questions/containers/LabelEditorForm.jsx b/frontend/src/metabase/questions/containers/LabelEditorForm.jsx
index b1954d1ab91a3ab528fc895323fa28aeaf398f58..9923d0189f9e8d4d1dc599399bf6838366bb9eb1 100644
--- a/frontend/src/metabase/questions/containers/LabelEditorForm.jsx
+++ b/frontend/src/metabase/questions/containers/LabelEditorForm.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import S from "./LabelEditorForm.css";
-
+import { t } from 'c-3po';
 import LabelIconPicker from "../components/LabelIconPicker.jsx";
 
 import { reduxForm } from "redux-form";
@@ -18,7 +18,7 @@ import cx from "classnames";
             errors.name = true;
         }
         if (!values.icon) {
-            errors.icon = "Icon is required";
+            errors.icon = t`Icon is required`;
         }
         return errors;
     }
@@ -43,7 +43,7 @@ export default class LabelEditorForm extends Component {
                     <LabelIconPicker {...icon} />
                     <div className="full">
                         <div className="flex">
-                          <input className={cx(S.nameInput, "input", { [S.invalid]: nameInvalid })} type="text" placeholder="Name" {...name}/>
+                          <input className={cx(S.nameInput, "input", { [S.invalid]: nameInvalid })} type="text" placeholder={t`Name`} {...name}/>
                           <button className={cx("Button", "ml1", { "disabled": invalid, "Button--primary": !invalid })} type="submit">{submitButtonText}</button>
                         </div>
                         { nameInvalid && errorMessage && <div className={S.errorMessage}>{errorMessage}</div> }
diff --git a/frontend/src/metabase/questions/containers/MoveToCollection.jsx b/frontend/src/metabase/questions/containers/MoveToCollection.jsx
index e078cbd065273929dbb1039314e5a04b548866c8..ca2043b20daea44f37ccedce513b9010205662c7 100644
--- a/frontend/src/metabase/questions/containers/MoveToCollection.jsx
+++ b/frontend/src/metabase/questions/containers/MoveToCollection.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import { connect } from "react-redux";
-
+import { t } from 'c-3po';
 import Button from "metabase/components/Button";
 import Icon from "metabase/components/Icon";
 import ModalContent from "metabase/components/ModalContent";
@@ -51,17 +51,17 @@ export default class MoveToCollection extends Component {
         const { currentCollection, error } = this.state;
         return (
             <ModalContent
-                title="Which collection should this be in?"
+                title={t`Which collection should this be in?`}
                 footer={
                     <div>
                         { error &&
                             <span className="text-error mr1">{error.data && error.data.message}</span>
                         }
                         <Button className="mr1" onClick={onClose}>
-                            Cancel
+                            {t`Cancel`}
                         </Button>
                         <Button primary disabled={currentCollection.id === undefined} onClick={() => this.onMove(currentCollection)}>
-                            Move
+                            {t`Move`}
                         </Button>
                     </div>
                 }
diff --git a/frontend/src/metabase/questions/containers/QuestionIndex.jsx b/frontend/src/metabase/questions/containers/QuestionIndex.jsx
index 71c90a73857886d5e73c55967d6e4bbeb5341b7d..21f396fdec7c1371103ae0ca78d08b5abf27f80a 100644
--- a/frontend/src/metabase/questions/containers/QuestionIndex.jsx
+++ b/frontend/src/metabase/questions/containers/QuestionIndex.jsx
@@ -2,7 +2,7 @@ import React, { Component } from "react";
 import { connect } from "react-redux";
 import { Link } from "react-router";
 import cx from "classnames";
-
+import { t } from 'c-3po';
 import Icon from "metabase/components/Icon";
 import Button from "metabase/components/Button";
 
@@ -26,26 +26,26 @@ export const CollectionEmptyState = () =>
     <div className="flex align-center p2 mt4 bordered border-med border-brand rounded bg-grey-0 text-brand">
         <Icon name="collection" size={32} className="mr2"/>
         <div className="flex-full">
-            <h3>Create collections for your saved questions</h3>
+            <h3>{t`Create collections for your saved questions`}</h3>
             <div className="mt1">
-                Collections help you organize your questions and allow you to decide who gets to see what.
+                {t`Collections help you organize your questions and allow you to decide who gets to see what.`}
                 {" "}
                 <a href="http://www.metabase.com/docs/latest/administration-guide/06-collections.html" target="_blank">
-                    Learn more
+                    {t`Learn more`}
                 </a>
             </div>
         </div>
         <Link to="/collections/create">
-            <Button primary>Create a collection</Button>
+            <Button primary>{t`Create a collection`}</Button>
         </Link>
     </div>;
 
 export const NoSavedQuestionsState = () =>
     <div className="flex-full flex align-center justify-center mb4">
         <EmptyState
-            message={<span>Explore your data, create charts or maps, and save what you find.</span>}
+            message={<span>${t`Explore your data, create charts or maps, and save what you find.`}</span>}
             image="/app/img/questions_illustration"
-            action="Ask a question"
+            action={t`Ask a question"`}
             link="/question"
         />
     </div>;
@@ -69,11 +69,11 @@ export const QuestionIndexHeader = ({questions, collections, isAdmin, onSearch})
             <CollectionActions>
                 { showSetPermissionsLink &&
                 <Link to="/collections/permissions">
-                    <Icon size={18} name="lock" tooltip="Set permissions for collections"/>
+                    <Icon size={18} name="lock" tooltip={t`Set permissions for collections`}/>
                 </Link>
                 }
                 <Link to="/questions/archive">
-                    <Icon size={20} name="viewArchive" tooltip="View the archive"/>
+                    <Icon size={20} name="viewArchive" tooltip={t`View the archive`}/>
                 </Link>
             </CollectionActions>
         </div>
diff --git a/frontend/src/metabase/questions/containers/SearchResults.jsx b/frontend/src/metabase/questions/containers/SearchResults.jsx
index 019e2a30d1fb1f462c9b3d4828ee4e18b64cd1b4..dd8068fbe968d57035ff4301d685b9783bd38bd7 100644
--- a/frontend/src/metabase/questions/containers/SearchResults.jsx
+++ b/frontend/src/metabase/questions/containers/SearchResults.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import { connect } from "react-redux";
-
+import { t } from 'c-3po';
 import HeaderWithBack from "metabase/components/HeaderWithBack";
 
 import ExpandingSearchField from "../components/ExpandingSearchField";
@@ -39,14 +39,14 @@ class SearchResults extends Component {
                 <div className="flex align-center mb3">
                     <HeaderWithBack name={totalCount != null ?
                         `${totalCount} ${inflect("result", totalCount)}` :
-                        "Search results"}
+                        t`Search results`}
                     />
                 </div>
                 <EntityList
                     entityType="cards"
                     entityQuery={this.props.location.query}
                     showSearchWidget={false}
-                    defaultEmptyState="No matching questions found"
+                    defaultEmptyState={t`No matching questions found`}
                 />
             </div>
           </div>
diff --git a/frontend/src/metabase/questions/questions.js b/frontend/src/metabase/questions/questions.js
index b09640bddec913bc42bef782203b8f23f0accb47..80b1377a57de0b88f4051bff5599eefbad1d3ba5 100644
--- a/frontend/src/metabase/questions/questions.js
+++ b/frontend/src/metabase/questions/questions.js
@@ -72,7 +72,7 @@ export const setFavorited = createThunkAction(SET_FAVORITED, (cardId, favorited)
 
 import React from "react";
 import { Link } from "react-router";
-
+import { t } from 'c-3po';
 function createUndo(type, actions, collection) {
     return {
         type: type,
@@ -80,9 +80,9 @@ function createUndo(type, actions, collection) {
         message: (undo) => // eslint-disable-line react/display-name
             <div className="flex flex-column">
                 <div>
-                    { undo.count + " " + inflect(null, undo.count, "question was", "questions were") + " " + type }
+                    { inflect(null, undo.count, t`${undo.count} question was ${type}`, t`${undo.count} questions were ${type}`) }
                     { undo.count === 1 && collection &&
-                        <span> to the <Link className="link" to={Urls.collection(collection)}>{collection.name}</Link> collection.</span>
+                        <span> {t`to the`} <Link className="link" to={Urls.collection(collection)}>{collection.name}</Link> {t`collection`}.</span>
                     }
                 </div>
             </div>,
diff --git a/frontend/src/metabase/questions/selectors.js b/frontend/src/metabase/questions/selectors.js
index 3c9aae81994841fce3c999aac3cfd150c6f4c23a..a9a0e0363c7ce876d0b1ca266da88d46b8faefa4 100644
--- a/frontend/src/metabase/questions/selectors.js
+++ b/frontend/src/metabase/questions/selectors.js
@@ -3,7 +3,7 @@ import { createSelector } from 'reselect';
 import moment from "moment";
 import { getIn } from "icepick";
 import _ from "underscore";
-
+import { t } from 'c-3po';
 import visualizations from "metabase/visualizations";
 import {caseInsensitiveSearch} from "metabase/lib/string"
 
@@ -132,11 +132,11 @@ export const getSectionIsArchive = createSelector(
 );
 
 const sections = [
-    { id: "all",       name: "All questions",   icon: "all" },
-    { id: "fav",       name: "Favorites",       icon: "star" },
-    { id: "recent",    name: "Recently viewed", icon: "recents" },
-    { id: "mine",      name: "Saved by me",     icon: "mine" },
-    { id: "popular",   name: "Most popular",    icon: "popular" }
+    { id: "all",       name: t`All questions`,   icon: "all" },
+    { id: "fav",       name: t`Favorites`,       icon: "star" },
+    { id: "recent",    name: t`Recently viewed`, icon: "recents" },
+    { id: "mine",      name: t`Saved by me`,     icon: "mine" },
+    { id: "popular",   name: t`Most popular`,    icon: "popular" }
 ];
 
 export const getSections    = (state, props) => sections;
@@ -194,7 +194,7 @@ export const getSectionName = createSelector(
             if (section) {
                 return section.name || "";
             } else if (sectionId === "archived") {
-                return "Archive";
+                return t`Archive`;
             }
         }
         return "";
diff --git a/frontend/src/metabase/reference/components/Detail.jsx b/frontend/src/metabase/reference/components/Detail.jsx
index ca98acc90f7fbe539474c23d480cd00635eb3e9c..1421f08eb9ca1371d023e972604774abda7c398f 100644
--- a/frontend/src/metabase/reference/components/Detail.jsx
+++ b/frontend/src/metabase/reference/components/Detail.jsx
@@ -3,7 +3,7 @@ import React from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
 import S from "./Detail.css";
-
+import { t } from 'c-3po';
 import cx from "classnames";
 import pure from "recompose/pure";
 
@@ -26,7 +26,7 @@ const Detail = ({ name, description, placeholder, subtitleClass, url, icon, isEd
                         // to allow for reinitializing on cancel (see GettingStartedGuide.jsx)
                         defaultValue={description}
                     /> :
-                    <span className={subtitleClass}>{description || placeholder || 'No description yet'}</span>
+                    <span className={subtitleClass}>{description || placeholder || t`No description yet`}</span>
                 }
                 { isEditing && field.error && field.touched &&
                     <span className="text-error">{field.error}</span>
diff --git a/frontend/src/metabase/reference/components/EditButton.jsx b/frontend/src/metabase/reference/components/EditButton.jsx
index 81d2513fef3b9ce4addac03620a50da0c6924aba..3d0200e6e99a031231a3272cd8c144dae1786803 100644
--- a/frontend/src/metabase/reference/components/EditButton.jsx
+++ b/frontend/src/metabase/reference/components/EditButton.jsx
@@ -2,7 +2,7 @@ import React from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
 import pure from "recompose/pure";
-
+import { t } from 'c-3po';
 import S from "./EditButton.css";
 
 import Icon from "metabase/components/Icon.jsx";
@@ -18,7 +18,7 @@ const EditButton = ({
     >
         <div className={S.editButtonBody}>
             <Icon name="pencil" size={16} />
-            <span className="ml1">Edit</span>
+            <span className="ml1">{t`Edit`}</span>
         </div>
     </button>
 
diff --git a/frontend/src/metabase/reference/components/EditHeader.jsx b/frontend/src/metabase/reference/components/EditHeader.jsx
index dbeb1ea6fc99614f0897592fbc9156051369fbfd..687654e4bbd5fc070c89aab04926ef5bc847543d 100644
--- a/frontend/src/metabase/reference/components/EditHeader.jsx
+++ b/frontend/src/metabase/reference/components/EditHeader.jsx
@@ -2,7 +2,7 @@ import React from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
 import pure from "recompose/pure";
-
+import { t } from 'c-3po';
 import S from "./EditHeader.css";
 
 import RevisionMessageModal from "metabase/reference/components/RevisionMessageModal.jsx";
@@ -17,7 +17,7 @@ const EditHeader = ({
 }) =>
     <div className={cx("EditHeader wrapper py1", S.editHeader)}>
         <div>
-            You are editing this page
+            {t`You are editing this page`}
         </div>
         <div className={S.editHeaderButtons}>
             <button
@@ -28,7 +28,7 @@ const EditHeader = ({
                     reinitializeForm();
                 }}
             >
-                Cancel
+                {t`Cancel`}
             </button>
 
             { hasRevisionHistory ?
@@ -42,7 +42,7 @@ const EditHeader = ({
                         type="button"
                         disabled={submitting}
                     >
-                        Save
+                        {t`Save`}
                     </button>
                 </RevisionMessageModal> :
                 <button
@@ -50,7 +50,7 @@ const EditHeader = ({
                     type="submit"
                     disabled={submitting}
                 >
-                    Save
+                    {t`Save`}
                 </button>
             }
         </div>
diff --git a/frontend/src/metabase/reference/components/EditableReferenceHeader.jsx b/frontend/src/metabase/reference/components/EditableReferenceHeader.jsx
index 0ea58d8248ae8af39e41f63b20462db1f1af4d70..e79917865d4bb20b1e03e06ea6da1f62a4022133 100644
--- a/frontend/src/metabase/reference/components/EditableReferenceHeader.jsx
+++ b/frontend/src/metabase/reference/components/EditableReferenceHeader.jsx
@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
 import { Link } from "react-router";
 import cx from "classnames";
 import pure from "recompose/pure";
-
+import { t } from 'c-3po';
 import S from "./ReferenceHeader.css";
 import L from "metabase/components/List.css";
 import E from "metabase/reference/components/EditButton.css";
@@ -89,7 +89,7 @@ const EditableReferenceHeader = ({
                                     data-metabase-event={`Data Reference;Entity -> QB click;${type}`}
                                 >
                                     <div className="flex align-center relative">
-                                        <span className="mr1 flex-no-shrink">See this {type}</span>
+                                        <span className="mr1 flex-no-shrink">{t`See this ${type}`}</span>
                                         <Icon name="chevronright" size={16} />
                                     </div>
                                 </Link>
@@ -104,7 +104,7 @@ const EditableReferenceHeader = ({
         { type === 'segment' && table &&
             <div className={S.subheader}>
                 <div className={cx(S.subheaderBody)}>
-                    A subset of <Link
+                    {t`A subset of`} <Link
                         className={S.subheaderLink}
                         to={`/reference/databases/${table.db_id}/tables/${table.id}`}
                     >
diff --git a/frontend/src/metabase/reference/components/Field.jsx b/frontend/src/metabase/reference/components/Field.jsx
index c5c020605f3b18cd830e4b1bcb0fc0377da4e6d0..7816c81b34884a8db027c9e1ae9d2e59ae223378 100644
--- a/frontend/src/metabase/reference/components/Field.jsx
+++ b/frontend/src/metabase/reference/components/Field.jsx
@@ -2,7 +2,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
-
+import { t } from 'c-3po';
 import * as MetabaseCore from "metabase/lib/core";
 import { isNumericBaseType } from "metabase/lib/schema_metadata";
 import { isa, isFK, TYPE } from "metabase/lib/types";
@@ -48,7 +48,7 @@ const Field = ({
                     { isEditing ?
                         <Select
                             triggerClasses={F.fieldSelect}
-                            placeholder="Select a field type"
+                            placeholder={t`Select a field type`}
                             value={
                                 MetabaseCore.field_special_types_map[formField.special_type.value] ||
                                 MetabaseCore.field_special_types_map[field.special_type]
@@ -57,8 +57,8 @@ const Field = ({
                                 MetabaseCore.field_special_types
                                     .concat({
                                         'id': null,
-                                        'name': 'No field type',
-                                        'section': 'Other'
+                                        'name': t`No field type`,
+                                        'section': t`Other`
                                     })
                                     .filter(type =>
                                         isNumericBaseType(field) || !isa(type && type.id, TYPE.UNIXTimestamp)
@@ -70,7 +70,7 @@ const Field = ({
                             { getIn(
                                     MetabaseCore.field_special_types_map,
                                     [field.special_type, 'name']
-                                ) || 'No field type'
+                                ) || t`No field type`
                             }
                         </span>
                     }
@@ -89,7 +89,7 @@ const Field = ({
                         (isFK(field.special_type) && formField.special_type.value === undefined)) &&
                         <Select
                             triggerClasses={F.fieldSelect}
-                            placeholder="Select a field type"
+                            placeholder={t`Select a field type`}
                             value={
                                 foreignKeys[formField.fk_target_field_id.value] ||
                                 foreignKeys[field.fk_target_field_id] ||
diff --git a/frontend/src/metabase/reference/components/FieldToGroupBy.jsx b/frontend/src/metabase/reference/components/FieldToGroupBy.jsx
index 87852ec59defb46f0a006b2f2f0d58ef2af136c4..4d9e06aa8e4c9f21536688897f1112b16fc24437 100644
--- a/frontend/src/metabase/reference/components/FieldToGroupBy.jsx
+++ b/frontend/src/metabase/reference/components/FieldToGroupBy.jsx
@@ -1,7 +1,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 import pure from "recompose/pure";
-
+import { t } from 'c-3po';
 import S from "./FieldToGroupBy.css";
 import Q from "metabase/components/QueryButton.css";
 
@@ -20,7 +20,7 @@ const FieldToGroupBy = ({
         <a className={Q.queryButton} onClick={onClick}>
             <span className={S.fieldToGroupByText}>
                 <span>
-                    {`${metric.name} by `}
+                    {`${metric.name} ` + t`by` + ` `}
                 </span>
                 <span className="ml1 text-brand">
                     {field.display_name}
diff --git a/frontend/src/metabase/reference/components/FieldTypeDetail.jsx b/frontend/src/metabase/reference/components/FieldTypeDetail.jsx
index c42ab2572e4fcd38901e1f44b86a80d73b4be7bd..7c36b62a2543bdb6a48dd2fcc7461e90e1fa5e70 100644
--- a/frontend/src/metabase/reference/components/FieldTypeDetail.jsx
+++ b/frontend/src/metabase/reference/components/FieldTypeDetail.jsx
@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
 import cx from "classnames";
 import { getIn } from "icepick";
 import pure from "recompose/pure";
-
+import { t } from 'c-3po';
 import * as MetabaseCore from "metabase/lib/core";
 import { isNumericBaseType } from "metabase/lib/schema_metadata";
 import { isFK } from "metabase/lib/types";
@@ -22,14 +22,14 @@ const FieldTypeDetail = ({
     <div className={cx(D.detail)}>
         <div className={D.detailBody}>
             <div className={D.detailTitle}>
-                <span className={D.detailName}>Field type</span>
+                <span className={D.detailName}>{t`Field type`}</span>
             </div>
             <div className={cx(D.detailSubtitle, { "mt1" : true })}>
                 <span>
                     { isEditing ?
                         <Select
                             triggerClasses="rounded bordered p1 inline-block"
-                            placeholder="Select a field type"
+                            placeholder={t`Select a field type`}
                             value={
                                 MetabaseCore.field_special_types_map[fieldTypeFormField.value] ||
                                 MetabaseCore.field_special_types_map[field.special_type]
@@ -38,8 +38,8 @@ const FieldTypeDetail = ({
                                 MetabaseCore.field_special_types
                                     .concat({
                                         'id': null,
-                                        'name': 'No field type',
-                                        'section': 'Other'
+                                        'name': t`No field type`,
+                                        'section': t`Other`
                                     })
                                     .filter(type => !isNumericBaseType(field) ?
                                         !(type.id && type.id.startsWith("timestamp_")) :
@@ -52,7 +52,7 @@ const FieldTypeDetail = ({
                             { getIn(
                                     MetabaseCore.field_special_types_map,
                                     [field.special_type, 'name']
-                                ) || 'No field type'
+                                ) || t`No field type`
                             }
                         </span>
                     }
@@ -63,11 +63,11 @@ const FieldTypeDetail = ({
                         (isFK(field.special_type) && fieldTypeFormField.value === undefined)) &&
                         <Select
                             triggerClasses="rounded bordered p1 inline-block"
-                            placeholder="Select a field type"
+                            placeholder={t`Select a field type`}
                             value={
                                 foreignKeys[foreignKeyFormField.value] ||
                                 foreignKeys[field.fk_target_field_id] ||
-                                {name: "Select a Foreign Key"}
+                                {name: t`Select a Foreign Key`}
                             }
                             options={Object.values(foreignKeys)}
                             onChange={(foreignKey) => foreignKeyFormField.onChange(foreignKey.id)}
diff --git a/frontend/src/metabase/reference/components/Formula.jsx b/frontend/src/metabase/reference/components/Formula.jsx
index fad946d8f147dc5f10be18c2c3c487a7aba25a4a..3373453fbe91b79b316725df28a98e83ab6ab8ce 100644
--- a/frontend/src/metabase/reference/components/Formula.jsx
+++ b/frontend/src/metabase/reference/components/Formula.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import cx from "classnames";
 import { connect } from "react-redux";
-
+import { t } from 'c-3po';
 import ReactCSSTransitionGroup from "react-addons-css-transition-group";
 
 import S from "./Formula.css";
@@ -43,7 +43,7 @@ export default class Formula extends Component {
             >
                 <div className={S.formulaHeader}>
                     <Icon name="beaker" size={24}/>
-                    <span className={S.formulaTitle}>View the {type} formula</span>
+                    <span className={S.formulaTitle}>{t`View the ${type} formula`}</span>
                 </div>
                 <ReactCSSTransitionGroup
                     transitionName="formulaDefinition"
diff --git a/frontend/src/metabase/reference/components/GuideDetail.jsx b/frontend/src/metabase/reference/components/GuideDetail.jsx
index d3cb5b285410b20aaa69deaf1bdac0ed337a51a5..cfc9a14618428c1203f3e5bbc7f6f745386dde3e 100644
--- a/frontend/src/metabase/reference/components/GuideDetail.jsx
+++ b/frontend/src/metabase/reference/components/GuideDetail.jsx
@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
 import { Link } from "react-router";
 import pure from "recompose/pure";
 import cx from "classnames";
-
+import { t } from 'c-3po';
 import Icon from "metabase/components/Icon"
 import * as Urls from "metabase/lib/urls";
 
@@ -52,7 +52,6 @@ const GuideDetail = ({
     const linkHoverClass = `${typeToLinkClass[type]}-hover`
     const bgClass = typeToBgClass[type]
     const hasLearnMore = type === 'metric' || type === 'segment' || type === 'table';
-    const interestingOrImportant = type === 'dashboard' ? 'important' : 'interesting';
 
     return <div className="relative mt2 pb3">
         <div className="flex align-center">
@@ -69,35 +68,35 @@ const GuideDetail = ({
         </div>
         <div className="mt2">
             <ContextHeading>
-                { `Why this ${type} is ${interestingOrImportant}` }
+                { type === 'dashboard' ? t`Why this ${type} is important` : t`Why this ${type} is interesting` }
             </ContextHeading>
 
             <ContextContent empty={!points_of_interest}>
-                {points_of_interest || `Nothing ${interestingOrImportant} yet`}
+                {points_of_interest || (type === 'dashboard' ? t`Nothing important yet` : t`Nothing interesting yet`)}
             </ContextContent>
 
             <div className="mt2">
                 <ContextHeading>
-                    {`Things to be aware of about this ${type}`}
+                    {t`Things to be aware of about this ${type}`}
                 </ContextHeading>
 
                 <ContextContent empty={!caveats}>
-                    {caveats || 'Nothing to be aware of yet'}
+                    {caveats || t`Nothing to be aware of yet`}
                 </ContextContent>
             </div>
 
             { has(exploreLinks) && [
                 <div className="mt2">
-                    <ContextHeading key="detailLabel">Explore this metric</ContextHeading>
+                    <ContextHeading key="detailLabel">{t`Explore this metric`}</ContextHeading>
                     <div key="detailLinks">
-                        <h4 className="inline-block mr2 link text-bold">View this metric</h4>
+                        <h4 className="inline-block mr2 link text-bold">{t`View this metric`}</h4>
                         { exploreLinks.map(link =>
                             <Link
                                 className="inline-block text-bold text-brand mr2 link"
                                 key={link.url}
                                 to={link.url}
                             >
-                                {`By ${link.name}`}
+                                {t`By ${link.name}`}
                             </Link>
                         )}
                     </div>
@@ -108,7 +107,7 @@ const GuideDetail = ({
                     className={cx('block mt3 no-decoration text-underline-hover text-bold', linkClass)}
                     to={learnMoreLink}
                 >
-                    Learn more
+                    {t`Learn more`}
                 </Link>
             }
         </div>
diff --git a/frontend/src/metabase/reference/components/GuideDetailEditor.jsx b/frontend/src/metabase/reference/components/GuideDetailEditor.jsx
index cd82f2e410b31dd39156855fb6714f02a20b4b88..405115cd5e87d4b568091ad70da75975655a1fbf 100644
--- a/frontend/src/metabase/reference/components/GuideDetailEditor.jsx
+++ b/frontend/src/metabase/reference/components/GuideDetailEditor.jsx
@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
 // FIXME: using pure seems to mess with redux form updates
 // import pure from "recompose/pure";
 import cx from "classnames";
-
+import { t } from 'c-3po';
 import S from "./GuideDetailEditor.css";
 
 import Select from "metabase/components/Select.jsx";
@@ -78,7 +78,7 @@ const GuideDetailEditor = ({
                                 );
                             }
                         }}
-                        placeholder={'Select...'}
+                        placeholder={t`Select...`}
                     /> :
                     <DataSelector
                         className={cx(selectClasses, 'inline-block', 'rounded', 'text-bold')}
@@ -133,7 +133,7 @@ const GuideDetailEditor = ({
                 }
             </div>
             <div className="ml-auto cursor-pointer text-grey-2">
-                <Tooltip tooltip="Remove item">
+                <Tooltip tooltip={t`Remove item`}>
                     <Icon
                         name="close"
                         width={16}
@@ -147,13 +147,13 @@ const GuideDetailEditor = ({
             <div className={cx('mb2', { 'disabled' : disabled })}>
                 <EditLabel>
                     { type === 'dashboard' ?
-                            `Why is this dashboard the most important?` :
-                            `What is useful or interesting about this ${type}?`
+                            t`Why is this dashboard the most important?` :
+                            t`What is useful or interesting about this ${type}?`
                     }
                 </EditLabel>
                 <textarea
                     className={S.guideDetailEditorTextarea}
-                    placeholder="Write something helpful here"
+                    placeholder={t`Write something helpful here`}
                     {...formField.points_of_interest}
                     disabled={disabled}
                 />
@@ -162,13 +162,13 @@ const GuideDetailEditor = ({
             <div className={cx('mb2', { 'disabled' : disabled })}>
                 <EditLabel>
                     { type === 'dashboard' ?
-                            `Is there anything users of this dashboard should be aware of?` :
-                            `Anything users should be aware of about this ${type}?`
+                            t`Is there anything users of this dashboard should be aware of?` :
+                            t`Anything users should be aware of about this ${type}?`
                     }
                 </EditLabel>
                 <textarea
                     className={S.guideDetailEditorTextarea}
-                    placeholder="Write something helpful here"
+                    placeholder={t`Write something helpful here`}
                     {...formField.caveats}
                     disabled={disabled}
                 />
@@ -176,12 +176,12 @@ const GuideDetailEditor = ({
             { type === 'metric' &&
                 <div className={cx('mb2', { 'disabled' : disabled })}>
                     <EditLabel key="metricFieldsLabel">
-                        Which 2-3 fields do you usually group this metric by?
+                        {t`Which 2-3 fields do you usually group this metric by?`}
                     </EditLabel>
                     <Select
                         options={fieldsByMetric}
                         optionNameFn={option => option.display_name || option.name}
-                        placeholder="Select..."
+                        placeholder={t`Select...`}
                         values={formField.important_fields.value || []}
                         disabledOptionIds={formField.important_fields.value && formField.important_fields.value.length === 3 ?
                             fieldsByMetric
diff --git a/frontend/src/metabase/reference/components/GuideHeader.jsx b/frontend/src/metabase/reference/components/GuideHeader.jsx
index 863e7d4f08305f80434755a1079647d5037327a1..84b0a4586538fe5cc2a896d2307a6e062cde3b90 100644
--- a/frontend/src/metabase/reference/components/GuideHeader.jsx
+++ b/frontend/src/metabase/reference/components/GuideHeader.jsx
@@ -1,7 +1,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 import pure from "recompose/pure";
-
+import { t } from 'c-3po';
 import EditButton from "metabase/reference/components/EditButton.jsx";
 
 const GuideHeader = ({
@@ -11,14 +11,14 @@ const GuideHeader = ({
     <div>
         <div className="wrapper wrapper--trim py4 my3">
             <div className="flex align-center">
-                <h1 className="text-dark" style={{fontWeight: 700}}>Start here.</h1>
+                <h1 className="text-dark" style={{fontWeight: 700}}>{t`Start here.`}</h1>
                 { isSuperuser &&
                     <span className="ml-auto">
                         <EditButton startEditing={startEditing}/>
                     </span>
                 }
             </div>
-            <p className="text-paragraph" style={{maxWidth: 620}}>This is the perfect place to start if you’re new to your company’s data, or if you just want to check in on what’s going on.</p>
+            <p className="text-paragraph" style={{maxWidth: 620}}>{t`This is the perfect place to start if you’re new to your company’s data, or if you just want to check in on what’s going on.`}</p>
         </div>
     </div>;
 
diff --git a/frontend/src/metabase/reference/components/MetricImportantFieldsDetail.jsx b/frontend/src/metabase/reference/components/MetricImportantFieldsDetail.jsx
index f21e13d63269a23865f5366b2976204a8079c85a..f64b66f8e4660a573009ceb81cac2f8cbbaa82c9 100644
--- a/frontend/src/metabase/reference/components/MetricImportantFieldsDetail.jsx
+++ b/frontend/src/metabase/reference/components/MetricImportantFieldsDetail.jsx
@@ -2,7 +2,7 @@ import React from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
 import pure from "recompose/pure";
-
+import { t } from 'c-3po';
 import FieldsToGroupBy from "metabase/reference/components/FieldsToGroupBy.jsx";
 
 import Select from "metabase/components/Select.jsx";
@@ -22,7 +22,7 @@ const MetricImportantFieldsDetail = ({
         <div className={D.detailBody}>
             <div className={D.detailTitle}>
                 <span className={D.detailName}>
-                    Which 2-3 fields do you usually group this metric by?
+                    {t`Which 2-3 fields do you usually group this metric by?`}
                 </span>
             </div>
             <div className={cx(D.detailSubtitle, { "mt1" : true })}>
@@ -31,7 +31,7 @@ const MetricImportantFieldsDetail = ({
                     triggerClasses="input p1 block"
                     options={table.fields.map(fieldId => allFields[fieldId])} 
                     optionNameFn={option => option.display_name || option.name}
-                    placeholder="Select..."
+                    placeholder={t`Select...`}
                     values={formField.value || []}
                     disabledOptionIds={formField.value && formField.value.length === 3 ?
                         table.fields
@@ -55,7 +55,7 @@ const MetricImportantFieldsDetail = ({
             fields={fields}
             databaseId={table.db_id} 
             metric={metric}
-            title={"Most useful fields to group this metric by"}
+            title={t`Most useful fields to group this metric by`}
             onChangeLocation={onChangeLocation}
         /> :
         null; 
diff --git a/frontend/src/metabase/reference/components/ReferenceHeader.jsx b/frontend/src/metabase/reference/components/ReferenceHeader.jsx
index fe467edba01f61786c34486a014e17088bdf68d4..dbebc2fa803f44bdd28c552f6bda98fa7012f905 100644
--- a/frontend/src/metabase/reference/components/ReferenceHeader.jsx
+++ b/frontend/src/metabase/reference/components/ReferenceHeader.jsx
@@ -11,7 +11,7 @@ import E from "metabase/reference/components/EditButton.css";
 import IconBorder from "metabase/components/IconBorder.jsx";
 import Icon from "metabase/components/Icon.jsx";
 import Ellipsified from "metabase/components/Ellipsified.jsx";
-
+import { t } from 'c-3po';
 
 const ReferenceHeader = ({
     name,
@@ -56,7 +56,7 @@ const ReferenceHeader = ({
                             data-metabase-event={`Data Reference;Entity -> QB click;${type}`}
                         >
                             <div className="flex align-center relative">
-                                <span className="mr1 flex-no-shrink">See this {type}</span>
+                                <span className="mr1 flex-no-shrink">{t`See this ${type}`}</span>
                                 <Icon name="chevronright" size={16} />
                             </div>
                         </Link>
diff --git a/frontend/src/metabase/reference/components/RevisionMessageModal.jsx b/frontend/src/metabase/reference/components/RevisionMessageModal.jsx
index 282e4f54b0a8ddde81a6a8409f9831d71b9caf64..cd94a2a69d9250fc4d829ab801a8fe552a43da2e 100644
--- a/frontend/src/metabase/reference/components/RevisionMessageModal.jsx
+++ b/frontend/src/metabase/reference/components/RevisionMessageModal.jsx
@@ -1,7 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
 import ModalContent from "metabase/components/ModalContent.jsx";
 
@@ -30,20 +30,20 @@ export default class RevisionMessageModal extends Component {
         return (
             <ModalWithTrigger ref="modal" triggerElement={children}>
                 <ModalContent
-                    title="Reason for changes"
+                    title={t`Reason for changes`}
                     onClose={onClose}
                 >
                     <div className={S.modalBody}>
                         <textarea
                             className={S.modalTextArea}
-                            placeholder="Leave a note to explain what changes you made and why they were required"
+                            placeholder={t`Leave a note to explain what changes you made and why they were required`}
                             {...field}
                         />
                     </div>
 
                     <div className="Form-actions">
-                        <button type="button" className="Button Button--primary" onClick={onAction} disabled={submitting || field.error}>Save changes</button>
-                        <button type="button" className="Button ml1" onClick={onClose}>Cancel</button>
+                        <button type="button" className="Button Button--primary" onClick={onAction} disabled={submitting || field.error}>{t`Save changes`}</button>
+                        <button type="button" className="Button ml1" onClick={onClose}>{t`Cancel`}</button>
                     </div>
                 </ModalContent>
             </ModalWithTrigger>
diff --git a/frontend/src/metabase/reference/components/UsefulQuestions.jsx b/frontend/src/metabase/reference/components/UsefulQuestions.jsx
index bac4d8e2bcbd5ed42f67312ba03cd5ba7160784d..4b0db066365b1b7c1a44d0fd702ead24ace80508 100644
--- a/frontend/src/metabase/reference/components/UsefulQuestions.jsx
+++ b/frontend/src/metabase/reference/components/UsefulQuestions.jsx
@@ -2,7 +2,7 @@ import React from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
 import pure from "recompose/pure";
-
+import { t } from 'c-3po';
 import S from "./UsefulQuestions.css";
 import D from "metabase/reference/components/Detail.css";
 import L from "metabase/components/List.css";
@@ -15,7 +15,7 @@ const UsefulQuestions = ({
     <div className={cx(D.detail)}>
         <div className={D.detailBody}>
             <div className={D.detailTitle}>
-                <span className={D.detailName}>Potentially useful questions</span>
+                <span className={D.detailName}>{t`Potentially useful questions`}</span>
             </div>
             <div className={S.usefulQuestions}>
                 { questions.map((question, index, questions) =>
diff --git a/frontend/src/metabase/reference/databases/DatabaseDetail.jsx b/frontend/src/metabase/reference/databases/DatabaseDetail.jsx
index 00d42fcc9d4ef7148dabe0fc6e34b7edf7d3c99d..31c047bd9b5fcdb6cd6bb67e5f562c71ede31de9 100644
--- a/frontend/src/metabase/reference/databases/DatabaseDetail.jsx
+++ b/frontend/src/metabase/reference/databases/DatabaseDetail.jsx
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
 import { push } from "react-router-redux";
-
+import { t } from 'c-3po';
 import List from "metabase/components/List.jsx";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
 
@@ -139,9 +139,9 @@ export default class DatabaseDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="description"
-                                    name="Description"
+                                    name={t`Description`}
                                     description={entity.description}
-                                    placeholder="No description yet"
+                                    placeholder={t`No description yet`}
                                     isEditing={isEditing}
                                     field={description}
                                 />
@@ -149,9 +149,9 @@ export default class DatabaseDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="points_of_interest"
-                                    name={`Why this database is interesting`}
+                                    name={t`Why this database is interesting`}
                                     description={entity.points_of_interest}
-                                    placeholder="Nothing interesting yet"
+                                    placeholder={t`Nothing interesting yet`}
                                     isEditing={isEditing}
                                     field={points_of_interest}
                                     />
@@ -159,9 +159,9 @@ export default class DatabaseDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="caveats"
-                                    name={`Things to be aware of about this database`}
+                                    name={t`Things to be aware of about this database`}
                                     description={entity.caveats}
-                                    placeholder="Nothing to be aware of yet"
+                                    placeholder={t`Nothing to be aware of yet`}
                                     isEditing={isEditing}
                                     field={caveats}
                                 />
diff --git a/frontend/src/metabase/reference/databases/DatabaseList.jsx b/frontend/src/metabase/reference/databases/DatabaseList.jsx
index 8d5e4bc78a8f72704ac1afa9f4cf192273e2e0d6..92ff27535bb375a81b850b5012f5bb811fbb8db3 100644
--- a/frontend/src/metabase/reference/databases/DatabaseList.jsx
+++ b/frontend/src/metabase/reference/databases/DatabaseList.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-
+import { t } from 'c-3po';
 import { isQueryable } from "metabase/lib/table";
 
 import S from "metabase/components/List.css";
@@ -53,14 +53,14 @@ export default class DatabaseList extends Component {
 
         return (
             <div style={style} className="full">
-                <ReferenceHeader 
-                    name="Databases and tables"
+                <ReferenceHeader
+                    name={t`Databases and tables`}
                 />
                 <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
                 { () => Object.keys(entities).length > 0 ?
                     <div className="wrapper wrapper--trim">
                         <List>
-                            { 
+                            {
                                 Object.values(entities).filter(isQueryable).map((entity, index) =>
                                     entity && entity.id && entity.name &&
                                           <li className="relative" key={entity.id}>
diff --git a/frontend/src/metabase/reference/databases/DatabaseSidebar.jsx b/frontend/src/metabase/reference/databases/DatabaseSidebar.jsx
index 9ef1dd1adba36db281bb6f96000c2fb22f399d65..261c60bf7ed16709b6167e03f2ab10a5007e5e55 100644
--- a/frontend/src/metabase/reference/databases/DatabaseSidebar.jsx
+++ b/frontend/src/metabase/reference/databases/DatabaseSidebar.jsx
@@ -2,7 +2,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 import S from "metabase/components/Sidebar.css";
-
+import { t } from 'c-3po';
 import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
 import SidebarItem from "metabase/components/SidebarItem.jsx"
 
@@ -19,20 +19,20 @@ const DatabaseSidebar = ({
             <div className={S.breadcrumbs}>
                 <Breadcrumbs
                     className="py4"
-                    crumbs={[["Databases","/reference/databases"],
+                    crumbs={[[t`Databases`,"/reference/databases"],
                              [database.name]]}
                     inSidebar={true}
-                    placeholder="Data Reference"
+                    placeholder={t`Data Reference`}
                 />
             </div>
                 <SidebarItem key={`/reference/databases/${database.id}`} 
                              href={`/reference/databases/${database.id}`} 
                              icon="document" 
-                             name="Details" />
+                             name={t`Details`} />
                 <SidebarItem key={`/reference/databases/${database.id}/tables`} 
                              href={`/reference/databases/${database.id}/tables`} 
                              icon="table2" 
-                             name={`Tables in ${database.name}`} />
+                             name={t`Tables in ${database.name}`} />
         </ul>
     </div>
 DatabaseSidebar.propTypes = {
diff --git a/frontend/src/metabase/reference/databases/FieldDetail.jsx b/frontend/src/metabase/reference/databases/FieldDetail.jsx
index 4935084040b338422efaae53f2a2416431bf9753..051021c80d59652a2efd04a2611fbe737abab9d4 100644
--- a/frontend/src/metabase/reference/databases/FieldDetail.jsx
+++ b/frontend/src/metabase/reference/databases/FieldDetail.jsx
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
 import { push } from "react-router-redux";
-
+import { t } from 'c-3po';
 import S from "metabase/reference/Reference.css";
 
 import List from "metabase/components/List.jsx";
@@ -39,7 +39,7 @@ import * as actions from 'metabase/reference/reference';
 const interestingQuestions = (database, table, field, metadata) => {
     return [
         {
-            text: `Number of ${table.display_name} grouped by ${field.display_name}`,
+            text: t`Number of ${table.display_name} grouped by ${field.display_name}`,
             icon: { name: "bar", scale: 1, viewBox: "8 8 16 16" },
             link: getQuestionUrl({
                 dbId: database.id,
@@ -51,7 +51,7 @@ const interestingQuestions = (database, table, field, metadata) => {
             })
         },
         {
-            text: `Number of ${table.display_name} grouped by ${field.display_name}`,
+            text: t`Number of ${table.display_name} grouped by ${field.display_name}`,
             icon: { name: "pie", scale: 1, viewBox: "8 8 16 16" },
             link: getQuestionUrl({
                 dbId: database.id,
@@ -63,7 +63,7 @@ const interestingQuestions = (database, table, field, metadata) => {
             })
         },
         {
-            text: `All distinct values of ${field.display_name}`,
+            text: t`All distinct values of ${field.display_name}`,
             icon: "table2",
             link: getQuestionUrl({
                 dbId: database.id,
@@ -192,9 +192,9 @@ export default class FieldDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="description"
-                                    name="Description"
+                                    name={t`Description`}
                                     description={entity.description}
-                                    placeholder="No description yet"
+                                    placeholder={t`No description yet`}
                                     isEditing={isEditing}
                                     field={description}
                                 />
@@ -203,7 +203,7 @@ export default class FieldDetail extends Component {
                                 <li className="relative">
                                     <Detail
                                         id="name"
-                                        name="Actual name in database"
+                                        name={t`Actual name in database`}
                                         description={entity.name}
                                         subtitleClass={S.tableActualName}
                                     />
@@ -212,9 +212,9 @@ export default class FieldDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="points_of_interest"
-                                    name={`Why this field is interesting`}
+                                    name={t`Why this field is interesting`}
                                     description={entity.points_of_interest}
-                                    placeholder="Nothing interesting yet"
+                                    placeholder={t`Nothing interesting yet`}
                                     isEditing={isEditing}
                                     field={points_of_interest}
                                     />
@@ -222,9 +222,9 @@ export default class FieldDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="caveats"
-                                    name={`Things to be aware of about this field`}
+                                    name={t`Things to be aware of about this field`}
                                     description={entity.caveats}
-                                    placeholder="Nothing to be aware of yet"
+                                    placeholder={t`Nothing to be aware of yet`}
                                     isEditing={isEditing}
                                     field={caveats}
                                 />
@@ -235,7 +235,7 @@ export default class FieldDetail extends Component {
                                 <li className="relative">
                                     <Detail
                                         id="base_type"
-                                        name={`Data type`}
+                                        name={t`Data type`}
                                         description={entity.base_type}
                                     />
                                 </li>
diff --git a/frontend/src/metabase/reference/databases/FieldList.jsx b/frontend/src/metabase/reference/databases/FieldList.jsx
index 5f68fb113e336b4a264b64bc0c280ccbaff77dd3..add676b9a65da451a3a3bd0131f4b7eb73170e88 100644
--- a/frontend/src/metabase/reference/databases/FieldList.jsx
+++ b/frontend/src/metabase/reference/databases/FieldList.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
-
+import { t } from 'c-3po';
 import S from "metabase/components/List.css";
 import R from "metabase/reference/Reference.css";
 import F from "metabase/reference/components/Field.css"
@@ -39,7 +39,7 @@ import * as actions from 'metabase/reference/reference';
 
 
 const emptyStateData = {
-    message: `Fields in this table will appear here as they're added`,
+    message: t`Fields in this table will appear here as they're added`,
     icon: "fields"
 }
 
@@ -128,7 +128,7 @@ export default class FieldList extends Component {
                 }
                 <EditableReferenceHeader 
                     headerIcon="table2"
-                    name={`Fields in ${table.display_name}`}
+                    name={t`Fields in ${table.display_name}`}
                     user={user} 
                     isEditing={isEditing} 
                     startEditing={startEditing} 
@@ -139,13 +139,13 @@ export default class FieldList extends Component {
                         <div className={S.item}>
                             <div className={R.columnHeader}>
                                 <div className={cx(S.itemTitle, F.fieldNameTitle)}>
-                                    Field name
+                                    {t`Field name`}
                                 </div>
                                 <div className={cx(S.itemTitle, F.fieldType)}>
-                                    Field type
+                                    {t`Field type`}
                                 </div>
                                 <div className={cx(S.itemTitle, F.fieldDataType)}>
-                                    Data type
+                                    {t`Data type`}
                                 </div>
                             </div>
                         </div>
diff --git a/frontend/src/metabase/reference/databases/FieldSidebar.jsx b/frontend/src/metabase/reference/databases/FieldSidebar.jsx
index f2e66b1c4879cf4e07ab585d1e78b9a340901375..da0092589a264ebe3dceb339fa70f224ab3a1be9 100644
--- a/frontend/src/metabase/reference/databases/FieldSidebar.jsx
+++ b/frontend/src/metabase/reference/databases/FieldSidebar.jsx
@@ -2,7 +2,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 import S from "metabase/components/Sidebar.css";
-
+import { t } from 'c-3po';
 import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
 import SidebarItem from "metabase/components/SidebarItem.jsx"
 
@@ -26,19 +26,19 @@ const FieldSidebar =({
                              [table.name,`/reference/databases/${database.id}/tables/${table.id}`],
                              [field.name]]}
                     inSidebar={true}
-                    placeholder="Data Reference"
+                    placeholder={t`Data Reference`}
                 />
             </div>
             <SidebarItem key={`/reference/databases/${database.id}/tables/${table.id}/fields/${field.id}`}
                          href={`/reference/databases/${database.id}/tables/${table.id}/fields/${field.id}`}
                          icon="document"
-                         name="Details" />
+                         name={t`Details`} />
              { showXray && (
                  <SidebarItem
                      key={`/xray/field/${field.id}/approximate`}
                      href={`/xray/field/${field.id}/approximate`}
                      icon="beaker"
-                     name="X-ray this Field" />
+                     name={t`X-ray this Field`} />
              )}
         </ul>
     </div>
diff --git a/frontend/src/metabase/reference/databases/NoDatabasesEmptyState.jsx b/frontend/src/metabase/reference/databases/NoDatabasesEmptyState.jsx
index 3d43a056193c513663efd7405ea3f8caca8d8419..c7a850ae52f5c65fc40011a3221b61100a963b1c 100644
--- a/frontend/src/metabase/reference/databases/NoDatabasesEmptyState.jsx
+++ b/frontend/src/metabase/reference/databases/NoDatabasesEmptyState.jsx
@@ -3,13 +3,13 @@ import AdminAwareEmptyState from "metabase/components/AdminAwareEmptyState";
 
 const NoDatabasesEmptyState = (user) =>
     <AdminAwareEmptyState
-        title={"Metabase is no fun without any data"}
-        adminMessage={"Your databases will appear here once you connect one"}
-        message={"Databases will appear here once your admins have added some"}
+        title={t`Metabase is no fun without any data`}
+        adminMessage={t`Your databases will appear here once you connect one`}
+        message={t`Databases will appear here once your admins have added some`}
         image={"app/assets/img/databases-list"}
-        adminAction={"Connect a database"}
+        adminAction={t`Connect a database`}
         adminLink={"/admin/databases/create"}
         user={user}
     />
 
-export default NoDatabasesEmptyState
\ No newline at end of file
+export default NoDatabasesEmptyState
diff --git a/frontend/src/metabase/reference/databases/TableDetail.jsx b/frontend/src/metabase/reference/databases/TableDetail.jsx
index 4af371e7455c1a6814ec1a75bc36959a10e98207..973207104189c9cec6beb87561206ec220ac74ff 100644
--- a/frontend/src/metabase/reference/databases/TableDetail.jsx
+++ b/frontend/src/metabase/reference/databases/TableDetail.jsx
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
 import { push } from "react-router-redux";
-
+import { t } from 'c-3po';
 import S from "metabase/reference/Reference.css";
 
 import List from "metabase/components/List.jsx";
@@ -38,7 +38,7 @@ import * as actions from 'metabase/reference/reference';
 const interestingQuestions = (table) => {
     return [
         {
-            text: `Count of ${table.display_name}`,
+            text: t`Count of ${table.display_name}`,
             icon: { name: "number", scale: 1, viewBox: "8 8 16 16" },
             link: getQuestionUrl({
                 dbId: table.db_id,
@@ -47,7 +47,7 @@ const interestingQuestions = (table) => {
             })
         },
         {
-            text: `See raw data for ${table.display_name}`,
+            text: t`See raw data for ${table.display_name}`,
             icon: "table2",
             link: getQuestionUrl({
                 dbId: table.db_id,
@@ -155,7 +155,7 @@ export default class TableDetail extends Component {
                     type="table"
                     headerIcon="table2"
                     headerLink={getQuestionUrl({ dbId: entity.db_id, tableId: entity.id})}
-                    name="Details"
+                    name={t`Details`}
                     user={user}
                     isEditing={isEditing}
                     hasSingleSchema={hasSingleSchema}
@@ -171,9 +171,9 @@ export default class TableDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="description"
-                                    name="Description"
+                                    name={t`Description`}
                                     description={entity.description}
-                                    placeholder="No description yet"
+                                    placeholder={t`No description yet`}
                                     isEditing={isEditing}
                                     field={description}
                                 />
@@ -182,7 +182,7 @@ export default class TableDetail extends Component {
                                 <li className="relative">
                                     <Detail
                                         id="name"
-                                        name="Actual name in database"
+                                        name={t`Actual name in database`}
                                         description={entity.name}
                                         subtitleClass={S.tableActualName}
                                     />
@@ -191,9 +191,9 @@ export default class TableDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="points_of_interest"
-                                    name={`Why this table is interesting`}
+                                    name={t`Why this table is interesting`}
                                     description={entity.points_of_interest}
-                                    placeholder="Nothing interesting yet"
+                                    placeholder={t`Nothing interesting yet`}
                                     isEditing={isEditing}
                                     field={points_of_interest}
                                     />
@@ -201,9 +201,9 @@ export default class TableDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="caveats"
-                                    name={`Things to be aware of about this table`}
+                                    name={t`Things to be aware of about this table`}
                                     description={entity.caveats}
-                                    placeholder="Nothing to be aware of yet"
+                                    placeholder={t`Nothing to be aware of yet`}
                                     isEditing={isEditing}
                                     field={caveats}
                                 />
diff --git a/frontend/src/metabase/reference/databases/TableList.jsx b/frontend/src/metabase/reference/databases/TableList.jsx
index 2fbe9254fc84e6142f766f909536fa2a9da559b0..30445649cc19733b6d4b9cd62cf8ce25060eab5d 100644
--- a/frontend/src/metabase/reference/databases/TableList.jsx
+++ b/frontend/src/metabase/reference/databases/TableList.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-
+import { t } from 'c-3po';
 import { isQueryable } from "metabase/lib/table";
 
 import S from "metabase/components/List.css";
@@ -29,7 +29,7 @@ import * as metadataActions from "metabase/redux/metadata";
 
 
 const emptyStateData = {
-    message: `Tables in this database will appear here as they're added`,
+    message: t`Tables in this database will appear here as they're added`,
     icon: "table2"
 }
 
@@ -110,7 +110,7 @@ export default class TableList extends Component {
         return (
             <div style={style} className="full">
                 <ReferenceHeader 
-                    name={`Tables in ${database.name}`}
+                    name={t`Tables in ${database.name}`}
                     type="tables"
                     headerIcon="database"
                 />
diff --git a/frontend/src/metabase/reference/databases/TableQuestions.jsx b/frontend/src/metabase/reference/databases/TableQuestions.jsx
index fe94f2292a9005ee42012c3c1dd084b56087031e..0907f41451720a77195bac97d82f9c37f0629aa7 100644
--- a/frontend/src/metabase/reference/databases/TableQuestions.jsx
+++ b/frontend/src/metabase/reference/databases/TableQuestions.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import moment from "moment";
-
+import { t } from 'c-3po';
 import visualizations from "metabase/visualizations";
 import { isQueryable } from "metabase/lib/table";
 import * as Urls from "metabase/lib/urls";
@@ -34,9 +34,9 @@ import * as metadataActions from "metabase/redux/metadata";
 
 const emptyStateData = (table) =>  {
     return {
-        message: "Questions about this table will appear here as they're added",
+        message: t`Questions about this table will appear here as they're added`,
         icon: "all",
-        action: "Ask a question",
+        action: t`Ask a question`,
         link: getQuestionUrl({
             dbId: table.db_id,
             tableId: table.id,
@@ -78,7 +78,7 @@ export default class TableQuestions extends Component {
         return (
             <div style={style} className="full">
                 <ReferenceHeader 
-                    name={`Questions about ${this.props.table.display_name}`}
+                    name={t`Questions about ${this.props.table.display_name}`}
                     type="questions"
                     headerIcon="table2"
                 />
@@ -94,7 +94,7 @@ export default class TableQuestions extends Component {
                                                     id={entity.id}
                                                     index={index}
                                                     name={entity.display_name || entity.name}
-                                                    description={ `Created ${moment(entity.created_at).fromNow()} by ${entity.creator.common_name}` }
+                                                    description={ t`Created ${moment(entity.created_at).fromNow()} by ${entity.creator.common_name}` }
                                                     url={ Urls.question(entity.id) }
                                                     icon={ visualizations.get(entity.display).iconName }
                                                 />
diff --git a/frontend/src/metabase/reference/databases/TableSidebar.jsx b/frontend/src/metabase/reference/databases/TableSidebar.jsx
index edd315ea0ec38d9299489c44ecce0ed818c06cc1..2181e234919ee453c3d1f1802765bd949d9fb79d 100644
--- a/frontend/src/metabase/reference/databases/TableSidebar.jsx
+++ b/frontend/src/metabase/reference/databases/TableSidebar.jsx
@@ -2,7 +2,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 import S from "metabase/components/Sidebar.css";
-
+import { t } from 'c-3po';
 import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
 import SidebarItem from "metabase/components/SidebarItem.jsx"
 
@@ -20,31 +20,31 @@ const TableSidebar = ({
         <div className={S.breadcrumbs}>
             <Breadcrumbs
                 className="py4"
-                crumbs={[["Databases","/reference/databases"],
+                crumbs={[[t`Databases`,"/reference/databases"],
                          [database.name, `/reference/databases/${database.id}`],
                          [table.name]]}
                 inSidebar={true}
-                placeholder="Data Reference"
+                placeholder={t`Data Reference`}
             />
         </div>
         <ol>
             <SidebarItem key={`/reference/databases/${database.id}/tables/${table.id}`}
                          href={`/reference/databases/${database.id}/tables/${table.id}`}
                          icon="document"
-                         name="Details" />
+                         name={t`Details`} />
             <SidebarItem key={`/reference/databases/${database.id}/tables/${table.id}/fields`}
                          href={`/reference/databases/${database.id}/tables/${table.id}/fields`}
                          icon="fields"
-                         name="Fields in this table" />
+                         name={t`Fields in this table`} />
             <SidebarItem key={`/reference/databases/${database.id}/tables/${table.id}/questions`}
                          href={`/reference/databases/${database.id}/tables/${table.id}/questions`}
                          icon="all"
-                         name="Questions about this table" />
+                         name={t`Questions about this table`} />
             { showXray && (
                 <SidebarItem key={`/xray/table/${table.id}/approximate`}
                              href={`/xray/table/${table.id}/approximate`}
                              icon="beaker"
-                             name="X-ray this table" />
+                             name={t`X-ray this table`} />
             )}
         </ol>
     </div>
diff --git a/frontend/src/metabase/reference/guide/BaseSidebar.jsx b/frontend/src/metabase/reference/guide/BaseSidebar.jsx
index e52cd9f9ac9d0533f6e2b494812b173e0d6ffbb0..d1626df9f9ba5a7d02e4954a115ca48ac6ede2b4 100644
--- a/frontend/src/metabase/reference/guide/BaseSidebar.jsx
+++ b/frontend/src/metabase/reference/guide/BaseSidebar.jsx
@@ -2,7 +2,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 import S from "metabase/components/Sidebar.css";
-
+import { t } from 'c-3po';
 import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
 import SidebarItem from "metabase/components/SidebarItem.jsx"
 
@@ -17,28 +17,28 @@ const BaseSidebar = ({
         <div className={S.breadcrumbs}>
             <Breadcrumbs
                 className="py4"
-                crumbs={[["Data Reference"]]}
+                crumbs={[[t`Data Reference`]]}
                 inSidebar={true}
-                placeholder="Data Reference"
+                placeholder={t`Data Reference`}
             />
         </div>
         <ol>
             <SidebarItem key="/reference/guide" 
                          href="/reference/guide" 
                          icon="reference" 
-                         name="Start here" />
+                         name={t`Start here`} />
             <SidebarItem key="/reference/metrics" 
                          href="/reference/metrics" 
                          icon="ruler" 
-                         name="Metrics" />
+                         name={t`Metrics`} />
             <SidebarItem key="/reference/segments" 
                          href="/reference/segments" 
                          icon="segment" 
-                         name="Segments" />
+                         name={t`Segments`} />
             <SidebarItem key="/reference/databases" 
                          href="/reference/databases" 
                          icon="database" 
-                         name="Databases and tables" />
+                         name={t`Databases and tables`} />
         </ol>
     </div>
 
diff --git a/frontend/src/metabase/reference/guide/GettingStartedGuide.jsx b/frontend/src/metabase/reference/guide/GettingStartedGuide.jsx
index 6b207af002cde2f0ccf0a70c3a942beea7aa985e..7fccc74d452a72569dd1bd4ca31b35b1ece56697 100644
--- a/frontend/src/metabase/reference/guide/GettingStartedGuide.jsx
+++ b/frontend/src/metabase/reference/guide/GettingStartedGuide.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
 import { connect } from 'react-redux';
-
+import { t } from 'c-3po';
 import cx from "classnames";
 
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
@@ -142,15 +142,15 @@ export default class GettingStartedGuide extends Component {
                         <div className="wrapper wrapper--trim">
                             { (!guide || isGuideEmpty(guide)) && user && user.is_superuser && (
                                 <AdminInstructions>
-                                    <h2 className="py2">Help your team get started with your data.</h2>
+                                    <h2 className="py2">{t`Help your team get started with your data.`}</h2>
                                     <GuideText>
-                                        Show your team what’s most important by choosing your top dashboard, metrics, and segments.
+                                        {t`Show your team what’s most important by choosing your top dashboard, metrics, and segments.`}
                                     </GuideText>
                                     <button
                                         className="Button Button--primary"
                                         onClick={startEditing}
                                     >
-                                        Get started
+                                        {t`Get started`}
                                     </button>
                                 </AdminInstructions>
                             )}
@@ -158,7 +158,7 @@ export default class GettingStartedGuide extends Component {
                             { guide.most_important_dashboard !== null && [
                                 <div className="my2">
                                     <SectionHeader key={'dashboardTitle'}>
-                                        Our most important dashboard
+                                        {t`Our most important dashboard`}
                                     </SectionHeader>
                                     <GuideDetail
                                         key={'dashboardDetail'}
@@ -171,7 +171,7 @@ export default class GettingStartedGuide extends Component {
                             { Object.keys(metrics).length > 0  && (
                                     <div className="my4 pt4">
                                         <SectionHeader trim={guide.important_metrics.length === 0}>
-                                            { guide.important_metrics && guide.important_metrics.length > 0 ? 'Numbers that we pay attention to' : 'Metrics' }
+                                            { guide.important_metrics && guide.important_metrics.length > 0 ? t`Numbers that we pay attention to` : t`Metrics` }
                                         </SectionHeader>
                                         { (guide.important_metrics && guide.important_metrics.length > 0) ? [
                                             <div className="my2">
@@ -187,12 +187,12 @@ export default class GettingStartedGuide extends Component {
                                             </div>
                                         ] :
                                             <GuideText>
-                                                Metrics are important numbers your company cares about. They often represent a core indicator of how the business is performing.
+                                                {t`Metrics are important numbers your company cares about. They often represent a core indicator of how the business is performing.`}
                                             </GuideText>
                                         }
                                         <div>
                                             <Link className="Button Button--primary" to={'/reference/metrics'}>
-                                                See all metrics
+                                                {t`See all metrics`}
                                             </Link>
                                         </div>
                                     </div>
@@ -201,7 +201,7 @@ export default class GettingStartedGuide extends Component {
 
                             <div className="mt4 pt4">
                                 <SectionHeader trim={(!has(guide.important_segments) && !has(guide.important_tables))}>
-                                    { Object.keys(segments).length > 0 ? 'Segments and tables' : 'Tables' }
+                                    { Object.keys(segments).length > 0 ? t`Segments and tables` : t`Tables` }
                                 </SectionHeader>
                                 { has(guide.important_segments) || has(guide.important_tables) ?
                                     <div className="my2">
@@ -226,16 +226,16 @@ export default class GettingStartedGuide extends Component {
                                     <GuideText>
                                         { Object.keys(segments).length > 0 ? (
                                             <span>
-                                                Segments and tables are the building blocks of your company's data. Tables are collections of the raw information while segments are specific slices with specific meanings, like <b>"Recent orders."</b>
+                                                {t`Segments and tables are the building blocks of your company's data. Tables are collections of the raw information while segments are specific slices with specific meanings, like <b>"Recent orders."</b>`}
                                             </span>
-                                        ) : "Tables are the building blocks of your company's data."
+                                        ) : t`Tables are the building blocks of your company's data.`
                                         }
                                     </GuideText>
                                 }
                                 <div>
                                     { Object.keys(segments).length > 0 && (
                                         <Link className="Button Button--purple mr2" to={'/reference/segments'}>
-                                            See all segments
+                                            {t`See all segments`}
                                         </Link>
                                     )}
                                     <Link
@@ -245,33 +245,33 @@ export default class GettingStartedGuide extends Component {
                                         )}
                                         to={'/reference/databases'}
                                     >
-                                        See all tables
+                                        {t`See all tables`}
                                     </Link>
                                 </div>
                             </div>
 
                             <div className="mt4 pt4">
                                 <SectionHeader trim={!guide.things_to_know}>
-                                    { guide.things_to_know ? 'Other things to know about our data' : 'Find out more' }
+                                    { guide.things_to_know ? t`Other things to know about our data` : t`Find out more` }
                                 </SectionHeader>
                                 <GuideText>
-                                    { guide.things_to_know ? guide.things_to_know : "A good way to get to know your data is by spending a bit of time exploring the different tables and other info available to you. It may take a while, but you'll start to recognize names and meanings over time."
+                                    { guide.things_to_know ? guide.things_to_know : t`A good way to get to know your data is by spending a bit of time exploring the different tables and other info available to you. It may take a while, but you'll start to recognize names and meanings over time.`
                                     }
                                 </GuideText>
                                 <Link className="Button link text-bold" to={'/reference/databases'}>
-                                    Explore our data
+                                    {t`Explore our data`}
                                 </Link>
                             </div>
 
                             <div className="mt4">
                                 { guide.contact && (guide.contact.name || guide.contact.email) && [
                                     <SectionHeader key={'contactTitle'}>
-                                        Have questions?
+                                        {t`Have questions?`}
                                     </SectionHeader>,
                                     <div className="mb4 pb4" key={'contactDetails'}>
                                             { guide.contact.name &&
                                                 <span className="text-dark mr3">
-                                                    {`Contact ${guide.contact.name}`}
+                                                    {t`Contact ${guide.contact.name}`}
                                                 </span>
                                             }
                                             { guide.contact.email &&
diff --git a/frontend/src/metabase/reference/guide/GettingStartedGuideEditForm.jsx b/frontend/src/metabase/reference/guide/GettingStartedGuideEditForm.jsx
index f1bc464660b0b47b24246e43e853cbaf3f87e1ac..81161dee31e84eeed4c0b7da24b89104084bcd42 100644
--- a/frontend/src/metabase/reference/guide/GettingStartedGuideEditForm.jsx
+++ b/frontend/src/metabase/reference/guide/GettingStartedGuideEditForm.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from 'react-redux';
 import { reduxForm } from "redux-form";
-
+import { t } from 'c-3po';
 import cx from "classnames";
 
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
@@ -227,28 +227,25 @@ export default class GettingStartedGuideEditForm extends Component {
                     <div className="wrapper wrapper--trim">
                         <div className="mt4 py2">
                             <h1 className="my3 text-dark">
-                                Help new Metabase users find their way around.
+                                {t`Help new Metabase users find their way around.`}
                             </h1>
                             <p className="text-paragraph text-measure">
-                                The Getting Started guide highlights the dashboard,
-                                metrics, segments, and tables that matter most,
-                                and informs your users of important things they
-                                should know before digging into the data.
+                                {t`The Getting Started guide highlights the dashboard, metrics, segments, and tables that matter most, and informs your users of important things they should know before digging into the data.`}
                             </p>
                         </div>
 
                         <GuideEditSection
                             isCollapsed={most_important_dashboard.id.value === undefined}
                             isDisabled={!dashboards || Object.keys(dashboards).length === 0}
-                            collapsedTitle="Is there an important dashboard for your team?"
+                            collapsedTitle={t`Is there an important dashboard for your team?`}
                             collapsedIcon="dashboard"
-                            linkMessage="Create a dashboard now"
+                            linkMessage={t`Create a dashboard now`}
                             action={showDashboardModal}
                             expand={() => most_important_dashboard.id.onChange(null)}
                         >
                             <div>
                                 <SectionHeader>
-                                    What is your most important dashboard?
+                                    {t`What is your most important dashboard?`}
                                 </SectionHeader>
                                 <GuideDetailEditor
                                     type="dashboard"
@@ -267,15 +264,15 @@ export default class GettingStartedGuideEditForm extends Component {
                         <GuideEditSection
                             isCollapsed={important_metrics.length === 0}
                             isDisabled={!metrics || Object.keys(metrics).length === 0}
-                            collapsedTitle="Do you have any commonly referenced metrics?"
+                            collapsedTitle={t`Do you have any commonly referenced metrics?`}
                             collapsedIcon="ruler"
-                            linkMessage="Learn how to define a metric"
+                            linkMessage={t`Learn how to define a metric`}
                             link="http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html#creating-a-metric"
                             expand={() => important_metrics.addField({id: null, caveats: null, points_of_interest: null, important_fields: null})}
                         >
                             <div className="my2">
                                 <SectionHeader>
-                                    What are your 3-5 most commonly referenced metrics?
+                                    {t`What are your 3-5 most commonly referenced metrics?`}
                                 </SectionHeader>
                                 <div>
                                     { important_metrics.map((metricField, index, metricFields) =>
@@ -310,7 +307,7 @@ export default class GettingStartedGuideEditForm extends Component {
                                             type="button"
                                             onClick={() => important_metrics.addField({id: null, caveats: null, points_of_interest: null})}
                                         >
-                                            Add another metric
+                                            {t`Add another metric`}
                                         </button>
                                 }
                             </div>
@@ -320,16 +317,15 @@ export default class GettingStartedGuideEditForm extends Component {
                             isCollapsed={important_segments_and_tables.length === 0}
                             isDisabled={(!segments || Object.keys(segments).length === 0) && (!tables || Object.keys(tables).length === 0)}
                             showLink={!segments || Object.keys(segments).length === 0}
-                            collapsedTitle="Do you have any commonly referenced segments or tables?"
+                            collapsedTitle={t`Do you have any commonly referenced segments or tables?`}
                             collapsedIcon="table2"
-                            linkMessage="Learn how to create a segment"
+                            linkMessage={t`Learn how to create a segment`}
                             link="http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html#creating-a-segment"
                             expand={() => important_segments_and_tables.addField({id: null, type: null, caveats: null, points_of_interest: null})}
                         >
                             <div>
                                 <h2 className="text-measure text-dark">
-                                    What are 3-5 commonly referenced segments or tables
-                                    that would be useful for this audience?
+                                    {t`What are 3-5 commonly referenced segments or tables that would be useful for this audience?`}
                                 </h2>
                                 <div className="mb2">
                                     { important_segments_and_tables.map((segmentOrTableField, index, segmentOrTableFields) =>
@@ -362,7 +358,7 @@ export default class GettingStartedGuideEditForm extends Component {
                                             type="button"
                                             onClick={() => important_segments_and_tables.addField({id: null, type: null, caveats: null, points_of_interest: null})}
                                         >
-                                            Add another segment or table
+                                            {t`Add another segment or table`}
                                         </button>
                                 }
                             </div>
@@ -371,20 +367,17 @@ export default class GettingStartedGuideEditForm extends Component {
                         <GuideEditSection
                             isCollapsed={things_to_know.value === null}
                             isDisabled={false}
-                            collapsedTitle="Is there anything your users should understand or know before they start accessing the data?"
+                            collapsedTitle={t`Is there anything your users should understand or know before they start accessing the data?`}
                             collapsedIcon="reference"
                             expand={() => things_to_know.onChange('')}
                         >
                             <div className="text-measure">
                                 <SectionHeader>
-                                    What should a user of this data know before they start
-                                    accessing it?
+                                    {t`What should a user of this data know before they start accessing it?`}
                                 </SectionHeader>
                                 <textarea
                                     className={S.guideDetailEditorTextarea}
-                                    placeholder="E.g., expectations around data privacy and use,
-                                        common pitfalls or misunderstandings, information about
-                                        data warehouse performance, legal notices, etc."
+                                    placeholder={t`E.g., expectations around data privacy and use, common pitfalls or misunderstandings, information about data warehouse performance, legal notices, etc.`}
                                     {...things_to_know}
                                 />
                             </div>
@@ -393,7 +386,7 @@ export default class GettingStartedGuideEditForm extends Component {
                         <GuideEditSection
                             isCollapsed={contact.name.value === null && contact.email.value === null}
                             isDisabled={false}
-                            collapsedTitle="Is there someone your users could contact for help if they're confused about this guide?"
+                            collapsedTitle={t`Is there someone your users could contact for help if they're confused about this guide?`}
                             collapsedIcon="mail"
                             expand={() => {
                                 contact.name.onChange('');
@@ -402,11 +395,11 @@ export default class GettingStartedGuideEditForm extends Component {
                         >
                             <div>
                                 <SectionHeader>
-                                    Who should users contact for help if they're confused about this data?
+                                    {t`Who should users contact for help if they're confused about this data?`}
                                 </SectionHeader>
                                 <div className="flex">
                                     <div className="flex-full">
-                                        <h3 className="mb1">Name</h3>
+                                        <h3 className="mb1">{t`Name`}</h3>
                                         <input
                                             className="input text-paragraph"
                                             placeholder="Julie McHelpfulson"
@@ -415,7 +408,7 @@ export default class GettingStartedGuideEditForm extends Component {
                                         />
                                     </div>
                                     <div className="flex-full">
-                                        <h3 className="mb1">Email address</h3>
+                                        <h3 className="mb1">{t`Email address`}</h3>
                                         <input
                                             className="input text-paragraph"
                                             placeholder="julie.mchelpfulson@acme.com"
diff --git a/frontend/src/metabase/reference/metrics/MetricDetail.jsx b/frontend/src/metabase/reference/metrics/MetricDetail.jsx
index 981fef4ed3348e3b40a04834d6b86ab402faba13..be4694c8f6cf29151c6a7a503561a95809e9d216 100644
--- a/frontend/src/metabase/reference/metrics/MetricDetail.jsx
+++ b/frontend/src/metabase/reference/metrics/MetricDetail.jsx
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
 import { push } from "react-router-redux";
-
+import { t } from 'c-3po';
 import List from "metabase/components/List.jsx";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
 import EditHeader from "metabase/reference/components/EditHeader.jsx";
@@ -71,7 +71,7 @@ const mapDispatchToProps = {
 };
 
 const validate = (values, props) =>  !values.revision_message ? 
-    { revision_message: "Please enter a revision message" } : {} 
+    { revision_message: t`Please enter a revision message` } : {} 
 
 @connect(mapStateToProps, mapDispatchToProps)
 @reduxForm({
@@ -153,7 +153,7 @@ export default class MetricDetail extends Component {
                     type="metric"
                     headerIcon="ruler"
                     headerLink={getQuestionUrl({ dbId: table && table.db_id, tableId: entity.table_id, metricId: entity.id})}
-                    name="Details"
+                    name={t`Details`}
                     user={user}
                     isEditing={isEditing}
                     hasSingleSchema={false}
@@ -169,9 +169,9 @@ export default class MetricDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="description"
-                                    name="Description"
+                                    name={t`Description`}
                                     description={entity.description}
-                                    placeholder="No description yet"
+                                    placeholder={t`No description yet`}
                                     isEditing={isEditing}
                                     field={description}
                                 />
@@ -179,9 +179,9 @@ export default class MetricDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="points_of_interest"
-                                    name="Why this Metric is interesting"
+                                    name={t`Why this Metric is interesting`}
                                     description={entity.points_of_interest}
-                                    placeholder="Nothing interesting yet"
+                                    placeholder={t`Nothing interesting yet`}
                                     isEditing={isEditing}
                                     field={points_of_interest}
                                     />
@@ -189,9 +189,9 @@ export default class MetricDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="caveats"
-                                    name="Things to be aware of about this Metric"
+                                    name={t`Things to be aware of about this Metric`}
                                     description={entity.caveats}
-                                    placeholder="Nothing to be aware of yet"
+                                    placeholder={t`Nothing to be aware of yet`}
                                     isEditing={isEditing}
                                     field={caveats}
                                 />
@@ -199,9 +199,9 @@ export default class MetricDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="how_is_this_calculated"
-                                    name="How this Metric is calculated"
+                                    name={t`How this Metric is calculated`}
                                     description={entity.how_is_this_calculated}
-                                    placeholder="Nothing on how it's calculated yet"
+                                    placeholder={t`Nothing on how it's calculated yet`}
                                     isEditing={isEditing}
                                     field={how_is_this_calculated}
                                 />
@@ -245,8 +245,8 @@ export default class MetricDetail extends Component {
                                         databaseId={table && table.db_id}
                                         metric={entity}
                                         title={ guide && guide.metric_important_fields[entity.id] ?
-                                            "Other fields you can group this metric by" :
-                                            "Fields you can group this metric by"
+                                            t`Other fields you can group this metric by` :
+                                            t`Fields you can group this metric by`
                                         }
                                         onChangeLocation={onChangeLocation}
                                     />
diff --git a/frontend/src/metabase/reference/metrics/MetricList.jsx b/frontend/src/metabase/reference/metrics/MetricList.jsx
index b61989b147b1b6fa10900dbb28ff4a5ee7feb329..d7df6aeb2a59a7cdb0312e47caca1d7d0787310f 100644
--- a/frontend/src/metabase/reference/metrics/MetricList.jsx
+++ b/frontend/src/metabase/reference/metrics/MetricList.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-
+import { t } from 'c-3po';
 import { isQueryable } from "metabase/lib/table";
 
 import S from "metabase/components/List.css";
@@ -25,11 +25,11 @@ import * as metadataActions from "metabase/redux/metadata";
 
 
 const emptyStateData = {
-    title: "Metrics are the official numbers that your team cares about",
-    adminMessage: "Defining common metrics for your team makes it even easier to ask questions",
-    message: "Metrics will appear here once your admins have created some",
+    title: t`Metrics are the official numbers that your team cares about`,
+    adminMessage: t`Defining common metrics for your team makes it even easier to ask questions`,
+    message: t`Metrics will appear here once your admins have created some`,
     image: "app/assets/img/metrics-list",
-    adminAction: "Learn how to create metrics",
+    adminAction: t`Learn how to create metrics`,
     adminLink: "http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html"
 }
 
@@ -63,8 +63,8 @@ export default class MetricList extends Component {
 
         return (
             <div style={style} className="full">
-                <ReferenceHeader 
-                    name="Metrics"
+                <ReferenceHeader
+                    name={t`Metrics`}
                 />
                 <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
                 { () => Object.keys(entities).length > 0 ?
diff --git a/frontend/src/metabase/reference/metrics/MetricQuestions.jsx b/frontend/src/metabase/reference/metrics/MetricQuestions.jsx
index 759abf19695807d4ab15594a7a55f754538b5ed8..262a0bfbc16d6509e1a2aa1d59b7a7088e53f95f 100644
--- a/frontend/src/metabase/reference/metrics/MetricQuestions.jsx
+++ b/frontend/src/metabase/reference/metrics/MetricQuestions.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import moment from "moment";
-
+import { t } from 'c-3po';
 import visualizations from "metabase/visualizations";
 import { isQueryable } from "metabase/lib/table";
 import * as Urls from "metabase/lib/urls";
@@ -34,9 +34,9 @@ import * as metadataActions from "metabase/redux/metadata";
 
 const emptyStateData = (table, metric) => {
     return {
-        message: "Questions about this metric will appear here as they're added",
+        message: t`Questions about this metric will appear here as they're added`,
         icon: "all",
-        action: "Ask a question",
+        action: t`Ask a question`,
         link: getQuestionUrl({
             dbId: table && table.db_id,
             tableId: metric.table_id,
@@ -81,7 +81,7 @@ export default class MetricQuestions extends Component {
         return (
             <div style={style} className="full">
                 <ReferenceHeader 
-                    name={`Questions about ${this.props.metric.name}`}
+                    name={t`Questions about ${this.props.metric.name}`}
                     type="questions"
                     headerIcon="ruler"
                 />
@@ -97,7 +97,7 @@ export default class MetricQuestions extends Component {
                                                 id={entity.id}
                                                 index={index}
                                                 name={entity.display_name || entity.name}
-                                                description={ `Created ${moment(entity.created_at).fromNow()} by ${entity.creator.common_name}` }
+                                                description={ t`Created ${moment(entity.created_at).fromNow()} by ${entity.creator.common_name}` }
                                                 url={ Urls.question(entity.id) }
                                                 icon={ visualizations.get(entity.display).iconName }
                                             />
diff --git a/frontend/src/metabase/reference/metrics/MetricRevisions.jsx b/frontend/src/metabase/reference/metrics/MetricRevisions.jsx
index f9dcbf7af14725e4b5d3bcaabac14b3485720fce..c2af974958069e96fbdd145cc3d1aeb25f9e70ea 100644
--- a/frontend/src/metabase/reference/metrics/MetricRevisions.jsx
+++ b/frontend/src/metabase/reference/metrics/MetricRevisions.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-
+import { t } from 'c-3po';
 import { getIn } from "icepick";
 
 import S from "metabase/components/List.css";
@@ -27,7 +27,7 @@ import ReferenceHeader from "../components/ReferenceHeader.jsx";
 
 
 const emptyStateData =  {
-    message: "There are no revisions for this metric"
+    message: t`There are no revisions for this metric`
 }
 
 const mapStateToProps = (state, props) => {
@@ -83,7 +83,7 @@ export default class MetricRevisions extends Component {
         return (
             <div style={style} className="full">
                 <ReferenceHeader 
-                    name={`Revision history for ${this.props.metric.name}`}
+                    name={t`Revision history for ${this.props.metric.name}`}
                     headerIcon="ruler"
                 />
                 <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
diff --git a/frontend/src/metabase/reference/metrics/MetricSidebar.jsx b/frontend/src/metabase/reference/metrics/MetricSidebar.jsx
index 3803507470966b3e83b3aa63cd84480f4f467be6..5e0fe9e5686e4f10c5cbc2f344c7ac3cc302c503 100644
--- a/frontend/src/metabase/reference/metrics/MetricSidebar.jsx
+++ b/frontend/src/metabase/reference/metrics/MetricSidebar.jsx
@@ -2,7 +2,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 import S from "metabase/components/Sidebar.css";
-
+import { t } from 'c-3po';
 import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
 import SidebarItem from "metabase/components/SidebarItem.jsx"
 
@@ -20,26 +20,26 @@ const MetricSidebar = ({
             <div className={S.breadcrumbs}>
                 <Breadcrumbs
                     className="py4"
-                    crumbs={[["Metrics","/reference/metrics"],
+                    crumbs={[[t`Metrics`,"/reference/metrics"],
                              [metric.name]]}
                     inSidebar={true}
-                    placeholder="Data Reference"
+                    placeholder={t`Data Reference`}
                 />
             </div>
                 <SidebarItem key={`/reference/metrics/${metric.id}`} 
                              href={`/reference/metrics/${metric.id}`} 
                              icon="document" 
-                             name="Details" />
+                             name={t`Details`} />
                 <SidebarItem key={`/reference/metrics/${metric.id}/questions`} 
                              href={`/reference/metrics/${metric.id}/questions`} 
                              icon="all" 
-                             name={`Questions about ${metric.name}`} />
+                             name={t`Questions about ${metric.name}`} />
              { user && user.is_superuser &&
 
                 <SidebarItem key={`/reference/metrics/${metric.id}/revisions`}
                              href={`/reference/metrics/${metric.id}/revisions`}
                              icon="history" 
-                             name={`Revision history for ${metric.name}`} />
+                             name={t`Revision history for ${metric.name}`} />
              }
         </ul>
     </div>
diff --git a/frontend/src/metabase/reference/segments/SegmentDetail.jsx b/frontend/src/metabase/reference/segments/SegmentDetail.jsx
index 4bce042b45f24f8363818a644ec17e41d5a680e9..c69d8ad07f11888f9051a7cc456853bf435c287a 100644
--- a/frontend/src/metabase/reference/segments/SegmentDetail.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentDetail.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
-
+import { t } from 'c-3po';
 import List from "metabase/components/List.jsx";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
 
@@ -35,7 +35,7 @@ import * as actions from 'metabase/reference/reference';
 const interestingQuestions = (table, segment) => {
     return [
         {
-            text: `Number of ${segment.name}`,
+            text: t`Number of ${segment.name}`,
             icon: { name: "number", scale: 1, viewBox: "8 8 16 16" },
             link: getQuestionUrl({
                 dbId: table && table.db_id,
@@ -45,7 +45,7 @@ const interestingQuestions = (table, segment) => {
             })
         },
         {
-            text: `See all ${segment.name}`,
+            text: t`See all ${segment.name}`,
             icon: "table2",
             link: getQuestionUrl({
                 dbId: table && table.db_id,
@@ -90,7 +90,7 @@ const mapDispatchToProps = {
 };
 
 const validate = (values, props) =>  !values.revision_message ? 
-    { revision_message: "Please enter a revision message" } : {} 
+    { revision_message: t`Please enter a revision message` } : {} 
 
 
 @connect(mapStateToProps, mapDispatchToProps)
@@ -167,7 +167,7 @@ export default class SegmentDetail extends Component {
                     type="segment"
                     headerIcon="segment"
                     headerLink={getQuestionUrl({ dbId: table&&table.db_id, tableId: entity.table_id, segmentId: entity.id})}
-                    name="Details"
+                    name={t`Details`}
                     user={user}
                     isEditing={isEditing}
                     hasSingleSchema={false}
@@ -183,9 +183,9 @@ export default class SegmentDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="description"
-                                    name="Description"
+                                    name={t`Description`}
                                     description={entity.description}
-                                    placeholder="No description yet"
+                                    placeholder={t`No description yet`}
                                     isEditing={isEditing}
                                     field={description}
                                 />
@@ -193,9 +193,9 @@ export default class SegmentDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="points_of_interest"
-                                    name={`Why this Segment is interesting`}
+                                    name={t`Why this Segment is interesting`}
                                     description={entity.points_of_interest}
-                                    placeholder="Nothing interesting yet"
+                                    placeholder={t`Nothing interesting yet`}
                                     isEditing={isEditing}
                                     field={points_of_interest}
                                     />
@@ -203,9 +203,9 @@ export default class SegmentDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="caveats"
-                                    name={`Things to be aware of about this Segment`}
+                                    name={t`Things to be aware of about this Segment`}
                                     description={entity.caveats}
-                                    placeholder="Nothing to be aware of yet"
+                                    placeholder={t`Nothing to be aware of yet`}
                                     isEditing={isEditing}
                                     field={caveats}
                                 />
diff --git a/frontend/src/metabase/reference/segments/SegmentFieldDetail.jsx b/frontend/src/metabase/reference/segments/SegmentFieldDetail.jsx
index bfc652b22a2b1de0817640a35a3aa8ec3b693610..022f029560cd348303732708f1fb0828202116d3 100644
--- a/frontend/src/metabase/reference/segments/SegmentFieldDetail.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentFieldDetail.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
-
+import { t } from 'c-3po';
 import S from "metabase/reference/Reference.css";
 
 import List from "metabase/components/List.jsx";
@@ -38,7 +38,7 @@ import * as actions from 'metabase/reference/reference';
 const interestingQuestions = (table, field) => {
     return [
         {
-            text: `Number of ${table && table.display_name} grouped by ${field.display_name}`,
+            text: t`Number of ${table && table.display_name} grouped by ${field.display_name}`,
             icon: { name: "number", scale: 1, viewBox: "8 8 16 16" },
             link: getQuestionUrl({
                 dbId: table && table.db_id,
@@ -48,7 +48,7 @@ const interestingQuestions = (table, field) => {
             })
         },
         {
-            text: `All distinct values of ${field.display_name}`,
+            text: t`All distinct values of ${field.display_name}`,
             icon: "table2",
             link: getQuestionUrl({
                 dbId: table && table.db_id,
@@ -165,7 +165,7 @@ export default class SegmentFieldDetail extends Component {
                     entity={entity}
                     table={table}
                     headerIcon="field"
-                    name="Details"
+                    name={t`Details`}
                     type="field"
                     user={user}
                     isEditing={isEditing}
@@ -182,9 +182,9 @@ export default class SegmentFieldDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="description"
-                                    name="Description"
+                                    name={t`Description`}
                                     description={entity.description}
-                                    placeholder="No description yet"
+                                    placeholder={t`No description yet`}
                                     isEditing={isEditing}
                                     field={description}
                                 />
@@ -193,7 +193,7 @@ export default class SegmentFieldDetail extends Component {
                                 <li className="relative">
                                     <Detail
                                         id="name"
-                                        name="Actual name in database"
+                                        name={t`Actual name in database`}
                                         description={entity.name}
                                         subtitleClass={S.tableActualName}
                                     />
@@ -202,9 +202,9 @@ export default class SegmentFieldDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="points_of_interest"
-                                    name={`Why this field is interesting`}
+                                    name={t`Why this field is interesting`}
                                     description={entity.points_of_interest}
-                                    placeholder="Nothing interesting yet"
+                                    placeholder={t`Nothing interesting yet`}
                                     isEditing={isEditing}
                                     field={points_of_interest}
                                     />
@@ -212,9 +212,9 @@ export default class SegmentFieldDetail extends Component {
                             <li className="relative">
                                 <Detail
                                     id="caveats"
-                                    name={`Things to be aware of about this field`}
+                                    name={t`Things to be aware of about this field`}
                                     description={entity.caveats}
-                                    placeholder="Nothing to be aware of yet"
+                                    placeholder={t`Nothing to be aware of yet`}
                                     isEditing={isEditing}
                                     field={caveats}
                                 />
@@ -225,7 +225,7 @@ export default class SegmentFieldDetail extends Component {
                                 <li className="relative">
                                     <Detail
                                         id="base_type"
-                                        name={`Data type`}
+                                        name={t`Data type`}
                                         description={entity.base_type}
                                     />
                                 </li>
diff --git a/frontend/src/metabase/reference/segments/SegmentFieldList.jsx b/frontend/src/metabase/reference/segments/SegmentFieldList.jsx
index b2c25f9308f521b6326d8cdb8478394032213d93..0cbc96fde6d42f086ec9247c448c92468e3248e6 100644
--- a/frontend/src/metabase/reference/segments/SegmentFieldList.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentFieldList.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
-
+import { t } from 'c-3po';
 import S from "metabase/components/List.css";
 import R from "metabase/reference/Reference.css";
 import F from "metabase/reference/components/Field.css"
@@ -39,7 +39,7 @@ import * as actions from 'metabase/reference/reference';
 
 
 const emptyStateData = {
-    message: `Fields in this table will appear here as they're added`,
+    message: t`Fields in this table will appear here as they're added`,
     icon: "fields"
 }
 
@@ -129,7 +129,7 @@ export default class SegmentFieldList extends Component {
                 <EditableReferenceHeader 
                     type="segment"
                     headerIcon="segment"
-                    name={`Fields in ${segment.name}`}
+                    name={t`Fields in ${segment.name}`}
                     user={user} 
                     isEditing={isEditing} 
                     startEditing={startEditing} 
@@ -140,13 +140,13 @@ export default class SegmentFieldList extends Component {
                         <div className={S.item}>
                             <div className={R.columnHeader}>
                                 <div className={cx(S.itemTitle, F.fieldNameTitle)}>
-                                    Field name
+                                    {t`Field name`}
                                 </div>
                                 <div className={cx(S.itemTitle, F.fieldType)}>
-                                    Field type
+                                    {t`Field type`}
                                 </div>
                                 <div className={cx(S.itemTitle, F.fieldDataType)}>
-                                    Data type
+                                    {t`Data type`}
                                 </div>
                             </div>
                         </div>
diff --git a/frontend/src/metabase/reference/segments/SegmentFieldSidebar.jsx b/frontend/src/metabase/reference/segments/SegmentFieldSidebar.jsx
index 3a4788f09428752a5dfb9965a0464073c1f19684..f3f6ded65a286ca4ec16ac01e59c109873470750 100644
--- a/frontend/src/metabase/reference/segments/SegmentFieldSidebar.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentFieldSidebar.jsx
@@ -2,7 +2,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 import S from "metabase/components/Sidebar.css";
-
+import { t } from 'c-3po';
 import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
 import SidebarItem from "metabase/components/SidebarItem.jsx"
 
@@ -20,17 +20,17 @@ const SegmentFieldSidebar = ({
             <div className={S.breadcrumbs}>
                 <Breadcrumbs
                     className="py4"
-                    crumbs={[["Segments","/reference/segments"],
+                    crumbs={[[t`Segments`,"/reference/segments"],
                              [segment.name, `/reference/segments/${segment.id}`],
                              [field.name]]}
                     inSidebar={true}
-                    placeholder="Data Reference"
+                    placeholder={t`Data Reference`}
                 />
             </div>
                 <SidebarItem key={`/reference/segments/${segment.id}/fields/${field.id}`} 
                              href={`/reference/segments/${segment.id}/fields/${field.id}`} 
                              icon="document" 
-                             name="Details" />
+                             name={t`Details`} />
         </ul>
     </div>
 
diff --git a/frontend/src/metabase/reference/segments/SegmentList.jsx b/frontend/src/metabase/reference/segments/SegmentList.jsx
index 15859838279199d286cfadc398bcbba47e77fc35..6db7bb7f6550df2ca003a9851406842fc7949b2c 100644
--- a/frontend/src/metabase/reference/segments/SegmentList.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentList.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-
+import { t } from 'c-3po';
 import { isQueryable } from "metabase/lib/table";
 
 import S from "metabase/components/List.css";
@@ -24,13 +24,13 @@ import {
 import * as metadataActions from "metabase/redux/metadata";
 
 const emptyStateData = {
-            title: "Segments are interesting subsets of tables",
-            adminMessage: "Defining common segments for your team makes it even easier to ask questions",
-            message: "Segments will appear here once your admins have created some",
-            image: "app/assets/img/segments-list",
-            adminAction: "Learn how to create segments",
-            adminLink: "http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html"
-        }
+    title: t`Segments are interesting subsets of tables`,
+    adminMessage: t`Defining common segments for your team makes it even easier to ask questions`,
+    message: t`Segments will appear here once your admins have created some`,
+    image: "app/assets/img/segments-list",
+    adminAction: t`Learn how to create segments`,
+    adminLink: "http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html"
+}
 
 const mapStateToProps = (state, props) => ({
     entities: getSegments(state, props),
@@ -63,7 +63,7 @@ export default class SegmentList extends Component {
         return (
             <div style={style} className="full">
                 <ReferenceHeader
-                    name="Segments"
+                    name={t`Segments`}
                 />
                 <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
                 { () => Object.keys(entities).length > 0 ?
diff --git a/frontend/src/metabase/reference/segments/SegmentQuestions.jsx b/frontend/src/metabase/reference/segments/SegmentQuestions.jsx
index ffef951c2565956d76af291d8121f4d247c6019b..2ca1c5c80ad8608c8b539f6bf301eb005c00000a 100644
--- a/frontend/src/metabase/reference/segments/SegmentQuestions.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentQuestions.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import moment from "moment";
-
+import { t } from 'c-3po';
 import visualizations from "metabase/visualizations";
 import { isQueryable } from "metabase/lib/table";
 import * as Urls from "metabase/lib/urls";
@@ -35,9 +35,9 @@ import * as metadataActions from "metabase/redux/metadata";
 
 const emptyStateData = (table, segment) =>{  
     return {
-        message: "Questions about this segment will appear here as they're added",
+        message: t`Questions about this segment will appear here as they're added`,
         icon: "all",
-        action: "Ask a question",
+        action: t`Ask a question`,
         link: getQuestionUrl({
             dbId: table && table.db_id,
             tableId: segment.table_id,
@@ -79,7 +79,7 @@ export default class SegmentQuestions extends Component {
         return (
             <div style={style} className="full">
                 <ReferenceHeader 
-                    name={`Questions about ${this.props.segment.name}`}
+                    name={t`Questions about ${this.props.segment.name}`}
                     type='questions'
                     headerIcon="segment"
                 />
@@ -95,7 +95,7 @@ export default class SegmentQuestions extends Component {
                                                     id={entity.id}
                                                     index={index}
                                                     name={entity.display_name || entity.name}
-                                                    description={ `Created ${moment(entity.created_at).fromNow()} by ${entity.creator.common_name}` }
+                                                    description={ t`Created ${moment(entity.created_at).fromNow()} by ${entity.creator.common_name}` }
                                                     url={ Urls.question(entity.id) }
                                                     icon={ visualizations.get(entity.display).iconName }
                                                 />
diff --git a/frontend/src/metabase/reference/segments/SegmentRevisions.jsx b/frontend/src/metabase/reference/segments/SegmentRevisions.jsx
index b4a8d396e415d02f7a3438ac0a1f04b125f987c1..16b25c00124e500c170aaa8b697b0e3c7dd98ec5 100644
--- a/frontend/src/metabase/reference/segments/SegmentRevisions.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentRevisions.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-
+import { t } from 'c-3po';
 import { getIn } from "icepick";
 
 import S from "metabase/components/List.css";
@@ -26,7 +26,7 @@ import EmptyState from "metabase/components/EmptyState.jsx";
 import ReferenceHeader from "../components/ReferenceHeader.jsx";
 
 const emptyStateData =  {
-    message: "There are no revisions for this segment"
+    message: t`There are no revisions for this segment`
 }
 
 const mapStateToProps = (state, props) => {
@@ -82,7 +82,7 @@ export default class SegmentRevisions extends Component {
         return (
             <div style={style} className="full">
                 <ReferenceHeader 
-                    name={`Revision history for ${this.props.segment.name}`}
+                    name={t`Revision history for ${this.props.segment.name}`}
                     headerIcon="segment"
                 />
                 <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
diff --git a/frontend/src/metabase/reference/segments/SegmentSidebar.jsx b/frontend/src/metabase/reference/segments/SegmentSidebar.jsx
index f560f36573f2ee0832419949eb8b2cf4926b94c0..e1e38ccfa86e172de03b4269463da89a548ecee8 100644
--- a/frontend/src/metabase/reference/segments/SegmentSidebar.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentSidebar.jsx
@@ -2,7 +2,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 import S from "metabase/components/Sidebar.css";
-
+import { t } from 'c-3po';
 import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
 import SidebarItem from "metabase/components/SidebarItem.jsx"
 
@@ -20,34 +20,34 @@ const SegmentSidebar = ({
             <div className={S.breadcrumbs}>
                 <Breadcrumbs
                     className="py4"
-                    crumbs={[["Segments","/reference/segments"],
+                    crumbs={[[t`Segments`,"/reference/segments"],
                              [segment.name]]}
                     inSidebar={true}
-                    placeholder="Data Reference"
+                    placeholder={t`Data Reference`}
                 />
             </div>
                 <SidebarItem key={`/reference/segments/${segment.id}`}
                              href={`/reference/segments/${segment.id}`}
                              icon="document"
-                             name="Details" />
+                             name={t`Details`} />
                 <SidebarItem key={`/reference/segments/${segment.id}/fields`}
                              href={`/reference/segments/${segment.id}/fields`}
                              icon="fields"
-                             name="Fields in this segment" />
+                             name={t`Fields in this segment`} />
                 <SidebarItem key={`/reference/segments/${segment.id}/questions`}
                              href={`/reference/segments/${segment.id}/questions`}
                              icon="all"
-                             name={`Questions about this segment`} />
+                             name={t`Questions about this segment`} />
                 <SidebarItem key={`/xray/segment/${segment.id}/approximate`}
                              href={`/xray/segment/${segment.id}/approximate`}
                              icon="all"
-                             name={`X-ray this segment`} />
+                             name={t`X-ray this segment`} />
              { user && user.is_superuser &&
 
                 <SidebarItem key={`/reference/segments/${segment.id}/revisions`}
                              href={`/reference/segments/${segment.id}/revisions`}
                              icon="history"
-                             name={`Revision history`} />
+                             name={t`Revision history`} />
              }
         </ul>
     </div>
diff --git a/frontend/src/metabase/setup/components/DatabaseConnectionStep.jsx b/frontend/src/metabase/setup/components/DatabaseConnectionStep.jsx
index a7ad59bd077de2f31872fa204da3cc7aa5f55069..a9c9f1fd0486cbcd56f64f9c34b366d2c68ce48c 100644
--- a/frontend/src/metabase/setup/components/DatabaseConnectionStep.jsx
+++ b/frontend/src/metabase/setup/components/DatabaseConnectionStep.jsx
@@ -1,7 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import StepTitle from './StepTitle.jsx'
 import CollapsedStep from "./CollapsedStep.jsx";
 
@@ -133,9 +133,9 @@ export default class DatabaseConnectionStep extends Component {
         let { engine, formError } = this.state;
         let engines = MetabaseSettings.get('engines');
 
-        let stepText = 'Add your data';
+        let stepText = t`Add your data`;
         if (activeStep > stepNumber) {
-            stepText = (databaseDetails === null) ? "I'll add my own data later" : 'Connecting to '+databaseDetails.name;
+            stepText = (databaseDetails === null) ? t`I'll add my own data later` : t`Connecting to ${databaseDetails.name}`;
         }
 
 
@@ -147,7 +147,7 @@ export default class DatabaseConnectionStep extends Component {
                     <StepTitle title={stepText} circleText={"2"} />
                     <div className="mb4">
                         <div style={{maxWidth: 600}} className="Form-field Form-offset">
-                            You’ll need some info about your database, like the username and password.  If you don’t have that right now, Metabase also comes with a sample dataset you can get started with.
+                            {t`You’ll need some info about your database, like the username and password. If you don’t have that right now, Metabase also comes with a sample dataset you can get started with.`}
                         </div>
 
                         <FormField fieldName="engine">
@@ -170,7 +170,7 @@ export default class DatabaseConnectionStep extends Component {
                           : null }
 
                           <div className="Form-field Form-offset">
-                              <a className="link" onClick={this.skipDatabase.bind(this)}>I'll add my data later</a>
+                              <a className="link" onClick={this.skipDatabase.bind(this)}>{t`I'll add my data later`}</a>
                           </div>
                     </div>
                 </section>
diff --git a/frontend/src/metabase/setup/components/DatabaseSchedulingStep.jsx b/frontend/src/metabase/setup/components/DatabaseSchedulingStep.jsx
index e03bd310c67d491b15ca61ad4872ac43a04cb0e6..716bf77142f75db4c853bc131e87c2469eb07986 100644
--- a/frontend/src/metabase/setup/components/DatabaseSchedulingStep.jsx
+++ b/frontend/src/metabase/setup/components/DatabaseSchedulingStep.jsx
@@ -1,7 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import StepTitle from './StepTitle.jsx'
 import CollapsedStep from "./CollapsedStep.jsx";
 
@@ -38,7 +38,7 @@ export default class DatabaseSchedulingStep extends Component {
         let { activeStep, databaseDetails, setActiveStep, stepNumber } = this.props;
         let { formError } = this.state;
 
-        let stepText = 'Control automatic scans';
+        let stepText = t`Control automatic scans`;
 
         const schedulingIcon =
             <Icon
@@ -60,7 +60,7 @@ export default class DatabaseSchedulingStep extends Component {
                                     formState={{ formError }}
                                     // Use saveDatabase both for db creation and updating
                                     save={this.schedulingDetailsCaptured}
-                                    submitButtonText={ "Next"}
+                                    submitButtonText={ t`Next` }
                                 />
                             </div>
                     </div>
diff --git a/frontend/src/metabase/setup/components/PreferencesStep.jsx b/frontend/src/metabase/setup/components/PreferencesStep.jsx
index 47fad2df8562af0bd0dd28dc66dd53b433bd527e..3d14f511ea9607cc5a825a7569d0e30453938c2b 100644
--- a/frontend/src/metabase/setup/components/PreferencesStep.jsx
+++ b/frontend/src/metabase/setup/components/PreferencesStep.jsx
@@ -1,7 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 import Toggle from "metabase/components/Toggle.jsx";
@@ -42,9 +42,9 @@ export default class PreferencesStep extends Component {
         let { activeStep, allowTracking, setupComplete, stepNumber, setActiveStep } = this.props;
         const { tag } = MetabaseSettings.get('version');
 
-        let stepText = 'Usage data preferences';
+        let stepText = t`Usage data preferences`;
         if (setupComplete) {
-            stepText = allowTracking ? "Thanks for helping us improve" : "We won't collect any usage events";
+            stepText = allowTracking ? t`Thanks for helping us improve` : t`We won't collect any usage events`;
         }
 
         if (activeStep !== stepNumber || setupComplete) {
@@ -55,29 +55,29 @@ export default class PreferencesStep extends Component {
                     <StepTitle title={stepText} circleText={"3"} />
                     <form onSubmit={this.formSubmitted.bind(this)} noValidate>
                         <div className="Form-field Form-offset">
-                            In order to help us improve Metabase, we'd like to collect certain data about usage through Google Analytics.  <a className="link" href={"http://www.metabase.com/docs/"+tag+"/information-collection.html"} target="_blank">Here's a full list of everything we track and why.</a>
+                            {t`In order to help us improve Metabase, we'd like to collect certain data about usage through Google Analytics.`} <a className="link" href={"http://www.metabase.com/docs/"+tag+"/information-collection.html"} target="_blank">{t`Here's a full list of everything we track and why.`}</a>
                         </div>
 
                         <div className="Form-field Form-offset mr4">
                             <div style={{borderWidth: "2px"}} className="flex align-center bordered rounded p2">
                                 <Toggle value={allowTracking} onChange={this.toggleTracking.bind(this)} className="inline-block" />
-                                <span className="ml1">Allow Metabase to anonymously collect usage events</span>
+                                <span className="ml1">{t`Allow Metabase to anonymously collect usage events`}</span>
                             </div>
                         </div>
 
                         { allowTracking ?
                             <div className="Form-field Form-offset">
                                 <ul style={{listStyle: "disc inside", lineHeight: "200%"}}>
-                                    <li>Metabase <span style={{fontWeight: "bold"}}>never</span> collects anything about your data or question results.</li>
-                                    <li>All collection is completely anonymous.</li>
-                                    <li>Collection can be turned off at any point in your admin settings.</li>
+                                    <li>{t`Metabase <span style={{fontWeight: "bold"}}>never</span> collects anything about your data or question results.`}</li>
+                                    <li>{t`All collection is completely anonymous.`}</li>
+                                    <li>{t`Collection can be turned off at any point in your admin settings.`}</li>
                                 </ul>
                             </div>
                         : null }
 
                         <div className="Form-actions">
                             <button className="Button Button--primary">
-                                Next
+                                {t`Next`}
                             </button>
                             { /* FIXME: <mb-form-message form="usageForm"></mb-form-message>*/ }
                         </div>
diff --git a/frontend/src/metabase/setup/components/Setup.jsx b/frontend/src/metabase/setup/components/Setup.jsx
index e863124f0d7a7feac351929ca12090dba04977fb..994410db63e44799db370c544acea95e2189cb7c 100644
--- a/frontend/src/metabase/setup/components/Setup.jsx
+++ b/frontend/src/metabase/setup/components/Setup.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import ReactDOM from "react-dom";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
-
+import { t } from 'c-3po';
 import LogoIcon from 'metabase/components/LogoIcon.jsx';
 import NewsletterForm from 'metabase/components/NewsletterForm.jsx';
 import MetabaseAnalytics from "metabase/lib/analytics";
@@ -42,7 +42,7 @@ export default class Setup extends Component {
         const { tag } = MetabaseSettings.get('version');
         return (
             <div className="SetupHelp bordered border-dashed p2 rounded mb4" >
-                If you feel stuck, <a className="link" href={"http://www.metabase.com/docs/"+tag+"/setting-up-metabase"} target="_blank">our getting started guide</a> is just a click away.
+                {t`If you feel stuck`}, <a className="link" href={"http://www.metabase.com/docs/"+tag+"/setting-up-metabase"} target="_blank">{t`our getting started guide`}</a> {t`is just a click away.`}
             </div>
         );
     }
@@ -68,9 +68,9 @@ export default class Setup extends Component {
                     <div className="wrapper wrapper--trim text-centered">
                         <LogoIcon className="text-brand mb4" width={89} height={118}></LogoIcon>
                         <div className="relative z2 text-centered ml-auto mr-auto" style={{maxWidth: 550}}>
-                            <h1 style={{fontSize: '2.2rem'}} className="text-brand">Welcome to Metabase</h1>
-                            <p className="text-body">Looks like everything is working. Now let’s get to know you, connect to your data, and start finding you some answers!</p>
-                            <button className="Button Button--primary mt4" onClick={() => (this.completeWelcome())}>Let's get started</button>
+                            <h1 style={{fontSize: '2.2rem'}} className="text-brand">{t`Welcome to Metabase`}</h1>
+                            <p className="text-body">{t`Looks like everything is working. Now let’s get to know you, connect to your data, and start finding you some answers!`}</p>
+                            <button className="Button Button--primary mt4" onClick={() => (this.completeWelcome())}>{t`Let's get started`}</button>
                         </div>
                         <div className="absolute z1 bottom left right">
                             <div className="inline-block">
@@ -104,12 +104,12 @@ export default class Setup extends Component {
 
                             { setupComplete ?
                                 <section className="SetupStep rounded SetupStep--active flex flex-column layout-centered p4">
-                                    <h1 style={{fontSize: "xx-large"}} className="text-light pt2 pb2">You're all set up!</h1>
+                                    <h1 style={{fontSize: "xx-large"}} className="text-light pt2 pb2">{t`You're all set up!`}</h1>
                                     <div className="pt4">
                                         <NewsletterForm initialEmail={userDetails && userDetails.email} />
                                     </div>
                                     <div className="pt4 pb2">
-                                        <Link to="/?new" className="Button Button--primary" onClick={this.completeSetup.bind(this)}>Take me to Metabase</Link>
+                                        <Link to="/?new" className="Button Button--primary" onClick={this.completeSetup.bind(this)}>{t`Take me to Metabase`}</Link>
                                     </div>
                                 </section>
                             : null }
diff --git a/frontend/src/metabase/setup/components/UserStep.jsx b/frontend/src/metabase/setup/components/UserStep.jsx
index 3c909f3ea45e1391ff1bb122671a5368d28edde6..3608bfb0579774d1c970d9ceb331b9bce6ee6d66 100644
--- a/frontend/src/metabase/setup/components/UserStep.jsx
+++ b/frontend/src/metabase/setup/components/UserStep.jsx
@@ -1,7 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import FormField from "metabase/components/form/FormField.jsx";
 import FormLabel from "metabase/components/form/FormLabel.jsx";
 import FormMessage from "metabase/components/form/FormMessage.jsx";
@@ -95,14 +95,14 @@ export default class UserStep extends Component {
 
         // validate email address
         if (!MetabaseUtils.validEmail(fieldValues.email)) {
-            formErrors.data.errors.email = "Not a valid formatted email address";
+            formErrors.data.errors.email = t`Not a valid formatted email address`;
         }
 
         // TODO - validate password complexity
 
         // validate password match
         if (fieldValues.password !== fieldValues.password_confirm) {
-            formErrors.data.errors.password_confirm = "Passwords do not match";
+            formErrors.data.errors.password_confirm = t`Passwords do not match`;
         }
 
         if (_.keys(formErrors.data.errors).length > 0) {
@@ -141,7 +141,7 @@ export default class UserStep extends Component {
         let { formError, passwordError, valid } = this.state;
 
         const passwordComplexityDesc = MetabaseSettings.passwordComplexity();
-        const stepText = (activeStep <= stepNumber) ? 'What should we call you?' : 'Hi, ' + userDetails.first_name + '. nice to meet you!';
+        const stepText = (activeStep <= stepNumber) ? t`What should we call you?` : t`Hi, ${userDetails.first_name}. nice to meet you!`;
 
         if (activeStep !== stepNumber) {
             return (<CollapsedStep stepNumber={stepNumber} stepCircleText="1" stepText={stepText} isCompleted={activeStep > stepNumber} setActiveStep={setActiveStep}></CollapsedStep>)
@@ -152,45 +152,45 @@ export default class UserStep extends Component {
                     <form name="userForm" onSubmit={this.formSubmitted} noValidate className="mt2">
                         <FormField className="Grid mb3" fieldName="first_name" formError={formError}>
                             <div>
-                                <FormLabel title="First name" fieldName="first_name" formError={formError}></FormLabel>
+                                <FormLabel title={t`First name`} fieldName="first_name" formError={formError}></FormLabel>
                                 <input className="Form-input Form-offset full" name="first_name" defaultValue={(userDetails) ? userDetails.first_name : ""} placeholder="Johnny" required autoFocus={true} onChange={this.onFirstNameChange} />
                                 <span className="Form-charm"></span>
                             </div>
                             <div>
-                                <FormLabel title="Last name" fieldName="last_name" formError={formError}></FormLabel>
+                                <FormLabel title={t`Last name`} fieldName="last_name" formError={formError}></FormLabel>
                                 <input className="Form-input Form-offset" name="last_name" defaultValue={(userDetails) ? userDetails.last_name : ""} placeholder="Appleseed" required onChange={this.onLastNameChange} />
                                 <span className="Form-charm"></span>
                             </div>
                         </FormField>
 
                         <FormField fieldName="email" formError={formError}>
-                            <FormLabel title="Email address" fieldName="email" formError={formError}></FormLabel>
+                            <FormLabel title={t`Email address`} fieldName="email" formError={formError}></FormLabel>
                             <input className="Form-input Form-offset full" name="email" defaultValue={(userDetails) ? userDetails.email : ""} placeholder="youlooknicetoday@email.com" required onChange={this.onEmailChange} />
                             <span className="Form-charm"></span>
                         </FormField>
 
                         <FormField fieldName="password" formError={formError} error={(passwordError !== null)}>
-                            <FormLabel title="Create a password" fieldName="password" formError={formError} message={passwordError}></FormLabel>
+                            <FormLabel title={t`Create a password`} fieldName="password" formError={formError} message={passwordError}></FormLabel>
                             <span style={{fontWeight: "normal"}} className="Form-label Form-offset">{passwordComplexityDesc}</span>
-                            <input className="Form-input Form-offset full" name="password" type="password" defaultValue={(userDetails) ? userDetails.password : ""} placeholder="Shhh..." required onChange={this.onPasswordChange} onBlur={this.onPasswordBlur}/>
+                            <input className="Form-input Form-offset full" name="password" type="password" defaultValue={(userDetails) ? userDetails.password : ""} placeholder={t`Shhh...`} required onChange={this.onPasswordChange} onBlur={this.onPasswordBlur}/>
                             <span className="Form-charm"></span>
                         </FormField>
 
                         <FormField fieldName="password_confirm" formError={formError}>
-                            <FormLabel title="Confirm password" fieldName="password_confirm" formError={formError}></FormLabel>
-                            <input className="Form-input Form-offset full" name="password_confirm" type="password" defaultValue={(userDetails) ? userDetails.password : ""} placeholder="Shhh... but one more time so we get it right" required onChange={this.onPasswordConfirmChange} />
+                            <FormLabel title={t`Confirm password`} fieldName="password_confirm" formError={formError}></FormLabel>
+                            <input className="Form-input Form-offset full" name="password_confirm" type="password" defaultValue={(userDetails) ? userDetails.password : ""} placeholder={t`Shhh... but one more time so we get it right`} required onChange={this.onPasswordConfirmChange} />
                             <span className="Form-charm"></span>
                         </FormField>
 
                         <FormField fieldName="site_name" formError={formError}>
-                            <FormLabel title="Your company or team name" fieldName="site_name" formError={formError}></FormLabel>
-                            <input className="Form-input Form-offset full" name="site_name" type="text" defaultValue={(userDetails) ? userDetails.site_name : ""} placeholder="Department of awesome" required onChange={this.onSiteNameChange} />
+                            <FormLabel title={t`Your company or team name`} fieldName="site_name" formError={formError}></FormLabel>
+                            <input className="Form-input Form-offset full" name="site_name" type="text" defaultValue={(userDetails) ? userDetails.site_name : ""} placeholder={t`Department of awesome`} required onChange={this.onSiteNameChange} />
                             <span className="Form-charm"></span>
                         </FormField>
 
                         <div className="Form-actions">
                             <button className={cx("Button", {"Button--primary": valid})} disabled={!valid}>
-                                Next
+                                {t`Next`}
                             </button>
                             <FormMessage></FormMessage>
                         </div>
diff --git a/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx b/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx
index 2dc826cae1a60e15f0ba3adaec321a47e2e396b7..f8716c37c1e832c9b1d0e7a5be0e1f898323fb56 100644
--- a/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx
+++ b/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx
@@ -1,7 +1,7 @@
 /* eslint-disable react/display-name */
 
 import React, { Component } from "react";
-
+import { t } from 'c-3po';
 import Tutorial, { qs, qsWithContent } from "./Tutorial.jsx";
 
 import RetinaImage from "react-retina-image";
@@ -12,9 +12,9 @@ const QUERY_BUILDER_STEPS = [
         getModal: (props) =>
             <div className="text-centered">
                 <RetinaImage className="mb2" forceOriginalDimensions={false} src="app/assets/img/qb_tutorial/question_builder.png" width={186} />
-                <h3>Welcome to the Query Builder!</h3>
-                <p>The Query Builder lets you assemble questions (or "queries") to ask about your data.</p>
-                <a className="Button Button--primary" onClick={props.onNext}>Tell me more</a>
+                <h3>{t`Welcome to the Query Builder!`}</h3>
+                <p>{t`The Query Builder lets you assemble questions (or "queries") to ask about your data.`}</p>
+                <a className="Button Button--primary" onClick={props.onNext}>{t`Tell me more`}</a>
             </div>
     },
     {
@@ -23,8 +23,8 @@ const QUERY_BUILDER_STEPS = [
         getModal: (props) =>
             <div className="text-centered">
                 <RetinaImage id="QB-TutorialTableImg" className="mb2" forceOriginalDimensions={false} src="app/assets/img/qb_tutorial/table.png" width={157} />
-                <h3>Start by picking the table with the data that you have a question about.</h3>
-                <p>Go ahead and select the "Orders" table from the dropdown menu.</p>
+                <h3>{t`Start by picking the table with the data that you have a question about.`}</h3>
+                <p>{t`Go ahead and select the "Orders" table from the dropdown menu.`}</p>
             </div>,
         shouldAllowEvent: (e) => qs(".GuiBuilder-data a").contains(e.target)
     },
@@ -51,8 +51,8 @@ const QUERY_BUILDER_STEPS = [
                     src="app/assets/img/qb_tutorial/funnel.png"
                     width={135}
                 />
-                <h3>Filter your data to get just what you want.</h3>
-                <p>Click the plus button and select the "Created At" field.</p>
+                <h3>{t`Filter your data to get just what you want.`}</h3>
+                <p>{t`Click the plus button and select the "Created At" field.`}</p>
             </div>,
         shouldAllowEvent: (e) => qs(".GuiBuilder-filtered-by a").contains(e.target)
     },
@@ -63,7 +63,7 @@ const QUERY_BUILDER_STEPS = [
     },
     {
         getPortalTarget: () => qs(".GuiBuilder-filtered-by"),
-        getPageFlagText: () => "Here we can pick how many days we want to see data for, try 10",
+        getPageFlagText: () => t`Here we can pick how many days we want to see data for, try 10`,
         getPageFlagTarget: () => qs('[data-ui-tag="relative-date-input"]'),
         shouldAllowEvent: (e) => qs('[data-ui-tag="relative-date-input"]').contains(e.target)
     },
@@ -84,8 +84,8 @@ const QUERY_BUILDER_STEPS = [
                     src="app/assets/img/qb_tutorial/calculator.png"
                     width={115}
                 />
-                <h3>Here's where you can choose to add or average your data, count the number of rows in the table, or just view the raw data.</h3>
-                <p>Try it: click on <strong>Raw Data</strong> to change it to <strong>Count of rows</strong> so we can count how many orders there are in this table.</p>
+                <h3>{t`Here's where you can choose to add or average your data, count the number of rows in the table, or just view the raw data.`}</h3>
+                <p>{t`Try it: click on <strong>Raw Data</strong> to change it to <strong>Count of rows</strong> so we can count how many orders there are in this table.`}</p>
             </div>,
         shouldAllowEvent: (e) => qs('.View-section-aggregation').contains(e.target)
     },
@@ -106,15 +106,15 @@ const QUERY_BUILDER_STEPS = [
                     src="app/assets/img/qb_tutorial/banana.png"
                     width={232}
                 />
-                <h3>Add a grouping to break out your results by category, day, month, and more.</h3>
-                <p>Let's do it: click on <strong>Add a grouping</strong>, and choose <strong>Created At: by Week</strong>.</p>
+                <h3>{t`Add a grouping to break out your results by category, day, month, and more.`}</h3>
+                <p>{t`Let's do it: click on <strong>Add a grouping</strong>, and choose <strong>Created At: by Week</strong>.`}</p>
             </div>,
         shouldAllowEvent: (e) => qs('.Query-section-breakout').contains(e.target)
     },
     {
         getPortalTarget: () => qs(".Query-section-breakout"),
         getPageFlagTarget: () => qs(".FieldList-grouping-trigger"),
-        getPageFlagText: () => "Click on \"by day\" to change it to \"Week.\"",
+        getPageFlagText: () => t`Click on "by day" to change it to "Week."`,
         shouldAllowEvent: (e) => qs(".FieldList-grouping-trigger").contains(e.target)
     },
     {
@@ -134,8 +134,8 @@ const QUERY_BUILDER_STEPS = [
                     src="app/assets/img/qb_tutorial/rocket.png"
                     width={217}
                 />
-                <h3>Run Your Query.</h3>
-                <p>You're doing so well! Click <strong>Run query</strong> to get your results!</p>
+                <h3>{t`Run Your Query.`}</h3>
+                <p>{t`You're doing so well! Click <strong>Run query</strong> to get your results!`}</p>
             </div>,
         shouldAllowEvent: (e) => qs(".RunButton").contains(e.target)
     },
@@ -151,8 +151,8 @@ const QUERY_BUILDER_STEPS = [
                     src="app/assets/img/qb_tutorial/chart.png"
                     width={160}
                 />
-                <h3>You can view your results as a chart instead of a table.</h3>
-                <p>Everbody likes charts! Click the <strong>Visualization</strong> dropdown and select <strong>Line</strong>.</p>
+                <h3>{t`You can view your results as a chart instead of a table.`}</h3>
+                <p>{t`Everbody likes charts! Click the <strong>Visualization</strong> dropdown and select <strong>Line</strong>.`}</p>
             </div>,
         shouldAllowEvent: (e) => qs(".VisualizationSettings a").contains(e.target)
     },
@@ -171,18 +171,18 @@ const QUERY_BUILDER_STEPS = [
                     id="QB-TutorialBoatImg"
                     src="app/assets/img/qb_tutorial/boat.png" width={190}
                 />
-                <h3>Well done!</h3>
-                <p>That's all! If you still have questions, check out our <a className="link" target="_blank" href="http://www.metabase.com/docs/latest/users-guide/start">User's Guide</a>. Have fun exploring your data!</p>
-                <a className="Button Button--primary" onClick={props.onNext}>Thanks!</a>
+                <h3>{t`Well done!`}</h3>
+                <p>{t`That's all! If you still have questions, check out our`} <a className="link" target="_blank" href="http://www.metabase.com/docs/latest/users-guide/start">{t`User's Guide`}</a>. {t`Have fun exploring your data!`}</p>
+                <a className="Button Button--primary" onClick={props.onNext}>{t`Thanks`}!</a>
             </div>
     },
     {
         getModalTarget: () => qsWithContent(".Header-buttonSection a", "Save"),
         getModal: (props) =>
             <div className="text-centered">
-                <h3>Save Your Questions!</h3>
-                <p>By the way, you can save your questions so you can refer to them later. Saved Questions can also be put into dashboards or Pulses.</p>
-                <a className="Button Button--primary" onClick={props.onClose}>Sounds good</a>
+                <h3>{t`Save Your Questions`}!</h3>
+                <p>{t`By the way, you can save your questions so you can refer to them later. Saved Questions can also be put into dashboards or Pulses.`}</p>
+                <a className="Button Button--primary" onClick={props.onClose}>{t`Sounds good`}</a>
             </div>
     }
 ]
diff --git a/frontend/src/metabase/tutorial/Tutorial.jsx b/frontend/src/metabase/tutorial/Tutorial.jsx
index 81066e500a9f72c96f350a131ba899d48c484177..c112bb52d8fe05094080217384b4d26988be029e 100644
--- a/frontend/src/metabase/tutorial/Tutorial.jsx
+++ b/frontend/src/metabase/tutorial/Tutorial.jsx
@@ -1,5 +1,5 @@
 import React, { Component } from "react";
-
+import { t } from 'c-3po';
 import Modal from "metabase/components/Modal.jsx";
 import Popover from "metabase/components/Popover.jsx";
 
@@ -215,9 +215,9 @@ export default class Tutorial extends Component {
                         onClose={this.close}
                     >
                         <div className="text-centered">
-                            <h2>​Whoops!</h2>
-                            <p className="my2">Sorry, it looks like something went wrong. Please try restarting the tutorial in a minute.</p>
-                            <button className="Button Button--primary" onClick={this.close}>Okay</button>
+                            <h2>{t`Whoops!`}</h2>
+                            <p className="my2">{t`Sorry, it looks like something went wrong. Please try restarting the tutorial in a minute.`}</p>
+                            <button className="Button Button--primary" onClick={this.close}>{t`Okay`}</button>
                         </div>
                     </TutorialModal>
                 </Modal>
diff --git a/frontend/src/metabase/tutorial/TutorialModal.jsx b/frontend/src/metabase/tutorial/TutorialModal.jsx
index b1c2c73c5db9ebd689034bc2530fe5f450df5e5a..03a1849550e1c3c33ce359534e1f50aaef2aff7a 100644
--- a/frontend/src/metabase/tutorial/TutorialModal.jsx
+++ b/frontend/src/metabase/tutorial/TutorialModal.jsx
@@ -1,5 +1,5 @@
 import React, { Component } from "react";
-
+import { t } from 'c-3po';
 import Icon from "metabase/components/Icon.jsx";
 
 const ENABLE_BACK_BUTTON = false; // disabled due to possibility of getting in inconsistent states
@@ -21,7 +21,7 @@ export default class TutorialModal extends Component {
                 </div>
                 <div className="flex">
                     { showBackButton && <a className="text-grey-4 cursor-pointer" onClick={this.props.onBack}>back</a> }
-                    { showStepCount && <span className="text-grey-4 flex-align-right">{modalStepIndex + 1} of {modalStepCount}</span> }
+                    { showStepCount && <span className="text-grey-4 flex-align-right">{modalStepIndex + 1} {t`of`} {modalStepCount}</span> }
                 </div>
             </div>
         );
diff --git a/frontend/src/metabase/user/actions.js b/frontend/src/metabase/user/actions.js
index 6c01c55d570be2ba888c7c3ff3193e7dff7e8794..5d4aac8d8cce0ab204e3858ab4f83c167b27b140 100644
--- a/frontend/src/metabase/user/actions.js
+++ b/frontend/src/metabase/user/actions.js
@@ -1,5 +1,5 @@
 import { createAction } from "redux-actions";
-
+import { t } from 'c-3po';
 import { createThunkAction } from "metabase/lib/redux";
 
 import { UserApi } from "metabase/services";
@@ -28,7 +28,7 @@ export const updatePassword = createThunkAction(UPDATE_PASSWORD, function(user_i
             return {
                 success: true,
                 data:{
-                    message: "Password updated successfully!"
+                    message: t`Password updated successfully!`
                 }
             };
 
@@ -48,7 +48,7 @@ export const updateUser = createThunkAction(UPDATE_USER, function(user) {
             return {
                 success: true,
                 data:{
-                    message: "Account updated successfully!"
+                    message: t`Account updated successfully!`
                 }
             };
 
diff --git a/frontend/src/metabase/user/components/SetUserPassword.jsx b/frontend/src/metabase/user/components/SetUserPassword.jsx
index 85af677fc4ede5f7dbd02e211fb6086c2aa76831..aa63cc4926907e62d53eb5b0b23cae44112ffba7 100644
--- a/frontend/src/metabase/user/components/SetUserPassword.jsx
+++ b/frontend/src/metabase/user/components/SetUserPassword.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
-
+import { t } from 'c-3po';
 import FormField from "metabase/components/form/FormField.jsx";
 import FormLabel from "metabase/components/form/FormLabel.jsx";
 import FormMessage from "metabase/components/form/FormMessage.jsx";
@@ -62,7 +62,7 @@ export default class SetUserPassword extends Component {
 
         // make sure new passwords match
         if (ReactDOM.findDOMNode(this.refs.password).value !== ReactDOM.findDOMNode(this.refs.password2).value) {
-            formErrors.data.errors.password2 = "Passwords do not match";
+            formErrors.data.errors.password2 = t`Passwords do not match`;
         }
 
         if (_.keys(formErrors.data.errors).length > 0) {
@@ -92,27 +92,27 @@ export default class SetUserPassword extends Component {
             <div>
                 <form className="Form-new bordered rounded shadowed" onSubmit={this.formSubmitted.bind(this)} noValidate>
                     <FormField fieldName="old_password" formError={formError}>
-                        <FormLabel title="Current password" fieldName="old_password" formError={formError}></FormLabel>
-                        <input ref="oldPassword" type="password" className="Form-input Form-offset full" name="old_password" placeholder="Shhh..." onChange={this.onChange.bind(this)} autoFocus={true} required />
+                        <FormLabel title={t`Current password`} fieldName="old_password" formError={formError}></FormLabel>
+                        <input ref="oldPassword" type="password" className="Form-input Form-offset full" name="old_password" placeholder={t`Shhh...`} onChange={this.onChange.bind(this)} autoFocus={true} required />
                         <span className="Form-charm"></span>
                     </FormField>
 
                     <FormField fieldName="password" formError={formError}>
                         <FormLabel title="New password" fieldName="password" formError={formError} ></FormLabel>
                         <span style={{fontWeight: "400"}} className="Form-label Form-offset">{passwordComplexity}</span>
-                        <input ref="password" type="password" className="Form-input Form-offset full" name="password" placeholder="Make sure its secure like the instructions above" onChange={this.onChange.bind(this)} required />
+                        <input ref="password" type="password" className="Form-input Form-offset full" name="password" placeholder={t`Make sure its secure like the instructions above`} onChange={this.onChange.bind(this)} required />
                         <span className="Form-charm"></span>
                     </FormField>
 
                     <FormField fieldName="password2" formError={formError}>
                         <FormLabel title="Confirm new password" fieldName="password2" formError={formError} ></FormLabel>
-                        <input ref="password2" type="password" className="Form-input Form-offset full" name="password" placeholder="Make sure it matches the one you just entered" required onChange={this.onChange.bind(this)} />
+                        <input ref="password2" type="password" className="Form-input Form-offset full" name="password" placeholder={t`Make sure it matches the one you just entered`} required onChange={this.onChange.bind(this)} />
                         <span className="Form-charm"></span>
                     </FormField>
 
                     <div className="Form-actions">
                         <button className={cx("Button", {"Button--primary": valid})} disabled={!valid}>
-                            Save
+                            {t`Save`}
                         </button>
                         <FormMessage formError={(updatePasswordResult && !updatePasswordResult.success && !formError) ? updatePasswordResult : undefined} formSuccess={(updatePasswordResult && updatePasswordResult.success) ? updatePasswordResult : undefined} />
                     </div>
diff --git a/frontend/src/metabase/user/components/UpdateUserDetails.jsx b/frontend/src/metabase/user/components/UpdateUserDetails.jsx
index f58c3c5ec05fe8ff75a8278a1e7a0b1df6258b12..7442b7e8a70cf0a71b59be8bd0c80d4e11180537 100644
--- a/frontend/src/metabase/user/components/UpdateUserDetails.jsx
+++ b/frontend/src/metabase/user/components/UpdateUserDetails.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
-
+import { t } from 'c-3po';
 import FormField from "metabase/components/form/FormField.jsx";
 import FormLabel from "metabase/components/form/FormLabel.jsx";
 import FormMessage from "metabase/components/form/FormMessage.jsx";
@@ -61,7 +61,7 @@ export default class UpdateUserDetails extends Component {
 
         // validate email address
         if (!MetabaseUtils.validEmail(ReactDOM.findDOMNode(this.refs.email).value)) {
-            formErrors.data.errors.email = "Not a valid formatted email address";
+            formErrors.data.errors.email = t`Not a valid formatted email address`;
         }
 
         if (_.keys(formErrors.data.errors).length > 0) {
@@ -89,19 +89,19 @@ export default class UpdateUserDetails extends Component {
             <div>
                 <form className="Form-new bordered rounded shadowed" onSubmit={this.formSubmitted.bind(this)} noValidate>
                     <FormField fieldName="first_name" formError={formError}>
-                        <FormLabel title="First name" fieldName="first_name" formError={formError}></FormLabel>
+                        <FormLabel title={t`First name`} fieldName="first_name" formError={formError}></FormLabel>
                         <input ref="firstName" className="Form-input Form-offset full" name="name" defaultValue={(user) ? user.first_name : null} placeholder="Johnny" onChange={this.onChange.bind(this)} />
                         <span className="Form-charm"></span>
                     </FormField>
 
                     <FormField fieldName="last_name" formError={formError}>
-                        <FormLabel title="Last name" fieldName="last_name" formError={formError} ></FormLabel>
+                        <FormLabel title={t`Last name`} fieldName="last_name" formError={formError} ></FormLabel>
                         <input ref="lastName" className="Form-input Form-offset full" name="name" defaultValue={(user) ? user.last_name : null} placeholder="Appleseed" required onChange={this.onChange.bind(this)} />
                         <span className="Form-charm"></span>
                     </FormField>
 
                     <FormField fieldName="email" formError={formError}>
-                        <FormLabel title={ user.google_auth ? "Sign in with Google Email address" : "Email address"} fieldName="email" formError={formError} ></FormLabel>
+                        <FormLabel title={ user.google_auth ? t`Sign in with Google Email address` : t`Email address`} fieldName="email" formError={formError} ></FormLabel>
                         <input
                             ref="email"
                             className={
@@ -122,7 +122,7 @@ export default class UpdateUserDetails extends Component {
 
                     <div className="Form-actions">
                         <button className={cx("Button", {"Button--primary": valid})} disabled={!valid}>
-                            Save
+                            {t`Save`}
                         </button>
                         <FormMessage formError={(updateUserResult && !updateUserResult.success) ? updateUserResult : undefined} formSuccess={(updateUserResult && updateUserResult.success) ? updateUserResult : undefined} />
                     </div>
diff --git a/frontend/src/metabase/user/components/UserSettings.jsx b/frontend/src/metabase/user/components/UserSettings.jsx
index ed1ade9083e471000ce283e301c0bad2b8784c38..97235318600d6312e11b38cad13cb8e585efb434 100644
--- a/frontend/src/metabase/user/components/UserSettings.jsx
+++ b/frontend/src/metabase/user/components/UserSettings.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
-
+import { t } from 'c-3po';
 import SetUserPassword from "./SetUserPassword.jsx";
 import UpdateUserDetails from "./UpdateUserDetails.jsx";
 
@@ -44,7 +44,7 @@ export default class UserSettings extends Component {
             <div>
                 <div className="py4 border-bottom">
                     <div className="wrapper wrapper--trim">
-                        <h2 className="text-grey-4">Account settings</h2>
+                        <h2 className="text-grey-4">{t`Account settings`}</h2>
                     </div>
                 </div>
                 <div className="mt2 md-mt4 wrapper wrapper--trim">
@@ -53,12 +53,12 @@ export default class UserSettings extends Component {
                             <div className="Grid-cell Grid Grid--fit md-flex-column md-Cell--1of3">
                               <a className={cx(tabClasses['details'])}
                                 onClick={this.onSetTab.bind(this, 'details')}>
-                                User Details
+                                {t`User Details`}
                               </a>
 
                               <a className={cx(tabClasses['password'])}
                                 onClick={this.onSetTab.bind(this, 'password')}>
-                                Password
+                                {t`Password`}
                               </a>
                             </div>
                         )}
diff --git a/frontend/src/metabase/visualizations/components/ChartSettings.jsx b/frontend/src/metabase/visualizations/components/ChartSettings.jsx
index 79f6d96f76e13d19eefb75619b121eb72b317e02..24b5d576000be7067f75810062fecd0e2094b710 100644
--- a/frontend/src/metabase/visualizations/components/ChartSettings.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartSettings.jsx
@@ -2,7 +2,7 @@ import React, { Component } from "react";
 import cx from "classnames";
 import { assocIn } from "icepick";
 import _ from "underscore";
-
+import { t } from 'c-3po';
 import Warnings from "metabase/query_builder/components/Warnings.jsx";
 
 import Visualization from "metabase/visualizations/components/Visualization.jsx"
@@ -121,7 +121,7 @@ class ChartSettings extends Component {
 
         return (
             <div className="flex flex-column spread p4">
-                <h2 className="my2">Customize this {this.getChartTypeName()}</h2>
+                <h2 className="my2">{t`Customize this ${this.getChartTypeName()}`}</h2>
 
                 { tabNames.length > 1 &&
                     <ChartSettingsTabs tabs={tabNames} selectTab={this.selectTab} activeTab={currentTab}/>
@@ -152,12 +152,12 @@ class ChartSettings extends Component {
                 </div>
                 <div className="pt1">
                     { !_.isEqual(this.state.settings, {}) &&
-                        <a className="Button Button--danger float-right" onClick={this.onResetSettings} data-metabase-event="Chart Settings;Reset">Reset to defaults</a>
+                        <a className="Button Button--danger float-right" onClick={this.onResetSettings} data-metabase-event="Chart Settings;Reset">{t`Reset to defaults`}</a>
                     }
 
                     <div className="float-left">
-                      <a className="Button Button--primary ml2" onClick={() => this.onDone()} data-metabase-event="Chart Settings;Done">Done</a>
-                      <a className="Button ml2" onClick={onClose} data-metabase-event="Chart Settings;Cancel">Cancel</a>
+                      <a className="Button Button--primary ml2" onClick={() => this.onDone()} data-metabase-event="Chart Settings;Done">{t`Done`}</a>
+                      <a className="Button ml2" onClick={onClose} data-metabase-event="Chart Settings;Cancel">{t`Cancel`}</a>
                     </div>
                 </div>
             </div>
diff --git a/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx b/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx
index 49c96054693a1888f2ce8a11b9f111f899ab9e81..eef5eb7503bbd2390464a0fa503ef0dd2986e515 100644
--- a/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx
+++ b/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx
@@ -1,5 +1,5 @@
 import React, { Component } from "react";
-
+import { t } from 'c-3po';
 import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
 
 import { isString } from "metabase/lib/schema_metadata";
@@ -114,7 +114,7 @@ export default class ChoroplethMap extends Component {
         const details = this._getDetails(this.props);
         if (!details) {
             return (
-                <div>unknown map</div>
+                <div>{t`unknown map`}</div>
             );
         }
 
diff --git a/frontend/src/metabase/visualizations/components/LeafletGridHeatMap.jsx b/frontend/src/metabase/visualizations/components/LeafletGridHeatMap.jsx
index cfd816ebd7419284af9fec4c359482c4a6c9cbe1..d4f122ceb18b5b9c4cca6d087f8d9591099fd57d 100644
--- a/frontend/src/metabase/visualizations/components/LeafletGridHeatMap.jsx
+++ b/frontend/src/metabase/visualizations/components/LeafletGridHeatMap.jsx
@@ -1,6 +1,6 @@
 import LeafletMap from "./LeafletMap.jsx";
 import L from "leaflet";
-
+import { t } from 'c-3po';
 import d3 from "d3";
 
 import { rangeForValue } from "metabase/lib/dataset";
@@ -22,7 +22,7 @@ export default class LeafletGridHeatMap extends LeafletMap {
 
             const { latitudeColumn, longitudeColumn } = this._getLatLonColumns();
             if (!latitudeColumn.binning_info || !longitudeColumn.binning_info) {
-                throw new Error("Grid map requires binned longitude/latitude.");
+                throw new Error(t`Grid map requires binned longitude/latitude.`);
             }
 
             const color = d3.scale.linear().domain([min,max])
diff --git a/frontend/src/metabase/visualizations/components/LegendVertical.jsx b/frontend/src/metabase/visualizations/components/LegendVertical.jsx
index 4d0cf771e2a966aa3f541546e10a1a69fd88aaf8..41b027628bcf1a6aae95f4e9049b31e3a73d24b8 100644
--- a/frontend/src/metabase/visualizations/components/LegendVertical.jsx
+++ b/frontend/src/metabase/visualizations/components/LegendVertical.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import ReactDOM from "react-dom";
 import styles from "./Legend.css";
-
+import { t } from 'c-3po';
 import Tooltip from "metabase/components/Tooltip.jsx";
 
 import LegendItem from "./LegendItem.jsx";
@@ -82,7 +82,7 @@ export default class LegendVertical extends Component {
                     <li key="extra" className="flex flex-no-shrink" >
                         <Tooltip tooltip={<LegendVertical className="p2" titles={extraItems} colors={extraColors} />}>
                             <LegendItem
-                                title={(overflowCount + 1) + " more"}
+                                title={(overflowCount + 1) + " " + t`more`}
                                 color="gray"
                                 showTooltip={false}
                             />
diff --git a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
index 84db634bbc5fb41b57986e119b0f5f6d2a105289..0d7c4a910935757aa0ec86c8af670388569b45c5 100644
--- a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
+++ b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
@@ -2,7 +2,7 @@
 
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-
+import { t } from 'c-3po';
 import CardRenderer from "./CardRenderer.jsx";
 import LegendHeader from "./LegendHeader.jsx";
 import { TitleLegendHeader } from "./TitleLegendHeader.jsx";
@@ -68,7 +68,7 @@ export default class LineAreaBarChart extends Component {
         const dimensions = (settings["graph.dimensions"] || []).filter(name => name);
         const metrics = (settings["graph.metrics"] || []).filter(name => name);
         if (dimensions.length < 1 || metrics.length < 1) {
-            throw new ChartSettingsError("Which fields do you want to use for the X and Y axes?", "Data", "Choose fields");
+            throw new ChartSettingsError(t`Which fields do you want to use for the X and Y axes?`, t`Data`, t`Choose fields`);
         }
     }
 
diff --git a/frontend/src/metabase/visualizations/components/PinMap.jsx b/frontend/src/metabase/visualizations/components/PinMap.jsx
index be8035193338c16e89204336e64dcc6e1949f707..220626d05b94011396b71aec25ec8bd38c156c52 100644
--- a/frontend/src/metabase/visualizations/components/PinMap.jsx
+++ b/frontend/src/metabase/visualizations/components/PinMap.jsx
@@ -1,7 +1,7 @@
 /* @flow */
 
 import React, { Component } from "react";
-
+import { t } from 'c-3po';
 import { hasLatitudeAndLongitudeColumns } from "metabase/lib/schema_metadata";
 import { LatitudeLongitudeError } from "metabase/visualizations/lib/errors";
 
@@ -44,7 +44,7 @@ export default class PinMap extends Component {
     props: Props;
     state: State;
 
-    static uiName = "Pin Map";
+    static uiName = t`Pin Map`;
     static identifier = "pin_map";
     static iconName = "pinmap";
 
@@ -169,7 +169,7 @@ export default class PinMap extends Component {
                 <div className="absolute top right m1 z2 flex flex-column hover-child">
                     { isEditing || !isDashboard ?
                         <div className={cx("PinMapUpdateButton Button Button--small mb1", { "PinMapUpdateButton--disabled": disableUpdateButton })} onClick={this.updateSettings}>
-                            Save as default view
+                            {t`Save as default view`}
                         </div>
                     : null }
                     { !isDashboard &&
@@ -183,7 +183,7 @@ export default class PinMap extends Component {
                                 }
                             }}
                         >
-                            { !this.state.filtering ? "Draw box to filter" : "Cancel filter" }
+                            { !this.state.filtering ? t`Draw box to filter` : t`Cancel filter` }
                         </div>
                     }
                 </div>
diff --git a/frontend/src/metabase/visualizations/components/TableInteractive.jsx b/frontend/src/metabase/visualizations/components/TableInteractive.jsx
index 6271fde23fb58701c7819cbd4a77e14cb27d95a1..45bb412ecdf9486d2a6331ee091da64f4a3f3c7d 100644
--- a/frontend/src/metabase/visualizations/components/TableInteractive.jsx
+++ b/frontend/src/metabase/visualizations/components/TableInteractive.jsx
@@ -3,7 +3,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
-
+import { t } from 'c-3po';
 import "./TableInteractive.css";
 
 import Icon from "metabase/components/Icon.jsx";
@@ -255,7 +255,7 @@ export default class TableInteractive extends Component {
 
         let columnTitle = formatColumn(column);
         if (!columnTitle && this.props.isPivoted && columnIndex !== 0) {
-            columnTitle = "Unset";
+            columnTitle = t`Unset`;
         }
 
         let clicked;
diff --git a/frontend/src/metabase/visualizations/components/TableSimple.jsx b/frontend/src/metabase/visualizations/components/TableSimple.jsx
index 2e68740a628bc06cb363e2ac9dc979c00a474f98..8b5d9d077f59de73c39fc92504599e8bd4827e1b 100644
--- a/frontend/src/metabase/visualizations/components/TableSimple.jsx
+++ b/frontend/src/metabase/visualizations/components/TableSimple.jsx
@@ -4,7 +4,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import styles from "./Table.css";
-
+import { t } from 'c-3po';
 import ExplicitSize from "metabase/components/ExplicitSize.jsx";
 import Ellipsified from "metabase/components/Ellipsified.jsx";
 import Icon from "metabase/components/Icon.jsx";
@@ -148,7 +148,7 @@ export default class TableSimple extends Component {
                 </div>
                 { pageSize < rows.length ?
                     <div ref="footer" className="p1 flex flex-no-shrink flex-align-right fullscreen-normal-text fullscreen-night-text">
-                        <span className="text-bold">Rows {start + 1}-{end + 1} of {rows.length}</span>
+                        <span className="text-bold">{t`Rows ${start + 1}-${end + 1} of ${rows.length}`}</span>
                         <span className={cx("text-brand-hover px1 cursor-pointer", { disabled: start === 0 })} onClick={() => this.setState({ page: page - 1 })}>
                             <Icon name="left" size={10} />
                         </span>
diff --git a/frontend/src/metabase/visualizations/components/Visualization.jsx b/frontend/src/metabase/visualizations/components/Visualization.jsx
index f336f993a9ce5ee9f22143bf55bbe44ae423d67a..48039f7808f41d33a2fd64c56892b0179a398da9 100644
--- a/frontend/src/metabase/visualizations/components/Visualization.jsx
+++ b/frontend/src/metabase/visualizations/components/Visualization.jsx
@@ -9,7 +9,7 @@ import ChartClickActions from "metabase/visualizations/components/ChartClickActi
 import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
 import Icon from "metabase/components/Icon.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
-
+import { t, jt } from 'c-3po';
 import { duration, formatNumber } from "metabase/lib/formatting";
 import MetabaseAnalytics from "metabase/lib/analytics";
 
@@ -147,7 +147,7 @@ export default class Visualization extends Component {
         if (state.series[0].card.display !== "table") {
             warnings = warnings.concat(props.series
                 .filter(s => s.data && s.data.rows_truncated != null)
-                .map(s => `Data truncated to ${formatNumber(s.data.rows_truncated)} rows.`));
+                .map(s => t`Data truncated to ${formatNumber(s.data.rows_truncated)} rows.`));
         }
         return warnings;
     }
@@ -279,14 +279,14 @@ export default class Visualization extends Component {
         if (!loading && !error) {
             settings = this.props.settings || getSettings(series);
             if (!CardVisualization) {
-                error = "Could not find visualization";
+                error = t`Could not find visualization`;
             } else {
                 try {
                     if (CardVisualization.checkRenderable) {
                         CardVisualization.checkRenderable(series, settings);
                     }
                 } catch (e) {
-                    error = e.message || "Could not display this chart with this data.";
+                    error = e.message || t`Could not display this chart with this data.`;
                     if (e instanceof ChartSettingsError && this.props.onOpenChartSettings) {
                         error = (
                             <div>
@@ -353,7 +353,7 @@ export default class Visualization extends Component {
                 // on dashboards we should show the "No results!" warning if there are no rows or there's a MinRowsError and actualRows === 0
                 : isDashboard && noResults ?
                     <div className={"flex-full px1 pb1 text-centered flex flex-column layout-centered " + (isDashboard ? "text-slate-light" : "text-slate")}>
-                        <Tooltip tooltip="No results!" isEnabled={small}>
+                        <Tooltip tooltip={t`No results!`} isEnabled={small}>
                             <img src="../app/assets/img/no_results.svg" />
                         </Tooltip>
                         { !small &&
@@ -377,16 +377,16 @@ export default class Visualization extends Component {
                     <div className="flex-full p1 text-centered text-brand flex flex-column layout-centered">
                         { isSlow ?
                             <div className="text-slate">
-                                <div className="h4 text-bold mb1">Still Waiting...</div>
+                                <div className="h4 text-bold mb1">{t`Still Waiting...`}</div>
                                 { isSlow === "usually-slow" ?
                                     <div>
-                                        This usually takes an average of <span style={{whiteSpace: "nowrap"}}>{duration(expectedDuration)}</span>.
+                                        {jt`This usually takes an average of ${<span style={{whiteSpace: "nowrap"}}>{duration(expectedDuration)}</span>}.`}
                                         <br />
-                                        (This is a bit long for a dashboard)
+                                        {t`(This is a bit long for a dashboard)`}
                                     </div>
                                 :
                                     <div>
-                                        This is usually pretty fast, but seems to be taking awhile right now.
+                                        {t`This is usually pretty fast, but seems to be taking awhile right now.`}
                                     </div>
                                 }
                             </div>
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx
index 32b6a0c6cc4a339df7a9bd5294da23d70e28756d..7f69d72cfb877b89febacfd555b7d7873fae7712 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx
@@ -1,5 +1,5 @@
 import React from "react";
-
+import { t } from 'c-3po';
 import Icon from "metabase/components/Icon";
 import cx from "classnames";
 
@@ -11,8 +11,8 @@ const ChartSettingFieldPicker = ({ value, options, onChange, onRemove }) =>
             value={value}
             options={options}
             onChange={onChange}
-            placeholder="Select a field"
-            placeholderNoOptions="No valid fields"
+            placeholder={t`Select a field`}
+            placeholderNoOptions={t`No valid fields`}
             isInitiallyOpen={value === undefined}
         />
         <Icon
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPicker.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPicker.jsx
index 09463240b7e5ad01fb497df7bda19b2833540a93..8c5d0a4aa638aa9b70dc7c0a4e50de0fd26c1612 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPicker.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPicker.jsx
@@ -1,5 +1,5 @@
 import React from "react";
-
+import { t } from 'c-3po';
 import ChartSettingFieldPicker from "./ChartSettingFieldPicker.jsx";
 
 const ChartSettingFieldsPicker = ({ value = [], options, onChange, addAnother }) =>
@@ -25,7 +25,7 @@ const ChartSettingFieldsPicker = ({ value = [], options, onChange, addAnother })
                     null
                 }
             />
-        ) : <span className="text-error">error</span>}
+        ) : <span className="text-error">{t`error`}</span>}
         { addAnother &&
             <div className="mt1">
                 <a onClick={() => {
diff --git a/frontend/src/metabase/visualizations/index.js b/frontend/src/metabase/visualizations/index.js
index 3cc598688c6bcbd1ba34ca24e0488c49c7af69eb..8f3aa2d77dc6ef123ccfd59482f9e9a6750db6ce 100644
--- a/frontend/src/metabase/visualizations/index.js
+++ b/frontend/src/metabase/visualizations/index.js
@@ -12,7 +12,7 @@ import MapViz      from "./visualizations/Map.jsx";
 import ScatterPlot from "./visualizations/ScatterPlot.jsx";
 import Funnel      from "./visualizations/Funnel.jsx";
 import ObjectDetail from "./visualizations/ObjectDetail.jsx";
-
+import { t } from 'c-3po';
 import _ from "underscore";
 
 import type { Series } from "metabase/meta/types/Visualization";
@@ -27,10 +27,10 @@ visualizations.get = function(key) {
 export function registerVisualization(visualization) {
     let identifier = visualization.identifier;
     if (identifier == null) {
-        throw new Error("Visualization must define an 'identifier' static variable: " + visualization.name);
+        throw new Error(t`Visualization must define an 'identifier' static variable: ` + visualization.name);
     }
     if (visualizations.has(identifier)) {
-        throw new Error("Visualization with that identifier is already registered: " + visualization.name);
+        throw new Error(t`Visualization with that identifier is already registered: ` + visualization.name);
     }
     visualizations.set(identifier, visualization);
     for (let alias of visualization.aliases || []) {
@@ -56,7 +56,7 @@ export function getVisualizationTransformed(series: Series) {
     do {
         CardVisualization = visualizations.get(series[0].card.display);
         if (!CardVisualization) {
-            throw new Error("No visualization for " + series[0].card.display);
+            throw new Error(t`No visualization for ${series[0].card.display}`);
         }
         lastSeries = series;
         if (typeof CardVisualization.transformSeries === "function") {
diff --git a/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js b/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js
index b60cdfae1feb893a177de5ef485960f53ae7f8ba..e8c42a0c22cb8023aa3fc4113737de03b4eaf5fe 100644
--- a/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js
+++ b/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js
@@ -5,6 +5,7 @@ import d3 from "d3";
 import dc from "dc";
 import _ from "underscore";
 import { updateIn } from "icepick";
+import { t } from 'c-3po';
 
 import {
     computeSplit,
@@ -60,7 +61,7 @@ import type { VisualizationProps } from "metabase/meta/types/Visualization"
 const BAR_PADDING_RATIO = 0.2;
 const DEFAULT_INTERPOLATION = "linear";
 
-const UNAGGREGATED_DATA_WARNING = (col) => `"${getFriendlyName(col)}" is an unaggregated field: if it has more than one value at a point on the x-axis, the values will be summed.`
+const UNAGGREGATED_DATA_WARNING = (col) => t`"${getFriendlyName(col)}" is an unaggregated field: if it has more than one value at a point on the x-axis, the values will be summed.`
 
 const enableBrush = (series, onChangeCardAndRun) => !!(
     onChangeCardAndRun &&
@@ -73,11 +74,11 @@ const enableBrush = (series, onChangeCardAndRun) => !!(
 
 function checkSeriesIsValid({ series, maxSeries }) {
     if (getFirstNonEmptySeries(series).data.cols.length < 2) {
-        throw new Error("This chart type requires at least 2 columns.");
+        throw new Error(t`This chart type requires at least 2 columns.`);
     }
 
     if (series.length > maxSeries) {
-        throw new Error(`This chart type doesn't support more than ${maxSeries} series of data.`);
+        throw new Error(t`This chart type doesn't support more than ${maxSeries} series of data.`);
     }
 }
 
@@ -421,7 +422,7 @@ function addGoalChartAndGetOnGoalHover({ settings, onHoverChange }, xDomain, par
     return (element) => {
         onHoverChange(element && {
             element,
-            data: [{ key: "Goal", value: goalValue }]
+            data: [{ key: t`Goal`, value: goalValue }]
         });
     };
 }
diff --git a/frontend/src/metabase/visualizations/lib/errors.js b/frontend/src/metabase/visualizations/lib/errors.js
index f894ec7586706b5f44bc62095e86fb4c382c7fec..6b96ff3a8ba62d8ae61189c6b6cd3b4e611bd01a 100644
--- a/frontend/src/metabase/visualizations/lib/errors.js
+++ b/frontend/src/metabase/visualizations/lib/errors.js
@@ -1,24 +1,24 @@
 /* @flow */
 
 import { inflect } from "metabase/lib/formatting";
-
+import { t } from 'c-3po';
 // NOTE: extending Error with Babel requires babel-plugin-transform-builtin-extend
 
 export class MinColumnsError extends Error {
     constructor(minColumns: number, actualColumns: number) {
-        super(`Doh! The data from your query doesn't fit the chosen display choice. This visualization requires at least ${actualColumns} ${inflect("column", actualColumns)} of data.`);
+        super(t`Doh! The data from your query doesn't fit the chosen display choice. This visualization requires at least ${actualColumns} ${inflect("column", actualColumns)} of data.`);
     }
 }
 
 export class MinRowsError extends Error {
     constructor(minRows: number, actualRows: number) {
-        super(`No dice. We have ${actualRows} data ${inflect("point", actualRows)} to show and that's not enough for this visualization.`);
+        super(t`No dice. We have ${actualRows} data ${inflect("point", actualRows)} to show and that's not enough for this visualization.`);
     }
 }
 
 export class LatitudeLongitudeError extends Error {
     constructor() {
-        super("Bummer. We can't actually do a pin map for this data because we require both a latitude and longitude column.");
+        super(t`Bummer. We can't actually do a pin map for this data because we require both a latitude and longitude column.`);
     }
 }
 
@@ -26,8 +26,8 @@ export class ChartSettingsError extends Error {
     section: ?string;
     buttonText: ?string;
     constructor(message: string, section?: string, buttonText?: string) {
-        super(message || "Please configure this chart in the chart settings");
+        super(message || t`Please configure this chart in the chart settings`);
         this.section = section;
-        this.buttonText = buttonText || "Edit Settings";
+        this.buttonText = buttonText || t`Edit Settings`;
     }
 }
diff --git a/frontend/src/metabase/visualizations/lib/fill_data.js b/frontend/src/metabase/visualizations/lib/fill_data.js
index 718063ca9fed03ab1d83779d7ed62e97db63bd23..37f9ce63265321ee8c9754ad143f0032ff9500d9 100644
--- a/frontend/src/metabase/visualizations/lib/fill_data.js
+++ b/frontend/src/metabase/visualizations/lib/fill_data.js
@@ -29,7 +29,7 @@ function fillMissingValues(datas, xValues, fillValue, getKey = (v) => v) {
                 }
             });
             if (map.size > 0) {
-                console.warn("xValues missing!", map, newRows)
+                console.warn(t`"xValues missing!`, map, newRows)
             }
             return newRows;
         });
diff --git a/frontend/src/metabase/visualizations/lib/settings.js b/frontend/src/metabase/visualizations/lib/settings.js
index ca36d2b40d61e4ed6b4f988bbb748e8cbcb5e66b..8deb2e068dd3376c32bf7d23cb83dd36136abe91 100644
--- a/frontend/src/metabase/visualizations/lib/settings.js
+++ b/frontend/src/metabase/visualizations/lib/settings.js
@@ -1,5 +1,5 @@
 import { getVisualizationRaw } from "metabase/visualizations";
-
+import { t } from 'c-3po';
 import {
     columnsAreValid,
     getChartTypeFromData,
@@ -145,14 +145,14 @@ export function fieldSetting(id, filter, getDefault) {
 
 const COMMON_SETTINGS = {
     "card.title": {
-        title: "Title",
+        title: t`Title`,
         widget: "input",
         getDefault: (series) => series.length === 1 ? series[0].card.name : null,
         dashboard: true,
         useRawSeries: true
     },
     "card.description": {
-        title: "Description",
+        title: t`Description`,
         widget: "input",
         getDefault: (series) => series.length === 1 ? series[0].card.description : null,
         dashboard: true,
diff --git a/frontend/src/metabase/visualizations/lib/settings/graph.js b/frontend/src/metabase/visualizations/lib/settings/graph.js
index e581f27458971121ecc67724fb0a2048133b5c85..32687c7235b7838cc6494fead692524c4ee0a2be 100644
--- a/frontend/src/metabase/visualizations/lib/settings/graph.js
+++ b/frontend/src/metabase/visualizations/lib/settings/graph.js
@@ -1,6 +1,6 @@
 import { capitalize } from "metabase/lib/formatting";
 import { isDimension, isMetric, isNumeric, isAny } from "metabase/lib/schema_metadata";
-
+import { t } from 'c-3po';
 import { getDefaultColumns, getOptionFromColumn } from "metabase/visualizations/lib/settings";
 import { columnsAreValid, getCardColors, getFriendlyName } from "metabase/visualizations/lib/utils";
 import { dimensionIsNumeric } from "metabase/visualizations/lib/numeric";
@@ -27,7 +27,7 @@ export const GRAPH_DATA_SETTINGS = {
   },
   "graph.dimensions": {
       section: "Data",
-      title: "X-axis",
+      title: t`X-axis`,
       widget: "fields",
       isValid: ([{ card, data }], vizSettings) =>
           columnsAreValid(card.visualization_settings["graph.dimensions"], data, vizSettings["graph._dimension_filter"]) &&
@@ -40,7 +40,7 @@ export const GRAPH_DATA_SETTINGS = {
           return {
               options,
               addAnother: (options.length > value.length && value.length < 2 && vizSettings["graph.metrics"].length < 2) ?
-                  "Add a series breakout..." : null
+                  t`Add a series breakout...` : null
           };
       },
       readDependencies: ["graph._dimension_filter", "graph._metric_filter"],
@@ -50,7 +50,7 @@ export const GRAPH_DATA_SETTINGS = {
   },
   "graph.metrics": {
       section: "Data",
-      title: "Y-axis",
+      title: t`Y-axis`,
       widget: "fields",
       isValid: ([{ card, data }], vizSettings) =>
           columnsAreValid(card.visualization_settings["graph.dimensions"], data, vizSettings["graph._dimension_filter"]) &&
@@ -63,7 +63,7 @@ export const GRAPH_DATA_SETTINGS = {
           return {
               options,
               addAnother: options.length > value.length && vizSettings["graph.dimensions"].length < 2 ?
-                  "Add another series..." : null
+                  t`Add another series...` : null
           };
       },
       readDependencies: ["graph._dimension_filter", "graph._metric_filter"],
@@ -76,7 +76,7 @@ export const GRAPH_DATA_SETTINGS = {
 export const GRAPH_BUBBLE_SETTINGS = {
     "scatter.bubble": {
         section: "Data",
-        title: "Bubble size",
+        title: t`Bubble size`,
         widget: "field",
         isValid: ([{ card, data }], vizSettings) =>
             columnsAreValid([card.visualization_settings["scatter.bubble"]], data, isNumeric),
@@ -98,20 +98,20 @@ export const GRAPH_BUBBLE_SETTINGS = {
 export const LINE_SETTINGS = {
   "line.interpolate": {
       section: "Display",
-      title: "Style",
+      title: t`Style`,
       widget: "select",
       props: {
           options: [
-              { name: "Line", value: "linear" },
-              { name: "Curve", value: "cardinal" },
-              { name: "Step", value: "step-after" },
+              { name: t`Line`, value: "linear" },
+              { name: t`Curve`, value: "cardinal" },
+              { name: t`Step`, value: "step-after" },
           ]
       },
       getDefault: () => "linear"
   },
   "line.marker_enabled": {
       section: "Display",
-      title: "Show point markers on lines",
+      title: t`Show point markers on lines`,
       widget: "toggle"
   },
 }
@@ -119,13 +119,13 @@ export const LINE_SETTINGS = {
 export const STACKABLE_SETTINGS = {
   "stackable.stack_type": {
       section: "Display",
-      title: "Stacking",
+      title: t`Stacking`,
       widget: "radio",
       getProps: (series, vizSettings) => ({
           options: [
-              { name: "Don't stack", value: null },
-              { name: "Stack", value: "stacked" },
-              { name: "Stack - 100%", value: "normalized" }
+              { name: t`Don't stack`, value: null },
+              { name: t`Stack`, value: "stacked" },
+              { name: t`Stack - 100%`, value: "normalized" }
           ]
       }),
       getDefault: ([{ card, data }], vizSettings) =>
@@ -141,13 +141,13 @@ export const STACKABLE_SETTINGS = {
 export const GRAPH_GOAL_SETTINGS = {
   "graph.show_goal": {
       section: "Display",
-      title: "Show goal",
+      title: t`Show goal`,
       widget: "toggle",
       default: false
   },
   "graph.goal_value": {
       section: "Display",
-      title: "Goal value",
+      title: t`Goal value`,
       widget: "number",
       default: 0,
       getHidden: (series, vizSettings) => vizSettings["graph.show_goal"] !== true,
@@ -158,14 +158,14 @@ export const GRAPH_GOAL_SETTINGS = {
 export const LINE_SETTINGS_2 = {
   "line.missing": {
       section: "Display",
-      title: "Replace missing values with",
+      title: t`Replace missing values with`,
       widget: "select",
       default: "interpolate",
       getProps: (series, vizSettings) => ({
           options: [
-              { name: "Zero", value: "zero" },
-              { name: "Nothing", value: "none" },
-              { name: "Linear Interpolated", value: "interpolate" },
+              { name: t`Zero`, value: "zero" },
+              { name: t`Nothing`, value: "none" },
+              { name: t`Linear Interpolated`, value: "interpolate" },
           ]
       })
   },
@@ -204,7 +204,7 @@ export const GRAPH_AXIS_SETTINGS = {
   },
   "graph.x_axis.scale": {
       section: "Axes",
-      title: "X-axis scale",
+      title: t`X-axis scale`,
       widget: "select",
       default: "ordinal",
       readDependencies: [
@@ -220,61 +220,61 @@ export const GRAPH_AXIS_SETTINGS = {
       getProps: (series, vizSettings) => {
           const options = [];
           if (vizSettings["graph.x_axis._is_timeseries"]) {
-              options.push({ name: "Timeseries", value: "timeseries" });
+              options.push({ name: t`Timeseries`, value: "timeseries" });
           }
           if (vizSettings["graph.x_axis._is_numeric"]) {
-              options.push({ name: "Linear", value: "linear" });
+              options.push({ name: t`Linear`, value: "linear" });
               if (!vizSettings["graph.x_axis._is_histogram"]) {
-                  options.push({ name: "Power", value: "pow" });
-                  options.push({ name: "Log", value: "log" });
+                  options.push({ name: t`Power`, value: "pow" });
+                  options.push({ name: t`Log`, value: "log" });
               }
-              options.push({ name: "Histogram", value: "histogram" });
+              options.push({ name: t`Histogram`, value: "histogram" });
           }
-          options.push({ name: "Ordinal", value: "ordinal" });
+          options.push({ name: t`Ordinal`, value: "ordinal" });
           return { options };
       }
   },
   "graph.y_axis.scale": {
       section: "Axes",
-      title: "Y-axis scale",
+      title: t`Y-axis scale`,
       widget: "select",
       default: "linear",
       getProps: (series, vizSettings) => ({
           options: [
-              { name: "Linear", value: "linear" },
-              { name: "Power", value: "pow" },
-              { name: "Log", value: "log" }
+              { name: t`Linear`, value: "linear" },
+              { name: t`Power`, value: "pow" },
+              { name: t`Log`, value: "log" }
           ]
       })
   },
   "graph.x_axis.axis_enabled": {
       section: "Axes",
-      title: "Show x-axis line and marks",
+      title: t`Show x-axis line and marks`,
       widget: "toggle",
       default: true
   },
   "graph.y_axis.axis_enabled": {
       section: "Axes",
-      title: "Show y-axis line and marks",
+      title: t`Show y-axis line and marks`,
       widget: "toggle",
       default: true
   },
   "graph.y_axis.auto_range": {
       section: "Axes",
-      title: "Auto y-axis range",
+      title: t`Auto y-axis range`,
       widget: "toggle",
       default: true
   },
   "graph.y_axis.min": {
       section: "Axes",
-      title: "Min",
+      title: t`Min`,
       widget: "number",
       default: 0,
       getHidden: (series, vizSettings) => vizSettings["graph.y_axis.auto_range"] !== false
   },
   "graph.y_axis.max": {
       section: "Axes",
-      title: "Max",
+      title: t`Max`,
       widget: "number",
       default: 100,
       getHidden: (series, vizSettings) => vizSettings["graph.y_axis.auto_range"] !== false
@@ -282,20 +282,20 @@ export const GRAPH_AXIS_SETTINGS = {
 /*
   "graph.y_axis_right.auto_range": {
       section: "Axes",
-      title: "Auto right-hand y-axis range",
+      title: t`Auto right-hand y-axis range`,
       widget: "toggle",
       default: true
   },
   "graph.y_axis_right.min": {
       section: "Axes",
-      title: "Min",
+      title: t`Min`,
       widget: "number",
       default: 0,
       getHidden: (series, vizSettings) => vizSettings["graph.y_axis_right.auto_range"] !== false
   },
   "graph.y_axis_right.max": {
       section: "Axes",
-      title: "Max",
+      title: t`Max`,
       widget: "number",
       default: 100,
       getHidden: (series, vizSettings) => vizSettings["graph.y_axis_right.auto_range"] !== false
@@ -303,20 +303,20 @@ export const GRAPH_AXIS_SETTINGS = {
 */
   "graph.y_axis.auto_split": {
       section: "Axes",
-      title: "Use a split y-axis when necessary",
+      title: t`Use a split y-axis when necessary`,
       widget: "toggle",
       default: true,
       getHidden: (series) => series.length < 2
   },
   "graph.x_axis.labels_enabled": {
       section: "Labels",
-      title: "Show label on x-axis",
+      title: t`Show label on x-axis`,
       widget: "toggle",
       default: true
   },
   "graph.x_axis.title_text": {
       section: "Labels",
-      title: "X-axis label",
+      title: t`X-axis label`,
       widget: "input",
       getHidden: (series, vizSettings) =>
           vizSettings["graph.x_axis.labels_enabled"] === false,
@@ -325,13 +325,13 @@ export const GRAPH_AXIS_SETTINGS = {
   },
   "graph.y_axis.labels_enabled": {
       section: "Labels",
-      title: "Show label on y-axis",
+      title: t`Show label on y-axis`,
       widget: "toggle",
       default: true
   },
   "graph.y_axis.title_text": {
       section: "Labels",
-      title: "Y-axis label",
+      title: t`Y-axis label`,
       widget: "input",
       getHidden: (series, vizSettings) =>
           vizSettings["graph.y_axis.labels_enabled"] === false,
diff --git a/frontend/src/metabase/visualizations/lib/utils.js b/frontend/src/metabase/visualizations/lib/utils.js
index 7e95683e8c6e2f1c5714ecca8b3312c52fd8f52e..73d501edac2cc370b6ef6cd28ad8d63d2865cb87 100644
--- a/frontend/src/metabase/visualizations/lib/utils.js
+++ b/frontend/src/metabase/visualizations/lib/utils.js
@@ -3,7 +3,7 @@
 import React from "react";
 import _ from "underscore";
 import d3 from "d3";
-
+import { t } from 'c-3po';
 import crossfilter from "crossfilter";
 
 import * as colors from "metabase/lib/colors";
@@ -97,11 +97,11 @@ export function computeSplit(extents) {
 }
 
 const FRIENDLY_NAME_MAP = {
-    "avg": "Average",
-    "count": "Count",
-    "sum": "Sum",
-    "distinct": "Distinct",
-    "stddev": "Standard Deviation"
+    "avg": t`Average`,
+    "count": t`Count`,
+    "sum": t`Sum`,
+    "distinct": t`Distinct`,
+    "stddev": t`Standard Deviation`
 };
 
 export function getXValues(datas, chartType) {
diff --git a/frontend/src/metabase/visualizations/visualizations/AreaChart.jsx b/frontend/src/metabase/visualizations/visualizations/AreaChart.jsx
index 6043514557e20b730e858285f8f5da3c933b0ebc..495810557dcd94724c52e26edb8411e2aebb19fc 100644
--- a/frontend/src/metabase/visualizations/visualizations/AreaChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/AreaChart.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-
+import { t } from 'c-3po';
 import LineAreaBarChart from "../components/LineAreaBarChart.jsx";
 import { areaRenderer } from "../lib/LineAreaBarRenderer";
 
@@ -15,10 +15,10 @@ import {
 } from "../lib/settings/graph";
 
 export default class AreaChart extends LineAreaBarChart {
-    static uiName = "Area";
+    static uiName = t`Area`;
     static identifier = "area";
     static iconName = "area";
-    static noun = "area chart";
+    static noun = t`area chart`;
 
     static settings = {
         ...GRAPH_DATA_SETTINGS,
diff --git a/frontend/src/metabase/visualizations/visualizations/BarChart.jsx b/frontend/src/metabase/visualizations/visualizations/BarChart.jsx
index aa2b8bf74d93325e5929a4b2484331c37277dc61..084143bd3413e4cfaa0bc9beb2de8e97ea514e28 100644
--- a/frontend/src/metabase/visualizations/visualizations/BarChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/BarChart.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-
+import { t } from 'c-3po';
 import LineAreaBarChart from "../components/LineAreaBarChart.jsx";
 import { barRenderer } from "../lib/LineAreaBarRenderer";
 
@@ -13,10 +13,10 @@ import {
 } from "../lib/settings/graph";
 
 export default class BarChart extends LineAreaBarChart {
-    static uiName = "Bar";
+    static uiName = t`Bar`;
     static identifier = "bar";
     static iconName = "bar";
-    static noun = "bar chart";
+    static noun = t`bar chart`;
 
     static settings = {
         ...GRAPH_DATA_SETTINGS,
diff --git a/frontend/src/metabase/visualizations/visualizations/Funnel.jsx b/frontend/src/metabase/visualizations/visualizations/Funnel.jsx
index 113958feebc824305e5342676c9e26b06f2f7666..8c77fa47e8b15640b801b9d13450ccaf3e017ad6 100644
--- a/frontend/src/metabase/visualizations/visualizations/Funnel.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Funnel.jsx
@@ -1,7 +1,7 @@
 /* @flow */
 
 import React, { Component } from "react";
-
+import { t } from 'c-3po';
 import { MinRowsError, ChartSettingsError } from "metabase/visualizations/lib/errors";
 
 import { formatValue } from "metabase/lib/formatting";
@@ -21,7 +21,7 @@ import { TitleLegendHeader } from "metabase/visualizations/components/TitleLegen
 export default class Funnel extends Component {
     props: VisualizationProps;
 
-    static uiName = "Funnel";
+    static uiName = t`Funnel`;
     static identifier = "funnel";
     static iconName = "funnel";
 
@@ -46,31 +46,31 @@ export default class Funnel extends Component {
             throw new MinRowsError(1, rows.length);
         }
         if (!settings["funnel.dimension"] || !settings["funnel.metric"]) {
-            throw new ChartSettingsError("Which fields do you want to use?", "Data", "Choose fields");
+            throw new ChartSettingsError(t`Which fields do you want to use?`, t`Data`, t`Choose fields`);
         }
     }
 
     static settings = {
         "funnel.dimension": {
             section: "Data",
-            title: "Step",
+            title: t`Step`,
             ...dimensionSetting("funnel.dimension"),
             dashboard: false,
             useRawSeries: true,
         },
         "funnel.metric": {
             section: "Data",
-            title: "Measure",
+            title: t`Measure`,
             ...metricSetting("funnel.metric"),
             dashboard: false,
             useRawSeries: true,
         },
         "funnel.type": {
-            title: "Funnel type",
+            title: t`Funnel type`,
             section: "Display",
             widget: "select",
             props: {
-                options: [{ name: "Funnel", value: "funnel"}, { name: "Bar chart", value: "bar"}]
+                options: [{ name: t`Funnel`, value: "funnel"}, { name: t`Bar chart`, value: "bar"}]
             },
             // legacy "bar" funnel was only previously available via multiseries
             getDefault: (series) => series.length > 1 ? "bar" : "funnel",
diff --git a/frontend/src/metabase/visualizations/visualizations/LineChart.jsx b/frontend/src/metabase/visualizations/visualizations/LineChart.jsx
index 9b981abc43242dd86c0e7172f779db69e9e62f63..64a81a533a6a2e826b3c466fb359921bd338b2e7 100644
--- a/frontend/src/metabase/visualizations/visualizations/LineChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/LineChart.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-
+import { t } from 'c-3po';
 import LineAreaBarChart from "../components/LineAreaBarChart.jsx";
 import { lineRenderer } from "../lib/LineAreaBarRenderer";
 
@@ -14,10 +14,10 @@ import {
 } from "../lib/settings/graph";
 
 export default class LineChart extends LineAreaBarChart {
-    static uiName = "Line";
+    static uiName = t`Line`;
     static identifier = "line";
     static iconName = "line";
-    static noun = "line chart";
+    static noun = t`line chart`;
 
     static settings = {
         ...GRAPH_DATA_SETTINGS,
diff --git a/frontend/src/metabase/visualizations/visualizations/Map.jsx b/frontend/src/metabase/visualizations/visualizations/Map.jsx
index a48894ac2e9adbd8532fe079dcd2eea992376abb..0eccd185a46a43e982bf0e685557ede767acaa44 100644
--- a/frontend/src/metabase/visualizations/visualizations/Map.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Map.jsx
@@ -1,7 +1,7 @@
 /* @flow */
 
 import React, { Component } from "react";
-
+import { t } from 'c-3po';
 import ChoroplethMap from "../components/ChoroplethMap.jsx";
 import PinMap from "../components/PinMap.jsx";
 
@@ -19,7 +19,7 @@ import _ from "underscore";
 const PIN_MAP_TYPES = new Set(["pin"]);
 
 export default class Map extends Component {
-    static uiName = "Map";
+    static uiName = t`Map`;
     static identifier = "map";
     static iconName = "pinmap";
 
@@ -33,12 +33,12 @@ export default class Map extends Component {
 
     static settings = {
         "map.type": {
-            title: "Map type",
+            title: t`Map type`,
             widget: "select",
             props: {
                 options: [
-                    { name: "Region map", value: "region" },
-                    { name: "Pin map", value: "pin" }
+                    { name: t`Region map`, value: "region" },
+                    { name: t`Pin map`, value: "pin" }
                     // NOTE Atte Keinänen 8/2/17: Heat/grid maps disabled in the first merged version of binning
                     // { name: "Heat map", value: "heat" },
                     // { name: "Grid map", value: "grid" }
@@ -73,13 +73,13 @@ export default class Map extends Component {
             readDependencies: ["map.latitude_column", "map.longitude_column", "map.metric_column"]
         },
         "map.pin_type": {
-            title: "Pin type",
+            title: t`Pin type`,
             // Don't expose this in the UI for now
             // widget: "select",
             props: {
                 options: [
-                    { name: "Tiles", value: "tiles" },
-                    { name: "Markers", value: "markers" },
+                    { name: t`Tiles`, value: "tiles" },
+                    { name: t`Markers`, value: "markers" },
                     // NOTE Atte Keinänen 8/2/17: Heat/grid maps disabled in the first merged version of binning
                     // { name: "Heat", value: "heat" },
                     // { name: "Grid", value: "grid" }
@@ -97,19 +97,19 @@ export default class Map extends Component {
             getHidden: (series, vizSettings) => !PIN_MAP_TYPES.has(vizSettings["map.type"])
         },
         "map.latitude_column": {
-            title: "Latitude field",
+            title: t`Latitude field`,
             ...fieldSetting("map.latitude_column", isNumeric,
                 ([{ data: { cols }}]) => (_.find(cols, isLatitude) || {}).name),
             getHidden: (series, vizSettings) => !PIN_MAP_TYPES.has(vizSettings["map.type"])
         },
         "map.longitude_column": {
-            title: "Longitude field",
+            title: t`Longitude field`,
             ...fieldSetting("map.longitude_column", isNumeric,
                 ([{ data: { cols }}]) => (_.find(cols, isLongitude) || {}).name),
             getHidden: (series, vizSettings) => !PIN_MAP_TYPES.has(vizSettings["map.type"])
         },
         "map.metric_column": {
-            title: "Metric field",
+            title: t`Metric field`,
             ...metricSetting("map.metric_column"),
             getHidden: (series, vizSettings) =>
                 !PIN_MAP_TYPES.has(vizSettings["map.type"]) || (
@@ -117,7 +117,7 @@ export default class Map extends Component {
                 ),
         },
         "map.region": {
-            title: "Region map",
+            title: t`Region map`,
             widget: "select",
             getDefault: ([{ card, data: { cols }}]) => {
                 if (card.display === "state" || _.any(cols, isState)) {
@@ -134,12 +134,12 @@ export default class Map extends Component {
             getHidden: (series, vizSettings) => vizSettings["map.type"] !== "region"
         },
         "map.metric": {
-            title: "Metric field",
+            title: t`Metric field`,
             ...metricSetting("map.metric"),
             getHidden: (series, vizSettings) => vizSettings["map.type"] !== "region"
         },
         "map.dimension": {
-            title: "Region field",
+            title: t`Region field`,
             widget: "select",
             ...dimensionSetting("map.dimension"),
             getHidden: (series, vizSettings) => vizSettings["map.type"] !== "region"
@@ -151,25 +151,25 @@ export default class Map extends Component {
         "map.center_longitude": {
         },
         "map.heat.radius": {
-            title: "Radius",
+            title: t`Radius`,
             widget: "number",
             default: 30,
             getHidden: (series, vizSettings) => vizSettings["map.type"] !== "heat"
         },
         "map.heat.blur": {
-            title: "Blur",
+            title: t`Blur`,
             widget: "number",
             default: 60,
             getHidden: (series, vizSettings) => vizSettings["map.type"] !== "heat"
         },
         "map.heat.min-opacity": {
-            title: "Min Opacity",
+            title: t`Min Opacity`,
             widget: "number",
             default: 0,
             getHidden: (series, vizSettings) => vizSettings["map.type"] !== "heat"
         },
         "map.heat.max-zoom": {
-            title: "Max Zoom",
+            title: t`Max Zoom`,
             widget: "number",
             default: 1,
             getHidden: (series, vizSettings) => vizSettings["map.type"] !== "heat"
@@ -179,14 +179,14 @@ export default class Map extends Component {
     static checkRenderable([{ data: { cols, rows} }], settings) {
         if (PIN_MAP_TYPES.has(settings["map.type"])) {
             if (!settings["map.longitude_column"] || !settings["map.latitude_column"]) {
-                throw new ChartSettingsError("Please select longitude and latitude columns in the chart settings.", "Data");
+                throw new ChartSettingsError(t`Please select longitude and latitude columns in the chart settings.`, "Data");
             }
         } else if (settings["map.type"] === "region"){
             if (!settings["map.region"]) {
-                throw new ChartSettingsError("Please select a region map.", "Data");
+                throw new ChartSettingsError(t`Please select a region map.`, "Data");
             }
             if (!settings["map.dimension"] || !settings["map.metric"]) {
-                throw new ChartSettingsError("Please select region and metric columns in the chart settings.", "Data");
+                throw new ChartSettingsError(t`Please select region and metric columns in the chart settings.`, "Data");
             }
         }
     }
diff --git a/frontend/src/metabase/visualizations/visualizations/ObjectDetail.jsx b/frontend/src/metabase/visualizations/visualizations/ObjectDetail.jsx
index e32e035b0a5b42a15f0000d59ef379550b3bf451..212b1bb33d5c5f38ba41906d2d2d0966c35857ec 100644
--- a/frontend/src/metabase/visualizations/visualizations/ObjectDetail.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/ObjectDetail.jsx
@@ -2,7 +2,7 @@
 
 import React, { Component } from "react";
 import { connect } from 'react-redux';
-
+import { t } from 'c-3po';
 import DirectionalButton from 'metabase/components/DirectionalButton';
 import ExpandableString from 'metabase/query_builder/components/ExpandableString.jsx';
 import Icon from 'metabase/components/Icon.jsx';
@@ -37,10 +37,10 @@ const mapDispatchToProps = {
 export class ObjectDetail extends Component {
     props: Props
 
-    static uiName = "Object Detail";
+    static uiName = t`Object Detail`;
     static identifier = "object";
     static iconName = "document";
-    static noun = "object";
+    static noun = t`object`;
 
     static hidden = true;
 
@@ -88,7 +88,7 @@ export class ObjectDetail extends Component {
             isLink = false;
         } else {
             if (value === null || value === undefined || value === "") {
-                cellValue = (<span className="text-grey-2">Empty</span>);
+                cellValue = (<span className="text-grey-2">{t`Empty`}</span>);
             } else if (isa(column.special_type, TYPE.SerializedJSON)) {
                 let formattedJson = JSON.stringify(JSON.parse(value), null, 2);
                 cellValue = (<pre className="ObjectJSON">{formattedJson}</pre>);
@@ -147,7 +147,7 @@ export class ObjectDetail extends Component {
         tableForeignKeys = tableForeignKeys.filter(fk => isQueryable(fk.origin.table));
 
         if (tableForeignKeys.length < 1) {
-            return (<p className="my4 text-centered">No relationships found.</p>);
+            return (<p className="my4 text-centered">{t`No relationships found.`}</p>);
         }
 
         const fkCountsByTable = foreignKeyCountsByOriginTable(tableForeignKeys);
@@ -176,7 +176,7 @@ export class ObjectDetail extends Component {
             );
 
             const relationName = inflect(fk.origin.table.display_name, fkCountValue);
-            const via = (fkCountsByTable[fk.origin.table.id] > 1) ? (<span className="text-grey-3 text-normal"> via {fk.origin.display_name}</span>) : null;
+            const via = (fkCountsByTable[fk.origin.table.id] > 1) ? (<span className="text-grey-3 text-normal"> {t`via ${fk.origin.display_name}`}</span>) : null;
 
             const info = (
                 <div>
@@ -233,7 +233,7 @@ export class ObjectDetail extends Component {
             return false;
         }
 
-        const tableName = (this.props.tableMetadata) ? singularize(this.props.tableMetadata.display_name) : "Unknown";
+        const tableName = (this.props.tableMetadata) ? singularize(this.props.tableMetadata.display_name) : t`Unknown`;
         // TODO: once we nail down the "title" column of each table this should be something other than the id
         const idValue = this.getIdValue();
 
@@ -250,7 +250,7 @@ export class ObjectDetail extends Component {
                         <div className="p4 flex align-center text-bold text-grey-3">
                             <Icon name="connections" size={17} />
                             <div className="ml2">
-                                This <span className="text-dark">{tableName}</span> is connected to:
+                                {t`This <span className="text-dark">${tableName}</span> is connected to:`}
                             </div>
                         </div>
                     </div>
diff --git a/frontend/src/metabase/visualizations/visualizations/PieChart.jsx b/frontend/src/metabase/visualizations/visualizations/PieChart.jsx
index b78c0cfcd8615cd2edf450236fe9f733447e5b2e..c1201592aeed6aa6b012ed7f1e1d6ce651d17fbd 100644
--- a/frontend/src/metabase/visualizations/visualizations/PieChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/PieChart.jsx
@@ -3,7 +3,7 @@
 import React, { Component } from "react";
 import ReactDOM from "react-dom";
 import styles from "./PieChart.css";
-
+import { t } from 'c-3po';
 import ChartTooltip from "../components/ChartTooltip.jsx";
 import ChartWithLegend from "../components/ChartWithLegend.jsx";
 
@@ -34,7 +34,7 @@ import type { VisualizationProps } from "metabase/meta/types/Visualization";
 export default class PieChart extends Component {
     props: VisualizationProps;
 
-    static uiName = "Pie";
+    static uiName = t`Pie`;
     static identifier = "pie";
     static iconName = "pie";
 
@@ -46,35 +46,35 @@ export default class PieChart extends Component {
 
     static checkRenderable([{ data: { cols, rows} }], settings) {
         if (!settings["pie.dimension"] || !settings["pie.metric"]) {
-            throw new ChartSettingsError("Which columns do you want to use?", "Data");
+            throw new ChartSettingsError(t`Which columns do you want to use?`, t`Data`);
         }
     }
 
     static settings = {
         "pie.dimension": {
             section: "Data",
-            title: "Dimension",
+            title: t`Dimension`,
             ...dimensionSetting("pie.dimension")
         },
         "pie.metric": {
             section: "Data",
-            title: "Measure",
+            title: t`Measure`,
             ...metricSetting("pie.metric")
         },
         "pie.show_legend": {
             section: "Display",
-            title: "Show legend",
+            title: t`Show legend`,
             widget: "toggle"
         },
         "pie.show_legend_perecent": {
             section: "Display",
-            title: "Show percentages in legend",
+            title: t`Show percentages in legend`,
             widget: "toggle",
             default: true
         },
         "pie.slice_threshold": {
             section: "Display",
-            title: "Minimum slice percentage",
+            title: t`Minimum slice percentage`,
             widget: "number"
         },
     }
@@ -173,7 +173,7 @@ export default class PieChart extends Component {
             title = formatDimension(slices[hovered.index].key);
             value = formatMetric(slices[hovered.index].value);
         } else {
-            title = "Total";
+            title = t`Total`;
             value = formatMetric(total);
         }
 
diff --git a/frontend/src/metabase/visualizations/visualizations/Progress.jsx b/frontend/src/metabase/visualizations/visualizations/Progress.jsx
index 5141e68ec6ae539fb0ed1e6499fd7ccac08c9e56..860e8b6acf064a5e63f1fd2177ecd72ed58e35d3 100644
--- a/frontend/src/metabase/visualizations/visualizations/Progress.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Progress.jsx
@@ -2,7 +2,7 @@
 
 import React, { Component } from "react";
 import ReactDOM from "react-dom";
-
+import { t } from 'c-3po';
 import { formatValue } from "metabase/lib/formatting";
 import { isNumeric } from "metabase/lib/schema_metadata";
 import Icon from "metabase/components/Icon.jsx";
@@ -20,7 +20,7 @@ import type { VisualizationProps } from "metabase/meta/types/Visualization";
 export default class Progress extends Component {
     props: VisualizationProps;
 
-    static uiName = "Progress";
+    static uiName = t`Progress`;
     static identifier = "progress";
     static iconName = "progress";
 
@@ -32,20 +32,20 @@ export default class Progress extends Component {
 
     static checkRenderable([{ data: { cols, rows} }]) {
         if (!isNumeric(cols[0])) {
-            throw new Error("Progress visualization requires a number.");
+            throw new Error(t`Progress visualization requires a number.`);
         }
     }
 
     static settings = {
         "progress.goal": {
             section: "Display",
-            title: "Goal",
+            title: t`Goal`,
             widget: "number",
             default: 0
         },
         "progress.color": {
             section: "Display",
-            title: "Color",
+            title: t`Color`,
             widget: "color",
             default: normal.green
         },
@@ -121,9 +121,9 @@ export default class Progress extends Component {
 
         let barMessage;
         if (value === goal) {
-            barMessage = "Goal met";
+            barMessage = t`Goal met`;
         } else if (value > goal) {
-            barMessage = "Goal exceeded";
+            barMessage = t`Goal exceeded`;
         }
 
         const clicked = {
@@ -189,7 +189,7 @@ export default class Progress extends Component {
                     </div>
                     <div className="mt1">
                         <span className="float-left">0</span>
-                        <span className="float-right">Goal {formatValue(goal, { comma: true })}</span>
+                        <span className="float-right">{t`Goal ${formatValue(goal, { comma: true })}`}</span>
                     </div>
                 </div>
             </div>
diff --git a/frontend/src/metabase/visualizations/visualizations/RowChart.jsx b/frontend/src/metabase/visualizations/visualizations/RowChart.jsx
index aeebb7c474ed0cafcc05ec8d7283f72a25d731dd..9ab5caaa23102c030082bcd0d5f3321dd6f01b0a 100644
--- a/frontend/src/metabase/visualizations/visualizations/RowChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/RowChart.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-
+import { t } from 'c-3po';
 import LineAreaBarChart from "../components/LineAreaBarChart.jsx";
 import rowRenderer from "../lib/RowRenderer.js";
 
@@ -10,10 +10,10 @@ import {
 } from "metabase/visualizations/lib/settings/graph";
 
 export default class RowChart extends LineAreaBarChart {
-    static uiName = "Row Chart";
+    static uiName = t`Row Chart`;
     static identifier = "row";
     static iconName = "horizontal_bar";
-    static noun = "row chart";
+    static noun = t`row chart`;
 
     static supportsSeries = false;
 
@@ -28,9 +28,9 @@ export default class RowChart extends LineAreaBarChart {
 // rename these settings
 RowChart.settings["graph.metrics"] = {
     ...RowChart.settings["graph.metrics"],
-    title: "X-axis"
+    title: t`X-axis`
 }
 RowChart.settings["graph.dimensions"] = {
     ...RowChart.settings["graph.dimensions"],
-    title: "Y-axis"
+    title: t`Y-axis`
 }
diff --git a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
index 2ca4b525eb0b9d160c8dd037adbceaa8b51e3519..e4f2b72218071737550a7f06ca2629b927f07159 100644
--- a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
@@ -2,7 +2,7 @@
 
 import React, { Component } from "react";
 import styles from "./Scalar.css";
-
+import { t } from 'c-3po';
 import Icon from "metabase/components/Icon.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
 import Ellipsified from "metabase/components/Ellipsified.jsx";
@@ -19,7 +19,7 @@ import type { VisualizationProps } from "metabase/meta/types/Visualization";
 export default class Scalar extends Component {
     props: VisualizationProps;
 
-    static uiName = "Number";
+    static uiName = t`Number`;
     static identifier = "scalar";
     static iconName = "number";
 
@@ -59,7 +59,7 @@ export default class Scalar extends Component {
                 },
                 data: {
                     cols: [
-                        { base_type: TYPE.Text, display_name: "Name", name: "name" },
+                        { base_type: TYPE.Text, display_name: t`Name`, name: "name" },
                         { ...s.data.cols[0] }],
                     rows: [
                         [s.card.name, s.data.rows[0][0]]
@@ -73,7 +73,7 @@ export default class Scalar extends Component {
 
     static settings = {
         "scalar.locale": {
-            title: "Separator style",
+            title: t`Separator style`,
             widget: "select",
             props: {
                 options: [
@@ -86,19 +86,19 @@ export default class Scalar extends Component {
             default: "en"
         },
         "scalar.decimals": {
-            title: "Number of decimal places",
+            title: t`Number of decimal places`,
             widget: "number"
         },
         "scalar.prefix": {
-            title: "Add a prefix",
+            title: t`Add a prefix`,
             widget: "input"
         },
         "scalar.suffix": {
-            title: "Add a suffix",
+            title: t`Add a suffix`,
             widget: "input"
         },
         "scalar.scale": {
-            title: "Multiply by a number",
+            title: t`Multiply by a number`,
             widget: "number"
         },
     };
diff --git a/frontend/src/metabase/visualizations/visualizations/ScatterPlot.jsx b/frontend/src/metabase/visualizations/visualizations/ScatterPlot.jsx
index 034929f09cd561d47692bebb9ab2d877d4bcbc3f..484c88b72fe6a8e94dc936586a7b86c8abb71206 100644
--- a/frontend/src/metabase/visualizations/visualizations/ScatterPlot.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/ScatterPlot.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 
-
+import { t } from 'c-3po';
 import LineAreaBarChart from "../components/LineAreaBarChart.jsx";
 import { scatterRenderer } from "../lib/LineAreaBarRenderer";
 
@@ -13,10 +13,10 @@ import {
 } from "../lib/settings/graph";
 
 export default class ScatterPlot extends LineAreaBarChart {
-    static uiName = "Scatter";
+    static uiName = t`Scatter`;
     static identifier = "scatter";
     static iconName = "bubble";
-    static noun = "scatter plot";
+    static noun = t`scatter plot`;
 
     static renderer = scatterRenderer;
 
diff --git a/frontend/src/metabase/visualizations/visualizations/Table.jsx b/frontend/src/metabase/visualizations/visualizations/Table.jsx
index 679bbcb16bf2e18a29029b7d94fd706d33c3192c..12e1eb50c76f2326aa40d243142fd1c586842729 100644
--- a/frontend/src/metabase/visualizations/visualizations/Table.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Table.jsx
@@ -4,7 +4,7 @@ import React, { Component } from "react";
 
 import TableInteractive from "../components/TableInteractive.jsx";
 import TableSimple from "../components/TableSimple.jsx";
-
+import { t } from 'c-3po';
 import * as DataGrid from "metabase/lib/data_grid";
 
 import Query from "metabase/lib/query";
@@ -34,7 +34,7 @@ export default class Table extends Component {
     props: Props;
     state: State;
 
-    static uiName = "Table";
+    static uiName = t`Table`;
     static identifier = "table";
     static iconName = "table";
 
@@ -50,7 +50,7 @@ export default class Table extends Component {
 
     static settings = {
         "table.pivot": {
-            title: "Pivot the table",
+            title: t`Pivot the table`,
             widget: "toggle",
             getHidden: ([{ card, data }]) => (
                 data && data.cols.length !== 3
@@ -63,7 +63,7 @@ export default class Table extends Component {
             )
         },
         "table.columns": {
-            title: "Fields to include",
+            title: t`Fields to include`,
             widget: ChartSettingOrderedFields,
             getHidden: (series, vizSettings) => vizSettings["table.pivot"],
             isValid: ([{ card, data }]) =>
diff --git a/frontend/src/metabase/xray/components/ComparisonHeader.jsx b/frontend/src/metabase/xray/components/ComparisonHeader.jsx
index 7729674e9b8aa6c96777e10e54c0a43ed48ed48f..e7b7cb1ce6acd31f7b7c31fddc99fe9fb5d41aff 100644
--- a/frontend/src/metabase/xray/components/ComparisonHeader.jsx
+++ b/frontend/src/metabase/xray/components/ComparisonHeader.jsx
@@ -1,5 +1,5 @@
 import React from 'react'
-
+import { t } from 'c-3po';
 import Icon from 'metabase/components/Icon'
 import CostSelect from 'metabase/xray/components/CostSelect'
 
@@ -7,10 +7,10 @@ const ComparisonHeader = ({ cost }) =>
     <div className="my4 flex align-center">
         <h1 className="flex align-center">
             <Icon name="compare" className="mr1" size={32} />
-            Comparing
+            {t`Comparing`}
         </h1>
         <div className="ml-auto flex align-center">
-            <h3 className="text-grey-3 mr1">Fidelity</h3>
+            <h3 className="text-grey-3 mr1">{t`Fidelity`}</h3>
             <CostSelect
                 currentCost={cost}
             />
diff --git a/frontend/src/metabase/xray/components/InsightCard.jsx b/frontend/src/metabase/xray/components/InsightCard.jsx
index 15a95418623bf0d2bb1c36b7bcfaa5ee4db5d1aa..0b6905f8ece2778c23c808ef68c78f017e99857e 100644
--- a/frontend/src/metabase/xray/components/InsightCard.jsx
+++ b/frontend/src/metabase/xray/components/InsightCard.jsx
@@ -168,7 +168,7 @@ export class SeasonalityInsight extends Component {
 
         return (
             <InsightText>
-                { jt`Your data has a ${ quality } seasonal compoment.` }
+                { jt`Your data has a ${ quality } seasonal component.` }
             </InsightText>
         )
     }
diff --git a/frontend/src/metabase/xray/components/PreviewBanner.jsx b/frontend/src/metabase/xray/components/PreviewBanner.jsx
index 7a19820bcf8f2268af2a94076f8adfb92315e926..ade85123e5791d303025a62993d0df20e6f1a644 100644
--- a/frontend/src/metabase/xray/components/PreviewBanner.jsx
+++ b/frontend/src/metabase/xray/components/PreviewBanner.jsx
@@ -1,12 +1,12 @@
 import React from 'react'
 import Icon from 'metabase/components/Icon'
-
+import { jt } from 'c-3po';
 const SURVEY_LINK = 'https://docs.google.com/forms/d/e/1FAIpQLSc92WzF76ViiT8l4646lvFSWejNUhh4lhCSMXdZECILVwJG2A/viewform?usp=sf_link'
 
 const PreviewBanner = () =>
     <div className="full py2 flex align-center justify-center full md-py3 text-centered text-slate text-paragraph bg-white border-bottom">
         <Icon name='beaker' size={28} className="mr1 text-brand" style={{ marginTop: -5 }} />
-        <span>Welcome to the x-ray preview! We'd love <a className="link"  href={SURVEY_LINK} target="_blank">your feedback.</a></span>
+        <span>{jt`Welcome to the x-ray preview! We'd love ${<a className="link" href={SURVEY_LINK} target="_blank">your feedback</a>}`}</span>
     </div>
 
 export default PreviewBanner
diff --git a/frontend/src/metabase/xray/components/XRayComparison.jsx b/frontend/src/metabase/xray/components/XRayComparison.jsx
index 37f70cf075cab9fa68cc938007378c85da37af07..0f49fad3e786cc0c9c7b8a318c589bd6d39d90fa 100644
--- a/frontend/src/metabase/xray/components/XRayComparison.jsx
+++ b/frontend/src/metabase/xray/components/XRayComparison.jsx
@@ -2,7 +2,7 @@ import React from 'react'
 import { Link } from 'react-router'
 import Color from 'color'
 import Visualization from 'metabase/visualizations/components/Visualization'
-
+import { t } from 'c-3po';
 import Icon from 'metabase/components/Icon'
 import Tooltip from 'metabase/components/Tooltip'
 import { XRayPageWrapper, Heading } from 'metabase/xray/components/XRayLayout'
@@ -195,9 +195,9 @@ const XRayComparison = ({
             </div>
         </div>
 
-        <Heading heading="Overview" />
+        <Heading heading={t`Overview`} />
         <div className="bordered rounded bg-white shadowed p4">
-            <h3 className="text-grey-3">Count</h3>
+            <h3 className="text-grey-3">{t`Count`}</h3>
             <div className="flex my1">
                 <h1
                     className="mr1"
@@ -214,7 +214,7 @@ const XRayComparison = ({
 
         { contributors && (
             <div>
-                <Heading heading="Potentially interesting differences" />
+                <Heading heading={t`Potentially interesting differences`} />
                 <ol className="Grid Grid--gutters Grid--1of3">
                     { contributors.map(contributor =>
                         <li className="Grid-cell" key={contributor.field.id}>
@@ -229,7 +229,7 @@ const XRayComparison = ({
             </div>
         )}
 
-        <Heading heading="Full breakdown" />
+        <Heading heading={t`Full breakdown`} />
         <div className="bordered rounded bg-white shadowed">
 
             <div className="flex p2">
@@ -248,7 +248,7 @@ const XRayComparison = ({
             <table className="ComparisonTable full">
                 <thead className="full border-bottom">
                     <tr>
-                        <th className="px2">Field</th>
+                        <th className="px2">{t`Field`}</th>
                         {comparisonFields.map(c =>
                             <th
                                 key={c}
diff --git a/frontend/src/metabase/xray/containers/CardXRay.jsx b/frontend/src/metabase/xray/containers/CardXRay.jsx
index 5083579ee505316b23f6206e97dd358b2dff4bf3..e77086592bb81ac0016629ba1b4aa9eb8caec752 100644
--- a/frontend/src/metabase/xray/containers/CardXRay.jsx
+++ b/frontend/src/metabase/xray/containers/CardXRay.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from 'react'
 import cxs from 'cxs'
 import { connect } from 'react-redux'
-
+import { t } from 'c-3po';
 import { saturated } from 'metabase/lib/colors'
 
 import { fetchCardXray, initialize } from 'metabase/xray/xray'
@@ -130,7 +130,7 @@ class CardXRay extends Component {
                                             {
                                                 card: {
                                                     display: 'line',
-                                                    name: 'Growth Trend',
+                                                    name: t`Growth Trend`,
                                                     visualization_settings: {
 
                                                     }
@@ -152,7 +152,7 @@ class CardXRay extends Component {
                                         {
                                             card: {
                                                 display: 'line',
-                                                name: 'Trend',
+                                                name: t`Trend`,
                                                 visualization_settings: {
 
                                                 }
@@ -183,7 +183,7 @@ class CardXRay extends Component {
                                         {
                                             card: {
                                                 display: 'line',
-                                                name: 'Trend',
+                                                name: t`Trend`,
                                                 visualization_settings: {}
                                             },
                                             data: xray.features['seasonal-decomposition'].value.trend
@@ -191,7 +191,7 @@ class CardXRay extends Component {
                                         {
                                             card: {
                                                 display: 'line',
-                                                name: 'Seasonal',
+                                                name: t`Seasonal`,
                                                 visualization_settings: {}
                                             },
                                             data: xray.features['seasonal-decomposition'].value.seasonal
@@ -199,7 +199,7 @@ class CardXRay extends Component {
                                         {
                                             card: {
                                                 display: 'line',
-                                                name: 'Residual',
+                                                name: t`Residual`,
                                                 visualization_settings: {}
                                             },
                                             data: xray.features['seasonal-decomposition'].value.residual
diff --git a/frontend/src/metabase/xray/containers/FieldXray.jsx b/frontend/src/metabase/xray/containers/FieldXray.jsx
index 1c110f23745e0bdef03b6ced28d830eb0ba18c64..84363a7cc5ea6bb5ef1a0f4aa8d8089ec59b6748 100644
--- a/frontend/src/metabase/xray/containers/FieldXray.jsx
+++ b/frontend/src/metabase/xray/containers/FieldXray.jsx
@@ -4,7 +4,7 @@ import React, { Component } from 'react'
 import { connect } from 'react-redux'
 import title from 'metabase/hoc/Title'
 import { Link } from 'react-router'
-
+import { t } from 'c-3po';
 import { isDate } from 'metabase/lib/schema_metadata'
 import { fetchFieldXray, initialize } from 'metabase/xray/xray'
 import {
@@ -68,7 +68,7 @@ const mapDispatchToProps = {
 }
 
 @connect(mapStateToProps, mapDispatchToProps)
-@title(({ features }) => features && features.model.display_name || "Field")
+@title(({ features }) => features && features.model.display_name || t`Field`)
 class FieldXRay extends Component {
     props: Props
 
@@ -121,10 +121,10 @@ class FieldXRay extends Component {
                                         <h1 className="flex align-center">
                                             {features.model.display_name}
                                             <Icon name="chevronright" className="mx1 text-grey-3" size={16} />
-                                            <span className="text-grey-3">X-ray</span>
+                                            <span className="text-grey-3">{t`X-ray`}</span>
                                         </h1>
                                         <div className="ml-auto flex align-center">
-                                            <h3 className="mr2 text-grey-3">Fidelity</h3>
+                                            <h3 className="mr2 text-grey-3">{t`Fidelity`}</h3>
                                             <CostSelect
                                                 xrayType='field'
                                                 id={features.model.id}
@@ -144,7 +144,7 @@ class FieldXRay extends Component {
                                 </div>
                             }
                             <div className="mt4">
-                                <Heading heading="Distribution" />
+                                <Heading heading={t`Distribution`} />
                                 <div className="bg-white bordered shadowed">
                                     <div className="lg-p4">
                                         <div style={{ height: 300 }}>
@@ -159,20 +159,20 @@ class FieldXRay extends Component {
                             { isDate(features.model) && <Periodicity xray={features} /> }
 
                             <StatGroup
-                                heading="Values overview"
+                                heading={t`Values overview`}
                                 xray={features}
                                 stats={VALUES_OVERVIEW}
                             />
 
                             <StatGroup
-                                heading="Statistical overview"
+                                heading={t`Statistical overview`}
                                 xray={features}
                                 showDescriptions
                                 stats={STATS_OVERVIEW}
                             />
 
                             <StatGroup
-                                heading="Robots"
+                                heading={t`Robots`}
                                 xray={features}
                                 showDescriptions
                                 stats={ROBOTS}
diff --git a/frontend/src/metabase/xray/containers/SegmentXRay.jsx b/frontend/src/metabase/xray/containers/SegmentXRay.jsx
index ab7301094d5eaa233cbbe7b132e6c2914d4e2cb8..1576394a1ccfbca45b1822ab9e5d5a004abc94dd 100644
--- a/frontend/src/metabase/xray/containers/SegmentXRay.jsx
+++ b/frontend/src/metabase/xray/containers/SegmentXRay.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from 'react'
 import { connect } from 'react-redux'
 import title from 'metabase/hoc/Title'
-
+import { t } from 'c-3po';
 import { Link } from 'react-router'
 import { push } from "react-router-redux";
 
@@ -61,7 +61,7 @@ const mapDispatchToProps = {
 }
 
 @connect(mapStateToProps, mapDispatchToProps)
-@title(({ features }) => features && features.model.name || "Segment" )
+@title(({ features }) => features && features.model.name || t`Segment` )
 class SegmentXRay extends Component {
 
     props: Props
@@ -128,14 +128,14 @@ class SegmentXRay extends Component {
                                     <h1 className="mt2 flex align-center">
                                         {features.model.name}
                                         <Icon name="chevronright" className="mx1 text-grey-3" size={16} />
-                                        <span className="text-grey-3">X-ray</span>
+                                        <span className="text-grey-3">{t`X-ray`}</span>
                                     </h1>
                                     <p className="mt1 text-paragraph text-measure">
                                         {features.model.description}
                                     </p>
                                 </div>
                                 <div className="ml-auto flex align-center">
-                                   <h3 className="mr2 text-grey-3">Fidelity</h3>
+                                   <h3 className="mr2 text-grey-3">{t`Fidelity`}</h3>
                                     <CostSelect
                                         currentCost={params.cost}
                                         xrayType='segment'
@@ -150,7 +150,7 @@ class SegmentXRay extends Component {
                             />
                             }
                             <div className="mt2">
-                                <Heading heading="Fields in this segment" />
+                                <Heading heading={t`Fields in this segment`} />
                                 <ol>
                                     { constituents.map((c, i) => {
                                         return (
diff --git a/frontend/src/metabase/xray/containers/TableXRay.jsx b/frontend/src/metabase/xray/containers/TableXRay.jsx
index ab7a8699943ddce42cce210a19f3b3c504511443..2eb8ec9add4c6ddf91a89efabd1fb3c332b1f299 100644
--- a/frontend/src/metabase/xray/containers/TableXRay.jsx
+++ b/frontend/src/metabase/xray/containers/TableXRay.jsx
@@ -1,5 +1,5 @@
 import React, { Component } from 'react'
-
+import { t } from 'c-3po';
 import { connect } from 'react-redux'
 import title from 'metabase/hoc/Title'
 
@@ -58,7 +58,7 @@ const mapDispatchToProps = {
 }
 
 @connect(mapStateToProps, mapDispatchToProps)
-@title(({ features }) => features && features.model.display_name || "Table")
+@title(({ features }) => features && features.model.display_name || t`Table`)
 class TableXRay extends Component {
     props: Props
 
@@ -104,12 +104,12 @@ class TableXRay extends Component {
                                     <h1 className="mt2 flex align-center">
                                         {features.model.display_name}
                                         <Icon name="chevronright" className="mx1 text-grey-3" size={16} />
-                                        <span className="text-grey-3">XRay</span>
+                                        <span className="text-grey-3">{t`XRay`}</span>
                                     </h1>
                                     <p className="m0 text-paragraph text-measure">{features.model.description}</p>
                                 </div>
                                 <div className="ml-auto flex align-center">
-                                    <h3 className="mr2">Fidelity:</h3>
+                                    <h3 className="mr2">{t`Fidelity:`}</h3>
                                     <CostSelect
                                         xrayType='table'
                                         currentCost={params.cost}
diff --git a/frontend/src/metabase/xray/costs.js b/frontend/src/metabase/xray/costs.js
index f3fa5d1720c1eabf3daaec8c226ba77a5b70321b..005cd15ecf6b22ca02b3b82a4bab0400c7248ee6 100644
--- a/frontend/src/metabase/xray/costs.js
+++ b/frontend/src/metabase/xray/costs.js
@@ -2,10 +2,11 @@
  * human understandable groupings.
  * for more info on the actual values see src/metabase/fingerprints/costs.clj
  */
+import { t } from 'c-3po';
 
 const approximate = {
-    display_name: "Approximate",
-    description: `
+    display_name: t`Approximate`,
+    description: t`
         Get a sense for this data by looking at a sample.
         This is faster but less precise.
     `,
@@ -17,8 +18,8 @@ const approximate = {
 }
 
 const exact = {
-    display_name: "Exact",
-    description: `
+    display_name: t`Exact`,
+    description: t`
         Go deeper into this data by performing a full scan.
         This is more precise but slower.
     `,
@@ -30,8 +31,8 @@ const exact = {
 }
 
 const extended = {
-    display_name: "Extended",
-    description: `
+    display_name: t`Extended`,
+    description: t`
         Adds additional info about this entity by including related objects.
         This is the slowest but highest fidelity method.
     `,
diff --git a/frontend/src/metabase/xray/utils.js b/frontend/src/metabase/xray/utils.js
index 0232b159c1846913e16e97bf92d4b6a78fcae08d..f236ce5b091a0ecef10f50af2b86dbdc1e617f65 100644
--- a/frontend/src/metabase/xray/utils.js
+++ b/frontend/src/metabase/xray/utils.js
@@ -1,15 +1,16 @@
 // takes a distance float and uses it to return a human readable phrase
 // indicating how similar two items in a comparison are
+import { t } from 'c-3po';
 
 export const distanceToPhrase = (distance) => {
     if(distance >= 0.75) {
-        return  'Very different'
+        return t`Very different`
     } else if (distance < 0.75 && distance >= 0.5) {
-        return 'Somewhat different'
+        return t`Somewhat different`
     } else if (distance < 0.5 && distance >= 0.25) {
-        return 'Somewhat similar'
+        return t`Somewhat similar`
     } else {
-        return 'Very similar'
+        return t`Very similar`
     }
 }
 
@@ -23,11 +24,11 @@ export const hasXray = has
 export const hasComparison = has
 
 export const xrayLoadingMessages = [
-    'Generating your x-ray...',
-    'Still working...',
+    t`Generating your x-ray...`,
+    t`Still working...`,
 ]
 
 export const comparisonLoadingMessages = [
-    'Generating your comparison...',
-    'Still working...',
+    t`Generating your comparison...`,
+    t`Still working...`,
 ]
diff --git a/frontend/test/components/AccordianList.unit.test.js b/frontend/test/components/AccordianList.unit.test.js
index 5077647356cbdc07ee804943d9c181fca238a905..ee159d9e228455d401405de3dbeef0bdfed5cc79 100644
--- a/frontend/test/components/AccordianList.unit.test.js
+++ b/frontend/test/components/AccordianList.unit.test.js
@@ -1,8 +1,6 @@
 import React from "react";
-import { shallow, mount } from "enzyme";
-import { Link } from "react-router";
+import { mount } from "enzyme";
 
-import Icon from "metabase/components/Icon";
 import AccordianList from "metabase/components/AccordianList";
 import ListSearchField from "metabase/components/ListSearchField";
 
diff --git a/frontend/test/lib/utils.unit.spec.js b/frontend/test/lib/utils.unit.spec.js
index 7ddaca9459f1d9d020c11375f209497844de5442..6290639d136c00807faaf92f0fc6899559449cfe 100644
--- a/frontend/test/lib/utils.unit.spec.js
+++ b/frontend/test/lib/utils.unit.spec.js
@@ -79,6 +79,12 @@ describe('utils', () => {
             expect(MetabaseUtils.isEmpty(" ")).toEqual(true);
         });
     });
+
+    describe("isJWT", () => {
+        it("should allow for JWT tokens with dashes", () => {
+            expect(MetabaseUtils.isJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXJhbXMiOnsicGFyYW0xIjoidGVzdCIsInBhcmFtMiI6ImFiIiwicGFyYW0zIjoiMjAwMC0wMC0wMFQwMDowMDowMCswMDowMCIsInBhcmFtNCI6Iu-8iO-8iSJ9LCJyZXNvdXJjZSI6eyJkYXNoYm9hcmQiOjB9fQ.wsNWliHJNwJBv_hx0sPo1EGY0nATdgEa31TM1AYotIA")).toEqual(true);
+        });
+    });
 });
 
 function shuffle(a) {
diff --git a/frontend/test/pulse/pulse.unit.spec.js b/frontend/test/pulse/pulse.unit.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..971c0b7346954f1ab3e55babef12e1609f2c2989
--- /dev/null
+++ b/frontend/test/pulse/pulse.unit.spec.js
@@ -0,0 +1,204 @@
+import React from 'react'
+import { shallow } from 'enzyme'
+
+import {
+    KEYCODE_DOWN,
+    KEYCODE_TAB,
+    KEYCODE_ENTER,
+    KEYCODE_COMMA
+} from "metabase/lib/keyboard"
+
+import Input from "metabase/components/Input"
+import UserAvatar from 'metabase/components/UserAvatar'
+import RecipientPicker from 'metabase/pulse/components/RecipientPicker'
+
+// We have to do some mocking here to avoid calls to GA and to Metabase settings
+jest.mock('metabase/lib/settings', () => ({
+    get: () => 'v'
+}))
+
+global.ga = jest.fn()
+
+
+const TEST_USERS = [
+    { id: 1, common_name: 'Barb', email: 'barb_holland@hawkins.mail' }, // w
+    { id: 2, common_name: 'Dustin', email: 'dustin_henderson@hawkinsav.club' }, // w
+    { id: 3, common_name: 'El', email: '011@energy.gov' },
+    { id: 4, common_name: 'Lucas', email: 'lucas.sinclair@hawkins.mail' }, // w
+    { id: 5, common_name: 'Mike', email: 'dm_mike@hawkins.mail' }, // w
+    { id: 6, common_name: 'Nancy', email: '' },
+    { id: 7, common_name: 'Steve', email: '' },
+    { id: 8, common_name: 'Will', email: 'zombieboy@upside.down' }, // w
+]
+
+describe('recipient picker', () => {
+    describe('focus', () => {
+        it('should be focused if there are no recipients', () => {
+            const wrapper = shallow(
+                <RecipientPicker
+                    recipients={[]}
+                    users={TEST_USERS}
+                    isNewPulse={true}
+                    onRecipientsChange={() => alert('why?')}
+                />
+            )
+
+            expect(wrapper.state().focused).toBe(true)
+        })
+        it('should not be focused if there are existing recipients', () => {
+            const wrapper = shallow(
+                <RecipientPicker
+                    recipients={[TEST_USERS[0]]}
+                    users={TEST_USERS}
+                    isNewPulse={true}
+                    onRecipientsChange={() => alert('why?')}
+                />
+            )
+
+            expect(wrapper.state().focused).toBe(false)
+        })
+    })
+    describe('filtering', () => {
+        it('should properly filter users based on input', () => {
+            const wrapper = shallow(
+                <RecipientPicker
+                    recipients={[]}
+                    users={TEST_USERS}
+                    isNewPulse={true}
+                    onRecipientsChange={() => alert('why?')}
+                />
+            )
+
+            const spy = jest.spyOn(wrapper.instance(), 'setInputValue')
+            const input = wrapper.find(Input)
+
+            // we should start off with no users
+            expect(wrapper.state().filteredUsers.length).toBe(0)
+
+            // simulate typing 'w'
+            input.simulate('change', { target: { value: 'w' }})
+
+            expect(spy).toHaveBeenCalled()
+            expect(wrapper.state().inputValue).toEqual('w')
+
+            // 5 of the test users have a w in their name or email
+            expect(wrapper.state().filteredUsers.length).toBe(5)
+        })
+    })
+
+    describe('recipient selection', () => {
+        it('should allow the user to click to select a recipient', () => {
+            const spy = jest.fn()
+            const wrapper = shallow(
+                <RecipientPicker
+                    recipients={[]}
+                    users={TEST_USERS}
+                    isNewPulse={true}
+                    onRecipientsChange={spy}
+                />
+            )
+
+            const input = wrapper.find(Input)
+
+            // limit our options to one user by typing
+            input.simulate('change', { target: { value: 'steve' }})
+            expect(wrapper.state().filteredUsers.length).toBe(1)
+
+            const user = wrapper.find(UserAvatar).closest('li')
+            user.simulate('click', { target: {}})
+
+            expect(spy).toHaveBeenCalled()
+        })
+
+        describe('key selection', () => {
+            [KEYCODE_TAB, KEYCODE_ENTER, KEYCODE_COMMA].map(key =>
+                it(`should allow the user to use arrow keys and then ${key} to select a recipient`, () => {
+                    const spy = jest.fn()
+
+                    const wrapper = shallow(
+                        <RecipientPicker
+                            recipients={[]}
+                            users={TEST_USERS}
+                            isNewPulse={true}
+                            onRecipientsChange={spy}
+                        />
+                    )
+
+                    const input = wrapper.find(Input)
+
+
+                    // limit our options to  user by typing
+                    input.simulate('change', { target: { value: 'w' }})
+
+                    // the initially selected user should be the first user
+                    expect(wrapper.state().selectedUserID).toBe(TEST_USERS[0].id)
+
+                    input.simulate('keyDown', {
+                        keyCode: KEYCODE_DOWN,
+                        preventDefault: jest.fn()
+                    })
+
+                    // the next possible user should be selected now
+                    expect(wrapper.state().selectedUserID).toBe(TEST_USERS[1].id)
+
+                    input.simulate('keydown', {
+                        keyCode: key,
+                        preventDefalut: jest.fn()
+                    })
+
+                    expect(spy).toHaveBeenCalledTimes(1)
+                    expect(spy).toHaveBeenCalledWith([TEST_USERS[1]])
+                })
+            )
+        })
+
+        describe('usage', () => {
+            it('should all work good', () => {
+                class TestComponent extends React.Component {
+                    state = {
+                        recipients: []
+                    }
+
+                    render () {
+                        const { recipients } = this.state
+                        return (
+                            <RecipientPicker
+                                recipients={recipients}
+                                users={TEST_USERS}
+                                isNewPulse={true}
+                                onRecipientsChange={recipients => {
+                                    this.setState({ recipients })
+                                }}
+                            />
+                        )
+                    }
+                }
+                const wrapper = shallow(<TestComponent />)
+
+                // something about the popover code makes it not work with mount
+                // in the test env, so we have to use shallow and  dive here to
+                // actually get  the selection list to render anything that we
+                // can interact with
+                const picker = wrapper.find(RecipientPicker).dive()
+
+                const input = picker.find(Input)
+                input.simulate('change', { target: { value: 'will' }})
+
+                const user = picker.find(UserAvatar).closest('li')
+                user.simulate('click', { target: {}})
+
+
+                // there should be one user selected
+                expect(wrapper.state().recipients.length).toBe(1)
+
+                // grab the updated state of RecipientPicker
+                const postAddPicker = wrapper.find(RecipientPicker).dive()
+
+                // there should only be one user in the picker now , "Will" and then the input
+                // so there will be two list items
+                expect(postAddPicker.find('li').length).toBe(2)
+
+            })
+        })
+    })
+})
diff --git a/project.clj b/project.clj
index 3a01cc3f1f747afc72af7333ac00d608999dc97c..458ada1b38902c73e7f475e26856468a85e3010b 100644
--- a/project.clj
+++ b/project.clj
@@ -37,7 +37,8 @@
                  [clj-time "0.13.0"]                                  ; library for dealing with date/time
                  [clojurewerkz/quartzite "2.0.0"]                     ; scheduling library
                  [colorize "0.1.1" :exclusions [org.clojure/clojure]] ; string output with ANSI color codes (for logging)
-                 [com.amazon.redshift/redshift-jdbc41 "1.2.8.1005"]   ; Redshift JDBC driver
+                 [com.amazon.redshift/redshift-jdbc41-no-awssdk       ; Redshift JDBC driver without embedded Amazon SDK
+                  "1.2.8.1005"]
                  [com.cemerick/friend "0.2.3"                         ; auth library
                   :exclusions [commons-codec
                                org.apache.httpcomponents/httpclient
diff --git a/resources/migrations/000_migrations.yaml b/resources/migrations/000_migrations.yaml
index 3f703605eeede22832689855506c684a3e4a938b..13de99f8472463c96dccfd3f0b8d2f8ae56ac577 100644
--- a/resources/migrations/000_migrations.yaml
+++ b/resources/migrations/000_migrations.yaml
@@ -3847,6 +3847,7 @@ databaseChangeLog:
   - changeSet:
       id: 67
       author: attekei
+      comment: 'Added 0.27.0'
       changes:
         - createTable:
             tableName: computation_job
@@ -3930,6 +3931,7 @@ databaseChangeLog:
   - changeSet:
     - id: 68
     - author: sbelak
+    - comment: 'Added 0.27.0'
     - changes:
       - addColumn:
             tableName: computation_job
diff --git a/src/metabase/api/alert.clj b/src/metabase/api/alert.clj
index 31c67719bbe8ab402d3b28626c285a6c81c485da..413db034ea8055ece9ec4edcff67dc74ed2af7a3 100644
--- a/src/metabase/api/alert.clj
+++ b/src/metabase/api/alert.clj
@@ -5,6 +5,7 @@
             [medley.core :as m]
             [metabase
              [email :as email]
+             [events :as events]
              [util :as u]]
             [metabase.api
              [common :as api]
@@ -43,6 +44,47 @@
 (defn- only-alert-keys [request]
   (select-keys request [:alert_condition :alert_first_only :alert_above_goal]))
 
+(defn- email-channel [alert]
+  (m/find-first #(= :email (:channel_type %)) (:channels alert)))
+
+(defn- slack-channel [alert]
+  (m/find-first #(= :slack (:channel_type %)) (:channels alert)))
+
+(defn- key-by [key-fn coll]
+  (zipmap (map key-fn coll) coll))
+
+(defn- notify-recipient-changes!
+  "This function compares `OLD-ALERT` and `UPDATED-ALERT` to determine if there have been any recipient related
+  changes. Recipients that have been added or removed will be notified."
+  [old-alert updated-alert]
+  (let [{old-recipients :recipients} (email-channel old-alert)
+        {new-recipients :recipients} (email-channel updated-alert)
+        old-ids->users (key-by :id old-recipients)
+        new-ids->users (key-by :id new-recipients)
+        [removed-ids added-ids _] (data/diff (set (keys old-ids->users))
+                                             (set (keys new-ids->users)))]
+    (doseq [old-id removed-ids
+            :let [removed-user (get old-ids->users old-id)]]
+      (messages/send-admin-unsubscribed-alert-email! old-alert removed-user @api/*current-user*))
+
+    (doseq [new-id added-ids
+            :let [added-user (get new-ids->users new-id)]]
+      (messages/send-you-were-added-alert-email! updated-alert added-user @api/*current-user*))))
+
+(defn- collect-alert-recipients [alert]
+  (set (:recipients (email-channel alert))))
+
+(defn- non-creator-recipients [{{creator-id :id} :creator :as alert}]
+ (remove #(= creator-id (:id %)) (collect-alert-recipients alert)))
+
+(defn- notify-new-alert-created! [alert]
+  (when (email/email-configured?)
+
+    (messages/send-new-alert-email! alert)
+
+    (doseq [recipient (non-creator-recipients alert)]
+      (messages/send-you-were-added-alert-email! alert recipient @api/*current-user*))))
+
 (api/defendpoint POST "/"
   "Create a new alert (`Pulse`)"
   [:as {{:keys [alert_condition card channels alert_first_only alert_above_goal] :as req} :body}]
@@ -56,8 +98,8 @@
                    (-> req
                        only-alert-keys
                        (pulse/create-alert! api/*current-user-id* (u/get-id card) channels)))]
-    (when (email/email-configured?)
-      (messages/send-new-alert-email! new-alert))
+
+    (notify-new-alert-created! new-alert)
 
     new-alert))
 
@@ -77,33 +119,6 @@
     (api/check-403 (and (= api/*current-user-id* (:creator_id alert))
                         (contains? (recipient-ids alert) api/*current-user-id*)))))
 
-(defn- email-channel [alert]
-  (m/find-first #(= :email (:channel_type %)) (:channels alert)))
-
-(defn- slack-channel [alert]
-  (m/find-first #(= :slack (:channel_type %)) (:channels alert)))
-
-(defn- key-by [key-fn coll]
-  (zipmap (map key-fn coll) coll))
-
-(defn- notify-recipient-changes!
-  "This function compares `OLD-ALERT` and `UPDATED-ALERT` to determine if there have been any recipient related
-  changes. Recipients that have been added or removed will be notified."
-  [old-alert updated-alert]
-  (let [{old-recipients :recipients} (email-channel old-alert)
-        {new-recipients :recipients} (email-channel updated-alert)
-        old-ids->users (key-by :id old-recipients)
-        new-ids->users (key-by :id new-recipients)
-        [removed-ids added-ids _] (data/diff (set (keys old-ids->users))
-                                             (set (keys new-ids->users)))]
-    (doseq [old-id removed-ids
-            :let [removed-user (get old-ids->users old-id)]]
-      (messages/send-admin-unsubscribed-alert-email! old-alert removed-user @api/*current-user*))
-
-    (doseq [new-id added-ids
-            :let [added-user (get new-ids->users new-id)]]
-      (messages/send-you-were-added-alert-email! updated-alert added-user @api/*current-user*))))
-
 (api/defendpoint PUT "/:id"
   "Update a `Alert` with ID."
   [id :as {{:keys [alert_condition card channels alert_first_only alert_above_goal card channels] :as req} :body}]
@@ -157,8 +172,20 @@
 
     api/generic-204-no-content))
 
-(defn- collect-alert-recipients [alert]
-  (set (:recipients (email-channel alert))))
+(defn- notify-on-delete-if-needed!
+  "When an alert is deleted, we notify the creator that their alert is being deleted and any recipieint that they are
+  no longer going to be receiving that alert"
+  [alert]
+  (when (email/email-configured?)
+    (let [{creator-id :id :as creator} (:creator alert)
+          ;; The creator might also be a recipient, no need to notify them twice
+          recipients (non-creator-recipients alert)]
+
+      (doseq [recipient recipients]
+        (messages/send-admin-unsubscribed-alert-email! alert recipient @api/*current-user*))
+
+      (when-not (= creator-id api/*current-user-id*)
+        (messages/send-admin-deleted-your-alert! alert creator @api/*current-user*)))))
 
 (api/defendpoint DELETE "/:id"
   "Remove an alert"
@@ -166,18 +193,11 @@
   (api/let-404 [alert (pulse/retrieve-alert id)]
     (api/check-superuser)
 
-    ;; When an alert is deleted, we notify the creator that their alert is being deleted and any recipieint that they
-    ;; are no longer going to be receiving that alert
-    (let [creator (:creator alert)
-          ;; The creator might also be a recipient, no need to notify them twice
-          recipients (remove #(= (:id creator) (:id %)) (collect-alert-recipients alert))]
+    (db/delete! Pulse :id id)
 
-      (db/delete! Pulse :id id)
+    (events/publish-event! :alert-delete (assoc alert :actor_id api/*current-user-id*))
 
-      (when (email/email-configured?)
-        (doseq [recipient recipients]
-          (messages/send-admin-unsubscribed-alert-email! alert recipient @api/*current-user*))
-        (messages/send-admin-deleted-your-alert! alert creator @api/*current-user*)))
+    (notify-on-delete-if-needed! alert)
 
     api/generic-204-no-content))
 
diff --git a/src/metabase/api/card.clj b/src/metabase/api/card.clj
index 1b350273d70cca412cb35e1354cab47f3505f854..ee0c7c6081c64109cba0eb395a7d1742fa2fc7bd 100644
--- a/src/metabase/api/card.clj
+++ b/src/metabase/api/card.clj
@@ -1,4 +1,5 @@
 (ns metabase.api.card
+  "/api/card endpoints."
   (:require [cheshire.core :as json]
             [clojure.data :as data]
             [clojure.tools.logging :as log]
@@ -43,7 +44,7 @@
              [hydrate :refer [hydrate]]])
   (:import java.util.UUID))
 
-;;; ------------------------------------------------------------ Hydration ------------------------------------------------------------
+;;; --------------------------------------------------- Hydration ----------------------------------------------------
 
 (defn- ^:deprecated hydrate-labels
   "Efficiently hydrate the `Labels` for a large collection of `Cards`."
@@ -60,12 +61,14 @@
   "Efficiently add `favorite` status for a large collection of `Cards`."
   [cards]
   (when (seq cards)
-    (let [favorite-card-ids (db/select-field :card_id CardFavorite, :owner_id api/*current-user-id*, :card_id [:in (map :id cards)])]
+    (let [favorite-card-ids (db/select-field :card_id CardFavorite
+                              :owner_id api/*current-user-id*
+                              :card_id  [:in (map :id cards)])]
       (for [card cards]
         (assoc card :favorite (contains? favorite-card-ids (:id card)))))))
 
 
-;;; ------------------------------------------------------------ Filtered Fetch Fns ------------------------------------------------------------
+;;; ----------------------------------------------- Filtered Fetch Fns -----------------------------------------------
 
 (defn- cards:all
   "Return all `Cards`."
@@ -116,7 +119,8 @@
 
 (defn- cards:popular
   "All `Cards`, sorted by popularity (the total number of times they are viewed in `ViewLogs`).
-   (yes, this isn't actually filtering anything, but for the sake of simplicitiy it is included amongst the filter options for the time being)."
+  (yes, this isn't actually filtering anything, but for the sake of simplicitiy it is included amongst the filter
+  options for the time being)."
   []
   (cards-with-ids (map :model_id (db/select [ViewLog :model_id [:%count.* :count]]
                                    :model "card"
@@ -129,27 +133,27 @@
   (db/select Card, :archived true, {:order-by [[:%lower.name :asc]]}))
 
 (def ^:private filter-option->fn
-  "Functions that should be used to return cards for a given filter option. These functions are all be called with `model-id` as the sole paramenter;
-   functions that don't use the param discard it via `u/drop-first-arg`.
+  "Functions that should be used to return cards for a given filter option. These functions are all be called with
+  `model-id` as the sole paramenter; functions that don't use the param discard it via `u/drop-first-arg`.
 
      ((filter->option->fn :recent) model-id) -> (cards:recent)"
-  {:all           (u/drop-first-arg cards:all)
-   :mine          (u/drop-first-arg cards:mine)
-   :fav           (u/drop-first-arg cards:fav)
-   :database      cards:database
-   :table         cards:table
-   :recent        (u/drop-first-arg cards:recent)
-   :popular       (u/drop-first-arg cards:popular)
-   :archived      (u/drop-first-arg cards:archived)})
+  {:all      (u/drop-first-arg cards:all)
+   :mine     (u/drop-first-arg cards:mine)
+   :fav      (u/drop-first-arg cards:fav)
+   :database cards:database
+   :table    cards:table
+   :recent   (u/drop-first-arg cards:recent)
+   :popular  (u/drop-first-arg cards:popular)
+   :archived (u/drop-first-arg cards:archived)})
 
 (defn- ^:deprecated card-has-label? [label-slug card]
   (contains? (set (map :slug (:labels card))) label-slug))
 
 (defn- collection-slug->id [collection-slug]
   (when (seq collection-slug)
-    ;; special characters in the slugs are always URL-encoded when stored in the DB, e.g.
-    ;; "Obsługa klienta" becomes "obs%C5%82uga_klienta". But for some weird reason sometimes the slug is passed in like
-    ;; "obsługa_klientaa" (not URL-encoded) so go ahead and URL-encode the input as well so we can match either case
+    ;; special characters in the slugs are always URL-encoded when stored in the DB, e.g.  "Obsługa klienta" becomes
+    ;; "obs%C5%82uga_klienta". But for some weird reason sometimes the slug is passed in like "obsługa_klientaa" (not
+    ;; URL-encoded) so go ahead and URL-encode the input as well so we can match either case
     (api/check-404 (db/select-one-id Collection
                      {:where [:or [:= :slug collection-slug]
                               [:= :slug (codec/url-encode collection-slug)]]}))))
@@ -160,8 +164,9 @@
                   (hydrate :creator :collection)
                   hydrate-labels
                   hydrate-favorites)]
-    ;; Since labels and collections are hydrated in Clojure-land we need to wait until this point to apply label/collection filtering if applicable
-    ;; COLLECTION can optionally be an empty string which is used to repre
+    ;; Since labels and collections are hydrated in Clojure-land we need to wait until this point to apply
+    ;; label/collection filtering. If applicable COLLECTION can optionally be an empty string which is used to
+    ;; represent *no collection*
     (filter (cond
               collection-slug (let [collection-id (collection-slug->id collection-slug)]
                                 (comp (partial = collection-id) :collection_id))
@@ -170,34 +175,42 @@
             cards)))
 
 
-;;; ------------------------------------------------------------ Fetching a Card or Cards ------------------------------------------------------------
+;;; -------------------------------------------- Fetching a Card or Cards --------------------------------------------
 
 (def ^:private CardFilterOption
   "Schema for a valid card filter option."
   (apply s/enum (map name (keys filter-option->fn))))
 
 (api/defendpoint GET "/"
-  "Get all the `Cards`. Option filter param `f` can be used to change the set of Cards that are returned; default is `all`,
-   but other options include `mine`, `fav`, `database`, `table`, `recent`, `popular`, and `archived`. See corresponding implementation
-   functions above for the specific behavior of each filter option. :card_index:
+  "Get all the `Cards`. Option filter param `f` can be used to change the set of Cards that are returned; default is
+  `all`, but other options include `mine`, `fav`, `database`, `table`, `recent`, `popular`, and `archived`. See
+  corresponding implementation functions above for the specific behavior of each filter option. :card_index:
 
-   Optionally filter cards by LABEL or COLLECTION slug. (COLLECTION can be a blank string, to signify cards with *no collection* should be returned.)
+  Optionally filter cards by LABEL or COLLECTION slug. (COLLECTION can be a blank string, to signify cards with *no
+  collection* should be returned.)
 
-   NOTES:
+  NOTES:
 
-   *  Filtering by LABEL is considered *deprecated*, as `Labels` will be removed from an upcoming version of Metabase in favor of `Collections`.
-   *  LABEL and COLLECTION params are mutually exclusive; if both are specified, LABEL will be ignored and Cards will only be filtered by their `Collection`.
-   *  If no `Collection` exists with the slug COLLECTION, this endpoint will return a 404."
+  *  Filtering by LABEL is considered *deprecated*, as `Labels` will be removed from an upcoming version of Metabase
+     in favor of `Collections`.
+  *  LABEL and COLLECTION params are mutually exclusive; if both are specified, LABEL will be ignored and Cards will
+     only be filtered by their `Collection`.
+  *  If no `Collection` exists with the slug COLLECTION, this endpoint will return a 404."
   [f model_id label collection]
-  {f (s/maybe CardFilterOption), model_id (s/maybe su/IntGreaterThanZero), label (s/maybe su/NonBlankString), collection (s/maybe s/Str)}
+  {f          (s/maybe CardFilterOption)
+   model_id   (s/maybe su/IntGreaterThanZero)
+   label      (s/maybe su/NonBlankString)
+   collection (s/maybe s/Str)}
   (let [f (keyword f)]
     (when (contains? #{:database :table} f)
-      (api/checkp (integer? model_id) "id" (format "id is required parameter when filter mode is '%s'" (name f)))
+      (api/checkp (integer? model_id) "model_id" (format "model_id is a required parameter when filter mode is '%s'"
+                                                         (name f)))
       (case f
         :database (api/read-check Database model_id)
         :table    (api/read-check Database (db/select-one-field :db_id Table, :id model_id))))
     (->> (cards-for-filter-option f model_id label collection)
-         (filterv mi/can-read?)))) ; filterv because we want make sure all the filtering is done while current user perms set is still bound
+         ;; filterv because we want make sure all the filtering is done while current user perms set is still bound
+         (filterv mi/can-read?))))
 
 
 (api/defendpoint GET "/:id"
@@ -210,38 +223,40 @@
       (dissoc :actor_id)))
 
 
-;;; ------------------------------------------------------------ Saving Cards ------------------------------------------------------------
+;;; -------------------------------------------------- Saving Cards --------------------------------------------------
 
-;; When a new Card is saved, we wouldn't normally have the results metadata for it until the first time its query is ran.
-;; As a workaround, we'll calculate this metadata and return it with all query responses, and then let the frontend
-;; pass it back to us when saving or updating a Card.
-;; As a basic step to make sure the Metadata is valid we'll also pass a simple checksum and have the frontend pass it back to us.
-;; See the QP `results-metadata` middleware namespace for more details
+;; When a new Card is saved, we wouldn't normally have the results metadata for it until the first time its query is
+;; ran.  As a workaround, we'll calculate this metadata and return it with all query responses, and then let the
+;; frontend pass it back to us when saving or updating a Card.  As a basic step to make sure the Metadata is valid
+;; we'll also pass a simple checksum and have the frontend pass it back to us.  See the QP `results-metadata`
+;; middleware namespace for more details
 
-(s/defn ^:private ^:always-validate result-metadata-for-query :- results-metadata/ResultsMetadata
+(s/defn ^:private result-metadata-for-query :- results-metadata/ResultsMetadata
   "Fetch the results metadata for a QUERY by running the query and seeing what the QP gives us in return.
    This is obviously a bit wasteful so hopefully we can avoid having to do this."
   [query]
   (binding [qpi/*disable-qp-logging* true]
     (get-in (qp/process-query query) [:data :results_metadata :columns])))
 
-(s/defn ^:private ^:always-validate result-metadata :- (s/maybe results-metadata/ResultsMetadata)
+(s/defn ^:private result-metadata :- (s/maybe results-metadata/ResultsMetadata)
   "Get the right results metadata for this CARD. We'll check to see whether the METADATA passed in seems valid;
    otherwise we'll run the query ourselves to get the right values."
   [query metadata checksum]
   (let [valid-metadata? (and (results-metadata/valid-checksum? metadata checksum)
                              (s/validate results-metadata/ResultsMetadata metadata))]
-    (log/info (str "Card results metadata passed in to API is " (cond
-                                                                  valid-metadata? "VALID. Thanks!"
-                                                                  metadata        "INVALID. Running query to fetch correct metadata."
-                                                                  :else           "MISSING. Running query to fetch correct metadata.")))
+    (log/info (str "Card results metadata passed in to API is "
+                   (cond
+                     valid-metadata? "VALID. Thanks!"
+                     metadata        "INVALID. Running query to fetch correct metadata."
+                     :else           "MISSING. Running query to fetch correct metadata.")))
     (if valid-metadata?
       metadata
       (result-metadata-for-query query))))
 
 (api/defendpoint POST "/"
   "Create a new `Card`."
-  [:as {{:keys [dataset_query description display name visualization_settings collection_id result_metadata metadata_checksum]} :body}]
+  [:as {{:keys [dataset_query description display name visualization_settings collection_id result_metadata
+                metadata_checksum]} :body}]
   {name                   su/NonBlankString
    description            (s/maybe su/NonBlankString)
    display                su/NonBlankString
@@ -250,31 +265,35 @@
    result_metadata        (s/maybe results-metadata/ResultsMetadata)
    metadata_checksum      (s/maybe su/NonBlankString)}
   ;; check that we have permissions to run the query that we're trying to save
-  (api/check-403 (perms/set-has-full-permissions-for-set? @api/*current-user-permissions-set* (card/query-perms-set dataset_query :write)))
+  (api/check-403 (perms/set-has-full-permissions-for-set? @api/*current-user-permissions-set*
+                                                          (card/query-perms-set dataset_query :write)))
   ;; check that we have permissions for the collection we're trying to save this card to, if applicable
   (when collection_id
-    (api/check-403 (perms/set-has-full-permissions? @api/*current-user-permissions-set* (perms/collection-readwrite-path collection_id))))
+    (api/check-403 (perms/set-has-full-permissions? @api/*current-user-permissions-set*
+                                                    (perms/collection-readwrite-path collection_id))))
   ;; everything is g2g, now save the card
   (let [card (db/insert! Card
-         :creator_id             api/*current-user-id*
-         :dataset_query          dataset_query
-         :description            description
-         :display                display
-         :name                   name
-         :visualization_settings visualization_settings
-         :collection_id          collection_id
-         :result_metadata        (result-metadata dataset_query result_metadata metadata_checksum))]
-       (events/publish-event! :card-create card)
-       ;; include same information returned by GET /api/card/:id since frontend replaces the Card it currently has with returned one -- See #4283
-       (hydrate card :creator :dashboard_count :labels :can_write :collection)))
-
-
-;;; ------------------------------------------------------------ Updating Cards ------------------------------------------------------------
+               :creator_id             api/*current-user-id*
+               :dataset_query          dataset_query
+               :description            description
+               :display                display
+               :name                   name
+               :visualization_settings visualization_settings
+               :collection_id          collection_id
+               :result_metadata        (result-metadata dataset_query result_metadata metadata_checksum))]
+    (events/publish-event! :card-create card)
+    ;; include same information returned by GET /api/card/:id since frontend replaces the Card it currently has
+    ;; with returned one -- See #4283
+    (hydrate card :creator :dashboard_count :labels :can_write :collection)))
+
+
+;;; ------------------------------------------------- Updating Cards -------------------------------------------------
 
 (defn- check-permissions-for-collection
   "Check that we have permissions to add or remove cards from `Collection` with COLLECTION-ID."
   [collection-id]
-  (api/check-403 (perms/set-has-full-permissions? @api/*current-user-permissions-set* (perms/collection-readwrite-path collection-id))))
+  (api/check-403 (perms/set-has-full-permissions? @api/*current-user-permissions-set*
+                                                  (perms/collection-readwrite-path collection-id))))
 
 (defn- check-allowed-to-change-collection
   "If we're changing the `collection_id` of the Card, make sure we have write permissions for the new group."
@@ -287,7 +306,8 @@
   "Check that we have *data* permissions to run the QUERY in question."
   [query]
   {:pre [(map? query)]}
-  (api/check-403 (perms/set-has-full-permissions-for-set? @api/*current-user-permissions-set* (card/query-perms-set query :read))))
+  (api/check-403 (perms/set-has-full-permissions-for-set? @api/*current-user-permissions-set*
+                                                          (card/query-perms-set query :read))))
 
 (defn- check-allowed-to-modify-query
   "If the query is being modified, check that we have data permissions to run the query."
@@ -304,7 +324,8 @@
     (check-data-permissions-for-query (:dataset_query card))))
 
 (defn- check-allowed-to-change-embedding
-  "You must be a superuser to change the value of `enable_embedding` or `embedding_params`. Embedding must be enabled."
+  "You must be a superuser to change the value of `enable_embedding` or `embedding_params`. Embedding must be
+  enabled."
   [card enable-embedding? embedding-params]
   (when (or (and (not (nil? enable-embedding?))
                  (not= enable-embedding? (:enable_embedding card)))
@@ -315,9 +336,9 @@
 
 
 (defn- result-metadata-for-updating
-  "If CARD's query is being updated, return the value that should be saved for `result_metadata`. This *should* be passed
-   in to the API; if so, verifiy that it was correct (the checksum is valid); if not, go fetch it.
-   If the query has not changed, this returns `nil`, which means the value won't get updated below."
+  "If CARD's query is being updated, return the value that should be saved for `result_metadata`. This *should* be
+  passed in to the API; if so, verifiy that it was correct (the checksum is valid); if not, go fetch it.  If the query
+  has not changed, this returns `nil`, which means the value won't get updated below."
   [card query metadata checksum]
   (when (and query
              (not= query (:dataset_query card)))
@@ -325,7 +346,8 @@
     (result-metadata query metadata checksum)))
 
 (defn- publish-card-update!
-  "Publish an event appropriate for the update(s) done to this CARD (`:card-update`, or archiving/unarchiving events)."
+  "Publish an event appropriate for the update(s) done to this CARD (`:card-update`, or archiving/unarchiving
+  events)."
   [card archived?]
   (let [event (cond
                 ;; card was archived
@@ -419,7 +441,8 @@
 
 (api/defendpoint PUT "/:id"
   "Update a `Card`."
-  [id :as {{:keys [dataset_query description display name visualization_settings archived collection_id enable_embedding embedding_params result_metadata metadata_checksum], :as body} :body}]
+  [id :as {{:keys [dataset_query description display name visualization_settings archived collection_id
+                   enable_embedding embedding_params result_metadata metadata_checksum], :as body} :body}]
   {name                   (s/maybe su/NonBlankString)
    dataset_query          (s/maybe su/Map)
    display                (s/maybe su/NonBlankString)
@@ -438,38 +461,43 @@
     (check-allowed-to-unarchive card-before-update archived)
     (check-allowed-to-change-embedding card-before-update enable_embedding embedding_params)
     ;; make sure we have the correct `result_metadata`
-    (let [body (assoc body :result_metadata (result-metadata-for-updating card-before-update dataset_query result_metadata metadata_checksum))]
+    (let [body (assoc body :result_metadata (result-metadata-for-updating card-before-update dataset_query
+                                                                          result_metadata metadata_checksum))]
       ;; ok, now save the Card
       (db/update! Card id
-        ;; `collection_id` and `description` can be `nil` (in order to unset them). Other values should only be modified if they're passed in as non-nil
+        ;; `collection_id` and `description` can be `nil` (in order to unset them). Other values should only be
+        ;; modified if they're passed in as non-nil
         (u/select-keys-when body
           :present #{:collection_id :description}
-          :non-nil #{:dataset_query :display :name :visualization_settings :archived :enable_embedding :embedding_params :result_metadata})))
+          :non-nil #{:dataset_query :display :name :visualization_settings :archived :enable_embedding
+                     :embedding_params :result_metadata})))
     ;; Fetch the updated Card from the DB
     (let [card (Card id)]
 
       (delete-alerts-if-needed! card-before-update card)
 
       (publish-card-update! card archived)
-      ;; include same information returned by GET /api/card/:id since frontend replaces the Card it currently has with returned one -- See #4142
+      ;; include same information returned by GET /api/card/:id since frontend replaces the Card it currently has with
+      ;; returned one -- See #4142
       (hydrate card :creator :dashboard_count :labels :can_write :collection))))
 
 
-;;; ------------------------------------------------------------ Deleting Cards ------------------------------------------------------------
+;;; ------------------------------------------------- Deleting Cards -------------------------------------------------
 
-;; TODO - Pretty sure this endpoint is not actually used any more, since Cards are supposed to get archived (via PUT /api/card/:id) instead of deleted.
-;;        Should we remove this?
+;; TODO - Pretty sure this endpoint is not actually used any more, since Cards are supposed to get archived (via PUT
+;;        /api/card/:id) instead of deleted.  Should we remove this?
 (api/defendpoint DELETE "/:id"
   "Delete a `Card`."
   [id]
-  (log/warn "DELETE /api/card/:id is deprecated. Instead of deleting a Card, you should change its `archived` value via PUT /api/card/:id.")
+  (log/warn (str "DELETE /api/card/:id is deprecated. Instead of deleting a Card, "
+                 "you should change its `archived` value via PUT /api/card/:id."))
   (let [card (api/write-check Card id)]
     (db/delete! Card :id id)
     (events/publish-event! :card-delete (assoc card :actor_id api/*current-user-id*)))
   api/generic-204-no-content)
 
 
-;;; ------------------------------------------------------------ Favoriting ------------------------------------------------------------
+;;; --------------------------------------------------- Favoriting ---------------------------------------------------
 
 (api/defendpoint POST "/:card-id/favorite"
   "Favorite a Card."
@@ -487,7 +515,7 @@
   api/generic-204-no-content)
 
 
-;;; ------------------------------------------------------------ Editing Card Labels ------------------------------------------------------------
+;;; ---------------------------------------------- Editing Card Labels -----------------------------------------------
 
 
 (api/defendpoint POST "/:card-id/labels"
@@ -506,7 +534,7 @@
   {:status :ok})
 
 
-;;; ------------------------------------------------------------ Bulk Collections Update ------------------------------------------------------------
+;;; -------------------------------------------- Bulk Collections Update ---------------------------------------------
 
 (defn- move-cards-to-collection! [new-collection-id-or-nil card-ids]
   ;; if moving to a collection, make sure we have write perms for it
@@ -530,25 +558,28 @@
       :collection_id new-collection-id-or-nil)))
 
 (api/defendpoint POST "/collections"
-  "Bulk update endpoint for Card Collections. Move a set of `Cards` with CARD_IDS into a `Collection` with COLLECTION_ID,
-   or remove them from any Collections by passing a `null` COLLECTION_ID."
+  "Bulk update endpoint for Card Collections. Move a set of `Cards` with CARD_IDS into a `Collection` with
+  COLLECTION_ID, or remove them from any Collections by passing a `null` COLLECTION_ID."
   [:as {{:keys [card_ids collection_id]} :body}]
   {card_ids [su/IntGreaterThanZero], collection_id (s/maybe su/IntGreaterThanZero)}
   (move-cards-to-collection! collection_id card_ids)
   {:status :ok})
 
 
-;;; ------------------------------------------------------------ Running a Query ------------------------------------------------------------
+;;; ------------------------------------------------ Running a Query -------------------------------------------------
 
 (defn- query-magic-ttl
-  "Compute a 'magic' cache TTL time (in seconds) for QUERY by multipling its historic average execution times by the `query-caching-ttl-ratio`.
-   If the TTL is less than a second, this returns `nil` (i.e., the cache should not be utilized.)"
+  "Compute a 'magic' cache TTL time (in seconds) for QUERY by multipling its historic average execution times by the
+  `query-caching-ttl-ratio`. If the TTL is less than a second, this returns `nil` (i.e., the cache should not be
+  utilized.)"
   [query]
   (when-let [average-duration (query/average-execution-time-ms (qputil/query-hash query))]
     (let [ttl-seconds (Math/round (float (/ (* average-duration (public-settings/query-caching-ttl-ratio))
                                             1000.0)))]
       (when-not (zero? ttl-seconds)
-        (log/info (format "Question's average execution duration is %d ms; using 'magic' TTL of %d seconds" average-duration ttl-seconds) (u/emoji "💾"))
+        (log/info (format "Question's average execution duration is %d ms; using 'magic' TTL of %d seconds"
+                          average-duration ttl-seconds)
+                  (u/emoji "💾"))
         ttl-seconds))))
 
 (defn- query-for-card [card parameters constraints]
@@ -584,7 +615,8 @@
     (run-query-for-card card-id, :parameters parameters)))
 
 (api/defendpoint POST "/:card-id/query/:export-format"
-  "Run the query associated with a Card, and return its results as a file in the specified format. Note that this expects the parameters as serialized JSON in the 'parameters' parameter"
+  "Run the query associated with a Card, and return its results as a file in the specified format. Note that this
+  expects the parameters as serialized JSON in the 'parameters' parameter"
   [card-id export-format parameters]
   {parameters    (s/maybe su/JSONString)
    export-format dataset-api/ExportFormat}
@@ -596,12 +628,12 @@
         :context     (dataset-api/export-format->context export-format)))))
 
 
-;;; ------------------------------------------------------------ Sharing is Caring ------------------------------------------------------------
+;;; ----------------------------------------------- Sharing is Caring ------------------------------------------------
 
 (api/defendpoint POST "/:card-id/public_link"
-  "Generate publically-accessible links for this Card. Returns UUID to be used in public links.
-   (If this Card has already been shared, it will return the existing public link rather than creating a new one.)
-   Public sharing must be enabled."
+  "Generate publically-accessible links for this Card. Returns UUID to be used in public links. (If this Card has
+  already been shared, it will return the existing public link rather than creating a new one.)  Public sharing must
+  be enabled."
   [card-id]
   (api/check-superuser)
   (api/check-public-sharing-enabled)
@@ -631,7 +663,8 @@
   (db/select [Card :name :id :public_uuid], :public_uuid [:not= nil], :archived false))
 
 (api/defendpoint GET "/embeddable"
-  "Fetch a list of Cards where `enable_embedding` is `true`. The cards can be embedded using the embedding endpoints and a signed JWT."
+  "Fetch a list of Cards where `enable_embedding` is `true`. The cards can be embedded using the embedding endpoints
+  and a signed JWT."
   []
   (api/check-superuser)
   (api/check-embedding-enabled)
diff --git a/src/metabase/api/database.clj b/src/metabase/api/database.clj
index 84ec5ba8837a06a390b591fde788e71021ec8a38..bd885a2eb6685145658338709b19d6c23e4ac145 100644
--- a/src/metabase/api/database.clj
+++ b/src/metabase/api/database.clj
@@ -162,7 +162,7 @@
        "Map of expanded schedule maps")
     "value must be a valid map of schedule maps for a DB."))
 
-(s/defn ^:private ^:always-validate expanded-schedules [db :- DatabaseInstance]
+(s/defn ^:private expanded-schedules [db :- DatabaseInstance]
   {:cache_field_values (cron-util/cron-string->schedule-map (:cache_field_values_schedule db))
    :metadata_sync      (cron-util/cron-string->schedule-map (:metadata_sync_schedule db))})
 
@@ -322,7 +322,7 @@
                             (:name field)))]
     (contains? driver-props "ssl")))
 
-(s/defn ^:private ^:always-validate test-connection-details :- su/Map
+(s/defn ^:private test-connection-details :- su/Map
   "Try a making a connection to database ENGINE with DETAILS.
    Tries twice: once with SSL, and a second time without if the first fails.
    If either attempt is successful, returns the details used to successfully connect.
@@ -348,7 +348,7 @@
   {(s/optional-key :metadata_sync_schedule)      cron-util/CronScheduleString
    (s/optional-key :cache_field_values_schedule) cron-util/CronScheduleString})
 
-(s/defn ^:always-validate schedule-map->cron-strings :- CronSchedulesMap
+(s/defn schedule-map->cron-strings :- CronSchedulesMap
   "Convert a map of `:schedules` as passed in by the frontend to a map of cron strings with the approriate keys for
    Database. This map can then be merged directly inserted into the DB, or merged with a map of other columns to
    insert/update."
diff --git a/src/metabase/api/dataset.clj b/src/metabase/api/dataset.clj
index 27c8c73b7e4ad19033af5408d0e58412a14d4bf8..43ef5e1df397e57b88df117debb25a392ebb4bec 100644
--- a/src/metabase/api/dataset.clj
+++ b/src/metabase/api/dataset.clj
@@ -22,7 +22,7 @@
   (:import [java.io ByteArrayInputStream ByteArrayOutputStream]
            org.apache.poi.ss.usermodel.Cell))
 
-;;; ------------------------------------------------------------ Constants ------------------------------------------------------------
+;;; --------------------------------------------------- Constants ----------------------------------------------------
 
 (def ^:private ^:const max-results-bare-rows
   "Maximum number of rows to return specifically on :rows type queries via the API."
@@ -38,7 +38,7 @@
    :max-results-bare-rows max-results-bare-rows})
 
 
-;;; ------------------------------------------------------------ Running a Query Normally ------------------------------------------------------------
+;;; -------------------------------------------- Running a Query Normally --------------------------------------------
 
 (defn- query->source-card-id
   "Return the ID of the Card used as the \"source\" query of this query, if applicable; otherwise return `nil`.
@@ -64,16 +64,17 @@
       {:executed-by api/*current-user-id*, :context :ad-hoc, :card-id source-card-id, :nested? (boolean source-card-id)})))
 
 
-;;; ------------------------------------------------------------ Downloading Query Results in Other Formats ------------------------------------------------------------
+;;; ----------------------------------- Downloading Query Results in Other Formats -----------------------------------
 
-;; add a generic implementation for the method that writes values to XLSX cells that just piggybacks off the implementations
-;; we've already defined for encoding things as JSON. These implementations live in `metabase.middleware`.
+;; add a generic implementation for the method that writes values to XLSX cells that just piggybacks off the
+;; implementations we've already defined for encoding things as JSON. These implementations live in
+;; `metabase.middleware`.
 (defmethod spreadsheet/set-cell! Object [^Cell cell, value]
   (when (= (.getCellType cell) Cell/CELL_TYPE_FORMULA)
     (.setCellType cell Cell/CELL_TYPE_STRING))
-  ;; stick the object in a JSON map and encode it, which will force conversion to a string. Then unparse that JSON and use the resulting value
-  ;; as the cell's new String value.
-  ;; There might be some more efficient way of doing this but I'm not sure what it is.
+  ;; stick the object in a JSON map and encode it, which will force conversion to a string. Then unparse that JSON and
+  ;; use the resulting value as the cell's new String value. There might be some more efficient way of doing this but
+  ;; I'm not sure what it is.
   (.setCellValue cell (str (-> (json/generate-string {:v value})
                                (json/parse-string keyword)
                                :v))))
@@ -113,9 +114,10 @@
   (apply s/enum (keys export-formats)))
 
 (defn export-format->context
-  "Return the `:context` that should be used when saving a QueryExecution triggered by a request to download results in EXPORT-FORAMT.
+  "Return the `:context` that should be used when saving a QueryExecution triggered by a request to download results
+  in EXPORT-FORAMT.
 
-     (export-format->context :json) ;-> :json-download"
+    (export-format->context :json) ;-> :json-download"
   [export-format]
   (or (get-in export-formats [export-format :context])
       (throw (Exception. (str "Invalid export format: " export-format)))))
@@ -154,15 +156,15 @@
         {:executed-by api/*current-user-id*, :context (export-format->context export-format)}))))
 
 
-;;; ------------------------------------------------------------ Other Endpoints ------------------------------------------------------------
+;;; ------------------------------------------------ Other Endpoints -------------------------------------------------
 
 ;; TODO - this is no longer used. Should we remove it?
 (api/defendpoint POST "/duration"
   "Get historical query execution duration."
   [:as {{:keys [database], :as query} :body}]
   (api/read-check Database database)
-  ;; try calculating the average for the query as it was given to us, otherwise with the default constraints if there's no data there.
-  ;; if we still can't find relevant info, just default to 0
+  ;; try calculating the average for the query as it was given to us, otherwise with the default constraints if
+  ;; there's no data there. If we still can't find relevant info, just default to 0
   {:average (or (query/average-execution-time-ms (qputil/query-hash query))
                 (query/average-execution-time-ms (qputil/query-hash (assoc query :constraints default-query-constraints)))
                 0)})
diff --git a/src/metabase/api/embed.clj b/src/metabase/api/embed.clj
index 41e1276225963f599a29c2432656928a5ce67af8..a6a404574d09203b429e085ad5b73a7ad5deb42e 100644
--- a/src/metabase/api/embed.clj
+++ b/src/metabase/api/embed.clj
@@ -35,7 +35,7 @@
             [schema.core :as s]
             [toucan.db :as db]))
 
-;;; ------------------------------------------------------------ Param Checking ------------------------------------------------------------
+;;; ------------------------------------------------- Param Checking -------------------------------------------------
 
 (defn- validate-params-are-allowed
   "Check that the conditions specified by `object-embedding-params` are satisfied."
@@ -64,15 +64,15 @@
         [400 (format "Unknown parameter %s." k)]))))
 
 (defn- validate-param-sets
-  "Validate that sets of params passed as part of the JWT token and by the user (as query params, i.e. as part of the URL)
-   are valid for the OBJECT-EMBEDDING-PARAMS. TOKEN-PARAMS and USER-PARAMS should be sets of all valid param keys specified in
-   the JWT or by the user, respectively."
+  "Validate that sets of params passed as part of the JWT token and by the user (as query params, i.e. as part of the
+  URL) are valid for the OBJECT-EMBEDDING-PARAMS. TOKEN-PARAMS and USER-PARAMS should be sets of all valid param keys
+  specified in the JWT or by the user, respectively."
   [object-embedding-params token-params user-params]
   ;; TODO - maybe make this log/debug once embedding is wrapped up
-  (log/info "Validating params for embedded object:\n"
-            "object embedding params:" object-embedding-params
-            "token params:"            token-params
-            "user params:"             user-params)
+  (log/debug "Validating params for embedded object:\n"
+             "object embedding params:" object-embedding-params
+             "token params:"            token-params
+             "user params:"             user-params)
   (validate-params-are-allowed object-embedding-params token-params user-params)
   (validate-params-exist object-embedding-params (set/union token-params user-params)))
 
@@ -83,10 +83,11 @@
        (or (not (string? v))
            (not (str/blank? v)))))
 
-(s/defn ^:private ^:always-validate validate-params
-  "Validate that the TOKEN-PARAMS passed in the JWT and the USER-PARAMS (passed as part of the URL) are allowed, and that ones that
-   are required are specified by checking them against a Card or Dashboard's OBJECT-EMBEDDING-PARAMS (the object's value of
-   `:embedding_params`). Throws a 400 if any of the checks fail. If all checks are successful, returns a merged parameters map."
+(s/defn ^:private validate-params
+  "Validate that the TOKEN-PARAMS passed in the JWT and the USER-PARAMS (passed as part of the URL) are allowed, and
+  that ones that are required are specified by checking them against a Card or Dashboard's OBJECT-EMBEDDING-PARAMS
+  (the object's value of `:embedding_params`). Throws a 400 if any of the checks fail. If all checks are successful,
+  returns a merged parameters map."
   [object-embedding-params :- su/EmbeddingParams, token-params user-params]
   (validate-param-sets object-embedding-params
                        (set (keys (m/filter-vals valid-param? token-params)))
@@ -95,7 +96,7 @@
   (merge user-params token-params))
 
 
-;;; ------------------------------------------------------------ Other Param Util Fns ------------------------------------------------------------
+;;; ---------------------------------------------- Other Param Util Fns ----------------------------------------------
 
 (defn- remove-params-in-set
   "Remove any PARAMS from the list whose `:slug` is in the PARAMS-TO-REMOVE set."
@@ -104,9 +105,10 @@
         :when (not (contains? params-to-remove (keyword (:slug param))))]
     param))
 
-(s/defn ^:private ^:always-validate remove-locked-and-disabled-params
-  "Remove the `:parameters` for DASHBOARD-OR-CARD that listed as `disabled` or `locked` in the EMBEDDING-PARAMS whitelist,
-   or not present in the whitelist. This is done so the frontend doesn't display widgets for params the user can't set."
+(s/defn ^:private remove-locked-and-disabled-params
+  "Remove the `:parameters` for DASHBOARD-OR-CARD that listed as `disabled` or `locked` in the EMBEDDING-PARAMS
+  whitelist, or not present in the whitelist. This is done so the frontend doesn't display widgets for params the user
+  can't set."
   [dashboard-or-card, embedding-params :- su/EmbeddingParams]
   (let [params-to-remove (set (concat (for [[param status] embedding-params
                                             :when          (not= status "enabled")]
@@ -144,7 +146,8 @@
   (update card :parameters concat (template-tag-parameters card)))
 
 (defn- apply-parameter-values
-  "Adds `value` to parameters with `slug` matching a key in `parameter-values` and removes parameters without a `value`"
+  "Adds `value` to parameters with `slug` matching a key in `parameter-values` and removes parameters without a
+   `value`."
   [parameters parameter-values]
   (for [param parameters
         :let  [value (get parameter-values (keyword (:slug param)))]
@@ -163,9 +166,9 @@
   "Returns parameters for a card on a dashboard with `:target` resolved via `:parameter_mappings`."
   [dashboard-id dashcard-id card-id]
   (let [param-id->param (u/key-by :id (db/select-one-field :parameters Dashboard :id dashboard-id))]
-    ;; throw a 404 if there's no matching DashboardCard so people can't get info about other Cards that aren't in this Dashboard
-    ;; we don't need to check that card-id matches the DashboardCard because we might be trying to get param info for
-    ;; a series belonging to this dashcard (card-id might be for a series)
+    ;; throw a 404 if there's no matching DashboardCard so people can't get info about other Cards that aren't in this
+    ;; Dashboard we don't need to check that card-id matches the DashboardCard because we might be trying to get param
+    ;; info for a series belonging to this dashcard (card-id might be for a series)
     (for [param-mapping (api/check-404 (db/select-one-field :parameter_mappings DashboardCard
                                          :id           dashcard-id
                                          :dashboard_id dashboard-id))
@@ -175,7 +178,7 @@
       (assoc param :target (:target param-mapping)))))
 
 
-;;; ------------------------------------------------------------ Card Fns used by both /api/embed and /api/preview_embed ------------------------------------------------------------
+;;; ---------------------------- Card Fns used by both /api/embed and /api/preview_embed -----------------------------
 
 (defn card-for-unsigned-token
   "Return the info needed for embedding about Card specified in TOKEN.
@@ -201,7 +204,7 @@
     (apply public-api/run-query-for-card-with-id card-id parameters, :context :embedded-question, options)))
 
 
-;;; ------------------------------------------------------------ Dashboard Fns used by both /api/embed and /api/preview_embed ------------------------------------------------------------
+;;; -------------------------- Dashboard Fns used by both /api/embed and /api/preview_embed --------------------------
 
 (defn dashboard-for-unsigned-token
   "Return the info needed for embedding about Dashboard specified in TOKEN.
@@ -219,13 +222,14 @@
   "Return results for running the query belonging to a DashboardCard."
   {:style/indent 0}
   [& {:keys [dashboard-id dashcard-id card-id embedding-params token-params query-params]}]
-  {:pre [(integer? dashboard-id) (integer? dashcard-id) (integer? card-id) (u/maybe? map? embedding-params) (map? token-params) (map? query-params)]}
+  {:pre [(integer? dashboard-id) (integer? dashcard-id) (integer? card-id) (u/maybe? map? embedding-params)
+         (map? token-params) (map? query-params)]}
   (let [parameter-values (validate-params embedding-params token-params query-params)
         parameters       (apply-parameter-values (resolve-dashboard-parameters dashboard-id dashcard-id card-id) parameter-values)]
     (public-api/public-dashcard-results dashboard-id card-id parameters, :context :embedded-dashboard)))
 
 
-;;; ------------------------------------------------------------ Other /api/embed-specific utility fns ------------------------------------------------------------
+;;; ------------------------------------- Other /api/embed-specific utility fns --------------------------------------
 
 (defn- check-embedding-enabled-for-object
   "Check that embedding is enabled, that OBJECT exists, and embedding for OBJECT is enabled."
@@ -242,7 +246,7 @@
 (def ^:private ^{:arglists '([card-id])}      check-embedding-enabled-for-card      (partial check-embedding-enabled-for-object Card))
 
 
-;;; ------------------------------------------------------------ /api/embed/card endpoints ------------------------------------------------------------
+;;; ------------------------------------------- /api/embed/card endpoints --------------------------------------------
 
 (api/defendpoint GET "/card/:token"
   "Fetch a Card via a JSON Web Token signed with the `embedding-secret-key`.
@@ -257,7 +261,8 @@
 
 
 (defn- run-query-for-unsigned-token
-  "Run the query belonging to Card identified by UNSIGNED-TOKEN. Checks that embedding is enabled both globally and for this Card."
+  "Run the query belonging to Card identified by UNSIGNED-TOKEN. Checks that embedding is enabled both globally and
+  for this Card."
   [unsigned-token query-params & options]
   (let [card-id (eu/get-in-unsigned-token-or-throw unsigned-token [:resource :question])]
     (check-embedding-enabled-for-card card-id)
@@ -288,7 +293,7 @@
     (run-query-for-unsigned-token (eu/unsign token) query-params, :constraints nil)))
 
 
-;;; ------------------------------------------------------------ /api/embed/dashboard endpoints ------------------------------------------------------------
+;;; ----------------------------------------- /api/embed/dashboard endpoints -----------------------------------------
 
 
 (api/defendpoint GET "/dashboard/:token"
@@ -304,7 +309,8 @@
 
 
 (api/defendpoint GET "/dashboard/:token/dashcard/:dashcard-id/card/:card-id"
-  "Fetch the results of running a Card belonging to a Dashboard using a JSON Web Token signed with the `embedding-secret-key`.
+  "Fetch the results of running a Card belonging to a Dashboard using a JSON Web Token signed with the
+   `embedding-secret-key`.
 
    Token should have the following format:
 
diff --git a/src/metabase/db/metadata_queries.clj b/src/metabase/db/metadata_queries.clj
index ccf32485a608c8708a3248261ec4646473d7ae63..8f7b9d412ca89804f6b4979bb6f7953beae47841 100644
--- a/src/metabase/db/metadata_queries.clj
+++ b/src/metabase/db/metadata_queries.clj
@@ -24,7 +24,8 @@
 (defn- field-query [{table-id :table_id} query]
   {:pre [(integer? table-id)]}
   (qp-query (db/select-one-field :db_id Table, :id table-id)
-            ;; this seeming useless `merge` statement IS in fact doing something important. `ql/query` is a threading macro for building queries. Do not remove
+            ;; this seeming useless `merge` statement IS in fact doing something important. `ql/query` is a threading
+            ;; macro for building queries. Do not remove
             (ql/query (merge query)
                       (ql/source-table table-id))))
 
diff --git a/src/metabase/db/migrations.clj b/src/metabase/db/migrations.clj
index 5fa638637bc6b9837b17b76d6cba3ee7fe022c44..ec688001f41f67b964197e370e22bc076fa9564b 100644
--- a/src/metabase/db/migrations.clj
+++ b/src/metabase/db/migrations.clj
@@ -19,7 +19,7 @@
              [activity :refer [Activity]]
              [card :refer [Card]]
              [dashboard-card :refer [DashboardCard]]
-             [database :refer [Database]]
+             [database :refer [Database virtual-id]]
              [field :refer [Field]]
              [permissions :as perms :refer [Permissions]]
              [permissions-group :as perm-group]
@@ -329,8 +329,9 @@
 ;; There was a bug (#5998) preventing database_id from being persisted with
 ;; native query type cards. This migration populates all of the Cards
 ;; missing those database ids
-(defmigration ^{:author "senior", :added "0.26.1"} populate-card-database-id
+(defmigration ^{:author "senior", :added "0.27.0"} populate-card-database-id
   (doseq [[db-id cards] (group-by #(get-in % [:dataset_query :database])
-                                  (db/select [Card :dataset_query :id] :database_id [:= nil]))]
+                                  (db/select [Card :dataset_query :id] :database_id [:= nil]))
+          :when (not= db-id virtual-id)]
     (db/update-where! Card {:id [:in (map :id cards)]}
       :database_id db-id)))
diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj
index 44a926d311cfc7f4fab40f2bfbfa5022e76d8ae2..99a42e013231ad71e62313b1d6c81a24db5a385b 100644
--- a/src/metabase/driver.clj
+++ b/src/metabase/driver.clj
@@ -1,4 +1,15 @@
 (ns metabase.driver
+  "Metabase Drivers handle various things we need to do with connected data warehouse databases, including things like
+  introspecting their schemas and processing and running MBQL queries. Each Metabase driver lives in a namespace like
+  `metabase.driver.<driver>`, e.g. `metabase.driver.postgres`. Each driver must implement the `IDriver` protocol
+  below.
+
+  JDBC-based drivers for SQL databases can use the 'Generic SQL' driver which acts as a sort of base class and
+  implements most of this protocol. Instead, those drivers should implement the `ISQLDriver` protocol which can be
+  found in `metabase.driver.generic-sql`.
+
+  This namespace also contains various other functions for fetching drivers, testing database connections, and the
+  like."
   (:require [clj-time.format :as tformat]
             [clojure.tools.logging :as log]
             [medley.core :as m]
@@ -417,7 +428,7 @@
   10000)
 
 ;; TODO - move this to the metadata-queries namespace or something like that instead
-(s/defn ^:always-validate ^{:style/indent 1} table-rows-sample :- (s/maybe si/TableSample)
+(s/defn ^{:style/indent 1} table-rows-sample :- (s/maybe si/TableSample)
   "Run a basic MBQL query to fetch a sample of rows belonging to a Table."
   [table :- si/TableInstance, fields :- [si/FieldInstance]]
   (let [results ((resolve 'metabase.query-processor/process-query)
diff --git a/src/metabase/driver/generic_sql.clj b/src/metabase/driver/generic_sql.clj
index bbd1df8b302960bbca7babdc434eed6fbb64d4a8..9b5071abd4cf1f65e339077afacc9e39fdfee3b1 100644
--- a/src/metabase/driver/generic_sql.clj
+++ b/src/metabase/driver/generic_sql.clj
@@ -1,4 +1,5 @@
 (ns metabase.driver.generic-sql
+  "Shared code for drivers for SQL databases using their respective JDBC drivers under the hood."
   (:require [clojure
              [set :as set]
              [string :as str]]
@@ -31,16 +32,20 @@
    Methods marked *OPTIONAL* have default implementations in `ISQLDriverDefaultsMixin`."
 
   (active-tables ^java.util.Set [this, ^DatabaseMetaData metadata]
-    "*OPTIONAL* Return a set of maps containing information about the active tables/views, collections, or equivalent that currently exist in DATABASE.
-     Each map should contain the key `:name`, which is the string name of the table. For databases that have a concept of schemas,
-     this map should also include the string name of the table's `:schema`.
-
-   Two different implementations are provided in this namespace: `fast-active-tables` (the default), and `post-filtered-active-tables`. You should be fine using
-   the default, but refer to the documentation for those functions for more details on the differences.")
-
-  ;; The following apply-* methods define how the SQL Query Processor handles given query clauses. Each method is called when a matching clause is present
-  ;; in QUERY, and should return an appropriately modified version of KORMA-QUERY. Most drivers can use the default implementations for all of these methods,
-  ;; but some may need to override one or more (e.g. SQL Server needs to override the behavior of `apply-limit`, since T-SQL uses `TOP` instead of `LIMIT`).
+    "*OPTIONAL* Return a set of maps containing information about the active tables/views, collections, or equivalent
+     that currently exist in DATABASE. Each map should contain the key `:name`, which is the string name of the table.
+     For databases that have a concept of schemas, this map should also include the string name of the table's
+     `:schema`.
+
+   Two different implementations are provided in this namespace: `fast-active-tables` (the default), and
+   `post-filtered-active-tables`. You should be fine using the default, but refer to the documentation for those
+   functions for more details on the differences.")
+
+  ;; The following apply-* methods define how the SQL Query Processor handles given query clauses. Each method is
+  ;; called when a matching clause is present in QUERY, and should return an appropriately modified version of
+  ;; `HONEYSQL-FORM`. Most drivers can use the default implementations for all of these methods, but some may need to
+  ;; override one or more (e.g. SQL Server needs to override the behavior of `apply-limit`, since T-SQL uses `TOP`
+  ;; instead of `LIMIT`).
   (apply-aggregation [this honeysql-form, ^Map query] "*OPTIONAL*.")
   (apply-breakout    [this honeysql-form, ^Map query] "*OPTIONAL*.")
   (apply-fields      [this honeysql-form, ^Map query] "*OPTIONAL*.")
@@ -61,57 +66,64 @@
     "Given a `Database` DETAILS-MAP, return a JDBC connection spec.")
 
   (current-datetime-fn [this]
-    "*OPTIONAL*. HoneySQL form that should be used to get the current `DATETIME` (or equivalent). Defaults to `:%now`.")
+    "*OPTIONAL*. HoneySQL form that should be used to get the current `DATETIME` (or equivalent). Defaults to
+     `:%now`.")
 
   (date [this, ^Keyword unit, field-or-value]
-    "Return a HoneySQL form for truncating a date or timestamp field or value to a given resolution, or extracting a date component.")
+    "Return a HoneySQL form for truncating a date or timestamp field or value to a given resolution, or extracting a
+     date component.")
 
   (excluded-schemas ^java.util.Set [this]
     "*OPTIONAL*. Set of string names of schemas to skip syncing tables from.")
 
   (field->identifier [this, ^FieldInstance field]
     "*OPTIONAL*. Return a HoneySQL form that should be used as the identifier for FIELD.
-     The default implementation returns a keyword generated by from the components returned by `field/qualified-name-components`.
-     Other drivers like BigQuery need to do additional qualification, e.g. the dataset name as well.
-     (At the time of this writing, this is only used by the SQL parameters implementation; in the future it will probably be used in more places as well.)")
+     The default implementation returns a keyword generated by from the components returned by
+     `field/qualified-name-components`. Other drivers like BigQuery need to do additional qualification, e.g. the
+     dataset name as well. (At the time of this writing, this is only used by the SQL parameters implementation; in
+     the future it will probably be used in more places as well.)")
 
   (field->alias ^String [this, ^Field field]
-    "*OPTIONAL*. Return the alias that should be used to for FIELD, i.e. in an `AS` clause. The default implementation calls `name`, which
-     returns the *unqualified* name of `Field`.
+    "*OPTIONAL*. Return the alias that should be used to for FIELD, i.e. in an `AS` clause. The default implementation
+     calls `name`, which returns the *unqualified* name of `Field`.
 
      Return `nil` to prevent FIELD from being aliased.")
 
   (prepare-sql-param [this obj]
-    "*OPTIONAL*. Do any neccesary type conversions, etc. to an object being passed as a prepared statment argument in a parameterized raw SQL query.
-     For example, a raw SQL query with a date param, `x`, e.g. `WHERE date > {{x}}`, is converted to SQL like `WHERE date > ?`, and the value of
-     `x` is passed as a `java.sql.Timestamp`. Some databases, notably SQLite, don't work with `Timestamps`, and dates must be passed as string literals
-     instead; the SQLite driver overrides this method to convert dates as needed.
+    "*OPTIONAL*. Do any neccesary type conversions, etc. to an object being passed as a prepared statment argument in
+     a parameterized raw SQL query. For example, a raw SQL query with a date param, `x`, e.g. `WHERE date > {{x}}`, is
+     converted to SQL like `WHERE date > ?`, and the value of `x` is passed as a `java.sql.Timestamp`. Some databases,
+     notably SQLite, don't work with `Timestamps`, and dates must be passed as string literals instead; the SQLite
+     driver overrides this method to convert dates as needed.
 
   The default implementation is `identity`.
 
-  NOTE - This method is only used for parameters in raw SQL queries. It's not needed for MBQL queries because other functions like `prepare-value` are
-  used for similar purposes; at some point in the future, we might be able to combine them into a single method used in both places.")
+  NOTE - This method is only used for parameters in raw SQL queries. It's not needed for MBQL queries because other
+  functions like `prepare-value` are used for similar purposes; at some point in the future, we might be able to
+  combine them into a single method used in both places.")
 
   (prepare-value [this, ^Value value]
-    "*OPTIONAL*. Prepare a value (e.g. a `String` or `Integer`) that will be used in a HoneySQL form. By default, this returns VALUE's `:value` as-is, which
-     is eventually passed as a parameter in a prepared statement. Drivers such as BigQuery that don't support prepared statements can skip this
-     behavior by returning a HoneySQL `raw` form instead, or other drivers can perform custom type conversion as appropriate.")
+    "*OPTIONAL*. Prepare a value (e.g. a `String` or `Integer`) that will be used in a HoneySQL form. By default, this
+     returns VALUE's `:value` as-is, which is eventually passed as a parameter in a prepared statement. Drivers such
+     as BigQuery that don't support prepared statements can skip this behavior by returning a HoneySQL `raw` form
+     instead, or other drivers can perform custom type conversion as appropriate.")
 
   (quote-style ^clojure.lang.Keyword [this]
-    "*OPTIONAL*. Return the quoting style that should be used by [HoneySQL](https://github.com/jkk/honeysql) when building a SQL statement.
-      Defaults to `:ansi`, but other valid options are `:mysql`, `:sqlserver`, `:oracle`, and `:h2` (added in `metabase.util.honeysql-extensions`;
-      like `:ansi`, but uppercases the result).
+    "*OPTIONAL*. Return the quoting style that should be used by [HoneySQL](https://github.com/jkk/honeysql) when
+     building a SQL statement. Defaults to `:ansi`, but other valid options are `:mysql`, `:sqlserver`, `:oracle`, and
+     `:h2` (added in `metabase.util.honeysql-extensions`; like `:ansi`, but uppercases the result).
 
         (hsql/format ... :quoting (quote-style driver))")
 
   (set-timezone-sql ^String [this]
-    "*OPTIONAL*. This should be a format string containing a SQL statement to be used to set the timezone for the current transaction.
-     The `%s` will be replaced with a string literal for a timezone, e.g. `US/Pacific`.
+    "*OPTIONAL*. This should be a format string containing a SQL statement to be used to set the timezone for the
+     current transaction. The `%s` will be replaced with a string literal for a timezone, e.g. `US/Pacific.`
 
        \"SET @@session.timezone = %s;\"")
 
   (stddev-fn ^clojure.lang.Keyword [this]
-    "*OPTIONAL*. Keyword name of the SQL function that should be used to do a standard deviation aggregation. Defaults to `:STDDEV`.")
+    "*OPTIONAL*. Keyword name of the SQL function that should be used to do a standard deviation aggregation. Defaults
+     to `:STDDEV`.")
 
   (string-length-fn ^clojure.lang.Keyword [this, ^Keyword field-key]
     "Return a HoneySQL form appropriate for getting the length of a `Field` identified by fully-qualified FIELD-KEY.
@@ -120,8 +132,9 @@
       (hsql/call :length (hx/cast :VARCHAR field-key))")
 
   (unix-timestamp->timestamp [this, field-or-value, ^Keyword seconds-or-milliseconds]
-    "Return a HoneySQL form appropriate for converting a Unix timestamp integer field or value to an proper SQL `Timestamp`.
-     SECONDS-OR-MILLISECONDS refers to the resolution of the int in question and with be either `:seconds` or `:milliseconds`."))
+    "Return a HoneySQL form appropriate for converting a Unix timestamp integer field or value to an proper SQL
+     `Timestamp`. SECONDS-OR-MILLISECONDS refers to the resolution of the int in question and with be either
+     `:seconds` or `:milliseconds`."))
 
 
 ;; This does something important for the Crate driver, apparently (what?)
@@ -181,9 +194,9 @@
   "If DETAILS contains an `:addtional-options` key, append those options to the connection string in CONNECTION-SPEC.
    (Some drivers like MySQL provide this details field to allow special behavior where needed).
 
-   Optionally specify SEPERATOR-STYLE, which defaults to `:url` (e.g. `?a=1&b=2`). You may instead set it to `:semicolon`,
-   which will separate different options with semicolons instead (e.g. `;a=1;b=2`). (While most drivers require the former
-   style, some require the latter.)"
+   Optionally specify SEPERATOR-STYLE, which defaults to `:url` (e.g. `?a=1&b=2`). You may instead set it to
+   `:semicolon`, which will separate different options with semicolons instead (e.g. `;a=1;b=2`). (While most drivers
+   require the former style, some require the latter.)"
   {:arglists '([connection-spec] [connection-spec details & {:keys [seperator-style]}])}
   ;; single arity provided for cases when `connection-spec` is built by applying simple transformations to `details`
   ([connection-spec]
@@ -232,7 +245,8 @@
                               :quoting             (quote-style driver)
                               :allow-dashed-names? true))
                           (catch Throwable e
-                            (log/error (u/format-color 'red "Invalid HoneySQL form:\n%s" (u/pprint-to-str honeysql-form)))
+                            (log/error (u/format-color 'red "Invalid HoneySQL form:\n%s"
+                                                       (u/pprint-to-str honeysql-form)))
                             (throw e)))]
     (into [(hx/unescape-dots sql)] args)))
 
@@ -288,10 +302,12 @@
                                    (into-array String ["TABLE", "VIEW", "FOREIGN TABLE", "MATERIALIZED VIEW"]))))
 
 (defn fast-active-tables
-  "Default, fast implementation of `ISQLDriver/active-tables` best suited for DBs with lots of system tables (like Oracle).
-   Fetch list of schemas, then for each one not in `excluded-schemas`, fetch its Tables, and combine the results.
+  "Default, fast implementation of `ISQLDriver/active-tables` best suited for DBs with lots of system tables (like
+   Oracle). Fetch list of schemas, then for each one not in `excluded-schemas`, fetch its Tables, and combine the
+   results.
 
-   This is as much as 15x faster for Databases with lots of system tables than `post-filtered-active-tables` (4 seconds vs 60)."
+   This is as much as 15x faster for Databases with lots of system tables than `post-filtered-active-tables` (4
+   seconds vs 60)."
   [driver, ^DatabaseMetaData metadata]
   (let [all-schemas (set (map :table_schem (jdbc/result-set-seq (.getSchemas metadata))))
         schemas     (set/difference all-schemas (excluded-schemas driver))]
@@ -316,7 +332,8 @@
          (merge {:name      column_name
                  :custom    {:column-type type_name}
                  :base-type (or (column->base-type driver (keyword type_name))
-                                (do (log/warn (format "Don't know how to map column type '%s' to a Field base_type, falling back to :type/*." type_name))
+                                (do (log/warn (format "Don't know how to map column type '%s' to a Field base_type, falling back to :type/*."
+                                                      type_name))
                                     :type/*))}
                 (when calculated-special-type
                   (assert (isa? calculated-special-type :type/*)
@@ -361,8 +378,10 @@
   []
   (require 'metabase.driver.generic-sql.query-processor)
   {:active-tables        fast-active-tables
-   :apply-aggregation    (resolve 'metabase.driver.generic-sql.query-processor/apply-aggregation) ; don't resolve the vars yet so during interactive dev if the
-   :apply-breakout       (resolve 'metabase.driver.generic-sql.query-processor/apply-breakout)    ; underlying impl changes we won't have to reload all the drivers
+   ;; don't resolve the vars yet so during interactive dev if the underlying impl changes we won't have to reload all
+   ;; the drivers
+   :apply-aggregation    (resolve 'metabase.driver.generic-sql.query-processor/apply-aggregation)
+   :apply-breakout       (resolve 'metabase.driver.generic-sql.query-processor/apply-breakout)
    :apply-fields         (resolve 'metabase.driver.generic-sql.query-processor/apply-fields)
    :apply-filter         (resolve 'metabase.driver.generic-sql.query-processor/apply-filter)
    :apply-join-tables    (resolve 'metabase.driver.generic-sql.query-processor/apply-join-tables)
diff --git a/src/metabase/driver/google.clj b/src/metabase/driver/google.clj
index 5514dca3eaaf1c4cdaf0b3066fe75e5d6fb2b021..0da5b3fb65631ee72a8a41fd3149c733cb5abf8c 100644
--- a/src/metabase/driver/google.clj
+++ b/src/metabase/driver/google.clj
@@ -90,3 +90,8 @@
                     (.setTransport http-transport)))
       (.setAccessToken  access-token)
       (.setRefreshToken refresh-token))))
+
+(defn -init-driver
+  "Nothing to init as this is code used by the google drivers, but is not a driver itself"
+  []
+  true)
diff --git a/src/metabase/driver/postgres.clj b/src/metabase/driver/postgres.clj
index ff3006d91005f38375ff971b25f53df8f85e9f29..90b70212c53b847ac7ffc3632d73aeed9e433ed6 100644
--- a/src/metabase/driver/postgres.clj
+++ b/src/metabase/driver/postgres.clj
@@ -26,6 +26,7 @@
    :bytea         :type/*    ; byte array
    :cidr          :type/Text ; IPv4/IPv6 network address
    :circle        :type/*
+   :citext        :type/Text ; case-insensitive text
    :date          :type/Date
    :decimal       :type/Decimal
    :float4        :type/Float
diff --git a/src/metabase/driver/sqlserver.clj b/src/metabase/driver/sqlserver.clj
index 041dc056dbe9958497f3f330452c991b3ff30824..ae5176f6dc7d94ecbf67ddf8020b43e1559f963a 100644
--- a/src/metabase/driver/sqlserver.clj
+++ b/src/metabase/driver/sqlserver.clj
@@ -61,17 +61,20 @@
   (-> {:applicationName config/mb-app-id-string
        :classname       "com.microsoft.sqlserver.jdbc.SQLServerDriver"
        :subprotocol     "sqlserver"
-       ;; it looks like the only thing that actually needs to be passed as the `subname` is the host; everything else can be passed as part of the Properties
+       ;; it looks like the only thing that actually needs to be passed as the `subname` is the host; everything else
+       ;; can be passed as part of the Properties
        :subname         (str "//" host)
-       ;; everything else gets passed as `java.util.Properties` to the JDBC connection.
-       ;; (passing these as Properties instead of part of the `:subname` is preferable because they support things like passwords with special characters)
+       ;; everything else gets passed as `java.util.Properties` to the JDBC connection.  (passing these as Properties
+       ;; instead of part of the `:subname` is preferable because they support things like passwords with special
+       ;; characters)
        :database        db
        :port            port
        :password        password
        ;; Wait up to 10 seconds for connection success. If we get no response by then, consider the connection failed
        :loginTimeout    10
-       ;; apparently specifying `domain` with the official SQLServer driver is done like `user:domain\user` as opposed to specifying them seperately as with jTDS
-       ;; see also: https://social.technet.microsoft.com/Forums/sqlserver/en-US/bc1373f5-cb40-479d-9770-da1221a0bc95/connecting-to-sql-server-in-a-different-domain-using-jdbc-driver?forum=sqldataaccess
+       ;; apparently specifying `domain` with the official SQLServer driver is done like `user:domain\user` as opposed
+       ;; to specifying them seperately as with jTDS see also:
+       ;; https://social.technet.microsoft.com/Forums/sqlserver/en-US/bc1373f5-cb40-479d-9770-da1221a0bc95/connecting-to-sql-server-in-a-different-domain-using-jdbc-driver?forum=sqldataaccess
        :user            (str (when domain (str domain "\\"))
                              user)
        :instanceName    instance
@@ -95,9 +98,11 @@
     :minute-of-hour  (date-part :minute expr)
     :hour            (hx/->datetime (hx/format "yyyy-MM-dd HH:00:00" expr))
     :hour-of-day     (date-part :hour expr)
-    ;; jTDS is retarded; I sense an ongoing theme here. It returns DATEs as strings instead of as java.sql.Dates
-    ;; like every other SQL DB we support. Work around that by casting to DATE for truncation then back to DATETIME so we get the type we want
-    ;; TODO - I'm not sure we still need to do this now that we're using the official Microsoft JDBC driver. Maybe we can simplify this now?
+    ;; jTDS is retarded; I sense an ongoing theme here. It returns DATEs as strings instead of as java.sql.Dates like
+    ;; every other SQL DB we support. Work around that by casting to DATE for truncation then back to DATETIME so we
+    ;; get the type we want.
+    ;; TODO - I'm not sure we still need to do this now that we're using the official Microsoft JDBC driver. Maybe we
+    ;; can simplify this now?
     :day             (hx/->datetime (hx/->date expr))
     :day-of-week     (date-part :weekday expr)
     :day-of-month    (date-part :day expr)
diff --git a/src/metabase/email.clj b/src/metabase/email.clj
index 59832a47043d33491151903cd1d4ab16603c3306..1ae54db739f85b24a5b0c3d88cdf7831695c9feb 100644
--- a/src/metabase/email.clj
+++ b/src/metabase/email.clj
@@ -2,6 +2,7 @@
   (:require [clojure.tools.logging :as log]
             [metabase.models.setting :as setting :refer [defsetting]]
             [metabase.util :as u]
+            [metabase.util.schema :as su]
             [postal
              [core :as postal]
              [support :refer [make-props]]]
@@ -52,17 +53,23 @@
       (add-ssl-settings (email-smtp-security))))
 
 (def ^:private EmailMessage
-  {:subject      s/Str
-   :recipients   [(s/pred u/is-email?)]
-   :message-type (s/enum :text :html :attachments)
-   :message      s/Str})
+  (s/constrained
+   {:subject      s/Str
+    :recipients   [(s/pred u/is-email?)]
+    :message-type (s/enum :text :html :attachments)
+    :message      (s/cond-pre s/Str [su/Map])} ; TODO - what should this be a sequence of?
+   (fn [{:keys [message-type message]}]
+     (if (= message-type :attachments)
+       (and (sequential? message) (every? map? message))
+       (string? message)))
+   (str "Bad message-type/message combo: message-type `:attachments` should have a sequence of maps as its message; "
+        "other types should have a String message.")))
 
 (s/defn send-message-or-throw!
   "Send an email to one or more RECIPIENTS. Upon success, this returns the MESSAGE that was just sent. This function
   does not catch and swallow thrown exceptions, it will bubble up."
   {:style/indent 0}
   [{:keys [subject recipients message-type message]} :- EmailMessage]
-  {:pre [(if (= message-type :attachments) (sequential? message) (string? message))]}
   (when-not (email-smtp-host)
     (throw (Exception. "SMTP host is not set.")))
   ;; Now send the email
@@ -93,6 +100,7 @@
   (try
     (send-message-or-throw! msg-args)
     (catch Throwable e
+      (println "Failed to send email:" e)
       (log/warn e "Failed to send email")
       {:error   :ERROR
        :message (.getMessage e)})))
diff --git a/src/metabase/email/alert_admin_unsubscribed_you.mustache b/src/metabase/email/alert_admin_unsubscribed_you.mustache
index 65cd4962a13a17f40c23ab22183cec4655f9527b..8ee72f51965cb5ca50bbe29137e72fc449396919 100644
--- a/src/metabase/email/alert_admin_unsubscribed_you.mustache
+++ b/src/metabase/email/alert_admin_unsubscribed_you.mustache
@@ -1,5 +1,3 @@
 {{> metabase/email/_header }}
-  <div style="padding: 2em 4em; border: 1px solid #dddddd; border-radius: 2px; background-color: white; box-shadow: 0 1px 2px rgba(0, 0, 0, .08);">
     <p>Just letting you know that {{adminName}} unsubscribed you from alerts about <a href="{{questionURL}}">{{questionName}}</a>.</p>
-  </div>
 {{> metabase/email/_footer}}
diff --git a/src/metabase/email/alert_new_confirmation.mustache b/src/metabase/email/alert_new_confirmation.mustache
index 6150cdbaa739ec2d7e2847497af8663681c36ab3..b0bf747be296487846775d4dc831e7cc407251c0 100644
--- a/src/metabase/email/alert_new_confirmation.mustache
+++ b/src/metabase/email/alert_new_confirmation.mustache
@@ -1,6 +1,4 @@
 {{> metabase/email/_header }}
-  <div style="padding: 2em 4em; border: 1px solid #dddddd; border-radius: 2px; background-color: white; box-shadow: 0 1px 2px rgba(0, 0, 0, .08);">
     <p>This is just a confirmation that your alert about <a href="{{questionURL}}">{{questionName}}</a> has been set up.</p>
     <p>This alert will be sent {{alertCondition}}.</p>
-  </div>
 {{> metabase/email/_footer}}
diff --git a/src/metabase/email/alert_stopped_working.mustache b/src/metabase/email/alert_stopped_working.mustache
index 40a2ca7f47d24e58b37ee954b4c46e6069b67324..8fe2de49e19a37b705ee014c5e4bfa9a8eda2a00 100644
--- a/src/metabase/email/alert_stopped_working.mustache
+++ b/src/metabase/email/alert_stopped_working.mustache
@@ -1,5 +1,3 @@
 {{> metabase/email/_header }}
-  <div style="padding: 2em 4em; border: 1px solid #dddddd; border-radius: 2px; background-color: white; box-shadow: 0 1px 2px rgba(0, 0, 0, .08);">
-    <p>Alerts about {{questionName}} have stopped because {{deletionCause}}.
-  </div>
+    <p>Alerts about {{questionName}} have stopped because {{deletionCause}}.</p>
 {{> metabase/email/_footer}}
diff --git a/src/metabase/email/alert_unsubscribed.mustache b/src/metabase/email/alert_unsubscribed.mustache
index a1878974d7d788900dd200c8e72210cb025969a1..b5d431bf62c54e4fee39be6b7181f14c536d048b 100644
--- a/src/metabase/email/alert_unsubscribed.mustache
+++ b/src/metabase/email/alert_unsubscribed.mustache
@@ -1,5 +1,3 @@
 {{> metabase/email/_header }}
-  <div style="padding: 2em 4em; border: 1px solid #dddddd; border-radius: 2px; background-color: white; box-shadow: 0 1px 2px rgba(0, 0, 0, .08);">
     <p>You’re no longer receiving alerts about <a href="{{questionURL}}">{{questionName}}</a>.</p>
-  </div>
 {{> metabase/email/_footer}}
diff --git a/src/metabase/email/alert_was_deleted.mustache b/src/metabase/email/alert_was_deleted.mustache
index 7366e6f60c436c03bda96ed2bd87bf35d3fb8268..c7bf9a6b61851f1c92942c1bba5b424edbb2ddfb 100644
--- a/src/metabase/email/alert_was_deleted.mustache
+++ b/src/metabase/email/alert_was_deleted.mustache
@@ -1,6 +1,4 @@
 {{> metabase/email/_header }}
-  <div style="padding: 2em 4em; border: 1px solid #dddddd; border-radius: 2px; background-color: white; box-shadow: 0 1px 2px rgba(0, 0, 0, .08);">
     <p>{{adminName}} deleted an alert you created about <a href="{{questionURL}}">{{questionName}}.</p>
     <p>Since we're in the business of alerts, we thought we'd alert you.</p>
-  </div>
 {{> metabase/email/_footer}}
diff --git a/src/metabase/email/alert_you_were_added.mustache b/src/metabase/email/alert_you_were_added.mustache
index fe64cd54668e2e1c2df45691fd5b1f71ebcd3558..ec4d73b3ba0a30720aae5dc1d2c06f275615fa5f 100644
--- a/src/metabase/email/alert_you_were_added.mustache
+++ b/src/metabase/email/alert_you_were_added.mustache
@@ -1,7 +1,5 @@
 {{> metabase/email/_header }}
-  <div style="padding: 2em 4em; border: 1px solid #dddddd; border-radius: 2px; background-color: white; box-shadow: 0 1px 2px rgba(0, 0, 0, .08);">
     <p>You’re now getting alerts about <a href="{{questionURL}}">{{questionName}}</a>.</p>
-    <p>We’ll send you an email {{alertCondition}}</p>
+    <p>We’ll send you an email {{alertCondition}}.</p>
     <p>If you don’t want to receive these alerts, you can <a href="{{questionURL}}">go to this question</a> and unsubscribe from the menu in the top-right.</p>
-  </div>
 {{> metabase/email/_footer}}
diff --git a/src/metabase/events/activity_feed.clj b/src/metabase/events/activity_feed.clj
index f233c5d4d7106459637bb3bda6666d969291fb91..ad452f5add8a8723ae7de63d9183e6286f9f8bb9 100644
--- a/src/metabase/events/activity_feed.clj
+++ b/src/metabase/events/activity_feed.clj
@@ -15,7 +15,9 @@
 
 (def ^:const activity-feed-topics
   "The `Set` of event topics which are subscribed to for use in the Metabase activity feed."
-  #{:card-create
+  #{:alert-create
+    :alert-delete
+    :card-create
     :card-update
     :card-delete
     :dashboard-create
@@ -98,6 +100,16 @@
       :object      object
       :details-fn  details-fn)))
 
+(defn- process-alert-activity! [topic {:keys [card] :as alert}]
+  (let [details-fn #(select-keys (:card %) [:name])]
+    (activity/record-activity!
+      ;; Alerts are centered around a card/question. Users always interact with the alert via the question
+      :model       "card"
+      :model-id    (:id card)
+      :topic       topic
+      :object      alert
+      :details-fn  details-fn)))
+
 (defn- process-segment-activity! [topic object]
   (let [details-fn  #(select-keys % [:name :description :revision_message])
         table-id    (:table_id object)
@@ -123,7 +135,8 @@
     (db/insert! Activity, :topic "install", :model "install")))
 
 (def ^:private model->processing-fn
-  {"card"      process-card-activity!
+  {"alert"     process-alert-activity!
+   "card"      process-card-activity!
    "dashboard" process-dashboard-activity!
    "install"   process-install-activity!
    "metric"    process-metric-activity!
diff --git a/src/metabase/feature_extraction/async.clj b/src/metabase/feature_extraction/async.clj
index 46d9185a1385b3fd3c035b596e3f178d7bbb8b23..44aeacb0d93e417a01be3350791c921ad14c8259 100644
--- a/src/metabase/feature_extraction/async.clj
+++ b/src/metabase/feature_extraction/async.clj
@@ -19,40 +19,46 @@
   "Is the computation job still running?"
   (comp some? #{:running} :status))
 
+(def ^{:arglists '([job])} canceled?
+  "Has the computation job been canceled?"
+  (comp some? #{:canceled} :status))
+
 (defn- save-result
   [{:keys [id]} payload]
-  (db/transaction
-    (db/insert! ComputationJobResult
-      :job_id     id
-      :permanence :temporary
-      :payload    payload)
-    (db/update! ComputationJob id
-      :status   :done
-      :ended_at (u/new-sql-timestamp)))
-  (swap! running-jobs dissoc id)
-  (log/info (format "Async job %s done." id)))
-
-(defn- save-error
-  [{:keys [id]} error]
-  (let [error (Throwable->map error)]
-    (log/warn (format "Async job %s encountered an error:\n%s." id error))
+  (when-not (future-cancelled? (@running-jobs id))
     (db/transaction
       (db/insert! ComputationJobResult
         :job_id     id
         :permanence :temporary
-        :payload    error)
+        :payload    payload)
       (db/update! ComputationJob id
-        :status :error
+        :status   :done
         :ended_at (u/new-sql-timestamp))))
+  (swap! running-jobs dissoc id)
+  (log/info (format "Async job %s done." id)))
+
+(defn- save-error
+  [{:keys [id]} error]
+  (when-not (future-cancelled? (@running-jobs id))
+    (let [error (Throwable->map error)]
+      (log/warn (format "Async job %s encountered an error:\n%s." id error))
+      (db/transaction
+        (db/insert! ComputationJobResult
+          :job_id     id
+          :permanence :temporary
+          :payload    error)
+        (db/update! ComputationJob id
+          :status :error
+          :ended_at (u/new-sql-timestamp)))))
   (swap! running-jobs dissoc id))
 
 (defn cancel
   "Cancel computation job (if still running)."
   [{:keys [id] :as job}]
   (when (running? job)
+    (db/update! ComputationJob id :status :canceled)
     (future-cancel (@running-jobs id))
     (swap! running-jobs dissoc id)
-    (db/update! ComputationJob id :status :canceled)
     (log/info (format "Async job %s canceled." id))))
 
 (defn- time-delta-seconds
diff --git a/src/metabase/feature_extraction/feature_extractors.clj b/src/metabase/feature_extraction/feature_extractors.clj
index 54a3965089fbcebeff5bcfee9f2f6486b4e8a96e..aae70781204276c9bd69ce4bb6a26ff5e84c90ad 100644
--- a/src/metabase/feature_extraction/feature_extractors.clj
+++ b/src/metabase/feature_extraction/feature_extractors.clj
@@ -480,7 +480,7 @@
   (let [x-field (first field)]
     (-> features
         (update :series (partial series->dataset ts/from-double field))
-        (dissoc :series :autocorrelation :best-fit)
+        (dissoc :autocorrelation :best-fit)
         (assoc :insights ((merge-juxt insights/noisiness
                                       insights/variation-trend
                                       insights/autocorrelation
diff --git a/src/metabase/models/card.clj b/src/metabase/models/card.clj
index cbcd4f5bb747d9dc44c4379a0407f531563c58e9..dc41e0db95fa1358e12e64ac38e4583388c88776 100644
--- a/src/metabase/models/card.clj
+++ b/src/metabase/models/card.clj
@@ -1,4 +1,6 @@
 (ns metabase.models.card
+  "Underlying DB model for what is now most commonly referred to as a 'Question' in most user-facing situations. Card
+  is a historical name, but is the same thing; both terms are used interchangeably in the backend codebase."
   (:require [clojure.core.memoize :as memoize]
             [clojure.set :as set]
             [clojure.tools.logging :as log]
diff --git a/src/metabase/models/collection.clj b/src/metabase/models/collection.clj
index 2a885d5c72361b232f6fdc61d6d85b5a82cc6a39..0af15018fa4b32acffa7208a92f3020e22bf143a 100644
--- a/src/metabase/models/collection.clj
+++ b/src/metabase/models/collection.clj
@@ -114,20 +114,20 @@
   (into {} (for [[group-id perms] (group-by :group_id (db/select 'Permissions))]
              {group-id (set (map :object perms))})))
 
-(s/defn ^:private ^:always-validate perms-type-for-collection :- CollectionPermissions
+(s/defn ^:private perms-type-for-collection :- CollectionPermissions
   [permissions-set collection-id]
   (cond
     (perms/set-has-full-permissions? permissions-set (perms/collection-readwrite-path collection-id)) :write
     (perms/set-has-full-permissions? permissions-set (perms/collection-read-path collection-id))      :read
     :else                                                                                             :none))
 
-(s/defn ^:private ^:always-validate group-permissions-graph :- GroupPermissionsGraph
+(s/defn ^:private group-permissions-graph :- GroupPermissionsGraph
   "Return the permissions graph for a single group having PERMISSIONS-SET."
   [permissions-set collection-ids]
   (into {} (for [collection-id collection-ids]
              {collection-id (perms-type-for-collection permissions-set collection-id)})))
 
-(s/defn ^:always-validate graph :- PermissionsGraph
+(s/defn graph :- PermissionsGraph
   "Fetch a graph representing the current permissions status for every group and all permissioned collections.
    This works just like the function of the same name in `metabase.models.permissions`; see also the documentation for that function."
   []
@@ -140,7 +140,7 @@
 
 ;;; ---------------------------------------- Update Graph ----------------------------------------
 
-(s/defn ^:private ^:always-validate update-collection-permissions! [group-id :- su/IntGreaterThanZero, collection-id :- su/IntGreaterThanZero, new-collection-perms :- CollectionPermissions]
+(s/defn ^:private update-collection-permissions! [group-id :- su/IntGreaterThanZero, collection-id :- su/IntGreaterThanZero, new-collection-perms :- CollectionPermissions]
   ;; remove whatever entry is already there (if any) and add a new entry if applicable
   (perms/revoke-collection-permissions! group-id collection-id)
   (case new-collection-perms
@@ -148,7 +148,7 @@
     :read  (perms/grant-collection-read-permissions! group-id collection-id)
     :none  nil))
 
-(s/defn ^:private ^:always-validate update-group-permissions! [group-id :- su/IntGreaterThanZero, new-group-perms :- GroupPermissionsGraph]
+(s/defn ^:private update-group-permissions! [group-id :- su/IntGreaterThanZero, new-group-perms :- GroupPermissionsGraph]
   (doseq [[collection-id new-perms] new-group-perms]
     (update-collection-permissions! group-id collection-id new-perms)))
 
@@ -163,7 +163,7 @@
       :after   new
       :user_id *current-user-id*)))
 
-(s/defn ^:always-validate update-graph!
+(s/defn update-graph!
   "Update the collections permissions graph.
    This works just like the function of the same name in `metabase.models.permissions`, but for `Collections`;
    refer to that function's extensive documentation to get a sense for how this works."
diff --git a/src/metabase/models/permissions.clj b/src/metabase/models/permissions.clj
index 1bc8a0ca0f928bd4298d33d38d23b05f6d3c5dcb..af5bfc5ef55c1c2de27752eaf6260a6b3588cd87 100644
--- a/src/metabase/models/permissions.clj
+++ b/src/metabase/models/permissions.clj
@@ -293,7 +293,7 @@
               tables))
 
 ;; TODO - if a DB has no tables, then it won't show up in the permissions graph!
-(s/defn ^:always-validate graph :- PermissionsGraph
+(s/defn graph :- PermissionsGraph
   "Fetch a graph representing the current permissions status for every group and all permissioned databases."
   []
   (let [permissions (db/select [Permissions :group_id :object])
@@ -406,13 +406,13 @@
 
 ;;; ---------------------------------------- Graph Updating Fns ----------------------------------------
 
-(s/defn ^:private ^:always-validate update-table-perms!
+(s/defn ^:private update-table-perms!
   [group-id :- su/IntGreaterThanZero, db-id :- su/IntGreaterThanZero, schema :- s/Str, table-id :- su/IntGreaterThanZero, new-table-perms :- SchemaPermissionsGraph]
   (case new-table-perms
     :all  (grant-permissions! group-id db-id schema table-id)
     :none (revoke-permissions! group-id db-id schema table-id)))
 
-(s/defn ^:private ^:always-validate update-schema-perms!
+(s/defn ^:private update-schema-perms!
   [group-id :- su/IntGreaterThanZero, db-id :- su/IntGreaterThanZero, schema :- s/Str, new-schema-perms :- SchemaPermissionsGraph]
   (cond
     (= new-schema-perms :all)  (do (revoke-permissions! group-id db-id schema) ; clear out any existing related permissions
@@ -421,7 +421,7 @@
     (map? new-schema-perms)    (doseq [[table-id table-perms] new-schema-perms]
                                  (update-table-perms! group-id db-id schema table-id table-perms))))
 
-(s/defn ^:private ^:always-validate update-native-permissions!
+(s/defn ^:private update-native-permissions!
   [group-id :- su/IntGreaterThanZero, db-id :- su/IntGreaterThanZero, new-native-perms :- NativePermissionsGraph]
   ;; revoke-native-permissions! will delete all entires that would give permissions for native access.
   ;; Thus if you had a root DB entry like `/db/11/` this will delete that too.
@@ -436,7 +436,7 @@
     :none  nil))
 
 
-(s/defn ^:private ^:always-validate update-db-permissions!
+(s/defn ^:private update-db-permissions!
   [group-id :- su/IntGreaterThanZero, db-id :- su/IntGreaterThanZero, new-db-perms :- StrictDBPermissionsGraph]
   (when-let [new-native-perms (:native new-db-perms)]
     (update-native-permissions! group-id db-id new-native-perms))
@@ -448,7 +448,7 @@
       (map? schemas)    (doseq [schema (keys schemas)]
                           (update-schema-perms! group-id db-id schema (get-in new-db-perms [:schemas schema]))))))
 
-(s/defn ^:private ^:always-validate update-group-permissions!
+(s/defn ^:private update-group-permissions!
   [group-id :- su/IntGreaterThanZero, new-group-perms :- StrictGroupPermissionsGraph]
   (doseq [[db-id new-db-perms] new-group-perms]
     (update-db-permissions! group-id db-id new-db-perms)))
@@ -482,7 +482,7 @@
                      (u/pprint-to-str 'magenta old)
                      (u/pprint-to-str 'blue new))))
 
-(s/defn ^:always-validate update-graph!
+(s/defn update-graph!
   "Update the permissions graph, making any changes neccesary to make it match NEW-GRAPH.
    This should take in a graph that is exactly the same as the one obtained by `graph` with any changes made as
    needed. The graph is revisioned, so if it has been updated by a third party since you fetched it this function will
diff --git a/src/metabase/models/pulse.clj b/src/metabase/models/pulse.clj
index d376e8cffbcaab8b13bfb045acd066a6ca4c4dec..a845295f7c884071da81369373e1098c60ba8f56 100644
--- a/src/metabase/models/pulse.clj
+++ b/src/metabase/models/pulse.clj
@@ -243,15 +243,14 @@
     (doseq [[channel-type] pulse-channel/channel-types]
       (handle-channel channel-type))))
 
-(defn- create-notification [pulse card-ids channels retrieve-pulse-fn]
+(defn- create-notification [pulse card-ids channels]
   (db/transaction
     (let [{:keys [id] :as pulse} (db/insert! Pulse pulse)]
       ;; add card-ids to the Pulse
       (update-pulse-cards! pulse card-ids)
       ;; add channels to the Pulse
       (update-pulse-channels! pulse channels)
-      ;; return the full Pulse (and record our create event)
-      (events/publish-event! :pulse-create (retrieve-pulse-fn id)))))
+      id)))
 
 
 (defn create-pulse!
@@ -267,17 +266,21 @@
          (every? integer? card-ids)
          (coll? channels)
          (every? map? channels)]}
-  (create-notification {:creator_id    creator-id
-                        :name          pulse-name
-                        :skip_if_empty skip-if-empty?}
-                       card-ids channels retrieve-pulse))
+  (let [id (create-notification {:creator_id    creator-id
+                                 :name          pulse-name
+                                 :skip_if_empty skip-if-empty?}
+                                card-ids channels)]
+    ;; return the full Pulse (and record our create event)
+    (events/publish-event! :pulse-create (retrieve-pulse id))))
 
 (defn create-alert!
   "Creates a pulse with the correct fields specified for an alert"
   [alert creator-id card-id channels]
-  (-> alert
-      (assoc :skip_if_empty true :creator_id creator-id)
-      (create-notification [card-id] channels retrieve-alert)))
+  (let [id (-> alert
+               (assoc :skip_if_empty true :creator_id creator-id)
+               (create-notification [card-id] channels))]
+    ;; return the full Pulse (and record our create event)
+    (events/publish-event! :alert-create (retrieve-alert id))))
 
 (defn update-notification!
   "Updates the pulse/alert and updates the related channels"
diff --git a/src/metabase/models/setting.clj b/src/metabase/models/setting.clj
index 62ba5729ed61cadc0797375ebf77554856a61703..862d3ed7012abd07dd304f64c68acbb37055015a 100644
--- a/src/metabase/models/setting.clj
+++ b/src/metabase/models/setting.clj
@@ -67,7 +67,7 @@
 (defonce ^:private registered-settings
   (atom {}))
 
-(s/defn ^:private ^:always-validate resolve-setting :- SettingDefinition
+(s/defn ^:private resolve-setting :- SettingDefinition
   [setting-or-name]
   (if (map? setting-or-name)
     setting-or-name
@@ -209,7 +209,7 @@
                    "\nAssuming Setting already exists in DB and updating existing value.")
          (update-setting! setting-name new-value))))
 
-(s/defn ^:always-validate set-string!
+(s/defn set-string!
   "Set string value of SETTING-OR-NAME. A `nil` or empty NEW-VALUE can be passed to unset (i.e., delete)
    SETTING-OR-NAME."
   [setting-or-name, new-value :- (s/maybe s/Str)]
diff --git a/src/metabase/public_settings/metastore.clj b/src/metabase/public_settings/metastore.clj
index 36133805fed1ba3783f4371488f67bd7c16aee6c..78393c344c879561d391fd035651fbb84a01c29f 100644
--- a/src/metabase/public_settings/metastore.clj
+++ b/src/metabase/public_settings/metastore.clj
@@ -34,7 +34,7 @@
 
 (def ^:private ^:const fetch-token-status-timeout-ms 10000) ; 10 seconds
 
-(s/defn ^:private ^:always-validate fetch-token-status :- {:valid s/Bool, :status su/NonBlankString}
+(s/defn ^:private fetch-token-status :- {:valid s/Bool, :status su/NonBlankString}
   "Fetch info about the validity of TOKEN from the MetaStore. "
   [token :- ValidToken]
   (try
diff --git a/src/metabase/pulse.clj b/src/metabase/pulse.clj
index 01cf90cc321009c38232af78de90cc3604a6f952..c5ab006b7703f7a0ecac3afbfeea379be1688478 100644
--- a/src/metabase/pulse.clj
+++ b/src/metabase/pulse.clj
@@ -19,7 +19,8 @@
             [puppetlabs.i18n.core :refer [tru]]
             [schema.core :as s]
             [toucan.db :as db])
-  (:import java.util.TimeZone))
+  (:import java.util.TimeZone
+           metabase.models.card.CardInstance))
 
 ;;; ## ---------------------------------------- PULSE SENDING ----------------------------------------
 
@@ -46,7 +47,7 @@
 (s/defn defaulted-timezone :- TimeZone
   "Returns the timezone for the given `CARD`. Either the report
   timezone (if applicable) or the JVM timezone."
-  [card :- Card]
+  [card :- CardInstance]
   (let [^String timezone-str (or (some-> card database-id driver/database-id->driver driver/report-timezone-if-supported)
                                  (System/getProperty "user.timezone"))]
     (TimeZone/getTimeZone timezone-str)))
@@ -143,7 +144,7 @@
   "Polymorphoic function for creating notifications. This logic is different for pulse type (i.e. alert vs. pulse) and
   channel_type (i.e. email vs. slack)"
   (fn [pulse _ {:keys [channel_type] :as channel}]
-    [(alert-or-pulse pulse) channel_type]))
+    [(alert-or-pulse pulse) (keyword channel_type)]))
 
 (defmethod create-notification [:pulse :email]
   [{:keys [id name] :as pulse} results {:keys [recipients] :as channel}]
@@ -171,11 +172,12 @@
                                  (first-question-name pulse)
                                  (get alert-notification-condition-text condition-kwd))
         email-recipients (filterv u/is-email? (map :email recipients))
-        timezone         (-> results first :card defaulted-timezone)]
+        first-result     (first results)
+        timezone         (-> first-result :card defaulted-timezone)]
     {:subject      email-subject
      :recipients   email-recipients
      :message-type :attachments
-     :message      (messages/render-alert-email timezone pulse results (ui/find-goal-value results))}))
+     :message      (messages/render-alert-email timezone pulse results (ui/find-goal-value first-result))}))
 
 (defmethod create-notification [:alert :slack]
   [pulse results {{channel-id :channel} :details :as channel}]
@@ -184,6 +186,11 @@
    :message (str "Alert: " (first-question-name pulse))
    :attachments (create-slack-attachment-data results)})
 
+(defmethod create-notification :default
+  [_ _ {:keys [channel_type] :as channel}]
+  (let [^String ex-msg (tru "Unrecognized channel type {0}" (pr-str channel_type))]
+    (throw (UnsupportedOperationException. ex-msg))))
+
 (defmulti ^:private send-notification!
   "Invokes the side-affecty function for sending emails/slacks depending on the notification type"
   (fn [{:keys [channel-id] :as notification}]
diff --git a/src/metabase/query_processor.clj b/src/metabase/query_processor.clj
index b7253cedd21e125c5fede30201867c4b9be2ff7e..b87f5e8d1dedb2cea8dd30cc7d6e4ee4a00cf12e 100644
--- a/src/metabase/query_processor.clj
+++ b/src/metabase/query_processor.clj
@@ -1,5 +1,6 @@
 (ns metabase.query-processor
-  "Preprocessor that does simple transformations to all incoming queries, simplifing the driver-specific implementations."
+  "Preprocessor that does simple transformations to all incoming queries, simplifing the driver-specific
+  implementations."
   (:require [clojure.tools.logging :as log]
             [metabase
              [driver :as driver]
@@ -42,14 +43,15 @@
 ;;; +-------------------------------------------------------------------------------------------------------+
 
 (defn- execute-query
-  "The pivotal stage of the `process-query` pipeline where the query is actually executed by the driver's Query Processor methods.
-   This function takes the fully pre-processed query, runs it, and returns the results, which then run through the various
-   post-processing steps."
+  "The pivotal stage of the `process-query` pipeline where the query is actually executed by the driver's Query
+  Processor methods. This function takes the fully pre-processed query, runs it, and returns the results, which then
+  run through the various post-processing steps."
   [query]
   {:pre [(map? query) (:driver query)]}
   (driver/execute-query (:driver query) query))
 
-;; The way these functions are applied is actually straight-forward; it matches the middleware pattern used by Compojure.
+;; The way these functions are applied is actually straight-forward; it matches the middleware pattern used by
+;; Compojure.
 ;;
 ;; (defn- qp-middleware-fn [qp]
 ;;   (fn [query]
@@ -60,9 +62,10 @@
 ;; This returned function *pre-processes* QUERY as needed, and then passes it to QP.
 ;; The function may then *post-process* the results of (QP QUERY) as neeeded, and returns the results.
 ;;
-;; Many functions do both pre and post-processing; this middleware pattern allows them to return closures that maintain some sort of
-;; internal state. For example, `cumulative-sum` can determine if it needs to perform cumulative summing, and, if so, modify the query
-;; before passing it to QP; once the query is processed, it can use modify the results as needed.
+;; Many functions do both pre and post-processing; this middleware pattern allows them to return closures that
+;; maintain some sort of internal state. For example, `cumulative-sum` can determine if it needs to perform cumulative
+;; summing, and, if so, modify the query before passing it to QP; once the query is processed, it can use modify the
+;; results as needed.
 ;;
 ;; PRE-PROCESSING fns are applied from bottom to top, and POST-PROCESSING from top to bottom;
 ;; the easiest way to wrap your head around this is picturing a the query as a ball being thrown in the air
@@ -76,8 +79,8 @@
 
      (post-process (f (pre-process query)))
 
-   Normally F is something that runs the query, like the `execute-query` function above, but this can be swapped out when we want to do things like
-   process a query without actually running it."
+   Normally F is something that runs the query, like the `execute-query` function above, but this can be swapped out
+   when we want to do things like process a query without actually running it."
   [f]
   ;; ▼▼▼ POST-PROCESSING ▼▼▼  happens from TOP-TO-BOTTOM, e.g. the results of `f` are (eventually) passed to `limit`
   (-> f
@@ -111,7 +114,8 @@
 ;; ▲▲▲ PRE-PROCESSING ▲▲▲ happens from BOTTOM-TO-TOP, e.g. the results of `expand-macros` are (eventually) passed to `expand-resolve`
 
 (defn query->native
-  "Return the native form for QUERY (e.g. for a MBQL query on Postgres this would return a map containing the compiled SQL form)."
+  "Return the native form for QUERY (e.g. for a MBQL query on Postgres this would return a map containing the compiled
+  SQL form)."
   {:style/indent 0}
   [query]
   (let [results ((qp-pipeline identity) query)]
@@ -135,7 +139,8 @@
        parameters/substitute-parameters
        expand-macros/expand-macros
        fetch-source-query/fetch-source-query))
-;; ▲▲▲ This only does PRE-PROCESSING, so it happens from bottom to top, eventually returning the preprocessed query instead of running it
+;; ▲▲▲ This only does PRE-PROCESSING, so it happens from bottom to top, eventually returning the preprocessed query
+;; instead of running it
 
 
 ;;; +----------------------------------------------------------------------------------------------------+
@@ -182,7 +187,8 @@
                                                (:start_time_millis query-execution))
                               :result_rows  (get query-result :row_count 0))
                             (dissoc :start_time_millis))]
-    ;; only insert a new record into QueryExecution if the results *were not* cached (i.e., only if a Query was actually ran)
+    ;; only insert a new record into QueryExecution if the results *were not* cached (i.e., only if a Query was
+    ;; actually ran)
     (when-not (:cached query-result)
       (save-query-execution! query-execution))
     ;; ok, now return the results in the normal response format
@@ -203,9 +209,9 @@
     (throw (Exception. (str (get query-result :error "general error"))))))
 
 (def ^:dynamic ^Boolean *allow-queries-with-no-executor-id*
-  "Should we allow running queries (via `dataset-query`) without specifying the `executed-by` User ID?
-   By default this is `false`, but this constraint can be disabled for running queries not executed by a specific user
-   (e.g., public Cards)."
+  "Should we allow running queries (via `dataset-query`) without specifying the `executed-by` User ID?  By default
+  this is `false`, but this constraint can be disabled for running queries not executed by a specific user
+  (e.g., public Cards)."
   false)
 
 (defn- query-execution-info
@@ -227,7 +233,8 @@
    :start_time_millis (System/currentTimeMillis)})
 
 (defn- run-and-save-query!
-  "Run QUERY and save appropriate `QueryExecution` info, and then return results (or an error message) in the usual format."
+  "Run QUERY and save appropriate `QueryExecution` info, and then return results (or an error message) in the usual
+  format."
   [query]
   (let [query-execution (query-execution-info query)]
     (try
@@ -235,7 +242,9 @@
         (assert-query-status-successful result)
         (save-and-return-successful-query! query-execution result))
       (catch Throwable e
-        (log/warn (u/format-color 'red "Query failure: %s\n%s" (.getMessage e) (u/pprint-to-str (u/filtered-stacktrace e))))
+        (log/warn (u/format-color 'red "Query failure: %s\n%s"
+                    (.getMessage e)
+                    (u/pprint-to-str (u/filtered-stacktrace e))))
         (save-and-return-failed-query! query-execution (.getMessage e))))))
 
 (def ^:private DatasetQueryOptions
@@ -255,7 +264,7 @@
                        *allow-queries-with-no-executor-id*))
                  "executed-by cannot be nil unless *allow-queries-with-no-executor-id* is true"))
 
-(s/defn ^:always-validate process-query-and-save-execution!
+(s/defn process-query-and-save-execution!
   "Process and run a json based dataset query and return results.
 
   Takes 2 arguments:
diff --git a/src/metabase/query_processor/middleware/expand.clj b/src/metabase/query_processor/middleware/expand.clj
index f79e51002a49e6b3e6716dbc5ba1cab8fab79aa1..516a8570ef8dc03b2f55c81173c1778b2919ef55 100644
--- a/src/metabase/query_processor/middleware/expand.clj
+++ b/src/metabase/query_processor/middleware/expand.clj
@@ -1,6 +1,6 @@
 (ns metabase.query-processor.middleware.expand
-  "Converts a Query Dict as received by the API into an *expanded* one that contains extra information that will be needed to
-   construct the appropriate native Query, and perform various post-processing steps such as Field ordering."
+  "Converts a Query Dict as received by the API into an *expanded* one that contains extra information that will be
+  needed to construct the appropriate native Query, and perform various post-processing steps such as Field ordering."
   (:refer-clojure :exclude [< <= > >= = != and or not filter count distinct sum min max + - / *])
   (:require [clojure.core :as core]
             [clojure.tools.logging :as log]
@@ -19,7 +19,7 @@
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
 ;; TODO - check that there's a matching :aggregation clause in the query ?
-(s/defn ^:ql ^:always-validate aggregate-field :- AgFieldRef
+(s/defn ^:ql aggregate-field :- AgFieldRef
   "Aggregate field referece, e.g. for use in an `order-by` clause.
 
      (query (aggregate (count))
@@ -27,7 +27,7 @@
   [index :- s/Int]
   (i/map->AgFieldRef {:index index}))
 
-(s/defn ^:ql ^:always-validate field-id :- i/AnyField
+(s/defn ^:ql field-id :- i/AnyField
   "Create a generic reference to a `Field` with ID."
   [id]
   ;; If for some reason we were passed a field literal (e.g. [field-id [field-literal ...]])
@@ -39,7 +39,7 @@
       id)
     (i/map->FieldPlaceholder {:field-id id})))
 
-(s/defn ^:private ^:always-validate field :- i/AnyField
+(s/defn ^:private field :- i/AnyField
   "Generic reference to a `Field`. F can be an integer Field ID, or various other forms like `fk->` or `aggregation`."
   [f]
   (if (integer? f)
@@ -47,23 +47,25 @@
         (field-id f))
     f))
 
-(s/defn ^:ql ^:always-validate field-literal :- FieldLiteral
-  "Generic reference to a Field by FIELD-NAME. This is intended for use when using nested queries so as to allow one to refer to the fields coming back from
-   the source query."
+(s/defn ^:ql field-literal :- FieldLiteral
+  "Generic reference to a Field by FIELD-NAME. This is intended for use when using nested queries so as to allow one
+   to refer to the fields coming back from the source query."
   [field-name :- su/KeywordOrString, field-type :- su/KeywordOrString]
   (i/map->FieldLiteral {:field-name (u/keyword->qualified-name field-name), :base-type (keyword field-type)}))
 
-(s/defn ^:ql ^:always-validate named :- i/Aggregation
+(s/defn ^:ql named :- i/Aggregation
   "Specify a CUSTOM-NAME to use for a top-level AGGREGATION-OR-EXPRESSION in the results.
-   (This will probably be extended to support Fields in the future, but for now, only the `:aggregation` clause is supported.)"
+   (This will probably be extended to support Fields in the future, but for now, only the `:aggregation` clause is
+   supported.)"
   {:added "0.22.0"}
   [aggregation-or-expression :- i/Aggregation, custom-name :- su/NonBlankString]
   (assoc aggregation-or-expression :custom-name custom-name))
 
-(s/defn ^:ql ^:always-validate datetime-field :- i/AnyField
+(s/defn ^:ql datetime-field :- i/AnyField
   "Reference to a `DateTimeField`. This is just a `Field` reference with an associated datetime UNIT."
   ([f _ unit]
-   (log/warn (u/format-color 'yellow (str "The syntax for datetime-field has changed in MBQL '98. [:datetime-field <field> :as <unit>] is deprecated. "
+   (log/warn (u/format-color 'yellow (str "The syntax for datetime-field has changed in MBQL '98. "
+                                          "[:datetime-field <field> :as <unit>] is deprecated. "
                                           "Prefer [:datetime-field <field> <unit>] instead.")))
    (datetime-field f unit))
   ([f unit]
@@ -74,9 +76,9 @@
      ;; (:datetime-unit f)          f
      :else                       (assoc (field f) :datetime-unit (qputil/normalize-token unit)))))
 
-(s/defn ^:ql ^:always-validate fk-> :- FieldPlaceholder
-  "Reference to a `Field` that belongs to another `Table`. DEST-FIELD-ID is the ID of this Field, and FK-FIELD-ID is the ID of the foreign key field
-   belonging to the *source table* we should use to perform the join.
+(s/defn ^:ql fk-> :- FieldPlaceholder
+  "Reference to a `Field` that belongs to another `Table`. DEST-FIELD-ID is the ID of this Field, and FK-FIELD-ID is
+   the ID of the foreign key field belonging to the *source table* we should use to perform the join.
 
    `fk->` is so named because you can think of it as \"going through\" the FK Field to get to the dest Field:
 
@@ -96,7 +98,7 @@
                                    (:unit f)
                                    (:unit v))))
 
-(s/defn ^:private ^:always-validate value :- i/AnyValue
+(s/defn ^:private value :- i/AnyValue
   "Literal value. F is the `Field` it relates to, and V is `nil`, or a boolean, string, numerical, or datetime value."
   [f v]
   (cond
@@ -109,7 +111,7 @@
     (instance? FieldLiteral f)          (i/map->Value {:value v, :field f})
     :else                               (i/map->ValuePlaceholder {:field-placeholder (field f), :value v})))
 
-(s/defn ^:private ^:always-validate field-or-value
+(s/defn ^:private field-or-value
   "Use instead of `value` when something may be either a field or a value."
   [f v]
 
@@ -118,10 +120,11 @@
     v
     (value f v)))
 
-(s/defn ^:ql ^:always-validate relative-datetime :- RelativeDatetime
+(s/defn ^:ql relative-datetime :- RelativeDatetime
   "Value that represents a point in time relative to each moment the query is ran, e.g. \"today\" or \"1 year ago\".
 
-   With `:current` as the only arg, refer to the current point in time; otherwise N is some number and UNIT is a unit like `:day` or `:year`.
+   With `:current` as the only arg, refer to the current point in time; otherwise N is some number and UNIT is a unit
+   like `:day` or `:year`.
 
      (relative-datetime :current)
      (relative-datetime -31 :day)"
@@ -131,7 +134,7 @@
                                                                    :day                        ; give :unit a default value so we can simplify the schema a bit and require a :unit
                                                                    (qputil/normalize-token unit))})))
 
-(s/defn ^:ql ^:always-validate expression :- ExpressionRef
+(s/defn ^:ql expression :- ExpressionRef
   {:added "0.17.0"}
   [expression-name :- su/KeywordOrString]
   (i/strict-map->ExpressionRef {:expression-name (name expression-name)}))
@@ -151,7 +154,7 @@
     ;; otherwise if it's not an Expression it's a Field
     (field f)))
 
-(s/defn ^:private ^:always-validate ag-with-field :- i/Aggregation [ag-type f]
+(s/defn ^:private ag-with-field :- i/Aggregation [ag-type f]
   (i/map->AggregationWithField {:aggregation-type ag-type, :field (field-or-expression f)}))
 
 (def ^:ql ^{:arglists '([f])} avg      "Aggregation clause. Return the average value of F."                (partial ag-with-field :avg))
@@ -168,12 +171,12 @@
   (i/assert-driver-supports :standard-deviation-aggregations)
   (ag-with-field :stddev f))
 
-(s/defn ^:ql ^:always-validate count :- i/Aggregation
+(s/defn ^:ql count :- i/Aggregation
   "Aggregation clause. Return total row count (e.g., `COUNT(*)`). If F is specified, only count rows where F is non-null (e.g. `COUNT(f)`)."
   ([]  (i/map->AggregationWithoutField {:aggregation-type :count}))
   ([f] (ag-with-field :count f)))
 
-(s/defn ^:ql ^:always-validate cum-count :- i/Aggregation
+(s/defn ^:ql cum-count :- i/Aggregation
   "Aggregation clause. Return the cumulative row count (presumably broken out in some way)."
   []
   (i/map->AggregationWithoutField {:aggregation-type :cumulative-count}))
@@ -183,7 +186,7 @@
   []
   (log/warn (u/format-color 'yellow "Specifying :rows as the aggregation type is deprecated in MBQL '98. This is the default behavior, so you don't need to specify it.")))
 
-(s/defn ^:ql ^:always-validate aggregation
+(s/defn ^:ql aggregation
   "Specify the aggregation to be performed for this query.
 
      (aggregation {} (count 100))
@@ -213,7 +216,7 @@
 
 ;;; ## breakout & fields
 
-(s/defn ^:ql ^:always-validate binning-strategy :- FieldPlaceholder
+(s/defn ^:ql binning-strategy :- FieldPlaceholder
   "Reference to a `BinnedField`. This is just a `Field` reference with an associated `STRATEGY-NAME` and `STRATEGY-PARAM`"
   ([f strategy-name & [strategy-param]]
    (let [strategy (qputil/normalize-token strategy-name)
@@ -229,7 +232,7 @@
 
 ;;; ## filter
 
-(s/defn ^:private ^:always-validate compound-filter :- i/Filter
+(s/defn ^:private compound-filter :- i/Filter
   ([compound-type, subclause :- i/Filter]
    (log/warn (u/format-color 'yellow "You shouldn't specify an %s filter with only one subclause." compound-type))
    subclause)
@@ -240,7 +243,7 @@
 (def ^:ql ^{:arglists '([& subclauses])} and "Filter subclause. Return results that satisfy *all* SUBCLAUSES." (partial compound-filter :and))
 (def ^:ql ^{:arglists '([& subclauses])} or  "Filter subclause. Return results that satisfy *any* of the SUBCLAUSES." (partial compound-filter :or))
 
-(s/defn ^:private ^:always-validate equality-filter :- i/Filter
+(s/defn ^:private equality-filter :- i/Filter
   ([filter-type _ f v]
    (i/map->EqualityFilter {:filter-type filter-type, :field (field f), :value (field-or-value f v)}))
   ([filter-type compound-fn f v & more]
@@ -248,14 +251,16 @@
                         (equality-filter filter-type compound-fn f v)))))
 
 (def ^:ql ^{:arglists '([f v & more])} =
-  "Filter subclause. With a single value, return results where F == V. With two or more values, return results where F matches *any* of the values (i.e.`IN`)
+  "Filter subclause. With a single value, return results where F == V. With two or more values, return results where F
+  matches *any* of the values (i.e.`IN`)
 
      (= f v)
      (= f v1 v2) ; same as (or (= f v1) (= f v2))"
   (partial equality-filter := or))
 
 (def ^:ql ^{:arglists '([f v & more])} !=
-  "Filter subclause. With a single value, return results where F != V. With two or more values, return results where F does not match *any* of the values (i.e. `NOT IN`)
+  "Filter subclause. With a single value, return results where F != V. With two or more values, return results where F
+  does not match *any* of the values (i.e. `NOT IN`)
 
      (!= f v)
      (!= f v1 v2) ; same as (and (!= f v1) (!= f v2))"
@@ -264,7 +269,7 @@
 (defn ^:ql is-null  "Filter subclause. Return results where F is `nil`."     [f] (=  f nil)) ; TODO - Should we deprecate these? They're syntactic sugar, and not particualarly useful.
 (defn ^:ql not-null "Filter subclause. Return results where F is not `nil`." [f] (!= f nil)) ; not-null is doubly unnecessary since you could just use `not` instead.
 
-(s/defn ^:private ^:always-validate comparison-filter :- ComparisonFilter [filter-type f v]
+(s/defn ^:private comparison-filter :- ComparisonFilter [filter-type f v]
   (i/map->ComparisonFilter {:filter-type filter-type, :field (field f), :value (value f v)}))
 
 (def ^:ql ^{:arglists '([f v])} <  "Filter subclause. Return results where F is less than V. V must be orderable, i.e. a number or datetime."                (partial comparison-filter :<))
@@ -272,29 +277,30 @@
 (def ^:ql ^{:arglists '([f v])} >  "Filter subclause. Return results where F is greater than V. V must be orderable, i.e. a number or datetime."             (partial comparison-filter :>))
 (def ^:ql ^{:arglists '([f v])} >= "Filter subclause. Return results where F is greater than or equal to V. V must be orderable, i.e. a number or datetime." (partial comparison-filter :>=))
 
-(s/defn ^:ql ^:always-validate between :- BetweenFilter
+(s/defn ^:ql between :- BetweenFilter
   "Filter subclause. Return results where F is between MIN and MAX. MIN and MAX must be orderable, i.e. numbers or datetimes.
    This behaves like SQL `BETWEEN`, i.e. MIN and MAX are inclusive."
   [f min-val max-val]
   (i/map->BetweenFilter {:filter-type :between, :field (field f), :min-val (value f min-val), :max-val (value f max-val)}))
 
-(s/defn ^:ql ^:always-validate inside :- CompoundFilter
+(s/defn ^:ql inside :- CompoundFilter
   "Filter subclause for geo bounding. Return results where LAT-FIELD and LON-FIELD are between some set of bounding values."
   [lat-field lon-field lat-max lon-min lat-min lon-max]
   (and (between lat-field lat-min lat-max)
        (between lon-field lon-min lon-max)))
 
-(s/defn ^:private ^:always-validate string-filter :- StringFilter [filter-type f s]
+(s/defn ^:private string-filter :- StringFilter [filter-type f s]
   (i/map->StringFilter {:filter-type filter-type, :field (field f), :value (value f s)}))
 
 (def ^:ql ^{:arglists '([f s])} starts-with "Filter subclause. Return results where F starts with the string S."    (partial string-filter :starts-with))
 (def ^:ql ^{:arglists '([f s])} contains    "Filter subclause. Return results where F contains the string S."       (partial string-filter :contains))
 (def ^:ql ^{:arglists '([f s])} ends-with   "Filter subclause. Return results where F ends with with the string S." (partial string-filter :ends-with))
 
-(s/defn ^:ql ^:always-validate not :- i/Filter
+(s/defn ^:ql not :- i/Filter
   "Filter subclause. Return results that do *not* satisfy SUBCLAUSE.
 
-   For the sake of simplifying driver implementation, `not` automatically translates its argument to a simpler, logically equivalent form whenever possible:
+   For the sake of simplifying driver implementation, `not` automatically translates its argument to a simpler,
+   logically equivalent form whenever possible:
 
      (not (and x y)) -> (or (not x) (not y))
      (not (not x))   -> x
@@ -318,9 +324,11 @@
                             (> field max-val)))
              (i/strict-map->NotFilter {:compound-type :not, :subclause clause})))))
 
-(def ^:ql ^{:arglists '([f s]), :added "0.15.0"} does-not-contain "Filter subclause. Return results where F does not start with the string S." (comp not contains))
+(def ^:ql ^{:arglists '([f s]), :added "0.15.0"} does-not-contain
+  "Filter subclause. Return results where F does not start with the string S."
+  (comp not contains))
 
-(s/defn ^:ql ^:always-validate time-interval :- i/Filter
+(s/defn ^:ql time-interval :- i/Filter
   "Filter subclause. Syntactic sugar for specifying a specific time interval.
 
     ;; return rows where datetime Field 100's value is in the current day
@@ -341,7 +349,7 @@
         (core/> n  1) (between f (value f (relative-datetime  1 unit))
                                  (value f (relative-datetime  n unit)))))))
 
-(s/defn ^:ql ^:always-validate filter
+(s/defn ^:ql filter
   "Filter the results returned by the query.
 
      (filter {} := 100 true) ; return rows where Field 100 == true"
@@ -350,7 +358,7 @@
     (assoc query :filter filter-map)
     query))
 
-(s/defn ^:ql ^:always-validate limit
+(s/defn ^:ql limit
   "Limit the number of results returned by the query.
 
      (limit {} 10)"
@@ -362,7 +370,7 @@
 
 ;;; ## order-by
 
-(s/defn ^:private ^:always-validate order-by-subclause :- i/OrderBy
+(s/defn ^:private order-by-subclause :- i/OrderBy
   [direction :- i/OrderByDirection, f]
   ;; it's not particularly useful to sort datetime fields with the default `:day` bucketing,
   ;; so specifiy `:default` bucketing to prevent the default of `:day` from being set during resolution.
@@ -386,7 +394,7 @@
      (order-by {} (desc 100))"
   (partial order-by-subclause :descending))
 
-(s/defn ^:private ^:always-validate maybe-parse-order-by-subclause :- i/OrderBy
+(s/defn ^:private maybe-parse-order-by-subclause :- i/OrderBy
   [subclause]
   (cond
     (map? subclause)    subclause ; already parsed by `asc` or `desc`
@@ -407,7 +415,7 @@
 
 ;;; ## page
 
-(s/defn ^:ql ^:always-validate page
+(s/defn ^:ql page
   "Specify which 'page' of results to fetch (offset and limit the results).
 
      (page {} {:page 1, :items 20}) ; fetch first 20 rows"
@@ -418,7 +426,7 @@
 
 ;;; ## source-table
 
-(s/defn ^:ql ^:always-validate source-table
+(s/defn ^:ql source-table
   "Specify the ID of the table to query.
    Queries must specify *either* `:source-table` or `:source-query`.
 
@@ -428,7 +436,7 @@
 
 (declare expand-inner)
 
-(s/defn ^:ql ^:always-validate source-query
+(s/defn ^:ql source-query
   "Specify a query to use as the source for this query (e.g., as a `SUBSELECT`).
    Queries must specify *either* `:source-table` or `:source-query`.
 
@@ -443,13 +451,13 @@
 
 ;;; ## calculated columns
 
-(s/defn ^:ql ^:always-validate expressions
+(s/defn ^:ql expressions
   "Top-level clause. Add additional calculated fields to a query."
   {:added "0.17.0"}
   [query, m :- {s/Keyword Expression}]
   (assoc query :expressions m))
 
-(s/defn ^:private ^:always-validate expression-fn :- Expression
+(s/defn ^:private expression-fn :- Expression
   [k :- s/Keyword, & args]
   (i/map->Expression {:operator k, :args (vec (for [arg args]
                                                 (if (number? arg)
@@ -463,10 +471,11 @@
 
 ;;; Metric & Segment handlers
 
-;; These *do not* expand the normal Metric and Segment macros used in normal queries; that's handled in `metabase.query-processor.macros` before
-;; this namespace ever even sees the query. But since the GA driver's queries consist of custom `metric` and `segment` clauses we need to at least
-;; accept them without barfing so we can expand a query in order to check what permissions it requires.
-;; TODO - in the future, we should just make these functions expand Metric and Segment macros for consistency with the rest of the MBQL clauses
+;; These *do not* expand the normal Metric and Segment macros used in normal queries; that's handled in
+;; `metabase.query-processor.macros` before this namespace ever even sees the query. But since the GA driver's queries
+;; consist of custom `metric` and `segment` clauses we need to at least accept them without barfing so we can expand a
+;; query in order to check what permissions it requires.  TODO - in the future, we should just make these functions
+;; expand Metric and Segment macros for consistency with the rest of the MBQL clauses
 (defn ^:ql metric  "Placeholder expansion function for GA metric clauses. (This does not expand normal Metric macros; that is done in `metabase.query-processor.macros`.)"   [& _])
 (defn ^:ql segment "Placeholder expansion function for GA segment clauses. (This does not expand normal Segment macros; that is done in `metabase.query-processor.macros`.)" [& _])
 
@@ -491,7 +500,7 @@
     (core/or (token->ql-fn token)
              (throw (Exception. (str "Illegal clause (no matching fn found): " token))))))
 
-(s/defn ^:always-validate expand-ql-sexpr
+(s/defn expand-ql-sexpr
   "Expand a QL bracketed S-expression by dispatching to the appropriate `^:ql` function. If SEXPR is not a QL
    S-expression (the first item isn't a token), it is returned as-is.
 
@@ -511,7 +520,7 @@
         :else           x))
 
 
-(s/defn ^:always-validate expand-inner :- i/Query
+(s/defn expand-inner :- i/Query
   "Expand an inner query map."
   [inner-query :- (s/pred map?)]
   (loop [query {}, [[clause-name arg] & more] (seq inner-query)]
diff --git a/src/metabase/query_processor/middleware/fetch_source_query.clj b/src/metabase/query_processor/middleware/fetch_source_query.clj
index fc3e6eeaceea475203c67bf178a85656bce0ccb2..76a95249c44ef6f42bf2ed64a4609d638d6eb82e 100644
--- a/src/metabase/query_processor/middleware/fetch_source_query.clj
+++ b/src/metabase/query_processor/middleware/fetch_source_query.clj
@@ -1,4 +1,5 @@
 (ns metabase.query-processor.middleware.fetch-source-query
+  "Middleware responsible for 'hydrating' the source query for queries that use another query as their source."
   (:require [clojure.tools.logging :as log]
             [metabase.query-processor
              [interface :as i]
@@ -16,7 +17,8 @@
                  {:native        (:query native)
                   :template_tags (:template_tags native)})
                (throw (Exception. (str "Missing source query in Card " card-id))))
-      ;; include database ID as well; we'll pass that up the chain so it eventually gets put in its spot in the outer-query
+      ;; include database ID as well; we'll pass that up the chain so it eventually gets put in its spot in the
+      ;; outer-query
       :database (:database card-query))))
 
 (defn- source-table-str->source-query
@@ -28,8 +30,8 @@
         (log/info "\nFETCHED SOURCE QUERY FROM CARD" card-id-str ":\n" (u/pprint-to-str 'yellow <>))))))
 
 (defn- expand-card-source-tables
-  "If `source-table` is a Card reference (a string like `card__100`) then replace that with appropriate `:source-query` information.
-   Does nothing if `source-table` is a normal ID. Recurses for nested-nested queries."
+  "If `source-table` is a Card reference (a string like `card__100`) then replace that with appropriate
+  `:source-query` information. Does nothing if `source-table` is a normal ID. Recurses for nested-nested queries."
   [inner-query]
   (let [source-table (qputil/get-normalized inner-query :source-table)]
     (if-not (string? source-table)
@@ -39,7 +41,8 @@
         (-> inner-query
             ;; remove `source-table` `card__id` key
             (qputil/dissoc-normalized :source-table)
-            ;; Add new `source-query` info in its place. Pass the database ID up the chain, removing it from the source query
+            ;; Add new `source-query` info in its place. Pass the database ID up the chain, removing it from the
+            ;; source query
             (assoc
               :source-query (dissoc source-query :database)
               :database     (:database source-query)))))))
@@ -56,6 +59,7 @@
                {:database database})))))
 
 (defn fetch-source-query
-  "Middleware that assocs the `:source-query` for this query if it was specified using the shorthand `:source-table` `card__n` format."
+  "Middleware that assocs the `:source-query` for this query if it was specified using the shorthand `:source-table`
+  `card__n` format."
   [qp]
   (comp qp fetch-source-query*))
diff --git a/src/metabase/query_processor/middleware/parameters/sql.clj b/src/metabase/query_processor/middleware/parameters/sql.clj
index f1046f6716db62e700ccfefd483f5b8d4ecd5121..896234fc8a7896e30be66a6cc589dcade5157464 100644
--- a/src/metabase/query_processor/middleware/parameters/sql.clj
+++ b/src/metabase/query_processor/middleware/parameters/sql.clj
@@ -121,7 +121,7 @@
 ;;                                   :target ["\dimension"\ ["\template-tag"\ "\checkin_date"\]]
 ;;                                   :value  "\2015-01-01~2016-09-01"\}}}
 
-(s/defn ^:private ^:always-validate param-with-target
+(s/defn ^:private param-with-target
   "Return the param in PARAMS with a matching TARGET. TARGET is something like:
 
      [:dimension [:template-tag <param-name>]] ; for Dimensions (Field Filters)
@@ -138,7 +138,7 @@
 
 ;;; Dimension Params (Field Filters) (e.g. WHERE {{x}})
 
-(s/defn ^:private ^:always-validate default-value-for-dimension :- (s/maybe DimensionValue)
+(s/defn ^:private default-value-for-dimension :- (s/maybe DimensionValue)
   "Return the default value for a Dimension (Field Filter) param defined by the map TAG, if one is set."
   [tag :- TagParam]
   (when-let [default (:default tag)]
@@ -146,11 +146,11 @@
      :target ["dimension" ["template-tag" (:name tag)]]
      :value  default}))
 
-(s/defn ^:private ^:always-validate dimension->field-id :- su/IntGreaterThanZero
+(s/defn ^:private dimension->field-id :- su/IntGreaterThanZero
   [dimension]
   (:field-id (ql/expand-ql-sexpr dimension)))
 
-(s/defn ^:private ^:always-validate dimension-value-for-tag :- (s/maybe Dimension)
+(s/defn ^:private dimension-value-for-tag :- (s/maybe Dimension)
   "Return the \"Dimension\" value of a param, if applicable. \"Dimension\" here means what is called a \"Field
   Filter\" in the Native Query Editor."
   [tag :- TagParam, params :- (s/maybe [DimensionValue])]
@@ -166,11 +166,11 @@
 
 ;;; Non-Dimension Params (e.g. WHERE x = {{x}})
 
-(s/defn ^:private ^:always-validate param-value-for-tag [tag :- TagParam, params :- (s/maybe [DimensionValue])]
+(s/defn ^:private param-value-for-tag [tag :- TagParam, params :- (s/maybe [DimensionValue])]
   (when (not= (:type tag) "dimension")
     (:value (param-with-target params ["variable" ["template-tag" (:name tag)]]))))
 
-(s/defn ^:private ^:always-validate default-value-for-tag
+(s/defn ^:private default-value-for-tag
   "Return the `:default` value for a param if no explicit values were passsed. This only applies to non-Dimension
    (non-Field Filter) params. Default values for Dimension (Field Filter) params are handled above in
    `default-value-for-dimension`."
@@ -182,13 +182,13 @@
 
 ;;; Parsing Values
 
-(s/defn ^:private ^:always-validate parse-number :- s/Num
+(s/defn ^:private parse-number :- s/Num
   "Parse a string like `1` or `2.0` into a valid number. Done mostly to keep people from passing in
    things that aren't numbers, like SQL identifiers."
   [s :- s/Str]
   (.parse (NumberFormat/getInstance) ^String s))
 
-(s/defn ^:private ^:always-validate value->number :- (s/cond-pre s/Num CommaSeparatedNumbers)
+(s/defn ^:private value->number :- (s/cond-pre s/Num CommaSeparatedNumbers)
   "Parse a 'numeric' param value. Normally this returns an integer or floating-point number,
    but as a somewhat undocumented feature it also accepts comma-separated lists of numbers. This was a side-effect of
    the old parameter code that unquestioningly substituted any parameter passed in as a number directly into the SQL.
@@ -212,7 +212,7 @@
         ;; otherwise just return the single number
         (first parts)))))
 
-(s/defn ^:private ^:always-validate parse-value-for-type :- ParamValue
+(s/defn ^:private parse-value-for-type :- ParamValue
   [param-type value]
   (cond
     (instance? NoValue value)                        value
@@ -222,7 +222,7 @@
          (= (get-in value [:param :type]) "number")) (update-in value [:param :value] value->number)
     :else                                            value))
 
-(s/defn ^:private ^:always-validate value-for-tag :- ParamValue
+(s/defn ^:private value-for-tag :- ParamValue
   "Given a map TAG (a value in the `:template_tags` dictionary) return the corresponding value from the PARAMS
    sequence. The VALUE is something that can be compiled to SQL via `->replacement-snippet-info`."
   [tag :- TagParam, params :- (s/maybe [DimensionValue])]
@@ -232,7 +232,7 @@
                                         ;; TODO - what if value is specified but is `nil`?
                                         (NoValue.))))
 
-(s/defn ^:private ^:always-validate query->params-map :- ParamValues
+(s/defn ^:private query->params-map :- ParamValues
   "Extract parameters info from QUERY. Return a map of parameter name -> value.
 
      (query->params-map some-query)
@@ -272,15 +272,15 @@
 (defn- date-param-type?          [param-type] (str/starts-with? param-type "date/"))
 
 ;; for relative dates convert the param to a `DateRange` record type and call `->replacement-snippet-info` on it
-(s/defn ^:private ^:always-validate relative-date-dimension-value->replacement-snippet-info :- ParamSnippetInfo
+(s/defn ^:private relative-date-dimension-value->replacement-snippet-info :- ParamSnippetInfo
   [value]
   (->replacement-snippet-info (map->DateRange (date-params/date-string->range value *timezone*)))) ; TODO - get timezone from query dict
 
-(s/defn ^:private ^:always-validate dimension-value->equals-clause-sql :- ParamSnippetInfo
+(s/defn ^:private dimension-value->equals-clause-sql :- ParamSnippetInfo
   [value]
   (update (->replacement-snippet-info value) :replacement-snippet (partial str "= ")))
 
-(s/defn ^:private ^:always-validate dimension->replacement-snippet-info :- ParamSnippetInfo
+(s/defn ^:private dimension->replacement-snippet-info :- ParamSnippetInfo
   "Return `[replacement-snippet & prepared-statement-args]` appropriate for a DIMENSION parameter."
   [{param-type :type, value :value} :- DimensionValue]
   (cond
@@ -288,14 +288,14 @@
     (date-param-type? param-type)          (dimension-value->equals-clause-sql (map->Date {:s value}))     ; convert all other dates to `= <date>`
     :else                                  (dimension-value->equals-clause-sql value)))                    ; convert everything else to `= <value>`
 
-(s/defn ^:private ^:always-validate honeysql->replacement-snippet-info :- ParamSnippetInfo
+(s/defn ^:private honeysql->replacement-snippet-info :- ParamSnippetInfo
   "Convert X to a replacement snippet info map by passing it to HoneySQL's `format` function."
   [x]
   (let [[snippet & args] (hsql/format x, :quoting ((resolve 'metabase.driver.generic-sql/quote-style) *driver*))]
     {:replacement-snippet     snippet
      :prepared-statement-args args}))
 
-(s/defn ^:private ^:always-validate field->identifier :- su/NonBlankString
+(s/defn ^:private field->identifier :- su/NonBlankString
   "Return an approprate snippet to represent this FIELD in SQL given its param type.
    For non-date Fields, this is just a quoted identifier; for dates, the SQL includes appropriately bucketing based on
    the PARAM-TYPE."
@@ -306,7 +306,7 @@
                                               identifier)))
       :replacement-snippet))
 
-(s/defn ^:private ^:always-validate combine-replacement-snippet-maps :- ParamSnippetInfo
+(s/defn ^:private combine-replacement-snippet-maps :- ParamSnippetInfo
   "Combine multiple REPLACEMENT-SNIPPET-MAPS into a single map using a SQL `AND` clause."
   [replacement-snippet-maps :- [ParamSnippetInfo]]
   {:replacement-snippet     (str \( (str/join " AND " (map :replacement-snippet replacement-snippet-maps)) \))
@@ -371,14 +371,14 @@
 ;;     :optional-snippet "AND timestamp < {{timestamp}}"      ; portion of the snippet inside [[optional]] brackets, or `nil` if the snippet isn't optional
 ;;     :variable-snippet "{{timestamp}}"}                     ; portion of the snippet referencing the variable itself, e.g. {{x}}
 
-(s/defn ^:private ^:always-validate param-snippet->param-name :- s/Keyword
+(s/defn ^:private param-snippet->param-name :- s/Keyword
   "Return the keyword name of the param being referenced inside PARAM-SNIPPET.
 
      (param-snippet->param-name \"{{x}}\") -> :x"
   [param-snippet :- su/NonBlankString]
   (keyword (second (re-find #"\{\{\s*(\w+)\s*\}\}" param-snippet))))
 
-(s/defn ^:private ^:always-validate sql->params-snippets-info :- [ParamSnippetInfo]
+(s/defn ^:private sql->params-snippets-info :- [ParamSnippetInfo]
   "Return a sequence of maps containing information about the param snippets found by paring SQL."
   [sql :- su/NonBlankString]
   (for [[param-snippet optional-replacement-snippet] (re-seq #"(?:\[\[(.+?)\]\])|(?:\{\{\s*\w+\s*\}\})" sql)]
@@ -407,7 +407,7 @@
 ;; (Basically these functions take `:param-key`, `:optional-snippet`, and `:variable-snippet` from the Query Parsing stage and the info from the other stages
 ;; to add the appropriate info for `:replacement-snippet` and `:prepared-statement-args`.)
 
-(s/defn ^:private ^:always-validate snippet-value :- ParamValue
+(s/defn ^:private snippet-value :- ParamValue
   "Fetch the value from PARAM-KEY->VALUE for SNIPPET-INFO.
    If no value is specified, return `NoValue` if the snippet is optional; otherwise throw an Exception."
   [{:keys [param-key optional-snippet]} :- ParamSnippetInfo, param-key->value :- ParamValues]
@@ -418,7 +418,7 @@
       (throw (ex-info (format "Unable to substitute '%s': param not specified.\nFound: %s" param-key (keys param-key->value))
                {:status-code 400})))))
 
-(s/defn ^:private ^:always-validate handle-optional-snippet :- ParamSnippetInfo
+(s/defn ^:private handle-optional-snippet :- ParamSnippetInfo
   "Create the approprate `:replacement-snippet` for PARAM, combining the value of REPLACEMENT-SNIPPET from the Param->SQL Substitution phase
    with the OPTIONAL-SNIPPET, if any."
   [{:keys [variable-snippet optional-snippet replacement-snippet prepared-statement-args], :as snippet-info} :- ParamSnippetInfo]
@@ -433,7 +433,7 @@
                                (apply concat (repeat occurances prepared-statement-args))
                                prepared-statement-args)))
 
-(s/defn ^:private ^:always-validate add-replacement-snippet-info :- [ParamSnippetInfo]
+(s/defn ^:private add-replacement-snippet-info :- [ParamSnippetInfo]
   "Add `:replacement-snippet` and `:prepared-statement-args` info to the maps in PARAMS-SNIPPETS-INFO by looking at
    PARAM-KEY->VALUE and using the Param->SQL substituion functions."
   [params-snippets-info :- [ParamSnippetInfo], param-key->value :- ParamValues]
@@ -450,12 +450,12 @@
 ;; These functions take the information about parameters from the Params Details List functions and then convert the
 ;; original SQL Query into a SQL query with appropriate subtitutions and a sequence of prepared statement args
 
-(s/defn ^:private ^:always-validate substitute-one
+(s/defn ^:private substitute-one
   [sql :- su/NonBlankString, {:keys [original-snippet replacement-snippet]} :- ParamSnippetInfo]
   (str/replace-first sql original-snippet replacement-snippet))
 
 
-(s/defn ^:private ^:always-validate substitute :- {:query su/NonBlankString, :params [s/Any]}
+(s/defn ^:private substitute :- {:query su/NonBlankString, :params [s/Any]}
   "Using the PARAM-SNIPPET-INFO built from the stages above, replace the snippets in SQL and return a vector of
    `[sql & prepared-statement-params]`."
   {:style/indent 1}
@@ -474,7 +474,7 @@
 ;;; |                                            PUTTING IT ALL TOGETHER                                             |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
-(s/defn ^:private ^:always-validate expand-query-params
+(s/defn ^:private expand-query-params
   [{sql :query, :as native}, param-key->value :- ParamValues]
   (merge native (when-let [param-snippets-info (seq (add-replacement-snippet-info (sql->params-snippets-info sql) param-key->value))]
                   (substitute sql param-snippets-info))))
diff --git a/src/metabase/query_processor/middleware/resolve.clj b/src/metabase/query_processor/middleware/resolve.clj
index 8b824332e8531ec793fb18802247e7631e284f10..2e05b72a00a5a2a09e5042098d32c956540a7849 100644
--- a/src/metabase/query_processor/middleware/resolve.clj
+++ b/src/metabase/query_processor/middleware/resolve.clj
@@ -22,9 +22,10 @@
              [db :as db]
              [hydrate :refer [hydrate]]])
   (:import java.util.TimeZone
-           [metabase.query_processor.interface DateTimeField DateTimeValue ExpressionRef Field FieldPlaceholder RelativeDatetime RelativeDateTimeValue Value ValuePlaceholder]))
+           [metabase.query_processor.interface DateTimeField DateTimeValue ExpressionRef Field FieldPlaceholder
+            RelativeDatetime RelativeDateTimeValue Value ValuePlaceholder]))
 
-;; # ---------------------------------------------------------------------- UTIL FNS ------------------------------------------------------------
+;;; ---------------------------------------------------- UTIL FNS ----------------------------------------------------
 
 (defn rename-mb-field-keys
   "Rename the keys in a Metabase `Field` to match the format of those in Query Expander `Fields`."
@@ -73,7 +74,7 @@
                               (-> dims rename-dimension-keys i/map->Dimensions )
                               dims)))))
 
-;;; # ------------------------------------------------------------ IRESOLVE PROTOCOL ------------------------------------------------------------
+;;; ----------------------------------------------- IRESOLVE PROTOCOL ------------------------------------------------
 
 (defprotocol ^:private IResolve
   (^:private unresolved-field-id ^Integer [this]
@@ -84,8 +85,8 @@
 
   (^:private resolve-field [this, ^clojure.lang.IPersistentMap field-id->field]
    "This method is called when walking the Query after fetching `Fields`.
-    Placeholder objects should lookup the relevant Field in FIELD-ID->FIELDS and
-    return their expanded form. Other objects should just return themselves.")
+    Placeholder objects should lookup the relevant Field in FIELD-ID->FIELDS and return their expanded form. Other
+    objects should just return themselves.")
 
   (resolve-table [this, ^clojure.lang.IPersistentMap fk-id+table-id->tables]
    "Called when walking the Query after `Fields` have been resolved and `Tables` have been fetched.
@@ -101,7 +102,7 @@
 (u/strict-extend nil    IResolve IResolveDefaults)
 
 
-;;; ## ------------------------------------------------------------ FIELD ------------------------------------------------------------
+;;; ----------------------------------------------------- FIELD ------------------------------------------------------
 
 (defn- field-unresolved-field-id [{:keys [parent parent-id]}]
   (or (unresolved-field-id parent)
@@ -121,13 +122,19 @@
 (defn- field-resolve-table [{:keys [table-id fk-field-id field-id], :as this} fk-id+table-id->table]
   {:pre [(map? fk-id+table-id->table) (every? vector? (keys fk-id+table-id->table))]}
   (let [table (or (fk-id+table-id->table [fk-field-id table-id])
-                  ;; if we didn't find a matching table check and see whether we're trying to use a field from another table without wrapping it in an fk-> form
+                  ;; if we didn't find a matching table check and see whether we're trying to use a field from another
+                  ;; table without wrapping it in an fk-> form
                   (doseq [[fk table] (keys fk-id+table-id->table)
                           :when      (and fk (= table table-id))]
-                    (throw (Exception. (format "Invalid query: Field %d belongs to table %d. Since %d is not the source table, it must be wrapped in a fk-> form, e.g. [fk-> %d %d]."
+                    (throw (Exception. (format (str "Invalid query: Field %d belongs to table %d. Since %d is not "
+                                                    "the source table, it must be wrapped in a fk-> form, e.g. "
+                                                    "[fk-> %d %d].")
                                                field-id table-id table-id fk field-id))))
-                  ;; Otherwise, we're using what is most likely an invalid Field ID; complain about it and give a list of tables that are valid
-                  (throw (Exception. (format "Query expansion failed: could not find table %d (FK ID = %d). Resolved tables ([fk-id table-id]): %s" table-id fk-field-id (keys fk-id+table-id->table)))))]
+                  ;; Otherwise, we're using what is most likely an invalid Field ID; complain about it and give a list
+                  ;; of tables that are valid
+                  (throw (Exception. (format (str "Query expansion failed: could not find table %d (FK ID = %d). "
+                                                  "Resolved tables ([fk-id table-id]): %s")
+                                             table-id fk-field-id (keys fk-id+table-id->table)))))]
     (assoc this
       :table-name  (:name table)
       :schema-name (:schema table))))
@@ -139,7 +146,7 @@
                    :resolve-table       field-resolve-table}))
 
 
-;;; ## ------------------------------------------------------------ FIELD PLACEHOLDER ------------------------------------------------------------
+;;; ----------------------------------------------- FIELD PLACEHOLDER ------------------------------------------------
 
 (defn- resolve-binned-field [{:keys [binning-strategy binning-param] :as field-ph} field]
   (let [binned-field (i/map->BinnedField {:field    field
@@ -188,7 +195,7 @@
                    :resolve-field       field-ph-resolve-field}))
 
 
-;;; ## ------------------------------------------------------------ VALUE PLACEHOLDER ------------------------------------------------------------
+;;; ----------------------------------------------- VALUE PLACEHOLDER ------------------------------------------------
 
 (defprotocol ^:private IParseValueForField
   (^:private parse-value [this value]
@@ -235,7 +242,7 @@
                   {:resolve-field value-ph-resolve-field}))
 
 
-;;; # ------------------------------------------------------------ IMPL ------------------------------------------------------------
+;;; ------------------------------------------------------ IMPL ------------------------------------------------------
 
 (defn- collect-ids-with [f expanded-query-dict]
   (let [ids (transient #{})]
@@ -266,7 +273,9 @@
         ;; If there are no more Field IDs to resolve we're done.
         expanded-query-dict
         ;; Otherwise fetch + resolve the Fields in question
-        (let [fields (->> (u/key-by :id (-> (db/select [field/Field :name :display_name :base_type :special_type :visibility_type :table_id :parent_id :description :id :fingerprint]
+        (let [fields (->> (u/key-by :id (-> (db/select [field/Field :name :display_name :base_type :special_type
+                                                        :visibility_type :table_id :parent_id :description :id
+                                                        :fingerprint]
                                               :visibility_type [:not= "sensitive"]
                                               :id              [:in field-ids])
                                             (hydrate :values)
@@ -275,8 +284,8 @@
                           (m/map-vals #(assoc % :parent (when-let [parent-id (:parent-id %)]
                                                           (i/map->FieldPlaceholder {:field-id parent-id})))))]
           (->>
-           ;; Now record the IDs of Tables these fields references in the :table-ids property of the expanded query dict.
-           ;; Those will be used for Table resolution in the next step.
+           ;; Now record the IDs of Tables these fields references in the :table-ids property of the expanded query
+           ;; dict. Those will be used for Table resolution in the next step.
            (update expanded-query-dict :table-ids set/union (set (map :table-id (vals fields))))
            ;; Walk the query and resolve all fields
            (walk/postwalk (u/rpartial resolve-field fields))
@@ -284,9 +293,10 @@
            (recur (dec max-iterations))))))))
 
 (defn- fk-field-ids->info
-  "Given a SOURCE-TABLE-ID and collection of FK-FIELD-IDS, return a sequence of maps containing IDs and identifiers for those FK fields and their target tables and fields.
-   FK-FIELD-IDS are IDs of fields that belong to the source table. For example, SOURCE-TABLE-ID might be 'checkins' and FK-FIELD-IDS might have the IDs for 'checkins.user_id'
-   and the like."
+  "Given a SOURCE-TABLE-ID and collection of FK-FIELD-IDS, return a sequence of maps containing IDs and identifiers
+  for those FK fields and their target tables and fields. FK-FIELD-IDS are IDs of fields that belong to the source
+  table. For example, SOURCE-TABLE-ID might be 'checkins' and FK-FIELD-IDS might have the IDs for 'checkins.user_id'
+  and the like."
   [source-table-id fk-field-ids]
   (when (seq fk-field-ids)
     (db/query {:select    [[:source-fk.name      :source-field-name]
@@ -315,7 +325,8 @@
                                                                     :field-name target-field-name})
                               :source-field (i/map->JoinTableField {:field-id   source-field-id
                                                                     :field-name source-field-name})
-                              ;; some DBs like Oracle limit the length of identifiers to 30 characters so only take the first 30 here
+                              ;; some DBs like Oracle limit the length of identifiers to 30 characters so only take
+                              ;; the first 30 here
                               :join-alias  (apply str (take 30 (str target-table-name "__via__" source-field-name)))})))))
 
 (defn- resolve-tables
@@ -338,7 +349,8 @@
         (assoc-in <> [:query :join-tables]  joined-tables)
         (walk/postwalk #(resolve-table % fk-id+table-id->table) <>)))))
 
-;;; # ------------------------------------------------------------ PUBLIC INTERFACE ------------------------------------------------------------
+
+;;; ------------------------------------------------ PUBLIC INTERFACE ------------------------------------------------
 
 (defn resolve
   "Resolve placeholders by fetching `Fields`, `Databases`, and `Tables` that are referred to in EXPANDED-QUERY-DICT."
diff --git a/src/metabase/query_processor/middleware/resolve_driver.clj b/src/metabase/query_processor/middleware/resolve_driver.clj
index 511448e50912ea9575ff1209f64a6c1074153ed6..4abef0d1d78123c74fe85818a109cc8fda35238f 100644
--- a/src/metabase/query_processor/middleware/resolve_driver.clj
+++ b/src/metabase/query_processor/middleware/resolve_driver.clj
@@ -4,7 +4,8 @@
             [metabase.query-processor.interface :as i]))
 
 (defn resolve-driver
-  "Middleware that resolves the driver associated with a query, binds it to `*driver*`, and associates it in the query under the key `:driver`."
+  "Middleware that resolves the driver associated with a query, binds it to `*driver*`, and associates it in the query
+  under the key `:driver`."
   [qp]
   (fn [query]
     (let [driver (or (driver/database-id->driver (:database query))
diff --git a/src/metabase/query_processor/middleware/results_metadata.clj b/src/metabase/query_processor/middleware/results_metadata.clj
index 29805b2c19f57acd59b0c142ee9c19be9c6faba1..839a4ea8ebf7ce00b6a4d555f1702f82cf7add01 100644
--- a/src/metabase/query_processor/middleware/results_metadata.clj
+++ b/src/metabase/query_processor/middleware/results_metadata.clj
@@ -33,7 +33,7 @@
                                       "Valid array of results column metadata maps")
     "value must be an array of valid results column metadata maps."))
 
-(s/defn ^:private ^:always-validate results->column-metadata :- (s/maybe ResultsMetadata)
+(s/defn ^:private results->column-metadata :- (s/maybe ResultsMetadata)
   "Return the desired storage format for the column metadata coming back from RESULTS, or `nil` if no columns were returned."
   [results]
   ;; rarely certain queries will return columns with no names, for example `SELECT COUNT(*)` in SQL Server seems to come back with no name
diff --git a/src/metabase/query_processor/util.clj b/src/metabase/query_processor/util.clj
index 304527c8b2991153b26c81ec1ef879541da647d3..4000b9adf1631aa4bb8c141bfb9886962b78c25e 100644
--- a/src/metabase/query_processor/util.clj
+++ b/src/metabase/query_processor/util.clj
@@ -29,25 +29,29 @@
            (= (:aggregation-type (first aggregations)) :rows))))
 
 (defn query->remark
-  "Genarate an approparite REMARK to be prepended to a query to give DBAs additional information about the query being executed.
-   See documentation for `mbql->native` and [issue #2386](https://github.com/metabase/metabase/issues/2386) for more information."
-  ^String [{{:keys [executed-by query-hash query-type], :as info} :info}]
+  "Generate an approparite REMARK to be prepended to a query to give DBAs additional information about the query being
+  executed. See documentation for `mbql->native` and [issue #2386](https://github.com/metabase/metabase/issues/2386)
+  for more information."  ^String [{{:keys [executed-by query-hash query-type], :as info} :info}]
   (str "Metabase" (when info
                     (assert (instance? (Class/forName "[B") query-hash))
-                    (format ":: userID: %s queryType: %s queryHash: %s" executed-by query-type (codecs/bytes->hex query-hash)))))
+                    (format ":: userID: %s queryType: %s queryHash: %s"
+                            executed-by query-type (codecs/bytes->hex query-hash)))))
 
 
-;;; ------------------------------------------------------------ Normalization ------------------------------------------------------------
+;;; ------------------------------------------------- Normalization --------------------------------------------------
 
-;; The following functions make it easier to deal with MBQL queries, which are case-insensitive, string/keyword insensitive, and underscore/hyphen insensitive.
-;; These should be preferred instead of assuming the frontend will always pass in clauses the same way, since different variation are all legal under MBQL '98.
+;; The following functions make it easier to deal with MBQL queries, which are case-insensitive, string/keyword
+;; insensitive, and underscore/hyphen insensitive.  These should be preferred instead of assuming the frontend will
+;; always pass in clauses the same way, since different variation are all legal under MBQL '98.
 
-;; TODO - In the future it might make sense to simply walk the entire query and normalize the whole thing when it comes in. I've tried implementing middleware
-;; to do that but it ended up breaking a few things that wrongly assume different clauses will always use a certain case (e.g. SQL `:template_tags`). Fixing
-;; all of that is out-of-scope for the nested queries PR but should possibly be revisited in the future.
+;; TODO - In the future it might make sense to simply walk the entire query and normalize the whole thing when it
+;; comes in. I've tried implementing middleware to do that but it ended up breaking a few things that wrongly assume
+;; different clauses will always use a certain case (e.g. SQL `:template_tags`). Fixing all of that is out-of-scope
+;; for the nested queries PR but should possibly be revisited in the future.
 
-(s/defn ^:always-validate normalize-token :- s/Keyword
-  "Convert a string or keyword in various cases (`lisp-case`, `snake_case`, or `SCREAMING_SNAKE_CASE`) to a lisp-cased keyword."
+(s/defn normalize-token :- s/Keyword
+  "Convert a string or keyword in various cases (`lisp-case`, `snake_case`, or `SCREAMING_SNAKE_CASE`) to a lisp-cased
+  keyword."
   [token :- su/KeywordOrString]
   (-> (name token)
       str/lower-case
@@ -100,14 +104,16 @@
         :else                         (recur m                more)))))
 
 
-;;; ------------------------------------------------------------ Hashing ------------------------------------------------------------
+;;; ---------------------------------------------------- Hashing -----------------------------------------------------
 
 (defn- select-keys-for-hashing
   "Return QUERY with only the keys relevant to hashing kept.
-   (This is done so irrelevant info or options that don't affect query results doesn't result in the same query producing different hashes.)"
+  (This is done so irrelevant info or options that don't affect query results doesn't result in the same query
+  producing different hashes.)"
   [query]
   {:pre [(map? query)]}
-  (let [{:keys [constraints parameters], :as query} (select-keys query [:database :type :query :native :parameters :constraints])]
+  (let [{:keys [constraints parameters], :as query} (select-keys query [:database :type :query :native :parameters
+                                                                        :constraints])]
     (cond-> query
       (empty? constraints) (dissoc :constraints)
       (empty? parameters)  (dissoc :parameters))))
diff --git a/src/metabase/routes.clj b/src/metabase/routes.clj
index acf76d72d97c4c32d129346594c585d6fb977e85..594935eefd7dff9d98f870e14550ab2e23adabe4 100644
--- a/src/metabase/routes.clj
+++ b/src/metabase/routes.clj
@@ -1,4 +1,6 @@
 (ns metabase.routes
+  "Main Compojure routes tables. See https://github.com/weavejester/compojure/wiki/Routes-In-Detail for details about
+   how these work. `/api/` routes are in `metabase.api.routes`."
   (:require [cheshire.core :as json]
             [clojure.java.io :as io]
             [clojure.string :as str]
@@ -47,7 +49,9 @@
       (fallback-localization *locale*)))
     (fallback-localization *locale*)))
 
-(defn- entrypoint [entry embeddable? {:keys [uri]}]
+(defn- entrypoint
+  "Repsonse that serves up an entrypoint into the Metabase application, e.g. `index.html`."
+  [entry embeddable? {:keys [uri]}]
   (-> (if (init-status/complete?)
         (load-template (str "frontend_client/" entry ".html")
                        {:bootstrap_json    (escape-script (json/generate-string (public-settings/public-settings)))
@@ -63,16 +67,25 @@
 (def ^:private public (partial entrypoint "public" :embeddable))
 (def ^:private embed  (partial entrypoint "embed"  :embeddable))
 
+(defn- redirect-including-query-string
+  "Like `resp/redirect`, but passes along query string URL params as well. This is important because the public and
+   embedding routes below pass query params (such as template tags) as part of the URL."
+  [url]
+  (fn [{:keys [query-string]}]
+    (resp/redirect (str url "?" query-string))))
+
+;; /public routes. /public/question/:uuid.:export-format redirects to /api/public/card/:uuid/query/:export-format
 (defroutes ^:private public-routes
   (GET ["/question/:uuid.:export-format", :uuid u/uuid-regex, :export-format dataset-api/export-format-regex]
        [uuid export-format]
-       (resp/redirect (format "/api/public/card/%s/query/%s" uuid export-format)))
+       (redirect-including-query-string (format "/api/public/card/%s/query/%s" uuid export-format)))
   (GET "*" [] public))
 
+;; /embed routes. /embed/question/:token.:export-format redirects to /api/public/card/:token/query/:export-format
 (defroutes ^:private embed-routes
   (GET ["/question/:token.:export-format", :export-format dataset-api/export-format-regex]
        [token export-format]
-       (resp/redirect (format "/api/embed/card/%s/query/%s" token export-format)))
+       (redirect-including-query-string (format "/api/embed/card/%s/query/%s" token export-format)))
   (GET "*" [] embed))
 
 ;; Redirect naughty users who try to visit a page other than setup if setup is not yet complete
@@ -86,7 +99,8 @@
                           {:status 503, :body {:status "initializing", :progress (init-status/progress)}}))
   ;; ^/api/ -> All other API routes
   (context "/api" [] (fn [& args]
-                       ;; if Metabase is not finished initializing, return a generic error message rather than something potentially confusing like "DB is not set up"
+                       ;; if Metabase is not finished initializing, return a generic error message rather than
+                       ;; something potentially confusing like "DB is not set up"
                        (if-not (init-status/complete?)
                          {:status 503, :body "Metabase is still initializing. Please sit tight..."}
                          (apply api/routes args))))
diff --git a/src/metabase/sync.clj b/src/metabase/sync.clj
index da9999f3356448a483d6ec135130126e17e4b4c4..30aeec0468fe761b1d54a61ebc3b05deeb941807 100644
--- a/src/metabase/sync.clj
+++ b/src/metabase/sync.clj
@@ -16,7 +16,7 @@
             [schema.core :as s]
             [metabase.sync.util :as sync-util]))
 
-(s/defn ^:always-validate sync-database!
+(s/defn sync-database!
   "Perform all the different sync operations synchronously for DATABASE.
    This is considered a 'full sync' in that all the different sync operations are performed at the same time.
    Please note that this function is *not* what is called by the scheduled tasks. Those call different steps
@@ -32,7 +32,7 @@
     (field-values/update-field-values! database)))
 
 
-(s/defn ^:always-validate sync-table!
+(s/defn sync-table!
   "Perform all the different sync operations synchronously for a given TABLE."
   [table :- i/TableInstance]
   (sync-metadata/sync-table-metadata! table)
diff --git a/src/metabase/sync/analyze.clj b/src/metabase/sync/analyze.clj
index c21a8daf08bf4b7928178c30012670346d960a01..08d5b475daf55fc4c02f78797b9e73d06cb06511 100644
--- a/src/metabase/sync/analyze.clj
+++ b/src/metabase/sync/analyze.clj
@@ -55,7 +55,7 @@
 ;; `last_analyzed` is not `nil`.
 
 
-(s/defn ^:private ^:always-validate update-fields-last-analyzed!
+(s/defn ^:private update-fields-last-analyzed!
   "Update the `last_analyzed` date for all the recently re-fingerprinted/re-classified Fields in TABLE."
   [table :- i/TableInstance]
   ;; The WHERE portion of this query should match up with that of `classify/fields-to-classify`
@@ -65,7 +65,7 @@
     :last_analyzed (u/new-sql-timestamp)))
 
 
-(s/defn ^:always-validate analyze-table!
+(s/defn analyze-table!
   "Perform in-depth analysis for a TABLE."
   [table :- i/TableInstance]
   (table-row-count/update-row-count! table)
@@ -74,7 +74,7 @@
   (update-fields-last-analyzed! table))
 
 
-(s/defn ^:always-validate analyze-db!
+(s/defn analyze-db!
   "Perform in-depth analysis on the data for all Tables in a given DATABASE.
    This is dependent on what each database driver supports, but includes things like cardinality testing and table row
    counting. This also updates the `:last_analyzed` value for each affected Field."
diff --git a/src/metabase/sync/analyze/classifiers/category.clj b/src/metabase/sync/analyze/classifiers/category.clj
index 1c61b63d17ffae4724b011bc764fd6402b3daac8..1cd48a98de143a32f60cb5082ecd0a654cfc9a5a 100644
--- a/src/metabase/sync/analyze/classifiers/category.clj
+++ b/src/metabase/sync/analyze/classifiers/category.clj
@@ -9,12 +9,12 @@
             [schema.core :as s]))
 
 
-(s/defn ^:private ^:always-validate cannot-be-category? :- s/Bool
+(s/defn ^:private cannot-be-category? :- s/Bool
   [base-type :- su/FieldType]
   (or (isa? base-type :type/DateTime)
       (isa? base-type :type/Collection)))
 
-(s/defn ^:always-validate infer-is-category :- (s/maybe i/FieldInstance)
+(s/defn infer-is-category :- (s/maybe i/FieldInstance)
   "Classifier that attempts to determine whether FIELD ought to be marked as a Category based on its distinct count."
   [field :- i/FieldInstance, fingerprint :- (s/maybe i/Fingerprint)]
   (when-not (:special_type field)
diff --git a/src/metabase/sync/analyze/classifiers/name.clj b/src/metabase/sync/analyze/classifiers/name.clj
index 96920577fe1a2dca0f1ff84a38d01c5501bcc3ab..35f28f2fc081971b941213b2418245fc56a8a1b5 100644
--- a/src/metabase/sync/analyze/classifiers/name.clj
+++ b/src/metabase/sync/analyze/classifiers/name.clj
@@ -69,7 +69,7 @@
     (assert (isa? special-type :type/*))))
 
 
-(s/defn ^:private ^:always-validate special-type-for-name-and-base-type :- (s/maybe su/FieldType)
+(s/defn ^:private special-type-for-name-and-base-type :- (s/maybe su/FieldType)
   "If `name` and `base-type` matches a known pattern, return the `special_type` we should assign to it."
   [field-name :- su/NonBlankString, base-type :- su/FieldType]
   (or (when (= "id" (str/lower-case field-name)) :type/PK)
@@ -79,7 +79,7 @@
                 special-type))
             pattern+base-types+special-type)))
 
-(s/defn ^:always-validate infer-special-type :- (s/maybe i/FieldInstance)
+(s/defn infer-special-type :- (s/maybe i/FieldInstance)
   "Classifer that infers the special type of a FIELD based on its name and base type."
   [field :- i/FieldInstance, _ :- (s/maybe i/Fingerprint)]
   (when-let [inferred-special-type (special-type-for-name-and-base-type (:name field) (:base_type field))]
diff --git a/src/metabase/sync/analyze/classifiers/no_preview_display.clj b/src/metabase/sync/analyze/classifiers/no_preview_display.clj
index c83015b040c70361415c062d2a7c4d602587f115..d508aa8cee1f1d0f5004a65af1ebf910179520cc 100644
--- a/src/metabase/sync/analyze/classifiers/no_preview_display.clj
+++ b/src/metabase/sync/analyze/classifiers/no_preview_display.clj
@@ -9,7 +9,7 @@
   "Fields whose values' average length is greater than this amount should be marked as `preview_display = false`."
   50)
 
-(s/defn ^:always-validate infer-no-preview-display :- (s/maybe i/FieldInstance)
+(s/defn infer-no-preview-display :- (s/maybe i/FieldInstance)
   "Classifier that determines whether FIELD should be marked 'No Preview Display'.
    If FIELD is textual and its average length is too great, mark it so it isn't displayed in the UI."
   [field :- i/FieldInstance, fingerprint :- (s/maybe i/Fingerprint)]
diff --git a/src/metabase/sync/analyze/classifiers/text_fingerprint.clj b/src/metabase/sync/analyze/classifiers/text_fingerprint.clj
index a67bee524a17c4fe32c336c908673aff594f96d4..1208f590e823b702f11f59821f8b224ed23be0c6 100644
--- a/src/metabase/sync/analyze/classifiers/text_fingerprint.clj
+++ b/src/metabase/sync/analyze/classifiers/text_fingerprint.clj
@@ -13,7 +13,7 @@
    should be given the corresponding special type (such as `:type/Email`)."
   0.95)
 
-(s/defn ^:private ^:always-validate percent-key-below-threshold? :- s/Bool
+(s/defn ^:private percent-key-below-threshold? :- s/Bool
   "Is the value of PERCENT-KEY inside TEXT-FINGERPRINT above the `percent-valid-threshold`?"
   [text-fingerprint :- i/TextFingerprint, percent-key :- s/Keyword]
   (boolean
@@ -28,7 +28,7 @@
    :percent-url   :type/URL
    :percent-email :type/Email})
 
-(s/defn ^:private ^:always-validate infer-special-type-for-text-fingerprint :- (s/maybe su/FieldType)
+(s/defn ^:private infer-special-type-for-text-fingerprint :- (s/maybe su/FieldType)
   "Check various percentages inside the TEXT-FINGERPRINT and return the corresponding special type to mark the Field as if the percent passes the threshold."
   [text-fingerprint :- i/TextFingerprint]
   (some (fn [[percent-key special-type]]
@@ -37,7 +37,7 @@
         (seq percent-key->special-type)))
 
 
-(s/defn ^:always-validate infer-special-type :- (s/maybe i/FieldInstance)
+(s/defn infer-special-type :- (s/maybe i/FieldInstance)
   "Do classification for `:type/Text` Fields with a valid `TextFingerprint`.
    Currently this only checks the various recorded percentages, but this is subject to change in the future."
   [field :- i/FieldInstance, fingerprint :- (s/maybe i/Fingerprint)]
diff --git a/src/metabase/sync/analyze/classify.clj b/src/metabase/sync/analyze/classify.clj
index 6b7683807e5d932f8056d02211bca4f3312f1a73..5f070504a435798e91d020245c1b9e888fb306c9 100644
--- a/src/metabase/sync/analyze/classify.clj
+++ b/src/metabase/sync/analyze/classify.clj
@@ -39,7 +39,7 @@
   "Columns of Field that classifiers are allowed to set."
   #{:special_type :preview_display})
 
-(s/defn ^:private ^:always-validate save-field-updates!
+(s/defn ^:private save-field-updates!
   "Save the updates in UPDATED-FIELD."
   [original-field :- i/FieldInstance, updated-field :- i/FieldInstance]
   (let [[_ values-to-set] (data/diff original-field updated-field)]
@@ -64,7 +64,7 @@
    no-preview-display/infer-no-preview-display
    text-fingerprint/infer-special-type])
 
-(s/defn ^:private ^:always-validate run-classifiers :- i/FieldInstance
+(s/defn ^:private run-classifiers :- i/FieldInstance
   "Run all the available `classifiers` against FIELD and FINGERPRINT, and return the resulting FIELD with changes
    decided upon by the classifiers."
   [field :- i/FieldInstance, fingerprint :- (s/maybe i/Fingerprint)]
@@ -77,7 +77,7 @@
              more))))
 
 
-(s/defn ^:private ^:always-validate classify!
+(s/defn ^:private classify!
   "Run various classifiers on FIELD and its FINGERPRINT, and save any detected changes."
   ([field :- i/FieldInstance]
    (classify! field (or (:fingerprint field)
@@ -93,7 +93,7 @@
 ;;; |                                        CLASSIFYING ALL FIELDS IN A TABLE                                         |
 ;;; +------------------------------------------------------------------------------------------------------------------+
 
-(s/defn ^:private ^:always-validate fields-to-classify :- (s/maybe [i/FieldInstance])
+(s/defn ^:private fields-to-classify :- (s/maybe [i/FieldInstance])
   "Return a sequences of Fields belonging to TABLE for which we should attempt to determine special type.
    This should include Fields that have the latest fingerprint, but have not yet *completed* analysis."
   [table :- i/TableInstance]
@@ -102,7 +102,7 @@
          :fingerprint_version i/latest-fingerprint-version
          :last_analyzed       nil)))
 
-(s/defn ^:always-validate classify-fields!
+(s/defn classify-fields!
   "Run various classifiers on the appropriate FIELDS in a TABLE that have not been previously analyzed.
    These do things like inferring (and setting) the special types and preview display status for Fields
    belonging to TABLE."
diff --git a/src/metabase/sync/analyze/fingerprint.clj b/src/metabase/sync/analyze/fingerprint.clj
index 359da6bd7829e6d9d85aae2ae55ffbc27b6a5553..d184f38c2a3cc42f2d01ad04e44e8854ced4eff5 100644
--- a/src/metabase/sync/analyze/fingerprint.clj
+++ b/src/metabase/sync/analyze/fingerprint.clj
@@ -18,7 +18,7 @@
             [schema.core :as s]
             [toucan.db :as db]))
 
-(s/defn ^:private ^:always-validate type-specific-fingerprint :- (s/maybe i/TypeSpecificFingerprint)
+(s/defn ^:private type-specific-fingerprint :- (s/maybe i/TypeSpecificFingerprint)
   "Return type-specific fingerprint info for FIELD AND. a FieldSample of Values if it has an elligible base type"
   [field :- i/FieldInstance, values :- i/FieldSample]
   (condp #(isa? %2 %1) (:base_type field)
@@ -26,7 +26,7 @@
     :type/Number {:type/Number (number/number-fingerprint values)}
     nil))
 
-(s/defn ^:private ^:always-validate fingerprint :- i/Fingerprint
+(s/defn ^:private fingerprint :- i/Fingerprint
   "Generate a 'fingerprint' from a FieldSample of VALUES."
   [field :- i/FieldInstance, values :- i/FieldSample]
   (merge
@@ -36,7 +36,7 @@
      {:type type-specific-fingerprint})))
 
 
-(s/defn ^:private ^:always-validate save-fingerprint!
+(s/defn ^:private save-fingerprint!
   [field :- i/FieldInstance, fingerprint :- i/Fingerprint]
   ;; don't bother saving fingerprint if it's completely empty
   (when (seq fingerprint)
@@ -49,7 +49,7 @@
       :fingerprint_version i/latest-fingerprint-version
       :last_analyzed       nil)))
 
-(s/defn ^:private ^:always-validate fingerprint-table!
+(s/defn ^:private fingerprint-table!
   [table :- i/TableInstance, fields :- [i/FieldInstance]]
   (doseq [[field sample] (sample/sample-fields table fields)]
     (when sample
@@ -78,7 +78,7 @@
 ;;        (fingerprint_version < 2 AND
 ;;         base_type IN ("type/Text", "type/SerializedJSON")))
 
-(s/defn ^:private ^:always-validate base-types->descendants :- #{su/FieldTypeKeywordOrString}
+(s/defn ^:private base-types->descendants :- #{su/FieldTypeKeywordOrString}
   "Given a set of BASE-TYPES return an expanded set that includes those base types as well as all of their
    descendants. These types are converted to strings so HoneySQL doesn't confuse them for columns."
   [base-types :- #{su/FieldType}]
@@ -107,7 +107,7 @@
 ;;
 ;; This way we can also completely omit adding clauses for versions that have been "eclipsed" by others.
 ;; This would keep the SQL query from growing boundlessly as new fingerprint versions are added
-(s/defn ^:private ^:always-validate versions-clauses :- [s/Any]
+(s/defn ^:private versions-clauses :- [s/Any]
   []
   ;; keep track of all the base types (including descendants) for each version, starting from most recent
   (let [versions+base-types (reverse (sort-by first (seq i/fingerprint-version->types-that-should-be-re-fingerprinted)))
@@ -124,7 +124,7 @@
          [:< :fingerprint_version version]
          [:in :base_type not-yet-seen]]))))
 
-(s/defn ^:private ^:always-validate honeysql-for-fields-that-need-fingerprint-updating :- {:where s/Any}
+(s/defn ^:private honeysql-for-fields-that-need-fingerprint-updating :- {:where s/Any}
   "Return appropriate WHERE clause for all the Fields whose Fingerprint needs to be re-calculated."
   ([]
    {:where [:and
@@ -141,7 +141,7 @@
 ;;; |                                      FINGERPRINTING ALL FIELDS IN A TABLE                                      |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
-(s/defn ^:private ^:always-validate fields-to-fingerprint :- (s/maybe [i/FieldInstance])
+(s/defn ^:private fields-to-fingerprint :- (s/maybe [i/FieldInstance])
   "Return a sequences of Fields belonging to TABLE for which we should generate (and save) fingerprints.
    This should include NEW fields that are active and visibile."
   [table :- i/TableInstance]
@@ -149,7 +149,7 @@
          (honeysql-for-fields-that-need-fingerprint-updating table))))
 
 ;; TODO - `fingerprint-fields!` and `fingerprint-table!` should probably have their names switched
-(s/defn ^:always-validate fingerprint-fields!
+(s/defn fingerprint-fields!
   "Generate and save fingerprints for all the Fields in TABLE that have not been previously analyzed."
   [table :- i/TableInstance]
   (when-let [fields (fields-to-fingerprint table)]
diff --git a/src/metabase/sync/analyze/fingerprint/global.clj b/src/metabase/sync/analyze/fingerprint/global.clj
index a8fdedd4576a39f1ef48a35bdce650f2436d05b4..24c0ed7bb91b10a0e88992cd2dbbc8f65018bcb3 100644
--- a/src/metabase/sync/analyze/fingerprint/global.clj
+++ b/src/metabase/sync/analyze/fingerprint/global.clj
@@ -3,7 +3,7 @@
   (:require [metabase.sync.interface :as i]
             [schema.core :as s]))
 
-(s/defn ^:always-validate global-fingerprint :- i/GlobalFingerprint
+(s/defn global-fingerprint :- i/GlobalFingerprint
   "Generate a fingerprint of global information for Fields of all types."
   [values :- i/FieldSample]
   ;; TODO - this logic isn't as nice as the old logic that actually called the DB
diff --git a/src/metabase/sync/analyze/fingerprint/number.clj b/src/metabase/sync/analyze/fingerprint/number.clj
index b4bcbd7f73c22ece66fddcf634a685acda1e199f..dc85fa80b5e58184ea84b8dfae02589b73e24152 100644
--- a/src/metabase/sync/analyze/fingerprint/number.clj
+++ b/src/metabase/sync/analyze/fingerprint/number.clj
@@ -4,7 +4,7 @@
             [metabase.sync.interface :as i]
             [schema.core :as s]))
 
-(s/defn ^:always-validate number-fingerprint :- i/NumberFingerprint
+(s/defn number-fingerprint :- i/NumberFingerprint
   "Generate a fingerprint containing information about values that belong to a `:type/Number` Field."
   [values :- i/FieldSample]
   {:min (apply min values)
diff --git a/src/metabase/sync/analyze/fingerprint/sample.clj b/src/metabase/sync/analyze/fingerprint/sample.clj
index 9a62b7a8c5dc7c6f1495efcb73becc76491cec36..0baae1bfc40152dc1a31d80f591956b8c7c9715f 100644
--- a/src/metabase/sync/analyze/fingerprint/sample.clj
+++ b/src/metabase/sync/analyze/fingerprint/sample.clj
@@ -9,13 +9,13 @@
             [metabase.sync.interface :as i]
             [schema.core :as s]))
 
-(s/defn ^:private ^:always-validate basic-sample :- (s/maybe i/TableSample)
+(s/defn ^:private basic-sample :- (s/maybe i/TableSample)
   "Procure a sequence of table rows, up to `max-sample-rows` (10,000 at the time of this writing), for
    use in the fingerprinting sub-stage of analysis. Returns `nil` if no rows are available."
   [table :- i/TableInstance, fields :- [i/FieldInstance]]
   (seq (driver/table-rows-sample table fields)))
 
-(s/defn ^:private ^:always-validate table-sample->field-sample :- (s/maybe i/FieldSample)
+(s/defn ^:private table-sample->field-sample :- (s/maybe i/FieldSample)
   "Fetch a sample for the Field whose values are at INDEX in the TABLE-SAMPLE.
    Filters out `nil` values; returns `nil` if a non-empty sample cannot be obtained."
   [table-sample :- i/TableSample, i :- s/Int]
@@ -24,7 +24,7 @@
        (filter (complement nil?))
        seq))
 
-(s/defn ^:always-validate sample-fields :- [(s/pair i/FieldInstance "Field", (s/maybe i/FieldSample) "FieldSample")]
+(s/defn sample-fields :- [(s/pair i/FieldInstance "Field", (s/maybe i/FieldSample) "FieldSample")]
   "Fetch samples for a series of FIELDS. Returns tuples of Field and sample.
    This may return `nil` if the sample could not be fetched for some other reason."
   [table :- i/TableInstance, fields :- [i/FieldInstance]]
diff --git a/src/metabase/sync/analyze/fingerprint/text.clj b/src/metabase/sync/analyze/fingerprint/text.clj
index f825073fe7c1f2f05fe2ae06fea1c6df3323c0e1..20c995217a1bc0c2a35cd4e102e58cf1ca976916 100644
--- a/src/metabase/sync/analyze/fingerprint/text.clj
+++ b/src/metabase/sync/analyze/fingerprint/text.clj
@@ -5,7 +5,7 @@
             [metabase.util :as u]
             [schema.core :as s]))
 
-(s/defn ^:private ^:always-validate average-length :- (s/constrained Double #(>= % 0))
+(s/defn ^:private average-length :- (s/constrained Double #(>= % 0))
   "Return the average length of VALUES."
   [values :- i/FieldSample]
   (let [total-length (reduce + (for [value values]
@@ -13,7 +13,7 @@
     (/ (double total-length)
        (double (count values)))))
 
-(s/defn ^:private ^:always-validate percent-satisfying-predicate :- i/Percent
+(s/defn ^:private percent-satisfying-predicate :- i/Percent
   "Return the percentage of VALUES that satisfy PRED."
   [pred :- (s/pred fn?), values :- i/FieldSample]
   (let [total-count    (count values)
@@ -30,7 +30,7 @@
      (or (map? parsed-json)
          (sequential? parsed-json)))))
 
-(s/defn ^:always-validate text-fingerprint :- i/TextFingerprint
+(s/defn text-fingerprint :- i/TextFingerprint
   "Generate a fingerprint containing information about values that belong to a `:type/Text` Field."
   [values :- i/FieldSample]
   {:percent-json   (percent-satisfying-predicate valid-serialized-json? values)
diff --git a/src/metabase/sync/analyze/table_row_count.clj b/src/metabase/sync/analyze/table_row_count.clj
index a4aa5021f3ea3c79800914241708af3babc1371f..8efc35e9fb962b6808a351f630bc921cdee0aa00 100644
--- a/src/metabase/sync/analyze/table_row_count.clj
+++ b/src/metabase/sync/analyze/table_row_count.clj
@@ -10,13 +10,13 @@
             [schema.core :as s]
             [toucan.db :as db]))
 
-(s/defn ^:private ^:always-validate table-row-count :- (s/maybe s/Int)
+(s/defn ^:private table-row-count :- (s/maybe s/Int)
   "Determine the count of rows in TABLE by running a simple structured MBQL query."
   [table :- i/TableInstance]
   (sync-util/with-error-handling (format "Unable to determine row count for %s" (sync-util/name-for-logging table))
     (queries/table-row-count table)))
 
-(s/defn ^:always-validate update-row-count!
+(s/defn update-row-count!
   "Update the cached row count (`rows`) for a single TABLE."
   [table :- i/TableInstance]
   (sync-util/with-error-handling (format "Error setting table row count for %s" (sync-util/name-for-logging table))
diff --git a/src/metabase/sync/fetch_metadata.clj b/src/metabase/sync/fetch_metadata.clj
index 508599383cffbdab5d497ad519e4f74c0e20e3e7..6f26647f47d633d48425afc066dd0e768be1ce23 100644
--- a/src/metabase/sync/fetch_metadata.clj
+++ b/src/metabase/sync/fetch_metadata.clj
@@ -7,24 +7,24 @@
             [schema.core :as s])
   (:import [org.joda.time DateTime]))
 
-(s/defn ^:always-validate db-metadata :- i/DatabaseMetadata
+(s/defn db-metadata :- i/DatabaseMetadata
   "Get basic Metadata about a DATABASE and its Tables. Doesn't include information about the Fields."
   [database :- i/DatabaseInstance]
   (driver/describe-database (driver/->driver database) database))
 
-(s/defn ^:always-validate table-metadata :- i/TableMetadata
+(s/defn table-metadata :- i/TableMetadata
   "Get more detailed information about a TABLE belonging to DATABASE. Includes information about the Fields."
   [database :- i/DatabaseInstance, table :- i/TableInstance]
   (driver/describe-table (driver/->driver database) database table))
 
-(s/defn ^:always-validate fk-metadata :- i/FKMetadata
+(s/defn fk-metadata :- i/FKMetadata
   "Get information about the foreign keys belonging to TABLE."
   [database :- i/DatabaseInstance, table :- i/TableInstance]
   (let [driver (driver/->driver database)]
     (when (driver/driver-supports? driver :foreign-keys)
       (driver/describe-table-fks driver database table))))
 
-(s/defn ^:always-validate db-timezone :- i/TimeZoneId
+(s/defn db-timezone :- i/TimeZoneId
   [database :- i/DatabaseInstance]
   (let [db-time  ^DateTime (driver/current-db-time (driver/->driver database) database)]
     (-> db-time .getChronology .getZone .getID)))
diff --git a/src/metabase/sync/field_values.clj b/src/metabase/sync/field_values.clj
index 234b28e6347727b0c0ae9bfdff2b37165c9ce8ff..9bc8dffc7a3001acb56f7c2ca4346dd73c35cdc1 100644
--- a/src/metabase/sync/field_values.clj
+++ b/src/metabase/sync/field_values.clj
@@ -11,19 +11,19 @@
             [schema.core :as s]
             [toucan.db :as db]))
 
-(s/defn ^:private ^:always-validate clear-field-values-for-field! [field :- i/FieldInstance]
+(s/defn ^:private clear-field-values-for-field! [field :- i/FieldInstance]
   (when (db/exists? FieldValues :field_id (u/get-id field))
     (log/debug (format "Based on type info, %s should no longer have field values.\n" (sync-util/name-for-logging field))
                (format "(base type: %s, special type: %s, visibility type: %s)\n" (:base_type field) (:special_type field) (:visibility_type field))
                "Deleting FieldValues...")
     (db/delete! FieldValues :field_id (u/get-id field))))
 
-(s/defn ^:private ^:always-validate update-field-values-for-field! [field :- i/FieldInstance]
+(s/defn ^:private update-field-values-for-field! [field :- i/FieldInstance]
   (log/debug (u/format-color 'green "Looking into updating FieldValues for %s" (sync-util/name-for-logging field)))
   (field-values/create-or-update-field-values! field))
 
 
-(s/defn ^:always-validate update-field-values-for-table!
+(s/defn update-field-values-for-table!
   "Update the cached FieldValues for all Fields (as needed) for TABLE."
   [table :- i/TableInstance]
   (doseq [field (db/select Field :table_id (u/get-id table), :active true, :visibility_type "normal")]
@@ -33,7 +33,7 @@
         (clear-field-values-for-field! field)))))
 
 
-(s/defn ^:always-validate update-field-values!
+(s/defn update-field-values!
   "Update the cached FieldValues (distinct values for categories and certain other fields that are shown
    in widgets like filters) for the Tables in DATABASE (as needed)."
   [database :- i/DatabaseInstance]
diff --git a/src/metabase/sync/sync_metadata.clj b/src/metabase/sync/sync_metadata.clj
index 96249239041ee5a4ec46af15e6e986de11e1aa5f..e5a32f6d47d1c59fb3e08568126a4ba21aa4746a 100644
--- a/src/metabase/sync/sync_metadata.clj
+++ b/src/metabase/sync/sync_metadata.clj
@@ -17,7 +17,7 @@
              [tables :as sync-tables]]
             [schema.core :as s]))
 
-(s/defn ^:always-validate sync-db-metadata!
+(s/defn sync-db-metadata!
   "Sync the metadata for a Metabase DATABASE. This makes sure child Table & Field objects are synchronized."
   [database :- i/DatabaseInstance]
   (sync-util/sync-operation :sync-metadata database (format "Sync metadata for %s" (sync-util/name-for-logging database))
diff --git a/src/metabase/sync/sync_metadata/fields.clj b/src/metabase/sync/sync_metadata/fields.clj
index 10e9651145db17824b24874f489c76170e9780f6..0ba6e55f279a6ce0d868d0a24308f40f07e161c6 100644
--- a/src/metabase/sync/sync_metadata/fields.clj
+++ b/src/metabase/sync/sync_metadata/fields.clj
@@ -41,7 +41,7 @@
 ;;; |                                             CREATING / REACTIVATING FIELDS                                             |
 ;;; +------------------------------------------------------------------------------------------------------------------------+
 
-(s/defn ^:private ^:always-validate matching-inactive-field :- (s/maybe i/FieldInstance)
+(s/defn ^:private matching-inactive-field :- (s/maybe i/FieldInstance)
   "Return an inactive metabase Field that matches NEW-FIELD-METADATA, if any such Field existis."
   [table :- i/TableInstance, new-field-metadata :- i/TableMetadataField, parent-id :- ParentID]
   (db/select-one Field
@@ -50,7 +50,7 @@
     :parent_id   parent-id
     :active     false))
 
-(s/defn ^:private ^:always-validate ->metabase-field! :- i/FieldInstance
+(s/defn ^:private ->metabase-field! :- i/FieldInstance
   "Return an active Metabase Field instance that matches NEW-FIELD-METADATA. This object will be created or reactivated as a side effect of calling this function."
   [table :- i/TableInstance, new-field-metadata :- i/TableMetadataField, parent-id :- ParentID]
   (if-let [matching-inactive-field (matching-inactive-field table new-field-metadata parent-id)]
@@ -71,7 +71,7 @@
         :parent_id    parent-id))))
 
 
-(s/defn ^:private ^:always-validate create-or-reactivate-field!
+(s/defn ^:private create-or-reactivate-field!
   "Create (or reactivate) a Metabase Field object(s) for NEW-FIELD-METABASE and any nested fields."
   [table :- i/TableInstance, new-field-metadata :- i/TableMetadataField, parent-id :- ParentID]
   ;; Create (or reactivate) the Metabase Field entry for NEW-FIELD-METADATA...
@@ -85,7 +85,7 @@
 ;;; |                                               "RETIRING" INACTIVE FIELDS                                               |
 ;;; +------------------------------------------------------------------------------------------------------------------------+
 
-(s/defn ^:private ^:always-validate retire-field!
+(s/defn ^:private retire-field!
   "Mark an OLD-FIELD belonging to TABLE as inactive if corresponding Field object exists."
   [table :- i/TableInstance, old-field :- TableMetadataFieldWithID]
   (log/info (format "Marking %s Field '%s' as inactive." (sync-util/name-for-logging table) (:name old-field)))
@@ -100,7 +100,7 @@
 ;;; |                               SYNCING FIELDS IN DB (CREATING, REACTIVATING, OR RETIRING)                               |
 ;;; +------------------------------------------------------------------------------------------------------------------------+
 
-(s/defn ^:private ^:always-validate matching-field-metadata :- (s/maybe TableMetadataFieldWithOptionalID)
+(s/defn ^:private matching-field-metadata :- (s/maybe TableMetadataFieldWithOptionalID)
   "Find Metadata that matches FIELD-METADATA from a set of OTHER-METADATA, if any exists."
   [field-metadata :- TableMetadataFieldWithOptionalID, other-metadata :- #{TableMetadataFieldWithOptionalID}]
   (some (fn [other-field-metadata]
@@ -109,7 +109,7 @@
               other-field-metadata))
         other-metadata))
 
-(s/defn ^:private ^:always-validate sync-field-instances!
+(s/defn ^:private sync-field-instances!
   "Make sure the instances of Metabase Field are in-sync with the DB-METADATA."
   [table :- i/TableInstance, db-metadata :- #{i/TableMetadataField}, our-metadata :- #{TableMetadataFieldWithID}, parent-id :- ParentID]
   ;; Loop thru fields in DB-METADATA. Create/reactivate any fields that don't exist in OUR-METADATA.
@@ -136,7 +136,7 @@
 ;;; |                                                UPDATING FIELD METADATA                                                 |
 ;;; +------------------------------------------------------------------------------------------------------------------------+
 
-(s/defn ^:private ^:always-validate update-metadata!
+(s/defn ^:private update-metadata!
   "Make sure things like PK status and base-type are in sync with what has come back from the DB."
   [table :- i/TableInstance, db-metadata :- #{i/TableMetadataField}, parent-id :- ParentID]
   (let [existing-fields      (db/select [Field :base_type :special_type :name :id]
@@ -162,7 +162,7 @@
 ;;; |                                             FETCHING OUR CURRENT METADATA                                              |
 ;;; +------------------------------------------------------------------------------------------------------------------------+
 
-(s/defn ^:private ^:always-validate add-nested-fields :- TableMetadataFieldWithID
+(s/defn ^:private add-nested-fields :- TableMetadataFieldWithID
   "Recursively add entries for any nested-fields to FIELD."
   [field-metadata :- TableMetadataFieldWithID, parent-id->fields :- {ParentID #{TableMetadataFieldWithID}}]
   (let [nested-fields (get parent-id->fields (u/get-id field-metadata))]
@@ -171,7 +171,7 @@
       (assoc field-metadata :nested-fields (set (for [nested-field nested-fields]
                                                   (add-nested-fields nested-field parent-id->fields)))))))
 
-(s/defn ^:private ^:always-validate parent-id->fields :- {ParentID #{TableMetadataFieldWithID}}
+(s/defn ^:private parent-id->fields :- {ParentID #{TableMetadataFieldWithID}}
   "Build a map of the Metabase Fields we have for TABLE, keyed by their parent id (usually `nil`)."
   [table :- i/TableInstance]
   (->> (for [field (db/select [Field :name :base_type :special_type :parent_id :id]
@@ -190,7 +190,7 @@
                      (set (for [field fields]
                             (dissoc field :parent-id)))))))
 
-(s/defn ^:private ^:always-validate our-metadata :- #{TableMetadataFieldWithID}
+(s/defn ^:private our-metadata :- #{TableMetadataFieldWithID}
   "Return information we have about Fields for a TABLE currently in the application database
    in (almost) exactly the same `TableMetadataField` format returned by `describe-table`."
   [table :- i/TableInstance]
@@ -205,7 +205,7 @@
 ;;; |                                          FETCHING METADATA FROM CONNECTED DB                                           |
 ;;; +------------------------------------------------------------------------------------------------------------------------+
 
-(s/defn ^:private ^:always-validate db-metadata :- #{i/TableMetadataField}
+(s/defn ^:private db-metadata :- #{i/TableMetadataField}
   "Fetch metadata about Fields belonging to a given TABLE directly from an external database by calling its
    driver's implementation of `describe-table`."
   [database :- i/DatabaseInstance, table :- i/TableInstance]
@@ -216,7 +216,7 @@
 ;;; |                                                PUTTING IT ALL TOGETHER                                                 |
 ;;; +------------------------------------------------------------------------------------------------------------------------+
 
-(s/defn ^:always-validate sync-fields-for-table!
+(s/defn sync-fields-for-table!
   "Sync the Fields in the Metabase application database for a specific TABLE."
   ([table :- i/TableInstance]
    (sync-fields-for-table! (table/database table) table))
@@ -229,7 +229,7 @@
        (update-metadata! table db-metadata nil)))))
 
 
-(s/defn ^:always-validate sync-fields!
+(s/defn sync-fields!
   "Sync the Fields in the Metabase application database for all the Tables in a DATABASE."
   [database :- i/DatabaseInstance]
   (doseq [table (sync-util/db->sync-tables database)]
diff --git a/src/metabase/sync/sync_metadata/fks.clj b/src/metabase/sync/sync_metadata/fks.clj
index 5cee048f9421655a1b4d634f0ca7ca3f24d51162..54b61dd58c418043d0403d2b024051dc587a7e1a 100644
--- a/src/metabase/sync/sync_metadata/fks.clj
+++ b/src/metabase/sync/sync_metadata/fks.clj
@@ -19,7 +19,7 @@
    :dest-table   i/TableInstance
    :dest-field   i/FieldInstance})
 
-(s/defn ^:private ^:always-validate fetch-fk-relationship-objects :- (s/maybe FKRelationshipObjects)
+(s/defn ^:private fetch-fk-relationship-objects :- (s/maybe FKRelationshipObjects)
   "Fetch the Metabase objects (Tables and Fields) that are relevant to a foreign key relationship described by FK."
   [database :- i/DatabaseInstance, table :- i/TableInstance, fk :- i/FKMetadataEntry]
   (when-let [source-field (db/select-one Field
@@ -45,7 +45,7 @@
          :dest-field   dest-field}))))
 
 
-(s/defn ^:private ^:always-validate mark-fk!
+(s/defn ^:private mark-fk!
   [database :- i/DatabaseInstance, table :- i/TableInstance, fk :- i/FKMetadataEntry]
   (when-let [{:keys [source-field dest-table dest-field]} (fetch-fk-relationship-objects database table fk)]
     (log/info (u/format-color 'cyan "Marking foreign key from %s %s -> %s %s"
@@ -58,7 +58,7 @@
       :fk_target_field_id (u/get-id dest-field))))
 
 
-(s/defn ^:always-validate sync-fks-for-table!
+(s/defn sync-fks-for-table!
   "Sync the foreign keys for a specific TABLE."
   ([table :- i/TableInstance]
    (sync-fks-for-table! (table/database table) table))
@@ -67,7 +67,7 @@
      (doseq [fk (fetch-metadata/fk-metadata database table)]
        (mark-fk! database table fk)))))
 
-(s/defn ^:always-validate sync-fks!
+(s/defn sync-fks!
   "Sync the foreign keys in a DATABASE. This sets appropriate values for relevant Fields in the Metabase application DB
    based on values from the `FKMetadata` returned by `describe-table-fks`."
   [database :- i/DatabaseInstance]
diff --git a/src/metabase/sync/sync_metadata/metabase_metadata.clj b/src/metabase/sync/sync_metadata/metabase_metadata.clj
index 58e0513abe355da8cde26acbdf68643bfc1ccf39..5e0972082fd4f9554067ee62d70318a70ce78433 100644
--- a/src/metabase/sync/sync_metadata/metabase_metadata.clj
+++ b/src/metabase/sync/sync_metadata/metabase_metadata.clj
@@ -28,7 +28,7 @@
    :field-name (s/maybe su/NonBlankString)
    :k          s/Keyword})
 
-(s/defn ^:private ^:always-validate parse-keypath :- KeypathComponents
+(s/defn ^:private parse-keypath :- KeypathComponents
   "Parse a KEYPATH into components for easy use."
   ;; TODO: this does not support schemas in dbs :(
   [keypath :- su/NonBlankString]
@@ -40,7 +40,7 @@
      :field-name (when third-part second-part)
      :k          (keyword (or third-part second-part))}))
 
-(s/defn ^:private ^:always-validate set-property! :- s/Bool
+(s/defn ^:private set-property! :- s/Bool
   "Set a property for a Field or Table in DATABASE. Returns `true` if a property was successfully set."
   [database :- i/DatabaseInstance, {:keys [table-name field-name k]} :- KeypathComponents, value]
   (boolean
@@ -58,7 +58,7 @@
          (db/update! Table table-id
            k value))))))
 
-(s/defn ^:private ^:always-validate sync-metabase-metadata-table!
+(s/defn ^:private sync-metabase-metadata-table!
   "Databases may include a table named `_metabase_metadata` (case-insentive) which includes descriptions or other metadata about the `Tables` and `Fields`
    it contains. This table is *not* synced normally, i.e. a Metabase `Table` is not created for it. Instead, *this* function is called, which reads the data it
    contains and updates the relevant Metabase objects.
@@ -80,12 +80,12 @@
           (log/error (u/format-color 'red "Error syncing _metabase_metadata: no matching keypath: %s" keypath))))))
 
 
-(s/defn ^:always-validate is-metabase-metadata-table? :- s/Bool
+(s/defn is-metabase-metadata-table? :- s/Bool
   "Is this TABLE the special `_metabase_metadata` table?"
   [table :- i/DatabaseMetadataTable]
   (= "_metabase_metadata" (str/lower-case (:name table))))
 
-(s/defn ^:always-validate sync-metabase-metadata!
+(s/defn sync-metabase-metadata!
   "Sync the `_metabase_metadata` table, a special table with Metabase metadata, if present.
    This table contains information about type information, descriptions, and other properties that
    should be set for Metabase objects like Tables and Fields."
diff --git a/src/metabase/sync/sync_metadata/tables.clj b/src/metabase/sync/sync_metadata/tables.clj
index 520f417ff921437b47664f53b5f85428d8273d7c..d60b9e6ef7d0488526c09e34db8b770ff0024329 100644
--- a/src/metabase/sync/sync_metadata/tables.clj
+++ b/src/metabase/sync/sync_metadata/tables.clj
@@ -72,7 +72,7 @@
     ;; Lobos
     #"^lobos_migrations$"})
 
-(s/defn ^:private ^:always-validate is-crufty-table? :- s/Bool
+(s/defn ^:private is-crufty-table? :- s/Bool
   "Should we give newly created TABLE a `visibility_type` of `:cruft`?"
   [table :- i/DatabaseMetadataTable]
   (boolean (some #(re-find % (str/lower-case (:name table))) crufty-table-patterns)))
@@ -82,7 +82,7 @@
 
 ;; TODO - should we make this logic case-insensitive like it is for fields?
 
-(s/defn ^:private ^:always-validate create-or-reactivate-tables!
+(s/defn ^:private create-or-reactivate-tables!
   "Create NEW-TABLES for database, or if they already exist, mark them as active."
   [database :- i/DatabaseInstance, new-tables :- #{i/DatabaseMetadataTable}]
   (log/info "Found new tables:"
@@ -108,7 +108,7 @@
                            :cruft)))))
 
 
-(s/defn ^:private ^:always-validate retire-tables!
+(s/defn ^:private retire-tables!
   "Mark any OLD-TABLES belonging to DATABASE as inactive."
   [database :- i/DatabaseInstance, old-tables :- #{i/DatabaseMetadataTable}]
   (log/info "Marking tables as inactive:"
@@ -121,14 +121,14 @@
       :active false)))
 
 
-(s/defn ^:private ^:always-validate db-metadata :- #{i/DatabaseMetadataTable}
+(s/defn ^:private db-metadata :- #{i/DatabaseMetadataTable}
   "Return information about DATABASE by calling its driver's implementation of `describe-database`."
   [database :- i/DatabaseInstance]
   (set (for [table (:tables (fetch-metadata/db-metadata database))
              :when (not (metabase-metadata/is-metabase-metadata-table? table))]
          table)))
 
-(s/defn ^:private ^:always-validate our-metadata :- #{i/DatabaseMetadataTable}
+(s/defn ^:private our-metadata :- #{i/DatabaseMetadataTable}
   "Return information about what Tables we have for this DB in the Metabase application DB."
   [database :- i/DatabaseInstance]
   (set (map (partial into {})
@@ -136,7 +136,7 @@
               :db_id  (u/get-id database)
               :active true))))
 
-(s/defn ^:always-validate sync-tables!
+(s/defn sync-tables!
   "Sync the Tables recorded in the Metabase application database with the ones obtained by calling DATABASE's driver's implementation of `describe-database`."
   [database :- i/DatabaseInstance]
   ;; determine what's changed between what info we have and what's in the DB
diff --git a/src/metabase/task.clj b/src/metabase/task.clj
index 3cbe37d1e50d64129e45a272004da5be1f44c2e1..6d77ce8d0ab4fe27fcb858d1112869f2be8c579e 100644
--- a/src/metabase/task.clj
+++ b/src/metabase/task.clj
@@ -72,13 +72,13 @@
 ;;; |                                               SCHEDULING/DELETING TASKS                                                |
 ;;; +------------------------------------------------------------------------------------------------------------------------+
 
-(s/defn ^:always-validate schedule-task!
+(s/defn schedule-task!
   "Add a given job and trigger to our scheduler."
   [job :- JobDetail, trigger :- Trigger]
   (when-let [scheduler (scheduler)]
     (qs/schedule scheduler job trigger)))
 
-(s/defn ^:always-validate delete-task!
+(s/defn delete-task!
   "delete a task from the scheduler"
   [job-key :- JobKey, trigger-key :- TriggerKey]
   (when-let [scheduler (scheduler)]
diff --git a/src/metabase/task/sync_databases.clj b/src/metabase/task/sync_databases.clj
index f7ee16e67a65f58abe94f4147387e10c23bbb2ef..98b3b0851316d7e5ab1cb944c3112933ecb409a9 100644
--- a/src/metabase/task/sync_databases.clj
+++ b/src/metabase/task/sync_databases.clj
@@ -24,7 +24,7 @@
 ;;; |                                                       JOB LOGIC                                                        |
 ;;; +------------------------------------------------------------------------------------------------------------------------+
 
-(s/defn ^:private ^:always-validate job-context->database :- DatabaseInstance
+(s/defn ^:private job-context->database :- DatabaseInstance
   "Get the Database referred to in JOB-CONTEXT. Guaranteed to return a valid Database."
   [job-context]
   (Database (u/get-id (get (qc/from-job-data job-context) "db-id"))))
@@ -67,27 +67,27 @@
 
 ;; These getter functions are not strictly neccesary but are provided primarily so we can get some extra validation by using them
 
-(s/defn ^:private ^:always-validate job-key :- JobKey
+(s/defn ^:private job-key :- JobKey
   "Return an appropriate string key for the job described by TASK-INFO for DATABASE-OR-ID."
   [database :- DatabaseInstance, task-info :- TaskInfo]
   (jobs/key (format "metabase.task.%s.job.%d" (name (:key task-info)) (u/get-id database))))
 
-(s/defn ^:private ^:always-validate trigger-key :- TriggerKey
+(s/defn ^:private trigger-key :- TriggerKey
   "Return an appropriate string key for the trigger for TASK-INFO and DATABASE-OR-ID."
   [database :- DatabaseInstance, task-info :- TaskInfo]
   (triggers/key (format "metabase.task.%s.trigger.%d" (name (:key task-info)) (u/get-id database))))
 
-(s/defn ^:private ^:always-validate cron-schedule :- cron-util/CronScheduleString
+(s/defn ^:private cron-schedule :- cron-util/CronScheduleString
   "Fetch the appropriate cron schedule string for DATABASE and TASK-INFO."
   [database :- DatabaseInstance, task-info :- TaskInfo]
   (get database (:db-schedule-column task-info)))
 
-(s/defn ^:private ^:always-validate job-class :- Class
+(s/defn ^:private job-class :- Class
   "Get the Job class for TASK-INFO."
   [task-info :- TaskInfo]
   (:job-class task-info))
 
-(s/defn ^:private ^:always-validate description :- s/Str
+(s/defn ^:private description :- s/Str
   "Return an appropriate description string for a job/trigger for Database described by TASK-INFO."
   [database :- DatabaseInstance, task-info :- TaskInfo]
   (format "%s Database %d" (name (:key task-info)) (u/get-id database)))
@@ -97,7 +97,7 @@
 ;;; |                                                DELETING TASKS FOR A DB                                                 |
 ;;; +------------------------------------------------------------------------------------------------------------------------+
 
-(s/defn ^:private ^:always-validate delete-task!
+(s/defn ^:private delete-task!
   "Cancel a single sync job for DATABASE-OR-ID and TASK-INFO."
   [database :- DatabaseInstance, task-info :- TaskInfo]
   (let [job-key     (job-key database task-info)
@@ -105,7 +105,7 @@
     (log/debug (u/format-color 'red "Unscheduling task for Database %d: job: %s; trigger: %s" (u/get-id database) (.getName job-key) (.getName trigger-key)))
     (task/delete-task! job-key trigger-key)))
 
-(s/defn ^:always-validate unschedule-tasks-for-db!
+(s/defn unschedule-tasks-for-db!
   "Cancel *all* scheduled sync and FieldValues caching tassks for DATABASE-OR-ID."
   [database :- DatabaseInstance]
   (doseq [task-info task-infos]
@@ -116,7 +116,7 @@
 ;;; |                                             (RE)SCHEDULING TASKS FOR A DB                                              |
 ;;; +------------------------------------------------------------------------------------------------------------------------+
 
-(s/defn ^:private ^:always-validate job :- JobDetail
+(s/defn ^:private job :- JobDetail
   "Build a Quartz Job for DATABASE and TASK-INFO."
   [database :- DatabaseInstance, task-info :- TaskInfo]
   (jobs/build
@@ -125,7 +125,7 @@
    (jobs/using-job-data {"db-id" (u/get-id database)})
    (jobs/with-identity (job-key database task-info))))
 
-(s/defn ^:private ^:always-validate trigger :- CronTrigger
+(s/defn ^:private trigger :- CronTrigger
   "Build a Quartz Trigger for DATABASE and TASK-INFO."
   [database :- DatabaseInstance, task-info :- TaskInfo]
   (triggers/build
@@ -139,7 +139,7 @@
       (cron/with-misfire-handling-instruction-do-nothing)))))
 
 
-(s/defn ^:private ^:always-validate schedule-task-for-db!
+(s/defn ^:private schedule-task-for-db!
   "Schedule a new Quartz job for DATABASE and TASK-INFO."
   [database :- DatabaseInstance, task-info :- TaskInfo]
   (let [job     (job database task-info)
@@ -148,7 +148,7 @@
     (task/schedule-task! job trigger)))
 
 
-(s/defn ^:always-validate schedule-tasks-for-db!
+(s/defn schedule-tasks-for-db!
   "Schedule all the different sync jobs we have for DATABASE.
    Unschedules any existing jobs."
   [database :- DatabaseInstance]
diff --git a/src/metabase/util/cron.clj b/src/metabase/util/cron.clj
index 70fd20cc0537d02712e06a47faf8caeb095f4cbe..e30f5f07db176c0300b426efbd9f80edd925d72f 100644
--- a/src/metabase/util/cron.clj
+++ b/src/metabase/util/cron.clj
@@ -42,7 +42,7 @@
 ;;; |                                          SCHEDULE MAP -> CRON STRING                                           |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
-(s/defn ^:private ^:always-validate cron-string :- CronScheduleString
+(s/defn ^:private cron-string :- CronScheduleString
   "Build a cron string from key-value pair parts."
   {:style/indent 0}
   [{:keys [seconds minutes hours day-of-month month day-of-week year]}]
@@ -77,7 +77,7 @@
                       "mid"   "15"
                       "last"  "L"))))
 
-(s/defn ^:always-validate ^{:style/indent 0} schedule-map->cron-string :- CronScheduleString
+(s/defn ^{:style/indent 0} schedule-map->cron-string :- CronScheduleString
   "Convert the frontend schedule map into a cron string."
   [{day-of-week :schedule_day, frame :schedule_frame, hour :schedule_hour, schedule-type :schedule_type} :- ScheduleMap]
   (cron-string (case (keyword schedule-type)
@@ -132,7 +132,7 @@
     :else                                         "hourly"))
 
 
-(s/defn ^:always-validate ^{:style/indent 0} cron-string->schedule-map :- ScheduleMap
+(s/defn ^{:style/indent 0} cron-string->schedule-map :- ScheduleMap
   "Convert a normal CRON-STRING into the expanded ScheduleMap format used by the frontend."
   [cron-string :- CronScheduleString]
   (let [[_ _ hours day-of-month _ day-of-week _] (str/split cron-string #"\s+")]
diff --git a/src/metabase/util/infer_spaces.clj b/src/metabase/util/infer_spaces.clj
index 9a1126fadce51a9907737788fbb4ea5ef607eb39..ea4fb66088885ce890a54dd99355c13d203fe77b 100644
--- a/src/metabase/util/infer_spaces.clj
+++ b/src/metabase/util/infer_spaces.clj
@@ -4,22 +4,54 @@
   ;; TODO - The code in this namespace is very hard to understand. We should clean it up and make it readable.
   (:require [clojure.java.io :as io]
             [clojure.string :as s])
-  (:import java.lang.Math))
+  (:import java.lang.Math
+           java.util.Arrays))
 
 
 (def ^:const ^:private special-words ["checkins"])
 
-;; # Build a cost dictionary, assuming Zipf's law and cost = -math.log(probability).
-(def ^:private words (concat special-words (s/split-lines (slurp (io/resource "words-by-frequency.txt")))))
+(defn- slurp-words-by-frequency []
+  (concat special-words (s/split-lines (slurp (io/resource "words-by-frequency.txt")))))
 
 ;; wordcost = dict((k, log((i+1)*log(len(words)))) for i,k in enumerate(words))
-(def ^:private word-cost
-  (into {} (map-indexed (fn [idx word]
-                          [word (Math/log (* (inc idx) (Math/log (count words))))])
-                        words)))
+(defn- make-cost-map
+  "Creates a map keyed by the hash of the word and the cost as the value. The map is sorted by the hash value"
+  [words]
+  (let [log-count (Math/log (count words))]
+    (into (sorted-map)
+          (map-indexed (fn [idx word]
+                         [(hash word) (Math/log (* (inc idx) log-count))])
+                       words))))
 
-;; maxword = max(len(x) for x in words)
-(def ^:private max-word (apply max (map count words)))
+;; # Build arrays for a cost lookup, assuming Zipf's law and cost = -math.log(probability).
+;;
+;; This is structured as a let for efficiency reasons. It's reading in 120k strings and putting them into two
+;; correlated data structures. We want to ensure that those strings are garbage collected after we setup the
+;; structures and we don't want to read the file in twice
+(let [all-words (slurp-words-by-frequency)
+      sorted-words (make-cost-map all-words)]
+
+  (def ^:private ^"[I" word-hashes
+    "Array of word hash values, ordered by that hash value"
+    (int-array (keys sorted-words)))
+
+  (def ^:private ^"[D" word-cost
+    "Array of word cost doubles, ordered by the hash value for that word"
+    (double-array (vals sorted-words)))
+
+  ;; maxword = max(len(x) for x in words)
+  (def ^:private max-word
+    "Length of the longest word in the word list"
+    (apply max (map count all-words))))
+
+(defn- get-word-cost
+  "Finds `S` in the word list. If found, returns the cost, otherwise returns `DEFAULT` like clojure.core's `get`"
+  [s default]
+  (let [idx (Arrays/binarySearch word-hashes (int (hash s)))]
+    ;; binarySearch returns a negative number if not found
+    (if (< idx 0)
+      default
+      (aget word-cost idx))))
 
 ;; def infer_spaces(s):
 ;;     """Uses dynamic programming to infer the location of spaces in a string
@@ -34,7 +66,7 @@
 (defn- best-match
   [i s cost]
   (let [candidates (reverse (subvec cost (max 0 (- i max-word)) i))]
-    (apply min-key first (map-indexed (fn [k c] [(+ c (get word-cost (subs s (- i k 1) i) 9e9999)) (inc k)]) candidates))))
+    (apply min-key first (map-indexed (fn [k c] [(+ c (get-word-cost (subs s (- i k 1) i) 9e9999)) (inc k)]) candidates))))
 
 ;;     # Build the cost array.
 ;;     cost = [0]
@@ -65,7 +97,7 @@
   [input]
   (let [s (s/lower-case input)
         cost (build-cost-array s)]
-    (loop [i (count s)
+    (loop [i (double (count s))
            out []]
       (if-not (pos? i)
         (reverse out)
diff --git a/src/metabase/util/schema.clj b/src/metabase/util/schema.clj
index 455e17251a3305978c2253882b8142bc01e3ca19..b32bf539073df310bdf07499e8d98434b57e2fc4 100644
--- a/src/metabase/util/schema.clj
+++ b/src/metabase/util/schema.clj
@@ -7,6 +7,10 @@
             [metabase.util.password :as password]
             [schema.core :as s]))
 
+;; always validate all schemas in s/defn function declarations. See
+;; https://github.com/plumatic/schema#schemas-in-practice for details.
+(s/set-fn-validation! true)
+
 (defn with-api-error-message
   "Return SCHEMA with an additional API-ERROR-MESSAGE that will be used to explain the error if a parameter fails
    validation.
diff --git a/test/metabase/api/alert_test.clj b/test/metabase/api/alert_test.clj
index 2bb2f91eabfb372d98ec6ff9a0a094e3ad3c92c0..a4c0d32c92795169378d660e3c4f087eb9171a97 100644
--- a/test/metabase/api/alert_test.clj
+++ b/test/metabase/api/alert_test.clj
@@ -115,6 +115,12 @@
                                      "My question" true}
                                     body-map)}))
 
+(defn- rasta-added-to-alert-email [body-map]
+  (et/email-to :rasta {:subject "Crowberto Corv added you to an alert",
+                       :body (merge {"https://metabase.com/testmb" true,
+                                     "now getting alerts" true}
+                                    body-map)}))
+
 (defn- rasta-unsubscribe-email [body-map]
   (et/email-to :rasta {:subject "You unsubscribed from an alert",
                        :body (merge {"https://metabase.com/testmb" true}
@@ -168,6 +174,43 @@
                             #"has any results"
                             #"My question")]))
 
+(defn- setify-recipient-emails [results]
+  (update results :channels (fn [channels]
+                              (map #(update % :recipients set) channels))))
+
+;; An admin created alert should notify others they've been subscribed
+(tt/expect-with-temp [Card [card1 {:name "My question"}]]
+  [(-> (default-alert card1)
+       (assoc :creator (user-details :crowberto))
+       (update-in [:channels 0] merge {:schedule_hour 12, :schedule_type "daily", :recipients (set (map recipient-details [:rasta :crowberto]))}))
+   (merge (et/email-to :crowberto {:subject "You setup an alert",
+                                   :body {"https://metabase.com/testmb" true,
+                                          "My question" true
+                                          "now getting alerts" false
+                                          "confirmation that your alert" true}})
+          (rasta-added-to-alert-email {"My question" true
+                                       "now getting alerts" true
+                                       "confirmation that your alert" false}))]
+
+  (with-alert-setup
+    [(et/with-expected-messages 2
+       (-> ((alert-client :crowberto) :post 200 "alert"
+            {:card              {:id (:id card1)}
+             :alert_condition   "rows"
+             :alert_first_only  false
+             :channels          [{:enabled       true
+                                  :channel_type  "email"
+                                  :schedule_type "daily"
+                                  :schedule_hour 12
+                                  :schedule_day  nil
+                                  :details       {:emails nil}
+                                  :recipients    (mapv fetch-user [:crowberto :rasta])}]})
+           setify-recipient-emails))
+     (et/regex-email-bodies #"https://metabase.com/testmb"
+                            #"now getting alerts"
+                            #"confirmation that your alert"
+                            #"My question")]))
+
 ;; Check creation of a below goal alert
 (tt/expect-with-temp [Card [card1 {:name "My question"
                                    :display "line"}]]
@@ -322,10 +365,6 @@
      (default-alert-req card pc-id {:alert_first_only true, :alert_above_goal true, :alert_condition "goal"}
                         [(fetch-user :rasta)]))))
 
-(defn- setify-recipient-emails [results]
-  (update results :channels (fn [channels]
-                              (map #(update % :recipients set) channels))))
-
 ;; Admin users can add a recipieint, that recipient should be notified
 (tt/expect-with-temp [Pulse [{pulse-id :id}                (default-pulse-row)]
                       Card  [{card-id :id :as card}]
@@ -557,7 +596,7 @@
                       PulseChannelRecipient [_             {:user_id          (user->id :rasta)
                                                             :pulse_channel_id pc-id-1}]]
   [1
-   1                                    ;<-- Alert should not be deleted
+   1 ;;<-- Alert should not be deleted
    (rasta-unsubscribe-email {"Foo" true})]
   (with-alert-setup
    [(count ((user->client :rasta) :get 200 (format "alert/question/%d" card-id)))
@@ -675,3 +714,21 @@
     (count ((user->client :rasta) :get 200 (format "alert/question/%d" card-id)))
     (et/regex-email-bodies #"Crowberto Corv deleted an alert"
                            #"Crowberto Corv unsubscribed you from alerts")]))
+
+;; When an admin deletes their own alert, it should not notify them
+(tt/expect-with-temp [Card                 [{card-id :id}  (basic-alert-query)]
+                      Pulse                [{pulse-id :id} {:alert_condition   "rows"
+                                                            :alert_first_only  false
+                                                            :creator_id        (user->id :crowberto)}]
+                      PulseCard             [_             {:pulse_id pulse-id
+                                                            :card_id  card-id
+                                                            :position 0}]
+                      PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                      PulseChannelRecipient [_             {:user_id          (user->id :crowberto)
+                                                            :pulse_channel_id pc-id}]]
+  [1 nil 0 {}]
+  (with-alert-setup
+   [(count ((user->client :crowberto) :get 200 (format "alert/question/%d" card-id)))
+    ((user->client :crowberto) :delete 204 (format "alert/%d" pulse-id))
+    (count ((user->client :crowberto) :get 200 (format "alert/question/%d" card-id)))
+    (et/regex-email-bodies #".*")]))
diff --git a/test/metabase/api/async_test.clj b/test/metabase/api/async_test.clj
index 57b505728969177bfe2b3aaa05173f48937c702d..2a1a26642b5f06d7dcee8ee130a9adcef4b246df 100644
--- a/test/metabase/api/async_test.clj
+++ b/test/metabase/api/async_test.clj
@@ -1,8 +1,8 @@
 (ns metabase.api.async-test
   "Tests for /api/async endpoints."
   (:require [expectations :refer :all]
-            [metabase.test.async :refer :all]
-            [metabase.test.data :refer :all :as data]
+            [metabase.test.async :refer [result!]]
+            [metabase.test.data :refer :all]
             [metabase.test.data.users :refer :all]))
 
 (expect
@@ -13,8 +13,9 @@
 
 (expect
   true
-  (contains? (->> ((user->client :rasta) :get 200
-                   (str "x-ray/field/" (id :venues :price)))
-                  :job-id
-                  (call-with-retries :rasta))
-             :features))
+  (let [client (user->client :rasta)
+        job-id (:job-id (client :get 200 (str "x-ray/field/" (id :venues :price))))]
+    (result! job-id)
+    (-> (client :get 200 (str "async/" job-id))
+        :result
+        (contains? :features))))
diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj
index c9a36f021c191ccbeb22085116bab2bc10e9b2a3..8f9f4ba065ac53b79ac69caff53e5631402f203d 100644
--- a/test/metabase/api/card_test.clj
+++ b/test/metabase/api/card_test.clj
@@ -101,8 +101,8 @@
 (expect (get middleware/response-unauthentic :body) (http/client :put 401 "card/13"))
 
 
-;; Make sure `id` is required when `f` is :database
-(expect {:errors {:id "id is required parameter when filter mode is 'database'"}}
+;; Make sure `model_id` is required when `f` is :database
+(expect {:errors {:model_id "model_id is a required parameter when filter mode is 'database'"}}
   ((user->client :crowberto) :get 400 "card" :f :database))
 
 ;; Filter cards by table
@@ -124,8 +124,8 @@
      (card-returned? table-2-id card-1-id)
      (card-returned? table-2-id card-2-id)]))
 
-;; Make sure `id` is required when `f` is :table
-(expect {:errors {:id "id is required parameter when filter mode is 'table'"}}
+;; Make sure `model_id` is required when `f` is :table
+(expect {:errors {:model_id "model_id is a required parameter when filter mode is 'table'"}}
         ((user->client :crowberto) :get 400 "card", :f :table))
 
 
diff --git a/test/metabase/api/collection_test.clj b/test/metabase/api/collection_test.clj
index 53bda5027ad83210e7cc476e16f0778c47946502..dd7eeab69b9c5736f1bbba15586f17b79f39fcf6 100644
--- a/test/metabase/api/collection_test.clj
+++ b/test/metabase/api/collection_test.clj
@@ -116,10 +116,10 @@
 ;; Archiving a collection should delete any alerts associated with questions in the collection
 (tt/expect-with-temp [Collection            [{collection-id :id}]
                       Card                  [{card-id :id :as card} {:collection_id collection-id}]
-                      Pulse                 [{pulse-id :id} {:alert_condition   "rows"
-                                                             :alert_first_only  false
-                                                             :creator_id        (user->id :rasta)
-                                                             :name              "Original Alert Name"}]
+                      Pulse                 [{pulse-id :id} {:alert_condition  "rows"
+                                                             :alert_first_only false
+                                                             :creator_id       (user->id :rasta)
+                                                             :name             "Original Alert Name"}]
 
                       PulseCard             [_              {:pulse_id pulse-id
                                                              :card_id  card-id
@@ -129,14 +129,11 @@
                                                              :pulse_channel_id pc-id}]
                       PulseChannelRecipient [{pcr-id-2 :id} {:user_id          (user->id :rasta)
                                                              :pulse_channel_id pc-id}]]
-  [{"crowberto@metabase.com" [{:from "notifications@metabase.com",
-                               :to ["crowberto@metabase.com"],
-                               :subject "One of your alerts has stopped working",
-                               :body {"the question was archived by Crowberto Corv" true}}],
-    "rasta@metabase.com" [{:from "notifications@metabase.com",
-                           :to ["rasta@metabase.com"],
-                           :subject "One of your alerts has stopped working",
-                           :body {"the question was archived by Crowberto Corv" true}}]}
+
+  [(merge (et/email-to :crowberto {:subject "One of your alerts has stopped working",
+                                   :body    {"the question was archived by Crowberto Corv" true}})
+          (et/email-to :rasta {:subject "One of your alerts has stopped working",
+                               :body    {"the question was archived by Crowberto Corv" true}}))
    nil]
   (et/with-fake-inbox
     (et/with-expected-messages 2
diff --git a/test/metabase/api/embed_test.clj b/test/metabase/api/embed_test.clj
index a01303374e3f5119fdfc0d61e45149b7fe6cd161..6818ada0e8b198d27844d0fcf6ad07c8bf549715 100644
--- a/test/metabase/api/embed_test.clj
+++ b/test/metabase/api/embed_test.clj
@@ -1,9 +1,11 @@
 (ns metabase.api.embed-test
+  "Tests for /api/embed endpoints."
   (:require [buddy.sign.jwt :as jwt]
             [crypto.random :as crypto-random]
             [dk.ative.docjure.spreadsheet :as spreadsheet]
             [expectations :refer :all]
             [metabase
+             [config :as config]
              [http-client :as http]
              [util :as u]]
             [metabase.api
@@ -48,7 +50,9 @@
 (defmacro with-temp-dashcard {:style/indent 1} [[dashcard-binding {:keys [dash card dashcard]}] & body]
   `(with-temp-card [card# ~card]
      (tt/with-temp* [Dashboard     [dash# ~dash]
-                     DashboardCard [~dashcard-binding (merge {:card_id (u/get-id card#), :dashboard_id (u/get-id dash#)} ~dashcard)]]
+                     DashboardCard [~dashcard-binding (merge {:card_id      (u/get-id card#)
+                                                              :dashboard_id (u/get-id dash#)}
+                                                             ~dashcard)]]
        ~@body)))
 
 (defmacro with-embedding-enabled-and-new-secret-key {:style/indent 0} [& body]
@@ -59,9 +63,9 @@
 (defn successful-query-results
   ([]
    {:data       {:columns ["count"]
-                 :cols    [{:description nil, :table_id nil, :special_type "type/Number", :name "count", :source "aggregation",
-                            :extra_info {}, :id nil, :target nil, :display_name "count", :base_type "type/Integer"
-                            :remapped_from nil, :remapped_to nil}]
+                 :cols    [{:description nil, :table_id nil, :special_type "type/Number", :name "count",
+                            :source "aggregation", :extra_info {}, :id nil, :target nil, :display_name "count",
+                            :base_type "type/Integer", :remapped_from nil, :remapped_to nil}]
                  :rows    [[100]]}
     :json_query {:parameters []}
     :status     "completed"})
@@ -95,7 +99,7 @@
   {:description nil, :parameters (), :ordered_cards (), :param_values nil})
 
 
-;; ------------------------------------------------------------ GET /api/embed/card/:token ------------------------------------------------------------
+;;; ------------------------------------------- GET /api/embed/card/:token -------------------------------------------
 
 (defn- card-url [card & [additional-token-params]] (str "embed/card/" (card-token card additional-token-params)))
 
@@ -122,7 +126,8 @@
     (with-temp-card [card]
       (http/client :get 400 (card-url card)))))
 
-;; check that if embedding is enabled globally and for the object that requests fail if they are signed with the wrong key
+;; check that if embedding is enabled globally and for the object that requests fail if they are signed with the wrong
+;; key
 (expect
   "Message seems corrupt or manipulated."
   (with-embedding-enabled-and-new-secret-key
@@ -142,7 +147,7 @@
       (:parameters (http/client :get 200 (card-url card {:params {:c 100}}))))))
 
 
-;; ------------------------------------------------------------ GET /api/embed/card/:token/query (and JSON/CSV/XLSX variants)  ------------------------------------------------------------
+;;; ------------------------- GET /api/embed/card/:token/query (and JSON/CSV/XLSX variants) --------------------------
 
 (defn- card-query-url [card response-format & [additional-token-params]]
   (str "embed/card/"
@@ -150,7 +155,9 @@
        "/query"
        response-format))
 
-(defmacro ^:private expect-for-response-formats {:style/indent 1} [[response-format-binding request-options-binding] expected actual]
+(defmacro ^:private expect-for-response-formats
+  {:style/indent 1}
+  [[response-format-binding request-options-binding] expected actual]
   `(do
      ~@(for [[response-format request-options] [[""] ["/json"] ["/csv"] ["/xlsx" {:as :byte-array}]]]
          `(expect
@@ -168,11 +175,14 @@
     (with-temp-card [card {:enable_embedding true}]
       (http/client :get 200 (card-query-url card response-format) request-options))))
 
-;; but if the card has an invalid query we should just get a generic "query failed" exception (rather than leaking query info)
+;; but if the card has an invalid query we should just get a generic "query failed" exception (rather than leaking
+;; query info)
 (expect-for-response-formats [response-format]
   "An error occurred while running the query."
   (with-embedding-enabled-and-new-secret-key
-    (with-temp-card [card {:enable_embedding true, :dataset_query {:database (data/id), :type :native, :native {:query "SELECT * FROM XYZ"}}}]
+    (with-temp-card [card {:enable_embedding true, :dataset_query {:database (data/id)
+                                                                   :type     :native
+                                                                   :native   {:query "SELECT * FROM XYZ"}}}]
       (http/client :get 400 (card-query-url card response-format)))))
 
 ;; check that the endpoint doesn't work if embedding isn't enabled
@@ -190,16 +200,19 @@
     (with-temp-card [card]
       (http/client :get 400 (card-query-url card response-format)))))
 
-;; check that if embedding is enabled globally and for the object that requests fail if they are signed with the wrong key
+;; check that if embedding is enabled globally and for the object that requests fail if they are signed with the wrong
+;; key
 (expect-for-response-formats [response-format]
   "Message seems corrupt or manipulated."
   (with-embedding-enabled-and-new-secret-key
     (with-temp-card [card {:enable_embedding true}]
       (http/client :get 400 (with-new-secret-key (card-query-url card response-format))))))
 
+
 ;;; LOCKED params
 
-;; check that if embedding is enabled globally and for the object requests fail if the token is missing a `:locked` parameter
+;; check that if embedding is enabled globally and for the object requests fail if the token is missing a `:locked`
+;; parameter
 (expect-for-response-formats [response-format]
   "You must specify a value for :abc in the JWT."
   (with-embedding-enabled-and-new-secret-key
@@ -220,6 +233,7 @@
     (with-temp-card [card {:enable_embedding true, :embedding_params {:abc "locked"}}]
       (http/client :get 400 (str (card-query-url card response-format {:params {:abc 100}}) "?abc=100")))))
 
+
 ;;; DISABLED params
 
 ;; check that if embedding is enabled globally and for the object requests fail if they pass a `:disabled` parameter
@@ -260,9 +274,40 @@
       (http/client :get 200 (str (card-query-url card response-format) "?abc=200") request-options))))
 
 
-;; ------------------------------------------------------------ GET /api/embed/dashboard/:token ------------------------------------------------------------
+;; make sure CSV (etc.) downloads take editable params into account (#6407)
+
+(defn- card-with-date-field-filter []
+  {:dataset_query    {:database (data/id)
+                      :type     :native
+                      :native   {:query         "SELECT COUNT(*) AS \"count\" FROM CHECKINS WHERE {{date}}"
+                                 :template_tags {:date {:name         "date"
+                                                        :display_name "Date"
+                                                        :type         "dimension"
+                                                        :dimension    [:field-id (data/id :checkins :date)]
+                                                        :widget_type  "date/quarter-year"}}}}
+   :enable_embedding true
+   :embedding_params {:date :enabled}})
+
+(expect
+  "count\n107\n"
+  (with-embedding-enabled-and-new-secret-key
+    (tt/with-temp Card [card (card-with-date-field-filter)]
+      (http/client :get 200 (str (card-query-url card "/csv") "?date=Q1-2014")))))
+
+;; make sure it also works with the forwarded URL
+(expect
+  "count\n107\n"
+  (with-embedding-enabled-and-new-secret-key
+    (tt/with-temp Card [card (card-with-date-field-filter)]
+      ;; make sure the URL doesn't include /api/ at the beginning like it normally would
+      (binding [http/*url-prefix* (str "http://localhost:" (config/config-str :mb-jetty-port) "/")]
+        (http/client :get 200 (str "embed/question/" (card-token card) ".csv?date=Q1-2014"))))))
+
+
+;;; ---------------------------------------- GET /api/embed/dashboard/:token -----------------------------------------
 
-(defn- dashboard-url [dashboard & [additional-token-params]] (str "embed/dashboard/" (dash-token dashboard additional-token-params)))
+(defn- dashboard-url [dashboard & [additional-token-params]]
+  (str "embed/dashboard/" (dash-token dashboard additional-token-params)))
 
 ;; it should be possible to call this endpoint successfully
 (expect
@@ -287,7 +332,8 @@
     (tt/with-temp Dashboard [dash]
       (http/client :get 400 (dashboard-url dash)))))
 
-;; check that if embedding is enabled globally and for the object that requests fail if they are signed with the wrong key
+;; check that if embedding is enabled globally and for the object that requests fail if they are signed with the wrong
+;; key
 (expect
   "Message seems corrupt or manipulated."
   (with-embedding-enabled-and-new-secret-key
@@ -307,7 +353,7 @@
       (:parameters (http/client :get 200 (dashboard-url dash {:params {:c 100}}))))))
 
 
-;; ------------------------------------------------------------ GET /api/embed/dashboard/:token/dashcard/:dashcard-id/card/:card-id ------------------------------------------------------------
+;;; ---------------------- GET /api/embed/dashboard/:token/dashcard/:dashcard-id/card/:card-id -----------------------
 
 (defn- dashcard-url [dashcard & [additional-token-params]]
   (str "embed/dashboard/" (dash-token (:dashboard_id dashcard) additional-token-params)
@@ -321,12 +367,15 @@
     (with-temp-dashcard [dashcard {:dash {:enable_embedding true}}]
       (http/client :get 200 (dashcard-url dashcard)))))
 
-;; but if the card has an invalid query we should just get a generic "query failed" exception (rather than leaking query info)
+;; but if the card has an invalid query we should just get a generic "query failed" exception (rather than leaking
+;; query info)
 (expect
   "An error occurred while running the query."
   (with-embedding-enabled-and-new-secret-key
     (with-temp-dashcard [dashcard {:dash {:enable_embedding true}
-                                   :card {:dataset_query {:database (data/id), :type :native, :native {:query "SELECT * FROM XYZ"}}}}]
+                                   :card {:dataset_query {:database (data/id)
+                                                          :type     :native,
+                                                          :native   {:query "SELECT * FROM XYZ"}}}}]
       (http/client :get 400 (dashcard-url dashcard)))))
 
 ;; check that the endpoint doesn't work if embedding isn't enabled
@@ -344,7 +393,8 @@
     (with-temp-dashcard [dashcard]
       (http/client :get 400 (dashcard-url dashcard)))))
 
-;; check that if embedding is enabled globally and for the object that requests fail if they are signed with the wrong key
+;; check that if embedding is enabled globally and for the object that requests fail if they are signed with the wrong
+;; key
 (expect
   "Message seems corrupt or manipulated."
   (with-embedding-enabled-and-new-secret-key
@@ -353,7 +403,8 @@
 
 ;;; LOCKED params
 
-;; check that if embedding is enabled globally and for the object requests fail if the token is missing a `:locked` parameter
+;; check that if embedding is enabled globally and for the object requests fail if the token is missing a `:locked`
+;; parameter
 (expect
   "You must specify a value for :abc in the JWT."
   (with-embedding-enabled-and-new-secret-key
@@ -414,9 +465,10 @@
       (http/client :get 200 (str (dashcard-url dashcard) "?abc=200")))))
 
 
-;;; ------------------------------------------------------------ Other Tests ------------------------------------------------------------
+;;; -------------------------------------------------- Other Tests ---------------------------------------------------
 
-;; parameters that are not in the `embedding-params` map at all should get removed by `remove-locked-and-disabled-params`
+;; parameters that are not in the `embedding-params` map at all should get removed by
+;; `remove-locked-and-disabled-params`
 (expect
   {:parameters []}
   (#'embed-api/remove-locked-and-disabled-params {:parameters {:slug "foo"}} {}))
diff --git a/test/metabase/api/public_test.clj b/test/metabase/api/public_test.clj
index c705dea6958b15ada7ec1b9339297d8170e2fdf5..888d0c90746fd9e0eb5feab9cd8c5f38db062951 100644
--- a/test/metabase/api/public_test.clj
+++ b/test/metabase/api/public_test.clj
@@ -4,6 +4,7 @@
             [dk.ative.docjure.spreadsheet :as spreadsheet]
             [expectations :refer :all]
             [metabase
+             [config :as config]
              [http-client :as http]
              [query-processor-test :as qp-test]
              [util :as u]]
@@ -22,7 +23,7 @@
   (:import java.io.ByteArrayInputStream
            java.util.UUID))
 
-;;; ------------------------------------------------------------ Helper Fns ------------------------------------------------------------
+;;; --------------------------------------------------- Helper Fns ---------------------------------------------------
 
 (defn count-of-venues-card []
   {:dataset_query {:database (data/id)
@@ -62,7 +63,7 @@
 
 
 
-;;; ------------------------------------------------------------ GET /api/public/card/:uuid ------------------------------------------------------------
+;;; ------------------------------------------- GET /api/public/card/:uuid -------------------------------------------
 
 ;; Check that we *cannot* fetch a PublicCard if the setting is disabled
 (expect
@@ -99,7 +100,10 @@
                                 :human_readable_values {}
                                 :field_id              (data/id :categories :name)}}
   (tt/with-temp Card [card {:dataset_query {:type   :native
-                                            :native {:query         "SELECT COUNT(*) FROM venues LEFT JOIN categories ON venues.category_id = categories.id WHERE {{category}}"
+                                            :native {:query         (str "SELECT COUNT(*) "
+                                                                         "FROM venues "
+                                                                         "LEFT JOIN categories ON venues.category_id = categories.id "
+                                                                         "WHERE {{category}}")
                                                      :collection    "CATEGORIES"
                                                      :template_tags {:category {:name         "category"
                                                                                 :display_name "Category"
@@ -111,7 +115,7 @@
         (update-in [(data/id :categories :name) :values] count))))
 
 
-;;; ------------------------------------------------------------ GET /api/public/card/:uuid/query (and JSON/CSV/XSLX versions)  ------------------------------------------------------------
+;;; ------------------------- GET /api/public/card/:uuid/query (and JSON/CSV/XSLX versions) --------------------------
 
 ;; Check that we *cannot* execute a PublicCard if the setting is disabled
 (expect
@@ -173,8 +177,42 @@
       (get-in (http/client :get 200 (str "public/card/" uuid "/query"), :parameters (json/encode [{:type "category", :value 2}]))
               [:json_query :parameters]))))
 
+;; make sure CSV (etc.) downloads take editable params into account (#6407)
 
-;;; ------------------------------------------------------------ GET /api/public/dashboard/:uuid ------------------------------------------------------------
+(defn- card-with-date-field-filter []
+  (assoc (shared-obj)
+    :dataset_query {:database (data/id)
+                    :type     :native
+                    :native   {:query         "SELECT COUNT(*) AS \"count\" FROM CHECKINS WHERE {{date}}"
+                               :template_tags {:date {:name         "date"
+                                                      :display_name "Date"
+                                                      :type         "dimension"
+                                                      :dimension    [:field-id (data/id :checkins :date)]
+                                                      :widget_type  "date/quarter-year"}}}}))
+
+(expect
+  "count\n107\n"
+  (tu/with-temporary-setting-values [enable-public-sharing true]
+    (tt/with-temp Card [{uuid :public_uuid} (card-with-date-field-filter)]
+      (http/client :get 200 (str "public/card/" uuid "/query/csv")
+                   :parameters (json/encode [{:type   :date/quarter-year
+                                              :target [:dimension [:template-tag :date]]
+                                              :value  "Q1-2014"}])))))
+
+;; make sure it also works with the forwarded URL
+(expect
+  "count\n107\n"
+  (tu/with-temporary-setting-values [enable-public-sharing true]
+    (tt/with-temp Card [{uuid :public_uuid} (card-with-date-field-filter)]
+      ;; make sure the URL doesn't include /api/ at the beginning like it normally would
+      (binding [http/*url-prefix* (str "http://localhost:" (config/config-str :mb-jetty-port) "/")]
+        (http/client :get 200 (str "public/question/" uuid ".csv")
+                     :parameters (json/encode [{:type   :date/quarter-year
+                                              :target [:dimension [:template-tag :date]]
+                                              :value  "Q1-2014"}]))))))
+
+
+;;; ---------------------------------------- GET /api/public/dashboard/:uuid -----------------------------------------
 
 ;; Check that we *cannot* fetch PublicDashboard if setting is disabled
 (expect
@@ -211,7 +249,7 @@
       (fetch-public-dashboard dash))))
 
 
-;;; ------------------------------------------------------------ GET /api/public/dashboard/:uuid/card/:card-id ------------------------------------------------------------
+;;; --------------------------------- GET /api/public/dashboard/:uuid/card/:card-id ----------------------------------
 
 (defn- dashcard-url-path [dash card]
   (str "public/dashboard/" (:public_uuid dash) "/card/" (u/get-id card)))
@@ -281,7 +319,7 @@
           (qp-test/rows (http/client :get 200 (dashcard-url-path dash card-2))))))))
 
 
-;;; ------------------------------------------------------------ Check that parameter information comes back with Dashboard ------------------------------------------------------------
+;;; --------------------------- Check that parameter information comes back with Dashboard ---------------------------
 
 ;; double-check that the Field has FieldValues
 (expect
diff --git a/test/metabase/api/pulse_test.clj b/test/metabase/api/pulse_test.clj
index 1d9d803792b514045fc43dbd3277ac3f235abca6..f1c7ab3445e305ed864a6c7367874acf01708e90 100644
--- a/test/metabase/api/pulse_test.clj
+++ b/test/metabase/api/pulse_test.clj
@@ -2,6 +2,7 @@
   "Tests for /api/pulse endpoints."
   (:require [expectations :refer :all]
             [metabase
+             [email-test :as et]
              [http-client :as http]
              [middleware :as middleware]
              [util :as u]]
@@ -13,9 +14,13 @@
              [pulse :as pulse :refer [Pulse]]
              [pulse-card :refer [PulseCard]]
              [table :refer [Table]]]
-            [metabase.test.data.users :refer :all]
+            [metabase.test
+             [data :as data]
+             [util :as tu]]
+            [metabase.test.data
+             [dataset-definitions :as defs]
+             [users :refer :all]]
             [metabase.test.mock.util :refer [pulse-channel-defaults]]
-            [metabase.test.util :as tu]
             [toucan.db :as db]
             [toucan.util.test :as tt]))
 
@@ -232,3 +237,28 @@
 (tt/expect-with-temp [Pulse [{pulse-id :id} {:alert_condition "rows"}]]
   "Not found."
   ((user->client :rasta) :get 404 (str "pulse/" pulse-id)))
+
+;; ## POST /api/pulse/test
+(expect
+  [{:ok true}
+   (et/email-to :rasta {:subject "Pulse: Daily Sad Toucans"
+                        :body {"Daily Sad Toucans" true}})]
+  (tu/with-model-cleanup [Pulse]
+    (et/with-fake-inbox
+      (data/with-db (data/get-or-create-database! defs/sad-toucan-incidents)
+        (tt/with-temp* [Database  [{database-id :id}]
+                        Table     [{table-id :id}    {:db_id database-id}]
+                        Card      [{card-id :id}     {:dataset_query {:database database-id
+                                                                      :type     "query"
+                                                                      :query    {:source-table table-id,
+                                                                                 :aggregation  {:aggregation-type "count"}}}}]]
+          [((user->client :rasta) :post 200 "pulse/test" {:name          "Daily Sad Toucans"
+                                                          :cards         [{:id card-id}]
+                                                          :channels      [{:enabled       true
+                                                                           :channel_type  "email"
+                                                                           :schedule_type "daily"
+                                                                           :schedule_hour 12
+                                                                           :schedule_day  nil
+                                                                           :recipients    [(fetch-user :rasta)]}]
+                                                          :skip_if_empty false})
+           (et/regex-email-bodies #"Daily Sad Toucans")])))))
diff --git a/test/metabase/api/segment_test.clj b/test/metabase/api/segment_test.clj
index 2a2582f4e07b41cefaa32f1e03d82e9b504a6b89..a783d9b4318ab9721185b4baa354add8a561078b 100644
--- a/test/metabase/api/segment_test.clj
+++ b/test/metabase/api/segment_test.clj
@@ -363,9 +363,9 @@
 
 
 ;;; GET /api/segement/
-(tt/expect-with-temp [Segment [segment-1]
-                      Segment [segment-2]
-                      Segment [_          {:is_active false}]] ; inactive segments shouldn't show up
+(tt/expect-with-temp [Segment [segment-1 {:name "Segment 1"}]
+                      Segment [segment-2 {:name "Segment 2"}]
+                      Segment [_         {:is_active false}]] ; inactive segments shouldn't show up
   (tu/mappify (hydrate [segment-1
                         segment-2] :creator))
   ((user->client :rasta) :get 200 "segment/"))
diff --git a/test/metabase/api/x_ray_test.clj b/test/metabase/api/x_ray_test.clj
index e3ffcbd8d43043a7241bf8c3d086610b2f82b113..8023e0b84b00649f21b344014139205b487c8456 100644
--- a/test/metabase/api/x_ray_test.clj
+++ b/test/metabase/api/x_ray_test.clj
@@ -16,9 +16,10 @@
 
 (defn- async-call
   [method endpoint & args]
-  (->> (apply (user->client :rasta) method 200 endpoint args)
-       :job-id
-       (call-with-retries :rasta)))
+  (let [client (user->client :rasta)
+        job-id (:job-id (apply client method 200 endpoint args))]
+    (result! job-id)
+    (:result (client :get 200 (str "async/" job-id)))))
 
 (expect
   100.0
diff --git a/test/metabase/email_test.clj b/test/metabase/email_test.clj
index e557b985bf4026dcee676e4d491fdaab9b259182..02d029089b01eb12a6bdf8c0b799c3bbbdf20234 100644
--- a/test/metabase/email_test.clj
+++ b/test/metabase/email_test.clj
@@ -5,7 +5,8 @@
             [medley.core :as m]
             [metabase.email :as email]
             [metabase.test.data.users :as user]
-            [metabase.test.util :as tu]))
+            [metabase.test.util :as tu])
+  (:import javax.activation.MimeType))
 
 (def inbox
   "Map of email addresses -> sequence of messages they've received."
@@ -82,10 +83,10 @@
   return map of the stringified regex as the key and a boolean as the value. True if it returns results via `re-find`
   false otherwise."
   [regex-seq]
-  (fn [message-body-seq]
-    (let [{message-body :content} (first message-body-seq)]
+  (fn [message-body]
+    (let [{:keys [content]} message-body]
       (zipmap (map str regex-seq)
-              (map #(boolean (re-find % message-body)) regex-seq)))))
+              (map #(boolean (re-find % content)) regex-seq)))))
 
 (defn regex-email-bodies
   "Will be apply each regex to each email body in the fake inbox. The body will be replaced by a map with the
@@ -94,7 +95,36 @@
   (let [email-body->regex-boolean (create-email-body->regex-fn regexes)]
     (m/map-vals (fn [emails-for-recipient]
                   (for [email emails-for-recipient]
-                    (update email :body email-body->regex-boolean)))
+                    (-> email
+                        (update :to set)
+                        (update :body (comp email-body->regex-boolean first)))))
+                @inbox)))
+
+(defn- mime-type [mime-type-str]
+  (-> mime-type-str
+      MimeType.
+      .getBaseType))
+
+(defn- summarize-attachment [email-attachment]
+  (-> email-attachment
+      (update :content-type mime-type)
+      (update :content class)
+      (update :content-id boolean)))
+
+(defn summarize-multipart-email
+  "For text/html portions of an email, this is similar to `regex-email-bodies`, but for images in the attachments will
+  summarize the contents for comparison in expects"
+  [& regexes]
+  (let [email-body->regex-boolean (create-email-body->regex-fn regexes)]
+    (m/map-vals (fn [emails-for-recipient]
+                  (for [email emails-for-recipient]
+                    (-> email
+                        (update :to set)
+                        (update :body (fn [email-body-seq]
+                                        (for [{email-type :type :as email-part}  email-body-seq]
+                                          (if (string? email-type)
+                                            (email-body->regex-boolean email-part)
+                                            (summarize-attachment email-part))))))))
                 @inbox)))
 
 (defn email-to
@@ -102,7 +132,7 @@
   [user-kwd & [email-map]]
   (let [{:keys [email]} (user/fetch-user user-kwd)]
     {email [(merge {:from "notifications@metabase.com",
-                    :to [email]}
+                    :to #{email}}
                     email-map)]}))
 
 ;; simple test of email sending capabilities
diff --git a/test/metabase/feature_extraction/async_test.clj b/test/metabase/feature_extraction/async_test.clj
index d5baca1cc8238bca36e7b018e2268c3f69324303..604a47c9374eb27ff4b9e6476adccaf029aff14d 100644
--- a/test/metabase/feature_extraction/async_test.clj
+++ b/test/metabase/feature_extraction/async_test.clj
@@ -1,33 +1,33 @@
 (ns metabase.feature-extraction.async-test
   (:require [expectations :refer :all]
             [metabase.feature-extraction.async :refer :all]
-            [metabase.models.computation-job :refer [ComputationJob]]))
+            [metabase.models.computation-job :refer [ComputationJob]]
+            [metabase.test.async :refer :all]))
 
 (expect
   true
-  (let [job-id (compute (gensym) (constantly 1))]
-    (Thread/sleep 100)
+  (let [job-id (compute (gensym) (constantly 42))]
+    (result! job-id)
     (done? (ComputationJob job-id))))
 
 (expect
   [true :canceled false]
-  (let [job-id (compute (gensym) #(loop [] (Thread/sleep 100) (recur)))
+  (let [job-id (compute (gensym) #(do
+                                    (while-with-timeout (not (Thread/interrupted)))
+                                    42))
         r?     (running? (ComputationJob job-id))]
     (cancel (ComputationJob job-id))
     [r? (:status (ComputationJob job-id)) (running? (ComputationJob job-id))]))
 
 (expect
-  {:status :done
-   :result 1}
-  (let [job-id (compute (gensym) (constantly 1))]
-    (Thread/sleep 100)
-    (select-keys (result (ComputationJob job-id)) [:status :result])))
+  42
+  (-> (compute (gensym) (constantly 42))
+      result!
+      :result))
 
 (expect
-  [:error
-   "foo"]
-  (let [job-id (compute (gensym) #(throw (Throwable. "foo")))]
-    (Thread/sleep 100)
-    (let [job (ComputationJob job-id)]
-      [(:status job)
-       (-> job result :result :cause)])))
+  "foo"
+  (-> (compute (gensym) #(throw (Throwable. "foo")))
+      result!
+      :result
+      :cause))
diff --git a/test/metabase/feature_extraction/feature_extractors_test.clj b/test/metabase/feature_extraction/feature_extractors_test.clj
index c52e7ea7d156ae1c8b0df1cf262f395833d26e71..d0fec21e1ba6bba03a009a7d9aef18e09304b699 100644
--- a/test/metabase/feature_extraction/feature_extractors_test.clj
+++ b/test/metabase/feature_extraction/feature_extractors_test.clj
@@ -83,7 +83,7 @@
       t.coerce/to-sql-time))
 
 (def ^:private numbers [0.1 0.4 0.2 nil 0.5 0.3 0.51 0.55 0.22 0.0])
-(def ^:private ints [0 nil Long/MAX_VALUE Long/MIN_VALUE 5 -100])
+(def ^:private integers [0 nil Long/MAX_VALUE Long/MIN_VALUE 5 -100])
 (def ^:private datetimes [(make-sql-timestamp 2015 6 1)
                           nil
                           (make-sql-timestamp 2015 6 1)
@@ -107,7 +107,7 @@
    (var-get #'fe/Text)
    nil]
   [(-> (->features {:base_type :type/Number} numbers) :type)
-   (-> (->features {:base_type :type/Number} ints) :type)
+   (-> (->features {:base_type :type/Number} integers) :type)
    (-> (->features {:base_type :type/DateTime} datetimes) :type)
    (-> (->features {:base_type :type/Text
                     :special_type :type/Category}
diff --git a/test/metabase/middleware_test.clj b/test/metabase/middleware_test.clj
index 1d5f11a620ab1a093bc84d880997425b3c853a59..04acb8cc5b771fdac45239875fa328641ce82999 100644
--- a/test/metabase/middleware_test.clj
+++ b/test/metabase/middleware_test.clj
@@ -13,6 +13,7 @@
             [metabase.api.common :refer [*current-user* *current-user-id*]]
             [metabase.models.session :refer [Session]]
             [metabase.test.data.users :refer :all]
+            [metabase.test.async :refer [while-with-timeout]]
             [ring.mock.request :as mock]
             [ring.util.response :as resp]
             [toucan.db :as db]
@@ -247,25 +248,24 @@
 ;; test that handler is killed when connection closes
 ;; Note: this closing over state is a dirty hack and the whole test should be
 ;; rewritten.
-(def test-slow-handler-state1 (atom :unset))
-(def test-slow-handler-state2 (atom :unset))
 
 (defn- test-slow-handler [state]
   (fn [_]
     (log/debug (u/format-color 'yellow "starting test-slow-handler"))
+    (reset! state :test-started)
     (Thread/sleep 7000)  ;; this is somewhat long to make sure the keepalive polling has time to kill it.
     (reset! state :ran-to-compleation)
     (log/debug (u/format-color 'yellow "finished test-slow-handler"))
     (resp/response {:success true})))
 
 (defn- start-and-maybe-kill-test-request [state kill?]
-  (reset! state [:initial-state kill?])
+  (reset! state :initial-state)
   (let [path "test-slow-handler"]
     (with-redefs [metabase.routes/routes (compojure.core/routes
                                           (GET (str "/" path) [] (middleware/streaming-json-response
                                                                   (test-slow-handler state))))]
       (let  [reader (io/input-stream (str "http://localhost:" (config/config-int :mb-jetty-port) "/" path))]
-        (Thread/sleep 1500)
+        (while-with-timeout (= :initial-state @state))
         (when kill?
           (.close reader))
         (Thread/sleep 10000)))) ;; this is long enough to ensure that the handler has run to completion if it was not killed.
@@ -273,10 +273,10 @@
 
 ;; In this first test we will close the connection before the test handler gets to change the state
 (expect
-  [:initial-state true]
-  (start-and-maybe-kill-test-request test-slow-handler-state1 true))
+  :test-started
+  (start-and-maybe-kill-test-request (atom :unset) true))
 
 ;; and to make sure this test actually works, run the same test again and let it change the state.
 (expect
   :ran-to-compleation
-  (start-and-maybe-kill-test-request test-slow-handler-state2 false))
+  (start-and-maybe-kill-test-request (atom :unset) false))
diff --git a/test/metabase/models/pulse_channel_test.clj b/test/metabase/models/pulse_channel_test.clj
index 0cdb7aae235f263b3c78453af369303090f926cd..84b4165e8027d5cdc92b69d5396f6d3af54d2252 100644
--- a/test/metabase/models/pulse_channel_test.clj
+++ b/test/metabase/models/pulse_channel_test.clj
@@ -281,12 +281,12 @@
 ;; retrieve-scheduled-channels
 ;; test a simple scenario with a single Pulse and 2 channels on hourly/daily schedules
 (expect
-  [[{:schedule_type :hourly, :channel_type :slack}]
-   [{:schedule_type :hourly, :channel_type :slack}]
-   [{:schedule_type :daily,  :channel_type :email}
-    {:schedule_type :hourly, :channel_type :slack}]
-   [{:schedule_type :daily,  :channel_type :email}
-    {:schedule_type :hourly, :channel_type :slack}]]
+  [#{{:schedule_type :hourly, :channel_type :slack}}
+   #{{:schedule_type :hourly, :channel_type :slack}}
+   #{{:schedule_type :daily,  :channel_type :email}
+     {:schedule_type :hourly, :channel_type :slack}}
+   #{{:schedule_type :daily,  :channel_type :email}
+     {:schedule_type :hourly, :channel_type :slack}}]
   (tt/with-temp* [Pulse        [{pulse-id :id}]
                   PulseChannel [_ {:pulse_id pulse-id}] ;-> schedule_type = daily, schedule_hour = 15, channel_type = email
                   PulseChannel [_ {:pulse_id pulse-id, :channel_type :slack, :schedule_type :hourly}]
@@ -294,20 +294,20 @@
     (let [retrieve-channels (fn [hour day]
                               (for [channel (retrieve-scheduled-channels hour day :other :other)]
                                 (dissoc (into {} channel) :id :pulse_id)))]
-      [(retrieve-channels nil nil)
-       (retrieve-channels 12  nil)
-       (retrieve-channels 15  nil)
-       (retrieve-channels 15  "wed")])))
+      (map set [(retrieve-channels nil nil)
+                (retrieve-channels 12  nil)
+                (retrieve-channels 15  nil)
+                (retrieve-channels 15  "wed")]))))
 
 ;; more complex scenario with 2 Pulses, including weekly scheduling
 (expect
-  [[{:schedule_type :hourly, :channel_type :slack}]
-   [{:schedule_type :hourly, :channel_type :slack}
-    {:schedule_type :daily,  :channel_type :slack}]
-   [{:schedule_type :daily,  :channel_type :email}
-    {:schedule_type :hourly, :channel_type :slack}]
-   [{:schedule_type :hourly, :channel_type :slack}
-    {:schedule_type :weekly, :channel_type :email}]]
+  [#{{:schedule_type :hourly, :channel_type :slack}}
+   #{{:schedule_type :hourly, :channel_type :slack}
+     {:schedule_type :daily,  :channel_type :slack}}
+   #{{:schedule_type :daily,  :channel_type :email}
+     {:schedule_type :hourly, :channel_type :slack}}
+   #{{:schedule_type :hourly, :channel_type :slack}
+     {:schedule_type :weekly, :channel_type :email}}]
   (tt/with-temp* [Pulse        [{pulse-1-id :id}]
                   Pulse        [{pulse-2-id :id}]
                   PulseChannel [_ {:pulse_id pulse-1-id, :enabled true, :channel_type :email, :schedule_type :daily}]
@@ -317,20 +317,20 @@
     (let [retrieve-channels (fn [hour day]
                               (for [channel (retrieve-scheduled-channels hour day :other :other)]
                                 (dissoc (into {} channel) :id :pulse_id)))]
-      [(retrieve-channels nil nil)
-       (retrieve-channels 10  nil)
-       (retrieve-channels 15  nil)
-       (retrieve-channels 8   "mon")])))
+      (map set [(retrieve-channels nil nil)
+                (retrieve-channels 10  nil)
+                (retrieve-channels 15  nil)
+                (retrieve-channels 8   "mon")]))))
 
 ;; specific test for various monthly scheduling permutations
 (expect
-  [[]
-   [{:schedule_type :monthly, :channel_type :email}
-    {:schedule_type :monthly, :channel_type :slack}]
-   [{:schedule_type :monthly, :channel_type :slack}]
-   []
-   [{:schedule_type :monthly, :channel_type :slack}]
-   [{:schedule_type :monthly, :channel_type :email}]]
+  [#{}
+   #{{:schedule_type :monthly, :channel_type :email}
+     {:schedule_type :monthly, :channel_type :slack}}
+   #{{:schedule_type :monthly, :channel_type :slack}}
+   #{}
+   #{{:schedule_type :monthly, :channel_type :slack}}
+   #{{:schedule_type :monthly, :channel_type :email}}]
   (tt/with-temp* [Pulse        [{pulse-1-id :id}]
                   Pulse        [{pulse-2-id :id}]
                   PulseChannel [_ {:pulse_id pulse-1-id, :channel_type :email, :schedule_type :monthly, :schedule_hour 12, :schedule_frame :first}]
@@ -341,14 +341,14 @@
                               (for [channel (retrieve-scheduled-channels hour weekday monthday monthweek)]
                                 (dissoc (into {} channel) :id :pulse_id)))]
       ;; simple starter which should be empty
-      [(retrieve-channels nil nil :other :other)
-       ;; this should capture BOTH first absolute day of month + first monday of month schedules
-       (retrieve-channels 12 "mon" :first :first)
-       ;; this should only capture the first monday of the month
-       (retrieve-channels 12 "mon" :other :first)
-       ;; this makes sure hour checking is being enforced
-       (retrieve-channels 8 "mon" :first :first)
-       ;; middle of the month
-       (retrieve-channels 16 "fri" :mid :other)
-       ;; last friday of the month (but not the last day of month)
-       (retrieve-channels 8 "fri" :other :last)])))
+      (map set [(retrieve-channels nil nil :other :other)
+                ;; this should capture BOTH first absolute day of month + first monday of month schedules
+                (retrieve-channels 12 "mon" :first :first)
+                ;; this should only capture the first monday of the month
+                (retrieve-channels 12 "mon" :other :first)
+                ;; this makes sure hour checking is being enforced
+                (retrieve-channels 8 "mon" :first :first)
+                ;; middle of the month
+                (retrieve-channels 16 "fri" :mid :other)
+                ;; last friday of the month (but not the last day of month)
+                (retrieve-channels 8 "fri" :other :last)]))))
diff --git a/test/metabase/pulse_test.clj b/test/metabase/pulse_test.clj
index bfb14bcacb6f5caa14ce087f54840f196443be92..4c71edea5d331f8c9702b41e7667895629cea46b 100644
--- a/test/metabase/pulse_test.clj
+++ b/test/metabase/pulse_test.clj
@@ -3,13 +3,15 @@
             [expectations :refer :all]
             [medley.core :as m]
             [metabase.integrations.slack :as slack]
+            [metabase
+             [email-test :as et]
+             [pulse :refer :all]]
             [metabase.models
              [card :refer [Card]]
              [pulse :refer [Pulse retrieve-pulse retrieve-pulse-or-alert]]
              [pulse-card :refer [PulseCard]]
              [pulse-channel :refer [PulseChannel]]
              [pulse-channel-recipient :refer [PulseChannelRecipient]]]
-            [metabase.pulse :refer :all]
             [metabase.test
              [data :as data]
              [util :as tu]]
@@ -19,16 +21,6 @@
             [toucan.db :as db]
             [toucan.util.test :as tt]))
 
-(defn- email-body? [{message-type :type content :content}]
-  (and (= "text/html; charset=utf-8" message-type)
-       (string? content)
-       (.startsWith content "<html>")))
-
-(defn- attachment? [{message-type :type content-type :content-type content :content}]
-  (and (= :inline message-type)
-       (= "image/png" content-type)
-       (instance? java.io.File content)))
-
 (defn checkins-query
   "Basic query that will return results for an alert"
   [query-map]
@@ -49,295 +41,254 @@
   [data]
   (walk/postwalk identity data))
 
-(defmacro ^:private test-setup
-  "Macro that ensures test-data is present and disables sending of notifications"
+(defn- pulse-test-fixture
+  [f]
+  (data/with-db (data/get-or-create-database! defs/test-data)
+    (tu/with-temporary-setting-values [site-url "https://metabase.com/testmb"]
+      (f))))
+
+(defmacro ^:private slack-test-setup
+  "Macro that ensures test-data is present and disables sending of all notifications"
+  [& body]
+  `(with-redefs [metabase.pulse/send-notifications! realize-lazy-seqs
+                 slack/channels-list                (constantly [{:name "metabase_files"
+                                                                  :id   "FOO"}])]
+     (pulse-test-fixture (fn [] ~@body))))
+
+(defmacro ^:private email-test-setup
+  "Macro that ensures test-data is present and will use a fake inbox for emails"
   [& body]
-  `(data/with-db (data/get-or-create-database! defs/test-data)
-     (tu/with-temporary-setting-values [~'site-url "https://metabase.com/testmb"]
-       (with-redefs [metabase.pulse/send-notifications! realize-lazy-seqs
-                     slack/channels-list                (constantly [{:name "metabase_files"
-                                                                      :id   "FOO"}])]
-         ~@body))))
+  `(et/with-fake-inbox
+     (pulse-test-fixture (fn [] ~@body))))
+
+(def ^:private png-attachment
+  {:type :inline,
+   :content-id true,
+   :content-type "image/png",
+   :content java.io.File})
+
+(defn- rasta-pulse-email [& [email]]
+  (et/email-to :rasta (merge {:subject "Pulse: Pulse Name",
+                              :body  [{"Pulse Name" true}
+                                      png-attachment]}
+                             email)))
 
 ;; Basic test, 1 card, 1 recipient
-(tt/expect-with-temp [Card                 [{card-id :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
-                      Pulse                [{pulse-id :id} {:name "Pulse Name"
-                                                            :skip_if_empty false}]
-                      PulseCard             [_             {:pulse_id pulse-id
-                                                            :card_id  card-id
-                                                            :position 0}]
-                      PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
-                      PulseChannelRecipient [_             {:user_id (rasta-id)
-                                                            :pulse_channel_id pc-id}]]
-  [true
-   {:subject "Pulse: Pulse Name"
-    :recipients [(:email (users/fetch-user :rasta))]
-    :message-type :attachments}
-   2
-   true
-   true]
-  (test-setup
-   (let [[result & no-more-results] (send-pulse! (retrieve-pulse pulse-id))]
-     [(empty? no-more-results)
-      (select-keys result [:subject :recipients :message-type])
-      (count (:message result))
-      (email-body? (first (:message result)))
-      (attachment? (second (:message result)))])))
+(expect
+  (rasta-pulse-email)
+  (tt/with-temp* [Card                 [{card-id :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
+                  Pulse                [{pulse-id :id} {:name "Pulse Name"
+                                                        :skip_if_empty false}]
+                  PulseCard             [_             {:pulse_id pulse-id
+                                                        :card_id  card-id
+                                                        :position 0}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse pulse-id))
+     (et/summarize-multipart-email #"Pulse Name"))))
 
 ;; Pulse should be sent to two recipients
-(tt/expect-with-temp [Card                 [{card-id :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
-                      Pulse                [{pulse-id :id} {:name "Pulse Name"
-                                                            :skip_if_empty false}]
-                      PulseCard             [_             {:pulse_id pulse-id
-                                                            :card_id  card-id
-                                                            :position 0}]
-                      PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
-                      PulseChannelRecipient [_             {:user_id (rasta-id)
-                                                            :pulse_channel_id pc-id}]
-                      PulseChannelRecipient [_             {:user_id (users/user->id :crowberto)
-                                                            :pulse_channel_id pc-id}]]
-  [true
-   {:subject "Pulse: Pulse Name"
-    :recipients (set (map (comp :email users/fetch-user) [:rasta :crowberto]))
-    :message-type :attachments}
-   2
-   true
-   true]
-  (test-setup
-   (let [[result & no-more-results] (send-pulse! (retrieve-pulse pulse-id))]
-     [(empty? no-more-results)
-      (-> result
-          (select-keys [:subject :recipients :message-type])
-          (update :recipients set))
-      (count (:message result))
-      (email-body? (first (:message result)))
-      (attachment? (second (:message result)))])))
+(expect
+  (into {} (map (fn [user-kwd]
+                  (et/email-to user-kwd {:subject "Pulse: Pulse Name",
+                                         :to #{"rasta@metabase.com" "crowberto@metabase.com"}
+                                         :body [{"Pulse Name" true}
+                                                png-attachment]}))
+                [:rasta :crowberto]))
+  (tt/with-temp* [Card                 [{card-id :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
+                  Pulse                [{pulse-id :id} {:name "Pulse Name"
+                                                        :skip_if_empty false}]
+                  PulseCard             [_             {:pulse_id pulse-id
+                                                        :card_id  card-id
+                                                        :position 0}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id (rasta-id)
+                                                        :pulse_channel_id pc-id}]
+                  PulseChannelRecipient [_             {:user_id (users/user->id :crowberto)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse pulse-id))
+     (et/summarize-multipart-email #"Pulse Name"))))
 
 ;; 1 pulse that has 2 cards, should contain two attachments
-(tt/expect-with-temp [Card                 [{card-id-1 :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
-                      Card                 [{card-id-2 :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "day-of-week"]]})]
-                      Pulse                [{pulse-id :id} {:name "Pulse Name"
-                                                            :skip_if_empty false}]
-                      PulseCard             [_             {:pulse_id pulse-id
-                                                            :card_id  card-id-1
-                                                            :position 0}]
-                      PulseCard             [_             {:pulse_id pulse-id
-                                                            :card_id  card-id-2
-                                                            :position 1}]
-                      PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
-                      PulseChannelRecipient [_             {:user_id (rasta-id)
-                                                            :pulse_channel_id pc-id}]]
-  [true
-   {:subject "Pulse: Pulse Name"
-    :recipients [(:email (users/fetch-user :rasta))]
-    :message-type :attachments}
-   3
-   true
-   true]
-  (test-setup
-   (let [[result & no-more-results] (send-pulse! (retrieve-pulse pulse-id))]
-     [(empty? no-more-results)
-      (select-keys result [:subject :recipients :message-type])
-      (count (:message result))
-      (email-body? (first (:message result)))
-      (attachment? (second (:message result)))])))
+(expect
+  (rasta-pulse-email {:body [{"Pulse Name" true}
+                             png-attachment
+                             png-attachment]})
+  (tt/with-temp* [Card                 [{card-id-1 :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
+                  Card                 [{card-id-2 :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "day-of-week"]]})]
+                  Pulse                [{pulse-id :id} {:name "Pulse Name"
+                                                        :skip_if_empty false}]
+                  PulseCard             [_             {:pulse_id pulse-id
+                                                        :card_id  card-id-1
+                                                        :position 0}]
+                  PulseCard             [_             {:pulse_id pulse-id
+                                                        :card_id  card-id-2
+                                                        :position 1}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse pulse-id))
+     (et/summarize-multipart-email #"Pulse Name"))))
 
 ;; Pulse where the card has no results, but skip_if_empty is false, so should still send
-(tt/expect-with-temp [Card                  [{card-id :id}  (checkins-query {:filter   [">",["field-id" (data/id :checkins :date)],"2017-10-24"]
-                                                                             :breakout [["datetime-field" ["field-id" (data/id :checkins :date)] "hour"]]})]
-                      Pulse                 [{pulse-id :id} {:name          "Pulse Name"
-                                                             :skip_if_empty false}]
-                      PulseCard             [pulse-card     {:pulse_id pulse-id
-                                                             :card_id  card-id
-                                                             :position 0}]
-                      PulseChannel          [{pc-id :id}    {:pulse_id pulse-id}]
-                      PulseChannelRecipient [_              {:user_id          (rasta-id)
-                                                             :pulse_channel_id pc-id}]]
-  [true
-   {:subject      "Pulse: Pulse Name"
-    :recipients   [(:email (users/fetch-user :rasta))]
-    :message-type :attachments}
-   2
-   true
-   true]
-  (test-setup
-   (let [[result & no-more-results] (send-pulse! (retrieve-pulse pulse-id))]
-     [(empty? no-more-results)
-      (select-keys result [:subject :recipients :message-type])
-      (count (:message result))
-      (email-body? (first (:message result)))
-      (attachment? (second (:message result)))])))
+(expect
+  (rasta-pulse-email)
+  (tt/with-temp* [Card                  [{card-id :id}  (checkins-query {:filter   [">",["field-id" (data/id :checkins :date)],"2017-10-24"]
+                                                                         :breakout [["datetime-field" ["field-id" (data/id :checkins :date)] "hour"]]})]
+                  Pulse                 [{pulse-id :id} {:name          "Pulse Name"
+                                                         :skip_if_empty false}]
+                  PulseCard             [pulse-card     {:pulse_id pulse-id
+                                                         :card_id  card-id
+                                                         :position 0}]
+                  PulseChannel          [{pc-id :id}    {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_              {:user_id          (rasta-id)
+                                                         :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse pulse-id))
+     (et/summarize-multipart-email #"Pulse Name"))))
 
 ;; Pulse where the card has no results, skip_if_empty is true, so no pulse should be sent
-(tt/expect-with-temp [Card                  [{card-id :id}  (checkins-query {:filter   [">",["field-id" (data/id :checkins :date)],"2017-10-24"]
-                                                                             :breakout [["datetime-field" ["field-id" (data/id :checkins :date)] "hour"]]})]
-                      Pulse                 [{pulse-id :id} {:name          "Pulse Name"
-                                                             :skip_if_empty true}]
-                      PulseCard             [pulse-card     {:pulse_id pulse-id
-                                                             :card_id  card-id
-                                                             :position 0}]
-                      PulseChannel          [{pc-id :id}    {:pulse_id pulse-id}]
-                      PulseChannelRecipient [_              {:user_id          (rasta-id)
-                                                             :pulse_channel_id pc-id}]]
-  nil
-  (test-setup
-   (send-pulse! (retrieve-pulse pulse-id))))
+(expect
+  {}
+  (tt/with-temp* [Card                  [{card-id :id}  (checkins-query {:filter   [">",["field-id" (data/id :checkins :date)],"2017-10-24"]
+                                                                         :breakout [["datetime-field" ["field-id" (data/id :checkins :date)] "hour"]]})]
+                  Pulse                 [{pulse-id :id} {:name          "Pulse Name"
+                                                         :skip_if_empty true}]
+                  PulseCard             [pulse-card     {:pulse_id pulse-id
+                                                         :card_id  card-id
+                                                         :position 0}]
+                  PulseChannel          [{pc-id :id}    {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_              {:user_id          (rasta-id)
+                                                         :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse pulse-id))
+     @et/inbox)))
 
 ;; Rows alert with no data
-(tt/expect-with-temp [Card                  [{card-id :id}  (checkins-query {:filter   [">",["field-id" (data/id :checkins :date)],"2017-10-24"]
-                                                                             :breakout [["datetime-field" ["field-id" (data/id :checkins :date)] "hour"]]})]
-                      Pulse                 [{pulse-id :id} {:alert_condition  "rows"
-                                                             :alert_first_only false}]
-                      PulseCard             [pulse-card     {:pulse_id pulse-id
-                                                             :card_id  card-id
-                                                             :position 0}]
-                      PulseChannel          [{pc-id :id}    {:pulse_id pulse-id}]
-                      PulseChannelRecipient [_              {:user_id          (rasta-id)
-                                                             :pulse_channel_id pc-id}]]
-  nil
-  (test-setup
-   (send-pulse! (retrieve-pulse-or-alert pulse-id))))
-
-(defn- rows-email-body?
-  [{:keys [content] :as message}]
-  (boolean (re-find #"has results for you" content)))
-
-(defn- goal-above-email-body?
-  [{:keys [content] :as message}]
-  (boolean (re-find #"has reached" content)))
-
-(defn- goal-below-email-body?
-  [{:keys [content] :as message}]
-  (boolean (re-find #"has gone below" content)))
+(expect
+  {}
+  (tt/with-temp* [Card                  [{card-id :id}  (checkins-query {:filter   [">",["field-id" (data/id :checkins :date)],"2017-10-24"]
+                                                                         :breakout [["datetime-field" ["field-id" (data/id :checkins :date)] "hour"]]})]
+                  Pulse                 [{pulse-id :id} {:alert_condition  "rows"
+                                                         :alert_first_only false}]
+                  PulseCard             [pulse-card     {:pulse_id pulse-id
+                                                         :card_id  card-id
+                                                         :position 0}]
+                  PulseChannel          [{pc-id :id}    {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_              {:user_id          (rasta-id)
+                                                         :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse-or-alert pulse-id))
+     @et/inbox)))
 
-(defn- first-run-email-body?
-  [{:keys [content] :as message}]
-  (boolean (re-find #"stop sending you alerts" content)))
+(defn- rasta-alert-email
+  [subject email-body]
+  (et/email-to :rasta {:subject subject
+                       :body email-body}))
 
 ;; Rows alert with data
-(tt/expect-with-temp [Card                 [{card-id :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
-                      Pulse                [{pulse-id :id} {:alert_condition  "rows"
-                                                            :alert_first_only false}]
-                      PulseCard             [_             {:pulse_id pulse-id
-                                                            :card_id  card-id
-                                                            :position 0}]
-                      PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
-                      PulseChannelRecipient [_             {:user_id (rasta-id)
-                                                            :pulse_channel_id pc-id}]]
-  [true
-   {:subject "Metabase alert: Test card has results"
-    :recipients [(:email (users/fetch-user :rasta))]
-    :message-type :attachments}
-   2
-   true
-   true
-   true]
-  (test-setup
-   (let [[result & no-more-results] (send-pulse! (retrieve-pulse-or-alert pulse-id))]
-     [(empty? no-more-results)
-      (select-keys result [:subject :recipients :message-type])
-      (count (:message result))
-      (email-body? (first (:message result)))
-      (attachment? (second (:message result)))
-      (rows-email-body? (first (:message result)))])))
+(expect
+  (rasta-alert-email "Metabase alert: Test card has results"
+                     [{"Test card.*has results for you to see" true}, png-attachment])
+  (tt/with-temp* [Card                  [{card-id :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
+                  Pulse                 [{pulse-id :id} {:alert_condition  "rows"
+                                                         :alert_first_only false}]
+                  PulseCard             [_             {:pulse_id pulse-id
+                                                        :card_id  card-id
+                                                        :position 0}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse-or-alert pulse-id))
+     (et/summarize-multipart-email #"Test card.*has results for you to see"))))
 
 ;; Above goal alert with data
-(tt/expect-with-temp [Card                 [{card-id :id}  (merge (checkins-query {:filter   ["between",["field-id" (data/id :checkins :date)],"2014-04-01" "2014-06-01"]
-                                                                                   :breakout [["datetime-field" (data/id :checkins :date) "day"]]})
-                                                                  {:display :line
-                                                                   :visualization_settings {:graph.show_goal true :graph.goal_value 5.9}})]
-                      Pulse                [{pulse-id :id} {:alert_condition   "goal"
-                                                            :alert_first_only  false
-                                                            :alert_above_goal  true}]
-                      PulseCard             [_             {:pulse_id pulse-id
-                                                            :card_id  card-id
-                                                            :position 0}]
-                      PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
-                      PulseChannelRecipient [_             {:user_id          (rasta-id)
-                                                            :pulse_channel_id pc-id}]]
-  [true
-   {:subject      "Metabase alert: Test card has reached its goal"
-    :recipients   [(:email (users/fetch-user :rasta))]
-    :message-type :attachments}
-   2
-   true
-   true
-   true]
-  (test-setup
-   (let [[result & no-more-results] (send-pulse! (retrieve-pulse-or-alert pulse-id))]
-     [(empty? no-more-results)
-      (select-keys result [:subject :recipients :message-type])
-      (count (:message result))
-      (email-body? (first (:message result)))
-      (attachment? (second (:message result)))
-      (goal-above-email-body? (first (:message result)))])))
+(expect
+  (rasta-alert-email "Metabase alert: Test card has reached its goal"
+                     [{"Test card.*has reached its goal" true}, png-attachment])
+  (tt/with-temp* [Card                  [{card-id :id}  (merge (checkins-query {:filter   ["between",["field-id" (data/id :checkins :date)],"2014-04-01" "2014-06-01"]
+                                                                                :breakout [["datetime-field" (data/id :checkins :date) "day"]]})
+                                                               {:display :line
+                                                                :visualization_settings {:graph.show_goal true :graph.goal_value 5.9}})]
+                  Pulse                 [{pulse-id :id} {:alert_condition   "goal"
+                                                         :alert_first_only  false
+                                                         :alert_above_goal  true}]
+                  PulseCard             [_             {:pulse_id pulse-id
+                                                        :card_id  card-id
+                                                        :position 0}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id          (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse-or-alert pulse-id))
+     (et/summarize-multipart-email #"Test card.*has reached its goal"))))
 
 ;; Above goal alert, with no data above goal
-(tt/expect-with-temp [Card                 [{card-id :id}  (merge (checkins-query {:filter   ["between",["field-id" (data/id :checkins :date)],"2014-02-01" "2014-04-01"]
-                                                                                   :breakout [["datetime-field" (data/id :checkins :date) "day"]]})
-                                                                  {:display :area
-                                                                   :visualization_settings {:graph.show_goal true :graph.goal_value 5.9}})]
-                      Pulse                [{pulse-id :id} {:alert_condition   "goal"
-                                                            :alert_first_only  false
-                                                            :alert_above_goal  true}]
-                      PulseCard             [_             {:pulse_id pulse-id
-                                                            :card_id  card-id
-                                                            :position 0}]
-                      PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
-                      PulseChannelRecipient [_             {:user_id          (rasta-id)
-                                                            :pulse_channel_id pc-id}]]
-  nil
-  (test-setup
-   (send-pulse! (retrieve-pulse-or-alert pulse-id))))
+(expect
+  {}
+  (tt/with-temp* [Card                  [{card-id :id}  (merge (checkins-query {:filter   ["between",["field-id" (data/id :checkins :date)],"2014-02-01" "2014-04-01"]
+                                                                                :breakout [["datetime-field" (data/id :checkins :date) "day"]]})
+                                                               {:display :area
+                                                                :visualization_settings {:graph.show_goal true :graph.goal_value 5.9}})]
+                  Pulse                 [{pulse-id :id} {:alert_condition   "goal"
+                                                         :alert_first_only  false
+                                                         :alert_above_goal  true}]
+                  PulseCard             [_             {:pulse_id pulse-id
+                                                        :card_id  card-id
+                                                        :position 0}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id          (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse-or-alert pulse-id))
+     @et/inbox)))
 
 ;; Below goal alert with no satisfying data
-(tt/expect-with-temp [Card                 [{card-id :id}  (merge (checkins-query {:filter   ["between",["field-id" (data/id :checkins :date)],"2014-02-10" "2014-02-12"]
-                                                                                   :breakout [["datetime-field" (data/id :checkins :date) "day"]]})
-                                                                  {:display :bar
-                                                                   :visualization_settings {:graph.show_goal true :graph.goal_value 1.1}})]
-                      Pulse                [{pulse-id :id} {:alert_condition   "goal"
-                                                            :alert_first_only  false
-                                                            :alert_above_goal  false}]
-                      PulseCard             [_             {:pulse_id pulse-id
-                                                            :card_id  card-id
-                                                            :position 0}]
-                      PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
-                      PulseChannelRecipient [_             {:user_id          (rasta-id)
-                                                            :pulse_channel_id pc-id}]]
-  nil
-  (test-setup
-   (send-pulse! (retrieve-pulse-or-alert pulse-id))))
+(expect
+  {}
+  (tt/with-temp* [Card                  [{card-id :id}  (merge (checkins-query {:filter   ["between",["field-id" (data/id :checkins :date)],"2014-02-10" "2014-02-12"]
+                                                                                :breakout [["datetime-field" (data/id :checkins :date) "day"]]})
+                                                               {:display :bar
+                                                                :visualization_settings {:graph.show_goal true :graph.goal_value 1.1}})]
+                  Pulse                 [{pulse-id :id} {:alert_condition   "goal"
+                                                         :alert_first_only  false
+                                                         :alert_above_goal  false}]
+                  PulseCard             [_             {:pulse_id pulse-id
+                                                        :card_id  card-id
+                                                        :position 0}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id          (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse-or-alert pulse-id))
+     @et/inbox)))
 
 ;; Below goal alert with data
-(tt/expect-with-temp [Card                 [{card-id :id}  (merge (checkins-query {:filter   ["between",["field-id" (data/id :checkins :date)],"2014-02-12" "2014-02-17"]
-                                                                                   :breakout [["datetime-field" (data/id :checkins :date) "day"]]})
-                                                                  {:display                :line
-                                                                   :visualization_settings {:graph.show_goal true :graph.goal_value 1.1}})]
-                      Pulse                [{pulse-id :id} {:alert_condition   "goal"
-                                                            :alert_first_only  false
-                                                            :alert_above_goal  false}]
-                      PulseCard             [_             {:pulse_id pulse-id
-                                                            :card_id  card-id
-                                                            :position 0}]
-                      PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
-                      PulseChannelRecipient [_             {:user_id          (rasta-id)
-                                                            :pulse_channel_id pc-id}]]
-  [true
-   {:subject      "Metabase alert: Test card has gone below its goal"
-    :recipients   [(:email (users/fetch-user :rasta))]
-    :message-type :attachments}
-   2
-   true
-   true
-   true]
-  (test-setup
-   (let [[result & no-more-results] (send-pulse! (retrieve-pulse-or-alert pulse-id))]
-     [(empty? no-more-results)
-      (select-keys result [:subject :recipients :message-type])
-      (count (:message result))
-      (email-body? (first (:message result)))
-      (attachment? (second (:message result)))
-      (goal-below-email-body? (first (:message result)))])))
+(expect
+  (rasta-alert-email "Metabase alert: Test card has gone below its goal"
+                     [{"Test card.*has gone below its goal of 1.1" true}, png-attachment])
+  (tt/with-temp* [Card                  [{card-id :id}  (merge (checkins-query {:filter   ["between",["field-id" (data/id :checkins :date)],"2014-02-12" "2014-02-17"]
+                                                                                :breakout [["datetime-field" (data/id :checkins :date) "day"]]})
+                                                               {:display                :line
+                                                                :visualization_settings {:graph.show_goal true :graph.goal_value 1.1}})]
+                  Pulse                 [{pulse-id :id} {:alert_condition   "goal"
+                                                         :alert_first_only  false
+                                                         :alert_above_goal  false}]
+                  PulseCard             [_             {:pulse_id pulse-id
+                                                        :card_id  card-id
+                                                        :position 0}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id          (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+
+    (email-test-setup
+     (send-pulse! (retrieve-pulse-or-alert pulse-id))
+     (et/summarize-multipart-email #"Test card.*has gone below its goal of 1.1"))))
 
 (defn- thunk->boolean [{:keys [attachments] :as result}]
   (assoc result :attachments (for [attachment-info attachments]
@@ -362,7 +313,7 @@
      :attachment-name "image.png",
      :channel-id "FOO",
      :fallback "Test card"}]}
-  (test-setup
+  (slack-test-setup
    (-> (send-pulse! (retrieve-pulse pulse-id))
        first
        thunk->boolean)))
@@ -402,11 +353,21 @@
       :channel-id "FOO",
       :fallback "Test card 2"}]}
    true]
-  (test-setup
+  (slack-test-setup
    (let [[slack-data] (send-pulse! (retrieve-pulse pulse-id))]
      [(thunk->boolean slack-data)
       (every? produces-bytes? (:attachments slack-data))])))
 
+(defn- email-body? [{message-type :type content :content}]
+  (and (= "text/html; charset=utf-8" message-type)
+       (string? content)
+       (.startsWith content "<html>")))
+
+(defn- attachment? [{message-type :type content-type :content-type content :content}]
+  (and (= :inline message-type)
+       (= "image/png" content-type)
+       (instance? java.io.File content)))
+
 ;; Test with a slack channel and an email
 (tt/expect-with-temp [Card                  [{card-id :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
                       Pulse                 [{pulse-id :id} {:name "Pulse Name"
@@ -435,7 +396,7 @@
    2
    true
    true]
-  (test-setup
+  (slack-test-setup
    (let [pulse-data (send-pulse! (retrieve-pulse pulse-id))
          slack-data (m/find-first #(contains? % :channel-id) pulse-data)
          email-data (m/find-first #(contains? % :subject) pulse-data)]
@@ -463,7 +424,7 @@
                    :attachment-name "image.png", :channel-id "FOO",
                    :fallback "Test card"}]}
    true]
-  (test-setup
+  (slack-test-setup
    (let [[result] (send-pulse! (retrieve-pulse-or-alert pulse-id))]
      [(thunk->boolean result)
       (every? produces-bytes? (:attachments result))])))
@@ -477,104 +438,78 @@
 
 ;; Above goal alert with a progress bar
 (expect
-  [true
-   {:subject      "Metabase alert: Test card has reached its goal"
-    :recipients   [(:email (users/fetch-user :rasta))]
-    :message-type :attachments}
-   1
-   true]
-  (test-setup
-   (tt/with-temp* [Card                 [{card-id :id}  (merge (venues-query "max")
-                                                               {:display                :progress
-                                                                :visualization_settings {:progress.goal 3}})]
-                   Pulse                [{pulse-id :id} {:alert_condition   "goal"
-                                                         :alert_first_only  false
-                                                         :alert_above_goal  true}]
-                   PulseCard             [_             {:pulse_id pulse-id
-                                                         :card_id  card-id
-                                                         :position 0}]
-                   PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
-                   PulseChannelRecipient [_             {:user_id          (rasta-id)
-                                                         :pulse_channel_id pc-id}]]
-     (let [[result & no-more-results] (send-pulse! (retrieve-pulse-or-alert pulse-id))]
-       [(empty? no-more-results)
-        (select-keys result [:subject :recipients :message-type])
-        ;; The pulse code interprets progress graphs as just a scalar, so there are no attachments
-        (count (:message result))
-        (email-body? (first (:message result)))]))))
+  (rasta-alert-email "Metabase alert: Test card has reached its goal"
+                     [{"Test card.*has reached its goal of 3" true}])
+  (tt/with-temp* [Card                 [{card-id :id}  (merge (venues-query "max")
+                                                              {:display                :progress
+                                                               :visualization_settings {:progress.goal 3}})]
+                  Pulse                [{pulse-id :id} {:alert_condition   "goal"
+                                                        :alert_first_only  false
+                                                        :alert_above_goal  true}]
+                  PulseCard             [_             {:pulse_id pulse-id
+                                                        :card_id  card-id
+                                                        :position 0}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id          (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse-or-alert pulse-id))
+     (et/summarize-multipart-email #"Test card.*has reached its goal of 3"))))
 
 ;; Below goal alert with progress bar
 (expect
-  [true
-   {:subject      "Metabase alert: Test card has gone below its goal"
-    :recipients   [(:email (users/fetch-user :rasta))]
-    :message-type :attachments}
-   1
-   true
-   false]
-  (test-setup
-   (tt/with-temp* [Card                 [{card-id :id}  (merge (venues-query "min")
-                                                               {:display                :progress
-                                                                :visualization_settings {:progress.goal 2}})]
-                   Pulse                [{pulse-id :id} {:alert_condition   "goal"
-                                                         :alert_first_only  false
-                                                         :alert_above_goal  false}]
-                   PulseCard             [_             {:pulse_id pulse-id
-                                                         :card_id  card-id
-                                                         :position 0}]
-                   PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
-                   PulseChannelRecipient [_             {:user_id          (rasta-id)
-                                                         :pulse_channel_id pc-id}]]
-     (let [[result & no-more-results] (send-pulse! (retrieve-pulse-or-alert pulse-id))]
-       [(empty? no-more-results)
-        (select-keys result [:subject :recipients :message-type])
-        (count (:message result))
-        (email-body? (first (:message result)))
-        (first-run-email-body? (first (:message result)))]))))
-
+  (rasta-alert-email "Metabase alert: Test card has gone below its goal"
+                     [{"Test card.*has gone below its goal of 2" true}])
+  (tt/with-temp* [Card                 [{card-id :id}  (merge (venues-query "min")
+                                                              {:display                :progress
+                                                               :visualization_settings {:progress.goal 2}})]
+                  Pulse                [{pulse-id :id} {:alert_condition   "goal"
+                                                        :alert_first_only  false
+                                                        :alert_above_goal  false}]
+                  PulseCard             [_             {:pulse_id pulse-id
+                                                        :card_id  card-id
+                                                        :position 0}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id          (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse-or-alert pulse-id))
+     (et/summarize-multipart-email #"Test card.*has gone below its goal of 2"))))
 
 ;; Rows alert, first run only with data
 (expect
-  [true
-   {:subject "Metabase alert: Test card has results"
-    :recipients [(:email (users/fetch-user :rasta))]
-    :message-type :attachments}
-   2
-   true
-   true
-   true
-   false]
-  (test-setup
-   (tt/with-temp* [Card                 [{card-id :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
-                   Pulse                [{pulse-id :id} {:alert_condition  "rows"
+  (rasta-alert-email "Metabase alert: Test card has results"
+                     [{"Test card.*has results for you to see" true
+                       "stop sending you alerts"               true}
+                      png-attachment])
+  (tt/with-temp* [Card                  [{card-id :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
+                  Pulse                 [{pulse-id :id} {:alert_condition  "rows"
                                                          :alert_first_only true}]
-                   PulseCard             [_             {:pulse_id pulse-id
+                  PulseCard             [_             {:pulse_id pulse-id
+                                                        :card_id  card-id
+                                                        :position 0}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id          (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse-or-alert pulse-id))
+     (et/summarize-multipart-email #"Test card.*has results for you to see"
+                                   #"stop sending you alerts"))))
+
+;; First run alert with no data
+(expect
+  [{} true]
+  (tt/with-temp* [Card                  [{card-id :id}  (checkins-query {:filter   [">",["field-id" (data/id :checkins :date)],"2017-10-24"]
+                                                                         :breakout [["datetime-field" ["field-id" (data/id :checkins :date)] "hour"]]})]
+                  Pulse                 [{pulse-id :id} {:alert_condition  "rows"
+                                                         :alert_first_only true}]
+                  PulseCard             [pulse-card     {:pulse_id pulse-id
                                                          :card_id  card-id
                                                          :position 0}]
-                   PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
-                   PulseChannelRecipient [_             {:user_id (rasta-id)
+                  PulseChannel          [{pc-id :id}    {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_              {:user_id          (rasta-id)
                                                          :pulse_channel_id pc-id}]]
-     (let [[result & no-more-results] (send-pulse! (retrieve-pulse-or-alert pulse-id))]
-       [(empty? no-more-results)
-        (select-keys result [:subject :recipients :message-type])
-        (count (:message result))
-        (email-body? (first (:message result)))
-        (first-run-email-body? (first (:message result)))
-        (attachment? (second (:message result)))
-        (db/exists? Pulse :id pulse-id)]))))
-
-;; First run alert with no data
-(tt/expect-with-temp [Card                  [{card-id :id}  (checkins-query {:filter   [">",["field-id" (data/id :checkins :date)],"2017-10-24"]
-                                                                             :breakout [["datetime-field" ["field-id" (data/id :checkins :date)] "hour"]]})]
-                      Pulse                 [{pulse-id :id} {:alert_condition  "rows"
-                                                             :alert_first_only true}]
-                      PulseCard             [pulse-card     {:pulse_id pulse-id
-                                                             :card_id  card-id
-                                                             :position 0}]
-                      PulseChannel          [{pc-id :id}    {:pulse_id pulse-id}]
-                      PulseChannelRecipient [_              {:user_id          (rasta-id)
-                                                             :pulse_channel_id pc-id}]]
-  [nil true]
-  (test-setup
-   [(send-pulse! (retrieve-pulse-or-alert pulse-id))
-    (db/exists? Pulse :id pulse-id)]))
+    (email-test-setup
+     (send-pulse! (retrieve-pulse-or-alert pulse-id))
+     [@et/inbox
+      (db/exists? Pulse :id pulse-id)])))
diff --git a/test/metabase/test/async.clj b/test/metabase/test/async.clj
index 8e47267277432f742703070adfa09dc39b82ebd7..b3ee3cf2e43c77c2583bad8c001f33bf6bc3d19a 100644
--- a/test/metabase/test/async.clj
+++ b/test/metabase/test/async.clj
@@ -1,22 +1,34 @@
 (ns metabase.test.async
   "Utilities for testing async API endpoints."
   (:require [clojure.tools.logging :as log]
-            [metabase.test.data.users :refer :all]))
+            [metabase.feature-extraction.async :as async]
+            [metabase.models.computation-job :refer [ComputationJob]]))
 
-(def ^:private ^:const max-retries 20)
+(def ^:dynamic ^Integer *max-while-runtime*
+  "Maximal time in milliseconds `while-with-timeout` runs."
+  10000000)
 
-(defn call-with-retries
-  "Retries fetching job results until `max-retries` times."
-  [user job-id]
-  (loop [tries 1]
-    (let [{:keys [status result] :as response}
-          ((user->client user) :get 200 (str "async/" job-id))]
-      (cond
-        (= status "done")     result
-        (= status "error")    (throw (ex-info (str "Error encountered.\n" result)
-                                       result))
-        (> tries max-retries) (throw (ex-info "Timeout. Max retries exceeded." {}))
-        :else                 (do
-                                (log/info (format "Waiting for computation to finish. Retry: %" tries))
-                                (Thread/sleep (* 100 tries))
-                                (recur (inc tries)))))))
+(defmacro while-with-timeout
+  "Like `clojure.core/while` except it runs a maximum of `*max-while-runtime*`
+   milliseconds (assuming running time for one iteration is << `*max-while-runtime*`)."
+  [test & body]
+  `(let [start# (System/currentTimeMillis)]
+     (while (and ~test
+                 (< (- (System/currentTimeMillis) start#) *max-while-runtime*))
+       ~@body)
+     (when (>= (- (System/currentTimeMillis) start#) *max-while-runtime*)
+       (log/warn "While loop terminated due to exceeded max runtime."))))
+
+(defn result!
+  "Blocking version of async/result."
+  [job-id]
+  (when-let [f (-> #'async/running-jobs
+                   deref                  ; var
+                   deref                  ; atom
+                   (get job-id))]
+    (when-not (or (future-cancelled? f)
+                  (future-done? f))
+      @f))
+  ; Wait for the transaction to finish
+  (while-with-timeout (-> job-id ComputationJob async/running?))
+  (async/result (ComputationJob job-id)))
diff --git a/test/metabase/test/data.clj b/test/metabase/test/data.clj
index 7050b89e90330065ccc5cdd8151e739eb3a3ed66..45a6c71650832a49541ed376e939931f64130147 100644
--- a/test/metabase/test/data.clj
+++ b/test/metabase/test/data.clj
@@ -99,7 +99,7 @@
   `(ql/query (ql/source-table (id ~(keyword table)))
              ~@(map (partial $->id (keyword table)) forms)))
 
-(s/defn ^:always-validate wrap-inner-query
+(s/defn wrap-inner-query
   "Wrap inner QUERY with `:database` ID and other 'outer query' kvs. DB ID is fetched by looking up the Database for the query's `:source-table`."
   {:style/indent 0}
   [query :- qi/Query]
@@ -107,7 +107,7 @@
    :type     :query
    :query    query})
 
-(s/defn ^:always-validate run-query*
+(s/defn run-query*
   "Call `driver/process-query` on expanded inner QUERY, looking up the `Database` ID for the `source-table.`
 
      (run-query* (query (source-table 5) ...))"