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