/*
 * wee-doc.c - documentation generator
 *
 * Copyright (C) 2023 Sébastien Helleu <flashcode@flashtux.org>
 *
 * This file is part of WeeChat, the extensible chat client.
 *
 * WeeChat 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.
 *
 * WeeChat 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 WeeChat.  If not, see <https://www.gnu.org/licenses/>.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <libintl.h>
#include <locale.h>
#include <gcrypt.h>
#include <regex.h>

#include "weechat.h"
#include "wee-arraylist.h"
#include "wee-command.h"
#include "wee-config-file.h"
#include "wee-crypto.h"
#include "wee-dir.h"
#include "wee-hashtable.h"
#include "wee-hdata.h"
#include "wee-hook.h"
#include "wee-infolist.h"
#include "wee-string.h"
#include "wee-url.h"
#include "wee-utf8.h"
#include "../plugins/plugin.h"

#define ESCAPE_TABLE(msg) (doc_gen_escape_table (msg))
#define ESCAPE_ANCHOR(msg) (doc_gen_escape_anchor_link (msg))
#define TRANS(msg) ((msg && msg[0]) ? _(msg) : msg)
#define TRANS_DEF(msg, def) ((msg && msg[0]) ? _(msg) : def)
#define PLUGIN(plugin) ((plugin) ? plugin->name : "weechat")

typedef int (t_doc_gen_func)(const char *path, const char *lang);

int index_string_escaped;
char *string_escaped[32];


/*
 * Escapes a string to display in a table: replace "|" by "\|".
 */

char *
doc_gen_escape_table (const char *message)
{
    index_string_escaped = (index_string_escaped + 1) % 32;

    if (string_escaped[index_string_escaped])
        free (string_escaped[index_string_escaped]);

    string_escaped[index_string_escaped] = string_replace (message, "|", "\\|");

    return string_escaped[index_string_escaped];
}

/*
 * Escapes a string to be used as anchor link: replace ",", "@" and "*" by "-".
 */

char *
doc_gen_escape_anchor_link (const char *message)
{
    regex_t regex;

    if (string_regcomp (&regex, "[,@*():&|]+", REG_EXTENDED) != 0)
        return NULL;

    index_string_escaped = (index_string_escaped + 1) % 32;

    if (string_escaped[index_string_escaped])
        free (string_escaped[index_string_escaped]);

    string_escaped[index_string_escaped] = string_replace_regex (
        message, &regex, "-", '$', NULL, NULL);

    regfree (&regex);

    return string_escaped[index_string_escaped];
}

/*
 * Opens a file for write using:
 *   - path
 *   - doc: "api" or "user"
 *   - name
 *   - language (eg: "fr")
 *
 * Returns the file opened, NULL if error.
 */

FILE *
doc_gen_open_file (const char *path, const char *doc, const char *name,
                   const char *lang)
{
    char filename[PATH_MAX];
    FILE *file;

    snprintf (filename, sizeof (filename),
              "%s%s" "autogen_%s_%s.%s.adoc.temp",
              path, DIR_SEPARATOR, doc, name, lang);

    file = fopen (filename, "wb");
    if (!file)
    {
        string_fprintf (stderr,
                        "doc generator: ERROR: unable to write file \"%s\"\n",
                        filename);
        return NULL;
    }

    string_fprintf (
        file,
        "//\n"
        "// This file is auto-generated by WeeChat.\n"
        "// DO NOT EDIT BY HAND!\n"
        "//\n"
        "\n");

    return file;
}

/*
 * Closes the file and renames it without ".temp" suffix, if the target name
 * does not exist of if it exists with a different (obsolete) content.
 *
 * If the target name exists with same content it's kept as-is (so the
 * timestamp does not change) and the temporary file is just deleted.
 *
 * Returns:
 *    1: target file has been updated
 *    0: target file unchanged
 *   -1: error
 */

int
doc_gen_close_file (const char *path, const char *doc, const char *name,
                    const char *lang, FILE *file)
{
    char filename_temp[PATH_MAX], filename[PATH_MAX];
    char hash_temp[512 / 8], hash[512 / 8];
    int rc_temp, rc;

    fclose (file);

    snprintf (filename_temp, sizeof (filename_temp),
              "%s%s" "autogen_%s_%s.%s.adoc.temp",
              path, DIR_SEPARATOR, doc, name, lang);

    snprintf (filename, sizeof (filename),
              "%s%s" "autogen_%s_%s.%s.adoc",
              path, DIR_SEPARATOR, doc, name, lang);

    rc_temp = weecrypto_hash_file (filename_temp, GCRY_MD_SHA512,
                                   hash_temp, NULL);
    if (!rc_temp)
        return -1;

    rc = weecrypto_hash_file (filename, GCRY_MD_SHA512, hash, NULL);

    if (!rc || (memcmp (hash_temp, hash, sizeof (hash)) != 0))
    {
        rename (filename_temp, filename);
        return 1;
    }

    unlink (filename_temp);
    return 0;
}

/*
 * Checks if a command must be documented or not.
 *
 * All commands whose name == plugin name are documented, and all commands for
 * these plugins are documented as well:
 *   - weechat (core)
 *   - irc
 *   - xfer
 */

int
doc_gen_check_command (const char *plugin, const char *command)
{
    /* command name is the same as plugin: to document! */
    if (strcmp (plugin, command) == 0)
        return 1;

    /* document other options only for weechat, irc, xfer */
    return((strcmp (plugin, "weechat") == 0)
           || (strcmp (plugin, "irc") == 0)
           || (strcmp (plugin, "xfer") == 0)) ?
        1 : 0;
}

/*
 * Compares two hooks "command" to sort by plugin / command.
 */

int
doc_gen_hook_command_cmp_cb (void *data, struct t_arraylist *arraylist,
                             void *pointer1, void *pointer2)
{
    struct t_hook *ptr_hook1, *ptr_hook2;
    int rc;

    /* make C compiler happy */
    (void) data;
    (void) arraylist;

    ptr_hook1 = (struct t_hook *)pointer1;
    ptr_hook2 = (struct t_hook *)pointer2;

    rc = strcmp (PLUGIN(ptr_hook1->plugin), PLUGIN(ptr_hook2->plugin));
    if (rc != 0)
        return rc;

    return strcmp (HOOK_COMMAND(ptr_hook1, command),
                   HOOK_COMMAND(ptr_hook2, command));
}

/*
 * Generates files with commands.
 *
 * Returns:
 *    1: OK, target file updated
 *    0: OK, target file unchanged
 *   -1: error
 */

int
doc_gen_user_commands (const char *path, const char *lang)
{
    FILE *file;
    struct t_hook *ptr_hook;
    struct t_arraylist *list_hooks;
    int i, list_size, length, first_cmd_plugin, first_line;
    char old_plugin[1024], format[32], *value;
    const char *ptr_args, *pos_pipes, *pos_next;

    file = doc_gen_open_file (path, "user", "commands", lang);
    if (!file)
        return -1;

    list_hooks = arraylist_new (64, 1, 0,
                                &doc_gen_hook_command_cmp_cb, NULL,
                                NULL, NULL);
    for (ptr_hook = weechat_hooks[HOOK_TYPE_COMMAND]; ptr_hook;
         ptr_hook = ptr_hook->next_hook)
    {
        if (doc_gen_check_command (PLUGIN(ptr_hook->plugin),
                                   HOOK_COMMAND(ptr_hook, command)))
        {
            arraylist_add (list_hooks, ptr_hook);
        }
    }

    old_plugin[0] = '\0';
    first_cmd_plugin = 0;
    list_size = arraylist_size (list_hooks);
    for (i = 0; i < list_size; i++)
    {
        ptr_hook = (struct t_hook *)arraylist_get (list_hooks, i);
        if (strcmp (PLUGIN(ptr_hook->plugin), old_plugin) != 0)
        {
            if (i > 0)
            {
                string_fprintf (
                    file,
                    "----\n"
                    "// end::%s_commands[]\n"
                    "\n",
                    old_plugin);
            }
            string_fprintf (
                file,
                "// tag::%s_commands[]\n",
                PLUGIN(ptr_hook->plugin));
            strcpy (old_plugin, PLUGIN(ptr_hook->plugin));
            first_cmd_plugin = 1;
        }
        else
        {
            first_cmd_plugin = 0;
        }
        if (!first_cmd_plugin)
            string_fprintf (file, "----\n\n");
        string_fprintf (
            file,
            "[[command_%s_%s]]\n"
            "* `+%s+`: %s\n"
            "\n"
            "----\n",
            PLUGIN(ptr_hook->plugin),
            HOOK_COMMAND(ptr_hook, command),
            HOOK_COMMAND(ptr_hook, command),
            TRANS(HOOK_COMMAND(ptr_hook, description)));

        length = 1 + utf8_strlen (HOOK_COMMAND(ptr_hook, command)) + 2;
        snprintf (format, sizeof (format), "%%-%ds%%s\n", length);
        ptr_args = TRANS(HOOK_COMMAND(ptr_hook, args));
        first_line = 1;
        while (ptr_args && ptr_args[0])
        {
            value = NULL;
            pos_pipes = strstr (ptr_args, "||");
            if (pos_pipes)
            {
                pos_next = pos_pipes + 2;
                while (pos_next[0] == ' ')
                {
                    pos_next++;
                }
                if (pos_pipes > ptr_args)
                {
                    pos_pipes--;
                    while ((pos_pipes > ptr_args) && (pos_pipes[0] == ' '))
                    {
                        pos_pipes--;
                    }
                    value = strndup (ptr_args, pos_pipes - ptr_args + 1);
                }
            }
            else
            {
                value = strdup (ptr_args);
                pos_next = NULL;
            }
            if (value)
            {
                if (first_line)
                {
                    string_fprintf (file,
                                    "/%s  %s\n",
                                    HOOK_COMMAND(ptr_hook, command),
                                    value);
                }
                else
                {
                    string_fprintf (file, format, " ", value);
                }
                first_line = 0;
                free (value);
            }
            ptr_args = pos_next;
        }
        if (HOOK_COMMAND(ptr_hook, args_description)
            && HOOK_COMMAND(ptr_hook, args_description[0]))
        {
            string_fprintf (file,
                            "\n%s\n",
                            TRANS(HOOK_COMMAND(ptr_hook, args_description)));
        }
    }

    string_fprintf (
        file,
        "----\n"
        "// end::%s_commands[]\n",
        old_plugin);

    arraylist_free (list_hooks);

    return doc_gen_close_file (path, "user", "commands", lang, file);
}

/*
 * Checks if an option must be documented or not.
 */

int
doc_gen_check_option (struct t_config_option *option)
{
    if (option->config_file->plugin
        && (strcmp (option->config_file->plugin->name, "alias") == 0))
    {
        return 0;
    }

    if (option->config_file->plugin
        && (strcmp (option->config_file->plugin->name, "trigger") == 0)
        && (strcmp (option->section->name, "trigger") == 0))
    {
        return 0;
    }

    if (!option->config_file->plugin
        && (strcmp (option->section->name, "bar") == 0))
    {
        return 0;
    }

    return 1;
}

/*
 * Compares two options to sort by plugin / command.
 */

int
doc_gen_option_cmp_cb (void *data, struct t_arraylist *arraylist,
                       void *pointer1, void *pointer2)
{
    struct t_config_option *ptr_option1, *ptr_option2;
    int rc;

    /* make C compiler happy */
    (void) data;
    (void) arraylist;

    ptr_option1 = (struct t_config_option *)pointer1;
    ptr_option2 = (struct t_config_option *)pointer2;

    rc = strcmp (ptr_option1->config_file->name,
                 ptr_option2->config_file->name);
    if (rc != 0)
        return rc;

    rc = strcmp (ptr_option1->section->name, ptr_option2->section->name);
    if (rc != 0)
        return rc;

    return strcmp (ptr_option1->name, ptr_option2->name);
}

/*
 * Generates files with commands.
 *
 * Returns:
 *    1: OK, target file updated
 *    0: OK, target file unchanged
 *   -1: error
 */

int
doc_gen_user_options (const char *path, const char *lang)
{
    FILE *file;
    struct t_config_file *ptr_config, *old_config;
    struct t_config_section *ptr_section;
    struct t_config_option *ptr_option;
    struct t_arraylist *list_options;
    int i, list_size, index_option;
    char *desc_escaped, *values, str_values[256];
    char *default_value, *tmp;

    file = doc_gen_open_file (path, "user", "options", lang);
    if (!file)
        return -1;

    list_options = arraylist_new (64, 1, 0,
                                  &doc_gen_option_cmp_cb, NULL,
                                  NULL, NULL);
    for (ptr_config = config_files; ptr_config;
         ptr_config = ptr_config->next_config)
    {
        for (ptr_section = ptr_config->sections; ptr_section;
             ptr_section = ptr_section->next_section)
        {
            for (ptr_option = ptr_section->options; ptr_option;
                 ptr_option = ptr_option->next_option)
            {
                if (doc_gen_check_option (ptr_option))
                    arraylist_add (list_options, ptr_option);
            }
        }
    }

    old_config = NULL;
    index_option = 0;
    list_size = arraylist_size (list_options);
    for (i = 0; i < list_size; i++)
    {
        ptr_option = (struct t_config_option *)arraylist_get (list_options, i);
        if (ptr_option->config_file != old_config)
        {
            if (old_config)
            {
                string_fprintf (
                    file,
                    "// end::%s_options[]\n"
                    "\n",
                    old_config->name);
            }
            string_fprintf (
                file,
                "// tag::%s_options[]\n",
                ptr_option->config_file->name);
            old_config = ptr_option->config_file;
            index_option = 0;
        }
        else
        {
            index_option++;
        }
        if (index_option > 0)
            string_fprintf (file, "\n");
        desc_escaped = (ptr_option->description) ?
            string_replace (TRANS(ptr_option->description), "]", "\\]") :
            strdup ("");
        string_fprintf (file,
                        "* [[option_%s.%s.%s]] *pass:none[%s.%s.%s]*\n",
                        ptr_option->config_file->name,
                        ptr_option->section->name,
                        ESCAPE_ANCHOR(ptr_option->name),
                        ptr_option->config_file->name,
                        ptr_option->section->name,
                        ptr_option->name);
        string_fprintf (file,
                        "** %s: pass:none[%s]\n",
                        _("description"),
                        desc_escaped);
        string_fprintf (file,
                        "** %s: %s\n",
                        _("type"),
                        TRANS(config_option_type_string[ptr_option->type]));
        switch (ptr_option->type)
        {
            case CONFIG_OPTION_TYPE_BOOLEAN:
                values = strdup ("on, off");
                break;
            case CONFIG_OPTION_TYPE_INTEGER:
                if (ptr_option->string_values)
                {
                    values = string_rebuild_split_string (
                        (const char **)ptr_option->string_values, ", ", 0, -1);
                }
                else
                {
                    snprintf (str_values, sizeof (str_values),
                              "%d .. %d",
                              ptr_option->min,
                              ptr_option->max);
                    values = strdup (str_values);
                }
                break;
            case CONFIG_OPTION_TYPE_STRING:
                if (ptr_option->max <= 0)
                    values = strdup (_("any string"));
                else if (ptr_option->max == 1)
                    values = strdup (_("any char"));
                else
                {
                    snprintf (str_values, sizeof (str_values),
                              "%s (%s: %d)",
                              _("any string"),
                              _("max chars"),
                              ptr_option->max);
                    values = strdup (str_values);
                }
                break;
            case CONFIG_OPTION_TYPE_COLOR:
                values = strdup (command_help_option_color_values ());
                break;
            default:
                values = NULL;
                break;
        }
        string_fprintf (file, "** %s: %s\n", _("values"), values);
        default_value = config_file_option_value_to_string (ptr_option,
                                                            1, 0, 0);
        if (ptr_option->type == CONFIG_OPTION_TYPE_STRING)
        {
            tmp = string_replace (default_value, "\"", "\\\"");
            if (default_value)
                free (default_value);
            default_value = tmp;
        }
        string_fprintf (
            file,
            "** %s: `+%s%s%s+`\n",
            _("default value"),
            (ptr_option->type == CONFIG_OPTION_TYPE_STRING) ? "\"" : "",
            default_value,
            (ptr_option->type == CONFIG_OPTION_TYPE_STRING) ? "\"" : "");
        if (desc_escaped)
            free (desc_escaped);
        if (values)
            free (values);
        if (default_value)
            free (default_value);
    }

    if (old_config)
    {
        string_fprintf (
            file,
            "// end::%s_options[]\n",
            old_config->name);
    }

    arraylist_free (list_options);

    return doc_gen_close_file (path, "user", "options", lang, file);
}

/*
 * Generates files with default aliases.
 *
 * Returns:
 *    1: OK, target file updated
 *    0: OK, target file unchanged
 *   -1: error
 */

int
doc_gen_user_default_aliases (const char *path, const char *lang)
{
    FILE *file;
    struct t_infolist *ptr_infolist;
    const char *ptr_completion;

    file = doc_gen_open_file (path, "user", "default_aliases", lang);
    if (!file)
        return -1;

    string_fprintf (
        file,
        "// tag::default_aliases[]\n"
        "[width=\"100%\",cols=\"2m,5m,5\",options=\"header\"]\n"
        "|===\n"
        "| %s | %s | %s\n",
        ESCAPE_TABLE(_("Alias")),
        ESCAPE_TABLE(_("Command")),
        ESCAPE_TABLE(_("Completion")));

    ptr_infolist = hook_infolist_get (NULL, "alias_default", NULL, NULL);
    while (infolist_next (ptr_infolist))
    {
        ptr_completion = infolist_string(ptr_infolist, "completion");
        string_fprintf (file,
                        "| /%s | /%s | %s\n",
                        ESCAPE_TABLE(infolist_string(ptr_infolist, "name")),
                        ESCAPE_TABLE(infolist_string(ptr_infolist, "command")),
                        (ptr_completion && ptr_completion[0]) ?
                        ESCAPE_TABLE(ptr_completion) : "-");
    }
    infolist_free (ptr_infolist);

    string_fprintf (file,
                    "|===\n"
                    "// end::default_aliases[]\n");

    return doc_gen_close_file (path, "user", "default_aliases", lang, file);
}

/*
 * Generates files with IRC colors.
 *
 * Returns:
 *    1: OK, target file updated
 *    0: OK, target file unchanged
 *   -1: error
 */

int
doc_gen_user_irc_colors (const char *path, const char *lang)
{
    FILE *file;
    struct t_infolist *ptr_infolist;

    file = doc_gen_open_file (path, "user", "irc_colors", lang);
    if (!file)
        return -1;

    string_fprintf (
        file,
        "// tag::irc_colors[]\n"
        "[width=\"50%\",cols=\"^2m,3\",options=\"header\"]\n"
        "|===\n"
        "| %s | %s\n",
        ESCAPE_TABLE(_("IRC color")),
        ESCAPE_TABLE(_("WeeChat color")));

    ptr_infolist = hook_infolist_get (NULL, "irc_color_weechat", NULL, NULL);
    while (infolist_next (ptr_infolist))
    {
        string_fprintf (
            file,
            "| %s | %s\n",
            ESCAPE_TABLE(infolist_string(ptr_infolist, "color_irc")),
            ESCAPE_TABLE(infolist_string(ptr_infolist, "color_weechat")));
    }
    infolist_free (ptr_infolist);

    string_fprintf (file,
                    "|===\n"
                    "// end::irc_colors[]\n");

    return doc_gen_close_file (path, "user", "irc_colors", lang, file);
}

/*
 * Compares two hooks "info" to sort by plugin / info.
 */

int
doc_gen_hook_info_cmp_cb (void *data, struct t_arraylist *arraylist,
                          void *pointer1, void *pointer2)
{
    struct t_hook *ptr_hook1, *ptr_hook2;
    int rc;

    /* make C compiler happy */
    (void) data;
    (void) arraylist;

    ptr_hook1 = (struct t_hook *)pointer1;
    ptr_hook2 = (struct t_hook *)pointer2;

    rc = strcmp (PLUGIN(ptr_hook1->plugin), PLUGIN(ptr_hook2->plugin));
    if (rc != 0)
        return rc;

    return strcmp (HOOK_INFO(ptr_hook1, info_name),
                   HOOK_INFO(ptr_hook2, info_name));
}

/*
 * Generates files with infos.
 *
 * Returns:
 *    1: OK, target file updated
 *    0: OK, target file unchanged
 *   -1: error
 */

int
doc_gen_api_infos (const char *path, const char *lang)
{
    FILE *file;
    struct t_hook *ptr_hook;
    struct t_arraylist *list_hooks;
    int i, list_size;

    file = doc_gen_open_file (path, "api", "infos", lang);
    if (!file)
        return -1;

    string_fprintf (
        file,
        "// tag::infos[]\n"
        "[width=\"100%\",cols=\"^1,^2,6,6\",options=\"header\"]\n"
        "|===\n"
        "| %s | %s | %s | %s\n",
        ESCAPE_TABLE(_("Plugin")),
        ESCAPE_TABLE(_("Name")),
        ESCAPE_TABLE(_("Description")),
        ESCAPE_TABLE(_("Arguments")));

    list_hooks = arraylist_new (64, 1, 0,
                                &doc_gen_hook_info_cmp_cb, NULL,
                                NULL, NULL);
    for (ptr_hook = weechat_hooks[HOOK_TYPE_INFO]; ptr_hook;
         ptr_hook = ptr_hook->next_hook)
    {
        arraylist_add (list_hooks, ptr_hook);
    }

    list_size = arraylist_size (list_hooks);
    for (i = 0; i < list_size; i++)
    {
        ptr_hook = (struct t_hook *)arraylist_get (list_hooks, i);
        string_fprintf (
            file,
            "| %s | %s | %s | %s\n",
            ESCAPE_TABLE(PLUGIN(ptr_hook->plugin)),
            ESCAPE_TABLE(HOOK_INFO(ptr_hook, info_name)),
            ESCAPE_TABLE(TRANS(HOOK_INFO(ptr_hook, description))),
            ESCAPE_TABLE(TRANS_DEF(HOOK_INFO(ptr_hook, args_description), "-")));
    }

    arraylist_free (list_hooks);

    string_fprintf (file,
                    "|===\n"
                    "// end::infos[]\n");

    return doc_gen_close_file (path, "api", "infos", lang, file);
}

/*
 * Compares two hooks "info_hashtable" to sort by plugin / info.
 */

int
doc_gen_hook_info_hashtable_cmp_cb (void *data, struct t_arraylist *arraylist,
                                    void *pointer1, void *pointer2)
{
    struct t_hook *ptr_hook1, *ptr_hook2;
    int rc;

    /* make C compiler happy */
    (void) data;
    (void) arraylist;

    ptr_hook1 = (struct t_hook *)pointer1;
    ptr_hook2 = (struct t_hook *)pointer2;

    rc = strcmp (PLUGIN(ptr_hook1->plugin), PLUGIN(ptr_hook2->plugin));
    if (rc != 0)
        return rc;

    return strcmp (HOOK_INFO_HASHTABLE(ptr_hook1, info_name),
                   HOOK_INFO_HASHTABLE(ptr_hook2, info_name));
}

/*
 * Generates files with infos_hashtable.
 *
 * Returns:
 *    1: OK, target file updated
 *    0: OK, target file unchanged
 *   -1: error
 */

int
doc_gen_api_infos_hashtable (const char *path, const char *lang)
{
    FILE *file;
    struct t_hook *ptr_hook;
    struct t_arraylist *list_hooks;
    int i, list_size;

    file = doc_gen_open_file (path, "api", "infos_hashtable", lang);
    if (!file)
        return -1;

    string_fprintf (
        file,
        "// tag::infos_hashtable[]\n"
        "[width=\"100%\",cols=\"^1,^2,6,6,8\",options=\"header\"]\n"
        "|===\n"
        "| %s | %s | %s | %s | %s\n",
        ESCAPE_TABLE(_("Plugin")),
        ESCAPE_TABLE(_("Name")),
        ESCAPE_TABLE(_("Description")),
        ESCAPE_TABLE(_("Hashtable (input)")),
        ESCAPE_TABLE(_("Hashtable (output)")));

    list_hooks = arraylist_new (64, 1, 0,
                                &doc_gen_hook_info_hashtable_cmp_cb, NULL,
                                NULL, NULL);
    for (ptr_hook = weechat_hooks[HOOK_TYPE_INFO_HASHTABLE]; ptr_hook;
         ptr_hook = ptr_hook->next_hook)
    {
        arraylist_add (list_hooks, ptr_hook);
    }

    list_size = arraylist_size (list_hooks);
    for (i = 0; i < list_size; i++)
    {
        ptr_hook = (struct t_hook *)arraylist_get (list_hooks, i);
        string_fprintf (
            file,
            "| %s | %s | %s | %s | %s\n",
            ESCAPE_TABLE(PLUGIN(ptr_hook->plugin)),
            ESCAPE_TABLE(HOOK_INFO(ptr_hook, info_name)),
            ESCAPE_TABLE(TRANS(HOOK_INFO_HASHTABLE(ptr_hook, description))),
            ESCAPE_TABLE(TRANS_DEF(HOOK_INFO_HASHTABLE(ptr_hook, args_description), "-")),
            TRANS_DEF(HOOK_INFO_HASHTABLE(ptr_hook, output_description), "-"));
    }

    arraylist_free (list_hooks);

    string_fprintf (file,
                    "|===\n"
                    "// end::infos_hashtable[]\n");

    return doc_gen_close_file (path, "api", "infos_hashtable", lang, file);
}

/*
 * Compares two hooks "infolist" to sort by plugin / infolist.
 */

int
doc_gen_hook_infolist_cmp_cb (void *data, struct t_arraylist *arraylist,
                              void *pointer1, void *pointer2)
{
    struct t_hook *ptr_hook1, *ptr_hook2;
    int rc;

    /* make C compiler happy */
    (void) data;
    (void) arraylist;

    ptr_hook1 = (struct t_hook *)pointer1;
    ptr_hook2 = (struct t_hook *)pointer2;

    rc = strcmp (PLUGIN(ptr_hook1->plugin), PLUGIN(ptr_hook2->plugin));
    if (rc != 0)
        return rc;

    return strcmp (HOOK_INFOLIST(ptr_hook1, infolist_name),
                   HOOK_INFOLIST(ptr_hook2, infolist_name));
}

/*
 * Generates files with infolists.
 *
 * Returns:
 *    1: OK, target file updated
 *    0: OK, target file unchanged
 *   -1: error
 */

int
doc_gen_api_infolists (const char *path, const char *lang)
{
    FILE *file;
    struct t_hook *ptr_hook;
    struct t_arraylist *list_hooks;
    int i, list_size;

    file = doc_gen_open_file (path, "api", "infolists", lang);
    if (!file)
        return -1;

    string_fprintf (
        file,
        "// tag::infolists[]\n"
        "[width=\"100%\",cols=\"^1,^2,5,5,5\",options=\"header\"]\n"
        "|===\n"
        "| %s | %s | %s | %s | %s\n",
        ESCAPE_TABLE(_("Plugin")),
        ESCAPE_TABLE(_("Name")),
        ESCAPE_TABLE(_("Description")),
        ESCAPE_TABLE(_("Pointer")),
        ESCAPE_TABLE(_("Arguments")));

    list_hooks = arraylist_new (64, 1, 0,
                                &doc_gen_hook_infolist_cmp_cb, NULL,
                                NULL, NULL);
    for (ptr_hook = weechat_hooks[HOOK_TYPE_INFOLIST]; ptr_hook;
         ptr_hook = ptr_hook->next_hook)
    {
        arraylist_add (list_hooks, ptr_hook);
    }

    list_size = arraylist_size (list_hooks);
    for (i = 0; i < list_size; i++)
    {
        ptr_hook = (struct t_hook *)arraylist_get (list_hooks, i);
        string_fprintf (
            file,
            "| %s | %s | %s | %s | %s\n",
            ESCAPE_TABLE(PLUGIN(ptr_hook->plugin)),
            ESCAPE_TABLE(HOOK_INFOLIST(ptr_hook, infolist_name)),
            ESCAPE_TABLE(TRANS(HOOK_INFOLIST(ptr_hook, description))),
            ESCAPE_TABLE(TRANS_DEF(HOOK_INFOLIST(ptr_hook, pointer_description), "-")),
            ESCAPE_TABLE(TRANS_DEF(HOOK_INFOLIST(ptr_hook, args_description), "-")));
    }

    arraylist_free (list_hooks);

    string_fprintf (file,
                    "|===\n"
                    "// end::infolists[]\n");

    return doc_gen_close_file (path, "api", "infolists", lang, file);
}

/*
 * Compares two hooks "hdata" to sort by plugin / hdata.
 */

int
doc_gen_hook_hdata_cmp_cb (void *data, struct t_arraylist *arraylist,
                           void *pointer1, void *pointer2)
{
    struct t_hook *ptr_hook1, *ptr_hook2;
    int rc;

    /* make C compiler happy */
    (void) data;
    (void) arraylist;

    ptr_hook1 = (struct t_hook *)pointer1;
    ptr_hook2 = (struct t_hook *)pointer2;

    rc = strcmp (PLUGIN(ptr_hook1->plugin), PLUGIN(ptr_hook2->plugin));
    if (rc != 0)
        return rc;

    return strcmp (HOOK_HDATA(ptr_hook1, hdata_name),
                   HOOK_HDATA(ptr_hook2, hdata_name));
}

/*
 * Compares two hooks lists to sort by name (and lists beginning with "last_"
 * at the end).
 */

int
doc_gen_hdata_list_cmp_cb (void *data, struct t_arraylist *arraylist,
                           void *pointer1, void *pointer2)
{
    /* make C compiler happy */
    (void) data;
    (void) arraylist;

    if ((strncmp ((const char *)pointer1, "last_", 5) != 0)
        && (strncmp ((const char *)pointer2, "last_", 5) == 0))
    {
        return -1;
    }

    if ((strncmp ((const char *)pointer1, "last_", 5) == 0)
        && (strncmp ((const char *)pointer2, "last_", 5) != 0))
    {
        return 1;
    }

    return strcmp ((const char *)pointer1, (const char *)pointer2);
}

/*
 * Compares two hooks hdata keys to sort by offset.
 */

int
doc_gen_hdata_key_cmp_cb (void *data, struct t_arraylist *arraylist,
                          void *pointer1, void *pointer2)
{
    int offset1, offset2;

    /* make C compiler happy */
    (void) arraylist;

    offset1 = hdata_get_var_offset ((struct t_hdata *)data,
                                    (const char *)pointer1);
    offset2 = hdata_get_var_offset ((struct t_hdata *)data,
                                    (const char *)pointer2);
    return (offset1 < offset2) ?
        -1 : ((offset1 > offset2) ? 1 : 0);
}

/*
 * Generates content of a hdata.
 */

void
doc_gen_api_hdata_content (FILE *file, struct t_hdata *hdata)
{
    const char *ptr_lists, *ptr_keys, *var_hdata, *var_array_size, *ptr_key;
    const char *ptr_list;
    char **lists, **keys, str_var_hdata[1024], str_var_array_size[1024];
    int i, num_lists, num_keys, list_size;
    struct t_arraylist *list_lists, *list_keys, *list_vars_update;
    struct t_hashtable *hashtable;

    ptr_lists = hdata_get_string (hdata, "list_keys");
    if (ptr_lists)
    {
        lists = string_split (ptr_lists, ",", NULL, 0, 0, &num_lists);
        if (lists)
        {
            string_fprintf (file, "| ");
            list_lists = arraylist_new (64, 1, 0,
                                        &doc_gen_hdata_list_cmp_cb, hdata,
                                        NULL, NULL);
            for (i = 0; i < num_lists; i++)
            {
                arraylist_add (list_lists, lists[i]);
            }
            list_size = arraylist_size (list_lists);
            for (i = 0; i < list_size; i++)
            {
                ptr_list = (const char *)arraylist_get (list_lists, i);
                string_fprintf (file, "_%s_ +\n", ptr_list);
            }
            arraylist_free (list_lists);
            string_free_split (lists);
            string_fprintf (file, "\n");
        }
    }
    else
    {
        string_fprintf (file, "| -\n");
    }

    list_vars_update = arraylist_new (64, 0, 1, NULL, NULL, NULL, NULL);
    hashtable = hashtable_new (16,
                               WEECHAT_HASHTABLE_STRING,
                               WEECHAT_HASHTABLE_STRING,
                               NULL, NULL);

    ptr_keys = hdata_get_string (hdata, "var_keys");
    if (ptr_keys)
    {
        keys = string_split (ptr_keys, ",", NULL, 0, 0, &num_keys);
        if (keys)
        {
            string_fprintf (file, "| ");
            list_keys = arraylist_new (64, 1, 0,
                                       &doc_gen_hdata_key_cmp_cb, hdata,
                                       NULL, NULL);
            for (i = 0; i < num_keys; i++)
            {
                arraylist_add (list_keys, keys[i]);
            }
            list_size = arraylist_size (list_keys);
            for (i = 0; i < list_size; i++)
            {
                ptr_key = (const char *)arraylist_get (list_keys, i);
                hashtable_set (hashtable, "__update_allowed", ptr_key);
                if (hdata_update (hdata, NULL, hashtable))
                    arraylist_add (list_vars_update, (void *)ptr_key);
                var_array_size = hdata_get_var_array_size_string (
                    hdata, NULL, ptr_key);
                if (var_array_size)
                {
                    snprintf (str_var_array_size, sizeof (str_var_array_size),
                              ", array_size: \"%s\"",
                              var_array_size);
                }
                else
                {
                    str_var_array_size[0] = '\0';
                }
                var_hdata = hdata_get_var_hdata (hdata, ptr_key);
                if (var_hdata)
                {
                    snprintf (str_var_hdata, sizeof (str_var_hdata),
                              ", hdata: \"%s\"",
                              var_hdata);
                }
                else
                {
                    str_var_hdata[0] = '\0';
                }
                string_fprintf (file,
                                "_%s_   (%s%s%s) +\n",
                                ptr_key,
                                hdata_get_var_type_string (hdata, ptr_key),
                                str_var_array_size,
                                str_var_hdata);
            }
            hashtable_remove_all (hashtable);
            hashtable_set (hashtable, "__create_allowed", "");
            if (hdata_update(hdata, NULL, hashtable))
                arraylist_add (list_vars_update, "{hdata_update_create}");
            hashtable_remove_all (hashtable);
            hashtable_set (hashtable, "__delete_allowed", "");
            if (hdata_update(hdata, NULL, hashtable))
                arraylist_add (list_vars_update, "{hdata_update_delete}");
            list_size = arraylist_size (list_vars_update);
            if (list_size > 0)
            {
                string_fprintf (file, "\n");
                string_fprintf (file, "*%s* +\n", _("Update allowed:"));
                for (i = 0; i < list_size; i++)
                {
                    ptr_key = (const char *)arraylist_get (list_vars_update, i);
                    if (ptr_key[0] == '{')
                    {
                        string_fprintf (file, "    _%s_ +\n", ptr_key);
                    }
                    else
                    {
                        string_fprintf (
                            file,
                            "    _%s_ (%s) +\n",
                            ptr_key,
                            hdata_get_var_type_string (hdata, ptr_key));
                    }
                }
            }
            arraylist_free (list_keys);
            string_free_split (keys);
        }
    }

    hashtable_free (hashtable);
    arraylist_free (list_vars_update);

    string_fprintf (file, "\n");
}

/*
 * Generates files with hdata.
 *
 * Returns:
 *    1: OK, target file updated
 *    0: OK, target file unchanged
 *   -1: error
 */

int
doc_gen_api_hdata (const char *path, const char *lang)
{
    FILE *file;
    struct t_hook *ptr_hook;
    struct t_arraylist *list_hooks;
    struct t_hdata *ptr_hdata;
    int i, list_size;
    char str_anchor[256];

    file = doc_gen_open_file (path, "api", "hdata", lang);
    if (!file)
        return -1;

    string_fprintf (
        file,
        "// tag::hdata[]\n"
        ":hdata_update_create: __create\n"
        ":hdata_update_delete: __delete\n"
        "[width=\"100%\",cols=\"^1,^2,2,2,5\",options=\"header\"]\n"
        "|===\n"
        "| %s | %s | %s | %s | %s\n\n",
        ESCAPE_TABLE(_("Plugin")),
        ESCAPE_TABLE(_("Name")),
        ESCAPE_TABLE(_("Description")),
        ESCAPE_TABLE(_("Lists")),
        ESCAPE_TABLE(_("Variables")));

    list_hooks = arraylist_new (64, 1, 0,
                                &doc_gen_hook_hdata_cmp_cb, NULL,
                                NULL, NULL);
    for (ptr_hook = weechat_hooks[HOOK_TYPE_HDATA]; ptr_hook;
         ptr_hook = ptr_hook->next_hook)
    {
        arraylist_add (list_hooks, ptr_hook);
    }

    list_size = arraylist_size (list_hooks);
    for (i = 0; i < list_size; i++)
    {
        ptr_hook = (struct t_hook *)arraylist_get (list_hooks, i);
        snprintf (str_anchor, sizeof (str_anchor),
                  "hdata_%s",
                  HOOK_HDATA(ptr_hook, hdata_name));
        string_fprintf (file,
                        "| %s\n",
                        ESCAPE_TABLE(PLUGIN(ptr_hook->plugin)));
        string_fprintf (file,
                        "| [[%s]]<<%s,%s>>\n",
                        ESCAPE_TABLE(str_anchor),
                        ESCAPE_TABLE(str_anchor),
                        ESCAPE_TABLE(HOOK_HDATA(ptr_hook, hdata_name)));
        string_fprintf (file,
                        "| %s\n",
                        ESCAPE_TABLE(TRANS(HOOK_HDATA(ptr_hook, description))));
        ptr_hdata = hook_hdata_get (NULL, HOOK_HDATA(ptr_hook, hdata_name));
        if (ptr_hdata)
            doc_gen_api_hdata_content (file, ptr_hdata);
    }

    arraylist_free (list_hooks);

    string_fprintf (file,
                    "|===\n"
                    "// end::hdata[]\n");

    return doc_gen_close_file (path, "api", "hdata", lang, file);
}

/*
 * Compares two hooks "completion" to sort by plugin / completion.
 */

int
doc_gen_hook_completion_cmp_cb (void *data, struct t_arraylist *arraylist,
                                void *pointer1, void *pointer2)
{
    struct t_hook *ptr_hook1, *ptr_hook2;
    int rc;

    /* make C compiler happy */
    (void) data;
    (void) arraylist;

    ptr_hook1 = (struct t_hook *)pointer1;
    ptr_hook2 = (struct t_hook *)pointer2;

    rc = strcmp (PLUGIN(ptr_hook1->plugin), PLUGIN(ptr_hook2->plugin));
    if (rc != 0)
        return rc;

    return strcmp (HOOK_COMPLETION(ptr_hook1, completion_item),
                   HOOK_COMPLETION(ptr_hook2, completion_item));
}

/*
 * Generates files with completions.
 *
 * Returns:
 *    1: OK, target file updated
 *    0: OK, target file unchanged
 *   -1: error
 */

int
doc_gen_api_completions (const char *path, const char *lang)
{
    FILE *file;
    struct t_hook *ptr_hook;
    struct t_arraylist *list_hooks;
    int i, list_size;

    file = doc_gen_open_file (path, "api", "completions", lang);
    if (!file)
        return -1;

    string_fprintf (
        file,
        "// tag::completions[]\n"
        "[width=\"100%\",cols=\"^1,^2,7\",options=\"header\"]\n"
        "|===\n"
        "| %s | %s | %s\n",
        ESCAPE_TABLE(_("Plugin")),
        ESCAPE_TABLE(_("Name")),
        ESCAPE_TABLE(_("Description")));

    list_hooks = arraylist_new (64, 1, 0,
                                &doc_gen_hook_completion_cmp_cb, NULL,
                                NULL, NULL);
    for (ptr_hook = weechat_hooks[HOOK_TYPE_COMPLETION]; ptr_hook;
         ptr_hook = ptr_hook->next_hook)
    {
        arraylist_add (list_hooks, ptr_hook);
    }

    list_size = arraylist_size (list_hooks);
    for (i = 0; i < list_size; i++)
    {
        ptr_hook = (struct t_hook *)arraylist_get (list_hooks, i);
        string_fprintf (
            file,
            "| %s | %s | %s\n",
            ESCAPE_TABLE(PLUGIN(ptr_hook->plugin)),
            ESCAPE_TABLE(HOOK_COMPLETION(ptr_hook, completion_item)),
            ESCAPE_TABLE(TRANS(HOOK_COMPLETION(ptr_hook, description))));
    }

    arraylist_free (list_hooks);

    string_fprintf (file,
                    "|===\n"
                    "// end::completions[]\n");

    return doc_gen_close_file (path, "api", "completions", lang, file);
}

/*
 * Generates files with URL options.
 *
 * Returns:
 *    1: OK, target file updated
 *    0: OK, target file unchanged
 *   -1: error
 */

int
doc_gen_api_url_options (const char *path, const char *lang)
{
    FILE *file;
    int i, j;
    char *name, *constant;

    file = doc_gen_open_file (path, "api", "url_options", lang);
    if (!file)
        return -1;

    string_fprintf (
        file,
        "// tag::url_options[]\n"
        "[width=\"100%\",cols=\"2,^1,7\",options=\"header\"]\n"
        "|===\n"
        "| %s | %s ^(1)^ | %s ^(2)^\n",
        ESCAPE_TABLE(_("Option")),
        ESCAPE_TABLE(_("Type")),
        ESCAPE_TABLE(_("Constants")));

    for (i = 0; url_options[i].name; i++)
    {
        name = string_tolower (url_options[i].name);
        string_fprintf (
            file,
            "| %s | %s |",
            ESCAPE_TABLE(name),
            ESCAPE_TABLE(url_type_string[url_options[i].type]));
        if (name)
            free (name);
        if (url_options[i].constants)
        {
            for (j = 0; url_options[i].constants[j].name; j++)
            {
                if (j > 0)
                    string_fprintf (file, ",");
                constant = string_tolower (url_options[i].constants[j].name);
                string_fprintf (file, " %s", constant);
                if (constant)
                    free (constant);
            }
        }
        string_fprintf (file, "\n");
    }

    string_fprintf (file,
                    "|===\n"
                    "// end::url_options[]\n");

    return doc_gen_close_file (path, "api", "url_options", lang, file);
}

/*
 * Compares two plugins to sort by priority (descending).
 */

int
doc_gen_plugin_cmp_cb (void *data, struct t_arraylist *arraylist,
                       void *pointer1, void *pointer2)
{
    struct t_weechat_plugin *ptr_plugin1, *ptr_plugin2;

    /* make C compiler happy */
    (void) data;
    (void) arraylist;

    ptr_plugin1 = (struct t_weechat_plugin *)pointer1;
    ptr_plugin2 = (struct t_weechat_plugin *)pointer2;

    if (ptr_plugin1->priority != ptr_plugin2->priority)
        return (ptr_plugin1->priority > ptr_plugin2->priority) ?
            -1 : ((ptr_plugin1->priority < ptr_plugin2->priority) ? 1 : 0);

    return strcmp (ptr_plugin1->name, ptr_plugin2->name);
}

/*
 * Generates files with plugins priority.
 *
 * Returns:
 *    1: OK, target file updated
 *    0: OK, target file unchanged
 *   -1: error
 */

int
doc_gen_api_plugins_priority (const char *path, const char *lang)
{
    FILE *file;
    struct t_weechat_plugin *ptr_plugin;
    struct t_arraylist *list_plugins;
    int i, index, list_size;

    file = doc_gen_open_file (path, "api", "plugins_priority", lang);
    if (!file)
        return -1;

    string_fprintf (
        file,
        "// tag::plugins_priority[]\n"
        "[width=\"30%\",cols=\"1,3,2\",options=\"header\"]\n"
        "|===\n"
        "| %s | %s | %s\n",
        ESCAPE_TABLE(_("Rank")),
        ESCAPE_TABLE(_("Plugin")),
        ESCAPE_TABLE(_("Priority")));

    list_plugins = arraylist_new (64, 1, 0,
                                  &doc_gen_plugin_cmp_cb, NULL,
                                  NULL, NULL);
    for (ptr_plugin = weechat_plugins; ptr_plugin;
         ptr_plugin = ptr_plugin->next_plugin)
    {
        arraylist_add (list_plugins, ptr_plugin);
    }

    index = 1;
    list_size = arraylist_size (list_plugins);
    for (i = 0; i < list_size; i++)
    {
        ptr_plugin = (struct t_weechat_plugin *)arraylist_get (list_plugins, i);
        string_fprintf (file,
                        "| %d | %s | %d\n",
                        index,
                        ptr_plugin->name,
                        ptr_plugin->priority);
        index++;
    }

    arraylist_free (list_plugins);

    string_fprintf (file,
                    "|===\n"
                    "// end::plugins_priority[]\n");

    return doc_gen_close_file (path, "api", "plugins_priority", lang, file);
}

/*
 * Compares two configurations to sort by priority (descending).
 */

int
doc_gen_config_cmp_cb (void *data, struct t_arraylist *arraylist,
                       void *pointer1, void *pointer2)
{
    struct t_config_file *ptr_config1, *ptr_config2;

    /* make C compiler happy */
    (void) data;
    (void) arraylist;

    ptr_config1 = (struct t_config_file *)pointer1;
    ptr_config2 = (struct t_config_file *)pointer2;

    if (ptr_config1->priority != ptr_config2->priority)
        return (ptr_config1->priority > ptr_config2->priority) ?
            -1 : ((ptr_config1->priority < ptr_config2->priority) ? 1 : 0);

    return strcmp (ptr_config1->name, ptr_config2->name);
}

/*
 * Generates files with config priority.
 *
 * Returns:
 *    1: OK, target file updated
 *    0: OK, target file unchanged
 *   -1: error
 */

int
doc_gen_api_config_priority (const char *path, const char *lang)
{
    FILE *file;
    struct t_config_file *ptr_config;
    struct t_arraylist *list_configs;
    int i, index, list_size;

    file = doc_gen_open_file (path, "api", "config_priority", lang);
    if (!file)
        return -1;

    string_fprintf (
        file,
        "// tag::config_priority[]\n"
        "[width=\"30%\",cols=\"1,3,2\",options=\"header\"]\n"
        "|===\n"
        "| %s | %s | %s\n",
        ESCAPE_TABLE(_("Rank")),
        ESCAPE_TABLE(_("File")),
        ESCAPE_TABLE(_("Priority")));

    list_configs = arraylist_new (64, 1, 0,
                                  &doc_gen_config_cmp_cb, NULL,
                                  NULL, NULL);
    for (ptr_config = config_files; ptr_config;
         ptr_config = ptr_config->next_config)
    {
        arraylist_add (list_configs, ptr_config);
    }

    index = 1;
    list_size = arraylist_size (list_configs);
    for (i = 0; i < list_size; i++)
    {
        ptr_config = (struct t_config_file *)arraylist_get (list_configs, i);
        string_fprintf (file,
                        "| %d | %s.conf | %d\n",
                        index,
                        ptr_config->name,
                        ptr_config->priority);
        index++;
    }

    arraylist_free (list_configs);

    string_fprintf (file,
                    "|===\n"
                    "// end::config_priority[]\n");

    return doc_gen_close_file (path, "api", "config_priority", lang, file);
}

/*
 * Generates WeeChat files used to build documentation.
 *
 * Returns:
 *   1: OK
 *   0: error
 */

int
doc_generate (const char *path)
{
    int i, j, rc_doc_gen, rc, num_files;
    char *locales[] = {
        "de_DE.UTF-8",
        "en_US.UTF-8",
        "fr_FR.UTF-8",
        "it_IT.UTF-8",
        "ja_JP.UTF-8",
        "pl_PL.UTF-8",
        "sr_RS.UTF-8",
        NULL,
    };
    t_doc_gen_func *doc_gen_functions[] = {
        doc_gen_user_commands,
        doc_gen_user_options,
        doc_gen_user_default_aliases,
        doc_gen_user_irc_colors,
        doc_gen_api_infos,
        doc_gen_api_infos_hashtable,
        doc_gen_api_infolists,
        doc_gen_api_hdata,
        doc_gen_api_completions,
        doc_gen_api_url_options,
        doc_gen_api_plugins_priority,
        doc_gen_api_config_priority,
        NULL,
    };
    char lang[3], *localedir;

    rc_doc_gen = 0;
    num_files = 0;

    index_string_escaped = 0;
    memset (string_escaped, 0, sizeof (string_escaped));

    if (!weechat_plugins)
    {
        string_fprintf (
            stderr,
            "doc generator: WARNING: no plugins loaded, docs will be "
            "incomplete!\n");
    }

    if (!dir_mkdir_parents (path, 0755))
    {
        string_fprintf (
            stderr,
            "doc generator: ERROR: failed to create directory \"%s\")\n",
            path);
        goto end;
    }

    /*
     * set a specific localedir to find .mo files
     * (this is used to generate documentation without installing WeeChat,
     * that means no need to run `make install`)
     */
#ifdef ENABLE_NLS
    localedir = getenv ("WEECHAT_DOCGEN_LOCALEDIR");
    if (localedir && localedir[0])
        bindtextdomain (PACKAGE, localedir);
#endif /* ENABLE_NLS */

    for (i = 0; locales[i]; i++)
    {
        setenv ("LANGUAGE", locales[i], 1);
        if (!setlocale (LC_ALL, locales[i]))
        {
            /* warning on missing locale */
            string_fprintf (
                stderr,
                "doc generator: WARNING: failed to set locale \"%s\", "
                "docs will include auto-generated English content\n",
                locales[i]);
            /* fallback to English */
            setlocale (LC_ALL, "C");
        }
        memcpy (lang, locales[i], 2);
        lang[2] = '\0';
        for (j = 0; doc_gen_functions[j]; j++)
        {
            rc = (int) (doc_gen_functions[j] (path, lang));
            if (rc < 0)
                goto end;
            num_files += rc;
        }
    }

    if (num_files > 0)
        printf ("doc generator: OK, %d files updated\n", num_files);
    else
        printf ("doc generator: OK, no changes\n");

    rc_doc_gen = 1;

end:
    for (i = 0; i < 32; i++)
    {
        if (string_escaped[i])
            free (string_escaped[i]);
    }
    return rc_doc_gen;
}
