diff --git a/.gitignore b/.gitignore index 1a6b0c4..42a96a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ doc-gen/ +src/config.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a3aa435..3d9e0a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,118 +1,121 @@ cmake_minimum_required(VERSION 3.0) set(KF5_VERSION "5.48.0") # handled by release scripts project(KI18n VERSION ${KF5_VERSION}) # ECM setup include(FeatureSummary) find_package(ECM 5.48.0 NO_MODULE) set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://projects.kde.org/projects/kdesupport/extra-cmake-modules") feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${KI18n_SOURCE_DIR}/cmake ${KI18n_BINARY_DIR}/cmake) include(GenerateExportHeader) include(CMakePackageConfigHelpers) include(ECMSetupVersion) include(ECMGenerateHeaders) include(ECMAddQch) ecm_setup_version( PROJECT VARIABLE_PREFIX KI18N VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/ki18n_version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF5I18nConfigVersion.cmake" SOVERSION 5) # Dependencies set(REQUIRED_QT_VERSION 5.8.0) find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Core) option(BUILD_WITH_QTSCRIPT "Build with support for scripted translations (recommended)" ON) if (BUILD_WITH_QTSCRIPT) find_package(Qt5Script ${REQUIRED_QT_VERSION} CONFIG REQUIRED) endif() include(KDEInstallDirs) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(KDECMakeSettings) find_package(LibIntl) set_package_properties(LibIntl PROPERTIES TYPE REQUIRED URL "http://gnuwin32.sourceforge.net/packages/libintl.htm" PURPOSE "Needed for building KI18n unless glibc is the system libc implementation" ) # KF5I18NMacros.cmake only needs to know the python executable path. # Due to CMake caching the variables, and KF5I18NMacros being included by KF5I18nConfig, # we have to hardcode the PYTHON_EXECUTABLE path or anything depending on KF5I18n # would be unable to find another Python version. find_package(PythonInterp REQUIRED) configure_file(cmake/KF5I18NMacros.cmake.in ${KI18n_BINARY_DIR}/cmake/KF5I18NMacros.cmake @ONLY) # Needed to build the ki18n translations and run the autotests file(COPY ${KI18n_SOURCE_DIR}/cmake/build-pofiles.cmake DESTINATION ${KI18n_BINARY_DIR}/cmake) file(COPY ${KI18n_SOURCE_DIR}/cmake/build-tsfiles.cmake DESTINATION ${KI18n_BINARY_DIR}/cmake) file(COPY ${KI18n_SOURCE_DIR}/cmake/ts-pmap-compile.py DESTINATION ${KI18n_BINARY_DIR}/cmake) file(COPY ${KI18n_SOURCE_DIR}/cmake/kf5i18nuic.cmake DESTINATION ${KI18n_BINARY_DIR}/cmake) # usually is called using find_package(KF5I18n), # KF5I18NMacros.cmake needs to know the scripts directory set(KF5I18n_DIR ${CMAKE_CURRENT_LIST_DIR}/cmake) include(${KI18n_BINARY_DIR}/cmake/KF5I18NMacros.cmake) remove_definitions(-DQT_NO_CAST_FROM_BYTEARRAY) if(MSVC) remove_definitions(-DQT_STRICT_ITERATORS) endif() option(BUILD_QCH "Build API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)" OFF) add_feature_info(QCH ${BUILD_QCH} "API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)") if (IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/po") ki18n_install(po) endif() add_subdirectory(src) if (BUILD_TESTING) add_subdirectory(autotests) endif() +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/config.h.in" + "${CMAKE_CURRENT_SOURCE_DIR}/src/config.h") + # create a Config.cmake and a ConfigVersion.cmake file and install them set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF5I18n") if (BUILD_QCH) ecm_install_qch_export( TARGETS KF5I18n_QCH FILE KF5I18nQchTargets.cmake DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) set(PACKAGE_INCLUDE_QCHTARGETS "include(\"\${CMAKE_CURRENT_LIST_DIR}/KF5I18nQchTargets.cmake\")") endif() configure_package_config_file("${CMAKE_CURRENT_LIST_DIR}/KF5I18nConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/KF5I18nConfig.cmake" INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} PATH_VARS KF5_INCLUDE_INSTALL_DIR LIB_INSTALL_DIR CMAKE_INSTALL_PREFIX) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/KF5I18nConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/KF5I18nConfigVersion.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel) install(EXPORT KF5I18nTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR}" FILE KF5I18nTargets.cmake NAMESPACE KF5::) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/ki18n_version.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5} COMPONENT Devel) install( FILES ${KI18n_BINARY_DIR}/cmake/KF5I18NMacros.cmake cmake/kf5i18nuic.cmake cmake/build-pofiles.cmake cmake/build-tsfiles.cmake cmake/ts-pmap-compile.py DESTINATION ${CMAKECONFIG_INSTALL_DIR} COMPONENT Devel ) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/cmake/FindLibIntl.cmake b/cmake/FindLibIntl.cmake index 5b7bfc3..856c4d5 100644 --- a/cmake/FindLibIntl.cmake +++ b/cmake/FindLibIntl.cmake @@ -1,64 +1,67 @@ #.rst: # FindLibIntl # --------- # # Find libintl # # Find the libintl headers and libraries. On platforms that use glibc this is not required # and LibIntl_LIBRARIES will be empty # # :: # # LibIntl_INCLUDE_DIRS - where to find libintl.h # LibIntl_LIBRARIES - The libintl library if the current platform does not use glibc. # LibIntl_FOUND - True if libintl was found. #============================================================================= # Copyright 2014 Alex Richardson # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. Neither the name of the University nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. #============================================================================= find_path(LibIntl_INCLUDE_DIRS NAMES libintl.h) find_library(LibIntl_LIBRARIES NAMES intl libintl) include(CheckCXXSymbolExists) check_cxx_symbol_exists(dngettext libintl.h LibIntl_SYMBOL_FOUND) include(FindPackageHandleStandardArgs) if(LibIntl_SYMBOL_FOUND) message(STATUS "libintl is part of libc, no extra library is required.") set(LibIntl_LIBRARIES "") if(LibIntl_INCLUDE_DIRS) find_package_handle_standard_args(LibIntl REQUIRED_VARS LibIntl_INCLUDE_DIRS) else() # in the default search path but not found by find_path, e.g. host glibc when cross-compiling set(LibIntl_INCLUDE_DIRS "") set(LibIntl_FOUND TRUE) endif() else() message(STATUS "libintl is a separate library.") find_package_handle_standard_args(LibIntl REQUIRED_VARS LibIntl_INCLUDE_DIRS LibIntl_LIBRARIES) endif() + +set(CMAKE_REQUIRED_LIBRARIES ${LibIntl_LIBRARIES}) +check_cxx_source_compiles("extern \"C\" int _nl_msg_cat_cntr; int main(void) { ++_nl_msg_cat_cntr; return 0; }" HAVE_NL_MSG_CAT_CNTR) diff --git a/src/config.h.in b/src/config.h.in new file mode 100644 index 0000000..7f6b26f --- /dev/null +++ b/src/config.h.in @@ -0,0 +1,25 @@ +/* This file is part of the KDE libraries + Copyright (c) 2016 A. Wilcox + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KI18N_CONFIG_H +#define KI18N_CONFIG_H + +#cmakedefine HAVE_NL_MSG_CAT_CNTR + +#endif diff --git a/src/kcatalog.cpp b/src/kcatalog.cpp index c18d40f..adb6ba0 100644 --- a/src/kcatalog.cpp +++ b/src/kcatalog.cpp @@ -1,285 +1,286 @@ /* This file is part of the KDE libraries Copyright (c) 2001 Hans Petter Bieker Copyright (c) 2012, 2013 Chusslove Illich This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include "gettext.h" +#include "config.h" #include #include #include #include #include #include #include #include #include #include // not defined on win32 :( #ifdef _WIN32 #ifndef LC_MESSAGES #define LC_MESSAGES 42 #endif #endif -#if defined(__USE_GNU_GETTEXT) +#if defined(HAVE_NL_MSG_CAT_CNTR) extern "C" int Q_DECL_IMPORT _nl_msg_cat_cntr; #endif static char *langenv = nullptr; static const int langenvMaxlen = 42; // = "LANGUAGE=" + 32 chars for language code + terminating zero class KCatalogStaticData { public: KCatalogStaticData() {} QHash customCatalogDirs; QMutex mutex; }; Q_GLOBAL_STATIC(KCatalogStaticData, catalogStaticData) class KCatalogPrivate { public: KCatalogPrivate(); QByteArray domain; QByteArray language; QByteArray localeDir; QByteArray systemLanguage; bool bindDone; static QByteArray currentLanguage; void setupGettextEnv(); void resetSystemLanguage(); }; KCatalogPrivate::KCatalogPrivate() : bindDone(false) {} QByteArray KCatalogPrivate::currentLanguage; KCatalog::KCatalog(const QByteArray &domain, const QString &language_) : d(new KCatalogPrivate) { d->domain = domain; d->language = QFile::encodeName(language_); d->localeDir = QFile::encodeName(catalogLocaleDir(domain, language_)); if (!d->localeDir.isEmpty()) { // Always get translations in UTF-8, regardless of user's environment. bind_textdomain_codeset(d->domain, "UTF-8"); // Invalidate current language, to trigger binding at next translate call. KCatalogPrivate::currentLanguage.clear(); if (!langenv) { // Call putenv only here, to initialize LANGUAGE variable. // Later only change langenv to what is currently needed. langenv = new char[langenvMaxlen]; QByteArray baselang = qgetenv("LANGUAGE"); qsnprintf(langenv, langenvMaxlen, "LANGUAGE=%s", baselang.constData()); putenv(langenv); } } } KCatalog::~KCatalog() { delete d; } QString KCatalog::catalogLocaleDir(const QByteArray &domain, const QString &language) { QString relpath = QStringLiteral("%1/LC_MESSAGES/%2.mo") .arg(language, QFile::decodeName(domain)); { QMutexLocker lock(&catalogStaticData->mutex); const QString customLocaleDir = catalogStaticData->customCatalogDirs.value(domain); if (!customLocaleDir.isEmpty() && QFileInfo::exists(customLocaleDir + QLatin1Char('/') + relpath)) { return customLocaleDir; } } QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("locale/") + relpath); QString localeDir; if (file.isEmpty()) { localeDir = QString(); } else { // Path of the locale/ directory must be returned. localeDir = QFileInfo(file.left(file.size() - relpath.size())).absolutePath(); } return localeDir; } QSet KCatalog::availableCatalogLanguages(const QByteArray &domain_) { QString domain = QFile::decodeName(domain_); QStringList localeDirPaths = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("locale"), QStandardPaths::LocateDirectory); { QMutexLocker lock(&catalogStaticData->mutex); auto it = catalogStaticData->customCatalogDirs.constFind(domain_); if (it != catalogStaticData->customCatalogDirs.constEnd()) { localeDirPaths.prepend(*it); } } QSet availableLanguages; foreach (const QString &localDirPath, localeDirPaths) { QDir localeDir(localDirPath); QStringList languages = localeDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot); foreach (const QString &language, languages) { QString relPath = QStringLiteral("%1/LC_MESSAGES/%2.mo") .arg(language, domain); if (localeDir.exists(relPath)) { availableLanguages.insert(language); } } } return availableLanguages; } void KCatalogPrivate::setupGettextEnv() { // Point Gettext to current language, recording system value for recovery. systemLanguage = qgetenv("LANGUAGE"); if (systemLanguage != language) { // putenv has been called in the constructor, // it is enough to change the string set there. qsnprintf(langenv, langenvMaxlen, "LANGUAGE=%s", language.constData()); } // Rebind text domain if language actually changed from the last time, // as locale directories may differ for different languages of same catalog. if (language != currentLanguage || !bindDone) { currentLanguage = language; bindDone = true; //qDebug() << "bindtextdomain" << domain << localeDir; bindtextdomain(domain, localeDir); +#if defined(HAVE_NL_MSG_CAT_CNTR) // Magic to make sure GNU Gettext doesn't use stale cached translation // from previous language. -#if defined(__USE_GNU_GETTEXT) ++_nl_msg_cat_cntr; #endif } } void KCatalogPrivate::resetSystemLanguage() { if (language != systemLanguage) { qsnprintf(langenv, langenvMaxlen, "LANGUAGE=%s", systemLanguage.constData()); } } QString KCatalog::translate(const QByteArray &msgid) const { if (!d->localeDir.isEmpty()) { QMutexLocker locker(&catalogStaticData()->mutex); d->setupGettextEnv(); const char *msgstr = dgettext(d->domain.constData(), msgid.constData()); d->resetSystemLanguage(); return msgstr != msgid ? QString::fromUtf8(msgstr) : QString(); } else { return QString(); } } QString KCatalog::translate(const QByteArray &msgctxt, const QByteArray &msgid) const { if (!d->localeDir.isEmpty()) { QMutexLocker locker(&catalogStaticData()->mutex); d->setupGettextEnv(); const char *msgstr = dpgettext_expr(d->domain.constData(), msgctxt.constData(), msgid.constData()); d->resetSystemLanguage(); return msgstr != msgid ? QString::fromUtf8(msgstr) : QString(); } else { return QString(); } } QString KCatalog::translate(const QByteArray &msgid, const QByteArray &msgid_plural, qulonglong n) const { if (!d->localeDir.isEmpty()) { QMutexLocker locker(&catalogStaticData()->mutex); d->setupGettextEnv(); const char *msgstr = dngettext(d->domain.constData(), msgid.constData(), msgid_plural.constData(), n); d->resetSystemLanguage(); // If original and translation are same, dngettext will return // the original pointer, which is generally fine, except in // the corner cases where e.g. msgstr[1] is same as msgid. // Therefore check for pointer difference only with msgid or // only with msgid_plural, and not with both. return (n == 1 && msgstr != msgid) || (n != 1 && msgstr != msgid_plural) ? QString::fromUtf8(msgstr) : QString(); } else { return QString(); } } QString KCatalog::translate(const QByteArray &msgctxt, const QByteArray &msgid, const QByteArray &msgid_plural, qulonglong n) const { if (!d->localeDir.isEmpty()) { QMutexLocker locker(&catalogStaticData()->mutex); d->setupGettextEnv(); const char *msgstr = dnpgettext_expr(d->domain.constData(), msgctxt.constData(), msgid.constData(), msgid_plural.constData(), n); d->resetSystemLanguage(); return (n == 1 && msgstr != msgid) || (n != 1 && msgstr != msgid_plural) ? QString::fromUtf8(msgstr) : QString(); } else { return QString(); } } void KCatalog::addDomainLocaleDir(const QByteArray &domain, const QString &path) { QMutexLocker(&catalogStaticData()->mutex); catalogStaticData()->customCatalogDirs.insert(domain, path); }