Fix language change

- Load the language files correctly when language is changed
- Save user language in localstorage so that we can have it even if disconnected (but still load it from user settings eventually since
user might be on a different device)
- Load all locales from Cldr with Gettext
- Fix pt-PT -> pt-BR
- Clean some obsolete config.exs comments

Later changes will allow to set the language without an account
https://framagit.org/framasoft/mobilizon/-/issues/375

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2020-10-28 18:58:43 +01:00
parent 8e1082c194
commit 67b906cc96
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
9 changed files with 97 additions and 55 deletions

View file

@ -94,15 +94,11 @@ config :mobilizon, Mobilizon.Web.Email.Mailer,
hostname: "localhost",
# usually 25, 465 or 587
port: 25,
# or {:system, "SMTP_USERNAME"}
username: nil,
# or {:system, "SMTP_PASSWORD"}
password: nil,
# can be `:always` or `:never`
tls: :if_available,
# or {":system", ALLOWED_TLS_VERSIONS"} w/ comma seprated values (e.g. "tlsv1.1,tlsv1.2")
allowed_tls_versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
# can be `true`
retries: 1,
# can be `true`
no_mx_lookups: false

View file

@ -108,13 +108,14 @@
import { Component, Vue, Watch } from "vue-property-decorator";
import Logo from "@/components/Logo.vue";
import { GraphQLError } from "graphql";
import { CURRENT_USER_CLIENT } from "../graphql/user";
import { loadLanguageAsync } from "@/utils/i18n";
import { CURRENT_USER_CLIENT, USER_SETTINGS } from "../graphql/user";
import { changeIdentity, logout } from "../utils/auth";
import { CURRENT_ACTOR_CLIENT, IDENTITIES, UPDATE_DEFAULT_ACTOR } from "../graphql/actor";
import { IPerson, Person } from "../types/actor";
import { CONFIG } from "../graphql/config";
import { IConfig } from "../types/config.model";
import { ICurrentUser, ICurrentUserRole } from "../types/current-user.model";
import { ICurrentUser, ICurrentUserRole, IUser } from "../types/current-user.model";
import SearchField from "./SearchField.vue";
import RouteName from "../router/name";
@ -138,6 +139,12 @@ import RouteName from "../router/name";
},
},
config: CONFIG,
loggedUser: {
query: USER_SETTINGS,
skip() {
return this.currentUser.isLoggedIn === false;
},
},
},
components: {
Logo,
@ -151,6 +158,8 @@ export default class NavBar extends Vue {
currentUser!: ICurrentUser;
loggedUser!: IUser;
ICurrentUserRole = ICurrentUserRole;
identities: IPerson[] = [];
@ -182,6 +191,13 @@ export default class NavBar extends Vue {
}
}
@Watch("loggedUser")
setSavedLanguage(): void {
if (this.loggedUser?.locale) {
loadLanguageAsync(this.loggedUser.locale);
}
}
async handleErrors(errors: GraphQLError[]): Promise<void> {
if (
errors.length > 0 &&

View file

@ -4,3 +4,4 @@ export const AUTH_USER_ID = "auth-user-id";
export const AUTH_USER_EMAIL = "auth-user-email";
export const AUTH_USER_ACTOR_ID = "auth-user-actor-id";
export const AUTH_USER_ROLE = "auth-user-role";
export const USER_LOCALE = "user-locale";

View file

@ -15,7 +15,7 @@
"oc": "Occitan",
"pl": "Polski",
"pt": "Português",
"pt_PT": "Português (Portugal)",
"pt_BR": "Português brasileiro",
"ru": "Русский",
"sv": "Svenska"
}

View file

@ -16,6 +16,21 @@ export interface ICurrentUser {
defaultActor?: IPerson;
}
export enum INotificationPendingParticipationEnum {
NONE = "NONE",
DIRECT = "DIRECT",
ONE_DAY = "ONE_DAY",
ONE_HOUR = "ONE_HOUR",
}
export interface IUserSettings {
timezone: string;
notificationOnDay: boolean;
notificationEachWeek: boolean;
notificationBeforeEvent: boolean;
notificationPendingParticipation: INotificationPendingParticipationEnum;
}
export interface IUser extends ICurrentUser {
confirmedAt: Date;
confirmationSendAt: Date;
@ -42,18 +57,3 @@ export enum IAuthProvider {
GITLAB = "gitlab",
TWITTER = "twitter",
}
export enum INotificationPendingParticipationEnum {
NONE = "NONE",
DIRECT = "DIRECT",
ONE_DAY = "ONE_DAY",
ONE_HOUR = "ONE_HOUR",
}
export interface IUserSettings {
timezone: string;
notificationOnDay: boolean;
notificationEachWeek: boolean;
notificationBeforeEvent: boolean;
notificationPendingParticipation: INotificationPendingParticipationEnum;
}

View file

@ -5,6 +5,7 @@ import {
AUTH_USER_EMAIL,
AUTH_USER_ID,
AUTH_USER_ROLE,
USER_LOCALE,
} from "@/constants";
import { ILogin, IToken } from "@/types/login.model";
import { UPDATE_CURRENT_USER_CLIENT } from "@/graphql/user";
@ -14,7 +15,12 @@ import { IPerson } from "@/types/actor";
import { IDENTITIES, UPDATE_CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import { NormalizedCacheObject } from "apollo-cache-inmemory";
export function saveUserData(obj: ILogin) {
export function saveTokenData(obj: IToken): void {
localStorage.setItem(AUTH_ACCESS_TOKEN, obj.accessToken);
localStorage.setItem(AUTH_REFRESH_TOKEN, obj.refreshToken);
}
export function saveUserData(obj: ILogin): void {
localStorage.setItem(AUTH_USER_ID, `${obj.user.id}`);
localStorage.setItem(AUTH_USER_EMAIL, obj.user.email);
localStorage.setItem(AUTH_USER_ROLE, obj.user.role);
@ -22,29 +28,36 @@ export function saveUserData(obj: ILogin) {
saveTokenData(obj);
}
export function saveActorData(obj: IPerson) {
export function saveLocaleData(locale: string): void {
localStorage.setItem(USER_LOCALE, locale);
}
export function saveActorData(obj: IPerson): void {
localStorage.setItem(AUTH_USER_ACTOR_ID, `${obj.id}`);
}
export function saveTokenData(obj: IToken) {
localStorage.setItem(AUTH_ACCESS_TOKEN, obj.accessToken);
localStorage.setItem(AUTH_REFRESH_TOKEN, obj.refreshToken);
}
export function deleteUserData() {
for (const key of [AUTH_USER_ID, AUTH_USER_EMAIL, AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN, AUTH_USER_ROLE]) {
export function deleteUserData(): void {
[AUTH_USER_ID, AUTH_USER_EMAIL, AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN, AUTH_USER_ROLE].forEach((key) => {
localStorage.removeItem(key);
}
});
}
export class NoIdentitiesException extends Error {}
export async function changeIdentity(apollo: ApolloClient<NormalizedCacheObject>, identity: IPerson): Promise<void> {
await apollo.mutate({
mutation: UPDATE_CURRENT_ACTOR_CLIENT,
variables: identity,
});
saveActorData(identity);
}
/**
* We fetch from localStorage the latest actor ID used,
* then fetch the current identities to set in cache
* the current identity used
*/
export async function initializeCurrentActor(apollo: ApolloClient<any>) {
export async function initializeCurrentActor(apollo: ApolloClient<any>): Promise<void> {
const actorId = localStorage.getItem(AUTH_USER_ACTOR_ID);
const result = await apollo.query({
@ -59,19 +72,11 @@ export async function initializeCurrentActor(apollo: ApolloClient<any>) {
const activeIdentity = identities.find((identity: IPerson) => identity.id === actorId) || (identities[0] as IPerson);
if (activeIdentity) {
return await changeIdentity(apollo, activeIdentity);
await changeIdentity(apollo, activeIdentity);
}
}
export async function changeIdentity(apollo: ApolloClient<NormalizedCacheObject>, identity: IPerson) {
await apollo.mutate({
mutation: UPDATE_CURRENT_ACTOR_CLIENT,
variables: identity,
});
saveActorData(identity);
}
export async function logout(apollo: ApolloClient<NormalizedCacheObject>) {
export async function logout(apollo: ApolloClient<NormalizedCacheObject>): Promise<void> {
await apollo.mutate({
mutation: UPDATE_CURRENT_USER_CLIENT,
variables: {

View file

@ -1,12 +1,13 @@
import Vue from "vue";
import VueI18n from "vue-i18n";
import { DateFnsPlugin } from "@/plugins/dateFns";
import { USER_LOCALE } from "@/constants";
import en from "../i18n/en_US.json";
import langs from "../i18n/langs.json";
const DEFAULT_LOCALE = "en_US";
let language = document.documentElement.getAttribute("lang") as string;
let language = localStorage.getItem(USER_LOCALE) || (document.documentElement.getAttribute("lang") as string);
language = language || ((window.navigator as any).userLanguage || window.navigator.language).replace(/-/, "_");
export const locale =
language && Object.prototype.hasOwnProperty.call(langs, language) ? language : language.split("-")[0];
@ -53,7 +54,7 @@ function dateFnsfileForLanguage(lang: string) {
Vue.use(DateFnsPlugin, { locale: dateFnsfileForLanguage(locale) });
async function loadLanguageAsync(lang: string): Promise<string> {
export async function loadLanguageAsync(lang: string): Promise<string> {
// If the same language
if (i18n.locale === lang) {
return Promise.resolve(setI18nLanguage(lang));
@ -63,7 +64,6 @@ async function loadLanguageAsync(lang: string): Promise<string> {
if (loadedLanguages.includes(lang)) {
return Promise.resolve(setI18nLanguage(lang));
}
// If the language hasn't been loaded yet
const newMessages = await import(
/* webpackChunkName: "lang-[request]" */ `@/i18n/${vueI18NfileForLanguage(lang)}.json`

View file

@ -14,7 +14,7 @@
<b-field :label="$t('Language')">
<b-select
:loading="!config || !loggedUser"
v-model="$i18n.locale"
v-model="locale"
:placeholder="$t('Select a language')"
>
<option v-for="(language, lang) in langs" :value="lang" :key="lang">
@ -50,6 +50,7 @@
</template>
<script lang="ts">
import { Component, Vue, Watch } from "vue-property-decorator";
import { saveLocaleData } from "@/utils/auth";
import { TIMEZONES } from "../../graphql/config";
import { USER_SETTINGS, SET_USER_SETTINGS, UPDATE_USER_LOCALE } from "../../graphql/user";
import { IConfig } from "../../types/config.model";
@ -128,14 +129,17 @@ export default class Preferences extends Vue {
}
}
@Watch("$i18n.locale")
@Watch("locale")
async updateLocale(): Promise<void> {
await this.$apollo.mutate({
mutation: UPDATE_USER_LOCALE,
variables: {
locale: this.$i18n.locale,
},
});
if (this.locale) {
await this.$apollo.mutate({
mutation: UPDATE_USER_LOCALE,
variables: {
locale: this.locale,
},
});
saveLocaleData(this.locale);
}
}
}
</script>

View file

@ -4,6 +4,26 @@ defmodule Mobilizon.Cldr do
"""
use Cldr,
locales: ["cs", "de", "en", "es", "fr", "it", "ja", "nl", "pl", "pt", "ru"],
locales: [
"ar",
"be",
"ca",
"cs",
"de",
"en",
"es",
"fi",
"fr",
"gl",
"it",
"ja",
"nl",
"oc",
"pl",
"pt",
"ru",
"sv"
],
gettext: Mobilizon.Web.Gettext,
providers: [Cldr.Number, Cldr.Calendar, Cldr.DateTime, Cldr.Language]
end