When the original messages language matches the language of the locale
being used, these strings can be used directly and so AddCatalog()
should still return true for them but it didn't do it any more after the
changes of 94b1a17aeb (Add AddAvailableCatalog() and use it in
AddStdCatalog(), 2023-09-30).
See #18227.
Closes #24019.
Closes #24037.
1960 lines
55 KiB
C++
1960 lines
55 KiB
C++
/////////////////////////////////////////////////////////////////////////////
|
|
// 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 wxArrayString& arr)
|
|
{
|
|
wxLogTrace(TRACE_I18N, "%s: [%s]", prefix, wxJoin(arr, ','));
|
|
}
|
|
|
|
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
|
|
|
|
// Use locale-based detection as a fallback
|
|
wxString GetPreferredUILanguageFallback(const wxArrayString& WXUNUSED(available))
|
|
{
|
|
const wxString lang = wxUILocale::GetLanguageCanonicalName(wxUILocale::GetSystemLocale());
|
|
wxLogTrace(TRACE_I18N, " - obtained best language from locale: %s", lang);
|
|
return lang;
|
|
}
|
|
|
|
wxString GetPreferredUILanguage(const wxArrayString& available)
|
|
{
|
|
wxVector<wxString> preferred = wxUILocale::GetPreferredUILanguages();
|
|
LogTraceArray(" - system preferred languages", preferred);
|
|
|
|
wxString langNoMatchRegion;
|
|
for ( wxVector<wxString>::const_iterator j = preferred.begin();
|
|
j != preferred.end();
|
|
++j )
|
|
{
|
|
wxLocaleIdent localeId = wxLocaleIdent::FromTag(*j);
|
|
wxString lang = localeId.GetTag(wxLOCALE_TAGTYPE_POSIX);
|
|
|
|
if (available.Index(lang, /*bCase=*/false) != wxNOT_FOUND)
|
|
return lang;
|
|
|
|
size_t pos = lang.find('_');
|
|
if (pos != wxString::npos)
|
|
{
|
|
lang = lang.substr(0, pos);
|
|
if (available.Index(lang, /*bCase=*/false) != wxNOT_FOUND)
|
|
return lang;
|
|
}
|
|
|
|
if (langNoMatchRegion.empty())
|
|
{
|
|
// lang now holds only the language
|
|
// check for an available language with potentially non-matching region
|
|
for ( wxArrayString::const_iterator k = available.begin();
|
|
k != available.end();
|
|
++k )
|
|
{
|
|
if ((*k).Lower().StartsWith(lang.Lower()))
|
|
{
|
|
langNoMatchRegion = *k;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!langNoMatchRegion.empty())
|
|
return langNoMatchRegion;
|
|
|
|
return GetPreferredUILanguageFallback(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").
|
|
if ( AddAvailableCatalog("wxstd-" wxSTRINGIZE(wxMAJOR_VERSION) "." wxSTRINGIZE(wxMINOR_VERSION)) )
|
|
return true;
|
|
|
|
if ( AddCatalog(wxS("wxstd")) )
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool wxTranslations::AddAvailableCatalog(const wxString& domain)
|
|
{
|
|
const wxString domain_lang = GetBestAvailableTranslation(domain);
|
|
if ( domain_lang.empty() )
|
|
{
|
|
wxLogTrace(TRACE_I18N,
|
|
wxS("no suitable translation for domain '%s' found"),
|
|
domain);
|
|
return false;
|
|
}
|
|
|
|
return LoadCatalog(domain, domain_lang);
|
|
}
|
|
|
|
bool wxTranslations::AddCatalog(const wxString& domain,
|
|
wxLanguage msgIdLanguage)
|
|
{
|
|
if ( AddAvailableCatalog(domain) )
|
|
return true;
|
|
|
|
const wxString msgIdLang = wxUILocale::GetLanguageCanonicalName(msgIdLanguage);
|
|
|
|
// Check if the original strings can be used directly.
|
|
bool canUseUntranslated = false;
|
|
if ( m_lang.empty() )
|
|
{
|
|
// If we are using the default language, check if the message ID
|
|
// language is acceptable for this system.
|
|
const wxString domain_lang = GetBestTranslation(domain, msgIdLang);
|
|
|
|
if ( msgIdLang == domain_lang )
|
|
canUseUntranslated = true;
|
|
}
|
|
else // But if we have a fixed language, we should just check it instead.
|
|
{
|
|
// Consider message IDs for another region using the same language
|
|
// acceptable.
|
|
if ( msgIdLang.BeforeFirst('_') == m_lang.BeforeFirst('_') )
|
|
canUseUntranslated = true;
|
|
}
|
|
|
|
if ( canUseUntranslated )
|
|
{
|
|
wxLogTrace(TRACE_I18N,
|
|
wxS("not using translations for domain '%s' with msgid language '%s'"),
|
|
domain, msgIdLang);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
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)
|
|
{
|
|
wxString lang = GetBestAvailableTranslation(domain);
|
|
if ( lang.empty() )
|
|
{
|
|
wxArrayString available;
|
|
available.push_back(msgIdLanguage);
|
|
available.push_back(msgIdLanguage.BeforeFirst('_'));
|
|
lang = GetPreferredUILanguage(available);
|
|
if ( lang.empty() )
|
|
{
|
|
wxLogTrace(TRACE_I18N,
|
|
"no available language for domain '%s'", domain);
|
|
}
|
|
else
|
|
{
|
|
wxLogTrace(TRACE_I18N,
|
|
"using message ID language '%s' for domain '%s'", lang);
|
|
}
|
|
}
|
|
|
|
return lang;
|
|
}
|
|
|
|
wxString wxTranslations::GetBestAvailableTranslation(const wxString& domain)
|
|
{
|
|
const wxArrayString available(GetAvailableTranslations(domain));
|
|
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;
|
|
}
|
|
|
|
wxLogTrace(TRACE_I18N, "choosing best language for domain '%s'", domain);
|
|
LogTraceArray(" - available translations", available);
|
|
const wxString lang = GetPreferredUILanguage(available);
|
|
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 wxChar 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
|