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