added locale switcher to change language of fleetbase

This commit is contained in:
Ronald A. Richardson
2024-01-22 18:50:09 +08:00
parent d1d5b87c21
commit 313906d36a
16 changed files with 447 additions and 31 deletions

View File

@@ -0,0 +1,30 @@
<div class="next-user-button" ...attributes>
<BasicDropdown @defaultClass={{@wrapperClass}} @onOpen={{@onOpen}} @onClose={{@onClose}} @verticalPosition={{@verticalPosition}} @horizontalPosition={{@horizontalPosition}} @renderInPlace={{or @renderInPlace true}} @initiallyOpened={{@initiallyOpened}} as |dd|>
<dd.Trigger class={{@triggerClass}}>
<div class="next-org-button-trigger flex-shrink-0 {{if dd.isOpen 'is-open'}}">
<FaIcon @icon="globe" @size="sm" />
</div>
</dd.Trigger>
<dd.Content class={{@contentClass}}>
<div class="next-dd-menu {{@dropdownMenuClass}} {{if dd.isOpen 'is-open'}}">
{{#each-in this.availableLocales as |key country|}}
<div class="px-1">
<a href="javascript:;" class="next-dd-item" {{on "click" (fn this.changeLocale key)}}>
<div class="flex flex-row items-center justify-between w-full">
<div class="flex-1">
<span class="mr-1">{{country.emoji}}</span>
<span>{{country.language}}</span>
</div>
{{#if (eq this.currentLocale key)}}
<div>
<FaIcon @icon="check" class="text-green-400" />
</div>
{{/if}}
</div>
</a>
</div>
{{/each-in}}
</div>
</dd.Content>
</BasicDropdown>
</div>

View File

@@ -0,0 +1,144 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { task } from 'ember-concurrency-decorators';
export default class LocaleSelectorComponent extends Component {
/**
* Inject the intl service.
*
* @memberof LocaleSelectorComponent
*/
@service intl;
/**
* Inject the intl service.
*
* @memberof LocaleSelectorComponent
*/
@service fetch;
/**
* Tracks all the available locales.
*
* @memberof LocaleSelectorComponent
*/
@tracked locales = [];
/**
* All available countries data.
*
* @memberof LocaleSelectorComponent
*/
@tracked countries = [];
/**
* The current locale in use.
*
* @memberof LocaleSelectorComponent
*/
@tracked currentLocale;
/**
* Creates an instance of LocaleSelectorComponent.
* @memberof LocaleSelectorComponent
*/
constructor() {
super(...arguments);
this.locales = this.intl.locales;
this.currentLocale = this.intl.primaryLocale;
this.loadAvailableCountries.perform();
// Check for locale change
this.intl.onLocaleChanged(() => {
this.currentLocale = this.intl.primaryLocale;
});
}
/**
* Handles the change of locale.
* @param {string} selectedLocale - The selected locale.
* @returns {void}
* @memberof LocaleSelectorComponent
* @method changeLocale
* @instance
* @action
*/
@action changeLocale(selectedLocale) {
this.currentLocale = selectedLocale;
this.intl.setLocale(selectedLocale);
// Persist to server
this.saveUserLocale.perform(selectedLocale);
}
/**
* Loads available countries asynchronously.
* @returns {void}
* @memberof LocaleSelectorComponent
* @method loadAvailableCountries
* @instance
* @task
* @generator
*/
@task *loadAvailableCountries() {
this.countries = yield this.fetch.get('lookup/countries', { columns: ['name', 'cca2', 'flag', 'emoji', 'languages'] });
this.availableLocales = this._createAvailableLocaleMap();
}
/**
* Saves the user's selected locale to the server.
* @param {string} locale - The user's selected locale.
* @returns {void}
* @memberof LocaleSelectorComponent
* @method saveUserLocale
* @instance
* @task
* @generator
*/
@task *saveUserLocale(locale) {
yield this.fetch.post('users/locale', { locale });
}
/**
* Creates a map of available locales.
* @private
* @returns {Object} - The map of available locales.
* @memberof LocaleSelectorComponent
* @method _createAvailableLocaleMap
* @instance
*/
_createAvailableLocaleMap() {
const localeMap = {};
for (let i = 0; i < this.locales.length; i++) {
const locale = this.locales.objectAt(i);
localeMap[locale] = this._findCountryDataForLocale(locale);
}
return localeMap;
}
/**
* Finds country data for a given locale.
* @private
* @param {string} locale - The locale to find country data for.
* @returns {Object|null} - The country data or null if not found.
* @memberof LocaleSelectorComponent
* @method _findCountryDataForLocale
* @instance
*/
_findCountryDataForLocale(locale) {
const localeCountry = locale.split('-')[1];
const country = this.countries.find((country) => country.cca2.toLowerCase() === localeCountry);
if (country) {
// get the language
country.language = Object.values(country.languages)[0];
}
return country;
}
}

View File

@@ -3,6 +3,7 @@ import { action } from '@ember/object';
import { task } from 'ember-concurrency-decorators';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
/**
* Glimmer component for handling notification enforcement.
*

View File

@@ -163,7 +163,7 @@ export default class AuthLoginController extends Controller {
// Handle unverified user
if (error.toString().includes('not verified')) {
return this.sendUserForEmailVerification(email);
return this.sendUserForEmailVerification(identity);
}
return this.failure(error);
@@ -196,7 +196,7 @@ export default class AuthLoginController extends Controller {
* Creates an email verification session and transitions user to verification route.
*
* @param {String} email
* @return {Promise<Transition>}
* @return {Promise<Transition>}
* @memberof AuthLoginController
*/
sendUserForEmailVerification(email) {

View File

@@ -36,6 +36,13 @@ export default class AuthTwoFaController extends Controller {
*/
@service session;
/**
* Internationalization service.
*
* @var {Service}
*/
@service intl;
/**
* Tracked property for storing the verification token.
*
@@ -123,7 +130,7 @@ export default class AuthTwoFaController extends Controller {
const { token, verificationCode, clientToken, identity } = this;
if (!clientToken) {
this.notifications.error('Invalid session. Please try again.');
this.notifications.error(this.intl.t('auth.two-fa.verify-code.invalid-session-error-notification'));
return;
}
@@ -136,17 +143,17 @@ export default class AuthTwoFaController extends Controller {
});
// If verification is successful, transition to the desired route
this.notifications.success('Verification successful!');
this.notifications.success(this.intl.t('auth.two-fa.verify-code.verification-successful-notification'));
// authenticate user
return this.session.authenticate('authenticator:fleetbase', { authToken }).then((response) => {
return this.session.authenticate('authenticator:fleetbase', { authToken }).then(() => {
return this.router.transitionTo('console');
});
} catch (error) {
if (error.message.includes('Verification code has expired')) {
this.notifications.info('Verification code has expired. Please request a new one.');
this.notifications.info(this.intl.t('auth.two-fa.verify-code.verification-code-expired-notification'));
} else {
this.notifications.error('Verification failed. Please try again.');
this.notifications.error(this.intl.t('auth.two-fa.verify-code.verification-code-failed-notification'));
}
}
}
@@ -174,13 +181,13 @@ export default class AuthTwoFaController extends Controller {
this.twoFactorSessionExpiresAfter = this.getExpirationDateFromClientToken(clientToken);
this.countdownReady = true;
this.isCodeExpired = false;
this.notifications.success('Verification code resent successfully.');
this.notifications.success(this.intl.t('auth.two-fa.resend-code.verification-code-resent-notification'));
} else {
this.notifications.error('Unable to send verification code.');
this.notifications.error(this.intl.t('auth.two-fa.resend-code.verification-code-resent-error-notification'));
}
} catch (error) {
// Handle errors, show error notifications, etc.
this.notifications.error('Error resending verification code. Please try again.');
this.notifications.error(this.intl.t('auth.two-fa.resend-code.verification-code-resent-error-notification'));
}
}
@@ -196,7 +203,7 @@ export default class AuthTwoFaController extends Controller {
identity: this.identity,
token: this.token,
})
.then(({ ok }) => {
.then(() => {
return this.router.transitionTo('auth.login');
});
}

View File

@@ -190,11 +190,14 @@ export default class AuthVerificationController extends Controller {
modal.startLoading();
const phone = modal.getOption('phone');
return this.fetch.post('onboard/send-verification-sms', { phone, session: this.hello }).then(() => {
this.notifications.success('Verification code SMS sent!');
}).catch((error) => {
this.notifications.serverError(error);
});
return this.fetch
.post('onboard/send-verification-sms', { phone, session: this.hello })
.then(() => {
this.notifications.success('Verification code SMS sent!');
})
.catch((error) => {
this.notifications.serverError(error);
});
},
});
}
@@ -213,11 +216,14 @@ export default class AuthVerificationController extends Controller {
modal.startLoading();
const email = modal.getOption('email');
return this.fetch.post('onboard/send-verification-email', { email, session: this.hello }).then(() => {
this.notifications.success('Verification code email sent!');
}).catch((error) => {
this.notifications.serverError(error);
});
return this.fetch
.post('onboard/send-verification-email', { email, session: this.hello })
.then(() => {
this.notifications.success('Verification code email sent!');
})
.catch((error) => {
this.notifications.serverError(error);
});
},
});
}

View File

@@ -25,6 +25,13 @@ export default class ConsoleRoute extends Route {
*/
@service session;
/**
* Inject the `intl` service
*
* @var {Service}
*/
@service intl;
/**
* Inject the `currentUser` service
*
@@ -72,6 +79,12 @@ export default class ConsoleRoute extends Route {
@action setupController(controller, model) {
super.setupController(controller, model);
// Get and set user locale
this.fetch.get('users/locale').then(({ locale }) => {
this.intl.setLocale(locale);
});
// Get user organizations
this.fetch.get('auth/organizations').then((organizations) => {
this.currentUser.setOption('organizations', organizations);
controller.organizations = organizations;

View File

@@ -3,4 +3,3 @@
@import 'tailwindcss/utilities';
@import 'inter-ui/inter.css';
@import 'console.css';

View File

@@ -4,12 +4,10 @@
color: rgb(202 138 4);
}
body[data-theme="dark"] .two-fa-enforcement-alert button#two-fa-setup-button.btn.btn-warning,
.two-fa-enforcement-alert button#two-fa-setup-button.btn.btn-warning {
.two-fa-enforcement-alert button#two-fa-setup-button.btn.btn-warning,
body[data-theme='dark'] .two-fa-enforcement-alert button#two-fa-setup-button.btn.btn-warning {
background-color: rgb(202 138 4);
border-color: rgb(161 98 7);
color: rgb(254 249 195);
cursor: default;
}

View File

@@ -46,7 +46,7 @@
autocomplete="current-password"
required
class="relative block w-full px-3 py-2 text-gray-900 placeholder-gray-500 border border-gray-300 rounded-none appearance-none rounded-b-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5 dark:text-white dark:bg-gray-700 dark:border-gray-900"
placeholder{{t "auth.login.form.password-label"}}
placeholder={{t "auth.login.form.password-label"}}
disabled={{this.isLoading}}
/>
</div>

View File

@@ -1,4 +1,4 @@
{{page-title "Console"}}
{{page-title (t "app.name")}}
<Layout::Container>
<Layout::Header @brand={{@model}} @user={{this.user}} @organizations={{this.organizations}} @menuItems={{this.universe.headerMenuItems}} @extensions={{this.extensions}} @onAction={{this.onAction}} @showSidebarToggle={{true}} @sidebarToggleEnabled={{this.sidebarToggleEnabled}} @onSidebarToggle={{this.onSidebarToggle}} />
<Layout::Main>
@@ -13,4 +13,9 @@
</Layout::Section>
</Layout::Main>
<Layout::MobileNavbar @brand={{@model}} @user={{this.user}} @organizations={{this.organizations}} @menuItems={{this.universe.headerMenuItems}} @extensions={{this.extensions}} @onAction={{this.onAction}} />
</Layout::Container>
</Layout::Container>
{{!-- Add Locale Selector to Header --}}
<EmberWormhole @to="view-header-actions">
<LocaleSelector />
</EmberWormhole>

View File

@@ -4,7 +4,7 @@
<Layout::Sidebar::Item @route="console.admin.index" @icon="rectangle-list">{{t "common.overview"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.branding" @icon="palette">{{t "common.branding"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.notifications" @icon="bell">{{t "common.notifications"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.two-fa-settings" @icon="shield">{{t "common.2fa-config"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.two-fa-settings" @icon="shield-halved">{{t "common.2fa-config"}}</Layout::Sidebar::Item>
{{#each this.universe.adminMenuItems as |menuItem|}}
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.admin.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>
{{/each}}

View File

@@ -14,7 +14,7 @@ module.exports = function (/* environment */) {
* @type {String?}
* @default "null"
*/
fallbackLocale: null,
fallbackLocale: 'en-us',
/**
* Path where translations are stored. This is relative to the project root.

View File

@@ -0,0 +1,26 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | locale-selector', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });
await render(hbs`<LocaleSelector />`);
assert.dom().hasText('');
// Template block usage:
await render(hbs`
<LocaleSelector>
template block text
</LocaleSelector>
`);
assert.dom().hasText('template block text');
});
});

View File

@@ -0,0 +1,178 @@
app:
name: Fleetbase
terms:
new: Шинэ
sort: Эрэмбэлэх
filter: Шүүлт
columns: Баганууд
settings: Тохиргоо
home: Нүүр
admin: Админ
logout: Гарах
dashboard: Тааварлага
search: Хайлт
search-input: Хайх оруулга
common:
save-button-text: Өөрчлөлтүүдийг хадгалах
new: Шинэ
delete: Устгах
sort: Эрэмбэлэх
filter: Шүүлт
columns: Баганууд
settings: Тохиргоо
home: Нүүр
admin: Админ
logout: Гарах
dashboard: Тааварлага
search: Хайлт
search-input: Хайх оруулга
uploading: Хуулах...
profile: Профайл
your-profile: Таны Профайл
name: Нэр
email: Имэйл
phone: Таны утасны дугаар
date-of-birth: Төрсөн огноо
overview: Тойм
account: Акаунт
branding: Брэндинг
notifications: Мэдэгдэл
services: Үйлчилгээнүүд
mail: Имэйл
filesystem: Файл систем
queue: Дарааллын жагсаалт
socket: Сокет
notification-channels: Мэдэгдлийн чиглэл
2fa-config: 2FA тохиргоо
two-factor: 2 Хувийн
component:
two-fa-enforcement-alert:
message: Таны дансанд нэмэлт аюулгүйн хамгаалалт хийхийн тулд, таны байгууллага 2-хавтай нэвтрэхийг шаарддаг. Нэвтрэх тохиргооны "2FA" хэсэгтэйгээр 2FA-г идэвхжүүлнэ үү.
button-text: 2FA тохируулах
auth:
two-fa:
verify-code:
invalid-session-error-notification: Хүчинтэй хугацаа дууссан байна. Дахин оролдоно уу.
verification-successful-notification: Баталгаажсан!
verification-code-expired-notification: Баталгаажуулах код хугацаа нь дууссан байна. Дахин авахыг хүсэж байна.
verification-code-failed-notification: Баталгаажуулалт амжилтгүй боллоо. Дахин оролдоно уу.
resend-code:
verification-code-resent-notification: Шинэ баталгаажуулах код илгээгдлээ.
verification-code-resent-error-notification: Баталгаажуулах кодыг дахин илгээхэд алдаа гарлаа. Дахин оролдоно уу.
forgot-password:
success-message: Имэйлээ шалгаад дараах зааврыг дагаарай!
is-sent:
title: Хаалттай!
message: <strong>Имэйлээ шалгаарай!</strong><br> Таны имэйл руу илгээсэн сүлжээний холбоосоор таны нууц үгийг сэргээх боломжтой болно. Холбоос нь 15 минут дуусах болно.
not-sent:
title: Нууц үгээ мартсан уу?
message: <strong>Ахин бодохгүй, бид таны тусгай имэйлээ танд илгээж байна.</strong><br> {appName}-д нэвтрэхийн тулд ашиглаж буй имэйл хаягаа оруулна уу, бид танд нууц үг сэргээх зааврыг илгээнэ.
form:
email-label: Таны имэйл хаяг
submit-button: Заавал, Би Магадгүй!
nevermind-button: Тийм
login:
title: Таны акаунт руу нэвтрэх
no-identity-notification: Имэйл хаягаа оруулаагүй байна уу?
no-password-notification: Нууц үгээ оруулагүй байна уу?
unverified-notification: Таны акаунт баталгаажагдсан байх ёстой.
failed-attempt:
message: <strong>Нууц үгээ мартсан уу?</strong><br> Доорх товч дээр дарж нууц үгээ сэргээхийг хүсэлтэй байна.
button-text: Ой, магадгүйтэй болов уу!
form:
email-label: Имэйл хаяг
password-label: Нууц үг
remember-me-label: Намайг сана
forgot-password-label: Нууц үгээ мартсан уу?
sign-in-button: Нэвтрэх
create-account-button: Шинэ акаунт үүсгэх
slow-connection-message: Холболтын асуудалтай байна.
reset-password:
success-message: Таны нууц үг амжилттай сэргэгдлээ! Нэвтрэхийн тулд нэвтэрнэ үү.
title: Таны нууц үг сэргээх
form:
code:
label: Таны сэргээх код
help-text: Имэйлээр авсан баталгаажуулах код.
password:
label: Шинэ нууц үг
help-text: 6-с дээш тэмдэгт агуулсан нууц үг оруулна уу.
confirm-password:
label: Нууц үгээ давтах
help-text: 6-с дээш тэмдэгт агуулсан нууц үг оруулна уу.
submit-button: Нууц үг сэргээх
back-button: Буцах
console:
account:
index:
upload-new: Шинээр байршуулах
phone: Таны утасны дугаар.
photos: зургууд
admin:
config:
database:
title: Датабаз тохиргоо
filesystem:
title: Файл систем тохиргоо
mail:
title: Имэйл тохиргоо
notification-channels:
title: Мэдэгдлийн чиглэл тохиргоо
queue:
title: Дарааллын жагсаалт тохиргоо
services:
title: Үйлчилгээнүүдийн тохиргоо
socket:
title: Сокет тохиргоо
branding:
title: Брэндинг
icon-text: Зургийн текст
upload-new: Шинээр байршуулах
reset-default: Эхлэл руу буцаах
logo-text: Лого
theme: Өглөөний загвар
index:
total-users: Нийт хэрэглэгчид
total-organizations: Нийт байгууллагууд
total-transactions: Нийт гүйлгээ
notification:
title: Мэдэгдэл
notification-settings: Мэдэгдэл тохиргоо
settings:
index:
title: Байгууллагын тохиргоо
organization-name: Байгууллагын нэр
organization-description: Байгууллагын тайлбар
organization-phone: Байгууллагын утасны дугаар
organization-currency: Байгууллагын валют
organization-id: Байгууллагын дугаар
organization-branding: Байгууллагын брэндинг
logo: Лого
logo-help-text: Таны байгууллагын лого.
upload-new-logo: Шинээр лого байршуулах
backdrop: Дэлгэц
backdrop-help-text: Байгууллагын зурган эсвэл дэлгэц.
upload-new-backdrop: Шинээр дэлгэц байршуулах
extensions:
title: Өргөгдөл багууд хамгаалагдаагүй!
message: Та бусад хувилбарууд дээр хүлээж байна уу.
notifications:
select-all:
mark-as-read:
invite:
for-users:
invitation-message: Та {companyName}-д оролцох зөвшөөрлөө авсан байна
invitation-sent-message: Та {appName}-д {companyName} байгууллагт оролцох зөвшөөрлөө илгээлээ. Илгээлт кодыг имэйлээр авч, үргэлжлүүлнэ үү.
invitation-code-sent-text: Таны илгээлтийн код
accept-invitation-text: Зөвшөөрлөө авах
onboard:
index:
title: Таны данс үүсгэх
welcome-title: <strong>{companyName}-д тавтай морилно уу!</strong><br />
welcome-text: Дараах дүрэмтэйгээр мэдээллийг бөглөнө үү.
fullname: Таны овог нэр
email: Таны имэйл хаяг
phone: Таны утасны дугаар
organization-name: Таны байгууллагын нэр, бүх үйлчилгээнд энэ байгууллагаар ямар ч үйлчилгээг удирдах боломжтой болно, дараа нь та байгууллага үүсг

View File

@@ -51,6 +51,15 @@ component:
message: To enhance the security of your account, your organization requires Two-Factor Authentication (2FA). Enable 2FA in your account settings for an additional layer of protection.
button-text: Setup 2FA
auth:
two-fa:
verify-code:
invalid-session-error-notification: Invalid session. Please try again.
verification-successful-notification: Verification successful!
verification-code-expired-notification: Verification code has expired. Please request a new one.
verification-code-failed-notification: Verification failed. Please try again.
resend-code:
verification-code-resent-notification: New verification code sent.
verification-code-resent-error-notification: Error resending verification code. Please try again.
forgot-password:
success-message: Check your email to continue!
is-sent: