diff --git a/data/lang/.gitignore b/data/lang/.gitignore --- a/data/lang/.gitignore +++ b/data/lang/.gitignore @@ -1 +1 @@ -*.po +po diff --git a/data/lang/CMakeLists.txt b/data/lang/CMakeLists.txt --- a/data/lang/CMakeLists.txt +++ b/data/lang/CMakeLists.txt @@ -1,50 +1,66 @@ -SET (TARGET merge_ts_po) -PROJECT (${TARGET}) - -include_directories( - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_BINARY_DIR} -) - -set( ${TARGET}_SRC merge_ts_po.cpp ) -add_executable( ${TARGET} ${${TARGET}_SRC} ) - -target_link_libraries( ${TARGET} Qt5::Core ) - -# MARBLE_WRAP_PO(qmfiles pofile1 pofile2 ... ) -MACRO(MARBLE_WRAP_PO qmfiles) - FILE(GLOB_RECURSE all_sources RELATIVE "${CMAKE_SOURCE_DIR}/src/" "${CMAKE_SOURCE_DIR}/src/*.cpp" "${CMAKE_SOURCE_DIR}/src/*.h" "${CMAKE_SOURCE_DIR}/src/*.ui") - SET(tstemplate ${CMAKE_CURRENT_BINARY_DIR}/template.ts) - ADD_CUSTOM_COMMAND(OUTPUT ${tstemplate} - COMMAND ${QT_LUPDATE_EXECUTABLE} - ARGS ${all_sources} -ts ${tstemplate} - WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/src" - ) - FOREACH (pofile ${ARGN}) - GET_FILENAME_COMPONENT(pofile ${pofile} ABSOLUTE) - GET_FILENAME_COMPONENT(basename ${pofile} NAME_WE) - SET(tsfile "${CMAKE_CURRENT_BINARY_DIR}/${basename}.ts") - SET(qmfile "${CMAKE_CURRENT_BINARY_DIR}/${basename}.qm") - ADD_CUSTOM_COMMAND(OUTPUT ${tsfile} - COMMAND ${CMAKE_CURRENT_BINARY_DIR}/merge_ts_po - ARGS ${tstemplate} ${pofile} > ${tsfile} - OUTPUT ${qmfile} - COMMAND ${QT_LRELEASE_EXECUTABLE} - ARGS -silent ${tsfile} -qm ${qmfile} - DEPENDS ${TARGET} ${tstemplate} ${pofile} +# custom translation catalog , for creation of bundled binary packages +# TODO: disable for builds in normal linux distri builds, to not confuse people + + +# Find Qt translation tools +find_package(Qt5LinguistTools CONFIG) + +# enable target in any case +add_custom_target(bundle_translations) + +if(NOT Qt5LinguistTools_FOUND) + return() +endif() + + +if(TARGET Qt5::lconvert) + set(lconvert_executable Qt5::lconvert) +else() + # Qt < 5.3.1 does not define Qt5::lconvert + get_target_property(lrelease_location Qt5::lrelease LOCATION) + get_filename_component(lrelease_path ${lrelease_location} PATH) + find_program(lconvert_executable + NAMES lconvert-qt5 lconvert + PATHS ${lrelease_path} + NO_DEFAULT_PATH ) - SET(${qmfiles} ${${qmfiles}} "${qmfile}") - ENDFOREACH(pofile) -ENDMACRO(MARBLE_WRAP_PO) +endif() + +function(marble_process_po_files_as_qm lang po_file) + # Create commands to turn po files into qm files + get_filename_component(po_file ${po_file} ABSOLUTE) + get_filename_component(filename_base ${po_file} NAME_WE) + # Include ${lang} in build dir because we might be called multiple times + # with the same ${filename_base} + set(build_dir ${CMAKE_CURRENT_BINARY_DIR}/locale/${lang}) + set(ts_file ${build_dir}/${filename_base}.ts) + set(qm_file ${build_dir}/${filename_base}.qm) -FILE(GLOB LANG_SRC "*.po") + file(MAKE_DIRECTORY ${build_dir}) -# Create our translation files. -MARBLE_WRAP_PO(LANG_FILES ${LANG_SRC}) + # lconvert from .po to .ts, then lrelease from .ts to .qm. + add_custom_command(OUTPUT ${qm_file} + COMMAND ${lconvert_executable} + ARGS -i ${po_file} -o ${ts_file} -target-language ${lang} + COMMAND Qt5::lrelease + ARGS -removeidentical -nounfinished -silent ${ts_file} -qm ${qm_file} + DEPENDS ${po_file} + ) + install( + FILES ${qm_file} + DESTINATION ${data_dir}/locale/${lang} + OPTIONAL # if not build, ignore it + ) -ADD_CUSTOM_TARGET(translations ALL - DEPENDS ${LANG_FILES} -) + set(target_name translation_${lang}_${filename_base}) + add_custom_target(${target_name} DEPENDS ${qm_file}) + add_dependencies(bundle_translations ${target_name}) +endfunction() -install(FILES ${LANG_FILES} DESTINATION ${MARBLE_DATA_INSTALL_PATH}/lang) +file(GLOB po_files "po/*/*.po") +foreach(po_file ${po_files}) + get_filename_component(po_dir ${po_file} DIRECTORY) + get_filename_component(lang ${po_dir} NAME) + marble_process_po_files_as_qm(${lang} ${po_file}) +endforeach() diff --git a/data/lang/README b/data/lang/README new file mode 100644 --- /dev/null +++ b/data/lang/README @@ -0,0 +1,60 @@ +Handling translation catalogs for app bundles +============================================= + +Usage +----- +A package build is done as usual, just with 2 extra steps. + +# Provide marble sources, e.g. working copy of repo or tarball untarred +[...] + +# EXTRA STEP: Add translation sources to the sources (here for stable): +cd +data/lang/download-pos.sh stable + +# Configure build, run cmake as usual: +cd +cmake [...] + +# Build as usual +make + +# EXTRA STEP: Build translations for bundle package optionally +make bundle_translations + +# Install as usual +# If bundle_translations was called before, this will also install the qm files +make install + +# Package as usual +[...] + + +Challenge +--------- +Translation catalogs are not part of the Marble source repository, instead +are in a different repository. +So on packaging time the catalogs need to be added to the working copy, so +they can be processed as needed and become part of the package product. + +The usual KDE source packaging scripts are fetching all po files belonging to +the repo and are storing them in the toplevel dir in a subfolder po/$lang/*.po. +Additionally either the buildsystem has some if-po/-exists-process-and-install-po-files +or the packaging script will inject such logic into the buildsystem. + +When creating binary packages, this is often done directly from a checkout of the +sources and not from the released source tarball. So the step of fetching all po +files has to be done here as well as making sure the processing and installation is done. + +Not all products are build for all platforms. So e.g. for Android only Marble Maps +will be created (separate packaging done for Behaim app, ignored for now). Right now +the apps only or also released as bundles (Maps, Behaim, MarbleQt) or the lib only need +the one single marble_qt catalog. The other (ki18n) catalogs are only used on platforms +where binary packaging is done by the distributions and also unbundled. + + +So supporting translations with binary packaging needs to work independently: +* separate script to download po files, in separate folder: + download-pos.sh +* separate target name for creating the qm files: + bundle_translations diff --git a/data/lang/download-pos.sh b/data/lang/download-pos.sh new file mode 100755 --- /dev/null +++ b/data/lang/download-pos.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +# A script to download Marble Qt translations from KDE's SVN. +# +# To be used before cmake is called on the sources. +# +# This program is free software licensed under the GNU LGPL. You can +# find a copy of this license in LICENSE.txt in the top directory of +# the source code. +# +# Copyright 2016 Friedrich W. H. Kossebau +# + +# TODO: support updates properly (e.g. removing no longer existing po files) + +has_subdirs=true +case "$1" in + trunk) + echo "Downloading from trunk" + svn_path_prefix="svn://anonsvn.kde.org/home/kde/trunk/l10n-kf5" + ;; + stable) + echo "Downloading from stable" + svn_path_prefix="svn://anonsvn.kde.org/home/kde/branches/stable/l10n-kf5" + ;; + "") + echo "Syntax: $0 trunk|stable|\"KDE Applications tag\" [lang]" + exit 1 + ;; + *) + TAG=$1 + echo "Downloading from KDE Applications tag $1" + svn_path_prefix="svn://anonsvn.kde.org/home/kde/tags/Applications/${TAG}/kde-l10n/5" + has_subdirs=false + ;; +esac + +if [ ! -e data/lang ]; then + echo "$0 needs to be invoked from the toplevel source dir." + exit 1 +fi + + +workdir="$(mktemp -d)" +pofile="${workdir}//marble_qt.po" + +if [ $# -gt 1 ] ; then + languages=$2 +else + subdirs="${workdir}/subdirs" + if [ "$has_subdirs" = true ] ; then + svn -q export "${svn_path_prefix}/subdirs" ${subdirs} + else + # check if tag exists + if svn info ${svn_path_prefix} > /dev/null 2>&1 ; then + # tags don't have a subdirs file, so create one from the dirs present + svn list ${svn_path_prefix} | egrep "/$" | sed "s,/$,," > ${subdirs} + else + echo "There seems to be no tag $TAG." + exit 1 + fi + fi + languages=$(cat ${subdirs}) +fi + +for i in ${languages} +do + svn -q export "${svn_path_prefix}/${i}/messages/kdeedu/marble_qt.po" ${pofile} > /dev/null 2>&1 + # some languages might not have a catalog + if [ -e ${pofile} ]; then + chmod a-w ${pofile} # mark as not-to-be-edited + target_dir="data/lang/po/${i}" + mkdir -p ${target_dir} + mv -f ${pofile} ${target_dir} + echo "Downloaded for language $i" + fi +done + +if [ -e data/lang/po ]; then + # TODO: think about some way to check if there are local modifications to prevent their loss + touch data/lang/po/WARNING_CHANGES_TO_PO_FILES_WILL_BE_LOST +fi diff --git a/data/lang/download-translations.bash b/data/lang/download-translations.bash deleted file mode 100755 --- a/data/lang/download-translations.bash +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash - -# A script to download Marble Qt translations from KDE's SVN. -# -# This program is free software licensed under the GNU LGPL. You can -# find a copy of this license in LICENSE.txt in the top directory of -# the source code. -# -# Copyright 2011 Dennis Nienhüser -# Copyright 2013 Bernhard Beschow -# - -set -e - -workdir="$(mktemp -d)" - -#prefix="svn://anonsvn.kde.org/home/kde/branches/stable/l10n-kde4/" -#TAG="4.7.0" -#prefix="svn://anonsvn.kde.org/home/kde/tags/KDE/${TAG}/l10n-kde4/" -# Translations can also be loaded from SVN trunk, uncomment below. -prefix="svn://anonsvn.kde.org/home/kde/trunk/l10n-kde4" - -echo "Downloading translations, please wait. This may take some time..." - -svn -q export "${prefix}/subdirs" "${workdir}/subdirs" -for i in $(cat "${workdir}/subdirs") -do - if svn -q export --force "${prefix}/${i}/messages/kdeedu/marble_qt.po" "${workdir}/marble_qt.po" 2>/dev/null - then - if svn -q export --force "${prefix}/${i}/messages/kdeedu/marble.po" "${workdir}/marble.po" 2>/dev/null - then - echo >> "${workdir}/marble_qt.po" - cat "${workdir}/marble.po" >> "${workdir}/marble_qt.po" - mv "${workdir}/marble_qt.po" marble-${i}.po - fi - fi -done - -rm "${workdir}/marble.po" -rm "${workdir}/subdirs" -rmdir "${workdir}" - -test -e CMakeLists.txt && touch CMakeLists.txt - -echo "Done." diff --git a/data/lang/marble_i18n.sh b/data/lang/marble_i18n.sh deleted file mode 100755 --- a/data/lang/marble_i18n.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -TAG=$1 -if [ "${TAG}" = "" ]; then - TAG=4.7.3 - echo "Syntax: marble_i18n KDE-tag" - echo "E.g.: marble_i18n ${TAG}" - echo "-------------------------" - echo "Assuming ${TAG} as a KDE-tag version for now." -fi - -svn export svn://anonsvn.kde.org/home/kde/tags/KDE/${TAG}/kde-l10n/subdirs -for i in $(cat subdirs) -do - echo "Processing Language $i" - svn export svn://anonsvn.kde.org/home/kde/tags/KDE/${TAG}/kde-l10n/${i}/messages/kdeedu/marble.po - perl -pi -e 's/^[[:space:]]*#.*$//g' marble.po - lconvert marble.po -o marble_${i}.qm -done diff --git a/data/lang/merge_ts_po.cpp b/data/lang/merge_ts_po.cpp deleted file mode 100644 --- a/data/lang/merge_ts_po.cpp +++ /dev/null @@ -1,103 +0,0 @@ -// -// This file is part of the Marble Virtual Globe. -// -// This program is free software licensed under the GNU LGPL. You can -// find a copy of this license in LICENSE.txt in the top directory of -// the source code. -// -// Copyright 2011 Dennis Nienhüser -// - -#include -#include -#include -#include - -void usage( const QString &app ) -{ - qDebug() << "Usage: " << app << " input.ts input.po"; -} - -int main( int argc, char** argv ) -{ - if ( argc != 3 ) { - usage( argv[0] ); - return 0; - } - - QFileInfo const ts = QFileInfo( QString( argv[1] ) ); - if ( !ts.exists() ) { - usage( argv[0] ); - return 1; - } - - QFileInfo const po = QFileInfo( QString( argv[2] ) ); - if ( !po.exists() ) { - usage( argv[0] ); - return 2; - } - - QMap translations; - - // Open the .po file and build a map of translations - QFile poFile( po.absoluteFilePath() ); - poFile.open( QFile::ReadOnly ); - QTextStream poStream( &poFile ); - poStream.setCodec( "UTF-8" ); - poStream.setAutoDetectUnicode( true ); - QString source; - bool ignore = false; - while( !poStream.atEnd() ) { - QString line = poStream.readLine(); - if ( line.startsWith( QLatin1String( "#, fuzzy" ) ) ) { - ignore = true; - } else if ( line.startsWith( QLatin1String( "msgid " ) ) ) { - source = line.mid( 7, line.size() - 8 ); - } else if ( !source.isEmpty() && line.startsWith( QLatin1String( "msgstr " ) ) ) { - if ( ignore ) { - ignore = false; - } else { - QString translation = line.mid( 8, line.size() - 9 ); - source.replace( QLatin1Char( '&' ), QLatin1String( "&" ) ); - translation.replace( QLatin1Char( '&' ), QLatin1String( "& ") ); - source.replace( QLatin1Char( '<' ), QLatin1String( "<" ) ); - translation.replace( QLatin1Char( '<' ), QLatin1String( "<" ) ); - source.replace( QLatin1Char( '>' ), QLatin1String( ">" ) ); - translation.replace( QLatin1Char( '>' ), QLatin1String( ">" ) ); - if ( !translation.isEmpty() ) { - translations[source] = translation; - } - } - } - } - - QTextStream console( stdout ); - console.setCodec( "UTF-8" ); - - // Open the .ts file and replace source strings with translations - // The modified .to file is dumped to stdout - QFile tsFile( ts.absoluteFilePath() ); - tsFile.open( QFile::ReadOnly ); - QTextStream tsStream( &tsFile ); - tsStream.setCodec( "UTF-8" ); - tsStream.setAutoDetectUnicode( true ); - source.clear(); - while( !tsStream.atEnd() ) { - QString line = tsStream.readLine().trimmed(); - if ( line.startsWith( QLatin1String( "" ) ) ) { - source = line.mid( 8, line.size() - 17 ); - console << line << "\n"; - } else if ( !source.isEmpty() && - line == "" && - translations.contains( source ) ) { - console << "" << translations[source] << "\n"; - } else if ( !source.isEmpty() && - line == "" ) { - console << line << "\n"; - } else { - console << line << "\n"; - } - } - - return 0; -} diff --git a/src/apps/marble-maps/main.cpp b/src/apps/marble-maps/main.cpp --- a/src/apps/marble-maps/main.cpp +++ b/src/apps/marble-maps/main.cpp @@ -15,10 +15,59 @@ #include "declarative/MarbleDeclarativePlugin.h" #include #include "MarbleMaps.h" +#include "MarbleDirs.h" #include "TextToSpeechClient.h" using namespace Marble; +static bool loadTranslation(const QString &localeDirName, QApplication &app) +{ + // TODO: check if any translations for Qt modules have to be loaded, + // as they need to be explicitely loaded as well by the Qt-using app + +#ifdef Q_OS_ANDROID + // load translation file from bundled packaging installation + const QString fullPath = MarbleDirs::systemPath() + QLatin1String("/locale/") + localeDirName + QLatin1String("/marble_qt.qm"); +#else + // load translation file from normal "KDE Applications" packaging installation + const QString subPath = QLatin1String("locale/") + localeDirName + QLatin1String("/LC_MESSAGES/marble_qt.qm"); + const QString fullPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, subPath); + if (fullPath.isEmpty()) { + return false; + } +#endif + + QTranslator* translator = new QTranslator(&app); + if (!translator->load(fullPath)) { + delete translator; + return false; + } + + app.installTranslator(translator); + + return true; +} + +// load KDE translators system based translations +static void loadTranslations(QApplication &app) +{ + // Quote from ecm_create_qm_loader created code: + // The way Qt translation system handles plural forms makes it necessary to + // have a translation file which contains only plural forms for `en`. + // That's why we load the `en` translation unconditionally, then load the + // translation for the current locale to overload it. + const QString en(QStringLiteral("en")); + + loadTranslation(en, app); + + QLocale locale = QLocale::system(); + if (locale.name() != en) { + if (!loadTranslation(locale.name(), app)) { + loadTranslation(locale.bcp47Name(), app); + } + } +} + #ifdef Q_OS_ANDROID // Declare symbol of main method as exported as needed by Qt-on-Android, // where the Dalvik-native QtActivity class needs to find and invoke it @@ -35,6 +84,9 @@ app.setDesktopFileName(QStringLiteral("org.kde.marble.maps")); #endif + // Load Qt translation system catalog for libmarblewidget, the plugins and this app + loadTranslations(app); + #ifdef Q_OS_ANDROID MarbleGlobal::Profiles profiles = MarbleGlobal::SmallScreen | MarbleGlobal::HighResolution; MarbleGlobal::getInstance()->setProfiles( profiles );