generated from Seekra/repository-template
Feat(i18n): make language a setting #185
@@ -18,8 +18,10 @@ limitations under the License.
|
||||
import Navbar from './features/nav/components/Navbar.vue';
|
||||
import Footer from './features/footer/components/Footer.vue';
|
||||
|
||||
import { LANGUAGE_PATH } from './config/featureSettings.js';
|
||||
import { updatePageTitle } from './router';
|
||||
import { useSettings } from './features/settings/composables/useSettings.js';
|
||||
import { setLocale } from './i18n';
|
||||
import { computed, watch, watchEffect } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
@@ -51,6 +53,8 @@ watch(colorScheme, (newColorScheme) => {
|
||||
updateColorScheme(colorScheme.value);
|
||||
|
||||
watchEffect(() => updatePageTitle(route));
|
||||
|
||||
watch(() => getSetting(LANGUAGE_PATH), setLocale);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -17,20 +17,20 @@ limitations under the License.
|
||||
<script setup>
|
||||
import Icon from '@/features/icons/components/Icon.vue';
|
||||
|
||||
import { LANGUAGE_PATH } from '@/config/featureSettings';
|
||||
import { useSettings } from '@/features/settings/composables/useSettings';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { loadLanguage, LANGUAGES_RTL, SUPPORTED_LANGUAGES } from '@/i18n';
|
||||
import { SUPPORTED_LANGUAGES } from '@/i18n';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const { setSetting } = useSettings();
|
||||
|
||||
const isOpen = ref(false);
|
||||
const languageDropdown = ref(null);
|
||||
|
||||
async function selectLanguage(code) {
|
||||
await loadLanguage(code);
|
||||
localStorage.setItem('locale', code);
|
||||
document.documentElement.lang = code;
|
||||
document.documentElement.dir = LANGUAGES_RTL.includes(code) ? 'rtl' : 'ltr';
|
||||
const selectLanguage = function selectLanguage (code) {
|
||||
setSetting(LANGUAGE_PATH, code);
|
||||
close();
|
||||
};
|
||||
|
||||
|
||||
@@ -14,41 +14,19 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { expect, test, describe, vi, beforeEach } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { LANGUAGE_PATH } from '@/config/featureSettings.js';
|
||||
import { useSettings } from '@/features/settings/composables/useSettings.js';
|
||||
import { mountComponent } from '@/test-utils/mountComponent.js';
|
||||
import { expect, test, describe } from 'vitest';
|
||||
import LanguageSwitchButton from '../LanguageSwitchButton.vue';
|
||||
import { loadLanguage } from '@/i18n';
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
loadLanguage: vi.fn(() => Promise.resolve()),
|
||||
LANGUAGES_RTL: ['ar', 'he'],
|
||||
SUPPORTED_LANGUAGES: ['en', 'de', 'ar']
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key,
|
||||
locale: { value: 'de' }
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('@/features/icons/components/Icon.vue', () => ({
|
||||
default: {
|
||||
name: 'Icon',
|
||||
template: '<span>Icon</span>'
|
||||
}
|
||||
}));
|
||||
const getWrapper = function getWrapper () {
|
||||
return mountComponent(LanguageSwitchButton);
|
||||
};
|
||||
|
||||
describe('LanguageSwitchButton', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
document.documentElement.lang = '';
|
||||
document.documentElement.dir = '';
|
||||
});
|
||||
|
||||
test('renders correctly with initial state closed', () => {
|
||||
const wrapper = mount(LanguageSwitchButton);
|
||||
const wrapper = getWrapper();
|
||||
|
||||
expect(wrapper.find('.language-button').exists()).toBe(true);
|
||||
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
|
||||
@@ -56,7 +34,7 @@ describe('LanguageSwitchButton', () => {
|
||||
});
|
||||
|
||||
test('opens the dropdown when language button is clicked', async () => {
|
||||
const wrapper = mount(LanguageSwitchButton);
|
||||
const wrapper = getWrapper();
|
||||
const button = wrapper.find('.language-button');
|
||||
|
||||
await button.trigger('click');
|
||||
@@ -65,33 +43,28 @@ describe('LanguageSwitchButton', () => {
|
||||
expect(button.attributes('aria-expanded')).toBe('true');
|
||||
});
|
||||
|
||||
const languageTestCases = [
|
||||
{ code: 'en', expectedDir: 'ltr' },
|
||||
{ code: 'de', expectedDir: 'ltr' },
|
||||
{ code: 'ar', expectedDir: 'rtl' }
|
||||
];
|
||||
test('sets the language when clicking it', async () => {
|
||||
const { getSetting } = useSettings();
|
||||
|
||||
test.for(languageTestCases)('selectLanguage($code) sets localStorage, html attributes and changes layout direction to $expectedDir', async ({ code, expectedDir }) => {
|
||||
const wrapper = mount(LanguageSwitchButton);
|
||||
const wrapper = getWrapper();
|
||||
|
||||
await wrapper.find('.language-button').trigger('click');
|
||||
|
||||
const options = wrapper.findAll('.language-dropdown li');
|
||||
const optionToClick = options.find(opt => opt.text().includes(code));
|
||||
|
||||
await optionToClick.trigger('click');
|
||||
|
||||
expect(loadLanguage).toHaveBeenCalledWith(code);
|
||||
expect(localStorage.getItem('locale')).toBe(code);
|
||||
expect(document.documentElement.lang).toBe(code);
|
||||
expect(document.documentElement.dir).toBe(expectedDir);
|
||||
const enOption = options.find(opt => opt.text().includes('en'));
|
||||
await enOption.trigger('click');
|
||||
expect(getSetting(LANGUAGE_PATH)).toBe('en');
|
||||
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
|
||||
|
||||
const deOption = options.find(opt => opt.text().includes('de'));
|
||||
await deOption.trigger('click');
|
||||
expect(getSetting(LANGUAGE_PATH)).toBe('de');
|
||||
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('closes the dropdown when clicking outside the component', async () => {
|
||||
const wrapper = mount(LanguageSwitchButton, {
|
||||
attachTo: document.body
|
||||
});
|
||||
const wrapper = getWrapper();
|
||||
|
||||
await wrapper.find('.language-button').trigger('click');
|
||||
expect(wrapper.find('.language-dropdown').exists()).toBe(true);
|
||||
|
||||
+7
-1
@@ -57,4 +57,10 @@ export async function loadLanguage (locale) {
|
||||
i18n.global.locale.value = locale;
|
||||
|
||||
loadedLanguages.add(locale);
|
||||
};
|
||||
};
|
||||
|
||||
export const setLocale = async function setLocale (locale) {
|
||||
await loadLanguage(locale);
|
||||
document.documentElement.lang = locale;
|
||||
document.documentElement.dir = LANGUAGES_RTL.includes(locale) ? 'rtl' : 'ltr';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user