Add i18n, fix #1

This commit is contained in:
Lucas Verney 2018-03-06 18:29:37 +01:00
parent f042214c92
commit 1c4053de8e
10 changed files with 185 additions and 37 deletions

View File

@ -1,13 +1,13 @@
<template> <template>
<v-dialog v-if="error" v-model="error" max-width="500px"> <v-dialog v-if="error" v-model="error" max-width="500px">
<v-card> <v-card>
<v-card-title class="headline">Error</v-card-title> <v-card-title class="headline">{{ $t('error.title') }}</v-card-title>
<v-card-text> <v-card-text>
{{ description }} {{ value.message }}. {{ description }} {{ value.message }}.
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn color="secondary" flat @click.stop="error=null">Cancel</v-btn> <v-btn color="secondary" flat @click.stop="error=null">{{ $t('misc.cancel') }}</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>

View File

@ -2,10 +2,10 @@
<v-container fluid grid-list-md> <v-container fluid grid-list-md>
<Loader v-if="isLoading"></Loader> <Loader v-if="isLoading"></Loader>
<v-layout row wrap v-else> <v-layout row wrap v-else>
<ErrorDialog :v-model="error" description="Unable to load recipes: " /> <ErrorDialog :v-model="error" :description="$t('error.unable_load_recipes')" />
<v-flex xs12 v-if="!recipes.length" class="text-xs-center"> <v-flex xs12 v-if="!recipes.length" class="text-xs-center">
<p>Start by adding a recipe with the "+" button on the top right corner!</p> <p>{{ $t('home.onboarding') }}</p>
</v-flex> </v-flex>
<v-flex <v-flex
v-for="recipe in recipes" v-for="recipe in recipes"
@ -21,10 +21,10 @@
<p>{{ recipe.short_description }}</p> <p>{{ recipe.short_description }}</p>
<v-layout row text-xs-center> <v-layout row text-xs-center>
<v-flex xs6> <v-flex xs6>
<p><v-icon>timelapse</v-icon> {{ recipe.preparation_time }} mins</p> <p><v-icon>timelapse</v-icon> {{ $tc('misc.Nmins', recipe.preparation_time, { count: recipe.preparation_time }) }}</p>
</v-flex> </v-flex>
<v-flex xs6> <v-flex xs6>
<p><v-icon>whatshot</v-icon> {{ recipe.cooking_time }} mins</p> <p><v-icon>whatshot</v-icon> {{ $tc('misc.Nmins', recipe.cooking_time, { count: recipe.cooking_time }) }}</p>
</v-flex> </v-flex>
</v-layout> </v-layout>
</v-card> </v-card>

View File

@ -4,16 +4,16 @@
<Loader></Loader> <Loader></Loader>
<v-flex xs12> <v-flex xs12>
<p>Importing...</p> <p>{{ $t('new.importing') }}</p>
</v-flex> </v-flex>
</v-layout> </v-layout>
</v-container> </v-container>
<v-container text-xs-center v-else> <v-container text-xs-center v-else>
<v-layout row wrap> <v-layout row wrap>
<ErrorDialog v-model="error" description="Unable to import recipe: " /> <ErrorDialog v-model="error" :description="$t('error.unable_import_recipe')" />
<v-flex xs12> <v-flex xs12>
<h2>Import from URL</h2> <h2>{{ $t('new.import_from_url') }}</h2>
<v-form v-model="validImport"> <v-form v-model="validImport">
<v-text-field <v-text-field
label="URL" label="URL"
@ -25,61 +25,61 @@
@click="submitImport" @click="submitImport"
:disabled="!validImport || isImporting" :disabled="!validImport || isImporting"
> >
Import {{ $t('new.import') }}
</v-btn> </v-btn>
</v-form> </v-form>
</v-flex> </v-flex>
</v-layout> </v-layout>
<v-layout row wrap mt-5 v-if="featureAddManually"> <v-layout row wrap mt-5 v-if="featureAddManually">
<v-flex xs12> <v-flex xs12>
<h2>Add manually</h2> <h2>{{ $t('new.add_manually') }}</h2>
<v-form v-model="validAdd"> <v-form v-model="validAdd">
<v-text-field <v-text-field
label="Title" :label="$t('new.title')"
v-model="title" v-model="title"
required required
></v-text-field> ></v-text-field>
<v-text-field <v-text-field
label="Picture" :label="$t('new.picture')"
v-model="picture" v-model="picture"
></v-text-field> ></v-text-field>
<v-text-field <v-text-field
label="Short description" :label="$t('new.short_description')"
v-model="short_description" v-model="short_description"
textarea textarea
></v-text-field> ></v-text-field>
<v-layout row> <v-layout row>
<v-flex xs4 mr-3> <v-flex xs4 mr-3>
<v-text-field <v-text-field
label="Number of persons" :label="$t('new.nb_persons')"
v-model="nb_person" v-model="nb_person"
type="number" type="number"
></v-text-field> ></v-text-field>
</v-flex> </v-flex>
<v-flex xs4 mx-3> <v-flex xs4 mx-3>
<v-text-field <v-text-field
label="Preparation time" :label="$t('new.preparation_time')"
v-model="preparation_time" v-model="preparation_time"
type="number" type="number"
suffix="mins" :suffix="$t('new.mins')"
></v-text-field> ></v-text-field>
</v-flex> </v-flex>
<v-flex xs4 ml-3> <v-flex xs4 ml-3>
<v-text-field <v-text-field
label="Cooking time" :label="$t('new.cooking_time')"
v-model="cooking_time" v-model="cooking_time"
type="number" type="number"
suffix="mins" :suffix="$t('new.mins')"
></v-text-field> ></v-text-field>
</v-flex> </v-flex>
</v-layout> </v-layout>
<v-text-field <v-text-field
label="Ingredients" :label="$t('new.ingredients')"
v-model="ingredients" v-model="ingredients"
textarea textarea
></v-text-field> ></v-text-field>
<v-text-field <v-text-field
label="Instructions" :label="$t('new.instructions')"
v-model="instructions" v-model="instructions"
textarea textarea
required required
@ -89,7 +89,7 @@
@click="submitAdd" @click="submitAdd"
:disabled="!validAdd" :disabled="!validAdd"
> >
Add {{ $t('new.add') }}
</v-btn> </v-btn>
</v-form> </v-form>
</v-flex> </v-flex>

View File

@ -8,26 +8,26 @@
<v-dialog v-model="refetchConfirm" max-width="500px"> <v-dialog v-model="refetchConfirm" max-width="500px">
<v-card> <v-card>
<v-card-title class="headline">Refetch recipe</v-card-title> <v-card-title class="headline">{{ $t('recipe.refetch_recipe') }}</v-card-title>
<v-card-text> <v-card-text>
This will refetch the recipe from the website and replace all current data with newly fetched ones. Are you sure? {{ $t('recipe.refetch_recipe_description') }}
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn color="secondary" flat @click.stop="refetchConfirm=false">Cancel</v-btn> <v-btn color="secondary" flat @click.stop="refetchConfirm=false">{{ $t('misc.cancel') }}</v-btn>
<v-btn color="error" flat @click.stop="handleRefetch">Refetch</v-btn> <v-btn color="error" flat @click.stop="handleRefetch">{{ $t('recipe.refetch') }}</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
<v-dialog v-model="deleteConfirm" max-width="500px"> <v-dialog v-model="deleteConfirm" max-width="500px">
<v-card> <v-card>
<v-card-title class="headline">Delete recipe</v-card-title> <v-card-title class="headline">{{ $t('recipe.delete_recipe') }}</v-card-title>
<v-card-text>This will delete this recipe. Are you sure?</v-card-text> <v-card-text>{{ $t('recipe.delete_recipe_description') }}</v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn color="secondary" flat @click.stop="deleteConfirm=false">Cancel</v-btn> <v-btn color="secondary" flat @click.stop="deleteConfirm=false">{{ $t('misc.cancel') }}</v-btn>
<v-btn color="error" flat @click.stop="handleDelete">Delete</v-btn> <v-btn color="error" flat @click.stop="handleDelete">{{ $t('recipe.delete') }}</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
@ -46,29 +46,29 @@
<p>{{ recipe.nb_person }}</p> <p>{{ recipe.nb_person }}</p>
</v-flex> </v-flex>
<v-flex xs6> <v-flex xs6>
<p><v-icon>timelapse</v-icon> Preparation: {{ recipe.preparation_time }}&nbsp;mins</p> <p><v-icon>timelapse</v-icon> {{ $t('recipe.preparation') }} {{ $tc('misc.Nmins', recipe.preparation_time, { count: recipe.preparation_time }) }}</p>
<p><v-icon>whatshot</v-icon> Cooking: {{ recipe.cooking_time }}&nbsp;mins</p> <p><v-icon>whatshot</v-icon> {{ $t('recipe.cooking') }} {{ $tc('misc.Nmins', recipe.cooking_time, { count: recipe.cooking_time }) }}</p>
</v-flex> </v-flex>
</v-layout> </v-layout>
<p>{{ recipe.short_description }}</p> <p>{{ recipe.short_description }}</p>
<h2>Ingredients</h2> <h2>{{ $t('recipe.ingredients') }}</h2>
<ul class="ml-5"> <ul class="ml-5">
<li v-for="ingredient in recipe.ingredients"> <li v-for="ingredient in recipe.ingredients">
{{ ingredient }} {{ ingredient }}
</li> </li>
</ul> </ul>
<h2 class="mt-3">Instructions</h2> <h2 class="mt-3">{{ $t('recipe.instructions') }}</h2>
<p v-for="item in recipe.instructions"> <p v-for="item in recipe.instructions">
{{ item }} {{ item }}
</p> </p>
<p v-if="recipe.url" class="text-xs-center"> <p v-if="recipe.url" class="text-xs-center">
<v-btn :href="recipe.url"> <v-btn :href="recipe.url" :title="$t('recipe.website')">
<v-icon class="fa-icon">fa-external-link</v-icon> <v-icon class="fa-icon">fa-external-link</v-icon>
</v-btn> </v-btn>
<v-btn @click.stop="deleteConfirm = true"> <v-btn @click.stop="deleteConfirm = true" :title="$t('recipe.delete')">
<v-icon>delete</v-icon> <v-icon>delete</v-icon>
</v-btn> </v-btn>
<v-btn @click.stop="refetchConfirm = true"> <v-btn @click.stop="refetchConfirm = true" :title="$t('recipe.refetch')">
<v-icon>autorenew</v-icon> <v-icon>autorenew</v-icon>
</v-btn> </v-btn>
</p> </p>

44
cuizin/js_src/helpers.js Normal file
View File

@ -0,0 +1,44 @@
/**
* Get all locales supported by the client browser.
*/
export function getBrowserLocales() {
let langs = [];
if (navigator.languages) {
// Chrome does not currently set navigator.language correctly
// https://code.google.com/p/chromium/issues/detail?id=101138
// but it does set the first element of navigator.languages correctly
langs = navigator.languages;
} else if (navigator.userLanguage) {
// IE only
langs = [navigator.userLanguage];
} else {
// as of this writing the latest version of firefox + safari set this correctly
langs = [navigator.language];
}
// Some browsers does not return uppercase for second part
const locales = langs.map((lang) => {
const locale = lang.split('-');
return locale[1] ? `${locale[0]}-${locale[1].toUpperCase()}` : lang;
});
return locales;
}
/**
* Find the best matching locale from the browser preferred locales.
*
* @param messages A translation object with supported locales as keys.
* @param defaultLocale An optional default locale.
* @return The best locale to use.
*/
export function getBestMatchingLocale(messages, defaultLocale = 'en') {
const locales = getBrowserLocales();
let bestLocale = defaultLocale;
// Get best matching locale
locales.some(locale => (messages[locale] && (bestLocale = locale)));
return bestLocale;
}

View File

@ -0,0 +1,43 @@
export default {
error: {
title: 'Error',
unable_import_recipe: 'Unable to import recipe: ',
unable_load_recipes: 'Unable to load recipes: ',
},
home: {
onboarding: 'Start by adding a recipe with the "+" button on the top right corner!',
},
misc: {
cancel: 'Cancel',
Nmins: '1 min | {count} mins',
},
new: {
add: 'Add',
add_manually: 'Add manually',
cooking_time: 'Cooking time',
import: 'Import',
import_from_url: 'Import from URL',
importing: 'Importing…',
ingredients: 'Ingredients',
instructions: 'Instructions',
mins: 'mins',
nb_opersons: 'Number of persons',
picture: 'Picture',
preparation_time: 'Preparation time',
short_description: 'Short description',
title: 'Title',
},
recipe: {
cooking: 'Cooking:',
delete: 'Delete',
delete_recipe: 'Delete recipe',
delete_recipe_description: 'This will delete this recipe. Are you sure?',
ingredients: 'Ingredients',
instructions: 'Instructions',
preparation: 'Preparation:',
refetch: 'Refetch',
refetch_recipe: 'Refetch recipe',
refetch_recipe_description: 'This will refetch the recipe from the website and replace all current data with newly fetched ones. Are you sure?',
website: 'Recipe web page',
},
};

View File

@ -0,0 +1,43 @@
export default {
error: {
title: 'Erreur',
unable_import_recipe: 'Impossible d\'importer la recette : ',
unable_load_recipes: 'Impossible de charger les recettes : ',
},
home: {
onboarding: 'Commencez par ajouter une recette avec le bouton "+" dans le coin supérieur droit !',
},
misc: {
cancel: 'Annuler',
Nmins: '1 min | {count} mins',
},
new: {
add: 'Ajouter',
add_manually: 'Ajouter manuellement',
cooking_time: 'Temps de cuisson',
import: 'Importer',
import_from_url: 'Importer depuis l\'URL',
importing: 'En cours d\'import…',
ingredients: 'Ingrédients',
instructions: 'Instructions',
mins: 'mins',
nb_opersons: 'Nombre de personnes',
picture: 'Photo',
preparation_time: 'Temps de préparation',
short_description: 'Description brève',
title: 'Titre',
},
recipe: {
cooking: 'Cuisson :',
delete: 'Supprimer',
delete_recipe: 'Suppression d\'une recette',
delete_recipe_description: 'Vous allez supprimer cette recette. En êtes-vous sûr ?',
ingredients: 'Ingrédients',
instructions: 'Instructions',
preparation: 'Préparation :',
refetch: 'Réactualiser',
refetch_recipe: 'Réactualiser la recette',
refetch_recipe_description: 'Vous allez remplacer le contenu de cette recette par des données à jour récupérées depuis le site web de la recette. En êtes-vous sûr ?',
website: 'Page de la recette',
},
};

View File

@ -0,0 +1,7 @@
import en from './en';
import fr from './fr';
export default {
en,
fr,
};

View File

@ -2,25 +2,35 @@
// (runtime-only or standalone) has been set in webpack.base.conf with an alias. // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'; import Vue from 'vue';
import Vuetify from 'vuetify'; import Vuetify from 'vuetify';
import VueI18n from 'vue-i18n';
import 'roboto-fontface/css/roboto/roboto-fontface.css'; import 'roboto-fontface/css/roboto/roboto-fontface.css';
import 'font-awesome/css/font-awesome.css'; import 'font-awesome/css/font-awesome.css';
import 'material-design-icons/iconfont/material-icons.css'; import 'material-design-icons/iconfont/material-icons.css';
import 'vuetify/dist/vuetify.min.css'; import 'vuetify/dist/vuetify.min.css';
import App from './App'; import App from './App';
import messages from './i18n';
import router from './router'; import router from './router';
import { getBestMatchingLocale } from './helpers';
// Isomorphic fetch // Isomorphic fetch
require('es6-promise').polyfill(); require('es6-promise').polyfill();
require('isomorphic-fetch'); require('isomorphic-fetch');
Vue.use(Vuetify); Vue.use(Vuetify);
Vue.use(VueI18n);
Vue.config.productionTip = false; Vue.config.productionTip = false;
const i18n = new VueI18n({
locale: getBestMatchingLocale(messages),
messages,
});
/* eslint-disable no-new */ /* eslint-disable no-new */
new Vue({ new Vue({
el: '#app', el: '#app',
i18n,
router, router,
components: { App }, components: { App },
template: '<App/>', template: '<App/>',

View File

@ -17,6 +17,7 @@
"material-design-icons": "^3.0.1", "material-design-icons": "^3.0.1",
"roboto-fontface": "^0.9.0", "roboto-fontface": "^0.9.0",
"vue": "^2.5.2", "vue": "^2.5.2",
"vue-i18n": "^7.4.2",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vuetify": "^1.0.0" "vuetify": "^1.0.0"
}, },