From cc020fefbc262c15c2469b6572ea864be4b90de3 Mon Sep 17 00:00:00 2001
From: Tor-Einar Skog <tor-einar.skog@nibio.no>
Date: Wed, 3 Feb 2021 09:34:15 +0100
Subject: [PATCH] Added translation support

---
 .env                               |   2 +
 README.md                          |  35 ++++
 package-lock.json                  | 297 +++++++++++++++++++++++++++++
 package.json                       |  17 +-
 src/components/ObservationList.vue |   6 +-
 src/i18n.js                        |  23 +++
 src/locales/en.json                |   3 +
 src/locales/nb.json                |   3 +
 src/main.js                        |  28 ++-
 vue.config.js                      |  10 +
 10 files changed, 404 insertions(+), 20 deletions(-)
 create mode 100644 .env
 create mode 100644 src/i18n.js
 create mode 100644 src/locales/en.json
 create mode 100644 src/locales/nb.json
 create mode 100644 vue.config.js

diff --git a/.env b/.env
new file mode 100644
index 0000000..a492d13
--- /dev/null
+++ b/.env
@@ -0,0 +1,2 @@
+VUE_APP_I18N_LOCALE=nb
+VUE_APP_I18N_FALLBACK_LOCALE=en
diff --git a/README.md b/README.md
index 2e93379..36d8d54 100644
--- a/README.md
+++ b/README.md
@@ -67,3 +67,38 @@ The router components (the "pages" in the Single Page Application that you manag
 
 ### Checking out new code
 Remember to always run `npm install` after checking out new code from the repository. That way, new dependencies will be installed.
+
+### Translation
+[This tutorial](https://www.codeandweb.com/babeledit/tutorials/how-to-translate-your-vue-app-with-vue-i18n) has been used to enable translation.
+
+I couldn't get the per-component translation thing working (where you embed it in `<i18n>` tags),
+so all translations go into the `/locales/[languagecode].json` files.
+
+
+There's a simple translation example in /components/ObservationList.js:
+
+``` html
+<template>
+  <div class="hello">
+    <h1>{{ $t('startpage') }}</h1>
+    <router-link to="/observation" custom v-slot="{navigate}">
+      <button type="button" class="btn btn-primary" @click="navigate">+</button>
+    </router-link>
+    
+    </div>
+</template>
+```
+
+(TODO: Update this documentation when code in ObservationList changes)
+
+Change the startup language in the app here in `/i18n.js`:
+
+``` javascript
+export default new VueI18n({
+  locale: process.env.VUE_APP_I18N_LOCALE || 'nb', // Here you can customize
+  fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
+  messages: loadLocaleMessages()
+})
+```
+
+
diff --git a/package-lock.json b/package-lock.json
index 551dde8..63819ad 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4,6 +4,43 @@
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
+    "@intlify/vue-i18n-loader": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@intlify/vue-i18n-loader/-/vue-i18n-loader-1.0.0.tgz",
+      "integrity": "sha512-y7LlpKEQ01u7Yq14l4VNlbFYEHMmSEH1QXXASOMWspj9ZcIdCebhhvHCHqk5Oy5Epw3PtoxyRJNpb6Wle5udgA==",
+      "dev": true,
+      "requires": {
+        "js-yaml": "^3.13.1",
+        "json5": "^2.1.1"
+      },
+      "dependencies": {
+        "esprima": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+          "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+          "dev": true
+        },
+        "js-yaml": {
+          "version": "3.14.1",
+          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+          "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+          "dev": true,
+          "requires": {
+            "argparse": "^1.0.7",
+            "esprima": "^4.0.0"
+          }
+        },
+        "json5": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
+          "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
+          "dev": true,
+          "requires": {
+            "minimist": "^1.2.5"
+          }
+        }
+      }
+    },
     "@netflix/nerror": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/@netflix/nerror/-/nerror-1.1.3.tgz",
@@ -1815,6 +1852,17 @@
       "integrity": "sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg==",
       "dev": true
     },
+    "cli-table3": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz",
+      "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==",
+      "dev": true,
+      "requires": {
+        "colors": "^1.1.2",
+        "object-assign": "^4.1.0",
+        "string-width": "^2.1.1"
+      }
+    },
     "cliui": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
@@ -3466,6 +3514,12 @@
         "regexp.prototype.flags": "^1.2.0"
       }
     },
+    "deepmerge": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
+      "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
+      "dev": true
+    },
     "define-properties": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@@ -3713,6 +3767,24 @@
         "domelementtype": "1"
       }
     },
+    "dot-object": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/dot-object/-/dot-object-1.9.0.tgz",
+      "integrity": "sha512-7MPN6y7XhAO4vM4eguj5+5HNKLjJYfkVG1ZR1Aput4Q4TR6SYeSjhpVQ77IzJHoSHffKbDxBC+48aCiiRurDPw==",
+      "dev": true,
+      "requires": {
+        "commander": "^2.20.0",
+        "glob": "^7.1.4"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.20.3",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+          "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+          "dev": true
+        }
+      }
+    },
     "dot-prop": {
       "version": "5.3.0",
       "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
@@ -3722,6 +3794,12 @@
         "is-obj": "^2.0.0"
       }
     },
+    "dotenv": {
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
+      "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
+      "dev": true
+    },
     "duplexer": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@@ -3798,6 +3876,12 @@
         }
       }
     },
+    "emoji-regex": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+      "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+      "dev": true
+    },
     "emojis-list": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
@@ -4021,6 +4105,12 @@
         "estraverse": "^4.1.1"
       }
     },
+    "esm": {
+      "version": "3.2.25",
+      "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
+      "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
+      "dev": true
+    },
     "esprima": {
       "version": "2.7.3",
       "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz",
@@ -4482,6 +4572,12 @@
         "locate-path": "^2.0.0"
       }
     },
+    "flat": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+      "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+      "dev": true
+    },
     "flatten": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz",
@@ -5501,6 +5597,12 @@
       "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
       "dev": true
     },
+    "is-valid-glob": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz",
+      "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=",
+      "dev": true
+    },
     "is-windows": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
@@ -11249,6 +11351,48 @@
       "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
       "integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
     },
+    "vue-cli-plugin-i18n": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/vue-cli-plugin-i18n/-/vue-cli-plugin-i18n-1.0.1.tgz",
+      "integrity": "sha512-sLo6YzudaWgn5dOMvrKixE5bb/onYGxcxm+0YexqoOx0QtR+7hZ/P5WPFBMM9v/2i1ec2YYe2PvKTBel7KE+tA==",
+      "dev": true,
+      "requires": {
+        "debug": "^4.1.0",
+        "deepmerge": "^4.2.0",
+        "dotenv": "^8.2.0",
+        "flat": "^5.0.0",
+        "rimraf": "^3.0.0",
+        "vue": "^2.6.11",
+        "vue-i18n": "^8.17.0",
+        "vue-i18n-extract": "1.0.2"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.3.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+          "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+          "dev": true,
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        },
+        "rimraf": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+          "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+          "dev": true,
+          "requires": {
+            "glob": "^7.1.3"
+          }
+        }
+      }
+    },
     "vue-hot-reload-api": {
       "version": "2.3.4",
       "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
@@ -11260,6 +11404,159 @@
       "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.22.4.tgz",
       "integrity": "sha512-XLI5s0AdqMP2Lf4I4CmdmOq8kjb5DDFGR77wAuxCfpEuYSfhTRyyx6MetgZMiL6Lxa0DasjBOiOcciU3NkL3/Q=="
     },
+    "vue-i18n-extract": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/vue-i18n-extract/-/vue-i18n-extract-1.0.2.tgz",
+      "integrity": "sha512-+zwDKvle4KcfloXZnj5hF01ViKDiFr5RMx5507D7oyDXpSleRpekF5YHgZa/+Ra6Go68//z0Nya58J9tKFsCjw==",
+      "dev": true,
+      "requires": {
+        "cli-table3": "^0.5.1",
+        "dot-object": "^1.7.1",
+        "esm": "^3.2.13",
+        "glob": "^7.1.3",
+        "is-valid-glob": "^1.0.0",
+        "yargs": "^13.2.2"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+          "dev": true
+        },
+        "camelcase": {
+          "version": "5.3.1",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+          "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+          "dev": true
+        },
+        "cliui": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+          "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+          "dev": true,
+          "requires": {
+            "string-width": "^3.1.0",
+            "strip-ansi": "^5.2.0",
+            "wrap-ansi": "^5.1.0"
+          }
+        },
+        "find-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^3.0.0"
+          }
+        },
+        "get-caller-file": {
+          "version": "2.0.5",
+          "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+          "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+          "dev": true
+        },
+        "locate-path": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+          "dev": true,
+          "requires": {
+            "p-locate": "^3.0.0",
+            "path-exists": "^3.0.0"
+          }
+        },
+        "p-limit": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+          "dev": true,
+          "requires": {
+            "p-try": "^2.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+          "dev": true,
+          "requires": {
+            "p-limit": "^2.0.0"
+          }
+        },
+        "p-try": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+          "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+          "dev": true
+        },
+        "require-main-filename": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+          "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+          "dev": true
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^4.1.0"
+          }
+        },
+        "wrap-ansi": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+          "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.0",
+            "string-width": "^3.0.0",
+            "strip-ansi": "^5.0.0"
+          }
+        },
+        "yargs": {
+          "version": "13.3.2",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
+          "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+          "dev": true,
+          "requires": {
+            "cliui": "^5.0.0",
+            "find-up": "^3.0.0",
+            "get-caller-file": "^2.0.1",
+            "require-directory": "^2.1.1",
+            "require-main-filename": "^2.0.0",
+            "set-blocking": "^2.0.0",
+            "string-width": "^3.0.0",
+            "which-module": "^2.0.0",
+            "y18n": "^4.0.0",
+            "yargs-parser": "^13.1.2"
+          }
+        },
+        "yargs-parser": {
+          "version": "13.1.2",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
+          "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
+          "dev": true,
+          "requires": {
+            "camelcase": "^5.0.0",
+            "decamelize": "^1.2.0"
+          }
+        }
+      }
+    },
     "vue-loader": {
       "version": "13.7.3",
       "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-13.7.3.tgz",
diff --git a/package.json b/package.json
index e123358..690d801 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,14 @@
 {
   "name": "vipsobservationapp",
   "version": "1.0.0",
+  "private": true,
   "description": "The Field Pest Observation App for VIPS users",
   "author": "Tor-Einar Skog <tor-einar.skog@nibio.no>",
-  "private": true,
   "scripts": {
+    "build": "node build/build.js",
     "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
-    "start": "npm run dev",
-    "build": "node build/build.js"
+    "i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'",
+    "start": "npm run dev"
   },
   "dependencies": {
     "vue": "^2.5.2",
@@ -15,6 +16,7 @@
     "vue-router": "^3.0.1"
   },
   "devDependencies": {
+    "@intlify/vue-i18n-loader": "^1.0.0",
     "autoprefixer": "^7.1.2",
     "babel-core": "^6.22.1",
     "babel-helper-vue-jsx-merge-props": "^2.0.3",
@@ -48,6 +50,7 @@
     "shelljs": "^0.7.6",
     "uglifyjs-webpack-plugin": "^1.1.1",
     "url-loader": "^0.5.8",
+    "vue-cli-plugin-i18n": "~1.0.1",
     "vue-loader": "^13.3.0",
     "vue-style-loader": "^3.0.1",
     "vue-template-compiler": "^2.5.2",
@@ -56,10 +59,6 @@
     "webpack-dev-server": "^2.9.1",
     "webpack-merge": "^4.1.0"
   },
-  "engines": {
-    "node": ">= 6.0.0",
-    "npm": ">= 3.0.0"
-  },
   "browserslist": [
     "> 1%",
     "last 2 versions",
@@ -79,5 +78,9 @@
         "ANDROID_SUPPORT_V4_VERSION": "27.+"
       }
     }
+  },
+  "engines": {
+    "node": ">= 6.0.0",
+    "npm": ">= 3.0.0"
   }
 }
diff --git a/src/components/ObservationList.vue b/src/components/ObservationList.vue
index a467c7c..07bba50 100644
--- a/src/components/ObservationList.vue
+++ b/src/components/ObservationList.vue
@@ -1,6 +1,6 @@
 <template>
   <div class="hello">
-    <h1>{{ msg }}</h1>
+    <h1>{{ $t('startpage') }}</h1>
     <router-link to="/observation" custom v-slot="{navigate}">
       <button type="button" class="btn btn-primary" @click="navigate">+</button>
     </router-link>
@@ -13,7 +13,7 @@ export default {
   name: 'ObservationList',
   data () {
     return {
-      msg: 'Startsiden'
+      /*msg: 'Startsiden'*/
     }
   }
 }
@@ -35,4 +35,4 @@ li {
 a {
   color: #42b983;
 }
-</style>
+</style>
\ No newline at end of file
diff --git a/src/i18n.js b/src/i18n.js
new file mode 100644
index 0000000..313e372
--- /dev/null
+++ b/src/i18n.js
@@ -0,0 +1,23 @@
+import Vue from 'vue'
+import VueI18n from 'vue-i18n'
+
+Vue.use(VueI18n)
+
+function loadLocaleMessages () {
+  const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
+  const messages = {}
+  locales.keys().forEach(key => {
+    const matched = key.match(/([A-Za-z0-9-_]+)\./i)
+    if (matched && matched.length > 1) {
+      const locale = matched[1]
+      messages[locale] = locales(key)
+    }
+  })
+  return messages
+}
+
+export default new VueI18n({
+  locale: process.env.VUE_APP_I18N_LOCALE || 'nb', // Here you can customize
+  fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
+  messages: loadLocaleMessages()
+})
diff --git a/src/locales/en.json b/src/locales/en.json
new file mode 100644
index 0000000..9e9f5c7
--- /dev/null
+++ b/src/locales/en.json
@@ -0,0 +1,3 @@
+{
+  "startpage": "Start page"
+}
\ No newline at end of file
diff --git a/src/locales/nb.json b/src/locales/nb.json
new file mode 100644
index 0000000..d2862dd
--- /dev/null
+++ b/src/locales/nb.json
@@ -0,0 +1,3 @@
+{
+  "startpage": "Startsiden"
+}
\ No newline at end of file
diff --git a/src/main.js b/src/main.js
index 0823a9a..eeb5de6 100644
--- a/src/main.js
+++ b/src/main.js
@@ -3,24 +3,31 @@
 import Vue from 'vue'
 import App from './App'
 import router from './router'
+import i18n from './i18n'
 
 Vue.config.productionTip = false
 
 /* eslint-disable no-new */
 const init = () => {
 	new Vue({
-	  el: '#app',
-	  router,
-	  components: { App },
-	  template: '<App/>',
-	  data: {
+      el: '#app',
+      router,
+      components: { App },
+      template: '<App/>',
+
+      data: {
 	  },
-	  methods: {
+
+      methods: {
 	  },
-	  created() {
-		console.info("Vue is ready")
+
+      i18n,
+
+      created() {
+		console.info("Vue is ready");
+		console.info("User's preferred language is " + navigator.language);
 	  }
-	});
+    });
 };
 
 /**
@@ -29,7 +36,8 @@ const init = () => {
  */
 new Vue({
   el: '#vipsobsappmenu',
-  router
+  router,
+  i18n,
 });
 
 // Wait for the deviceready event to start the render
diff --git a/vue.config.js b/vue.config.js
new file mode 100644
index 0000000..93ef04a
--- /dev/null
+++ b/vue.config.js
@@ -0,0 +1,10 @@
+module.exports = {
+  pluginOptions: {
+    i18n: {
+      locale: 'nb',
+      fallbackLocale: 'en',
+      localeDir: 'locales',
+      enableInSFC: false
+    }
+  }
+}
-- 
GitLab