1176 lines
35 KiB
C++
1176 lines
35 KiB
C++
/****************************************************************************
|
|
**
|
|
** Copyright (C) 2016 The Qt Company Ltd.
|
|
** Contact: https://www.qt.io/licensing/
|
|
**
|
|
** This file is part of the QtCore module of the Qt Toolkit.
|
|
**
|
|
** $QT_BEGIN_LICENSE:LGPL$
|
|
** Commercial License Usage
|
|
** Licensees holding valid commercial Qt licenses may use this file in
|
|
** accordance with the commercial license agreement provided with the
|
|
** Software or, alternatively, in accordance with the terms contained in
|
|
** a written agreement between you and The Qt Company. For licensing terms
|
|
** and conditions see https://www.qt.io/terms-conditions. For further
|
|
** information use the contact form at https://www.qt.io/contact-us.
|
|
**
|
|
** GNU Lesser General Public License Usage
|
|
** Alternatively, this file may be used under the terms of the GNU Lesser
|
|
** General Public License version 3 as published by the Free Software
|
|
** Foundation and appearing in the file LICENSE.LGPL3 included in the
|
|
** packaging of this file. Please review the following information to
|
|
** ensure the GNU Lesser General Public License version 3 requirements
|
|
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
|
|
**
|
|
** GNU General Public License Usage
|
|
** Alternatively, this file may be used under the terms of the GNU
|
|
** General Public License version 2.0 or (at your option) the GNU General
|
|
** Public license version 3 or any later version approved by the KDE Free
|
|
** Qt Foundation. The licenses are as published by the Free Software
|
|
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
|
|
** included in the packaging of this file. Please review the following
|
|
** information to ensure the GNU General Public License requirements will
|
|
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
|
|
** https://www.gnu.org/licenses/gpl-3.0.html.
|
|
**
|
|
** $QT_END_LICENSE$
|
|
**
|
|
****************************************************************************/
|
|
|
|
#include "qplatformdefs.h"
|
|
|
|
#include "qtranslator.h"
|
|
|
|
#ifndef QT_NO_TRANSLATION
|
|
|
|
#include "qfileinfo.h"
|
|
#include "qstring.h"
|
|
#include "qstringlist.h"
|
|
#include "qcoreapplication.h"
|
|
#include "qcoreapplication_p.h"
|
|
#include "qdatastream.h"
|
|
#include "qendian.h"
|
|
#include "qfile.h"
|
|
#include "qmap.h"
|
|
#include "qalgorithms.h"
|
|
#include "qtranslator_p.h"
|
|
#include "qlocale.h"
|
|
#include "qendian.h"
|
|
#include "qresource.h"
|
|
|
|
#if defined(Q_OS_UNIX) && !defined(Q_OS_NACL) && !defined(Q_OS_INTEGRITY)
|
|
# define QT_USE_MMAP
|
|
# include "private/qcore_unix_p.h"
|
|
// for mmap
|
|
# include <sys/mman.h>
|
|
#endif
|
|
|
|
#include <stdlib.h>
|
|
#include <new>
|
|
|
|
#include "qobject_p.h"
|
|
|
|
QT_BEGIN_NAMESPACE
|
|
|
|
enum Tag { Tag_End = 1, Tag_SourceText16, Tag_Translation, Tag_Context16, Tag_Obsolete1,
|
|
Tag_SourceText, Tag_Context, Tag_Comment, Tag_Obsolete2 };
|
|
/*
|
|
$ mcookie
|
|
3cb86418caef9c95cd211cbf60a1bddd
|
|
$
|
|
*/
|
|
|
|
// magic number for the file
|
|
static const int MagicLength = 16;
|
|
static const uchar magic[MagicLength] = {
|
|
0x3c, 0xb8, 0x64, 0x18, 0xca, 0xef, 0x9c, 0x95,
|
|
0xcd, 0x21, 0x1c, 0xbf, 0x60, 0xa1, 0xbd, 0xdd
|
|
};
|
|
|
|
static inline QString dotQmLiteral() { return QStringLiteral(".qm"); }
|
|
|
|
static bool match(const uchar *found, uint foundLen, const char *target, uint targetLen)
|
|
{
|
|
// catch the case if \a found has a zero-terminating symbol and \a len includes it.
|
|
// (normalize it to be without the zero-terminating symbol)
|
|
if (foundLen > 0 && found[foundLen-1] == '\0')
|
|
--foundLen;
|
|
return ((targetLen == foundLen) && memcmp(found, target, foundLen) == 0);
|
|
}
|
|
|
|
static void elfHash_continue(const char *name, uint &h)
|
|
{
|
|
const uchar *k;
|
|
uint g;
|
|
|
|
k = (const uchar *) name;
|
|
while (*k) {
|
|
h = (h << 4) + *k++;
|
|
if ((g = (h & 0xf0000000)) != 0)
|
|
h ^= g >> 24;
|
|
h &= ~g;
|
|
}
|
|
}
|
|
|
|
static void elfHash_finish(uint &h)
|
|
{
|
|
if (!h)
|
|
h = 1;
|
|
}
|
|
|
|
static uint elfHash(const char *name)
|
|
{
|
|
uint hash = 0;
|
|
elfHash_continue(name, hash);
|
|
elfHash_finish(hash);
|
|
return hash;
|
|
}
|
|
|
|
/*
|
|
\internal
|
|
|
|
Determines whether \a rules are valid "numerus rules". Test input with this
|
|
function before calling numerusHelper, below.
|
|
*/
|
|
static bool isValidNumerusRules(const uchar *rules, uint rulesSize)
|
|
{
|
|
// Disabled computation of maximum numerus return value
|
|
// quint32 numerus = 0;
|
|
|
|
if (rulesSize == 0)
|
|
return true;
|
|
|
|
quint32 offset = 0;
|
|
do {
|
|
uchar opcode = rules[offset];
|
|
uchar op = opcode & Q_OP_MASK;
|
|
|
|
if (opcode & 0x80)
|
|
return false; // Bad op
|
|
|
|
if (++offset == rulesSize)
|
|
return false; // Missing operand
|
|
|
|
// right operand
|
|
++offset;
|
|
|
|
switch (op)
|
|
{
|
|
case Q_EQ:
|
|
case Q_LT:
|
|
case Q_LEQ:
|
|
break;
|
|
|
|
case Q_BETWEEN:
|
|
if (offset != rulesSize) {
|
|
// third operand
|
|
++offset;
|
|
break;
|
|
}
|
|
return false; // Missing operand
|
|
|
|
default:
|
|
return false; // Bad op (0)
|
|
}
|
|
|
|
// ++numerus;
|
|
if (offset == rulesSize)
|
|
return true;
|
|
|
|
} while (((rules[offset] == Q_AND)
|
|
|| (rules[offset] == Q_OR)
|
|
|| (rules[offset] == Q_NEWRULE))
|
|
&& ++offset != rulesSize);
|
|
|
|
// Bad op
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
\internal
|
|
|
|
This function does no validation of input and assumes it is well-behaved,
|
|
these assumptions can be checked with isValidNumerusRules, above.
|
|
|
|
Determines which translation to use based on the value of \a n. The return
|
|
value is an index identifying the translation to be used.
|
|
|
|
\a rules is a character array of size \a rulesSize containing bytecode that
|
|
operates on the value of \a n and ultimately determines the result.
|
|
|
|
This function has O(1) space and O(rulesSize) time complexity.
|
|
*/
|
|
static uint numerusHelper(int n, const uchar *rules, uint rulesSize)
|
|
{
|
|
uint result = 0;
|
|
uint i = 0;
|
|
|
|
if (rulesSize == 0)
|
|
return 0;
|
|
|
|
for (;;) {
|
|
bool orExprTruthValue = false;
|
|
|
|
for (;;) {
|
|
bool andExprTruthValue = true;
|
|
|
|
for (;;) {
|
|
bool truthValue = true;
|
|
int opcode = rules[i++];
|
|
|
|
int leftOperand = n;
|
|
if (opcode & Q_MOD_10) {
|
|
leftOperand %= 10;
|
|
} else if (opcode & Q_MOD_100) {
|
|
leftOperand %= 100;
|
|
} else if (opcode & Q_LEAD_1000) {
|
|
while (leftOperand >= 1000)
|
|
leftOperand /= 1000;
|
|
}
|
|
|
|
int op = opcode & Q_OP_MASK;
|
|
int rightOperand = rules[i++];
|
|
|
|
switch (op) {
|
|
case Q_EQ:
|
|
truthValue = (leftOperand == rightOperand);
|
|
break;
|
|
case Q_LT:
|
|
truthValue = (leftOperand < rightOperand);
|
|
break;
|
|
case Q_LEQ:
|
|
truthValue = (leftOperand <= rightOperand);
|
|
break;
|
|
case Q_BETWEEN:
|
|
int bottom = rightOperand;
|
|
int top = rules[i++];
|
|
truthValue = (leftOperand >= bottom && leftOperand <= top);
|
|
}
|
|
|
|
if (opcode & Q_NOT)
|
|
truthValue = !truthValue;
|
|
|
|
andExprTruthValue = andExprTruthValue && truthValue;
|
|
|
|
if (i == rulesSize || rules[i] != Q_AND)
|
|
break;
|
|
++i;
|
|
}
|
|
|
|
orExprTruthValue = orExprTruthValue || andExprTruthValue;
|
|
|
|
if (i == rulesSize || rules[i] != Q_OR)
|
|
break;
|
|
++i;
|
|
}
|
|
|
|
if (orExprTruthValue)
|
|
return result;
|
|
|
|
++result;
|
|
|
|
if (i == rulesSize)
|
|
return result;
|
|
|
|
i++; // Q_NEWRULE
|
|
}
|
|
|
|
Q_ASSERT(false);
|
|
return 0;
|
|
}
|
|
|
|
class QTranslatorPrivate : public QObjectPrivate
|
|
{
|
|
Q_DECLARE_PUBLIC(QTranslator)
|
|
public:
|
|
enum { Contexts = 0x2f, Hashes = 0x42, Messages = 0x69, NumerusRules = 0x88, Dependencies = 0x96, Language = 0xa7 };
|
|
|
|
QTranslatorPrivate() :
|
|
#if defined(QT_USE_MMAP)
|
|
used_mmap(0),
|
|
#endif
|
|
unmapPointer(nullptr), unmapLength(0), resource(nullptr),
|
|
messageArray(nullptr), offsetArray(nullptr), contextArray(nullptr), numerusRulesArray(nullptr),
|
|
messageLength(0), offsetLength(0), contextLength(0), numerusRulesLength(0) {}
|
|
|
|
#if defined(QT_USE_MMAP)
|
|
bool used_mmap : 1;
|
|
#endif
|
|
char *unmapPointer; // used memory (mmap, new or resource file)
|
|
qsizetype unmapLength;
|
|
|
|
// The resource object in case we loaded the translations from a resource
|
|
QResource *resource;
|
|
|
|
// used if the translator has dependencies
|
|
QList<QTranslator*> subTranslators;
|
|
|
|
// Pointers and offsets into unmapPointer[unmapLength] array, or user
|
|
// provided data array
|
|
const uchar *messageArray;
|
|
const uchar *offsetArray;
|
|
const uchar *contextArray;
|
|
const uchar *numerusRulesArray;
|
|
uint messageLength;
|
|
uint offsetLength;
|
|
uint contextLength;
|
|
uint numerusRulesLength;
|
|
|
|
QString language;
|
|
QString filePath;
|
|
|
|
bool do_load(const QString &filename, const QString &directory);
|
|
bool do_load(const uchar *data, qsizetype len, const QString &directory);
|
|
QString do_translate(const char *context, const char *sourceText, const char *comment,
|
|
int n) const;
|
|
void clear();
|
|
};
|
|
|
|
/*!
|
|
\class QTranslator
|
|
\inmodule QtCore
|
|
|
|
\brief The QTranslator class provides internationalization support for text
|
|
output.
|
|
|
|
\ingroup i18n
|
|
|
|
An object of this class contains a set of translations from a
|
|
source language to a target language. QTranslator provides
|
|
functions to look up translations in a translation file.
|
|
Translation files are created using \l{Qt Linguist}.
|
|
|
|
The most common use of QTranslator is to: load a translation
|
|
file, install it using QCoreApplication::installTranslator(), and use
|
|
it via QObject::tr(). Here's an example \c main() function using the
|
|
QTranslator:
|
|
|
|
\snippet hellotrmain.cpp 0
|
|
|
|
Note that the translator must be created \e before the
|
|
application's widgets.
|
|
|
|
Most applications will never need to do anything else with this
|
|
class. The other functions provided by this class are useful for
|
|
applications that work on translator files.
|
|
|
|
\section1 Looking up Translations
|
|
|
|
It is possible to look up a translation using translate() (as tr()
|
|
and QCoreApplication::translate() do). The translate() function takes
|
|
up to three parameters:
|
|
|
|
\list
|
|
\li The \e context - usually the class name for the tr() caller.
|
|
\li The \e {source text} - usually the argument to tr().
|
|
\li The \e disambiguation - an optional string that helps disambiguate
|
|
different uses of the same text in the same context.
|
|
\endlist
|
|
|
|
For example, the "Cancel" in a dialog might have "Anuluj" when the
|
|
program runs in Polish (in this case the source text would be
|
|
"Cancel"). The context would (normally) be the dialog's class
|
|
name; there would normally be no comment, and the translated text
|
|
would be "Anuluj".
|
|
|
|
But it's not always so simple. The Spanish version of a printer
|
|
dialog with settings for two-sided printing and binding would
|
|
probably require both "Activado" and "Activada" as translations
|
|
for "Enabled". In this case the source text would be "Enabled" in
|
|
both cases, and the context would be the dialog's class name, but
|
|
the two items would have disambiguations such as "two-sided printing"
|
|
for one and "binding" for the other. The disambiguation enables the
|
|
translator to choose the appropriate gender for the Spanish version,
|
|
and enables Qt to distinguish between translations.
|
|
|
|
\section1 Using Multiple Translations
|
|
|
|
Multiple translation files can be installed in an application.
|
|
Translations are searched for in the reverse order in which they were
|
|
installed, so the most recently installed translation file is searched
|
|
for translations first and the earliest translation file is searched
|
|
last. The search stops as soon as a translation containing a matching
|
|
string is found.
|
|
|
|
This mechanism makes it possible for a specific translation to be
|
|
"selected" or given priority over the others; simply uninstall the
|
|
translator from the application by passing it to the
|
|
QCoreApplication::removeTranslator() function and reinstall it with
|
|
QCoreApplication::installTranslator(). It will then be the first
|
|
translation to be searched for matching strings.
|
|
|
|
\sa QCoreApplication::installTranslator(), QCoreApplication::removeTranslator(),
|
|
QObject::tr(), QCoreApplication::translate(), {I18N Example},
|
|
{Hello tr() Example}, {Arrow Pad Example}, {Troll Print Example}
|
|
*/
|
|
|
|
/*!
|
|
Constructs an empty message file object with parent \a parent that
|
|
is not connected to any file.
|
|
*/
|
|
|
|
QTranslator::QTranslator(QObject * parent)
|
|
: QObject(*new QTranslatorPrivate, parent)
|
|
{
|
|
}
|
|
|
|
/*!
|
|
Destroys the object and frees any allocated resources.
|
|
*/
|
|
|
|
QTranslator::~QTranslator()
|
|
{
|
|
if (QCoreApplication::instance())
|
|
QCoreApplication::removeTranslator(this);
|
|
Q_D(QTranslator);
|
|
d->clear();
|
|
}
|
|
|
|
/*!
|
|
|
|
Loads \a filename + \a suffix (".qm" if the \a suffix is not
|
|
specified), which may be an absolute file name or relative to \a
|
|
directory. Returns \c true if the translation is successfully loaded;
|
|
otherwise returns \c false.
|
|
|
|
If \a directory is not specified, the current directory is used
|
|
(i.e., as \l{QDir::}{currentPath()}).
|
|
|
|
The previous contents of this translator object are discarded.
|
|
|
|
If the file name does not exist, other file names are tried
|
|
in the following order:
|
|
|
|
\list 1
|
|
\li File name without \a suffix appended.
|
|
\li File name with text after a character in \a search_delimiters
|
|
stripped ("_." is the default for \a search_delimiters if it is
|
|
an empty string) and \a suffix.
|
|
\li File name stripped without \a suffix appended.
|
|
\li File name stripped further, etc.
|
|
\endlist
|
|
|
|
For example, an application running in the fr_CA locale
|
|
(French-speaking Canada) might call load("foo.fr_ca",
|
|
"/opt/foolib"). load() would then try to open the first existing
|
|
readable file from this list:
|
|
|
|
\list 1
|
|
\li \c /opt/foolib/foo.fr_ca.qm
|
|
\li \c /opt/foolib/foo.fr_ca
|
|
\li \c /opt/foolib/foo.fr.qm
|
|
\li \c /opt/foolib/foo.fr
|
|
\li \c /opt/foolib/foo.qm
|
|
\li \c /opt/foolib/foo
|
|
\endlist
|
|
|
|
Usually, it is better to use the QTranslator::load(const QLocale &,
|
|
const QString &, const QString &, const QString &, const QString &)
|
|
function instead, because it uses \l{QLocale::uiLanguages()} and not simply
|
|
the locale name, which refers to the formatting of dates and numbers and not
|
|
necessarily the UI language.
|
|
*/
|
|
|
|
bool QTranslator::load(const QString & filename, const QString & directory,
|
|
const QString & search_delimiters,
|
|
const QString & suffix)
|
|
{
|
|
Q_D(QTranslator);
|
|
d->clear();
|
|
|
|
QString prefix;
|
|
if (QFileInfo(filename).isRelative()) {
|
|
prefix = directory;
|
|
if (prefix.length() && !prefix.endsWith(QLatin1Char('/')))
|
|
prefix += QLatin1Char('/');
|
|
}
|
|
|
|
const QString suffixOrDotQM = suffix.isNull() ? dotQmLiteral() : suffix;
|
|
QStringRef fname(&filename);
|
|
QString realname;
|
|
const QString delims = search_delimiters.isNull() ? QStringLiteral("_.") : search_delimiters;
|
|
|
|
for (;;) {
|
|
QFileInfo fi;
|
|
|
|
realname = prefix + fname + suffixOrDotQM;
|
|
fi.setFile(realname);
|
|
if (fi.isReadable() && fi.isFile())
|
|
break;
|
|
|
|
realname = prefix + fname;
|
|
fi.setFile(realname);
|
|
if (fi.isReadable() && fi.isFile())
|
|
break;
|
|
|
|
int rightmost = 0;
|
|
for (int i = 0; i < (int)delims.length(); i++) {
|
|
int k = fname.lastIndexOf(delims[i]);
|
|
if (k > rightmost)
|
|
rightmost = k;
|
|
}
|
|
|
|
// no truncations? fail
|
|
if (rightmost == 0)
|
|
return false;
|
|
|
|
fname.truncate(rightmost);
|
|
}
|
|
|
|
// realname is now the fully qualified name of a readable file.
|
|
return d->do_load(realname, directory);
|
|
}
|
|
|
|
bool QTranslatorPrivate::do_load(const QString &realname, const QString &directory)
|
|
{
|
|
QTranslatorPrivate *d = this;
|
|
bool ok = false;
|
|
|
|
if (realname.startsWith(QLatin1Char(':'))) {
|
|
// If the translation is in a non-compressed resource file, the data is already in
|
|
// memory, so no need to use QFile to copy it again.
|
|
Q_ASSERT(!d->resource);
|
|
d->resource = new QResource(realname);
|
|
if (resource->isValid() && resource->compressionAlgorithm() == QResource::NoCompression
|
|
&& resource->size() >= MagicLength
|
|
&& !memcmp(resource->data(), magic, MagicLength)) {
|
|
d->unmapLength = resource->size();
|
|
d->unmapPointer = reinterpret_cast<char *>(const_cast<uchar *>(resource->data()));
|
|
#if defined(QT_USE_MMAP)
|
|
d->used_mmap = false;
|
|
#endif
|
|
ok = true;
|
|
} else {
|
|
delete resource;
|
|
resource = nullptr;
|
|
}
|
|
}
|
|
|
|
if (!ok) {
|
|
QFile file(realname);
|
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Unbuffered))
|
|
return false;
|
|
|
|
qint64 fileSize = file.size();
|
|
if (fileSize < MagicLength || fileSize > std::numeric_limits<qsizetype>::max())
|
|
return false;
|
|
|
|
{
|
|
char magicBuffer[MagicLength];
|
|
if (MagicLength != file.read(magicBuffer, MagicLength)
|
|
|| memcmp(magicBuffer, magic, MagicLength))
|
|
return false;
|
|
}
|
|
|
|
d->unmapLength = qsizetype(fileSize);
|
|
|
|
#ifdef QT_USE_MMAP
|
|
|
|
#ifndef MAP_FILE
|
|
#define MAP_FILE 0
|
|
#endif
|
|
#ifndef MAP_FAILED
|
|
#define MAP_FAILED reinterpret_cast<void *>(-1)
|
|
#endif
|
|
|
|
int fd = file.handle();
|
|
if (fd >= 0) {
|
|
int protection = PROT_READ; // read-only memory
|
|
int flags = MAP_FILE | MAP_PRIVATE; // swap-backed map from file
|
|
void *ptr = QT_MMAP(nullptr, d->unmapLength,// any address, whole file
|
|
protection, flags,
|
|
fd, 0); // from offset 0 of fd
|
|
if (ptr != MAP_FAILED) {
|
|
file.close();
|
|
d->used_mmap = true;
|
|
d->unmapPointer = static_cast<char *>(ptr);
|
|
ok = true;
|
|
}
|
|
}
|
|
#endif // QT_USE_MMAP
|
|
|
|
if (!ok) {
|
|
d->unmapPointer = new (std::nothrow) char[d->unmapLength];
|
|
if (d->unmapPointer) {
|
|
file.seek(0);
|
|
qint64 readResult = file.read(d->unmapPointer, d->unmapLength);
|
|
if (readResult == qint64(unmapLength))
|
|
ok = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ok && d->do_load(reinterpret_cast<const uchar *>(d->unmapPointer), d->unmapLength, directory)) {
|
|
d->filePath = realname;
|
|
return true;
|
|
}
|
|
|
|
#if defined(QT_USE_MMAP)
|
|
if (used_mmap) {
|
|
used_mmap = false;
|
|
munmap(unmapPointer, unmapLength);
|
|
} else
|
|
#endif
|
|
if (!d->resource)
|
|
delete [] unmapPointer;
|
|
|
|
delete d->resource;
|
|
d->resource = nullptr;
|
|
d->unmapPointer = nullptr;
|
|
d->unmapLength = 0;
|
|
|
|
return false;
|
|
}
|
|
|
|
Q_NEVER_INLINE
|
|
static bool is_readable_file(const QString &name)
|
|
{
|
|
const QFileInfo fi(name);
|
|
return fi.isReadable() && fi.isFile();
|
|
}
|
|
|
|
static QString find_translation(const QLocale & locale,
|
|
const QString & filename,
|
|
const QString & prefix,
|
|
const QString & directory,
|
|
const QString & suffix)
|
|
{
|
|
QString path;
|
|
if (QFileInfo(filename).isRelative()) {
|
|
path = directory;
|
|
if (!path.isEmpty() && !path.endsWith(QLatin1Char('/')))
|
|
path += QLatin1Char('/');
|
|
}
|
|
const QString suffixOrDotQM = suffix.isNull() ? dotQmLiteral() : suffix;
|
|
|
|
QString realname;
|
|
realname += path + filename + prefix; // using += in the hope for some reserve capacity
|
|
const int realNameBaseSize = realname.size();
|
|
QStringList fuzzyLocales;
|
|
|
|
// see http://www.unicode.org/reports/tr35/#LanguageMatching for inspiration
|
|
|
|
QStringList languages = locale.uiLanguages();
|
|
#if defined(Q_OS_UNIX)
|
|
for (int i = languages.size()-1; i >= 0; --i) {
|
|
QString lang = languages.at(i);
|
|
QString lowerLang = lang.toLower();
|
|
if (lang != lowerLang)
|
|
languages.insert(i+1, lowerLang);
|
|
}
|
|
#endif
|
|
|
|
// try explicit locales names first
|
|
for (QString localeName : qAsConst(languages)) {
|
|
localeName.replace(QLatin1Char('-'), QLatin1Char('_'));
|
|
|
|
realname += localeName + suffixOrDotQM;
|
|
if (is_readable_file(realname))
|
|
return realname;
|
|
|
|
realname.truncate(realNameBaseSize + localeName.size());
|
|
if (is_readable_file(realname))
|
|
return realname;
|
|
|
|
realname.truncate(realNameBaseSize);
|
|
fuzzyLocales.append(localeName);
|
|
}
|
|
|
|
// start guessing
|
|
for (const QString &fuzzyLocale : qAsConst(fuzzyLocales)) {
|
|
QStringRef localeName(&fuzzyLocale);
|
|
for (;;) {
|
|
int rightmost = localeName.lastIndexOf(QLatin1Char('_'));
|
|
// no truncations? fail
|
|
if (rightmost <= 0)
|
|
break;
|
|
localeName.truncate(rightmost);
|
|
|
|
realname += localeName + suffixOrDotQM;
|
|
if (is_readable_file(realname))
|
|
return realname;
|
|
|
|
realname.truncate(realNameBaseSize + localeName.size());
|
|
if (is_readable_file(realname))
|
|
return realname;
|
|
|
|
realname.truncate(realNameBaseSize);
|
|
}
|
|
}
|
|
|
|
const int realNameBaseSizeFallbacks = path.size() + filename.size();
|
|
|
|
// realname == path + filename + prefix;
|
|
if (!suffix.isNull()) {
|
|
realname.replace(realNameBaseSizeFallbacks, prefix.size(), suffix);
|
|
// realname == path + filename;
|
|
if (is_readable_file(realname))
|
|
return realname;
|
|
realname.replace(realNameBaseSizeFallbacks, suffix.size(), prefix);
|
|
}
|
|
|
|
// realname == path + filename + prefix;
|
|
if (is_readable_file(realname))
|
|
return realname;
|
|
|
|
realname.truncate(realNameBaseSizeFallbacks);
|
|
// realname == path + filename;
|
|
if (is_readable_file(realname))
|
|
return realname;
|
|
|
|
realname.truncate(0);
|
|
return realname;
|
|
}
|
|
|
|
/*!
|
|
\since 4.8
|
|
|
|
Loads \a filename + \a prefix + \l{QLocale::uiLanguages()}{ui language
|
|
name} + \a suffix (".qm" if the \a suffix is not specified), which may be
|
|
an absolute file name or relative to \a directory. Returns \c true if the
|
|
translation is successfully loaded; otherwise returns \c false.
|
|
|
|
The previous contents of this translator object are discarded.
|
|
|
|
If the file name does not exist, other file names are tried
|
|
in the following order:
|
|
|
|
\list 1
|
|
\li File name without \a suffix appended.
|
|
\li File name with ui language part after a "_" character stripped and \a suffix.
|
|
\li File name with ui language part stripped without \a suffix appended.
|
|
\li File name with ui language part stripped further, etc.
|
|
\endlist
|
|
|
|
For example, an application running in the \a locale with the following
|
|
\l{QLocale::uiLanguages()}{ui languages} - "es", "fr-CA", "de" might call
|
|
load(QLocale(), "foo", ".", "/opt/foolib", ".qm"). load() would
|
|
replace '-' (dash) with '_' (underscore) in the ui language and then try to
|
|
open the first existing readable file from this list:
|
|
|
|
\list 1
|
|
\li \c /opt/foolib/foo.es.qm
|
|
\li \c /opt/foolib/foo.es
|
|
\li \c /opt/foolib/foo.fr_CA.qm
|
|
\li \c /opt/foolib/foo.fr_CA
|
|
\li \c /opt/foolib/foo.de.qm
|
|
\li \c /opt/foolib/foo.de
|
|
\li \c /opt/foolib/foo.fr.qm
|
|
\li \c /opt/foolib/foo.fr
|
|
\li \c /opt/foolib/foo.qm
|
|
\li \c /opt/foolib/foo.
|
|
\li \c /opt/foolib/foo
|
|
\endlist
|
|
|
|
On operating systems where file system is case sensitive, QTranslator also
|
|
tries to load a lower-cased version of the locale name.
|
|
*/
|
|
bool QTranslator::load(const QLocale & locale,
|
|
const QString & filename,
|
|
const QString & prefix,
|
|
const QString & directory,
|
|
const QString & suffix)
|
|
{
|
|
Q_D(QTranslator);
|
|
d->clear();
|
|
QString fname = find_translation(locale, filename, prefix, directory, suffix);
|
|
return !fname.isEmpty() && d->do_load(fname, directory);
|
|
}
|
|
|
|
/*!
|
|
\overload load()
|
|
|
|
Loads the QM file data \a data of length \a len into the
|
|
translator.
|
|
|
|
The data is not copied. The caller must be able to guarantee that \a data
|
|
will not be deleted or modified.
|
|
|
|
\a directory is only used to specify the base directory when loading the dependencies
|
|
of a QM file. If the file does not have dependencies, this argument is ignored.
|
|
*/
|
|
bool QTranslator::load(const uchar *data, int len, const QString &directory)
|
|
{
|
|
Q_D(QTranslator);
|
|
d->clear();
|
|
|
|
if (!data || len < MagicLength || memcmp(data, magic, MagicLength))
|
|
return false;
|
|
|
|
return d->do_load(data, len, directory);
|
|
}
|
|
|
|
static quint8 read8(const uchar *data)
|
|
{
|
|
return qFromBigEndian<quint8>(data);
|
|
}
|
|
|
|
static quint16 read16(const uchar *data)
|
|
{
|
|
return qFromBigEndian<quint16>(data);
|
|
}
|
|
|
|
static quint32 read32(const uchar *data)
|
|
{
|
|
return qFromBigEndian<quint32>(data);
|
|
}
|
|
|
|
bool QTranslatorPrivate::do_load(const uchar *data, qsizetype len, const QString &directory)
|
|
{
|
|
bool ok = true;
|
|
const uchar *end = data + len;
|
|
|
|
data += MagicLength;
|
|
|
|
QStringList dependencies;
|
|
while (data < end - 5) {
|
|
quint8 tag = read8(data++);
|
|
quint32 blockLen = read32(data);
|
|
data += 4;
|
|
if (!tag || !blockLen)
|
|
break;
|
|
if (quint32(end - data) < blockLen) {
|
|
ok = false;
|
|
break;
|
|
}
|
|
|
|
if (tag == QTranslatorPrivate::Language) {
|
|
language = QString::fromUtf8((const char*)data, blockLen);
|
|
} else if (tag == QTranslatorPrivate::Contexts) {
|
|
contextArray = data;
|
|
contextLength = blockLen;
|
|
} else if (tag == QTranslatorPrivate::Hashes) {
|
|
offsetArray = data;
|
|
offsetLength = blockLen;
|
|
} else if (tag == QTranslatorPrivate::Messages) {
|
|
messageArray = data;
|
|
messageLength = blockLen;
|
|
} else if (tag == QTranslatorPrivate::NumerusRules) {
|
|
numerusRulesArray = data;
|
|
numerusRulesLength = blockLen;
|
|
} else if (tag == QTranslatorPrivate::Dependencies) {
|
|
QDataStream stream(QByteArray::fromRawData((const char*)data, blockLen));
|
|
QString dep;
|
|
while (!stream.atEnd()) {
|
|
stream >> dep;
|
|
dependencies.append(dep);
|
|
}
|
|
}
|
|
|
|
data += blockLen;
|
|
}
|
|
|
|
if (ok && !isValidNumerusRules(numerusRulesArray, numerusRulesLength))
|
|
ok = false;
|
|
if (ok) {
|
|
const int dependenciesCount = dependencies.count();
|
|
subTranslators.reserve(dependenciesCount);
|
|
for (int i = 0 ; i < dependenciesCount; ++i) {
|
|
QTranslator *translator = new QTranslator;
|
|
subTranslators.append(translator);
|
|
ok = translator->load(dependencies.at(i), directory);
|
|
if (!ok)
|
|
break;
|
|
}
|
|
|
|
// In case some dependencies fail to load, unload all the other ones too.
|
|
if (!ok) {
|
|
qDeleteAll(subTranslators);
|
|
subTranslators.clear();
|
|
}
|
|
}
|
|
|
|
if (!ok) {
|
|
messageArray = nullptr;
|
|
contextArray = nullptr;
|
|
offsetArray = nullptr;
|
|
numerusRulesArray = nullptr;
|
|
messageLength = 0;
|
|
contextLength = 0;
|
|
offsetLength = 0;
|
|
numerusRulesLength = 0;
|
|
}
|
|
|
|
return ok;
|
|
}
|
|
|
|
static QString getMessage(const uchar *m, const uchar *end, const char *context,
|
|
const char *sourceText, const char *comment, uint numerus)
|
|
{
|
|
const uchar *tn = nullptr;
|
|
uint tn_length = 0;
|
|
const uint sourceTextLen = uint(strlen(sourceText));
|
|
const uint contextLen = uint(strlen(context));
|
|
const uint commentLen = uint(strlen(comment));
|
|
|
|
for (;;) {
|
|
uchar tag = 0;
|
|
if (m < end)
|
|
tag = read8(m++);
|
|
switch((Tag)tag) {
|
|
case Tag_End:
|
|
goto end;
|
|
case Tag_Translation: {
|
|
int len = read32(m);
|
|
if (len % 1)
|
|
return QString();
|
|
m += 4;
|
|
if (!numerus--) {
|
|
tn_length = len;
|
|
tn = m;
|
|
}
|
|
m += len;
|
|
break;
|
|
}
|
|
case Tag_Obsolete1:
|
|
m += 4;
|
|
break;
|
|
case Tag_SourceText: {
|
|
quint32 len = read32(m);
|
|
m += 4;
|
|
if (!match(m, len, sourceText, sourceTextLen))
|
|
return QString();
|
|
m += len;
|
|
}
|
|
break;
|
|
case Tag_Context: {
|
|
quint32 len = read32(m);
|
|
m += 4;
|
|
if (!match(m, len, context, contextLen))
|
|
return QString();
|
|
m += len;
|
|
}
|
|
break;
|
|
case Tag_Comment: {
|
|
quint32 len = read32(m);
|
|
m += 4;
|
|
if (*m && !match(m, len, comment, commentLen))
|
|
return QString();
|
|
m += len;
|
|
}
|
|
break;
|
|
default:
|
|
return QString();
|
|
}
|
|
}
|
|
end:
|
|
if (!tn)
|
|
return QString();
|
|
QString str(tn_length / 2, Qt::Uninitialized);
|
|
qFromBigEndian<ushort>(tn, str.length(), str.data());
|
|
return str;
|
|
}
|
|
|
|
QString QTranslatorPrivate::do_translate(const char *context, const char *sourceText,
|
|
const char *comment, int n) const
|
|
{
|
|
if (context == nullptr)
|
|
context = "";
|
|
if (sourceText == nullptr)
|
|
sourceText = "";
|
|
if (comment == nullptr)
|
|
comment = "";
|
|
|
|
uint numerus = 0;
|
|
size_t numItems = 0;
|
|
|
|
if (!offsetLength)
|
|
goto searchDependencies;
|
|
|
|
/*
|
|
Check if the context belongs to this QTranslator. If many
|
|
translators are installed, this step is necessary.
|
|
*/
|
|
if (contextLength) {
|
|
quint16 hTableSize = read16(contextArray);
|
|
uint g = elfHash(context) % hTableSize;
|
|
const uchar *c = contextArray + 2 + (g << 1);
|
|
quint16 off = read16(c);
|
|
c += 2;
|
|
if (off == 0)
|
|
return QString();
|
|
c = contextArray + (2 + (hTableSize << 1) + (off << 1));
|
|
|
|
const uint contextLen = uint(strlen(context));
|
|
for (;;) {
|
|
quint8 len = read8(c++);
|
|
if (len == 0)
|
|
return QString();
|
|
if (match(c, len, context, contextLen))
|
|
break;
|
|
c += len;
|
|
}
|
|
}
|
|
|
|
numItems = offsetLength / (2 * sizeof(quint32));
|
|
if (!numItems)
|
|
goto searchDependencies;
|
|
|
|
if (n >= 0)
|
|
numerus = numerusHelper(n, numerusRulesArray, numerusRulesLength);
|
|
|
|
for (;;) {
|
|
quint32 h = 0;
|
|
elfHash_continue(sourceText, h);
|
|
elfHash_continue(comment, h);
|
|
elfHash_finish(h);
|
|
|
|
const uchar *start = offsetArray;
|
|
const uchar *end = start + ((numItems-1) << 3);
|
|
while (start <= end) {
|
|
const uchar *middle = start + (((end - start) >> 4) << 3);
|
|
uint hash = read32(middle);
|
|
if (h == hash) {
|
|
start = middle;
|
|
break;
|
|
} else if (hash < h) {
|
|
start = middle + 8;
|
|
} else {
|
|
end = middle - 8;
|
|
}
|
|
}
|
|
|
|
if (start <= end) {
|
|
// go back on equal key
|
|
while (start != offsetArray && read32(start) == read32(start-8))
|
|
start -= 8;
|
|
|
|
while (start < offsetArray + offsetLength) {
|
|
quint32 rh = read32(start);
|
|
start += 4;
|
|
if (rh != h)
|
|
break;
|
|
quint32 ro = read32(start);
|
|
start += 4;
|
|
QString tn = getMessage(messageArray + ro, messageArray + messageLength, context,
|
|
sourceText, comment, numerus);
|
|
if (!tn.isNull())
|
|
return tn;
|
|
}
|
|
}
|
|
if (!comment[0])
|
|
break;
|
|
comment = "";
|
|
}
|
|
|
|
searchDependencies:
|
|
for (QTranslator *translator : subTranslators) {
|
|
QString tn = translator->translate(context, sourceText, comment, n);
|
|
if (!tn.isNull())
|
|
return tn;
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
/*
|
|
Empties this translator of all contents.
|
|
|
|
This function works with stripped translator files.
|
|
*/
|
|
|
|
void QTranslatorPrivate::clear()
|
|
{
|
|
Q_Q(QTranslator);
|
|
if (unmapPointer && unmapLength) {
|
|
#if defined(QT_USE_MMAP)
|
|
if (used_mmap) {
|
|
used_mmap = false;
|
|
munmap(unmapPointer, unmapLength);
|
|
} else
|
|
#endif
|
|
if (!resource)
|
|
delete [] unmapPointer;
|
|
}
|
|
|
|
delete resource;
|
|
resource = nullptr;
|
|
unmapPointer = nullptr;
|
|
unmapLength = 0;
|
|
messageArray = nullptr;
|
|
contextArray = nullptr;
|
|
offsetArray = nullptr;
|
|
numerusRulesArray = nullptr;
|
|
messageLength = 0;
|
|
contextLength = 0;
|
|
offsetLength = 0;
|
|
numerusRulesLength = 0;
|
|
|
|
qDeleteAll(subTranslators);
|
|
subTranslators.clear();
|
|
|
|
language.clear();
|
|
filePath.clear();
|
|
|
|
if (QCoreApplicationPrivate::isTranslatorInstalled(q))
|
|
QCoreApplication::postEvent(QCoreApplication::instance(),
|
|
new QEvent(QEvent::LanguageChange));
|
|
}
|
|
|
|
/*!
|
|
Returns the translation for the key (\a context, \a sourceText,
|
|
\a disambiguation). If none is found, also tries (\a context, \a
|
|
sourceText, ""). If that still fails, returns a null string.
|
|
|
|
\note Incomplete translations may result in unexpected behavior:
|
|
If no translation for (\a context, \a sourceText, "")
|
|
is provided, the method might in this case actually return a
|
|
translation for a different \a disambiguation.
|
|
|
|
If \a n is not -1, it is used to choose an appropriate form for
|
|
the translation (e.g. "%n file found" vs. "%n files found").
|
|
|
|
If you need to programatically insert translations into a
|
|
QTranslator, this function can be reimplemented.
|
|
|
|
\sa load()
|
|
*/
|
|
QString QTranslator::translate(const char *context, const char *sourceText, const char *disambiguation,
|
|
int n) const
|
|
{
|
|
Q_D(const QTranslator);
|
|
return d->do_translate(context, sourceText, disambiguation, n);
|
|
}
|
|
|
|
/*!
|
|
Returns \c true if this translator is empty, otherwise returns \c false.
|
|
This function works with stripped and unstripped translation files.
|
|
*/
|
|
bool QTranslator::isEmpty() const
|
|
{
|
|
Q_D(const QTranslator);
|
|
return !d->messageArray && !d->offsetArray && !d->contextArray
|
|
&& d->subTranslators.isEmpty();
|
|
}
|
|
|
|
/*!
|
|
\since 5.15
|
|
|
|
Returns the target language as stored in the translation file.
|
|
*/
|
|
QString QTranslator::language() const
|
|
{
|
|
Q_D(const QTranslator);
|
|
return d->language;
|
|
}
|
|
|
|
/*!
|
|
\since 5.15
|
|
|
|
Returns the path of the loaded translation file.
|
|
|
|
The file path is empty if no translation was loaded yet,
|
|
the loading failed, or if the translation was not loaded
|
|
from a file.
|
|
*/
|
|
QString QTranslator::filePath() const
|
|
{
|
|
Q_D(const QTranslator);
|
|
return d->filePath;
|
|
}
|
|
|
|
QT_END_NAMESPACE
|
|
|
|
#include "moc_qtranslator.cpp"
|
|
|
|
#endif // QT_NO_TRANSLATION
|