2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
8 /** @file game_text.cpp Implementation of handling translated strings. */
10 #include "../stdafx.h"
11 #include "../strgen/strgen.h"
13 #include "../fileio_func.h"
14 #include "../tar_type.h"
15 #include "../script/squirrel_class.hpp"
16 #include "../strings_func.h"
17 #include "game_text.hpp"
19 #include "game_info.hpp"
21 #include "table/strings.h"
26 #include "../safeguards.h"
28 void CDECL
strgen_warning(const char *s
, ...)
33 vseprintf(buf
, lastof(buf
), s
, va
);
35 Debug(script
, 0, "{}:{}: warning: {}", _file
, _cur_line
, buf
);
39 void CDECL
strgen_error(const char *s
, ...)
44 vseprintf(buf
, lastof(buf
), s
, va
);
46 Debug(script
, 0, "{}:{}: error: {}", _file
, _cur_line
, buf
);
50 void NORETURN CDECL
strgen_fatal(const char *s
, ...)
55 vseprintf(buf
, lastof(buf
), s
, va
);
57 Debug(script
, 0, "{}:{}: FATAL: {}", _file
, _cur_line
, buf
);
58 throw std::exception();
62 * Read all the raw language strings from the given file.
63 * @param file The file to read from.
64 * @return The raw strings, or nullptr upon error.
66 LanguageStrings
ReadRawLanguageStrings(const std::string
&file
)
69 FILE *fh
= FioFOpenFile(file
, "rb", GAME_DIR
, &to_read
);
70 if (fh
== nullptr) return LanguageStrings();
72 FileCloser
fhClose(fh
);
74 auto pos
= file
.rfind(PATHSEPCHAR
);
75 if (pos
== std::string::npos
) return LanguageStrings();
76 std::string langname
= file
.substr(pos
+ 1);
78 /* Check for invalid empty filename */
79 if (langname
.empty() || langname
.front() == '.') return LanguageStrings();
81 LanguageStrings
ret(langname
.substr(0, langname
.find('.')));
84 while (to_read
!= 0 && fgets(buffer
, sizeof(buffer
), fh
) != nullptr) {
85 size_t len
= strlen(buffer
);
87 /* Remove trailing spaces/newlines from the string. */
89 while (i
> 0 && (buffer
[i
- 1] == '\r' || buffer
[i
- 1] == '\n' || buffer
[i
- 1] == ' ')) i
--;
92 ret
.lines
.emplace_back(buffer
, i
);
105 /** A reader that simply reads using fopen. */
106 struct StringListReader
: StringReader
{
107 StringList::const_iterator p
; ///< The current location of the iteration.
108 StringList::const_iterator end
; ///< The end of the iteration.
112 * @param data The data to fill during reading.
113 * @param strings The language strings we are reading.
114 * @param master Are we reading the master file?
115 * @param translation Are we reading a translation?
117 StringListReader(StringData
&data
, const LanguageStrings
&strings
, bool master
, bool translation
) :
118 StringReader(data
, strings
.language
.c_str(), master
, translation
), p(strings
.lines
.begin()), end(strings
.lines
.end())
122 char *ReadLine(char *buffer
, const char *last
) override
124 if (this->p
== this->end
) return nullptr;
126 strecpy(buffer
, this->p
->c_str(), last
);
133 /** Class for writing an encoded language. */
134 struct TranslationWriter
: LanguageWriter
{
135 StringList
&strings
; ///< The encoded strings.
138 * Writer for the encoded data.
139 * @param strings The string table to add the strings to.
141 TranslationWriter(StringList
&strings
) : strings(strings
)
145 void WriteHeader(const LanguagePackHeader
*header
)
147 /* We don't use the header. */
155 void WriteLength(uint length
)
157 /* We don't write the length. */
160 void Write(const byte
*buffer
, size_t length
)
162 this->strings
.emplace_back((const char *)buffer
, length
);
166 /** Class for writing the string IDs. */
167 struct StringNameWriter
: HeaderWriter
{
168 StringList
&strings
; ///< The string names.
171 * Writer for the string names.
172 * @param strings The string table to add the strings to.
174 StringNameWriter(StringList
&strings
) : strings(strings
)
178 void WriteStringID(const char *name
, int stringid
)
180 if (stringid
== (int)this->strings
.size()) this->strings
.emplace_back(name
);
183 void Finalise(const StringData
&data
)
190 * Scanner to find language files in a GameScript directory.
192 class LanguageScanner
: protected FileScanner
{
199 LanguageScanner(GameStrings
*gs
, const std::string
&exclude
) : gs(gs
), exclude(exclude
) {}
204 void Scan(const char *directory
)
206 this->FileScanner::Scan(".txt", directory
, false);
209 bool AddFile(const std::string
&filename
, size_t basepath_length
, const std::string
&tar_filename
) override
211 if (exclude
== filename
) return true;
213 auto ls
= ReadRawLanguageStrings(filename
);
214 if (!ls
.IsValid()) return false;
216 gs
->raw_strings
.push_back(std::move(ls
));
222 * Load all translations that we know of.
223 * @return Container with all (compiled) translations.
225 GameStrings
*LoadTranslations()
227 const GameInfo
*info
= Game::GetInfo();
228 std::string
basename(info
->GetMainScript());
229 auto e
= basename
.rfind(PATHSEPCHAR
);
230 if (e
== std::string::npos
) return nullptr;
231 basename
.erase(e
+ 1);
233 std::string filename
= basename
+ "lang" PATHSEP
"english.txt";
234 if (!FioCheckFileExists(filename
, GAME_DIR
)) return nullptr;
236 auto ls
= ReadRawLanguageStrings(filename
);
237 if (!ls
.IsValid()) return nullptr;
239 GameStrings
*gs
= new GameStrings();
241 gs
->raw_strings
.push_back(std::move(ls
));
243 /* Scan for other language files */
244 LanguageScanner
scanner(gs
, filename
);
245 std::string ldir
= basename
+ "lang" PATHSEP
;
247 const std::string tar_filename
= info
->GetTarFile();
248 TarList::iterator iter
;
249 if (!tar_filename
.empty() && (iter
= _tar_list
[GAME_DIR
].find(tar_filename
)) != _tar_list
[GAME_DIR
].end()) {
250 /* The main script is in a tar file, so find all files that
251 * are in the same tar and add them to the langfile scanner. */
252 for (const auto &tar
: _tar_filelist
[GAME_DIR
]) {
253 /* Not in the same tar. */
254 if (tar
.second
.tar_filename
!= iter
->first
) continue;
256 /* Check the path and extension. */
257 if (tar
.first
.size() <= ldir
.size() || tar
.first
.compare(0, ldir
.size(), ldir
) != 0) continue;
258 if (tar
.first
.compare(tar
.first
.size() - 4, 4, ".txt") != 0) continue;
260 scanner
.AddFile(tar
.first
, 0, tar_filename
);
263 /* Scan filesystem */
264 scanner
.Scan(ldir
.c_str());
275 /** Compile the language. */
276 void GameStrings::Compile()
279 StringListReader
master_reader(data
, this->raw_strings
[0], true, false);
280 master_reader
.ParseFile();
281 if (_errors
!= 0) throw std::exception();
283 this->version
= data
.Version();
285 StringNameWriter
id_writer(this->string_names
);
286 id_writer
.WriteHeader(data
);
288 for (const auto &p
: this->raw_strings
) {
289 data
.FreeTranslation();
290 StringListReader
translation_reader(data
, p
, false, p
.language
!= "english");
291 translation_reader
.ParseFile();
292 if (_errors
!= 0) throw std::exception();
294 this->compiled_strings
.emplace_back(p
.language
);
295 TranslationWriter
writer(this->compiled_strings
.back().lines
);
296 writer
.WriteLang(data
);
300 /** The currently loaded game strings. */
301 GameStrings
*_current_data
= nullptr;
304 * Get the string pointer of a particular game string.
305 * @param id The ID of the game string.
306 * @return The encoded string.
308 const char *GetGameStringPtr(uint id
)
310 if (id
>= _current_data
->cur_language
->lines
.size()) return GetStringPtr(STR_UNDEFINED
);
311 return _current_data
->cur_language
->lines
[id
].c_str();
315 * Register the current translation to the Squirrel engine.
316 * @param engine The engine to update/
318 void RegisterGameTranslation(Squirrel
*engine
)
320 delete _current_data
;
321 _current_data
= LoadTranslations();
322 if (_current_data
== nullptr) return;
324 HSQUIRRELVM vm
= engine
->GetVM();
325 sq_pushroottable(vm
);
326 sq_pushstring(vm
, "GSText", -1);
327 if (SQ_FAILED(sq_get(vm
, -2))) return;
330 for (const auto &p
: _current_data
->string_names
) {
331 sq_pushstring(vm
, p
.c_str(), -1);
332 sq_pushinteger(vm
, idx
);
339 ReconsiderGameScriptLanguage();
343 * Reconsider the game script language, so we use the right one.
345 void ReconsiderGameScriptLanguage()
347 if (_current_data
== nullptr) return;
350 strecpy(temp
, _current_language
->file
, lastof(temp
));
352 /* Remove the extension */
353 char *l
= strrchr(temp
, '.');
354 assert(l
!= nullptr);
358 char *language
= strrchr(temp
, PATHSEPCHAR
);
359 assert(language
!= nullptr);
362 for (auto &p
: _current_data
->compiled_strings
) {
363 if (p
.language
== language
) {
364 _current_data
->cur_language
= &p
;
369 _current_data
->cur_language
= &_current_data
->compiled_strings
[0];