Skip to content

Commit 767d1d3

Browse files
committed
fix(settings): show active forced locale or language instead of 'No locale set' (fixes #41543)
Signed-off-by: Annabel Church <215145+arc64@users.noreply.github.com>
1 parent 27149b7 commit 767d1d3

File tree

5 files changed

+403
-16
lines changed

5 files changed

+403
-16
lines changed

apps/settings/lib/Settings/Personal/PersonalInfo.php

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,18 +224,41 @@ function (IAccountProperty $property) {
224224
return $emailMap;
225225
}
226226

227+
/**
228+
* Validates a forced language setting against available languages
229+
*/
230+
private function validateForcedLanguage(string $forcedLanguage, array $languages): ?array {
231+
$allLanguages = array_merge($languages['commonLanguages'], $languages['otherLanguages']);
232+
$forcedLang = array_filter($allLanguages, fn($lang) => $lang['code'] === $forcedLanguage);
233+
$forcedLang = reset($forcedLang);
234+
235+
if ($forcedLang && isset($forcedLang['name'])) {
236+
return [
237+
'code' => $forcedLanguage,
238+
'name' => $forcedLang['name']
239+
];
240+
}
241+
242+
return null;
243+
}
244+
227245
/**
228246
* returns the user's active language, common languages, and other languages in an
229247
* associative array
230248
*/
231249
private function getLanguageMap(IUser $user): array {
232250
$forceLanguage = $this->config->getSystemValue('force_language', false);
233251
if ($forceLanguage !== false) {
252+
$languages = $this->l10nFactory->getLanguages();
253+
$validated = $this->validateForcedLanguage($forceLanguage, $languages);
254+
255+
if ($validated !== null) {
256+
return ['forcedLanguage' => $validated];
257+
}
234258
return [];
235259
}
236260

237261
$uid = $user->getUID();
238-
239262
$userConfLang = $this->config->getUserValue($uid, 'core', 'lang', $this->l10nFactory->findLanguage());
240263
$languages = $this->l10nFactory->getLanguages();
241264

@@ -261,9 +284,32 @@ private function getLanguageMap(IUser $user): array {
261284
);
262285
}
263286

287+
/**
288+
* Validates a forced locale setting against available locales
289+
*/
290+
private function validateForcedLocale(string $forcedLocale, array $localeCodes): ?array {
291+
$forcedLocaleObj = array_filter($localeCodes, fn($locale) => $locale['code'] === $forcedLocale);
292+
$forcedLocaleObj = reset($forcedLocaleObj);
293+
294+
if ($forcedLocaleObj && isset($forcedLocaleObj['name'])) {
295+
return [
296+
'code' => $forcedLocale,
297+
'name' => $forcedLocaleObj['name']
298+
];
299+
}
300+
301+
return null;
302+
}
303+
264304
private function getLocaleMap(IUser $user): array {
265-
$forceLanguage = $this->config->getSystemValue('force_locale', false);
266-
if ($forceLanguage !== false) {
305+
$forceLocale = $this->config->getSystemValue('force_locale', false);
306+
if ($forceLocale !== false) {
307+
$localeCodes = $this->l10nFactory->findAvailableLocales();
308+
$validated = $this->validateForcedLocale($forceLocale, $localeCodes);
309+
310+
if ($validated !== null) {
311+
return ['forcedLocale' => $validated];
312+
}
267313
return [];
268314
}
269315

apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
:other-languages="otherLanguages"
1515
:language.sync="language" />
1616

17+
<span v-else-if="forcedLanguage && forcedLanguage.name">
18+
{{ t('settings', 'Language is forced to {language} by the administrator', { language: forcedLanguage.name }) }}
19+
</span>
1720
<span v-else>
1821
{{ t('settings', 'No language set') }}
1922
</span>
@@ -22,14 +25,13 @@
2225

2326
<script>
2427
import { loadState } from '@nextcloud/initial-state'
28+
import { t } from '@nextcloud/l10n'
2529
2630
import Language from './Language.vue'
2731
import HeaderBar from '../shared/HeaderBar.vue'
2832
2933
import { ACCOUNT_SETTING_PROPERTY_ENUM, ACCOUNT_SETTING_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants.js'
3034
31-
const { languageMap: { activeLanguage, commonLanguages, otherLanguages } } = loadState('settings', 'personalInfoParameters', {})
32-
3335
export default {
3436
name: 'LanguageSection',
3537
@@ -41,15 +43,23 @@ export default {
4143
setup() {
4244
// Non reactive instance properties
4345
return {
44-
commonLanguages,
45-
otherLanguages,
4646
propertyReadable: ACCOUNT_SETTING_PROPERTY_READABLE_ENUM.LANGUAGE,
4747
}
4848
},
4949
5050
data() {
51+
const state = loadState('settings', 'personalInfoParameters', {})
52+
const { activeLanguage, commonLanguages, otherLanguages, forcedLanguage } = state.languageMap || {}
5153
return {
52-
language: activeLanguage,
54+
language: activeLanguage || null,
55+
forcedLanguage: forcedLanguage && forcedLanguage.name
56+
? {
57+
code: forcedLanguage.code,
58+
name: forcedLanguage.name,
59+
}
60+
: null,
61+
commonLanguages: forcedLanguage ? [] : (commonLanguages || []),
62+
otherLanguages: forcedLanguage ? [] : (otherLanguages || []),
5363
}
5464
},
5565
@@ -59,9 +69,15 @@ export default {
5969
},
6070
6171
isEditable() {
62-
return Boolean(this.language)
72+
// Return false if language is forced or there's no active language
73+
return !this.forcedLanguage && this.language !== null
6374
},
6475
},
76+
77+
methods: {
78+
t,
79+
}
80+
6581
}
6682
</script>
6783

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { mount } from '@vue/test-utils'
2+
import { describe, it, expect, vi, beforeEach } from 'vitest'
3+
import { loadState } from '@nextcloud/initial-state'
4+
import LanguageSection from '../LanguageSection.vue'
5+
6+
/**
7+
* Mock child components
8+
*/
9+
vi.mock('../Language.vue', () => ({
10+
default: {
11+
name: 'Language',
12+
template: '<div class="language"></div>',
13+
props: {
14+
inputId: String,
15+
commonLanguages: Array,
16+
otherLanguages: Array,
17+
language: Object,
18+
},
19+
},
20+
}))
21+
22+
vi.mock('../shared/HeaderBar.vue', () => ({
23+
default: {
24+
name: 'HeaderBar',
25+
template: '<div class="header-bar"></div>',
26+
props: {
27+
inputId: String,
28+
readable: String,
29+
},
30+
},
31+
}))
32+
33+
/**
34+
* Mock Nextcloud modules
35+
*/
36+
vi.mock('../../../constants/AccountPropertyConstants.js', () => ({
37+
ACCOUNT_SETTING_PROPERTY_ENUM: { LANGUAGE: 'language' },
38+
ACCOUNT_SETTING_PROPERTY_READABLE_ENUM: { LANGUAGE: 'Language' },
39+
}))
40+
41+
vi.mock('@nextcloud/initial-state', () => ({
42+
loadState: vi.fn(() => ({
43+
languageMap: {
44+
activeLanguage: { code: 'en', name: 'English' },
45+
commonLanguages: [{ code: 'en', name: 'English' }],
46+
otherLanguages: [{ code: 'de', name: 'German' }],
47+
},
48+
})),
49+
}))
50+
51+
vi.mock('@nextcloud/l10n', () => ({
52+
t: (app, text, params) => {
53+
if (params) {
54+
return text.replace(/\{(\w+)\}/g, (match, key) => params[key] || match)
55+
}
56+
return text
57+
},
58+
getLanguage: () => 'en',
59+
isRTL: () => false,
60+
translate: (app, text, params) => {
61+
if (params) {
62+
return text.replace(/\{(\w+)\}/g, (match, key) => params[key] || match)
63+
}
64+
return text
65+
},
66+
}))
67+
68+
describe('LanguageSection', () => {
69+
let wrapper
70+
71+
const mountComponent = () => {
72+
return mount(LanguageSection)
73+
}
74+
75+
beforeEach(() => {
76+
wrapper = mountComponent()
77+
})
78+
79+
describe('when language is user-configurable', () => {
80+
const validLanguageData = {
81+
languageMap: {
82+
activeLanguage: { code: 'en', name: 'English' },
83+
commonLanguages: [{ code: 'en', name: 'English' }],
84+
otherLanguages: [{ code: 'de', name: 'German' }],
85+
},
86+
}
87+
88+
beforeEach(async () => {
89+
vi.mocked(loadState).mockReturnValueOnce(validLanguageData)
90+
wrapper = mountComponent()
91+
await wrapper.vm.$nextTick()
92+
})
93+
94+
it('enables language selection', () => {
95+
expect(wrapper.vm.isEditable).toBe(true)
96+
expect(wrapper.findComponent({ name: 'Language' }).exists()).toBe(true)
97+
})
98+
99+
it('passes correct props to Language', () => {
100+
const language = wrapper.findComponent({ name: 'Language' })
101+
expect(language.props('inputId')).toBe('account-setting-language')
102+
expect(language.props('commonLanguages')).toEqual(validLanguageData.languageMap.commonLanguages)
103+
expect(language.props('otherLanguages')).toEqual(validLanguageData.languageMap.otherLanguages)
104+
expect(language.props('language')).toEqual(validLanguageData.languageMap.activeLanguage)
105+
})
106+
})
107+
108+
describe('with empty language data', () => {
109+
it('handles empty languageMap', async () => {
110+
const emptyData = { languageMap: {} }
111+
vi.mocked(loadState).mockReturnValueOnce(emptyData)
112+
wrapper = mountComponent()
113+
await wrapper.vm.$nextTick()
114+
expect(wrapper.vm.isEditable).toBe(false)
115+
expect(wrapper.vm.language).toBeNull()
116+
expect(wrapper.vm.commonLanguages).toEqual([])
117+
expect(wrapper.vm.otherLanguages).toEqual([])
118+
expect(wrapper.vm.forcedLanguage).toBeNull()
119+
expect(wrapper.findComponent({ name: 'Language' }).exists()).toBe(false)
120+
})
121+
})
122+
123+
describe('when language is forced by administrator', () => {
124+
const forcedLanguageData = {
125+
languageMap: {
126+
forcedLanguage: { code: 'de', name: 'German' },
127+
},
128+
}
129+
130+
beforeEach(async () => {
131+
vi.mocked(loadState).mockReturnValueOnce(forcedLanguageData)
132+
wrapper = mountComponent()
133+
await wrapper.vm.$nextTick()
134+
})
135+
136+
it('disables language selection', () => {
137+
expect(wrapper.vm.isEditable).toBe(false)
138+
expect(wrapper.findComponent({ name: 'Language' }).exists()).toBe(false)
139+
})
140+
141+
it('displays forced language message', () => {
142+
expect(wrapper.text()).toContain('Language is forced to German by the administrator')
143+
})
144+
145+
it('initializes with forced language state', () => {
146+
expect(wrapper.vm.forcedLanguage).toEqual(forcedLanguageData.languageMap.forcedLanguage)
147+
expect(wrapper.vm.language).toBeNull()
148+
expect(wrapper.vm.commonLanguages).toEqual([])
149+
expect(wrapper.vm.otherLanguages).toEqual([])
150+
})
151+
})
152+
})

apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
:other-locales="otherLocales"
1515
:locale.sync="locale" />
1616

17+
<span v-else-if="forcedLocale && forcedLocale.name">
18+
{{ t('settings', 'Locale is forced to {locale} by the administrator', { locale: forcedLocale.name }) }}
19+
</span>
1720
<span v-else>
1821
{{ t('settings', 'No locale set') }}
1922
</span>
@@ -22,14 +25,13 @@
2225

2326
<script>
2427
import { loadState } from '@nextcloud/initial-state'
28+
import { t } from '@nextcloud/l10n'
2529
2630
import Locale from './Locale.vue'
2731
import HeaderBar from '../shared/HeaderBar.vue'
2832
2933
import { ACCOUNT_SETTING_PROPERTY_ENUM, ACCOUNT_SETTING_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants.js'
3034
31-
const { localeMap: { activeLocale, localesForLanguage, otherLocales } } = loadState('settings', 'personalInfoParameters', {})
32-
3335
export default {
3436
name: 'LocaleSection',
3537
@@ -38,12 +40,26 @@ export default {
3840
HeaderBar,
3941
},
4042
41-
data() {
43+
setup() {
44+
// Non reactive instance properties
4245
return {
4346
propertyReadable: ACCOUNT_SETTING_PROPERTY_READABLE_ENUM.LOCALE,
44-
localesForLanguage,
45-
otherLocales,
46-
locale: activeLocale,
47+
}
48+
},
49+
50+
data() {
51+
const state = loadState('settings', 'personalInfoParameters', {})
52+
const { activeLocale, localesForLanguage, otherLocales, forcedLocale } = state.localeMap || {}
53+
return {
54+
locale: activeLocale || null,
55+
forcedLocale: forcedLocale && forcedLocale.name
56+
? {
57+
code: forcedLocale.code,
58+
name: forcedLocale.name,
59+
}
60+
: null,
61+
localesForLanguage: forcedLocale ? [] : (localesForLanguage || []),
62+
otherLocales: forcedLocale ? [] : (otherLocales || []),
4763
}
4864
},
4965
@@ -53,9 +69,14 @@ export default {
5369
},
5470
5571
isEditable() {
56-
return Boolean(this.locale)
72+
// Return false if there's no locale data or if locale is forced
73+
return !this.forcedLocale && (this.locale !== null || this.localesForLanguage.length > 0)
5774
},
5875
},
76+
77+
methods: {
78+
t,
79+
},
5980
}
6081
</script>
6182

0 commit comments

Comments
 (0)