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", hostname: "localhost",
# usually 25, 465 or 587 # usually 25, 465 or 587
port: 25, port: 25,
# or {:system, "SMTP_USERNAME"}
username: nil, username: nil,
# or {:system, "SMTP_PASSWORD"}
password: nil, password: nil,
# can be `:always` or `:never` # can be `:always` or `:never`
tls: :if_available, 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"], allowed_tls_versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
# can be `true`
retries: 1, retries: 1,
# can be `true` # can be `true`
no_mx_lookups: false no_mx_lookups: false

View File

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

View File

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

View File

@ -16,6 +16,21 @@ export interface ICurrentUser {
defaultActor?: IPerson; 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 { export interface IUser extends ICurrentUser {
confirmedAt: Date; confirmedAt: Date;
confirmationSendAt: Date; confirmationSendAt: Date;
@ -42,18 +57,3 @@ export enum IAuthProvider {
GITLAB = "gitlab", GITLAB = "gitlab",
TWITTER = "twitter", 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_EMAIL,
AUTH_USER_ID, AUTH_USER_ID,
AUTH_USER_ROLE, AUTH_USER_ROLE,
USER_LOCALE,
} from "@/constants"; } from "@/constants";
import { ILogin, IToken } from "@/types/login.model"; import { ILogin, IToken } from "@/types/login.model";
import { UPDATE_CURRENT_USER_CLIENT } from "@/graphql/user"; 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 { IDENTITIES, UPDATE_CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import { NormalizedCacheObject } from "apollo-cache-inmemory"; 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_ID, `${obj.user.id}`);
localStorage.setItem(AUTH_USER_EMAIL, obj.user.email); localStorage.setItem(AUTH_USER_EMAIL, obj.user.email);
localStorage.setItem(AUTH_USER_ROLE, obj.user.role); localStorage.setItem(AUTH_USER_ROLE, obj.user.role);
@ -22,29 +28,36 @@ export function saveUserData(obj: ILogin) {
saveTokenData(obj); 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}`); localStorage.setItem(AUTH_USER_ACTOR_ID, `${obj.id}`);
} }
export function saveTokenData(obj: IToken) { export function deleteUserData(): void {
localStorage.setItem(AUTH_ACCESS_TOKEN, obj.accessToken); [AUTH_USER_ID, AUTH_USER_EMAIL, AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN, AUTH_USER_ROLE].forEach((key) => {
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]) {
localStorage.removeItem(key); localStorage.removeItem(key);
} });
} }
export class NoIdentitiesException extends Error {} 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, * We fetch from localStorage the latest actor ID used,
* then fetch the current identities to set in cache * then fetch the current identities to set in cache
* the current identity used * 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 actorId = localStorage.getItem(AUTH_USER_ACTOR_ID);
const result = await apollo.query({ 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); const activeIdentity = identities.find((identity: IPerson) => identity.id === actorId) || (identities[0] as IPerson);
if (activeIdentity) { if (activeIdentity) {
return await changeIdentity(apollo, activeIdentity); await changeIdentity(apollo, activeIdentity);
} }
} }
export async function changeIdentity(apollo: ApolloClient<NormalizedCacheObject>, identity: IPerson) { export async function logout(apollo: ApolloClient<NormalizedCacheObject>): Promise<void> {
await apollo.mutate({
mutation: UPDATE_CURRENT_ACTOR_CLIENT,
variables: identity,
});
saveActorData(identity);
}
export async function logout(apollo: ApolloClient<NormalizedCacheObject>) {
await apollo.mutate({ await apollo.mutate({
mutation: UPDATE_CURRENT_USER_CLIENT, mutation: UPDATE_CURRENT_USER_CLIENT,
variables: { variables: {

View File

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

View File

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

View File

@ -4,6 +4,26 @@ defmodule Mobilizon.Cldr do
""" """
use Cldr, 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] providers: [Cldr.Number, Cldr.Calendar, Cldr.DateTime, Cldr.Language]
end end