/**
* This file is part of Hercules.
* http://herc.ws - http://github.com/HerculesWS/Hercules
*
* Copyright (C) 2016 Hercules Dev Team
*
* Hercules 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, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "config/core.h"
#include "common/hercules.h"
#include "common/cbasetypes.h"
#include "common/memmgr.h"
#include "common/nullpo.h"
#include "common/showmsg.h"
#include "common/strlib.h"
#include "common/sysinfo.h"
#include "map/atcommand.h"
#include "map/map.h"
#include "map/npc.h"
#include "map/script.h"
#include "plugins/HPMHooking.h"
#include "common/HPMDataCheck.h"
#include
#include
#include
#include
HPExport struct hplugin_info pinfo = {
"generate-translations", // Plugin name
SERVER_TYPE_MAP, // Which server types this plugin works with?
"0.1", // Plugin version
HPM_VERSION, // HPM Version (don't change, macro is automatically updated)
};
struct DBMap *translatable_strings; // string map parsed (used when exporting strings only)
/* Set during startup when attempting to export the lang, unset after server initialization is over */
FILE *lang_export_fp;
char *lang_export_filepath;
#define DIRECTORYNAME "generated_translations"
struct script_string_buf lang_export_line_buf;
struct script_string_buf lang_export_escaped_buf;
int lang_export_stringcount_total;
int lang_export_stringcount_current;
/// Whether the translations template generator will automatically run.
bool generating_translations = false;
bool createdirectory(const char *dirname)
{
#ifdef WIN32
if (!CreateDirectory(dirname, NULL)) {
if (ERROR_ALREADY_EXISTS != GetLastError())
return false;
}
#else /* Not WIN32 */
struct stat st = { 0 };
if (stat(dirname, &st) == -1 ) {
if (mkdir(dirname, 0755) != 0)
return false;
}
#endif // WIN32 check
return true;
}
/**
* --generate-translations
*
* Creates "./generated_translations.pot"
* @see cmdline->exec
*/
CMDLINEARG(generatetranslations)
{
if (!createdirectory(DIRECTORYNAME)) {
ShowError("generatetranslations: Unable to create output directory '%s'.\n", DIRECTORYNAME);
return false;
}
generating_translations = true;
return true;
}
void script_add_translatable_string_posthook(const struct script_string_buf *string, const char *start_point)
{
bool duplicate = true;
bool is_translatable_string = false;
bool is_translatable_fmtstring = false;
if (!generating_translations || lang_export_fp == NULL)
return;
/* When exporting we don't know what is a translation and what isn't */
if (VECTOR_LENGTH(*string) > 1) {
// The length of *string will always be at least 1 because of the '\0'
if (translatable_strings == NULL) {
translatable_strings = strdb_alloc(DB_OPT_DUP_KEY|DB_OPT_ALLOW_NULL_DATA, 0);
}
if (!strdb_exists(translatable_strings, VECTOR_DATA(*string))) {
strdb_put(translatable_strings, VECTOR_DATA(*string), NULL);
duplicate = false;
}
}
if (!duplicate) {
if (script->syntax.last_func == script->buildin_mes_offset
|| script->syntax.last_func == script->buildin_select_offset
|| script->syntax.lang_macro_active
) {
is_translatable_string = true;
} else if (script->syntax.last_func == script->buildin_mesf_offset
|| script->syntax.lang_macro_fmtstring_active
) {
is_translatable_fmtstring = true;
}
}
if (is_translatable_string || is_translatable_fmtstring) {
const char *line_start = start_point;
const char *line_end = start_point;
int line_length;
bool has_percent_sign = false;
if (!is_translatable_fmtstring && strchr(VECTOR_DATA(*string), '%') != NULL) {
has_percent_sign = true;
}
while (line_start > script->parser_current_src && *line_start != '\n')
line_start--;
while (*line_end != '\n' && *line_end != '\0')
line_end++;
line_length = (int)(line_end - line_start);
if (line_length > 0) {
VECTOR_ENSURE(lang_export_line_buf, line_length + 1, 512);
VECTOR_PUSHARRAY(lang_export_line_buf, line_start, line_length);
VECTOR_PUSH(lang_export_line_buf, '\0');
normalize_name(VECTOR_DATA(lang_export_line_buf), "\r\n\t "); // [!] Note: VECTOR_LENGTH() will lie.
}
VECTOR_ENSURE(lang_export_escaped_buf, 4*VECTOR_LENGTH(*string)+1, 1);
VECTOR_LENGTH(lang_export_escaped_buf) = (int)sv->escape_c(VECTOR_DATA(lang_export_escaped_buf),
VECTOR_DATA(*string),
VECTOR_LENGTH(*string)-1, /* exclude null terminator */
"\"");
VECTOR_PUSH(lang_export_escaped_buf, '\0');
fprintf(lang_export_fp, "\n#: %s\n"
"# %s\n"
"%s"
"msgctxt \"%s\"\n"
"msgid \"%s\"\n"
"msgstr \"\"\n",
script->parser_current_file ? script->parser_current_file : "Unknown File",
VECTOR_DATA(lang_export_line_buf),
is_translatable_fmtstring ? "#, c-format\n" : (has_percent_sign ? "#, no-c-format\n" : ""),
script->parser_current_npc_name ? script->parser_current_npc_name : "Unknown NPC",
VECTOR_DATA(lang_export_escaped_buf)
);
lang_export_stringcount_total++;
lang_export_stringcount_current++;
VECTOR_TRUNCATE(lang_export_line_buf);
VECTOR_TRUNCATE(lang_export_escaped_buf);
}
}
struct script_code *parse_script_prehook(const char **src, const char **file, int *line, int *options, int **retval)
{
if (translatable_strings != NULL) {
db_destroy(translatable_strings);
translatable_strings = NULL;
}
return NULL;
}
void script_parser_clean_leftovers_posthook(void)
{
if (translatable_strings != NULL) {
db_destroy(translatable_strings);
translatable_strings = NULL;
}
VECTOR_CLEAR(lang_export_line_buf);
VECTOR_CLEAR(lang_export_escaped_buf);
}
bool translations_enter_file(const char *filepath)
{
const char *p = NULL;
int len, i;
if (!generating_translations)
return false;
p = filepath;
len = (int)strlen(filepath) + (int)strlen(DIRECTORYNAME) + (int)strlen(PATHSEP_STR);
lang_export_filepath = aCalloc(len + 4 + 1, sizeof(char)); // + ".pot"
strncat(lang_export_filepath, DIRECTORYNAME PATHSEP_STR, len);
lang_export_stringcount_current = 0;
i = (int)strlen(lang_export_filepath);
while (*p != '\0') {
if (Assert_chk(i < len)) {
aFree(lang_export_filepath);
lang_export_filepath = NULL;
return false;
}
if (*p == '.') {
lang_export_filepath[i] = '_';
} else if (*p == PATHSEP) {
if (!createdirectory(lang_export_filepath)) {
ShowError("generatetranslations: Unable to create output directory '%s'.\n", lang_export_filepath);
aFree(lang_export_filepath);
lang_export_filepath = NULL;
return false;
}
lang_export_filepath[i] = PATHSEP;
} else {
lang_export_filepath[i] = *p;
}
i++;
p++;
}
strncat(lang_export_filepath, ".pot", len + 4);
if ((lang_export_fp = fopen(lang_export_filepath, "wb")) == NULL) {
ShowError("export-dialog: failed to open '%s' for writing\n", lang_export_filepath);
aFree(lang_export_filepath);
lang_export_filepath = NULL;
return false;
}
{
time_t t = time(NULL);
struct tm *lt = localtime(&t);
int year = lt->tm_year+1900;
char timestring[128] = "";
strftime(timestring, sizeof(timestring), "%Y-%m-%d %H:%M:%S%z", lt);
fprintf(lang_export_fp,
"# This file is part of Hercules.\n"
"# http://herc.ws - http://github.com/HerculesWS/Hercules\n"
"#\n"
"# Copyright (C) 2013-%d Hercules Dev Team\n"
"#\n"
"# Hercules is free software: you can redistribute it and/or modify\n"
"# it under the terms of the GNU General Public License as published by\n"
"# the Free Software Foundation, either version 3 of the License, or\n"
"# (at your option) any later version.\n"
"#\n"
"# This program is distributed in the hope that it will be useful,\n"
"# but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
"# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
"# GNU General Public License for more details.\n"
"#\n"
"# You should have received a copy of the GNU General Public License\n"
"# along with this program. If not, see .\n\n"
"#,fuzzy\n"
"msgid \"\"\n"
"msgstr \"\"\n"
"\"Project-Id-Version: %s\\n\"\n"
"\"Report-Msgid-Bugs-To: dev@herc.ws\\n\"\n"
"\"POT-Creation-Date: %s\\n\"\n"
"\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n"
"\"Last-Translator: FULL NAME \\n\"\n"
"\"Language-Team: LANGUAGE \\n\"\n"
"\"Language: \\n\"\n"
"\"MIME-Version: 1.0\\n\"\n"
"\"Content-Type: text/plain; charset=ISO-8859-1\\n\"\n"
"\"Content-Transfer-Encoding: 8bit\\n\"\n\n",
year, sysinfo->vcsrevision_scripts(), timestring);
}
return true;
}
bool translations_leave_file(const char *filepath)
{
if (lang_export_fp != NULL) {
fclose(lang_export_fp);
lang_export_fp = NULL;
}
if (lang_export_filepath != NULL) {
if (lang_export_stringcount_current == 0) {
remove(lang_export_filepath);
} else {
ShowMessage("\r");
ShowInfo("%s => %s (%d strings)\n", filepath, lang_export_filepath, lang_export_stringcount_current);
}
aFree(lang_export_filepath);
lang_export_filepath = NULL;
}
lang_export_stringcount_current = 0;
return true;
}
bool msg_config_read_prehook(const char **cfg_name, bool *allow_override)
{
if (*allow_override) // allow_override is true in nested calls
return false;
translations_enter_file(*cfg_name);
return true;
}
bool msg_config_read_posthook(bool retVal, const char *cfg_name, bool allow_override)
{
int i;
if (!generating_translations || lang_export_fp == NULL)
return retVal;
if (allow_override) // allow_override is true in nested calls
return retVal;
if (retVal) {
for (i = 0; i < MAX_MSG; i++) {
if (atcommand->msg_table[0][i] == NULL)
continue;
fprintf(lang_export_fp, "\n#: conf/messages.conf\n"
"# %d: %s\n"
"#, c-format\n"
"msgctxt \"messages.conf\"\n"
"msgid \"%s\"\n"
"msgstr \"\"\n",
i, atcommand->msg_table[0][i],
atcommand->msg_table[0][i]
);
lang_export_stringcount_total++;
lang_export_stringcount_current++;
}
}
translations_leave_file(cfg_name);
return retVal;
}
int npc_parsesrcfile_prehook(const char **filepath, bool *runOnInit)
{
translations_enter_file(*filepath);
return 0;
}
int npc_parsesrcfile_posthook(int retVal, const char *filepath, bool runOnInit)
{
translations_leave_file(filepath);
return retVal;
}
HPExport void server_preinit(void)
{
addArg("--generate-translations", false, generatetranslations,
"Creates 'generated_translations/**/*.pot' file with all translateable strings from scripts, server terminates afterwards.");
VECTOR_INIT(lang_export_line_buf);
VECTOR_INIT(lang_export_escaped_buf);
addHookPost(script, add_translatable_string, script_add_translatable_string_posthook);
addHookPre(script, parse, parse_script_prehook);
addHookPost(script, parser_clean_leftovers, script_parser_clean_leftovers_posthook);
addHookPre(atcommand, msg_read, msg_config_read_prehook);
addHookPost(atcommand, msg_read, msg_config_read_posthook);
addHookPre(npc, parsesrcfile, npc_parsesrcfile_prehook);
addHookPost(npc, parsesrcfile, npc_parsesrcfile_posthook);
lang_export_stringcount_total = 0;
lang_export_stringcount_current = 0;
}
HPExport void plugin_init(void)
{
}
HPExport void server_online(void)
{
if (generating_translations) {
ShowInfo("Translations template exported to '%s' with %d strings.\n", DIRECTORYNAME, lang_export_stringcount_total);
}
core->runflag = CORE_ST_STOP;
}
HPExport void plugin_final(void)
{
}