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

View File

@ -2,7 +2,7 @@ import datetime
import os import os
import re import re
import secrets import secrets
from typing import Dict, Tuple, Union from typing import Dict, Optional, Tuple, Union
import jwt import jwt
from flask import Blueprint, current_app, request 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' 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: def send_account_confirmation_email(user: User) -> None:
if current_app.config['CAN_SEND_EMAILS']: if current_app.config['CAN_SEND_EMAILS']:
ui_url = current_app.config['UI_URL'] ui_url = current_app.config['UI_URL']
@ -57,7 +64,7 @@ def send_account_confirmation_email(user: User) -> None:
), ),
} }
user_data = { user_data = {
'language': 'en', 'language': get_language(user.language),
'email': user.email, 'email': user.email,
} }
account_confirmation_email.send(user_data, email_data) 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 username: username (3 to 30 characters required)
:<json string email: user email :<json string email: user email
:<json string password: password (8 characters required) :<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 200: success
:statuscode 400: :statuscode 400:
@ -138,6 +147,7 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
username = post_data.get('username') username = post_data.get('username')
email = post_data.get('email') email = post_data.get('email')
password = post_data.get('password') password = post_data.get('password')
language = get_language(post_data.get('language'))
try: try:
ret = register_controls(username, email, password) 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 = User(username=username, email=email, password=password)
new_user.timezone = 'Europe/Paris' new_user.timezone = 'Europe/Paris'
new_user.confirmation_token = secrets.token_urlsafe(30) new_user.confirmation_token = secrets.token_urlsafe(30)
new_user.language = language
db.session.add(new_user) db.session.add(new_user)
db.session.commit() db.session.commit()
@ -661,9 +672,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
if current_app.config['CAN_SEND_EMAILS']: if current_app.config['CAN_SEND_EMAILS']:
ui_url = current_app.config['UI_URL'] ui_url = current_app.config['UI_URL']
user_data = { user_data = {
'language': ( 'language': get_language(auth_user.language),
'en' if auth_user.language is None else auth_user.language
),
'email': auth_user.email, 'email': auth_user.email,
} }
data = { data = {
@ -830,7 +839,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
return InvalidPayloadErrorResponse() return InvalidPayloadErrorResponse()
imperial_units = post_data.get('imperial_units') imperial_units = post_data.get('imperial_units')
language = post_data.get('language') language = get_language(post_data.get('language'))
timezone = post_data.get('timezone') timezone = post_data.get('timezone')
weekm = post_data.get('weekm') weekm = post_data.get('weekm')
@ -1189,7 +1198,7 @@ def request_password_reset() -> Union[Dict, HttpResponse]:
if user: if user:
password_reset_token = user.encode_password_reset_token(user.id) password_reset_token = user.encode_password_reset_token(user.id)
ui_url = current_app.config['UI_URL'] 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 = { email_data = {
'expiration_delay': get_readable_duration( 'expiration_delay': get_readable_duration(
current_app.config['PASSWORD_TOKEN_EXPIRATION_SECONDS'], current_app.config['PASSWORD_TOKEN_EXPIRATION_SECONDS'],
@ -1280,9 +1289,7 @@ def update_password() -> Union[Dict, HttpResponse]:
if current_app.config['CAN_SEND_EMAILS']: if current_app.config['CAN_SEND_EMAILS']:
password_change_email.send( password_change_email.send(
{ {
'language': ( 'language': get_language(user.language),
'en' if user.language is None else user.language
),
'email': user.email, 'email': user.email,
}, },
{ {

View File

@ -23,6 +23,7 @@ from fittrackee.responses import (
from fittrackee.utils import get_readable_duration from fittrackee.utils import get_readable_duration
from fittrackee.workouts.models import Record, Workout, WorkoutSegment from fittrackee.workouts.models import Record, Workout, WorkoutSegment
from .auth import get_language
from .decorators import authenticate, authenticate_as_admin from .decorators import authenticate, authenticate_as_admin
from .exceptions import InvalidEmailException, UserNotFoundException from .exceptions import InvalidEmailException, UserNotFoundException
from .models import User, UserSportPreference 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']: 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'] ui_url = current_app.config['UI_URL']
if reset_password: if reset_password:
user_data = { user_data = {

View File

@ -35,6 +35,7 @@
import { ROOT_STORE } from '@/store/constants' import { ROOT_STORE } from '@/store/constants'
import { TAppConfig } from '@/types/application' import { TAppConfig } from '@/types/application'
import { useStore } from '@/use/useStore' import { useStore } from '@/use/useStore'
import { localeFromLanguage } from '@/utils/locales'
const store = useStore() const store = useStore()
@ -47,7 +48,10 @@
const hideScrollBar = ref(false) const hideScrollBar = ref(false)
const displayScrollButton = 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()) onMounted(() => scroll())
function updateHideScrollBar(isMenuOpen: boolean) { function updateHideScrollBar(isMenuOpen: boolean) {
@ -74,6 +78,18 @@
displayScrollButton.value = false displayScrollButton.value = false
}, 300) }, 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> </script>
<style lang="scss"> <style lang="scss">

View File

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

View File

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