/////////////////////////////////////////////////////////////////////////////
// Name:        src/common/translation.cpp
// Purpose:     Internationalization and localisation for wxWidgets
// Author:      Vadim Zeitlin, Vaclav Slavik,
//              Michael N. Filippov <michael@idisys.iae.nsk.su>
//              (2003/09/30 - PluralForms support)
// Created:     2010-04-23
// Copyright:   (c) 1998 Vadim Zeitlin <zeitlin@dptmaths.ens-cachan.fr>
// Licence:     wxWindows licence
/////////////////////////////////////////////////////////////////////////////

// ============================================================================
// declaration
// ============================================================================

// ----------------------------------------------------------------------------
// headers
// ----------------------------------------------------------------------------

// For compilers that support precompilation, includes "wx.h".
#include "wx/wxprec.h"


#if wxUSE_INTL

#ifndef WX_PRECOMP
    #include "wx/dynarray.h"
    #include "wx/string.h"
    #include "wx/intl.h"
    #include "wx/log.h"
    #include "wx/utils.h"
    #include "wx/module.h"
#endif // WX_PRECOMP

// standard headers
#include <ctype.h>
#include <stdlib.h>

#include "wx/arrstr.h"
#include "wx/dir.h"
#include "wx/file.h"
#include "wx/filename.h"
#include "wx/tokenzr.h"
#include "wx/fontmap.h"
#include "wx/stdpaths.h"
#include "wx/version.h"
#include "wx/uilocale.h"

#ifdef __WINDOWS__
    #include "wx/dynlib.h"
    #include "wx/scopedarray.h"
    #include "wx/msw/wrapwin.h"
    #include "wx/msw/missing.h"
#endif

#include <memory>
#include <unordered_set>

// ----------------------------------------------------------------------------
// simple types
// ----------------------------------------------------------------------------

typedef wxUint32 size_t32;

// ----------------------------------------------------------------------------
// constants
// ----------------------------------------------------------------------------

// magic number identifying the .mo format file
const size_t32 MSGCATALOG_MAGIC    = 0x950412de;
const size_t32 MSGCATALOG_MAGIC_SW = 0xde120495;

#define TRACE_I18N wxS("i18n")

// ============================================================================
// implementation
// ============================================================================

namespace
{

// ----------------------------------------------------------------------------
// Platform specific helpers
// ----------------------------------------------------------------------------

#if wxUSE_LOG_TRACE

void LogTraceArray(const char *prefix, const wxVector<wxString>& arr)
{
    wxString s;
    for (wxVector<wxString>::const_iterator j = arr.begin(); j != arr.end(); ++j)
    {
        if (j != arr.begin())
            s += ",";
        s += *j;
    }
    wxLogTrace(TRACE_I18N, "%s: [%s]", prefix, s);
}

void LogTraceLargeArray(const wxString& prefix, const wxArrayString& arr)
{
    wxLogTrace(TRACE_I18N, "%s:", prefix);
    for ( wxArrayString::const_iterator i = arr.begin(); i != arr.end(); ++i )
        wxLogTrace(TRACE_I18N, "    %s", *i);
}

#else // !wxUSE_LOG_TRACE

#define LogTraceArray(prefix, arr)
#define LogTraceLargeArray(prefix, arr)

#endif // wxUSE_LOG_TRACE/!wxUSE_LOG_TRACE

wxString GetPreferredUILanguage(const wxVector<wxString>& available)
{
    const wxVector<wxString>& preferred = wxUILocale::GetPreferredUILanguages();
    LogTraceArray(" - system preferred languages", preferred);

    return wxLocaleIdent::GetBestMatch(preferred, available);
}

} // anonymous namespace

// ----------------------------------------------------------------------------
// Plural forms parser
// ----------------------------------------------------------------------------

/*
                                Simplified Grammar

Expression:
    LogicalOrExpression '?' Expression ':' Expression
    LogicalOrExpression

LogicalOrExpression:
    LogicalAndExpression "||" LogicalOrExpression   // to (a || b) || c
    LogicalAndExpression

LogicalAndExpression:
    EqualityExpression "&&" LogicalAndExpression    // to (a && b) && c
    EqualityExpression

EqualityExpression:
    RelationalExpression "==" RelationalExperession
    RelationalExpression "!=" RelationalExperession
    RelationalExpression

RelationalExpression:
    MultiplicativeExpression '>' MultiplicativeExpression
    MultiplicativeExpression '<' MultiplicativeExpression
    MultiplicativeExpression ">=" MultiplicativeExpression
    MultiplicativeExpression "<=" MultiplicativeExpression
    MultiplicativeExpression

MultiplicativeExpression:
    PmExpression '%' PmExpression
    PmExpression

PmExpression:
    N
    Number
    '(' Expression ')'
*/

class wxPluralFormsToken
{
public:
    enum Type
    {
        T_ERROR, T_EOF, T_NUMBER, T_N, T_PLURAL, T_NPLURALS, T_EQUAL, T_ASSIGN,
        T_GREATER, T_GREATER_OR_EQUAL, T_LESS, T_LESS_OR_EQUAL,
        T_REMINDER, T_NOT_EQUAL,
        T_LOGICAL_AND, T_LOGICAL_OR, T_QUESTION, T_COLON, T_SEMICOLON,
        T_LEFT_BRACKET, T_RIGHT_BRACKET
    };
    Type type() const { return m_type; }
    void setType(Type t) { m_type = t; }
    // for T_NUMBER only
    typedef int Number;
    Number number() const { return m_number; }
    void setNumber(Number num) { m_number = num; }
private:
    Type m_type;
    Number m_number;
};


class wxPluralFormsScanner
{
public:
    wxPluralFormsScanner(const char* s);
    const wxPluralFormsToken& token() const { return m_token; }
    bool nextToken();  // returns false if error
private:
    const char* m_s;
    wxPluralFormsToken m_token;
};

wxPluralFormsScanner::wxPluralFormsScanner(const char* s) : m_s(s)
{
    nextToken();
}

bool wxPluralFormsScanner::nextToken()
{
    wxPluralFormsToken::Type type = wxPluralFormsToken::T_ERROR;
    while (isspace((unsigned char) *m_s))
    {
        ++m_s;
    }
    if (*m_s == 0)
    {
        type = wxPluralFormsToken::T_EOF;
    }
    else if (isdigit((unsigned char) *m_s))
    {
        wxPluralFormsToken::Number number = *m_s++ - '0';
        while (isdigit((unsigned char) *m_s))
        {
            number = number * 10 + (*m_s++ - '0');
        }
        m_token.setNumber(number);
        type = wxPluralFormsToken::T_NUMBER;
    }
    else if (isalpha((unsigned char) *m_s))
    {
        const char* begin = m_s++;
        while (isalnum((unsigned char) *m_s))
        {
            ++m_s;
        }
        size_t size = m_s - begin;
        if (size == 1 && memcmp(begin, "n", size) == 0)
        {
            type = wxPluralFormsToken::T_N;
        }
        else if (size == 6 && memcmp(begin, "plural", size) == 0)
        {
            type = wxPluralFormsToken::T_PLURAL;
        }
        else if (size == 8 && memcmp(begin, "nplurals", size) == 0)
        {
            type = wxPluralFormsToken::T_NPLURALS;
        }
    }
    else if (*m_s == '=')
    {
        ++m_s;
        if (*m_s == '=')
        {
            ++m_s;
            type = wxPluralFormsToken::T_EQUAL;
        }
        else
        {
            type = wxPluralFormsToken::T_ASSIGN;
        }
    }
    else if (*m_s == '>')
    {
        ++m_s;
        if (*m_s == '=')
        {
            ++m_s;
            type = wxPluralFormsToken::T_GREATER_OR_EQUAL;
        }
        else
        {
            type = wxPluralFormsToken::T_GREATER;
        }
    }
    else if (*m_s == '<')
    {
        ++m_s;
        if (*m_s == '=')
        {
            ++m_s;
            type = wxPluralFormsToken::T_LESS_OR_EQUAL;
        }
        else
        {
            type = wxPluralFormsToken::T_LESS;
        }
    }
    else if (*m_s == '%')
    {
        ++m_s;
        type = wxPluralFormsToken::T_REMINDER;
    }
    else if (*m_s == '!' && m_s[1] == '=')
    {
        m_s += 2;
        type = wxPluralFormsToken::T_NOT_EQUAL;
    }
    else if (*m_s == '&' && m_s[1] == '&')
    {
        m_s += 2;
        type = wxPluralFormsToken::T_LOGICAL_AND;
    }
    else if (*m_s == '|' && m_s[1] == '|')
    {
        m_s += 2;
        type = wxPluralFormsToken::T_LOGICAL_OR;
    }
    else if (*m_s == '?')
    {
        ++m_s;
        type = wxPluralFormsToken::T_QUESTION;
    }
    else if (*m_s == ':')
    {
        ++m_s;
        type = wxPluralFormsToken::T_COLON;
    } else if (*m_s == ';') {
        ++m_s;
        type = wxPluralFormsToken::T_SEMICOLON;
    }
    else if (*m_s == '(')
    {
        ++m_s;
        type = wxPluralFormsToken::T_LEFT_BRACKET;
    }
    else if (*m_s == ')')
    {
        ++m_s;
        type = wxPluralFormsToken::T_RIGHT_BRACKET;
    }
    m_token.setType(type);
    return type != wxPluralFormsToken::T_ERROR;
}

class wxPluralFormsNode;

using wxPluralFormsNodePtr = std::unique_ptr<wxPluralFormsNode>;

class wxPluralFormsNode
{
public:
    wxPluralFormsNode(const wxPluralFormsToken& t) : m_token(t) {}
    const wxPluralFormsToken& token() const { return m_token; }
    const wxPluralFormsNode* node(unsigned i) const
        { return m_nodes[i].get(); }
    void setNode(unsigned i, wxPluralFormsNode* n);
    wxPluralFormsNode* releaseNode(unsigned i);
    wxPluralFormsToken::Number evaluate(wxPluralFormsToken::Number n) const;

private:
    wxPluralFormsToken m_token;
    wxPluralFormsNodePtr m_nodes[3];
};

void wxPluralFormsNode::setNode(unsigned i, wxPluralFormsNode* n)
{
    m_nodes[i].reset(n);
}

wxPluralFormsNode*  wxPluralFormsNode::releaseNode(unsigned i)
{
    return m_nodes[i].release();
}

wxPluralFormsToken::Number
wxPluralFormsNode::evaluate(wxPluralFormsToken::Number n) const
{
    switch (token().type())
    {
        // leaf
        case wxPluralFormsToken::T_NUMBER:
            return token().number();
        case wxPluralFormsToken::T_N:
            return n;
        // 2 args
        case wxPluralFormsToken::T_EQUAL:
            return node(0)->evaluate(n) == node(1)->evaluate(n);
        case wxPluralFormsToken::T_NOT_EQUAL:
            return node(0)->evaluate(n) != node(1)->evaluate(n);
        case wxPluralFormsToken::T_GREATER:
            return node(0)->evaluate(n) > node(1)->evaluate(n);
        case wxPluralFormsToken::T_GREATER_OR_EQUAL:
            return node(0)->evaluate(n) >= node(1)->evaluate(n);
        case wxPluralFormsToken::T_LESS:
            return node(0)->evaluate(n) < node(1)->evaluate(n);
        case wxPluralFormsToken::T_LESS_OR_EQUAL:
            return node(0)->evaluate(n) <= node(1)->evaluate(n);
        case wxPluralFormsToken::T_REMINDER:
            {
                wxPluralFormsToken::Number number = node(1)->evaluate(n);
                if (number != 0)
                {
                    return node(0)->evaluate(n) % number;
                }
                else
                {
                    return 0;
                }
            }
        case wxPluralFormsToken::T_LOGICAL_AND:
            return node(0)->evaluate(n) && node(1)->evaluate(n);
        case wxPluralFormsToken::T_LOGICAL_OR:
            return node(0)->evaluate(n) || node(1)->evaluate(n);
        // 3 args
        case wxPluralFormsToken::T_QUESTION:
            return node(0)->evaluate(n)
                ? node(1)->evaluate(n)
                : node(2)->evaluate(n);
        default:
            return 0;
    }
}


class wxPluralFormsCalculator
{
public:
    wxPluralFormsCalculator() : m_nplurals(0), m_plural(nullptr) {}

    // input: number, returns msgstr index
    int evaluate(int n) const;

    // input: text after "Plural-Forms:" (e.g. "nplurals=2; plural=(n != 1);"),
    // if s == 0, creates default handler
    // returns 0 if error
    static wxPluralFormsCalculator* make(const char* s = nullptr);

    ~wxPluralFormsCalculator() {}

    void  init(wxPluralFormsToken::Number nplurals, wxPluralFormsNode* plural);

private:
    wxPluralFormsToken::Number m_nplurals;
    wxPluralFormsNodePtr m_plural;
};

void wxPluralFormsCalculator::init(wxPluralFormsToken::Number nplurals,
                                wxPluralFormsNode* plural)
{
    m_nplurals = nplurals;
    m_plural.reset(plural);
}

int wxPluralFormsCalculator::evaluate(int n) const
{
    if (m_plural.get() == nullptr)
    {
        return 0;
    }
    wxPluralFormsToken::Number number = m_plural->evaluate(n);
    if (number < 0 || number > m_nplurals)
    {
        return 0;
    }
    return number;
}


class wxPluralFormsParser
{
public:
    wxPluralFormsParser(wxPluralFormsScanner& scanner) : m_scanner(scanner) {}
    bool parse(wxPluralFormsCalculator& rCalculator);

private:
    wxPluralFormsNode* parsePlural();
    // stops at T_SEMICOLON, returns 0 if error
    wxPluralFormsScanner& m_scanner;
    const wxPluralFormsToken& token() const;
    bool nextToken();

    wxPluralFormsNode* expression();
    wxPluralFormsNode* logicalOrExpression();
    wxPluralFormsNode* logicalAndExpression();
    wxPluralFormsNode* equalityExpression();
    wxPluralFormsNode* multiplicativeExpression();
    wxPluralFormsNode* relationalExpression();
    wxPluralFormsNode* pmExpression();
};

bool wxPluralFormsParser::parse(wxPluralFormsCalculator& rCalculator)
{
    if (token().type() != wxPluralFormsToken::T_NPLURALS)
        return false;
    if (!nextToken())
        return false;
    if (token().type() != wxPluralFormsToken::T_ASSIGN)
        return false;
    if (!nextToken())
        return false;
    if (token().type() != wxPluralFormsToken::T_NUMBER)
        return false;
    wxPluralFormsToken::Number nplurals = token().number();
    if (!nextToken())
        return false;
    if (token().type() != wxPluralFormsToken::T_SEMICOLON)
        return false;
    if (!nextToken())
        return false;
    if (token().type() != wxPluralFormsToken::T_PLURAL)
        return false;
    if (!nextToken())
        return false;
    if (token().type() != wxPluralFormsToken::T_ASSIGN)
        return false;
    if (!nextToken())
        return false;
    wxPluralFormsNode* plural = parsePlural();
    if (plural == nullptr)
        return false;
    if (token().type() != wxPluralFormsToken::T_SEMICOLON)
        return false;
    if (!nextToken())
        return false;
    if (token().type() != wxPluralFormsToken::T_EOF)
        return false;
    rCalculator.init(nplurals, plural);
    return true;
}

wxPluralFormsNode* wxPluralFormsParser::parsePlural()
{
    wxPluralFormsNode* p = expression();
    if (p == nullptr)
    {
        return nullptr;
    }
    wxPluralFormsNodePtr n(p);
    if (token().type() != wxPluralFormsToken::T_SEMICOLON)
    {
        return nullptr;
    }
    return n.release();
}

const wxPluralFormsToken& wxPluralFormsParser::token() const
{
    return m_scanner.token();
}

bool wxPluralFormsParser::nextToken()
{
    if (!m_scanner.nextToken())
        return false;
    return true;
}

wxPluralFormsNode* wxPluralFormsParser::expression()
{
    wxPluralFormsNode* p = logicalOrExpression();
    if (p == nullptr)
        return nullptr;
    wxPluralFormsNodePtr n(p);
    if (token().type() == wxPluralFormsToken::T_QUESTION)
    {
        wxPluralFormsNodePtr qn(new wxPluralFormsNode(token()));
        if (!nextToken())
        {
            return nullptr;
        }
        p = expression();
        if (p == nullptr)
        {
            return nullptr;
        }
        qn->setNode(1, p);
        if (token().type() != wxPluralFormsToken::T_COLON)
        {
            return nullptr;
        }
        if (!nextToken())
        {
            return nullptr;
        }
        p = expression();
        if (p == nullptr)
        {
            return nullptr;
        }
        qn->setNode(2, p);
        qn->setNode(0, n.release());
        return qn.release();
    }
    return n.release();
}

wxPluralFormsNode*wxPluralFormsParser::logicalOrExpression()
{
    wxPluralFormsNode* p = logicalAndExpression();
    if (p == nullptr)
        return nullptr;
    wxPluralFormsNodePtr ln(p);
    if (token().type() == wxPluralFormsToken::T_LOGICAL_OR)
    {
        wxPluralFormsNodePtr un(new wxPluralFormsNode(token()));
        if (!nextToken())
        {
            return nullptr;
        }
        p = logicalOrExpression();
        if (p == nullptr)
        {
            return nullptr;
        }
        wxPluralFormsNodePtr rn(p);    // right
        if (rn->token().type() == wxPluralFormsToken::T_LOGICAL_OR)
        {
            // see logicalAndExpression comment
            un->setNode(0, ln.release());
            un->setNode(1, rn->releaseNode(0));
            rn->setNode(0, un.release());
            return rn.release();
        }


        un->setNode(0, ln.release());
        un->setNode(1, rn.release());
        return un.release();
    }
    return ln.release();
}

wxPluralFormsNode* wxPluralFormsParser::logicalAndExpression()
{
    wxPluralFormsNode* p = equalityExpression();
    if (p == nullptr)
        return nullptr;
    wxPluralFormsNodePtr ln(p);   // left
    if (token().type() == wxPluralFormsToken::T_LOGICAL_AND)
    {
        wxPluralFormsNodePtr un(new wxPluralFormsNode(token()));  // up
        if (!nextToken())
        {
            return nullptr;
        }
        p = logicalAndExpression();
        if (p == nullptr)
        {
            return nullptr;
        }
        wxPluralFormsNodePtr rn(p);    // right
        if (rn->token().type() == wxPluralFormsToken::T_LOGICAL_AND)
        {
// transform 1 && (2 && 3) -> (1 && 2) && 3
//     u                  r
// l       r     ->   u      3
//       2   3      l   2
            un->setNode(0, ln.release());
            un->setNode(1, rn->releaseNode(0));
            rn->setNode(0, un.release());
            return rn.release();
        }

        un->setNode(0, ln.release());
        un->setNode(1, rn.release());
        return un.release();
    }
    return ln.release();
}

wxPluralFormsNode* wxPluralFormsParser::equalityExpression()
{
    wxPluralFormsNode* p = relationalExpression();
    if (p == nullptr)
        return nullptr;
    wxPluralFormsNodePtr n(p);
    if (token().type() == wxPluralFormsToken::T_EQUAL
        || token().type() == wxPluralFormsToken::T_NOT_EQUAL)
    {
        wxPluralFormsNodePtr qn(new wxPluralFormsNode(token()));
        if (!nextToken())
        {
            return nullptr;
        }
        p = relationalExpression();
        if (p == nullptr)
        {
            return nullptr;
        }
        qn->setNode(1, p);
        qn->setNode(0, n.release());
        return qn.release();
    }
    return n.release();
}

wxPluralFormsNode* wxPluralFormsParser::relationalExpression()
{
    wxPluralFormsNode* p = multiplicativeExpression();
    if (p == nullptr)
        return nullptr;
    wxPluralFormsNodePtr n(p);
    if (token().type() == wxPluralFormsToken::T_GREATER
            || token().type() == wxPluralFormsToken::T_LESS
            || token().type() == wxPluralFormsToken::T_GREATER_OR_EQUAL
            || token().type() == wxPluralFormsToken::T_LESS_OR_EQUAL)
    {
        wxPluralFormsNodePtr qn(new wxPluralFormsNode(token()));
        if (!nextToken())
        {
            return nullptr;
        }
        p = multiplicativeExpression();
        if (p == nullptr)
        {
            return nullptr;
        }
        qn->setNode(1, p);
        qn->setNode(0, n.release());
        return qn.release();
    }
    return n.release();
}

wxPluralFormsNode* wxPluralFormsParser::multiplicativeExpression()
{
    wxPluralFormsNode* p = pmExpression();
    if (p == nullptr)
        return nullptr;
    wxPluralFormsNodePtr n(p);
    if (token().type() == wxPluralFormsToken::T_REMINDER)
    {
        wxPluralFormsNodePtr qn(new wxPluralFormsNode(token()));
        if (!nextToken())
        {
            return nullptr;
        }
        p = pmExpression();
        if (p == nullptr)
        {
            return nullptr;
        }
        qn->setNode(1, p);
        qn->setNode(0, n.release());
        return qn.release();
    }
    return n.release();
}

wxPluralFormsNode* wxPluralFormsParser::pmExpression()
{
    wxPluralFormsNodePtr n;
    if (token().type() == wxPluralFormsToken::T_N
        || token().type() == wxPluralFormsToken::T_NUMBER)
    {
        n.reset(new wxPluralFormsNode(token()));
        if (!nextToken())
        {
            return nullptr;
        }
    }
    else if (token().type() == wxPluralFormsToken::T_LEFT_BRACKET) {
        if (!nextToken())
        {
            return nullptr;
        }
        wxPluralFormsNode* p = expression();
        if (p == nullptr)
        {
            return nullptr;
        }
        n.reset(p);
        if (token().type() != wxPluralFormsToken::T_RIGHT_BRACKET)
        {
            return nullptr;
        }
        if (!nextToken())
        {
            return nullptr;
        }
    }
    else
    {
        return nullptr;
    }
    return n.release();
}

wxPluralFormsCalculator* wxPluralFormsCalculator::make(const char* s)
{
    wxPluralFormsCalculatorPtr calculator(new wxPluralFormsCalculator);
    if (s != nullptr)
    {
        wxPluralFormsScanner scanner(s);
        wxPluralFormsParser p(scanner);
        if (!p.parse(*calculator))
        {
            return nullptr;
        }
    }
    return calculator.release();
}




// ----------------------------------------------------------------------------
// wxMsgCatalogFile corresponds to one disk-file message catalog.
//
// This is a "low-level" class and is used only by wxMsgCatalog
// NOTE: for the documentation of the binary catalog (.MO) files refer to
//       the GNU gettext manual:
//       http://www.gnu.org/software/autoconf/manual/gettext/MO-Files.html
// ----------------------------------------------------------------------------

class wxMsgCatalogFile
{
public:
    typedef wxScopedCharBuffer DataBuffer;

    // ctor & dtor
    wxMsgCatalogFile();
    ~wxMsgCatalogFile();

    // load the catalog from disk
    bool LoadFile(const wxString& filename,
                  wxPluralFormsCalculatorPtr& rPluralFormsCalculator);
    bool LoadData(const DataBuffer& data,
                  wxPluralFormsCalculatorPtr& rPluralFormsCalculator);

    // fills the hash with string-translation pairs
    bool FillHash(wxTranslationsHashMap& hash, const wxString& domain) const;

    // return the charset of the strings in this catalog or empty string if
    // none/unknown
    wxString GetCharset() const { return m_charset; }

private:
    // this implementation is binary compatible with GNU gettext() version 0.10

    // an entry in the string table
    struct wxMsgTableEntry
    {
        size_t32   nLen;           // length of the string
        size_t32   ofsString;      // pointer to the string
    };

    // header of a .mo file
    struct wxMsgCatalogHeader
    {
        size_t32  magic,          // offset +00:  magic id
                  revision,       //        +04:  revision
                  numStrings;     //        +08:  number of strings in the file
        size_t32  ofsOrigTable,   //        +0C:  start of original string table
                  ofsTransTable;  //        +10:  start of translated string table
        size_t32  nHashSize,      //        +14:  hash table size
                  ofsHashTable;   //        +18:  offset of hash table start
    };

    // all data is stored here
    DataBuffer m_data;

    // data description
    size_t32          m_numStrings;   // number of strings in this domain
    const
    wxMsgTableEntry  *m_pOrigTable,   // pointer to original   strings
                     *m_pTransTable;  //            translated

    wxString m_charset;               // from the message catalog header


    // swap the 2 halves of 32 bit integer if needed
    size_t32 Swap(size_t32 ui) const
    {
        return m_bSwapped ? (ui << 24) | ((ui & 0xff00) << 8) |
                            ((ui >> 8) & 0xff00) | (ui >> 24)
                            : ui;
    }

    const char* StringAtOfs(const wxMsgTableEntry* pTable, size_t32 n) const
    {
        const wxMsgTableEntry * const ent = pTable + n;

        // this check could fail for a corrupt message catalog
        size_t32 ofsString = Swap(ent->ofsString);
        if ( ofsString + Swap(ent->nLen) > m_data.length())
        {
            return nullptr;
        }

        return m_data.data() + ofsString;
    }

    bool m_bSwapped;   // wrong endianness?

    wxDECLARE_NO_COPY_CLASS(wxMsgCatalogFile);
};

// ----------------------------------------------------------------------------
// wxMsgCatalogFile class
// ----------------------------------------------------------------------------

wxMsgCatalogFile::wxMsgCatalogFile()
{
}

wxMsgCatalogFile::~wxMsgCatalogFile()
{
}

// open disk file and read in its contents
bool wxMsgCatalogFile::LoadFile(const wxString& filename,
                                wxPluralFormsCalculatorPtr& rPluralFormsCalculator)
{
    wxFile fileMsg(filename);
    if ( !fileMsg.IsOpened() )
        return false;

    // get the file size (assume it is less than 4GB...)
    wxFileOffset lenFile = fileMsg.Length();
    if ( lenFile == wxInvalidOffset )
        return false;

    size_t nSize = wx_truncate_cast(size_t, lenFile);
    wxASSERT_MSG( nSize == lenFile + size_t(0), wxS("message catalog bigger than 4GB?") );

    wxMemoryBuffer filedata;

    // read the whole file in memory
    if ( fileMsg.Read(filedata.GetWriteBuf(nSize), nSize) != lenFile )
        return false;

    filedata.UngetWriteBuf(nSize);

    bool ok = LoadData
              (
                  DataBuffer::CreateOwned((char*)filedata.release(), nSize),
                  rPluralFormsCalculator
              );
    if ( !ok )
    {
        wxLogWarning(_("'%s' is not a valid message catalog."), filename);
        return false;
    }

    return true;
}


bool wxMsgCatalogFile::LoadData(const DataBuffer& data,
                                wxPluralFormsCalculatorPtr& rPluralFormsCalculator)
{
    // examine header
    bool bValid = data.length() > sizeof(wxMsgCatalogHeader);

    const wxMsgCatalogHeader* pHeader = reinterpret_cast<const wxMsgCatalogHeader*>(data.data());
    if ( bValid ) {
        // we'll have to swap all the integers if it's true
        m_bSwapped = pHeader->magic == MSGCATALOG_MAGIC_SW;

        // check the magic number
        bValid = m_bSwapped || pHeader->magic == MSGCATALOG_MAGIC;
    }

    if ( !bValid ) {
        // it's either too short or has incorrect magic number
        wxLogWarning(_("Invalid message catalog."));
        return false;
    }

    m_data = data;

    // initialize
    m_numStrings  = Swap(pHeader->numStrings);
    m_pOrigTable  = reinterpret_cast<const wxMsgTableEntry*>(data.data() +
                    Swap(pHeader->ofsOrigTable));
    m_pTransTable = reinterpret_cast<const wxMsgTableEntry*>(data.data() +
                    Swap(pHeader->ofsTransTable));

    // now parse catalog's header and try to extract catalog charset and
    // plural forms formula from it:

    const char* headerData = StringAtOfs(m_pOrigTable, 0);
    if ( headerData && headerData[0] == '\0' )
    {
        // Extract the charset:
        const char * const header = StringAtOfs(m_pTransTable, 0);
        const char *
            cset = strstr(header, "Content-Type: text/plain; charset=");
        if ( cset )
        {
            cset += 34; // strlen("Content-Type: text/plain; charset=")

            const char * const csetEnd = strchr(cset, '\n');
            if ( csetEnd )
            {
                m_charset = wxString(cset, csetEnd - cset);
                if ( m_charset == wxS("CHARSET") )
                {
                    // "CHARSET" is not valid charset, but lazy translator
                    m_charset.clear();
                }
            }
        }
        // else: incorrectly filled Content-Type header

        // Extract plural forms:
        const char * plurals = strstr(header, "Plural-Forms:");
        if ( plurals )
        {
            plurals += 13; // strlen("Plural-Forms:")
            const char * const pluralsEnd = strchr(plurals, '\n');
            if ( pluralsEnd )
            {
                const size_t pluralsLen = pluralsEnd - plurals;
                wxCharBuffer buf(pluralsLen);
                strncpy(buf.data(), plurals, pluralsLen);
                wxPluralFormsCalculator * const
                    pCalculator = wxPluralFormsCalculator::make(buf);
                if ( pCalculator )
                {
                    rPluralFormsCalculator.reset(pCalculator);
                }
                else
                {
                    wxLogVerbose(_("Failed to parse Plural-Forms: '%s'"),
                                 buf.data());
                }
            }
        }

        if ( !rPluralFormsCalculator.get() )
            rPluralFormsCalculator.reset(wxPluralFormsCalculator::make());
    }

    // everything is fine
    return true;
}

bool wxMsgCatalogFile::FillHash(wxTranslationsHashMap& hash,
                                const wxString& domain) const
{
    wxUnusedVar(domain); // silence warning in Unicode build

    // conversion to use to convert catalog strings to the GUI encoding
    wxMBConv *inputConv = nullptr;

    std::unique_ptr<wxMBConv> inputConvPtr; // just to delete inputConv if needed

    if ( !m_charset.empty() )
    {
        inputConv = new wxCSConv(m_charset);

        // As we allocated it ourselves, we need to delete it, so ensure
        // this happens.
        inputConvPtr.reset(inputConv);
    }
    else // no need to convert the encoding
    {
        // we must somehow convert the narrow strings in the message catalog to
        // wide strings, so use the default conversion if we have no charset
        inputConv = wxConvCurrent;
    }

    for (size_t32 i = 0; i < m_numStrings; i++)
    {
        const char *data = StringAtOfs(m_pOrigTable, i);
        if (!data)
            return false; // may happen for invalid MO files

        wxString msgid;
        msgid = wxString(data, *inputConv);
        data = StringAtOfs(m_pTransTable, i);
        if (!data)
            return false; // may happen for invalid MO files

        size_t length = Swap(m_pTransTable[i].nLen);
        size_t offset = 0;
        size_t index = 0;
        while (offset < length)
        {
            const char * const str = data + offset;

            wxString msgstr;
            msgstr = wxString(str, *inputConv);
            if ( !msgstr.empty() )
            {
                hash[index == 0 ? msgid : msgid + wxChar(index)] = msgstr;
            }

            // skip this string
            // IMPORTANT: accesses to the 'data' pointer are valid only for
            //            the first 'length+1' bytes (GNU specs says that the
            //            final NUL is not counted in length); using wxStrnlen()
            //            we make sure we don't access memory beyond the valid range
            //            (which otherwise may happen for invalid MO files):
            offset += wxStrnlen(str, length - offset) + 1;
            ++index;
        }
    }

    return true;
}


// ----------------------------------------------------------------------------
// wxMsgCatalog class
// ----------------------------------------------------------------------------

wxMsgCatalog::wxMsgCatalog(const wxString& domain)
    : m_pNext(nullptr), m_domain(domain)
{
}

wxMsgCatalog::~wxMsgCatalog() = default;

/* static */
wxMsgCatalog *wxMsgCatalog::CreateFromFile(const wxString& filename,
                                           const wxString& domain)
{
    std::unique_ptr<wxMsgCatalog> cat(new wxMsgCatalog(domain));

    wxMsgCatalogFile file;

    if ( !file.LoadFile(filename, cat->m_pluralFormsCalculator) )
        return nullptr;

    if ( !file.FillHash(cat->m_messages, domain) )
        return nullptr;

    return cat.release();
}

/* static */
wxMsgCatalog *wxMsgCatalog::CreateFromData(const wxScopedCharBuffer& data,
                                           const wxString& domain)
{
    std::unique_ptr<wxMsgCatalog> cat(new wxMsgCatalog(domain));

    wxMsgCatalogFile file;

    if ( !file.LoadData(data, cat->m_pluralFormsCalculator) )
        return nullptr;

    if ( !file.FillHash(cat->m_messages, domain) )
        return nullptr;

    return cat.release();
}

const wxString *wxMsgCatalog::GetString(const wxString& str, unsigned n, const wxString& context) const
{
    int index = 0;
    if (n != UINT_MAX)
    {
        index = m_pluralFormsCalculator->evaluate(n);
    }
    wxTranslationsHashMap::const_iterator i;
    if (index != 0)
    {
        if (context.IsEmpty())
            i = m_messages.find(wxString(str) + wxChar(index));   // plural, no context
        else
            i = m_messages.find(wxString(context) + wxString('\x04') + wxString(str) + wxChar(index));   // plural, context
    }
    else
    {
        if (context.IsEmpty())
            i = m_messages.find(str); // no context
        else
            i = m_messages.find(wxString(context) + wxString('\x04') + wxString(str)); // context
    }

    if ( i != m_messages.end() )
    {
        return &i->second;
    }
    else
        return nullptr;
}


// ----------------------------------------------------------------------------
// wxTranslations
// ----------------------------------------------------------------------------

namespace
{

wxTranslations *gs_translations = nullptr;
bool gs_translationsOwned = false;

} // anonymous namespace


/*static*/
wxTranslations *wxTranslations::Get()
{
    return gs_translations;
}

/*static*/
void wxTranslations::Set(wxTranslations *t)
{
    if ( gs_translationsOwned )
        delete gs_translations;
    gs_translations = t;
    gs_translationsOwned = true;
}

/*static*/
void wxTranslations::SetNonOwned(wxTranslations *t)
{
    if ( gs_translationsOwned )
        delete gs_translations;
    gs_translations = t;
    gs_translationsOwned = false;
}


wxTranslations::wxTranslations()
{
    m_pMsgCat = nullptr;
    m_loader = new wxFileTranslationsLoader;
}


wxTranslations::~wxTranslations()
{
    delete m_loader;

    // free catalogs memory
    while ( m_pMsgCat != nullptr )
    {
        wxMsgCatalog* pTmpCat;
        pTmpCat = m_pMsgCat;
        m_pMsgCat = m_pMsgCat->m_pNext;
        delete pTmpCat;
    }
}


void wxTranslations::SetLoader(wxTranslationsLoader *loader)
{
    wxCHECK_RET( loader, "loader can't be null" );

    delete m_loader;
    m_loader = loader;
}


void wxTranslations::SetLanguage(wxLanguage lang)
{
    if ( lang == wxLANGUAGE_DEFAULT )
        SetLanguage(wxString());
    else
        SetLanguage(wxUILocale::GetLanguageCanonicalName(lang));
}

void wxTranslations::SetLanguage(const wxString& lang)
{
    m_lang = lang;
}


wxArrayString wxTranslations::GetAvailableTranslations(const wxString& domain) const
{
    wxCHECK_MSG( m_loader, wxArrayString(), "loader can't be null" );

    return m_loader->GetAvailableTranslations(domain);
}


bool wxTranslations::AddStdCatalog()
{
    // Try loading the message catalog for this version first, but fall back to
    // the name without the version if it's not found, as message catalogs
    // typically won't have the version in their names under non-Unix platforms
    // (i.e. where they're not installed by our own "make install").
    wxString domain("wxstd-" wxSTRINGIZE(wxMAJOR_VERSION) "." wxSTRINGIZE(wxMINOR_VERSION));
    if ( GetBestAvailableTranslation(domain).empty() )
        domain = wxS("wxstd");

    return AddCatalog(domain);
}

bool wxTranslations::AddAvailableCatalog(const wxString& domain, wxLanguage msgIdLanguage)
{
    return DoAddCatalog(domain, msgIdLanguage) == Translations::Found;
}

bool wxTranslations::AddCatalog(const wxString& domain, wxLanguage msgIdLanguage)
{
    return DoAddCatalog(domain, msgIdLanguage) != Translations::NotFound;
}

wxTranslations::Translations wxTranslations::DoAddCatalog(const wxString& domain,
                                                          wxLanguage msgIdLanguage)
{
    const wxString msgIdLang = wxUILocale::GetLanguageCanonicalName(msgIdLanguage);
    const wxString domain_lang = GetBestTranslation(domain, msgIdLang);
    if ( domain_lang.empty() )
    {
        wxLogTrace(TRACE_I18N,
                    wxS("no suitable translation for domain '%s' found"),
                    domain);
        return Translations::NotFound;
    }

    if ( LoadCatalog(domain, domain_lang) )
    {
        wxLogTrace(TRACE_I18N,
                   wxS("adding '%s' translation for domain '%s' (msgid language '%s')"),
                   domain_lang, domain, msgIdLang);
        return Translations::Found;
    }

    // LoadCatalog() failed, but GetBestTranslation() returned non-empty language.
    // That must mean that msgIdLanguage was used.
    wxLogTrace(TRACE_I18N,
                wxS("not using translations for domain '%s' with msgid language '%s'"),
                domain, msgIdLang);
    return Translations::NotNeeded;
}


bool wxTranslations::LoadCatalog(const wxString& domain, const wxString& lang)
{
    wxCHECK_MSG( m_loader, false, "loader can't be null" );

    wxMsgCatalog *cat = nullptr;

#if wxUSE_FONTMAP
    // first look for the catalog for this language and the current locale:
    // notice that we don't use the system name for the locale as this would
    // force us to install catalogs in different locations depending on the
    // system but always use the canonical name
    wxFontEncoding encSys = wxLocale::GetSystemEncoding();
    if ( encSys != wxFONTENCODING_SYSTEM )
    {
        wxString fullname(lang);
        fullname << wxS('.') << wxFontMapperBase::GetEncodingName(encSys);

        cat = m_loader->LoadCatalog(domain, fullname);
    }
#endif // wxUSE_FONTMAP

    if ( !cat )
    {
        // Next try: use the provided name language name:
        cat = m_loader->LoadCatalog(domain, lang);
    }

    if ( !cat )
    {
        // Also try just base locale name: for things like "fr_BE" (Belgium
        // French) we should use fall back on plain "fr" if no Belgium-specific
        // message catalogs exist
        wxString baselang = lang.BeforeFirst('_');
        if ( lang != baselang )
            cat = m_loader->LoadCatalog(domain, baselang);
    }

    if ( cat )
    {
        // add it to the head of the list so that in GetString it will
        // be searched before the catalogs added earlier

        cat->m_pNext = m_pMsgCat;
        m_pMsgCat = cat;
        m_catalogMap[domain] = cat;

        return true;
    }
    else
    {
        // Nothing worked, the catalog just isn't there
        wxLogTrace(TRACE_I18N,
                   "Catalog \"%s.mo\" not found for language \"%s\".",
                   domain, lang);
        return false;
    }
}

// check if the given catalog is loaded
bool wxTranslations::IsLoaded(const wxString& domain) const
{
    return FindCatalog(domain) != nullptr;
}

wxString wxTranslations::GetBestTranslation(const wxString& domain,
                                            wxLanguage msgIdLanguage)
{
    const wxString lang = wxUILocale::GetLanguageCanonicalName(msgIdLanguage);
    return GetBestTranslation(domain, lang);
}

wxString wxTranslations::GetBestTranslation(const wxString& domain,
                                            const wxString& msgIdLanguage)
{
    // Determine the best language, including the msgId language, which is always
    // available because it is present in the code:
    wxString lang = DoGetBestAvailableTranslation(domain, msgIdLanguage);

    if ( lang.empty() )
    {
        wxLogTrace(TRACE_I18N,
                   "no available language for domain '%s'", domain);
    }
    else if ( lang == msgIdLanguage || lang == msgIdLanguage.BeforeFirst('_') )
    {
        wxLogTrace(TRACE_I18N,
                   "using message ID language '%s' for domain '%s'", lang, domain);
    }

    return lang;
}

wxString wxTranslations::GetBestAvailableTranslation(const wxString& domain)
{
    // Determine the best language from the ones with actual translation file:
    // As this function never considers the language of the original messages as being
    // available, pass empty string as message ID language to the helper function.
    return DoGetBestAvailableTranslation(domain, wxString());
}

wxString wxTranslations::DoGetBestAvailableTranslation(const wxString& domain, const wxString& additionalAvailableLanguage)
{
    wxArrayString available(GetAvailableTranslations(domain));
    if ( !additionalAvailableLanguage.empty() )
    {
        available.push_back(additionalAvailableLanguage);
        available.push_back(additionalAvailableLanguage.BeforeFirst('_'));
    }

    if ( !m_lang.empty() )
    {
        wxLogTrace(TRACE_I18N,
                   "searching for best translation to %s for domain '%s'",
                   m_lang, domain);

        wxString lang;
        if ( available.Index(m_lang) != wxNOT_FOUND )
        {
            lang = m_lang;
        }
        else
        {
            const wxString baselang = m_lang.BeforeFirst('_');
            if ( baselang != m_lang && available.Index(baselang) != wxNOT_FOUND )
                lang = baselang;
        }

        if ( lang.empty() )
            wxLogTrace(TRACE_I18N, " => no available translations found");
        else
            wxLogTrace(TRACE_I18N, " => found '%s'", lang);

        return lang;
    }

    const wxString lang = GetPreferredUILanguage(available.AsVector());
    wxLogTrace(TRACE_I18N, " => using language '%s'", lang);
    return lang;
}


/* static */
const wxString& wxTranslations::GetUntranslatedString(const wxString& str)
{
    thread_local std::unordered_set<wxString> wxPerThreadStrings;
    return *wxPerThreadStrings.insert(str).first;
}


const wxString *wxTranslations::GetTranslatedString(const wxString& origString,
                                                    const wxString& domain,
                                                    const wxString& context) const
{
    return GetTranslatedString(origString, UINT_MAX, domain, context);
}

const wxString *wxTranslations::GetTranslatedString(const wxString& origString,
                                                    unsigned n,
                                                    const wxString& domain,
                                                    const wxString& context) const
{
    if ( origString.empty() )
        return nullptr;

    const wxString *trans = nullptr;
    wxMsgCatalog *pMsgCat;

    if ( !domain.empty() )
    {
        pMsgCat = FindCatalog(domain);

        // does the catalog exist?
        if ( pMsgCat != nullptr )
            trans = pMsgCat->GetString(origString, n, context);
    }
    else
    {
        // search in all domains
        for ( pMsgCat = m_pMsgCat; pMsgCat != nullptr; pMsgCat = pMsgCat->m_pNext )
        {
            trans = pMsgCat->GetString(origString, n, context);
            if ( trans != nullptr )   // take the first found
                break;
        }
    }

    if ( trans == nullptr )
    {
        wxLogTrace
        (
            TRACE_I18N,
            "string \"%s\"%s not found in %s%slocale '%s'.",
            origString,
            (n != UINT_MAX ? wxString::Format("[%ld]", (long)n) : wxString()),
            (!domain.empty() ? wxString::Format("domain '%s' ", domain) : wxString()),
            (!context.empty() ? wxString::Format("context '%s' ", context) : wxString()),
            m_lang
        );
    }

    return trans;
}

wxString wxTranslations::GetHeaderValue(const wxString& header,
                                        const wxString& domain) const
{
    if ( header.empty() )
        return wxEmptyString;

    const wxString *trans = nullptr;
    wxMsgCatalog *pMsgCat;

    if ( !domain.empty() )
    {
        pMsgCat = FindCatalog(domain);

        // does the catalog exist?
        if ( pMsgCat == nullptr )
            return wxEmptyString;

        trans = pMsgCat->GetString(wxEmptyString, UINT_MAX);
    }
    else
    {
        // search in all domains
        for ( pMsgCat = m_pMsgCat; pMsgCat != nullptr; pMsgCat = pMsgCat->m_pNext )
        {
            trans = pMsgCat->GetString(wxEmptyString, UINT_MAX);
            if ( trans != nullptr )   // take the first found
                break;
        }
    }

    if ( !trans || trans->empty() )
        return wxEmptyString;

    size_t found = trans->find(header + wxS(": "));
    if ( found == wxString::npos )
        return wxEmptyString;

    found += header.length() + 2 /* ': ' */;

    // Every header is separated by \n

    size_t endLine = trans->find(wxS('\n'), found);
    size_t len = (endLine == wxString::npos) ?
                wxString::npos : (endLine - found);

    return trans->substr(found, len);
}


// find catalog by name
wxMsgCatalog *wxTranslations::FindCatalog(const wxString& domain) const
{
    const wxMsgCatalogMap::const_iterator found = m_catalogMap.find(domain);

    return found == m_catalogMap.end() ? nullptr : found->second;
}

// ----------------------------------------------------------------------------
// wxFileTranslationsLoader
// ----------------------------------------------------------------------------

namespace
{

// the list of the directories to search for message catalog files
wxArrayString gs_searchPrefixes;

// return the directories to search for message catalogs under the given
// prefix, separated by wxPATH_SEP
wxString GetMsgCatalogSubdirs(const wxString& prefix, const wxString& lang)
{
    // Search first in Unix-standard prefix/lang/LC_MESSAGES, then in
    // prefix/lang.
    //
    // Note that we use LC_MESSAGES on all platforms and not just Unix, because
    // it doesn't cost much to look into one more directory and doing it this
    // way has two important benefits:
    // a) we don't break compatibility with wx-2.6 and older by stopping to
    //    look in a directory where the catalogs used to be and thus silently
    //    breaking apps after they are recompiled against the latest wx
    // b) it makes it possible to package app's support files in the same
    //    way on all target platforms
    const wxString prefixAndLang = wxFileName(prefix, lang).GetFullPath();

    wxString searchPath;
    searchPath.reserve(4*prefixAndLang.length());

    searchPath
#ifdef __WXOSX__
               << prefixAndLang << ".lproj/LC_MESSAGES" << wxPATH_SEP
               << prefixAndLang << ".lproj" << wxPATH_SEP
#endif
               << prefixAndLang << wxFILE_SEP_PATH << "LC_MESSAGES" << wxPATH_SEP
               << prefixAndLang << wxPATH_SEP
               ;

    return searchPath;
}

bool HasMsgCatalogInDir(const wxString& dir, const wxString& domain)
{
    return wxFileName(dir, domain, "mo").FileExists() ||
           wxFileName(dir + wxFILE_SEP_PATH + "LC_MESSAGES", domain, "mo").FileExists();
}

// get prefixes to locale directories; if lang is empty, don't point to
// OSX's .lproj bundles
wxArrayString GetSearchPrefixes()
{
    wxArrayString paths;

    // first take the entries explicitly added by the program
    paths = gs_searchPrefixes;

#if wxUSE_STDPATHS
    // then look in the standard location
    wxString stdp;
    stdp = wxStandardPaths::Get().GetResourcesDir();
    if ( paths.Index(stdp) == wxNOT_FOUND )
        paths.Add(stdp);

  #ifdef wxHAS_STDPATHS_INSTALL_PREFIX
    stdp = wxStandardPaths::Get().GetInstallPrefix() + "/share/locale";
    if ( paths.Index(stdp) == wxNOT_FOUND )
        paths.Add(stdp);
  #endif
#endif // wxUSE_STDPATHS

    // last look in default locations
#ifdef __UNIX__
    // LC_PATH is a standard env var containing the search path for the .mo
    // files
    const char *pszLcPath = wxGetenv("LC_PATH");
    if ( pszLcPath )
    {
        const wxString lcp = pszLcPath;
        if ( paths.Index(lcp) == wxNOT_FOUND )
            paths.Add(lcp);
    }

    // also add the one from where wxWin was installed:
    wxString wxp = wxGetInstallPrefix();
    if ( !wxp.empty() )
    {
        wxp += wxS("/share/locale");
        if ( paths.Index(wxp) == wxNOT_FOUND )
            paths.Add(wxp);
    }
#endif // __UNIX__

    return paths;
}

// construct the search path for the given language
wxString GetFullSearchPath(const wxString& lang)
{
    wxString searchPath;
    searchPath.reserve(500);

    const wxArrayString prefixes = GetSearchPrefixes();

    for ( wxArrayString::const_iterator i = prefixes.begin();
          i != prefixes.end();
          ++i )
    {
        const wxString p = GetMsgCatalogSubdirs(*i, lang);

        if ( !searchPath.empty() )
            searchPath += wxPATH_SEP;
        searchPath += p;
    }

    return searchPath;
}

} // anonymous namespace


void wxFileTranslationsLoader::AddCatalogLookupPathPrefix(const wxString& prefix)
{
    if ( gs_searchPrefixes.Index(prefix) == wxNOT_FOUND )
    {
        gs_searchPrefixes.Add(prefix);
    }
    //else: already have it
}


wxMsgCatalog *wxFileTranslationsLoader::LoadCatalog(const wxString& domain,
                                                    const wxString& lang)
{
    wxString searchPath = GetFullSearchPath(lang);

    LogTraceLargeArray
    (
        wxString::Format("looking for \"%s.mo\" in search path", domain),
        wxSplit(searchPath, wxPATH_SEP[0])
    );

    wxFileName fn(wxString(), domain, wxS("mo"));

    wxString strFullName;
    if ( !wxFindFileInPath(&strFullName, searchPath, fn.GetFullPath()) )
        return nullptr;

    // open file and read its data
    wxLogVerbose(_("using catalog '%s' from '%s'."), domain, strFullName);
    wxLogTrace(TRACE_I18N, wxS("Using catalog \"%s\"."), strFullName);

    return wxMsgCatalog::CreateFromFile(strFullName, domain);
}


wxArrayString wxFileTranslationsLoader::GetAvailableTranslations(const wxString& domain) const
{
    wxArrayString langs;
    const wxArrayString prefixes = GetSearchPrefixes();

    LogTraceLargeArray
    (
        wxString::Format("looking for available translations of \"%s\" in search path", domain),
        prefixes
    );

    for ( wxArrayString::const_iterator i = prefixes.begin();
          i != prefixes.end();
          ++i )
    {
        if ( i->empty() )
            continue;
        wxDir dir;
        if ( !dir.Open(*i) )
            continue;

        wxString lang;
        for ( bool ok = dir.GetFirst(&lang, wxString(), wxDIR_DIRS);
              ok;
              ok = dir.GetNext(&lang) )
        {
            const wxString langdir = *i + wxFILE_SEP_PATH + lang;
            if ( HasMsgCatalogInDir(langdir, domain) )
            {
#ifdef __WXOSX__
                wxString rest;
                if ( lang.EndsWith(".lproj", &rest) )
                    lang = rest;
#endif // __WXOSX__

                wxLogTrace(TRACE_I18N,
                           "found %s translation of \"%s\" in %s",
                           lang, domain, langdir);
                langs.push_back(lang);
            }
        }
    }

    return langs;
}


// ----------------------------------------------------------------------------
// wxResourceTranslationsLoader
// ----------------------------------------------------------------------------

#ifdef __WINDOWS__

wxMsgCatalog *wxResourceTranslationsLoader::LoadCatalog(const wxString& domain,
                                                        const wxString& lang)
{
    const void *mo_data = nullptr;
    size_t mo_size = 0;

    // Language may contain non-alphabetic characters that are not allowed in the
    // resource names that must be valid identifiers, so sanitize the language
    // before using it as part of the resource name.
    wxString lang_sanitized = lang;
    for ( wxString::iterator it = lang_sanitized.begin(); it != lang_sanitized.end(); ++it )
    {
        const wxUniChar c = *it;
        if ( !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) )
            *it = '_';
    }
    const wxString resname = wxString::Format("%s_%s", domain, lang_sanitized);

    if ( !wxLoadUserResource(&mo_data, &mo_size,
                             resname,
                             GetResourceType().t_str(),
                             GetModule()) )
        return nullptr;

    wxLogTrace(TRACE_I18N,
               "Using catalog from Windows resource \"%s\".", resname);

    wxMsgCatalog *cat = wxMsgCatalog::CreateFromData(
        wxCharBuffer::CreateNonOwned(static_cast<const char*>(mo_data), mo_size),
        domain);

    if ( !cat )
    {
        wxLogWarning(_("Resource '%s' is not a valid message catalog."), resname);
    }

    return cat;
}

namespace
{

struct EnumCallbackData
{
    wxString prefix;
    wxArrayString langs;
};

BOOL CALLBACK EnumTranslations(HMODULE WXUNUSED(hModule),
                               LPCTSTR WXUNUSED(lpszType),
                               LPTSTR lpszName,
                               LONG_PTR lParam)
{
    wxString name(lpszName);
    name.MakeLower(); // resource names are case insensitive

    EnumCallbackData *data = reinterpret_cast<EnumCallbackData*>(lParam);

    wxString lang;
    if ( name.StartsWith(data->prefix, &lang) && !lang.empty() )
        data->langs.push_back(lang);

    return TRUE; // continue enumeration
}

} // anonymous namespace


wxArrayString wxResourceTranslationsLoader::GetAvailableTranslations(const wxString& domain) const
{
    EnumCallbackData data;
    data.prefix = domain + "_";
    data.prefix.MakeLower(); // resource names are case insensitive

    if ( !EnumResourceNames(GetModule(),
                            GetResourceType().t_str(),
                            EnumTranslations,
                            reinterpret_cast<LONG_PTR>(&data)) )
    {
        const DWORD err = GetLastError();
        if ( err != NO_ERROR && err != ERROR_RESOURCE_TYPE_NOT_FOUND )
        {
            wxLogSysError(_("Couldn't enumerate translations"));
        }
    }

    return data.langs;
}

#endif // __WINDOWS__


// ----------------------------------------------------------------------------
// wxTranslationsModule module (for destruction of gs_translations)
// ----------------------------------------------------------------------------

class wxTranslationsModule: public wxModule
{
    wxDECLARE_DYNAMIC_CLASS(wxTranslationsModule);
public:
        wxTranslationsModule() {}

        bool OnInit() override
        {
            return true;
        }

        void OnExit() override
        {
            if ( gs_translationsOwned )
                delete gs_translations;
            gs_translations = nullptr;
            gs_translationsOwned = true;
        }
};

wxIMPLEMENT_DYNAMIC_CLASS(wxTranslationsModule, wxModule);

#endif // wxUSE_INTL
