1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3
4#include "localization.h"
5
6#include <base/log.h>
7#include <base/system.h>
8
9#include <engine/console.h>
10#include <engine/shared/linereader.h>
11#include <engine/storage.h>
12
13const char *Localize(const char *pStr, const char *pContext)
14{
15 const char *pNewStr = g_Localization.FindString(Hash: str_quickhash(str: pStr), ContextHash: str_quickhash(str: pContext));
16 return pNewStr ? pNewStr : pStr;
17}
18
19void CLocalizationDatabase::LoadIndexfile(IStorage *pStorage, IConsole *pConsole)
20{
21 m_vLanguages.clear();
22
23 const std::vector<std::string> vEnglishLanguageCodes = {"en"};
24 m_vLanguages.emplace_back(args: "English", args: "", args: 826, args: vEnglishLanguageCodes);
25
26 const char *pFilename = "languages/index.txt";
27 CLineReader LineReader;
28 if(!LineReader.OpenFile(File: pStorage->OpenFile(pFilename, Flags: IOFLAG_READ, Type: IStorage::TYPE_ALL)))
29 {
30 log_error("localization", "Couldn't open index file '%s'", pFilename);
31 return;
32 }
33
34 while(const char *pLine = LineReader.Get())
35 {
36 if(!str_length(str: pLine) || pLine[0] == '#') // skip empty lines and comments
37 continue;
38
39 char aEnglishName[128];
40 str_copy(dst&: aEnglishName, src: pLine);
41
42 pLine = LineReader.Get();
43 if(!pLine)
44 {
45 log_error("localization", "Unexpected end of index file after language '%s'", aEnglishName);
46 break;
47 }
48 if(!str_startswith(str: pLine, prefix: "== "))
49 {
50 log_error("localization", "Missing native name for language '%s'", aEnglishName);
51 (void)LineReader.Get();
52 (void)LineReader.Get();
53 continue;
54 }
55 char aNativeName[128];
56 str_copy(dst&: aNativeName, src: pLine + 3);
57
58 pLine = LineReader.Get();
59 if(!pLine)
60 {
61 log_error("localization", "Unexpected end of index file after language '%s'", aEnglishName);
62 break;
63 }
64 if(!str_startswith(str: pLine, prefix: "== "))
65 {
66 log_error("localization", "Missing country code for language '%s'", aEnglishName);
67 (void)LineReader.Get();
68 continue;
69 }
70 char aCountryCode[128];
71 str_copy(dst&: aCountryCode, src: pLine + 3);
72
73 pLine = LineReader.Get();
74 if(!pLine)
75 {
76 log_error("localization", "Unexpected end of index file after language '%s'", aEnglishName);
77 break;
78 }
79 if(!str_startswith(str: pLine, prefix: "== "))
80 {
81 log_error("localization", "Missing language codes for language '%s'", aEnglishName);
82 continue;
83 }
84 const char *pLanguageCodes = pLine + 3;
85 char aLanguageCode[256];
86 std::vector<std::string> vLanguageCodes;
87 while((pLanguageCodes = str_next_token(str: pLanguageCodes, delim: ";", buffer: aLanguageCode, buffer_size: sizeof(aLanguageCode))))
88 {
89 if(aLanguageCode[0])
90 {
91 vLanguageCodes.emplace_back(args&: aLanguageCode);
92 }
93 }
94 if(vLanguageCodes.empty())
95 {
96 log_error("localization", "At least one language code required for language '%s'", aEnglishName);
97 continue;
98 }
99
100 char aFilename[IO_MAX_PATH_LENGTH];
101 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "languages/%s.txt", aEnglishName);
102 m_vLanguages.emplace_back(args&: aNativeName, args&: aFilename, args: str_toint(str: aCountryCode), args&: vLanguageCodes);
103 }
104
105 std::sort(first: m_vLanguages.begin(), last: m_vLanguages.end());
106}
107
108void CLocalizationDatabase::SelectDefaultLanguage(IConsole *pConsole, char *pFilename, size_t Length) const
109{
110 if(Languages().empty())
111 return;
112 if(Languages().size() == 1)
113 {
114 str_copy(dst: pFilename, src: Languages()[0].m_Filename.c_str(), dst_size: Length);
115 return;
116 }
117
118 char aLocaleStr[128];
119 os_locale_str(locale: aLocaleStr, length: sizeof(aLocaleStr));
120
121 log_info("localization", "Choosing default language based on user locale '%s'", aLocaleStr);
122
123 while(true)
124 {
125 const CLanguage *pPrefixMatch = nullptr;
126 for(const auto &Language : Languages())
127 {
128 for(const auto &LanguageCode : Language.m_vLanguageCodes)
129 {
130 if(LanguageCode == aLocaleStr)
131 {
132 // Exact match found, use it immediately
133 str_copy(dst: pFilename, src: Language.m_Filename.c_str(), dst_size: Length);
134 return;
135 }
136 else if(LanguageCode.starts_with(x: aLocaleStr))
137 {
138 // Locale is prefix of language code, e.g. locale is "en" and current language is "en-US"
139 pPrefixMatch = &Language;
140 }
141 }
142 }
143 // Use prefix match if no exact match was found
144 if(pPrefixMatch)
145 {
146 str_copy(dst: pFilename, src: pPrefixMatch->m_Filename.c_str(), dst_size: Length);
147 return;
148 }
149
150 // Remove last segment of locale string and try again with more generic locale, e.g. "en-US" -> "en"
151 int i = str_length(str: aLocaleStr) - 1;
152 for(; i >= 0; --i)
153 {
154 if(aLocaleStr[i] == '-')
155 {
156 aLocaleStr[i] = '\0';
157 break;
158 }
159 }
160
161 // Stop if no more locale segments are left
162 if(i <= 0)
163 break;
164 }
165}
166
167bool CLocalizationDatabase::Load(const char *pFilename, IStorage *pStorage, IConsole *pConsole)
168{
169 // empty string means unload
170 if(pFilename[0] == 0)
171 {
172 m_vStrings.clear();
173 m_StringsHeap.Reset();
174 return true;
175 }
176
177 CLineReader LineReader;
178 if(!LineReader.OpenFile(File: pStorage->OpenFile(pFilename, Flags: IOFLAG_READ, Type: IStorage::TYPE_ALL)))
179 return false;
180
181 log_info("localization", "loaded '%s'", pFilename);
182 m_vStrings.clear();
183 m_StringsHeap.Reset();
184
185 char aContext[512];
186 char aOrigin[512];
187 int Line = 0;
188 while(const char *pLine = LineReader.Get())
189 {
190 Line++;
191 if(!str_length(str: pLine))
192 continue;
193
194 if(pLine[0] == '#') // skip comments
195 continue;
196
197 if(pLine[0] == '[') // context
198 {
199 size_t Len = str_length(str: pLine);
200 if(Len < 1 || pLine[Len - 1] != ']')
201 {
202 log_error("localization", "malformed context '%s' on line %d", pLine, Line);
203 continue;
204 }
205 str_truncate(dst: aContext, dst_size: sizeof(aContext), src: pLine + 1, truncation_len: Len - 2);
206 pLine = LineReader.Get();
207 if(!pLine)
208 {
209 log_error("localization", "unexpected end of file after context line '%s' on line %d", aContext, Line);
210 break;
211 }
212 Line++;
213 }
214 else
215 {
216 aContext[0] = '\0';
217 }
218
219 str_copy(dst&: aOrigin, src: pLine);
220 const char *pReplacement = LineReader.Get();
221 if(!pReplacement)
222 {
223 log_error("localization", "unexpected end of file after original '%s' on line %d", aOrigin, Line);
224 break;
225 }
226 Line++;
227
228 if(pReplacement[0] != '=' || pReplacement[1] != '=' || pReplacement[2] != ' ')
229 {
230 log_error("localization", "malformed replacement '%s' for original '%s' on line %d", pReplacement, aOrigin, Line);
231 continue;
232 }
233
234 pReplacement += 3;
235 AddString(pOrgStr: aOrigin, pNewStr: pReplacement, pContext: aContext);
236 }
237 std::sort(first: m_vStrings.begin(), last: m_vStrings.end());
238 return true;
239}
240
241void CLocalizationDatabase::AddString(const char *pOrgStr, const char *pNewStr, const char *pContext)
242{
243 m_vStrings.emplace_back(args: str_quickhash(str: pOrgStr), args: str_quickhash(str: pContext), args: m_StringsHeap.StoreString(pSrc: *pNewStr ? pNewStr : pOrgStr));
244}
245
246const char *CLocalizationDatabase::FindString(unsigned Hash, unsigned ContextHash) const
247{
248 CString String;
249 String.m_Hash = Hash;
250 String.m_ContextHash = ContextHash;
251 String.m_pReplacement = nullptr;
252 auto Range1 = std::equal_range(first: m_vStrings.begin(), last: m_vStrings.end(), val: String);
253 if(std::distance(first: Range1.first, last: Range1.second) == 1)
254 return Range1.first->m_pReplacement;
255
256 const unsigned DefaultHash = str_quickhash(str: "");
257 if(ContextHash != DefaultHash)
258 {
259 // Do another lookup with the default context hash
260 String.m_ContextHash = DefaultHash;
261 auto Range2 = std::equal_range(first: m_vStrings.begin(), last: m_vStrings.end(), val: String);
262 if(std::distance(first: Range2.first, last: Range2.second) == 1)
263 return Range2.first->m_pReplacement;
264 }
265
266 return nullptr;
267}
268
269CLocalizationDatabase g_Localization;
270