1/*
2 * Copyright 2019-2024, Andrew Lindesay <apl@lindesay.co.nz>.
3 * All rights reserved. Distributed under the terms of the MIT License.
4 */
5#include "LanguageModel.h"
6
7#include <algorithm>
8
9#include <Locale.h>
10#include <LocaleRoster.h>
11
12#include "HaikuDepotConstants.h"
13#include "Logger.h"
14#include "LocaleUtils.h"
15
16
17LanguageModel::LanguageModel()
18	:
19	fPreferredLanguage(LanguageRef(new Language(LANGUAGE_DEFAULT)))
20{
21	const Language defaultLanguage = _DeriveDefaultLanguage();
22	fSupportedLanguages.push_back(LanguageRef(
23		new Language(defaultLanguage), true));
24	_SetPreferredLanguage(defaultLanguage);
25}
26
27
28LanguageModel::LanguageModel(BString forcedSystemDefaultLanguage)
29	:
30	fForcedSystemDefaultLanguage(forcedSystemDefaultLanguage)
31{
32}
33
34
35LanguageModel::~LanguageModel()
36{
37}
38
39
40void
41LanguageModel::ClearSupportedLanguages()
42{
43	fSupportedLanguages.clear();
44	HDINFO("did clear the supported languages");
45}
46
47
48const int32
49LanguageModel::CountSupportedLanguages() const
50{
51	return fSupportedLanguages.size();
52}
53
54
55const LanguageRef
56LanguageModel::SupportedLanguageAt(int32 index) const
57{
58	return fSupportedLanguages[index];
59}
60
61
62void
63LanguageModel::AddSupportedLanguage(const LanguageRef& value)
64{
65	int32 index = _IndexOfSupportedLanguage(value->Code(), value->CountryCode(),
66		value->ScriptCode());
67
68	if (-1 == index) {
69		std::vector<LanguageRef>::iterator itInsertionPt
70			= std::lower_bound(
71				fSupportedLanguages.begin(), fSupportedLanguages.end(),
72				value, &_IsLanguageBefore);
73		fSupportedLanguages.insert(itInsertionPt, value);
74		HDTRACE("did add the supported language [%s]" , value->ID());
75	}
76	else {
77		fSupportedLanguages[index] = value;
78		HDTRACE("did replace the supported language [%s]", value->ID());
79	}
80}
81
82
83void
84LanguageModel::SetPreferredLanguageToSystemDefault()
85{
86	// it could be that the preferred language does not exist in the
87	// list.  In this case it is necessary to choose one from the list.
88	_SetPreferredLanguage(_DeriveDefaultLanguage());
89}
90
91
92void
93LanguageModel::_SetPreferredLanguage(const Language& language)
94{
95	fPreferredLanguage = LanguageRef(new Language(language));
96	HDDEBUG("set preferred language [%s]", fPreferredLanguage->ID());
97}
98
99
100/*! This will derive the default language.  If there are no other
101    possible languages configured then the default language will be
102    assumed to exist.  Otherwise if there is a set of possible languages
103    then this method will ensure that the default language is in that
104    set.
105*/
106
107Language
108LanguageModel::_DeriveDefaultLanguage() const
109{
110	Language defaultLanguage = _DeriveSystemDefaultLanguage();
111	HDDEBUG("derived system default language [%s]", defaultLanguage.ID());
112
113	// if there are no supported languages; as is the case to start with as the
114	// application starts, the default language from the system is used anyway.
115	// The data queried in HDS will handle the case where the language is not
116	// 'known' at the HDS end so it doesn't matter if it is invalid when the
117	// HaikuDepot application requests data from the HaikuDepotServer system.
118
119	if (fSupportedLanguages.empty()) {
120		HDTRACE("no supported languages --> will use default language");
121		return defaultLanguage;
122	}
123
124	// if there are supported languages defined then the preferred language
125	// needs to be one of the supported ones.
126
127	Language* foundSupportedLanguage = _FindBestSupportedLanguage(
128		defaultLanguage.Code(),
129		defaultLanguage.CountryCode(),
130		defaultLanguage.ScriptCode());
131
132	if (foundSupportedLanguage == NULL) {
133		HDERROR("unable to find the language [%s] so will look for app default [%s]",
134			defaultLanguage.ID(), LANGUAGE_DEFAULT.ID());
135		foundSupportedLanguage = _FindBestSupportedLanguage(
136			LANGUAGE_DEFAULT.Code(),
137			LANGUAGE_DEFAULT.CountryCode(),
138			LANGUAGE_DEFAULT.ScriptCode());
139
140		if (foundSupportedLanguage == NULL) {
141			HDERROR("unable to find the app default language [%s] in the supported language so"
142				" will use the first supported language [%s]", LANGUAGE_DEFAULT.ID(),
143				fSupportedLanguages[0]->ID());
144			foundSupportedLanguage = fSupportedLanguages[0];
145		}
146	} else {
147		HDTRACE("did find supported language [%s] as best match to [%s] from %" B_PRIu32
148			" supported languages", foundSupportedLanguage->ID(), defaultLanguage.ID(),
149			CountSupportedLanguages());
150	}
151
152	return Language(*foundSupportedLanguage);
153}
154
155
156/*! This method will create a `Language` object that represents the user's
157	preferred language based on their system preferences. If it cannot find
158	any such language then it will return the absolute default (English).
159*/
160
161Language
162LanguageModel::_DeriveSystemDefaultLanguage() const
163{
164	if (!fForcedSystemDefaultLanguage.IsEmpty())
165		return Language(fForcedSystemDefaultLanguage, fForcedSystemDefaultLanguage, true);
166
167	BLocaleRoster* localeRoster = BLocaleRoster::Default();
168	if (localeRoster != NULL) {
169		BMessage preferredLanguages;
170		if (localeRoster->GetPreferredLanguages(&preferredLanguages) == B_OK) {
171			BString language;
172			if (preferredLanguages.FindString(
173				"language", 0, &language) == B_OK) {
174				return Language(language, language, true);
175			}
176		}
177	}
178
179	return LANGUAGE_DEFAULT;
180}
181
182
183/*! This method will take the supplied codes and will attempt to find the
184	supported language that best matches the codes. If there is really no
185	match then it will return `NULL`.
186*/
187
188Language*
189LanguageModel::_FindBestSupportedLanguage(
190	const char* code,
191	const char* countryCode,
192	const char* scriptCode) const
193{
194	int32 index = _IndexOfBestMatchingSupportedLanguage(code, countryCode, scriptCode);
195	if (-1 != index)
196		return SupportedLanguageAt(index);
197	return NULL;
198}
199
200
201/*! The supplied `languageId` here is the ICU code with the components separated
202	by underscore. This string can be obtained from the `BLanguage` using the
203	`ID()` method.
204*/
205
206int32
207LanguageModel::_IndexOfSupportedLanguage(
208	const char* code,
209    const char* countryCode,
210    const char* scriptCode) const
211{
212	size_t supportedLanguageSize = fSupportedLanguages.size();
213	for (uint32 i = 0; i < supportedLanguageSize; i++) {
214		const char *suppLangCode = fSupportedLanguages[i]->Code();
215		const char *suppLangCountryCode = fSupportedLanguages[i]->CountryCode();
216		const char *suppLangScriptCode = fSupportedLanguages[i]->ScriptCode();
217
218		if( 0 == _NullSafeStrCmp(code, suppLangCode)
219			&& 0 == _NullSafeStrCmp(countryCode, suppLangCountryCode)
220			&& 0 == _NullSafeStrCmp(scriptCode, suppLangScriptCode)) {
221			return i;
222		}
223	}
224
225	return -1;
226}
227
228
229/*! This will find the first supported language that matches the arguments
230	provided. In the case where one of the arguments is `NULL`, is will not
231	be considered.
232*/
233
234int32
235LanguageModel::_IndexOfBestMatchingSupportedLanguage(
236	const char* code,
237	const char* countryCode,
238	const char* scriptCode) const
239{
240	size_t supportedLanguageSize = fSupportedLanguages.size();
241
242	if (NULL != scriptCode) {
243		int32 index = _IndexOfSupportedLanguage(code, countryCode, scriptCode);
244			// looking for an exact match
245		if (-1 == index)
246			return index;
247	}
248
249	if (NULL != countryCode) {
250		for (uint32 i = 0; i < supportedLanguageSize; i++) {
251			if( 0 == _NullSafeStrCmp(code, fSupportedLanguages[i]->Code())
252				&& 0 == _NullSafeStrCmp(countryCode, fSupportedLanguages[i]->CountryCode()) ) {
253				return i;
254			}
255		}
256	}
257
258	if (NULL != code) {
259		for (uint32 i = 0; i < supportedLanguageSize; i++) {
260			if(0 == _NullSafeStrCmp(code, fSupportedLanguages[i]->Code()))
261				return i;
262		}
263	}
264
265	return -1;
266}
267
268
269/*static*/ int
270LanguageModel::_NullSafeStrCmp(const char* s1, const char* s2) {
271	if ((NULL == s1) && (NULL == s2))
272		return 0;
273	if (NULL == s1)
274		return -1;
275	if (NULL == s2)
276    	return 1;
277	return strcmp(s1, s2);
278}
279
280
281/*static*/ int
282LanguageModel::_LanguagesCompareFn(const LanguageRef& l1, const LanguageRef& l2)
283{
284	int result = _NullSafeStrCmp(l1->Code(), l2->Code());
285	if (0 == result)
286		result = _NullSafeStrCmp(l1->CountryCode(), l2->CountryCode());
287	if (0 == result)
288		result = _NullSafeStrCmp(l1->ScriptCode(), l2->ScriptCode());
289	return result;
290}
291
292
293/*static*/ bool
294LanguageModel::_IsLanguageBefore(const LanguageRef& l1, const LanguageRef& l2)
295{
296	return _LanguagesCompareFn(l1, l2) < 0;
297}