Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 58 additions & 3 deletions apps/settings/lib/Settings/Personal/PersonalInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -224,18 +224,47 @@ function (IAccountProperty $property) {
return $emailMap;
}

/**
* Validates a forced language setting against available languages
*/
private function validateForcedLanguage(string $forcedLanguage, array $languages): ?array {
$allLanguages = array_merge(
$languages['commonLanguages'] ?? [],
$languages['otherLanguages'] ?? []
);
$matchingLanguages = array_filter(
$allLanguages,
fn($lang) => $lang['code'] === $forcedLanguage
);
$matchingLanguage = reset($matchingLanguages);

if ($matchingLanguage && isset($matchingLanguage['name'])) {
return [
'code' => $forcedLanguage,
'name' => $matchingLanguage['name']
];
}

return null;
}

/**
* returns the user's active language, common languages, and other languages in an
* associative array
*/
private function getLanguageMap(IUser $user): array {
$forceLanguage = $this->config->getSystemValue('force_language', false);
if ($forceLanguage !== false) {
$languages = $this->l10nFactory->getLanguages();
$validated = $this->validateForcedLanguage($forceLanguage, $languages);

if ($validated !== null) {
return ['forcedLanguage' => $validated];
}
return [];
}

$uid = $user->getUID();

$userConfLang = $this->config->getUserValue($uid, 'core', 'lang', $this->l10nFactory->findLanguage());
$languages = $this->l10nFactory->getLanguages();

Expand All @@ -261,9 +290,35 @@ private function getLanguageMap(IUser $user): array {
);
}

/**
* Validates a forced locale setting against available locales
*/
private function validateForcedLocale(string $forcedLocale, array $localeCodes): ?array {
$matchingLocales = array_filter(
$localeCodes,
fn($locale) => $locale['code'] === $forcedLocale
);
$matchingLocale = reset($matchingLocales);

if ($matchingLocale && isset($matchingLocale['name'])) {
return [
'code' => $forcedLocale,
'name' => $matchingLocale['name']
];
}

return null;
}

private function getLocaleMap(IUser $user): array {
$forceLanguage = $this->config->getSystemValue('force_locale', false);
if ($forceLanguage !== false) {
$forceLocale = $this->config->getSystemValue('force_locale', false);
if ($forceLocale !== false) {
$localeCodes = $this->l10nFactory->findAvailableLocales();
$validated = $this->validateForcedLocale($forceLocale, $localeCodes);

if ($validated !== null) {
return ['forcedLocale' => $validated];
}
return [];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { loadState } from '@nextcloud/initial-state'
import LanguageSection from './LanguageSection.vue'

/**
* Mock Nextcloud modules
*/
vi.mock('@nextcloud/initial-state', () => ({
loadState: vi.fn(() => ({
languageMap: {
activeLanguage: { code: 'en', name: 'English' },
commonLanguages: [],
otherLanguages: [],
},
})),
}))

describe('LanguageSection', () => {
let wrapper

const mountComponent = () => {
return mount(LanguageSection, {
stubs: {
Language: {
template: '<div data-test="language-select" />',
},
HeaderBar: {
template: '<div data-test="header-bar" />',
},
},
})
}

describe('when the language is user-configurable', () => {
beforeEach(async () => {
const userConfigurableData = {
languageMap: {
activeLanguage: { code: 'en', name: 'English' },
commonLanguages: [{ code: 'en', name: 'English' }],
otherLanguages: [{ code: 'de', name: 'German' }],
},
}
vi.mocked(loadState).mockReturnValueOnce(userConfigurableData)
wrapper = mountComponent()
await wrapper.vm.$nextTick()
})

it('shows the language select component', () => {
expect(wrapper.find('[data-test="language-select"]').exists()).toBe(true)
})

})

describe('when there is no language data', () => {
beforeEach(async () => {
const noLanguageData = { languageMap: {} }
vi.mocked(loadState).mockReturnValueOnce(noLanguageData)
wrapper = mountComponent()
await wrapper.vm.$nextTick()
})

it('shows no language component', () => {
expect(wrapper.find('[data-test="no-language-message"]').exists()).toBe(true)
})
})

describe('when the language is forced by the administrator', () => {
beforeEach(async () => {
const forcedLanguageData = {
languageMap: {
forcedLanguage: { code: 'uk', name: 'Ukrainian' },
},
}
vi.mocked(loadState).mockReturnValueOnce(forcedLanguageData)
wrapper = mountComponent()
await wrapper.vm.$nextTick()
})

it('shows forced language component', () => {
expect(wrapper.find('[data-test="forced-language-message"]').exists()).toBe(true)
})

})

afterEach(() => {
if (wrapper) {
wrapper.destroy()
wrapper = null
}
vi.resetAllMocks()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,32 @@
:readable="propertyReadable" />

<Language v-if="isEditable"
data-test="language-select"
:input-id="inputId"
:common-languages="commonLanguages"
:other-languages="otherLanguages"
:language.sync="language" />

<span v-else>
<span v-else-if="forcedLanguage && forcedLanguage.name"
data-test="forced-language-message">
{{ t('settings', 'Language is forced to {language} by the administrator', { language: forcedLanguage.name }) }}
</span>
<span v-else
data-test="no-language-message">
{{ t('settings', 'No language set') }}
</span>
</section>
</template>

<script>
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'

import Language from './Language.vue'
import HeaderBar from '../shared/HeaderBar.vue'

import { ACCOUNT_SETTING_PROPERTY_ENUM, ACCOUNT_SETTING_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants.js'

const { languageMap: { activeLanguage, commonLanguages, otherLanguages } } = loadState('settings', 'personalInfoParameters', {})

export default {
name: 'LanguageSection',

Expand All @@ -41,15 +46,23 @@ export default {
setup() {
// Non reactive instance properties
return {
commonLanguages,
otherLanguages,
propertyReadable: ACCOUNT_SETTING_PROPERTY_READABLE_ENUM.LANGUAGE,
}
},

data() {
const state = loadState('settings', 'personalInfoParameters', {})
const { activeLanguage, commonLanguages, otherLanguages, forcedLanguage } = state.languageMap || {}
return {
language: activeLanguage,
language: activeLanguage || null,
forcedLanguage: forcedLanguage && forcedLanguage.name
? {
code: forcedLanguage.code,
name: forcedLanguage.name,
}
: null,
commonLanguages: forcedLanguage ? [] : (commonLanguages || []),
otherLanguages: forcedLanguage ? [] : (otherLanguages || []),
}
},

Expand All @@ -59,9 +72,14 @@ export default {
},

isEditable() {
return Boolean(this.language)
// Return false if language is forced or there's no active language
return !this.forcedLanguage && this.language !== null
},
},

methods: {
t,
},
}
</script>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { loadState } from '@nextcloud/initial-state'
import LocaleSection from './LocaleSection.vue'

vi.mock('@nextcloud/initial-state', () => ({
loadState: vi.fn(() => ({
localeMap: {
activeLocale: { code: 'en_GB', name: 'English (United Kingdom)' },
localesForLanguage: [],
otherLocales: [],
},
})),
}))

describe('LocaleSection', () => {
let wrapper

const mountComponent = () => {
return mount(LocaleSection, {
stubs: {
Locale: {
template: '<div data-test="locale-select" />',
},
HeaderBar: {
template: '<div data-test="header-bar" />',
},
NcPasswordField: {
template: '<input type="password" />',
},
},
})
}

describe('when the locale is user-configurable', () => {
beforeEach(async () => {
const userConfigurableData = {
localeMap: {
activeLocale: { code: 'en_GB', name: 'English (United Kingdom)' },
localesForLanguage: [{ code: 'en_GB', name: 'English (United Kingdom)' }],
otherLocales: [{ code: 'en_US', name: 'English (United States)' }],
},
}
vi.mocked(loadState).mockReturnValueOnce(userConfigurableData)
wrapper = mountComponent()
await wrapper.vm.$nextTick()
})

it('shows the locale select component', () => {
expect(wrapper.find('[data-test="locale-select"]').exists()).toBe(true)
})
})

describe('when there is no locale data', () => {
beforeEach(async () => {
const noLocaleData = { localeMap: {} }
vi.mocked(loadState).mockReturnValueOnce(noLocaleData)
wrapper = mountComponent()
await wrapper.vm.$nextTick()
})

it('shows no locale component', () => {
expect(wrapper.find('[data-test="no-locale-message"]').exists()).toBe(true)
})
})

describe('when the locale is forced by the administrator', () => {
beforeEach(async () => {
const forcedLocaleData = {
localeMap: {
forcedLocale: { code: 'uk_UA', name: 'Ukrainian' },
},
}
vi.mocked(loadState).mockReturnValueOnce(forcedLocaleData)
wrapper = mountComponent()
await wrapper.vm.$nextTick()
})

it('shows forced locale component', () => {
expect(wrapper.find('[data-test="forced-locale-message"]').exists()).toBe(true)
})

})

afterEach(() => {
if (wrapper) {
wrapper.destroy()
wrapper = null
}
vi.resetAllMocks()
})
})
Loading