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, "%s:%d: warning: %s", _file
, _cur_line
, buf
);
39 void CDECL
strgen_error(const char *s
, ...)
44 vseprintf(buf
, lastof(buf
), s
, va
);
46 DEBUG(script
, 0, "%s:%d: error: %s", _file
, _cur_line
, buf
);
50 void NORETURN CDECL
strgen_fatal(const char *s
, ...)
55 vseprintf(buf
, lastof(buf
), s
, va
);
57 DEBUG(script
, 0, "%s:%d: FATAL: %s", _file
, _cur_line
, buf
);
58 throw std::exception();
62 * Create a new container for language strings.
63 * @param language The language name.
64 * @param end If not nullptr, terminate \a language at this position.
66 LanguageStrings::LanguageStrings(const char *language
, const char *end
)
68 this->language
= stredup(language
, end
!= nullptr ? end
- 1 : nullptr);
71 /** Free everything. */
72 LanguageStrings::~LanguageStrings()
78 * Read all the raw language strings from the given file.
79 * @param file The file to read from.
80 * @return The raw strings, or nullptr upon error.
82 std::unique_ptr
<LanguageStrings
> ReadRawLanguageStrings(const char *file
)
86 FILE *fh
= FioFOpenFile(file
, "rb", GAME_DIR
, &to_read
);
87 if (fh
== nullptr) return nullptr;
89 FileCloser
fhClose(fh
);
91 const char *langname
= strrchr(file
, PATHSEPCHAR
);
92 if (langname
== nullptr) {
98 /* Check for invalid empty filename */
99 if (*langname
== '.' || *langname
== 0) return nullptr;
101 std::unique_ptr
<LanguageStrings
> ret(new LanguageStrings(langname
, strchr(langname
, '.')));
104 while (to_read
!= 0 && fgets(buffer
, sizeof(buffer
), fh
) != nullptr) {
105 size_t len
= strlen(buffer
);
107 /* Remove trailing spaces/newlines from the string. */
109 while (i
> 0 && (buffer
[i
- 1] == '\r' || buffer
[i
- 1] == '\n' || buffer
[i
- 1] == ' ')) i
--;
112 ret
->lines
.emplace_back(buffer
, i
);
128 /** A reader that simply reads using fopen. */
129 struct StringListReader
: StringReader
{
130 StringList::const_iterator p
; ///< The current location of the iteration.
131 StringList::const_iterator end
; ///< The end of the iteration.
135 * @param data The data to fill during reading.
136 * @param strings The language strings we are reading.
137 * @param master Are we reading the master file?
138 * @param translation Are we reading a translation?
140 StringListReader(StringData
&data
, const LanguageStrings
&strings
, bool master
, bool translation
) :
141 StringReader(data
, strings
.language
, master
, translation
), p(strings
.lines
.begin()), end(strings
.lines
.end())
145 char *ReadLine(char *buffer
, const char *last
) override
147 if (this->p
== this->end
) return nullptr;
149 strecpy(buffer
, this->p
->c_str(), last
);
156 /** Class for writing an encoded language. */
157 struct TranslationWriter
: LanguageWriter
{
158 StringList
&strings
; ///< The encoded strings.
161 * Writer for the encoded data.
162 * @param strings The string table to add the strings to.
164 TranslationWriter(StringList
&strings
) : strings(strings
)
168 void WriteHeader(const LanguagePackHeader
*header
)
170 /* We don't use the header. */
178 void WriteLength(uint length
)
180 /* We don't write the length. */
183 void Write(const byte
*buffer
, size_t length
)
185 this->strings
.emplace_back((const char *)buffer
, length
);
189 /** Class for writing the string IDs. */
190 struct StringNameWriter
: HeaderWriter
{
191 StringList
&strings
; ///< The string names.
194 * Writer for the string names.
195 * @param strings The string table to add the strings to.
197 StringNameWriter(StringList
&strings
) : strings(strings
)
201 void WriteStringID(const char *name
, int stringid
)
203 if (stringid
== (int)this->strings
.size()) this->strings
.emplace_back(name
);
206 void Finalise(const StringData
&data
)
213 * Scanner to find language files in a GameScript directory.
215 class LanguageScanner
: protected FileScanner
{
222 LanguageScanner(GameStrings
*gs
, const char *exclude
) : gs(gs
), exclude(stredup(exclude
)) {}
223 ~LanguageScanner() { free(exclude
); }
228 void Scan(const char *directory
)
230 this->FileScanner::Scan(".txt", directory
, false);
233 bool AddFile(const char *filename
, size_t basepath_length
, const char *tar_filename
) override
235 if (strcmp(filename
, exclude
) == 0) return true;
237 auto ls
= ReadRawLanguageStrings(filename
);
238 if (ls
== nullptr) return false;
240 gs
->raw_strings
.push_back(std::move(ls
));
246 * Load all translations that we know of.
247 * @return Container with all (compiled) translations.
249 GameStrings
*LoadTranslations()
251 const GameInfo
*info
= Game::GetInfo();
253 strecpy(filename
, info
->GetMainScript(), lastof(filename
));
254 char *e
= strrchr(filename
, PATHSEPCHAR
);
255 if (e
== nullptr) return nullptr;
256 e
++; // Make 'e' point after the PATHSEPCHAR
258 strecpy(e
, "lang" PATHSEP
"english.txt", lastof(filename
));
259 if (!FioCheckFileExists(filename
, GAME_DIR
)) return nullptr;
261 auto ls
= ReadRawLanguageStrings(filename
);
262 if (ls
== nullptr) return nullptr;
264 GameStrings
*gs
= new GameStrings();
266 gs
->raw_strings
.push_back(std::move(ls
));
268 /* Scan for other language files */
269 LanguageScanner
scanner(gs
, filename
);
270 strecpy(e
, "lang" PATHSEP
, lastof(filename
));
271 size_t len
= strlen(filename
);
273 const char *tar_filename
= info
->GetTarFile();
274 TarList::iterator iter
;
275 if (tar_filename
!= nullptr && (iter
= _tar_list
[GAME_DIR
].find(tar_filename
)) != _tar_list
[GAME_DIR
].end()) {
276 /* The main script is in a tar file, so find all files that
277 * are in the same tar and add them to the langfile scanner. */
278 TarFileList::iterator tar
;
279 FOR_ALL_TARS(tar
, GAME_DIR
) {
280 /* Not in the same tar. */
281 if (tar
->second
.tar_filename
!= iter
->first
) continue;
283 /* Check the path and extension. */
284 if (tar
->first
.size() <= len
|| tar
->first
.compare(0, len
, filename
) != 0) continue;
285 if (tar
->first
.compare(tar
->first
.size() - 4, 4, ".txt") != 0) continue;
287 scanner
.AddFile(tar
->first
.c_str(), 0, tar_filename
);
290 /* Scan filesystem */
291 scanner
.Scan(filename
);
302 /** Compile the language. */
303 void GameStrings::Compile()
306 StringListReader
master_reader(data
, *this->raw_strings
[0], true, false);
307 master_reader
.ParseFile();
308 if (_errors
!= 0) throw std::exception();
310 this->version
= data
.Version();
312 StringNameWriter
id_writer(this->string_names
);
313 id_writer
.WriteHeader(data
);
315 for (const auto &p
: this->raw_strings
) {
316 data
.FreeTranslation();
317 StringListReader
translation_reader(data
, *p
, false, strcmp(p
->language
, "english") != 0);
318 translation_reader
.ParseFile();
319 if (_errors
!= 0) throw std::exception();
321 this->compiled_strings
.emplace_back(new LanguageStrings(p
->language
));
322 TranslationWriter
writer(this->compiled_strings
.back()->lines
);
323 writer
.WriteLang(data
);
327 /** The currently loaded game strings. */
328 GameStrings
*_current_data
= nullptr;
331 * Get the string pointer of a particular game string.
332 * @param id The ID of the game string.
333 * @return The encoded string.
335 const char *GetGameStringPtr(uint id
)
337 if (id
>= _current_data
->cur_language
->lines
.size()) return GetStringPtr(STR_UNDEFINED
);
338 return _current_data
->cur_language
->lines
[id
].c_str();
342 * Register the current translation to the Squirrel engine.
343 * @param engine The engine to update/
345 void RegisterGameTranslation(Squirrel
*engine
)
347 delete _current_data
;
348 _current_data
= LoadTranslations();
349 if (_current_data
== nullptr) return;
351 HSQUIRRELVM vm
= engine
->GetVM();
352 sq_pushroottable(vm
);
353 sq_pushstring(vm
, "GSText", -1);
354 if (SQ_FAILED(sq_get(vm
, -2))) return;
357 for (const auto &p
: _current_data
->string_names
) {
358 sq_pushstring(vm
, p
.c_str(), -1);
359 sq_pushinteger(vm
, idx
);
366 ReconsiderGameScriptLanguage();
370 * Reconsider the game script language, so we use the right one.
372 void ReconsiderGameScriptLanguage()
374 if (_current_data
== nullptr) return;
377 strecpy(temp
, _current_language
->file
, lastof(temp
));
379 /* Remove the extension */
380 char *l
= strrchr(temp
, '.');
381 assert(l
!= nullptr);
385 char *language
= strrchr(temp
, PATHSEPCHAR
);
386 assert(language
!= nullptr);
389 for (auto &p
: _current_data
->compiled_strings
) {
390 if (strcmp(p
->language
, language
) == 0) {
391 _current_data
->cur_language
= p
;
396 _current_data
->cur_language
= _current_data
->compiled_strings
[0];