├── .browserslistrc ├── .env ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── data.json ├── favicon.ico ├── img │ ├── battlecreek-coffee-roasters-HvzR2yXtii4-unsplash.jpg │ ├── battlecreek-coffee-roasters-_1wDmr4dtuk-unsplash.jpg │ ├── jon-tyson-KRedbshBxEk-unsplash.jpg │ ├── lex-sirikiat-QouiCn7u6kw-unsplash.jpg │ ├── manki-kim-mv7kxYh5Rko-unsplash.jpg │ └── ryan-spaulding-_uncFvtOC-4-unsplash.jpg └── index.html ├── src ├── App.vue ├── EventBus.js ├── assets │ └── logo-circle-sm.png ├── components │ ├── Card.vue │ ├── Cards.vue │ ├── Footer.vue │ ├── LocaleSwitcher.vue │ ├── LocalizedLink.vue │ └── Nav.vue ├── config │ └── supported-locales.js ├── i18n.js ├── locales │ ├── ar.json │ ├── date-time-formats.js │ ├── en.json │ └── number-formats.js ├── main.js ├── router │ ├── Root.vue │ └── index.js ├── store │ └── index.js ├── util │ └── i18n │ │ ├── choice-index-for-plural.js │ │ ├── document.js │ │ ├── get-browser-locale.js │ │ └── supported-locales.js └── views │ ├── About.vue │ └── Home.vue ├── vue.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VUE_APP_I18N_LOCALE=en 2 | VUE_APP_I18N_FALLBACK_LOCALE=en 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ["plugin:vue/essential"], 7 | rules: { 8 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 9 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off", 10 | }, 11 | parserOptions: { 12 | parser: "babel-eslint", 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ashour 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Demo App for Phrase Blog Vue i18n Article 2 | 3 | This is a companion demo app for a [Phrase blog](https://phrase.com/blog) 4 | article. 5 | 6 | --- 7 | 8 | ## Project setup 9 | 10 | ``` 11 | npm install 12 | ``` 13 | 14 | ### Compiles and hot-reloads for development 15 | 16 | ``` 17 | npm run serve 18 | ``` 19 | 20 | ### Compiles and minifies for production 21 | 22 | ``` 23 | npm run build 24 | ``` 25 | 26 | ### Lints and fixes files 27 | 28 | ``` 29 | npm run lint 30 | ``` 31 | 32 | ### Customize configuration 33 | 34 | See [Configuration Reference](https://cli.vuejs.org/config/). 35 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"] 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-i18n", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'" 10 | }, 11 | "dependencies": { 12 | "core-js": "^3.4.4", 13 | "vue": "^2.6.10", 14 | "vue-i18n": "^8.0.0", 15 | "vue-router": "^3.1.3", 16 | "vuex": "^3.1.2" 17 | }, 18 | "devDependencies": { 19 | "@vue/cli-plugin-babel": "^4.1.0", 20 | "@vue/cli-plugin-eslint": "^4.1.0", 21 | "@vue/cli-plugin-router": "^4.1.0", 22 | "@vue/cli-plugin-vuex": "^4.1.0", 23 | "@vue/cli-service": "^4.1.0", 24 | "@vue/eslint-config-prettier": "^5.0.0", 25 | "babel-eslint": "^10.0.3", 26 | "eslint": "^5.16.0", 27 | "eslint-plugin-prettier": "^3.1.1", 28 | "eslint-plugin-vue": "^5.0.0", 29 | "prettier": "^1.19.1", 30 | "vue-cli-plugin-i18n": "^0.6.0", 31 | "vue-template-compiler": "^2.6.10" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "title": "Battlecreek Columbia Coldono", 5 | "price": 29.99, 6 | "imgUrl": "/img/battlecreek-coffee-roasters-_1wDmr4dtuk-unsplash.jpg", 7 | "addedOn": "2020-01-02", 8 | "likes": 3 9 | }, 10 | { 11 | "id": 2, 12 | "title": "Battlecreek Yirgacheffee", 13 | "price": 34.99, 14 | "imgUrl": "/img/battlecreek-coffee-roasters-HvzR2yXtii4-unsplash.jpg", 15 | "addedOn": "2020-01-05", 16 | "likes": 0 17 | }, 18 | { 19 | "id": 3, 20 | "title": "Primo Passo", 21 | "price": 19.99, 22 | "imgUrl": "/img/jon-tyson-KRedbshBxEk-unsplash.jpg", 23 | "addedOn": "2020-01-05", 24 | "likes": 9 25 | }, 26 | { 27 | "id": 4, 28 | "title": "Little Nap Brazil", 29 | "price": 39.99, 30 | "imgUrl": "/img/lex-sirikiat-QouiCn7u6kw-unsplash.jpg", 31 | "addedOn": "2020-01-06", 32 | "likes": 12 33 | }, 34 | { 35 | "id": 5, 36 | "title": "Little Nap Blend", 37 | "price": 39.99, 38 | "imgUrl": "/img/manki-kim-mv7kxYh5Rko-unsplash.jpg", 39 | "addedOn": "2020-01-07", 40 | "likes": 3 41 | }, 42 | { 43 | "id": 6, 44 | "title": "French Truck Peru Cajamarca", 45 | "price": 34.99, 46 | "imgUrl": "/img/ryan-spaulding-_uncFvtOC-4-unsplash.jpg", 47 | "addedOn": "2020-01-08", 48 | "likes": 1 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhraseApp-Blog/vue-i18n-demo/c21f0c543d340f51d4497682cedb12e0f2ae2151/public/favicon.ico -------------------------------------------------------------------------------- /public/img/battlecreek-coffee-roasters-HvzR2yXtii4-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhraseApp-Blog/vue-i18n-demo/c21f0c543d340f51d4497682cedb12e0f2ae2151/public/img/battlecreek-coffee-roasters-HvzR2yXtii4-unsplash.jpg -------------------------------------------------------------------------------- /public/img/battlecreek-coffee-roasters-_1wDmr4dtuk-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhraseApp-Blog/vue-i18n-demo/c21f0c543d340f51d4497682cedb12e0f2ae2151/public/img/battlecreek-coffee-roasters-_1wDmr4dtuk-unsplash.jpg -------------------------------------------------------------------------------- /public/img/jon-tyson-KRedbshBxEk-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhraseApp-Blog/vue-i18n-demo/c21f0c543d340f51d4497682cedb12e0f2ae2151/public/img/jon-tyson-KRedbshBxEk-unsplash.jpg -------------------------------------------------------------------------------- /public/img/lex-sirikiat-QouiCn7u6kw-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhraseApp-Blog/vue-i18n-demo/c21f0c543d340f51d4497682cedb12e0f2ae2151/public/img/lex-sirikiat-QouiCn7u6kw-unsplash.jpg -------------------------------------------------------------------------------- /public/img/manki-kim-mv7kxYh5Rko-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhraseApp-Blog/vue-i18n-demo/c21f0c543d340f51d4497682cedb12e0f2ae2151/public/img/manki-kim-mv7kxYh5Rko-unsplash.jpg -------------------------------------------------------------------------------- /public/img/ryan-spaulding-_uncFvtOC-4-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhraseApp-Blog/vue-i18n-demo/c21f0c543d340f51d4497682cedb12e0f2ae2151/public/img/ryan-spaulding-_uncFvtOC-4-unsplash.jpg -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-i18n 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | 34 | 50 | -------------------------------------------------------------------------------- /src/EventBus.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | 3 | const EventBus = new Vue() 4 | 5 | export default EventBus 6 | -------------------------------------------------------------------------------- /src/assets/logo-circle-sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhraseApp-Blog/vue-i18n-demo/c21f0c543d340f51d4497682cedb12e0f2ae2151/src/assets/logo-circle-sm.png -------------------------------------------------------------------------------- /src/components/Card.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 31 | 32 | 82 | -------------------------------------------------------------------------------- /src/components/Cards.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 30 | -------------------------------------------------------------------------------- /src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/LocaleSwitcher.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/LocalizedLink.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/Nav.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 26 | 27 | 59 | 65 | -------------------------------------------------------------------------------- /src/config/supported-locales.js: -------------------------------------------------------------------------------- 1 | export default { 2 | en: "English", 3 | ar: "عربي (Arabic)" 4 | } 5 | -------------------------------------------------------------------------------- /src/i18n.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import VueI18n from "vue-i18n" 3 | import getBrowserLocale from "@/util/i18n/get-browser-locale" 4 | import { supportedLocalesInclude } from "./util/i18n/supported-locales" 5 | import { 6 | getChoiceIndex, 7 | setDefaultChoiceIndexGet 8 | } from "./util/i18n/choice-index-for-plural" 9 | import dateTimeFormats from "@/locales/date-time-formats" 10 | import numberFormats from "@/locales/number-formats" 11 | import EventBus from "@/EventBus" 12 | 13 | Vue.use(VueI18n) 14 | 15 | // function loadLocaleMessages() { 16 | // const locales = require.context( 17 | // "./locales", 18 | // true, 19 | // /[A-Za-z0-9-_,\s]+\.json$/i 20 | // ) 21 | // const messages = {} 22 | // locales.keys().forEach(key => { 23 | // const matched = key.match(/([A-Za-z0-9-_]+)\./i) 24 | // if (matched && matched.length > 1) { 25 | // const locale = matched[1] 26 | // messages[locale] = locales(key) 27 | // } 28 | // }) 29 | // return messages 30 | // } 31 | 32 | function getStartingLocale() { 33 | const browserLocale = getBrowserLocale({ countryCodeOnly: true }) 34 | 35 | if (supportedLocalesInclude(browserLocale)) { 36 | return browserLocale 37 | } else { 38 | return process.env.VUE_APP_I18N_LOCALE || "en" 39 | } 40 | } 41 | 42 | setDefaultChoiceIndexGet(VueI18n.prototype.getChoiceIndex) 43 | 44 | VueI18n.prototype.getChoiceIndex = getChoiceIndex 45 | 46 | const startingLocale = getStartingLocale() 47 | 48 | const i18n = new VueI18n({ 49 | locale: startingLocale, 50 | fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || "en", 51 | messages: {}, 52 | // messages: loadLocaleMessages() 53 | dateTimeFormats, 54 | numberFormats 55 | }) 56 | 57 | const loadedLanguages = [] 58 | 59 | export function loadLocaleMessagesAsync(locale) { 60 | EventBus.$emit("i18n-load-start") 61 | 62 | if (loadedLanguages.length > 0 && i18n.locale === locale) { 63 | EventBus.$emit("i18n-load-complete") 64 | return Promise.resolve(locale) 65 | } 66 | 67 | // If the language was already loaded 68 | if (loadedLanguages.includes(locale)) { 69 | i18n.locale = locale 70 | EventBus.$emit("i18n-load-complete") 71 | return Promise.resolve(locale) 72 | } 73 | 74 | // If the language hasn't been loaded yet 75 | return import( 76 | /* webpackChunkName: "locale-[request]" */ `@/locales/${locale}.json` 77 | ).then(messages => { 78 | i18n.setLocaleMessage(locale, messages.default) 79 | 80 | loadedLanguages.push(locale) 81 | 82 | i18n.locale = locale 83 | 84 | EventBus.$emit("i18n-load-complete") 85 | return Promise.resolve(locale) 86 | }) 87 | } 88 | 89 | export default i18n 90 | -------------------------------------------------------------------------------- /src/locales/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "title": "قهوة الذواقة الدولية" 4 | }, 5 | "nav": { 6 | "home": "الرئيسية", 7 | "about": "نبذة عنا" 8 | }, 9 | "user_greeting": "مرحبا {name}", 10 | "footer": "بنيت بواسطة Vue و Vue I18n{0}مدعوم من كمية قهوة مبالغ فيها", 11 | "card": { 12 | "added": "تم إضافته", 13 | "likes": "لا توجد ❤️ إلى الآن 😕 | شخص {n} ❤️ هذا | شخصان ❤️ هذا | {n} أشخاص ❤️ هذا | {n} شخص ❤️ هذا | {n} شخص ❤️ هذا " 14 | }, 15 | "about": { 16 | "title": "نبذة عنا", 17 | "para1": "لكن لا بد أن أوضح لك أن كل هذه الأفكار المغلوطة حول استنكار النشوة وتمجيد الألم نشأت بالفعل، وسأعرض لك التفاصيل لتكتشف حقيقة وأساس تلك السعادة البشرية، فلا أحد يرفض أو يكره أو يتجنب الشعور بالسعادة، ولكن بفضل هؤلاء الأشخاص الذين لا يدركون بأن السعادة لا بد أن نستشعرها بصورة أكثر عقلانية ومنطقية فيعرضهم هذا لمواجهة الظروف الأليمة، وأكرر بأنه لا يوجد من يرغب في الحب ونيل المنال ويتلذذ بالآلام، الألم هو الألم ولكن نتيجة لظروف ما قد تكمن السعاده فيما نتحمله من كد وأسي.", 18 | "para2": "و سأعرض مثال حي لهذا، من منا لم يتحمل جهد بدني شاق إلا من أجل الحصول على ميزة أو فائدة؟ ولكن من لديه الحق أن ينتقد شخص ما أراد أن يشعر بالسعادة التي لا تشوبها عواقب أليمة أو آخر أراد أن يتجنب الألم الذي ربما تنجم عنه بعض المتعة ؟ ", 19 | "para3": "علي الجانب الآخر نشجب ونستنكر هؤلاء الرجال المفتونون بنشوة اللحظة الهائمون في رغباتهم فلا يدركون ما يعقبها من الألم والأسي المحتم، واللوم كذلك يشمل هؤلاء الذين أخفقوا في واجباتهم نتيجة لضعف إرادتهم فيتساوي مع هؤلاء الذين يتجنبون وينأون عن تحمل الكدح والألم ." 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/locales/date-time-formats.js: -------------------------------------------------------------------------------- 1 | const dateTimeFormats = { 2 | en: { 3 | short: { year: "numeric", month: "short", day: "numeric" } 4 | }, 5 | ar: { 6 | short: { year: "numeric", month: "long", day: "numeric" } 7 | } 8 | } 9 | 10 | export default dateTimeFormats 11 | -------------------------------------------------------------------------------- /src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "title": "International Gourmet Coffee" 4 | }, 5 | "nav": { 6 | "home": "Home", 7 | "about": "About" 8 | }, 9 | "user_greeting": "Hello, {name}", 10 | "footer": "Built with Vue and Vue I18n{0}Powered by an excessive amount of coffee", 11 | "card": { 12 | "added": "Added", 13 | "likes": "Nobody ❤️ this yet 😕 | {n} person ❤️ this | {n} people ❤️ this" 14 | }, 15 | "about": { 16 | "title": "About Us", 17 | "para1": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Nesciunt nobis sapiente quos officia vitae. Minus blanditiis doloremque error at consectetur magni nam voluptatem pariatur! Itaque deleniti delectus odio accusantium quas?", 18 | "para2": "Non eius beatae dolorum impedit enim repudiandae nulla ipsum animi. Ullam qui libero possimus aliquam, praesentium ipsam omnis, ab aut culpa reprehenderit odio sapiente ex molestias maiores? Temporibus, molestiae eum!", 19 | "para3": "Consectetur illo nobis, nemo necessitatibus quos corporis voluptates enim totam itaque ratione et ad iste corrupti eaque ex quas commodi. Perspiciatis vel quo consectetur quas nulla explicabo harum ratione consequatur." 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/locales/number-formats.js: -------------------------------------------------------------------------------- 1 | const numberFormats = { 2 | en: { 3 | currency: { style: "currency", currency: "USD" } 4 | }, 5 | ar: { 6 | currency: { style: "currency", currency: "USD", currencyDisplay: "code" } 7 | } 8 | } 9 | 10 | export default numberFormats 11 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import store from "./store"; 5 | import i18n from "./i18n"; 6 | 7 | Vue.config.productionTip = false; 8 | 9 | new Vue({ 10 | router, 11 | store, 12 | i18n, 13 | render: h => h(App) 14 | }).$mount("#app"); 15 | -------------------------------------------------------------------------------- /src/router/Root.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import VueRouter from "vue-router" 3 | import Home from "../views/Home.vue" 4 | import Root from "./Root" 5 | import i18n, { loadLocaleMessagesAsync } from "@/i18n" 6 | import { 7 | setDocumentDirectionPerLocale, 8 | setDocumentLang, 9 | setDocumentTitle 10 | } from "@/util/i18n/document" 11 | 12 | Vue.use(VueRouter) 13 | 14 | const { locale } = i18n 15 | 16 | const routes = [ 17 | { 18 | path: "/", 19 | redirect: locale 20 | }, 21 | { 22 | path: "/:locale", 23 | component: Root, 24 | children: [ 25 | { 26 | path: "", 27 | name: "home", 28 | component: Home 29 | }, 30 | { 31 | path: "about", 32 | name: "about", 33 | // route level code-splitting 34 | // this generates a separate chunk (about.[hash].js) for this route 35 | // which is lazy-loaded when the route is visited. 36 | component: () => 37 | import(/* webpackChunkName: "about" */ "../views/About.vue") 38 | } 39 | ] 40 | } 41 | ] 42 | 43 | const router = new VueRouter({ 44 | mode: "history", 45 | base: process.env.BASE_URL, 46 | routes 47 | }) 48 | 49 | router.beforeEach((to, from, next) => { 50 | if (to.params.locale === from.params.locale) { 51 | next() 52 | return 53 | } 54 | 55 | const { locale } = to.params 56 | 57 | loadLocaleMessagesAsync(locale).then(() => { 58 | setDocumentLang(locale) 59 | 60 | setDocumentDirectionPerLocale(locale) 61 | 62 | setDocumentTitle(i18n.t("app.title")) 63 | }) 64 | 65 | next() 66 | }) 67 | 68 | export default router 69 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | 4 | Vue.use(Vuex); 5 | 6 | export default new Vuex.Store({ 7 | state: {}, 8 | mutations: {}, 9 | actions: {}, 10 | modules: {} 11 | }); 12 | -------------------------------------------------------------------------------- /src/util/i18n/choice-index-for-plural.js: -------------------------------------------------------------------------------- 1 | let defaultChoiceIndex 2 | 3 | export function setDefaultChoiceIndexGet(fn) { 4 | defaultChoiceIndex = fn 5 | } 6 | 7 | /** 8 | * @param choice {number} a choice index given by the input to 9 | * $tc: `$tc('path.to.rule', choiceIndex)` 10 | * @param choicesLength {number} an overall amount of available choices 11 | * @returns a final choice index to select plural word by 12 | **/ 13 | export function getChoiceIndex(choice, choicesLength) { 14 | if (defaultChoiceIndex === undefined) { 15 | return choice 16 | } 17 | 18 | // this === VueI18n instance, so the locale property also exists here 19 | if (this.locale !== "ar") { 20 | return defaultChoiceIndex.apply(this, [choice, choicesLength]) 21 | } 22 | 23 | if ([0, 1, 2].includes(choice)) { 24 | return choice 25 | } 26 | 27 | if (3 <= choice && choice <= 10) { 28 | return 3 29 | } 30 | 31 | if (11 <= choice && choice <= 99) { 32 | return 4 33 | } 34 | 35 | return 5 36 | } 37 | -------------------------------------------------------------------------------- /src/util/i18n/document.js: -------------------------------------------------------------------------------- 1 | export function setDocumentDirectionPerLocale(locale) { 2 | document.dir = locale === "ar" ? "rtl" : "ltr" 3 | } 4 | 5 | export function setDocumentLang(lang) { 6 | document.documentElement.lang = lang 7 | } 8 | 9 | export function setDocumentTitle(newTitle) { 10 | document.title = newTitle 11 | } 12 | -------------------------------------------------------------------------------- /src/util/i18n/get-browser-locale.js: -------------------------------------------------------------------------------- 1 | export default function getBrowserLocale(options = {}) { 2 | const defaultOptions = { countryCodeOnly: false } 3 | 4 | const opt = { ...defaultOptions, ...options } 5 | 6 | const navigatorLocale = 7 | navigator.languages !== undefined 8 | ? navigator.languages[0] 9 | : navigator.language 10 | 11 | if (!navigatorLocale) { 12 | return undefined 13 | } 14 | 15 | const trimmedLocale = opt.countryCodeOnly 16 | ? navigatorLocale.trim().split(/-|_/)[0] 17 | : navigatorLocale.trim() 18 | 19 | return trimmedLocale 20 | } 21 | -------------------------------------------------------------------------------- /src/util/i18n/supported-locales.js: -------------------------------------------------------------------------------- 1 | import supportedLocales from "@/config/supported-locales" 2 | 3 | export function getSupportedLocales() { 4 | let annotatedLocales = [] 5 | 6 | for (const code of Object.keys(supportedLocales)) { 7 | annotatedLocales.push({ 8 | code, 9 | name: supportedLocales[code] 10 | }) 11 | } 12 | 13 | return annotatedLocales 14 | } 15 | 16 | export function supportedLocalesInclude(locale) { 17 | return Object.keys(supportedLocales).includes(locale) 18 | } 19 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | chainWebpack: config => { 3 | config.plugins.delete("prefetch") 4 | }, 5 | pluginOptions: { 6 | i18n: { 7 | locale: "en", 8 | fallbackLocale: "en", 9 | localeDir: "locales", 10 | enableInSFC: false 11 | } 12 | } 13 | } 14 | --------------------------------------------------------------------------------