diff --git a/.eslintignore b/.eslintignore
index c742b08c00540c1f31a99d296d17214a9e2e9bbb..1605e483e9e6387642a27955fd97013226f23212 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -7,3 +7,4 @@
 /vendor/
 karma.config.js
 webpack.config.js
+/app/assets/javascripts/locale/**/*.js
diff --git a/Gemfile b/Gemfile
index 6c1f63460715440111130ac27ad0345e905edc81..736e3e3b5784937ef3c974034269550f7a6e41f3 100644
--- a/Gemfile
+++ b/Gemfile
@@ -256,6 +256,7 @@ gem 'premailer-rails', '~> 1.9.0'
 
 # I18n
 gem 'gettext_i18n_rails', '~> 1.8.0'
+gem 'gettext_i18n_rails_js', '~> 1.2.0'
 gem 'gettext', '~> 3.2.2', require: false, group: :development
 
 # Metrics
diff --git a/Gemfile.lock b/Gemfile.lock
index ca4084e18a232e01931624d5859f8e9376a9bb51..94727810ac9e2ffd8e82a258be73d7b8028d1728 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -259,6 +259,11 @@ GEM
       text (>= 1.3.0)
     gettext_i18n_rails (1.8.0)
       fast_gettext (>= 0.9.0)
+    gettext_i18n_rails_js (1.2.0)
+      gettext (>= 3.0.2)
+      gettext_i18n_rails (>= 0.7.1)
+      po_to_json (>= 1.0.0)
+      rails (>= 3.2.0)
     gherkin-ruby (0.3.2)
     gitaly (0.5.0)
       google-protobuf (~> 3.1)
@@ -534,6 +539,8 @@ GEM
       ast (~> 2.2)
     path_expander (1.0.1)
     pg (0.18.4)
+    po_to_json (1.0.1)
+      json (>= 1.6.0)
     poltergeist (1.9.0)
       capybara (~> 2.1)
       cliver (~> 0.3.1)
@@ -914,6 +921,7 @@ DEPENDENCIES
   gemojione (~> 3.0)
   gettext (~> 3.2.2)
   gettext_i18n_rails (~> 1.8.0)
+  gettext_i18n_rails_js (~> 1.2.0)
   gitaly (~> 0.5.0)
   github-linguist (~> 4.7.0)
   gitlab-flowdock-git-hook (~> 1.0.1)
diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js
index abe48572347b07332cedb2eba71c1ff8c215827b..63e20478e9494f286abe2c4799cc7a09e32287ba 100644
--- a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js
@@ -11,7 +11,7 @@ export default {
           aria-hidden="true"
           title="Limited to showing 50 events at most"
           data-placement="top"></i>
-      Showing 50 events
+      {{ 'Showing %d event' | translate-plural('Showing %d events', 50) }}
     </span>
   `,
 };
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
index 80bd2df6f42eaf1fb91c6f72d4ed4129002a8b99..f72882872cdf1a76949576acfc92a6a934e156cc 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
@@ -28,11 +28,11 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({
             <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
             &middot;
             <span>
-              Opened
+              {{ 'Opened' | translate }}
               <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
             </span>
             <span>
-              by
+              {{ 'by' | translate }}
               <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
             </span>
           </div>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
index 20a43798fbedc752ca528f5523cba5dd74006867..bb265c8316feb957f506f39dc9fc2c4298e5189f 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
@@ -28,11 +28,11 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({
             <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
             &middot;
             <span>
-              Opened
+              {{ 'Opened' | translate }}
               <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
             </span>
             <span>
-              by
+              {{ 'by' | translate }}
               <a :href="issue.author.webUrl" class="issue-author-link">
                 {{ issue.author.name }}
               </a>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
index f33cac3da8248131689a98764d158f1f47c19c46..32b685faece6af262b0cec3e2c9034d769f64cfe 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
@@ -31,10 +31,10 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({
               </a>
             </h5>
             <span>
-              First
+              {{ 'First' | translate }}
               <span class="commit-icon">${iconCommit}</span>
               <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
-              pushed by
+              {{ 'pushed by' | translate }}
               <a :href="commit.author.webUrl" class="commit-author-link">
                 {{ commit.author.name }}
               </a>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
index 657f538537433799d46416188dcbade6875f3e85..5c9186a2e497cad268094d3ba43c7828ee1b08e0 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
@@ -28,11 +28,11 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({
             <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
             &middot;
             <span>
-              Opened
+              {{ 'Opened' | translate }}
               <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
             </span>
             <span>
-            by
+            {{ 'by' | translate }}
             <a :href="issue.author.webUrl" class="issue-author-link">
               {{ issue.author.name }}
             </a>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
index 8a801300647aac189d99c940f971bb771890b44b..a047573548df09c3300f9cb3058c48b290a8e26f 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
@@ -28,11 +28,11 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({
             <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
             &middot;
             <span>
-              Opened
+              {{ 'Opened' | translate }}
               <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
             </span>
             <span>
-              by
+              {{ 'by' | translate }}
               <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
             </span>
             <template v-if="mergeRequest.state === 'closed'">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
index 4a28637958848ce2fff20c3c63139226b90e489a..e8dfaf4294e192b979f5c0a82a9a63c4ee774fdd 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
@@ -32,7 +32,7 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({
             </h5>
             <span>
               <a :href="build.url" class="build-date">{{ build.date }}</a>
-              by
+              {{ 'by' | translate }}
               <a :href="build.author.webUrl" class="issue-author-link">
                 {{ build.author.name }}
               </a>
diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js b/app/assets/javascripts/cycle_analytics/components/total_time_component.js
index 77edcb7627323eaa7f22b02b6fe969b095a10490..a0d735f159ce01ded646fcc88e0062aa5aa3be54 100644
--- a/app/assets/javascripts/cycle_analytics/components/total_time_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js
@@ -12,9 +12,9 @@ global.cycleAnalytics.TotalTimeComponent = Vue.extend({
   template: `
     <span class="total-time">
       <template v-if="Object.keys(time).length">
-        <template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template>
-        <template v-if="time.hours">{{ time.hours }} <span>hr</span></template>
-        <template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template>
+        <template v-if="time.days">{{ time.days }} <span>{{ 'day' | translate-plural('days', time.days) }}</span></template>
+        <template v-if="time.hours">{{ time.hours }} <span v-translate>hr</span></template>
+        <template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ 'min' | translate-plural('mins', time.mins) }}</span></template>
         <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template>
       </template>
       <template v-else>
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 48cab437e02e679880300d2e474d8457ae6c02b0..c8e53cb554eb2bf7f50a861896a6ada312cf4ef2 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -2,6 +2,7 @@
 
 import Vue from 'vue';
 import Cookies from 'js-cookie';
+import Translate from '../vue_shared/translate';
 import LimitWarningComponent from './components/limit_warning_component';
 
 require('./components/stage_code_component');
@@ -16,6 +17,8 @@ require('./cycle_analytics_service');
 require('./cycle_analytics_store');
 require('./default_event_objects');
 
+Vue.use(Translate);
+
 $(() => {
   const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
   const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
index 6536a8fd7fa286a657afeebd765e8e05fde1f835..25d5092a1fd8e7fe5e16f1f88d33804f404f4de3 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
@@ -1,5 +1,7 @@
 /* eslint-disable no-param-reassign */
 
+import locale from '~/locale';
+
 require('../lib/utils/text_utility');
 const DEFAULT_EVENT_OBJECTS = require('./default_event_objects');
 
@@ -7,13 +9,13 @@ const global = window.gl || (window.gl = {});
 global.cycleAnalytics = global.cycleAnalytics || {};
 
 const EMPTY_STAGE_TEXTS = {
-  issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
-  plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
-  code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
-  test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
-  review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
-  staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
-  production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
+  issue: locale.gettext('The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.'),
+  plan: locale.gettext('The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.'),
+  code: locale.gettext('The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.'),
+  test: locale.gettext('The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.'),
+  review: locale.gettext('The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.'),
+  staging: locale.gettext('The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.'),
+  production: locale.gettext('The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.'),
 };
 
 global.cycleAnalytics.CycleAnalyticsStore = {
diff --git a/app/assets/javascripts/locale/de/app.js b/app/assets/javascripts/locale/de/app.js
new file mode 100644
index 0000000000000000000000000000000000000000..643e82a90a073d2e2af0799a9b7e0be7004fb6c0
--- /dev/null
+++ b/app/assets/javascripts/locale/de/app.js
@@ -0,0 +1 @@
+var locales = locales || {}; locales['de'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-12 22:37-0500","Last-Translator":"FULL NAME <EMAIL@ADDRESS>","Language-Team":"German","Language":"de","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","lang":"de","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"Deutsch":[""],"English":[""],"Spanish":[""]}}};
\ No newline at end of file
diff --git a/app/assets/javascripts/locale/en/app.js b/app/assets/javascripts/locale/en/app.js
new file mode 100644
index 0000000000000000000000000000000000000000..9070b519ff330853edbe8d721974d5861bcf996c
--- /dev/null
+++ b/app/assets/javascripts/locale/en/app.js
@@ -0,0 +1 @@
+var locales = locales || {}; locales['en'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-12 22:36-0500","Last-Translator":"FULL NAME <EMAIL@ADDRESS>","Language-Team":"English","Language":"en","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","lang":"en","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"Deutsch":[""],"English":[""],"Spanish":[""]}}};
diff --git a/app/assets/javascripts/locale/es/app.js b/app/assets/javascripts/locale/es/app.js
new file mode 100644
index 0000000000000000000000000000000000000000..41f6ddef5b868dadad790f3545eda577896c2182
--- /dev/null
+++ b/app/assets/javascripts/locale/es/app.js
@@ -0,0 +1 @@
+var locales = locales || {}; locales['es'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-13 00:07-0500","Language-Team":"Spanish","Language":"es","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"","X-Generator":"Poedit 2.0.1","lang":"es","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"Deutsch":["Alemán"],"English":["Inglés"],"Spanish":["Español"]}}};
diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..56791968e537563f8acaf45fc3025c5c047bc494
--- /dev/null
+++ b/app/assets/javascripts/locale/index.js
@@ -0,0 +1,15 @@
+import Jed from 'jed';
+import de from './de/app';
+import es from './es/app';
+import en from './en/app';
+
+const locales = {
+  de,
+  es,
+  en,
+};
+
+const lang = document.querySelector('html').getAttribute('lang') || 'en';
+
+export { lang };
+export default new Jed(locales[lang]);
diff --git a/app/assets/javascripts/vue_shared/translate.js b/app/assets/javascripts/vue_shared/translate.js
new file mode 100644
index 0000000000000000000000000000000000000000..072828b310ece4f132a1206e7d452b5501960ba8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/translate.js
@@ -0,0 +1,17 @@
+import locale from '../locale';
+
+export default (Vue) => {
+  Vue.filter('translate', text => locale.gettext(text));
+
+  Vue.filter('translate-plural', (text, pluralText, count) =>
+    locale.ngettext(text, pluralText, count).replace(/%d/g, count));
+
+  Vue.directive('translate', {
+    bind(el) {
+      const $el = el;
+      const text = $el.textContent.trim();
+
+      $el.textContent = locale.gettext(text);
+    },
+  });
+};
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 36543edc040f91e562494ecea3a5a441a7279b2d..dc926a615c76b213ca03c37765e7a6785b0ddbd0 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,5 +1,5 @@
 !!! 5
-%html{ lang: "en", class: "#{page_class}" }
+%html{ lang: I18n.locale, class: "#{page_class}" }
   = render "layouts/head"
   %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
     = Gon::Base.render_data
diff --git a/app/views/projects/cycle_analytics/_empty_stage.html.haml b/app/views/projects/cycle_analytics/_empty_stage.html.haml
index c3f95860e92375b46db2a01347c642a36869df2d..27190785fff2dd1014e537cbbbbf2e26c7e5950a 100644
--- a/app/views/projects/cycle_analytics/_empty_stage.html.haml
+++ b/app/views/projects/cycle_analytics/_empty_stage.html.haml
@@ -2,6 +2,6 @@
   .empty-stage
     .icon-no-data
       = custom_icon ('icon_no_data')
-    %h4 We don't have enough data to show this stage.
+    %h4 {{ 'We don\'t have enough data to show this stage.' | translate }}
     %p
       {{currentStage.emptyStageText}}
diff --git a/app/views/projects/cycle_analytics/_no_access.html.haml b/app/views/projects/cycle_analytics/_no_access.html.haml
index 0ffc79b318122803db8239817de6cab43108845d..474d0f410a704b2d720e95bd6e4a08e8bfac3096 100644
--- a/app/views/projects/cycle_analytics/_no_access.html.haml
+++ b/app/views/projects/cycle_analytics/_no_access.html.haml
@@ -2,6 +2,6 @@
   .no-access-stage
     .icon-lock
       = custom_icon ('icon_lock')
-    %h4 You need permission.
+    %h4 {{ 'You need permission.' | translate }}
     %p
-      Want to see the data? Please ask administrator for access.
+      {{ 'Want to see the data? Please ask administrator for access.' | translate }}
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index dd3fa814716ea390509549d3ca1c9120c1aa06c9..a7783b6958803bd867379b490ed9c332b88ddb7e 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -2,6 +2,7 @@
 - page_title "Cycle Analytics"
 - content_for :page_specific_javascripts do
   = page_specific_javascript_bundle_tag('common_vue')
+  = page_specific_javascript_bundle_tag('locale')
   = page_specific_javascript_bundle_tag('cycle_analytics')
 
 = render "projects/head"
@@ -15,16 +16,17 @@
           = custom_icon('icon_cycle_analytics_splash')
         .col-sm-8.col-xs-12.inner-content
           %h4
-            Introducing Cycle Analytics
+            {{ 'Introducing Cycle Analytics' | translate }}
           %p
-            Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
+            {{ 'Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.' | translate }}
 
-          = link_to "Read more",  help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
+          = link_to help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn' do
+            {{ 'Read more' | translate }}
   = icon("spinner spin", "v-show" => "isLoading")
   .wrapper{ "v-show" => "!isLoading && !hasError" }
     .panel.panel-default
       .panel-heading
-        Pipeline Health
+        {{ 'Pipeline Health' | translate }}
       .content-block
         .container-fluid
           .row
@@ -34,15 +36,15 @@
             .col-sm-3.col-xs-12.column
               .dropdown.inline.js-ca-dropdown
                 %button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" }
-                  %span.dropdown-label Last 30 days
+                  %span.dropdown-label {{ 'Last 30 days' | translate }}
                   %i.fa.fa-chevron-down
                 %ul.dropdown-menu.dropdown-menu-align-right
                   %li
                     %a{ "href" => "#", "data-value" => "30" }
-                      Last 30 days
+                      {{ 'Last 30 days' | translate }}
                   %li
                     %a{ "href" => "#", "data-value" => "90" }
-                      Last 90 days
+                      {{ 'Last 90 days' | translate }}
     .stage-panel-container
       .panel.panel-default.stage-panel
         .panel-heading
@@ -50,19 +52,19 @@
             %ul
               %li.stage-header
                 %span.stage-name
-                  Stage
+                  {{ 'Stage' | translate }}
                 %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The phase of the development lifecycle.", "aria-hidden" => "true" }
               %li.median-header
                 %span.stage-name
-                  Median
+                  {{ 'Median' | translate }}
                 %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.", "aria-hidden" => "true" }
               %li.event-header
                 %span.stage-name
-                  {{ currentStage ? currentStage.legend : 'Related Issues' }}
+                  {{ currentStage ? currentStage.legend : 'Related Issues' | translate }}
                 %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The collection of events added to the data gathered for that stage.", "aria-hidden" => "true" }
               %li.total-time-header
                 %span.stage-name
-                  Total Time
+                  {{ 'Total Time' | translate }}
                 %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The time taken by each data entry gathered by that stage.", "aria-hidden" => "true" }
         .stage-panel-body
           %nav.stage-nav
@@ -75,10 +77,10 @@
                     %span{ "v-if" => "stage.value" }
                       {{ stage.value }}
                     %span.stage-empty{ "v-else" => true }
-                      Not enough data
+                      {{ 'Not enough data' | translate }}
                   %template{ "v-else" => true }
                     %span.not-available
-                      Not available
+                      {{ 'Not available' | translate }}
           .section.stage-events
             %template{ "v-if" => "isLoadingStage" }
               = icon("spinner spin")
diff --git a/config/webpack.config.js b/config/webpack.config.js
index cb0a57a3a410fe77a0e7045a20b22ab04881abc1..b6fe8c6f565e84d628aa9aa1cbec66dc7bbce84f 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -35,6 +35,7 @@ var config = {
     groups_list:          './groups_list.js',
     issuable:             './issuable/issuable_bundle.js',
     issue_show:           './issue_show/index.js',
+    locale:               './locale/index.js',
     main:                 './main.js',
     merge_conflicts:      './merge_conflicts/merge_conflicts_bundle.js',
     merge_request_widget: './merge_request_widget/ci_bundle.js',
@@ -82,6 +83,10 @@ var config = {
         exclude: /node_modules/,
         loader: 'file-loader',
       },
+      {
+        test: /locale\/[a-z]+\/(.*)\.js$/,
+        loader: 'exports-loader?locales',
+      },
     ]
   },
 
@@ -146,6 +151,14 @@ var config = {
     new webpack.optimize.CommonsChunkPlugin({
       names: ['main', 'common', 'runtime'],
     }),
+
+    // locale common library
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'locale',
+      chunks: [
+        'cycle_analytics',
+      ],
+    }),
   ],
 
   resolve: {
diff --git a/package.json b/package.json
index a17399ddb8f614b897e3d2af9a2e97849ccb97b3..5cb07ddc3b75c9787b93aca109c4d929438fa116 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,9 @@
     "dropzone": "^4.2.0",
     "emoji-unicode-version": "^0.2.1",
     "eslint-plugin-html": "^2.0.1",
+    "exports-loader": "^0.6.4",
     "file-loader": "^0.11.1",
+    "jed": "^1.1.1",
     "jquery": "^2.2.1",
     "jquery-ujs": "^1.2.1",
     "js-cookie": "^2.1.3",
diff --git a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js
index 50000c5a5f5606bf6385ef564d4ca23e0ac0c0be..2fb9eb0ca856194f56e58c6ec7af110a2454c96e 100644
--- a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js
+++ b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js
@@ -1,6 +1,9 @@
 import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
 import limitWarningComp from '~/cycle_analytics/components/limit_warning_component';
 
+Vue.use(Translate);
+
 describe('Limit warning component', () => {
   let component;
   let LimitWarningComponent;
diff --git a/spec/javascripts/vue_shared/translate_spec.js b/spec/javascripts/vue_shared/translate_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..74bd4ff86b1bb9f364d4a92b3a0b72ade7910f74
--- /dev/null
+++ b/spec/javascripts/vue_shared/translate_spec.js
@@ -0,0 +1,90 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+
+Vue.use(Translate);
+
+describe('Vue translate filter', () => {
+  let el;
+
+  beforeEach(() => {
+    el = document.createElement('div');
+
+    document.body.appendChild(el);
+  });
+
+  it('translate single text', (done) => {
+    const comp = new Vue({
+      el,
+      template: `
+        <span>
+          {{ 'testing' | translate }}
+        </span>
+      `,
+    }).$mount();
+
+    Vue.nextTick(() => {
+      expect(
+        comp.$el.textContent.trim(),
+      ).toBe('testing');
+
+      done();
+    });
+  });
+
+  it('translate plural text with single count', (done) => {
+    const comp = new Vue({
+      el,
+      template: `
+        <span>
+          {{ '%d day' | translate-plural('%d days', 1) }}
+        </span>
+      `,
+    }).$mount();
+
+    Vue.nextTick(() => {
+      expect(
+        comp.$el.textContent.trim(),
+      ).toBe('1 day');
+
+      done();
+    });
+  });
+
+  it('translate plural text with multiple count', (done) => {
+    const comp = new Vue({
+      el,
+      template: `
+        <span>
+          {{ '%d day' | translate-plural('%d days', 2) }}
+        </span>
+      `,
+    }).$mount();
+
+    Vue.nextTick(() => {
+      expect(
+        comp.$el.textContent.trim(),
+      ).toBe('2 days');
+
+      done();
+    });
+  });
+
+  it('translate plural without replacing any text', (done) => {
+    const comp = new Vue({
+      el,
+      template: `
+        <span>
+          {{ 'day' | translate-plural('days', 2) }}
+        </span>
+      `,
+    }).$mount();
+
+    Vue.nextTick(() => {
+      expect(
+        comp.$el.textContent.trim(),
+      ).toBe('days');
+
+      done();
+    });
+  });
+});
diff --git a/yarn.lock b/yarn.lock
index e16cd9c36730167a9ef8534fd0e2a393be13c11d..b95df2d23838f4b16ee25f5308cbe3112e4edc24 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2143,6 +2143,13 @@ expand-range@^1.8.1:
   dependencies:
     fill-range "^2.1.0"
 
+exports-loader@^0.6.4:
+  version "0.6.4"
+  resolved "https://registry.yarnpkg.com/exports-loader/-/exports-loader-0.6.4.tgz#d70fc6121975b35fc12830cf52754be2740fc886"
+  dependencies:
+    loader-utils "^1.0.2"
+    source-map "0.5.x"
+
 express@^4.13.3, express@^4.14.1:
   version "4.14.1"
   resolved "https://registry.yarnpkg.com/express/-/express-4.14.1.tgz#646c237f766f148c2120aff073817b9e4d7e0d33"
@@ -3080,6 +3087,10 @@ jasmine-jquery@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/jasmine-jquery/-/jasmine-jquery-2.1.1.tgz#d4095e646944a26763235769ab018d9f30f0d47b"
 
+jed@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4"
+
 jodid25519@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967"
@@ -5071,6 +5082,10 @@ source-map-support@^0.4.2:
   dependencies:
     source-map "^0.5.3"
 
+source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3:
+  version "0.5.6"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
+
 source-map@^0.1.41:
   version "0.1.43"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346"
@@ -5083,10 +5098,6 @@ source-map@^0.4.4:
   dependencies:
     amdefine ">=0.0.4"
 
-source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3:
-  version "0.5.6"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
-
 source-map@~0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d"