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