Merge pull request #207 from SamR1/detect-language

Detect navigator language
This commit is contained in:
Sam 2022-07-03 14:18:03 +02:00 committed by GitHub
commit 39c8ba3f7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 93 additions and 33 deletions

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"/><link rel="stylesheet" href="/static/css/leaflet.css"/><title>FitTrackee</title><script defer="defer" src="/static/js/chunk-vendors.9821bfa6.js"></script><script defer="defer" src="/static/js/app.78bef568.js"></script><link href="/static/css/app.158f462d.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"/><link rel="stylesheet" href="/static/css/leaflet.css"/><title>FitTrackee</title><script defer="defer" src="/static/js/chunk-vendors.9821bfa6.js"></script><script defer="defer" src="/static/js/app.8b9fe8b8.js"></script><link href="/static/css/app.4edb0d03.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkfittrackee_client"]=self["webpackChunkfittrackee_client"]||[]).push([[193],{9161:function(e,s,t){t.r(s),t.d(s,{default:function(){return A}});t(6699);var a=t(6252),r=t(2262),l=t(3577),o=t(3324),n=t(9996);const c={class:"chart-menu"},i={class:"chart-arrow"},u={class:"time-frames custom-checkboxes-group"},d={class:"time-frames-checkboxes custom-checkboxes"},p=["id","name","checked","onInput"],m={class:"chart-arrow"};var v=(0,a.aZ)({__name:"StatsMenu",emits:["arrowClick","timeFrameUpdate"],setup(e,{emit:s}){const t=(0,r.iH)("month"),o=["week","month","year"];function n(e){t.value=e,s("timeFrameUpdate",e)}return(e,r)=>((0,a.wg)(),(0,a.iD)("div",c,[(0,a._)("div",i,[(0,a._)("i",{class:"fa fa-chevron-left","aria-hidden":"true",onClick:r[0]||(r[0]=e=>s("arrowClick",!0))})]),(0,a._)("div",u,[(0,a._)("div",d,[((0,a.wg)(),(0,a.iD)(a.HY,null,(0,a.Ko)(o,(s=>(0,a._)("div",{class:"time-frame custom-checkbox",key:s},[(0,a._)("label",null,[(0,a._)("input",{type:"radio",id:s,name:s,checked:t.value===s,onInput:e=>n(s)},null,40,p),(0,a._)("span",null,(0,l.zw)(e.$t(`statistics.TIME_FRAMES.${s}`)),1)])]))),64))])]),(0,a._)("div",m,[(0,a._)("i",{class:"fa fa-chevron-right","aria-hidden":"true",onClick:r[1]||(r[1]=e=>s("arrowClick",!1))})])]))}}),k=t(3744);const _=(0,k.Z)(v,[["__scopeId","data-v-22d55de2"]]);var S=_,w=t(631);const f={class:"sports-menu"},h=["id","name","checked","onInput"],U={class:"sport-label"};var b=(0,a.aZ)({__name:"StatsSportsMenu",props:{userSports:null,selectedSportIds:{default:()=>[]}},emits:["selectedSportIdsUpdate"],setup(e,{emit:s}){const t=e,{t:n}=(0,o.QT)(),c=(0,a.f3)("sportColors"),{selectedSportIds:i}=(0,r.BK)(t),u=(0,a.Fl)((()=>(0,w.xH)(t.userSports,n)));function d(e){s("selectedSportIdsUpdate",e)}return(e,s)=>{const t=(0,a.up)("SportImage");return(0,a.wg)(),(0,a.iD)("div",f,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)((0,r.SU)(u),(e=>((0,a.wg)(),(0,a.iD)("label",{type:"checkbox",key:e.id,style:(0,l.j5)({color:e.color?e.color:(0,r.SU)(c)[e.label]})},[(0,a._)("input",{type:"checkbox",id:e.id,name:e.label,checked:(0,r.SU)(i).includes(e.id),onInput:s=>d(e.id)},null,40,h),(0,a.Wm)(t,{"sport-label":e.label,color:e.color},null,8,["sport-label","color"]),(0,a._)("span",U,(0,l.zw)(e.translatedLabel),1)],4)))),128))])}}});const I=b;var g=I,T=t(9318);const y={key:0,id:"user-statistics"};var C=(0,a.aZ)({__name:"index",props:{sports:null,user:null},setup(e){const s=e,{t:t}=(0,o.QT)(),{sports:l,user:c}=(0,r.BK)(s),i=(0,r.iH)("month"),u=(0,r.iH)(v(i.value)),d=(0,a.Fl)((()=>(0,w.xH)(s.sports,t))),p=(0,r.iH)(_(s.sports));function m(e){i.value=e,u.value=v(i.value)}function v(e){return(0,T.aZ)(new Date,e,s.user.weekm)}function k(e){u.value=(0,T.FN)(u.value,e,s.user.weekm)}function _(e){return e.map((e=>e.id))}function f(e){p.value.includes(e)?p.value=p.value.filter((s=>s!==e)):p.value.push(e)}return(0,a.YP)((()=>s.sports),(e=>{p.value=_(e)})),(e,s)=>(0,r.SU)(d)?((0,a.wg)(),(0,a.iD)("div",y,[(0,a.Wm)(S,{onTimeFrameUpdate:m,onArrowClick:k}),(0,a.Wm)(n.Z,{sports:(0,r.SU)(l),user:(0,r.SU)(c),chartParams:u.value,"displayed-sport-ids":p.value,fullStats:!0},null,8,["sports","user","chartParams","displayed-sport-ids"]),(0,a.Wm)(g,{"selected-sport-ids":p.value,"user-sports":(0,r.SU)(l),onSelectedSportIdsUpdate:f},null,8,["selected-sport-ids","user-sports"])])):(0,a.kq)("",!0)}});const F=(0,k.Z)(C,[["__scopeId","data-v-d693c7da"]]);var Z=F,x=t(5630),D=t(8602),H=t(9917);const E={id:"statistics",class:"view"},R={key:0,class:"container"};var W=(0,a.aZ)({__name:"StatisticsView",setup(e){const s=(0,H.o)(),t=(0,a.Fl)((()=>s.getters[D.YN.GETTERS.AUTH_USER_PROFILE])),o=(0,a.Fl)((()=>s.getters[D.O8.GETTERS.SPORTS].filter((e=>t.value.sports_list.includes(e.id)))));return(e,s)=>{const n=(0,a.up)("Card");return(0,a.wg)(),(0,a.iD)("div",E,[(0,r.SU)(t).username?((0,a.wg)(),(0,a.iD)("div",R,[(0,a.Wm)(n,null,{title:(0,a.w5)((()=>[(0,a.Uk)((0,l.zw)(e.$t("statistics.STATISTICS")),1)])),content:(0,a.w5)((()=>[(0,a.Wm)(Z,{class:(0,l.C_)({"stats-disabled":0===(0,r.SU)(t).nb_workouts}),user:(0,r.SU)(t),sports:(0,r.SU)(o)},null,8,["class","user","sports"])])),_:1}),0===(0,r.SU)(t).nb_workouts?((0,a.wg)(),(0,a.j4)(x.Z,{key:0})):(0,a.kq)("",!0)])):(0,a.kq)("",!0)])}}});const P=(0,k.Z)(W,[["__scopeId","data-v-2e341d4e"]]);var A=P}}]);
//# sourceMappingURL=statistics.145d19e3.js.map
//# sourceMappingURL=statistics.c817d0d3.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,7 @@
import json
from datetime import datetime, timedelta
from io import BytesIO
from typing import Optional
from unittest.mock import MagicMock, Mock, patch
import pytest
@ -233,7 +234,16 @@ class TestUserRegistration(ApiTestCaseMixin):
assert data['status'] == 'success'
assert 'auth_token' not in data
def test_it_creates_user_with_inactive_account(self, app: Flask) -> None:
@pytest.mark.parametrize(
'input_language,expected_language',
[('en', 'en'), ('fr', 'fr'), ('invalid', 'en'), (None, 'en')],
)
def test_it_creates_user_with_inactive_account(
self,
app: Flask,
input_language: Optional[str],
expected_language: str,
) -> None:
client = app.test_client()
username = self.random_string()
email = self.random_email()
@ -245,6 +255,7 @@ class TestUserRegistration(ApiTestCaseMixin):
username=username,
email=email,
password=self.random_string(),
language=input_language,
)
),
content_type='application/json',
@ -254,9 +265,18 @@ class TestUserRegistration(ApiTestCaseMixin):
assert new_user.email == email
assert new_user.password is not None
assert new_user.is_active is False
assert new_user.language == expected_language
def test_it_calls_account_confirmation_email_if_payload_is_valid(
self, app: Flask, account_confirmation_email_mock: Mock
@pytest.mark.parametrize(
'input_language,expected_language',
[('en', 'en'), ('fr', 'fr'), ('invalid', 'en'), (None, 'en')],
)
def test_it_calls_account_confirmation_email_when_payload_is_valid(
self,
app: Flask,
account_confirmation_email_mock: Mock,
input_language: Optional[str],
expected_language: str,
) -> None:
client = app.test_client()
email = self.random_email()
@ -271,6 +291,7 @@ class TestUserRegistration(ApiTestCaseMixin):
username=username,
email=email,
password='12345678',
language=input_language,
)
),
content_type='application/json',
@ -279,7 +300,7 @@ class TestUserRegistration(ApiTestCaseMixin):
account_confirmation_email_mock.send.assert_called_once_with(
{
'language': 'en',
'language': expected_language,
'email': email,
},
{
@ -1227,8 +1248,16 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
self.assert_400(response)
@pytest.mark.parametrize(
'input_language,expected_language',
[('en', 'en'), ('fr', 'fr'), ('invalid', 'en'), (None, 'en')],
)
def test_it_updates_user_preferences(
self, app: Flask, user_1: User
self,
app: Flask,
user_1: User,
input_language: Optional[str],
expected_language: str,
) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
@ -1241,7 +1270,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
dict(
timezone='America/New_York',
weekm=True,
language='fr',
language=input_language,
imperial_units=True,
)
),
@ -1252,6 +1281,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
data = json.loads(response.data.decode())
assert data['status'] == 'success'
assert data['message'] == 'user preferences updated'
assert data['data']['language'] == expected_language
assert data['data'] == jsonify_dict(user_1.serialize(user_1))
@ -2237,6 +2267,7 @@ class TestResendAccountConfirmationEmail(ApiTestCaseMixin):
) -> None:
client = app.test_client()
expected_token = self.random_string()
inactive_user.language = 'fr'
with patch('secrets.token_urlsafe', return_value=expected_token):
client.post(
@ -2248,7 +2279,7 @@ class TestResendAccountConfirmationEmail(ApiTestCaseMixin):
account_confirmation_email_mock.send.assert_called_once_with(
{
'language': 'en',
'language': inactive_user.language,
'email': inactive_user.email,
},
{

View File

@ -2,7 +2,7 @@ import datetime
import os
import re
import secrets
from typing import Dict, Tuple, Union
from typing import Dict, Optional, Tuple, Union
import jwt
from flask import Blueprint, current_app, request
@ -43,6 +43,13 @@ HEX_COLOR_REGEX = regex = "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
NOT_FOUND_MESSAGE = 'the requested URL was not found on the server'
def get_language(language: Optional[str]) -> str:
# Note: some users may not have language preferences set
if not language or language not in current_app.config['LANGUAGES']:
language = 'en'
return language
def send_account_confirmation_email(user: User) -> None:
if current_app.config['CAN_SEND_EMAILS']:
ui_url = current_app.config['UI_URL']
@ -57,7 +64,7 @@ def send_account_confirmation_email(user: User) -> None:
),
}
user_data = {
'language': 'en',
'language': get_language(user.language),
'email': user.email,
}
account_confirmation_email.send(user_data, email_data)
@ -106,6 +113,8 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
:<json string username: username (3 to 30 characters required)
:<json string email: user email
:<json string password: password (8 characters required)
:<json string lang: user language preferences (if not provided or invalid,
fallback to 'en' (english))
:statuscode 200: success
:statuscode 400:
@ -138,6 +147,7 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
username = post_data.get('username')
email = post_data.get('email')
password = post_data.get('password')
language = get_language(post_data.get('language'))
try:
ret = register_controls(username, email, password)
@ -165,6 +175,7 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
new_user = User(username=username, email=email, password=password)
new_user.timezone = 'Europe/Paris'
new_user.confirmation_token = secrets.token_urlsafe(30)
new_user.language = language
db.session.add(new_user)
db.session.commit()
@ -661,9 +672,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
if current_app.config['CAN_SEND_EMAILS']:
ui_url = current_app.config['UI_URL']
user_data = {
'language': (
'en' if auth_user.language is None else auth_user.language
),
'language': get_language(auth_user.language),
'email': auth_user.email,
}
data = {
@ -830,7 +839,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
return InvalidPayloadErrorResponse()
imperial_units = post_data.get('imperial_units')
language = post_data.get('language')
language = get_language(post_data.get('language'))
timezone = post_data.get('timezone')
weekm = post_data.get('weekm')
@ -1189,7 +1198,7 @@ def request_password_reset() -> Union[Dict, HttpResponse]:
if user:
password_reset_token = user.encode_password_reset_token(user.id)
ui_url = current_app.config['UI_URL']
user_language = 'en' if user.language is None else user.language
user_language = get_language(user.language)
email_data = {
'expiration_delay': get_readable_duration(
current_app.config['PASSWORD_TOKEN_EXPIRATION_SECONDS'],
@ -1280,9 +1289,7 @@ def update_password() -> Union[Dict, HttpResponse]:
if current_app.config['CAN_SEND_EMAILS']:
password_change_email.send(
{
'language': (
'en' if user.language is None else user.language
),
'language': get_language(user.language),
'email': user.email,
},
{

View File

@ -23,6 +23,7 @@ from fittrackee.responses import (
from fittrackee.utils import get_readable_duration
from fittrackee.workouts.models import Record, Workout, WorkoutSegment
from .auth import get_language
from .decorators import authenticate, authenticate_as_admin
from .exceptions import InvalidEmailException, UserNotFoundException
from .models import User, UserSportPreference
@ -530,7 +531,7 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
)
if current_app.config['CAN_SEND_EMAILS']:
user_language = 'en' if user.language is None else user.language
user_language = get_language(user.language)
ui_url = current_app.config['UI_URL']
if reset_password:
user_data = {

View File

@ -35,6 +35,7 @@
import { ROOT_STORE } from '@/store/constants'
import { TAppConfig } from '@/types/application'
import { useStore } from '@/use/useStore'
import { localeFromLanguage } from '@/utils/locales'
const store = useStore()
@ -47,7 +48,10 @@
const hideScrollBar = ref(false)
const displayScrollButton = ref(false)
onBeforeMount(() => store.dispatch(ROOT_STORE.ACTIONS.GET_APPLICATION_CONFIG))
onBeforeMount(() => {
initLanguage()
store.dispatch(ROOT_STORE.ACTIONS.GET_APPLICATION_CONFIG)
})
onMounted(() => scroll())
function updateHideScrollBar(isMenuOpen: boolean) {
@ -74,6 +78,18 @@
displayScrollButton.value = false
}, 300)
}
function initLanguage() {
let language = 'en'
try {
const navigatorLanguage = navigator.language.split('-')[0]
if (navigatorLanguage in localeFromLanguage) {
language = navigatorLanguage
}
} catch (e) {
language = 'en'
}
store.dispatch(ROOT_STORE.ACTIONS.UPDATE_APPLICATION_LANGUAGE, language)
}
</script>
<style lang="scss">

View File

@ -191,6 +191,9 @@
const appConfig: ComputedRef<TAppConfig> = computed(
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
)
const language: ComputedRef<string> = computed(
() => store.getters[ROOT_STORE.GETTERS.LANGUAGE]
)
const registration_disabled: ComputedRef<boolean> = computed(
() =>
props.action === 'register' && !appConfig.value.is_registration_enabled
@ -245,6 +248,7 @@
}
)
default:
formData['language'] = language.value
store.dispatch(AUTH_USER_STORE.ACTIONS.LOGIN_OR_REGISTER, {
actionType,
formData,

View File

@ -93,6 +93,7 @@ export interface ILoginRegisterFormData {
username: string
email: string
password: string
language?: string
}
export interface ILoginOrRegisterData {