Author SHA1 Message Date
jakob.scheidandGitea 187332b95a test(legal): add test for the legal notice view
Added tests for the legal notice view (LegalNoticeView.vue).
2026-08-06 23:07:36 +02:00
jakob.scheidandGitea 76041afe56 test(legal): add tests for getLegalNotice 2026-08-06 23:07:36 +02:00
jakob.scheidandGitea 9d4439e415 feat(legal): add legal notice loading indicator 2026-08-06 23:07:36 +02:00
jakob.scheidandGitea ababff7d73 feat(legal): add legal notice error handling
The legal notice view now shows an error box if the legal notice could
not be loaded.
2026-08-06 23:07:36 +02:00
jakob.scheidandGitea 06426e37fd fix(style): set link color to --dark variable
This change was necessary due to the dynamically loaded legal notice.
Links would have the default color without this fix.
2026-08-06 23:07:36 +02:00
jakob.scheidandGitea 3228d811fd feat(legal): load and display legal notice 2026-08-06 23:07:36 +02:00
jakob.scheidandGitea d9801b5c6e feat(legal): add legal notice link in the footer
Added a link to the legal notice in the footer and revised the layout of
the last footer segment: The segment is now a grid container and the
legal links (currently only the legal notice link) are displayed at the
end. The copyright notice remains centered. On small screens, the legal
links are displayed centered below the copyright notice.
2026-08-06 23:07:36 +02:00
jakob.scheidandGitea 0af53975ee feat(legal): add route for the legal notice 2026-08-06 23:07:36 +02:00
20 changed files with 165 additions and 274 deletions
-34
View File
@@ -21,42 +21,8 @@ limitations under the License.
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0158D2" /> <meta name="theme-color" content="#0158D2" />
<title>Seekra</title> <title>Seekra</title>
<noscript>
<style>
body {
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
a {
color: #000;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
</noscript>
</head> </head>
<body> <body>
<noscript>
<h1>
Seekra
</h1>
<p>
You need to enable JavaScript to use Seekra.
</p>
<section>
<h2>Legal Content</h2>
<ul>
<li>
<a href="https://assets.seekra.jcloud-services.ddns.net/2026-08-04-76a067e/legal/legal_notice.html">
Legal Notice
</a>
</li>
</ul>
</section>
</noscript>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>
+1 -7
View File
@@ -18,11 +18,9 @@ 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, onMounted, watch, watchEffect } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
const route = useRoute(); const route = useRoute();
@@ -53,10 +51,6 @@ watch(colorScheme, (newColorScheme) => {
updateColorScheme(colorScheme.value); updateColorScheme(colorScheme.value);
watchEffect(() => updatePageTitle(route)); watchEffect(() => updatePageTitle(route));
onMounted(() => {
setLocale(getSetting(LANGUAGE_PATH));
});
</script> </script>
<template> <template>
-17
View File
@@ -1,17 +0,0 @@
/*
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export const LANGUAGE_PATH = ['general', 'language'];
@@ -17,25 +17,23 @@ 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 { ref } from 'vue';
import { useSettings } from '@/features/settings/composables/useSettings';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { setLocale, SUPPORTED_LANGUAGES } from '@/i18n'; import { loadLanguage, LANGUAGES_RTL, SUPPORTED_LANGUAGES } from '@/i18n';
const { t, locale } = useI18n(); const { t, locale } = useI18n();
const { setSetting, getSetting } = useSettings();
const isOpen = ref(false); const isOpen = ref(false);
const languageDropdown = ref(null); const languageDropdown = ref(null);
const selectLanguage = function selectLanguage (code) { async function selectLanguage(code) {
setSetting(LANGUAGE_PATH, code); await loadLanguage(code);
localStorage.setItem('locale', code);
document.documentElement.lang = code;
document.documentElement.dir = LANGUAGES_RTL.includes(code) ? 'rtl' : 'ltr';
close(); close();
}; };
watch(() => getSetting(LANGUAGE_PATH), setLocale);
const close = function () { const close = function () {
document.removeEventListener('click', closeWrapperOnClickOutsite); document.removeEventListener('click', closeWrapperOnClickOutsite);
isOpen.value = false; isOpen.value = false;
@@ -14,19 +14,41 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { LANGUAGE_PATH } from '@/config/featureSettings.js'; import { expect, test, describe, vi, beforeEach } from 'vitest';
import { useSettings } from '@/features/settings/composables/useSettings.js'; import { mount } from '@vue/test-utils';
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';
const getWrapper = function getWrapper () { vi.mock('@/i18n', () => ({
return mountComponent(LanguageSwitchButton); 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>'
}
}));
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 = getWrapper(); const wrapper = mount(LanguageSwitchButton);
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);
@@ -34,7 +56,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 = getWrapper(); const wrapper = mount(LanguageSwitchButton);
const button = wrapper.find('.language-button'); const button = wrapper.find('.language-button');
await button.trigger('click'); await button.trigger('click');
@@ -43,28 +65,33 @@ describe('LanguageSwitchButton', () => {
expect(button.attributes('aria-expanded')).toBe('true'); expect(button.attributes('aria-expanded')).toBe('true');
}); });
test('sets the language when clicking it', async () => { const languageTestCases = [
const { getSetting } = useSettings(); { code: 'en', expectedDir: 'ltr' },
{ code: 'de', expectedDir: 'ltr' },
{ code: 'ar', expectedDir: 'rtl' }
];
const wrapper = getWrapper(); test.for(languageTestCases)('selectLanguage($code) sets localStorage, html attributes and changes layout direction to $expectedDir', async ({ code, expectedDir }) => {
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');
const enOption = options.find(opt => opt.text().includes('en')); expect(loadLanguage).toHaveBeenCalledWith(code);
await enOption.trigger('click'); expect(localStorage.getItem('locale')).toBe(code);
expect(getSetting(LANGUAGE_PATH)).toBe('en'); expect(document.documentElement.lang).toBe(code);
expect(wrapper.find('.language-dropdown').exists()).toBe(false); expect(document.documentElement.dir).toBe(expectedDir);
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 = getWrapper(); const wrapper = mount(LanguageSwitchButton, {
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);
@@ -1,51 +0,0 @@
/*
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { getNavigatorLanguage } from '../navigatorLanguage';
import { describe, expect, test } from 'vitest';
describe('navigatorLanguage', () => {
test.for([
{ navigatorLanguage: 'en', expected: 'en'},
{ navigatorLanguage: 'de', expected: 'de'},
{ navigatorLanguage: 'fr', expected: 'fr'},
{ navigatorLanguage: 'en-US', expected: 'en'},
{ navigatorLanguage: 'en-AU', expected: 'en'},
{ navigatorLanguage: 'de-DE', expected: 'de'},
{ navigatorLanguage: 'fr-FR', expected: 'fr'},
{ navigatorLanguage: 'en-us', expected: 'en'},
{ navigatorLanguage: 'en-au', expected: 'en'},
{ navigatorLanguage: 'de-de', expected: 'de'},
{ navigatorLanguage: 'fr-fr', expected: 'fr'},
{ navigatorLanguage: 'zh-Hans-CN', expected: 'zh'},
{ navigatorLanguage: 'zh-Hant-TW', expected: 'zh'},
{ navigatorLanguage: 'uz-Latn-UZ', expected: 'uz'},
{ navigatorLanguage: 'en-US-u-ca-gregory', expected: 'en'},
{ navigatorLanguage: 'de-DE-u-co-phonebk', expected: 'de'},
{ navigatorLanguage: 'zh-Hant-TW-u-co-phonebk', expected: 'zh'}
])('returns language $expected with navigator language $navigatorLanguage', ({ navigatorLanguage, expected }) => {
Object.defineProperty(navigator, 'language', {
value: navigatorLanguage,
configurable: true
});
expect(getNavigatorLanguage()).toBe(expected);
});
});
@@ -54,9 +54,10 @@ const props = defineProps({
const store = useSettingsStore(); const store = useSettingsStore();
const optionType = computed(() => props.setting.allowMultiple ? 'checkbox' : 'radio'); const optionType = computed(() => props.setting.allowMultiple ? 'checkbox' : 'radio');
const selected = computed({ const selected = ref(normalizeSelectedValue(store.get(props.path) ?? props.setting.default));
get: () => normalizeSelectedValue(store.get(props.path) ?? props.setting.default),
set: (value) => store.set(props.path, value) watch(selected, (newValue) => {
store.set(props.path, newValue);
}); });
const labelId = useId(); const labelId = useId();
@@ -90,32 +90,6 @@ describe('Selection', () => {
expect(inputsChecked).toStrictEqual([false, true, false]); expect(inputsChecked).toStrictEqual([false, true, false]);
}); });
test('reacts on store change', async () => {
const wrapper = getWrapper({
options: exampleOptions,
piniaOptions: {
setupStores: () => {
const store = useSettingsStore();
store.set('selection', 'o1');
}
}
});
const settings = useSettingsStore();
{
const inputs = wrapper.findAll('input');
const inputsChecked = getChecked(inputs);
expect(inputsChecked).toStrictEqual([false, true, false]);
}
{
settings.set('selection', 'o2');
await nextTick();
const inputs = wrapper.findAll('input');
const inputsChecked = getChecked(inputs);
expect(inputsChecked).toStrictEqual([false, false, true]);
}
});
test('shows multiple values from store', () => { test('shows multiple values from store', () => {
const wrapper = getWrapper({ const wrapper = getWrapper({
options: exampleOptions, options: exampleOptions,
@@ -22,7 +22,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
const settingDefaultValue = vi.hoisted(() => 42); const settingDefaultValue = vi.hoisted(() => 42);
const settingPath = ['test', 'number']; const settingPath = ['test', 'number'];
vi.mock('../../config.js', () => ({ vi.mock('../../settings.json', () => ({
default: { default: {
contents: [ contents: [
{ {
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import settings from '../config.js'; import settings from '../settings.json';
import { useSettingsStore } from '../stores/settingsStore'; import { useSettingsStore } from '../stores/settingsStore';
import { getSettingRecursively } from '../utils/getSetting'; import { getSettingRecursively } from '../utils/getSetting';
-75
View File
@@ -1,75 +0,0 @@
/*
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { getNavigatorLanguage } from '../i18n/utils/navigatorLanguage';
import { fallbackLocale, SUPPORTED_LANGUAGES } from '@/i18n';
let defaultLanguage;
{
const navigatorLanguage = getNavigatorLanguage();
if (SUPPORTED_LANGUAGES.includes(navigatorLanguage)) {
defaultLanguage = navigatorLanguage;
} else {
defaultLanguage = fallbackLocale;
}
}
export default {
contents: [
{
name: 'general',
i18n: 'settings.settings.general.title',
content: [
{
name: 'language',
type: 'selection',
i18n: 'settings.settings.general.settings.language.title',
default: defaultLanguage,
options: SUPPORTED_LANGUAGES.map((language) => ({
name: language,
i18n: `preferences.locale.languages.${language}`
}))
}
]
},
{
name: 'appearance',
i18n: 'settings.settings.appearance.title',
content: [
{
name: 'colorScheme',
type: 'selection',
i18n: 'settings.settings.appearance.contents.colorScheme.title',
default: 'auto',
options: [
{
name: 'auto',
i18n: 'settings.settings.appearance.contents.colorScheme.options.auto'
},
{
name: 'light',
i18n: 'settings.settings.appearance.contents.colorScheme.options.light'
},
{
name: 'dark',
i18n: 'settings.settings.appearance.contents.colorScheme.options.dark'
}
]
}
]
}
]
};
+30
View File
@@ -0,0 +1,30 @@
{
"contents": [
{
"name": "appearance",
"i18n": "settings.settings.appearance.title",
"content": [
{
"name": "colorScheme",
"type": "selection",
"i18n": "settings.settings.appearance.contents.colorScheme.title",
"default": "auto",
"options": [
{
"name": "auto",
"i18n": "settings.settings.appearance.contents.colorScheme.options.auto"
},
{
"name": "light",
"i18n": "settings.settings.appearance.contents.colorScheme.options.light"
},
{
"name": "dark",
"i18n": "settings.settings.appearance.contents.colorScheme.options.dark"
}
]
}
]
}
]
}
+1 -1
View File
@@ -18,7 +18,7 @@ limitations under the License.
import LeftSidebarLayout from '@/layouts/LeftSidebarLayout.vue'; import LeftSidebarLayout from '@/layouts/LeftSidebarLayout.vue';
import SettingsPage from '../components/SettingsPage.vue'; import SettingsPage from '../components/SettingsPage.vue';
import settingsConfiguration from '../config.js'; import settingsConfiguration from '../settings.json';
import { getSettingRecursively } from '../utils/getSetting.js'; import { getSettingRecursively } from '../utils/getSetting.js';
import { computed, onMounted, ref, watch } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -197,7 +197,7 @@ const settings = settingsHoisted.values();
const originalCurrentSection = 's0'; const originalCurrentSection = 's0';
let currentSection = originalCurrentSection; let currentSection = originalCurrentSection;
vi.mock('../../config.js', () => ({ vi.mock('../../settings.json', () => ({
default: { contents: settingsHoisted } default: { contents: settingsHoisted }
})); }));
+3 -8
View File
@@ -15,6 +15,7 @@ limitations under the License.
*/ */
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
import getCurrentLanguage from './utils/currentLanguage';
export const fallbackLocale = 'en'; export const fallbackLocale = 'en';
@@ -33,7 +34,7 @@ export const SUPPORTED_LANGUAGES = [
export const i18n = createI18n({ export const i18n = createI18n({
legacy: false, legacy: false,
locale: fallbackLocale, locale: getCurrentLanguage(),
fallbackLocale: fallbackLocale, fallbackLocale: fallbackLocale,
messages: {} messages: {}
}); });
@@ -56,10 +57,4 @@ 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';
};
-8
View File
@@ -74,14 +74,6 @@
} }
} }
} }
},
"general": {
"title": "Allgemein",
"settings": {
"language": {
"title": "Sprache"
}
}
} }
} }
}, },
-8
View File
@@ -74,14 +74,6 @@
} }
} }
} }
},
"general": {
"title": "General",
"settings": {
"language": {
"title": "Language"
}
}
} }
} }
}, },
+4 -2
View File
@@ -20,6 +20,7 @@ import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import App from './App.vue' import App from './App.vue'
import { i18n, loadLanguage, fallbackLocale } from './i18n'; import { i18n, loadLanguage, fallbackLocale } from './i18n';
import getCurrentLanguage from './utils/currentLanguage';
import router from './router' import router from './router'
import './styles/common.css' import './styles/common.css'
import './styles/variables/colors.css' import './styles/variables/colors.css'
@@ -29,7 +30,7 @@ import rawFonts from './styles/fonts.json';
import './styles/common.css'; import './styles/common.css';
import './styles/variables/colors.css'; import './styles/variables/colors.css';
import settings from './features/settings/config.js'; import settings from './features/settings/settings.json';
import { validateSettingsConfig } from './features/settings/utils/settingsValidator'; import { validateSettingsConfig } from './features/settings/utils/settingsValidator';
(async () => { (async () => {
@@ -52,7 +53,8 @@ import { validateSettingsConfig } from './features/settings/utils/settingsValida
fonts.forEach((font) => document.fonts.add(font)); fonts.forEach((font) => document.fonts.add(font));
await loadLanguage(fallbackLocale); await loadLanguage(fallbackLocale);
await loadLanguage(getCurrentLanguage());
const pinia = createPinia(); const pinia = createPinia();
pinia.use(piniaPluginPersistedstate); pinia.use(piniaPluginPersistedstate);
@@ -0,0 +1,61 @@
/*
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { describe, expect, test } from 'vitest';
import getCurrentLanguage from '../currentLanguage';
describe('currentLanguage', () => {
test.for([
{ navigatorLanguage: 'en', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'en-US', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'en-AU', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-DE', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr-FR', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'en-us', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'en-au', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-de', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr-fr', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'zh-Hans-CN', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'zh-Hant-TW', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'uz-Latn-UZ', localStorageLanguage: null, expected: 'uz'},
{ navigatorLanguage: 'en-US-u-ca-gregory', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-DE-u-co-phonebk', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'zh-Hant-TW-u-co-phonebk', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'en', localStorageLanguage: 'de', expected: 'de'},
{ navigatorLanguage: 'de-DE', localStorageLanguage: 'en', expected: 'en'},
{ navigatorLanguage: 'de-de', localStorageLanguage: 'en', expected: 'en'},
{ navigatorLanguage: 'zh-Hans-CN', localStorageLanguage: 'fr', expected: 'fr'},
{ navigatorLanguage: 'en-US-u-ca-gregory', localStorageLanguage: 'zh', expected: 'zh'}
])('returns the language $expected (navigator: $navigatorLanguage; local storage: $localStorageLanguage)', ({ navigatorLanguage, localStorageLanguage, expected }) => {
Object.defineProperty(navigator, 'language', {
value: navigatorLanguage,
configurable: true
});
if (localStorageLanguage) {
localStorage.setItem('locale', localStorageLanguage);
};
expect(getCurrentLanguage()).toBe(expected);
});
});
@@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export const getNavigatorLanguage = function getNavigatorLanguage () { export default function getCurrentLanguage () {
const saved = localStorage.getItem('locale');
if (saved) return saved;
const locale = new Intl.Locale(navigator.language); const locale = new Intl.Locale(navigator.language);
return locale.language; return locale.language;
}; };