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}