This is an automated email from the git hooks/post-receive script. New commit to branch bow-v2-go in repository bow. See https://gitlab.nuiton.org/chorem/bow.git commit ff54968a1aab5ffa5d3cdf6146b4fab7b0121c49 Author: Benjamin <poussin@codelutin.com> Date: Fri Jun 5 00:25:21 2020 +0200 ajout du support i18n (ajout du champs lang dans les info du user) --- migrate/004_add_user_lang.sql | 9 +++ pkg/constant/const.go | 4 +- pkg/http/router.go | 5 ++ pkg/http/userResource.go | 24 ++++++++ pkg/model/user.go | 1 + pkg/repository/userRepository.go | 15 ++++- web/package.json | 3 +- web/public/i18n/available.json | 1 + web/public/i18n/fr.json | 3 + web/src/App.vue | 16 +++++- web/src/components/SearchInput.vue | 2 +- web/src/components/preferences/LangEditor.vue | 81 +++++++++++++++++++++++++++ web/src/main.js | 2 + web/src/utils/FetchHelper.js | 18 +++--- web/src/views/Preferences.vue | 4 +- web/yarn.lock | 5 ++ 16 files changed, 178 insertions(+), 15 deletions(-) diff --git a/migrate/004_add_user_lang.sql b/migrate/004_add_user_lang.sql new file mode 100644 index 0000000..a6ac978 --- /dev/null +++ b/migrate/004_add_user_lang.sql @@ -0,0 +1,9 @@ +-- migration des images de bytea en text base64 + +ALTER TABLE bowuser ADD lang TEXT DEFAULT 'en'; +GRANT UPDATE (lang) ON bowUser TO person; + +---- create above / drop below ---- + +REVOKE UPDATE (lang) ON bowUser FROM person; +ALTER TABLE bowuser DROP lang; diff --git a/pkg/constant/const.go b/pkg/constant/const.go index 51ceb3d..2ea47d2 100644 --- a/pkg/constant/const.go +++ b/pkg/constant/const.go @@ -26,9 +26,9 @@ Token le nom utiliser pour le token dans les cookies et les parameters de query const Token = "bow-token" /* -AuthFields la liste des champs necessaire pour l'utilisation du user sur le front et la back' +AuthFields la liste des champs necessaire pour l'utilisation du user sur le front et le back' */ -var AuthFields = []string{"id", "admin", "login", "creationdate", "maxtagincloud", "maxresult", "actions"} +var AuthFields = []string{"id", "admin", "login", "creationdate", "maxtagincloud", "maxresult", "actions", "lang"} /* TokenHeader le nom utiliser pour mettre dans le header de requete http (fallback Authorization) diff --git a/pkg/http/router.go b/pkg/http/router.go index f1299cc..f19d027 100644 --- a/pkg/http/router.go +++ b/pkg/http/router.go @@ -59,6 +59,7 @@ func Start(bowPublicURL string, addr string) { u.HandleFunc("/autofavicon", updateUserAutoFavicon).Methods(http.MethodPut, http.MethodOptions) u.HandleFunc("/maxtagincloud", updateUserMaxTagInCloud).Methods(http.MethodPut, http.MethodOptions) u.HandleFunc("/maxresult", updateUserMaxResult).Methods(http.MethodPut, http.MethodOptions) + u.HandleFunc("/lang", updateLang).Methods(http.MethodPut, http.MethodOptions) s.HandleFunc("/bookmarks", getBookmarks).Methods(http.MethodGet, http.MethodOptions) s.HandleFunc("/bookmarks", addBookmark).Methods(http.MethodPost, http.MethodOptions) @@ -289,6 +290,10 @@ func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // check whether a file exists at the given path _, err = os.Stat(path) if os.IsNotExist(err) { + if strings.HasPrefix(r.URL.Path, "/i18n/") { + http.Error(w, err.Error(), http.StatusNotFound) + return + } // file does not exist, serve index.html http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath)) return diff --git a/pkg/http/userResource.go b/pkg/http/userResource.go index 5dbe105..8ef2726 100644 --- a/pkg/http/userResource.go +++ b/pkg/http/userResource.go @@ -347,6 +347,30 @@ func updateUserMaxResult(w http.ResponseWriter, r *http.Request) { } } +func updateLang(w http.ResponseWriter, r *http.Request) { + currentUser := r.Context().Value(constant.User).(model.BowUser) + + id := mux.Vars(r)["id"] + + var data map[string]string + err := json.NewDecoder(r.Body).Decode(&data) + if err != nil { + http.Error(w, fmt.Sprintf("%s", err), 400) + return + } + + utils.LogDebug("updateLang", id, data) + + userJSON, err := repository.UpdateLang(currentUser, id, data["lang"]) + if err != nil { + http.Error(w, fmt.Sprintf("%s", err), 400) + return + } + + w.Header().Add("Content-Type", "application/json") + io.WriteString(w, userJSON) +} + func confirmUserEmail(w http.ResponseWriter, r *http.Request) { currentUser := r.Context().Value(constant.User).(model.BowUser) diff --git a/pkg/model/user.go b/pkg/model/user.go index 41e913b..f130bb2 100644 --- a/pkg/model/user.go +++ b/pkg/model/user.go @@ -26,6 +26,7 @@ type BowUser struct { MaxTagInCloud int16 `json:"maxtagincloud,omitempty"` MaxResult int16 `json:"maxresult,omitempty"` Actions []Action `json:"actions,omitempty"` + Lang string `json:"lang,omitempty"` } type Token struct { diff --git a/pkg/repository/userRepository.go b/pkg/repository/userRepository.go index cf5a448..daec7d0 100644 --- a/pkg/repository/userRepository.go +++ b/pkg/repository/userRepository.go @@ -23,7 +23,7 @@ all field are send except: func UserJSON(currentUser model.BowUser, id string, fields ...string) (string, error) { var askedFields string if len(fields) == 0 { - askedFields = "id, creationdate, updatedate, admin, login, tokens, emails, unconfirmedemails, authenticationinfo, autoscreenshot, autofavicon, maxtagincloud, maxresult, actions" + askedFields = "id, creationdate, updatedate, admin, login, tokens, emails, unconfirmedemails, authenticationinfo, autoscreenshot, autofavicon, maxtagincloud, maxresult, actions, lang" } else { askedFields = strings.Join(fields, ", ") } @@ -68,7 +68,7 @@ func User(currentUser model.BowUser, id string, fields ...string) (model.BowUser } /* -UserIDFromToken get user id by application token +UserFromToken get user id by application token */ func UserFromToken(token string, fields ...string) (model.BowUser, error) { currentUser := constant.Nobody @@ -312,6 +312,17 @@ func UpdateUserMaxResult(currentUser model.BowUser, id string, value int8) error return err } +/* +UpdateLang met a jour la lang de l'utilisateur +*/ +func UpdateLang(currentUser model.BowUser, id string, value string) (string, error) { + fields := strings.Join(constant.AuthFields, ",") + q := &query{sql: fmt.Sprintf(`WITH __all AS (UPDATE bowuser SET lang=$2 WHERE id=$1 RETURNING %s) SELECT json_agg(__all)->0 AS json FROM __all;`, fields)} + userJSON, err := q.QueryString(currentUser, id, value) + + return userJSON, err +} + /* ConfirmUserEmail verif et confirme un email */ diff --git a/web/package.json b/web/package.json index 2f1b7c5..ab3cdc6 100644 --- a/web/package.json +++ b/web/package.json @@ -15,7 +15,8 @@ "vue-property-decorator": "^8.4.1", "vue-router": "^3.1.6", "vue-select": "^3.9.5", - "vuex": "^3.4.0" + "vuex": "^3.4.0", + "vuex-i18n": "^1.13.1" }, "devDependencies": { "@vue/cli-plugin-babel": "~4.3.0", diff --git a/web/public/i18n/available.json b/web/public/i18n/available.json new file mode 100644 index 0000000..59ed88f --- /dev/null +++ b/web/public/i18n/available.json @@ -0,0 +1 @@ +["en", "fr"] \ No newline at end of file diff --git a/web/public/i18n/fr.json b/web/public/i18n/fr.json new file mode 100644 index 0000000..d185b57 --- /dev/null +++ b/web/public/i18n/fr.json @@ -0,0 +1,3 @@ +{ + "ex: dad or mum -\"little children\"": "ex: papa or maman -\"petits enfants\"" +} diff --git a/web/src/App.vue b/web/src/App.vue index 0ab65db..763f104 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -42,9 +42,23 @@ class App extends Vue { } reload() { - this.$store.commit('user', this.$storage.get('bow-user') || {}) + let user = this.$storage.get('bow-user') || {} + this.$store.commit('user', user) this.$store.commit('authenticated', !!this.$storage.getCookie('bow-token')) this.insertOpenSearch() + if (user.lang && user.lang != this.$i18n.locale()) { + console.log('loading lang...', user.lang) + this.$fetch.i18n(user.lang).then( + data => { + console.log('lang loaded', user.lang) + this.$i18n.add(user.lang, data) + this.$i18n.set(user.lang) + }, + err => { + this.errorMsg = err.cause + } + ) + } } beforeMount() { diff --git a/web/src/components/SearchInput.vue b/web/src/components/SearchInput.vue index 87edf6c..8c5d9e9 100644 --- a/web/src/components/SearchInput.vue +++ b/web/src/components/SearchInput.vue @@ -5,7 +5,7 @@ <TagsInput id="tags" v-model="tags" :availableTags="queryTags" :noCreation="true"></TagsInput> </div> <div> - <label for="fulltext" title='ex: papa or maman -"petits enfants"'>fulltext</label> + <label for="fulltext" :title='$t(`ex: dad or mum -"little children"`)'>fulltext</label> <input id="fulltext" type="text" v-model="fulltext" @keyup.enter="searchFulltext(mFulltext)" /> </div> <form :action="$fetch.createUrl('/opensearch')"> diff --git a/web/src/components/preferences/LangEditor.vue b/web/src/components/preferences/LangEditor.vue new file mode 100644 index 0000000..2ea7de1 --- /dev/null +++ b/web/src/components/preferences/LangEditor.vue @@ -0,0 +1,81 @@ +<template> + <div class="lang-editor"> + <label for="lang">Max result</label> + <select v-model="lang"> + <option v-for="l in availableLang" :key="l">{{ l }}</option> + </select> + <span class="errorMsg">{{ errorMsg }}</span> + </div> +</template> + +<script> +import { Component, Vue } from 'vue-property-decorator' + +@Component({ + name: 'LangEditor', + components: {} +}) +class LangEditor extends Vue { + errorMsg = '' + + availableLang = [] + + timeoutSave = 0 + + cancelTimeoutSave() { + this.timeoutSave && clearTimeout(this.timeoutSave) + this.timeoutSave = 0 + } + + get lang() { + return this.user.lang + } + + set lang(value) { + this.cancelTimeoutSave() + this.timeoutSave = setTimeout( + function(v) { + this.$fetch.put('/users/current/lang', { lang: v }).then( + (user) => { + this.saveUser(user) + + this.errorMsg = 'lang change to ' + v + }, + (err) => { + console.log('ko', err) + this.errorMsg = err.cause + } + ) + }.bind(this), + 800, + value + ) + } + + loadAvailableLang() { + this.$fetch.i18n('available').then( + (data) => { + console.log('LLLLL available', typeof(data), data) + this.availableLang = data + }, + (err) => { + this.errorMsg = err.cause + } + ) + } + + beforeMount() { + console.log('beforeMounted LangEditor') + this.loadAvailableLang() + } +} + +export default LangEditor +</script> + +<style scoped lang="less"> +.lang-editor { + display: flex; + flex-direction: row; +} +</style> diff --git a/web/src/main.js b/web/src/main.js index 8b2e15b..0d5e161 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -8,6 +8,7 @@ import store from './store' import FetchHelper from '@/utils/FetchHelper.js' import StoreHelper from './utils/Store' import VueDOMPurifyHTML from 'vue-dompurify-html' +import vuexI18n from 'vuex-i18n' window.BACKEND_URL = process.env.VUE_APP_BACKEND_URL window.FRONTEND_URL = process.env.BASE_URL @@ -19,6 +20,7 @@ if (typeof window !== 'undefined') { } Vue.use(VueDOMPurifyHTML) +Vue.use(vuexI18n.plugin, store) Vue.$fetch = FetchHelper Vue.$storage = StoreHelper diff --git a/web/src/utils/FetchHelper.js b/web/src/utils/FetchHelper.js index 824aacd..1f82ce9 100644 --- a/web/src/utils/FetchHelper.js +++ b/web/src/utils/FetchHelper.js @@ -9,7 +9,7 @@ let FetchHelper = { headers['Content-Type'] = 'application/json' } - return fetch(this.createUrl(url), { + return fetch(url, { headers, method, credentials: 'include', // en prod a priori le back et le front auront la meme origine donc pas besoin @@ -22,7 +22,7 @@ let FetchHelper = { } if (response.status === 200 || response.status === 201) { - if ((response.headers.get('Content-Type') || '').toLowerCase() === 'application/json') { + if ((response.headers.get('Content-Type') || '').toLowerCase().startsWith('application/json')) { return response.json() } else if (parseInt(response.headers.get('Content-Length')) > 0) { return response.text() @@ -46,24 +46,28 @@ let FetchHelper = { }) }, + i18n(lang) { + return this.fetch(`/i18n/${lang}.json`, 'GET') + }, + get(url) { - return this.fetch(url, 'GET') + return this.fetch(this.createUrl(url), 'GET') }, post(url, body) { - return this.fetch(url, 'POST', {}, body) + return this.fetch(this.createUrl(url), 'POST', {}, body) }, put(url, body) { - return this.fetch(url, 'PUT', {}, body) + return this.fetch(this.createUrl(url), 'PUT', {}, body) }, patch(url, body) { - return this.fetch(url, 'PATCH', {}, body) + return this.fetch(this.createUrl(url), 'PATCH', {}, body) }, delete(url) { - return this.fetch(url, 'DELETE') + return this.fetch(this.createUrl(url), 'DELETE') } } diff --git a/web/src/views/Preferences.vue b/web/src/views/Preferences.vue index 3939760..b891a53 100644 --- a/web/src/views/Preferences.vue +++ b/web/src/views/Preferences.vue @@ -2,6 +2,7 @@ <div class="preferences"> <div>{{ errorMsg }}</div> <a :href="bookmarkletAdd">bookmarklet add</a> Drag and drop this link to your toolbar browser. To bookmark page, select some text in page, and click on it, in your toolbar browser. + <LangEditor></LangEditor> <MaxResultEditor></MaxResultEditor> <MaxTagInCloudEditor></MaxTagInCloudEditor> <Actions :actions="user.actions"></Actions> @@ -12,6 +13,7 @@ <script> // @ is an alias to /src import { Component, Vue } from 'vue-property-decorator' +import LangEditor from '@/components/preferences/LangEditor' import MaxResultEditor from '@/components/preferences/MaxResultEditor' import MaxTagInCloudEditor from '@/components/preferences/MaxTagInCloudEditor' import Actions from '@/components/preferences/Actions' @@ -19,7 +21,7 @@ import TagsEditor from '@/components/preferences/TagsEditor' @Component({ name: 'Preferences', - components: {MaxTagInCloudEditor, MaxResultEditor, Actions, TagsEditor} + components: {LangEditor, MaxTagInCloudEditor, MaxResultEditor, Actions, TagsEditor} }) class Preferences extends Vue { errorMsg = '' diff --git a/web/yarn.lock b/web/yarn.lock index ee8d634..7b0a070 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -8041,6 +8041,11 @@ vue@^2.6.10, vue@^2.6.11: resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35..." integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ== +vuex-i18n@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/vuex-i18n/-/vuex-i18n-1.13.1.tgz#f9f6bf5de44f85..." + integrity sha512-VTy5QAyMI6BJwpRfN5qncWQT0ohKiAYK+iTRW4JxgV9dkNoPMuKKDqExbOm1fzpitdrIoIipC3Zqr5fJ706VQg== + vuex@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.4.0.tgz#20cc086062d750769fce1febb..." -- To stop receiving notification emails like this one, please contact chorem.org SCM administrator <admin+scm@chorem.org>.