diff options
Diffstat (limited to 'kbabel/filters/gettext')
-rw-r--r-- | kbabel/filters/gettext/Makefile.am | 20 | ||||
-rw-r--r-- | kbabel/filters/gettext/gettextexport.cpp | 352 | ||||
-rw-r--r-- | kbabel/filters/gettext/gettextexport.h | 88 | ||||
-rw-r--r-- | kbabel/filters/gettext/gettextimport.cpp | 821 | ||||
-rw-r--r-- | kbabel/filters/gettext/gettextimport.h | 70 | ||||
-rw-r--r-- | kbabel/filters/gettext/kbabel_gettext_export.desktop | 53 | ||||
-rw-r--r-- | kbabel/filters/gettext/kbabel_gettext_import.desktop | 53 |
7 files changed, 1457 insertions, 0 deletions
diff --git a/kbabel/filters/gettext/Makefile.am b/kbabel/filters/gettext/Makefile.am new file mode 100644 index 00000000..58186306 --- /dev/null +++ b/kbabel/filters/gettext/Makefile.am @@ -0,0 +1,20 @@ +####### General stuff + +INCLUDES= -I../../common -I$(srcdir)/../../common $(all_includes) + +kde_module_LTLIBRARIES = kbabel_gettextimport.la kbabel_gettextexport.la + +kbabel_gettextimport_la_SOURCES = gettextimport.cpp +kbabel_gettextimport_la_LIBADD = ../../common/libkbabelcommon.la +kbabel_gettextimport_la_LDFLAGS = -module $(KDE_PLUGIN) $(all_libraries) + +kbabel_gettextexport_la_SOURCES = gettextexport.cpp +kbabel_gettextexport_la_LIBADD = ../../common/libkbabelcommon.la +kbabel_gettextexport_la_LDFLAGS = -module $(KDE_PLUGIN) $(all_libraries) + +METASOURCES = AUTO + +service_DATA = kbabel_gettext_import.desktop kbabel_gettext_export.desktop +servicedir = $(kde_servicesdir) + +gettextexport.lo: ../../common/kbprojectsettings.h diff --git a/kbabel/filters/gettext/gettextexport.cpp b/kbabel/filters/gettext/gettextexport.cpp new file mode 100644 index 00000000..e951847b --- /dev/null +++ b/kbabel/filters/gettext/gettextexport.cpp @@ -0,0 +1,352 @@ +/* **************************************************************************** + This file is part of KBabel + + Copyright (C) 1999-2000 by Matthias Kiefer <matthias.kiefer@gmx.de> + 2001-2002 by Stanislav Visnovsky <visnovsky@kde.org> + Copyright (C) 2005,2006 by Nicolas GOUTTE <goutte@kde.org> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. + +**************************************************************************** */ + +#include "gettextexport.h" + +#include <resources.h> +#include "catalog.h" +#include "catalogitem.h" +#include "catalogsettings.h" +#include "kbprojectsettings.h" + +#include <qfile.h> +#include <qtextcodec.h> + +#include <kapplication.h> +#include <kdebug.h> +#include <kgenericfactory.h> + +K_EXPORT_COMPONENT_FACTORY( kbabel_gettextexport, KGenericFactory<GettextExportPlugin> ( "kbabelgettextexportfilter" ) ) + +using namespace KBabel; + +GettextExportPlugin::GettextExportPlugin(QObject* parent, const char* name, const QStringList &) : + CatalogExportPlugin(parent,name), m_wrapWidth( -1 ) +{ +} + +ConversionStatus GettextExportPlugin::save(const QString& localFile , const QString& mimetype, const Catalog* catalog) +{ + // check, whether we know how to handle the extra data + if( catalog->importPluginID() != "GNU gettext") + return UNSUPPORTED_TYPE; + + // we support on the application/x-gettext MIME type + if( mimetype != "application/x-gettext") + return UNSUPPORTED_TYPE; + + QFile file(localFile); + + if(file.open(IO_WriteOnly)) + { + int progressRatio = QMAX(100/ QMAX(catalog->numberOfEntries(),1), 1); + emit signalResetProgressBar(i18n("saving file"),100); + + QTextStream stream(&file); + + SaveSettings _saveSettings = catalog->saveSettings(); + + if(_saveSettings.useOldEncoding && catalog->fileCodec()) + { + stream.setCodec(catalog->fileCodec()); + } + else + { + switch(_saveSettings.encoding) + { + case ProjectSettingsBase::UTF8: + stream.setCodec(QTextCodec::codecForName("utf-8")); + break; + case ProjectSettingsBase::UTF16: + stream.setCodec(QTextCodec::codecForName("utf-16")); + break; + default: + stream.setCodec(QTextCodec::codecForLocale()); + break; + } + } + + // only save header if it is not empty + const QString headerComment( catalog->header().comment() ); + // ### TODO: why is this useful to have a header with an empty msgstr? + if( !headerComment.isEmpty() || !catalog->header().msgstr().isEmpty() ) + { + // write header + writeComment( stream, headerComment ); + + const QString headerMsgid = catalog->header().msgid().first(); + + // Gettext PO files should have an empty msgid as header + if ( !headerMsgid.isEmpty() ) + { + // ### TODO: perhaps it is grave enough for a user message + kdWarning() << "Non-empty msgid for the header, assuming empty msgid!" << endl << headerMsgid << "---" << endl; + } + + // ### FIXME: if it is the header, then the msgid should be empty! (Even if KBabel has made something out of a non-header first entry!) + stream << "msgid \"\"\n"; + + writeKeyword( stream, "msgstr", catalog->header().msgstr().first() ); + + stream << "\n"; + } + + QStringList list; + for( uint counter = 0; counter < catalog->numberOfEntries() ; counter++ ) + { + if(counter%10==0) { + emit signalProgress(counter/progressRatio); + } + + // write entry + writeComment( stream, catalog->comment(counter) ); + + const QString msgctxt = catalog->msgctxt(counter); + if (! msgctxt.isEmpty() ) + { + writeKeyword( stream, "msgctxt", msgctxt ); + } + + writeKeyword( stream, "msgid", catalog->msgid( counter ).first() ); + if( catalog->pluralForm( counter ) == Gettext ) + { + writeKeyword( stream, "msgid_plural", catalog->msgid( counter ).last() ); + } + + if( catalog->pluralForm(counter) != Gettext) + { + writeKeyword( stream, "msgstr", catalog->msgstr( counter ).first() ); + } + else + { + kdDebug(KBABEL) << "Saving gettext plural form" << endl; + const int forms = catalog->msgstr( counter ).count(); + for ( int i = 0; i < forms; ++i ) + { + QString keyword ( "msgstr[" ); + keyword += QString::number( i ); + keyword += ']'; + + writeKeyword( stream, keyword, *( catalog->msgstr( counter ).at( i ) ) ); + } + } + + stream << "\n"; + + kapp->processEvents(10); + if( isStopped() ) + { + return STOPPED; + } + } + + if( _saveSettings.saveObsolete ) + { + QValueList<QString>::ConstIterator oit; + + QStringList _obsolete = catalog->catalogExtraData(); + + for( oit = _obsolete.begin(); oit != _obsolete.end(); ++oit ) + { + stream << (*oit) << "\n\n"; + + kapp->processEvents(10); + if( isStopped() ) + { + return STOPPED; + } + } + } + + emit signalProgress(100); + file.close(); + + emit signalClearProgressBar(); + } + else + { + //emit signalError(i18n("Wasn't able to open file %1").arg(filename.ascii())); + return OS_ERROR; + } + + return OK; +} + +void GettextExportPlugin::writeComment( QTextStream& stream, const QString& comment ) const +{ + if( !comment.isEmpty() ) + { + // We must check that each comment line really starts with a #, to avoid syntax errors + int pos = 0; + for(;;) + { + const int newpos = comment.find( '\n', pos, false ); + if ( newpos == pos ) + { + ++pos; + stream << "\n"; + continue; + } + const QString span ( ( newpos == -1 ) ? comment.mid( pos ) : comment.mid( pos, newpos-pos ) ); + + const int len = span.length(); + QString spaces; // Stored leading spaces + for ( int i = 0 ; i < len ; ++i ) + { + const QChar& ch = span[ i ]; + if ( ch == '#' ) + { + stream << spaces << span.mid( i ); + break; + } + else if ( ch == ' ' || ch == '\t' ) + { + // We have a leading white space character, so store it temporary + spaces += ch; + } + else + { + // Not leading white space and not a # character. so consider that the # character was missing at first position. + stream << "# " << spaces << span.mid( i ); + break; + } + } + stream << "\n"; + + if ( newpos == -1 ) + break; + else + pos = newpos + 1; + } + } +} + +void GettextExportPlugin::writeKeyword( QTextStream& stream, const QString& keyword, const QString& text ) const +{ + if ( text.isEmpty() ) + { + // Whatever the wrapping mode, an empty line is an empty line + stream << keyword << " \"\"\n"; + return; + } + else if ( m_wrapWidth == -1 ) + { + // Traditional KBabel wrapping + QStringList list = QStringList::split( '\n', text ); + + if ( text.startsWith( "\n" ) ) + list.prepend( QString() ); + + if(list.isEmpty()) + list.append( QString() ); + + if( list.count() > 1 ) + list.prepend( QString() ); + + stream << keyword << " "; + + QStringList::const_iterator it; + for( it = list.constBegin(); it != list.constEnd(); ++it ) + { + stream << "\"" << (*it) << "\"\n"; + } + return; + } + else if ( ( !m_wrapWidth ) + || ( m_wrapWidth < 0 ) // Unknown special wrapping, so assume "no wrap" instead + ) + { + // No wrapping (like Gettext's --no.wrap or -w0 ) + + // we need to remove the \n characters, as they are extra characters + QString realText( text ); + realText.remove( '\n' ); + stream << keyword << " \"" << realText << "\"\n"; + return; + } + + // ### TODO: test! + // Normal wrapping like Gettext's -w parameter with a value bigger than 0 + // From here on, we assume that we have an non-empty text and a positive non-null m_wrapWidth + + // we need to remove the \n characters, as they are extra characters + QString realText( text ); + realText.remove( '\n' ); + + bool needFirstEmptyLine = false; + if ( realText.find( "\\n" ) != -1 ) + { + // We have more than one (logical) line, so write the extra empty line + needFirstEmptyLine = true; + } + else + { + // We must see if the text would fit in one line, including the keyword, a space and two quote characters. + const int rest = text.length() + keyword.length() + 3 - m_wrapWidth; + if ( rest > 0 ) + { + needFirstEmptyLine = true; + } + } + int availableWidth = m_wrapWidth; + if ( needFirstEmptyLine ) + { + stream << keyword << " \"\"\n"; + } + else + { + stream << keyword << " "; + availableWidth -= keyword.length(); + availableWidth--; // The space after the keyword + } + + const int spanLength = realText.length(); + for ( int pos = 0; pos < spanLength; ) + { + availableWidth -= 2; // Count the quote characters + if ( availableWidth < 2 ) + { + // Be sure that at least two useful characters are written, even if the wrap width is too small + availableWidth = 2; + } + const int newlinePos = realText.find ( "\\n", pos ); + if ( ( newlinePos >= 0 ) && ( newlinePos - pos + 2 < availableWidth ) ) + { + // The newline is near than the maximum available numbers of characters + availableWidth = newlinePos - pos + 2; + } + stream << '\"' << realText.mid( pos, availableWidth ) << "\"\n"; + pos += availableWidth; + } +} diff --git a/kbabel/filters/gettext/gettextexport.h b/kbabel/filters/gettext/gettextexport.h new file mode 100644 index 00000000..81cbe9c4 --- /dev/null +++ b/kbabel/filters/gettext/gettextexport.h @@ -0,0 +1,88 @@ +/* **************************************************************************** + This file is part of KBabel + + Copyright (C) 2002 by Stanislav Visnovsky <visnovsky@kde.org> + Copyright (C) 2006 by Nicolas GOUTTE <goutte@kde.org> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. + +**************************************************************************** */ +#ifndef GETTEXTEXPORTPLUGIN_H +#define GETTEXTEXPORTPLUGIN_H + +#include <catalogfileplugin.h> + +#include <qstringlist.h> + +namespace KBabel { +class Catalog; +} +class KURL; +class QFile; +class QTextCodec; + +/** + * @brief The class for exporting GNU gettext PO files. + * + * As an extra information, it stores the list of all obsolete entries. + */ + +class GettextExportPlugin: public KBabel::CatalogExportPlugin +{ +public: + GettextExportPlugin(QObject* parent, const char* name, const QStringList &); + virtual KBabel::ConversionStatus save(const QString& file, const QString& mimetype, const KBabel::Catalog* catalog); + +private: + /** + * Write a PO comment to @p stream and take care that each comment lines start with a # character + */ + void writeComment( QTextStream& stream, const QString& comment ) const; + + /** + * Write a PO keyword (msgctxt, msgid, msgstr, msgstr_plural, msgstr[0]) and the corresponding text. + * This includes wrapping the text. + */ + void writeKeyword( QTextStream& stream, const QString& keyword, const QString& text ) const; + +public: + /** + * @brief Width of the wrap + * + * This is the width of the wrap in characters (not bytes), including everything + * (e.g. keyword, quote characters, spaces). + * + * - A value of 0 means no wrap + * - A value of -1 means the traditional KBabel wrapping + * - Other negative values are reserved for future extensions (by default: no wrap) + * @note + * - Gettext's default value is 78 characters + * - Too small values might not be correctly supported. + */ + int m_wrapWidth; +}; + +#endif diff --git a/kbabel/filters/gettext/gettextimport.cpp b/kbabel/filters/gettext/gettextimport.cpp new file mode 100644 index 00000000..3f54301d --- /dev/null +++ b/kbabel/filters/gettext/gettextimport.cpp @@ -0,0 +1,821 @@ +// kate: space-indent on; indent-width 3; replace-tabs on; + +/* **************************************************************************** + This file is part of KBabel + + Copyright (C) 1999-2000 by Matthias Kiefer <matthias.kiefer@gmx.de> + 2001-2003 by Stanislav Visnovsky <visnovsky@kde.org> + Copyright (C) 2006 by Nicolas GOUTTE <nicolasg@snafu.de> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. + +**************************************************************************** */ + +#include "gettextimport.h" + +#include <catalogitem.h> +#include <resources.h> + +#include <qfile.h> +#include <qfileinfo.h> +#include <qregexp.h> +#include <qtextcodec.h> + +#include <kapplication.h> +#include <kdebug.h> +#include <kgenericfactory.h> +#include <klocale.h> + +K_EXPORT_COMPONENT_FACTORY( kbabel_gettextimport, KGenericFactory<GettextImportPlugin> ( "kbabelgettextimportfilter" ) ) + +using namespace KBabel; + +GettextImportPlugin::GettextImportPlugin(QObject* parent, const char* name, const QStringList &) : CatalogImportPlugin(parent,name) +{ +} + +ConversionStatus GettextImportPlugin::load(const QString& filename, const QString&) +{ + kdDebug( KBABEL ) << k_funcinfo << endl; + + if ( filename.isEmpty() ) { + kdDebug(KBABEL) << "fatal error: empty filename to open" << endl; + return NO_FILE; + } + + QFileInfo info(filename); + + if(!info.exists() || info.isDir()) + return NO_FILE; + + if(!info.isReadable()) + return NO_PERMISSIONS; + + QFile file(filename); + + if ( !file.open( IO_ReadOnly ) ) + return NO_PERMISSIONS; + + uint oldPercent = 0; + emit signalResetProgressBar(i18n("loading file"),100); + + QByteArray ba = file.readAll(); + file.close(); + + // find codec for file + bool hadCodec; + QTextCodec* codec=codecForArray( ba, &hadCodec ); + + bool recoveredErrorInHeader = false; + + QTextStream stream(ba,IO_ReadOnly); + + if(codec) + stream.setCodec(codec); + else + { + kdWarning() << "No encoding declared or found, using UTF-8" << endl; + stream.setEncoding( QTextStream::UnicodeUTF8 ); +#ifdef __GNUC__ +# warning Default UTF-8 encoding needs to be improved +#endif + // Templates define CHARSET, so if e make it a recoverable error, the template is not loadable anymore, as for templates recoverable errors are disqualifying. + //recoveredErrorInHeader = true; + } + + QIODevice *dev = stream.device(); + int fileSize = dev->size(); + + // if somethings goes wrong with the parsing, we don't have deleted the old contents + CatalogItem tempHeader; + QStringList tempObsolete; + + + kdDebug(KBABEL) << "start parsing..." << endl; + + // first read header + const ConversionStatus status = readHeader(stream); + + + if ( status == RECOVERED_PARSE_ERROR ) + { + kdDebug( KBABEL ) << "Recovered error in header entry" << endl; + recoveredErrorInHeader = true; + } + else if ( status != OK ) + { + emit signalClearProgressBar(); + + kdDebug( KBABEL ) << "Parse error in header entry" << endl; + return status; + } + + kdDebug() << "HEADER MSGID: " << _msgid << endl; + kdDebug() << "HEADER MSGSTR: " << _msgstr << endl; + if ( !_msgid.isEmpty() && !_msgid.first().isEmpty() ) + { + // The header must have an empty msgid + kdWarning(KBABEL) << "Header entry has non-empty msgid. Creating a temporary header! " << _msgid << endl; + tempHeader.setMsgid( QStringList() ); + QStringList tmp; + tmp.append( + "Content-Type: text/plain; charset=UTF-8\\n" // Unknown charset + "Content-Transfer-Encoding: 8bit\\n" + "Mime-Version: 1.0" ); + tempHeader.setMsgstr( tmp ); + // We keep the comment of the first entry, as it might really be a header comment (at least partially) + const QString comment( "# Header entry was created by KBabel!\n#\n" + _comment ); + tempHeader.setComment( comment ); + recoveredErrorInHeader = true; + } + else + { + tempHeader.setMsgid( _msgid ); + tempHeader.setMsgstr( _msgstr ); + tempHeader.setComment( _comment ); + } + if(tempHeader.isFuzzy()) + { + tempHeader.removeFuzzy(); + } + + // check if header seems to indicate docbook content generated by xml2pot + const bool docbookContent = (tempHeader.msgstr().find("application/x-xml2pot") != tempHeader.msgstr().end()); + + // now parse the rest of the file + uint counter=0; + QValueList<uint> errorIndex; + bool recoveredError=false; + bool docbookFile=false; + + while( !stream.eof() ) + { + kapp->processEvents(10); + if( isStopped() ) + { + return STOPPED; + } + + const ConversionStatus success=readEntry(stream); + + if(success==OK) + { + if( _obsolete ) + { + tempObsolete.append(_comment); + } + else + { + CatalogItem tempCatItem; + tempCatItem.setMsgctxt( _msgctxt ); + tempCatItem.setMsgid( _msgid ); + tempCatItem.setMsgstr( _msgstr ); + tempCatItem.setComment( _comment ); + tempCatItem.setGettextPluralForm( _gettextPluralForm ); + + // add new entry to the list of entries + appendCatalogItem(tempCatItem); + // check if first comment seems to indicate a docbook source file + if(counter==0) + docbookFile = ( tempCatItem.comment().find(".docbook") != -1 ); + } + } + else if(success==RECOVERED_PARSE_ERROR) + { + kdDebug( KBABEL ) << "Recovered parse error in entry: " << counter << endl; + recoveredError=true; + errorIndex.append(counter); + + CatalogItem tempCatItem; + tempCatItem.setMsgctxt( _msgctxt ); + tempCatItem.setMsgid( _msgid ); + tempCatItem.setMsgstr( _msgstr ); + tempCatItem.setComment( _comment ); + tempCatItem.setGettextPluralForm( _gettextPluralForm ); + + // add new entry to the list of entries + appendCatalogItem(tempCatItem); + } + else if ( success == PARSE_ERROR ) + { + kdDebug( KBABEL ) << "Parse error in entry: " << counter << endl; + return PARSE_ERROR; + } + else + { + kdWarning( KBABEL ) << "Unknown success status, assumig parse error " << success << endl; + return PARSE_ERROR; + } + counter++; + + const uint newPercent = (100*dev->at())/fileSize; + if(newPercent > oldPercent) + { + oldPercent = newPercent; + emit signalProgress(oldPercent); + } + } + + + // to be sure it is set to 100, if someone don't connect to + // signalClearProgressBar() + emit signalProgress(100); + + emit signalClearProgressBar(); + + + // ### TODO: can we check that there is no useful entry? + if ( !counter ) + { + // Empty file? (Otherwise, there would be a try of getting an entry and the count would be 1 !) + kdDebug( KBABEL ) << k_funcinfo << " Empty file?" << endl; + return PARSE_ERROR; + } + + kdDebug(KBABEL) << k_funcinfo << " ready" << endl; + + // We have succesfully loaded the file (perhaps with recovered errors) + + setGeneratedFromDocbook(docbookContent || docbookFile); + + setHeader(tempHeader); + setCatalogExtraData(tempObsolete); + setErrorIndex(errorIndex); + + if(hadCodec) + setFileCodec(codec); + else + setFileCodec(0); + + setMimeTypes( "application/x-gettext" ); + + if ( recoveredErrorInHeader ) + { + kdDebug( KBABEL ) << k_funcinfo << " Returning: header error" << endl; + return RECOVERED_HEADER_ERROR; + } + else if ( recoveredError ) + { + kdDebug( KBABEL ) << k_funcinfo << " Returning: recovered parse error" << endl; + return RECOVERED_PARSE_ERROR; + } + else + { + kdDebug( KBABEL ) << k_funcinfo << " Returning: OK! :-)" << endl; + return OK; + } +} + +QTextCodec* GettextImportPlugin::codecForArray(QByteArray& array, bool* hadCodec) +{ + if(hadCodec) + { + *hadCodec=false; + } + + QTextStream stream( array, IO_ReadOnly ); + // ### TODO Qt4: see if it can be done with QByteArray alone, in an encoding-neutral way. + // Set ISO-8859-1 as it is a relatively neutral encoding when reading (compared to UTF-8 or a random locale encoing) + stream.setEncoding( QTextStream::Latin1 ); + + // first read header + ConversionStatus status = readHeader(stream); + if(status!=OK && status != RECOVERED_PARSE_ERROR) + { + kdDebug(KBABEL) << "wasn't able to read header" << endl; + return 0; + } + + const QString head = _msgstr.first(); + + QRegExp regexp("Content-Type:\\s*\\w+/[-\\w]+;?\\s*charset\\s*=\\s*(\\S+)\\s*\\\\n"); + if( regexp.search( head ) == -1 ) + { + kdDebug(KBABEL) << "no charset entry found" << endl; + return 0; + } + + const QString charset = regexp.cap(1); + kdDebug(KBABEL) << "charset: " << charset << endl; + + QTextCodec* codec=0; + + if(!charset.isEmpty()) + { + // "CHARSET" is the default charset entry in a template (pot). + // characters in a template should be either pure ascii or + // at least utf8, so utf8-codec can be used for both. + if( charset == "CHARSET") + { + if(hadCodec) + *hadCodec=false; + + codec=QTextCodec::codecForName("utf8"); + kdDebug(KBABEL) + << QString("file seems to be a template: using utf-8 encoding.") + << endl; + } + else + { + codec=QTextCodec::codecForName(charset.latin1()); + if(hadCodec) + *hadCodec=true; + } + + if(!codec) + { + kdWarning(KBABEL) << "charset found, but no codec available, using UTF-8 instead" << endl; + } + } + else + { + // No charset? So it is probably ASCII, therefore UTF-8 + kdWarning(KBABEL) << "No charset defined! Assuming UTF-8!" << endl; + } + + + return codec; +} + +ConversionStatus GettextImportPlugin::readHeader(QTextStream& stream) +{ + CatalogItem temp; + int filePos=stream.device()->at(); + ConversionStatus status=readEntry(stream); + + if(status==OK || status==RECOVERED_PARSE_ERROR) + { + // test if this is the header + if(!_msgid.first().isEmpty()) + { + stream.device()->at(filePos); + } + + return status; + } + + return PARSE_ERROR; +} + +ConversionStatus GettextImportPlugin::readEntry(QTextStream& stream) +{ + //kdDebug( KBABEL ) << k_funcinfo << " START" << endl; + enum {Begin,Comment,Msgctxt,Msgid,Msgstr} part=Begin; + + QString line; + bool error=false; + bool recoverableError=false; + bool seenMsgctxt=false; + _msgstr.clear(); + _msgstr.append(QString()); + _msgid.clear(); + _msgid.append(QString()); + _msgctxt=QString(); + _comment=QString(); + _gettextPluralForm=false; + _obsolete=false; + + QStringList::Iterator msgstrIt=_msgstr.begin(); + + while( !stream.eof() ) + { + const int pos=stream.device()->at(); + + line=stream.readLine(); + + //kdDebug() << "Parsing line: " << line << endl; + + // ### Qt4: no need of a such a check + if(line.isNull()) // file end + break; + else if ( line.startsWith( "<<<<<<<" ) || line.startsWith( "=======" ) || line.startsWith( ">>>>>>>" ) ) + { + // We have found a CVS/SVN conflict marker. Abort. + // (It cannot be any useful data of the PO file, as otherwise the line would start with at least a quote) + kdError(KBABEL) << "CVS/SVN conflict marker found! Aborting!" << endl << line << endl; + return PARSE_ERROR; + } + + // remove whitespaces from beginning and end of line + line=line.stripWhiteSpace(); + + if(part==Begin) + { + // ignore trailing newlines + if(line.isEmpty()) + continue; + + if(line.startsWith("#~")) + { + _obsolete=true; + part=Comment; + _comment=line; + } + else if(line.startsWith("#")) + { + part=Comment; + _comment=line; + } + else if(line.find(QRegExp("^msgctxt\\s*\".*\"$")) != -1) + { + part=Msgctxt; + + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^msgctxt\\s*\"")); + line.remove(QRegExp("\"$")); + _msgctxt=line; + seenMsgctxt=true; + } + else if(line.find(QRegExp("^msgid\\s*\".*\"$")) != -1) + { + part=Msgid; + + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^msgid\\s*\"")); + line.remove(QRegExp("\"$")); + + (*(_msgid).begin())=line; + } + // one of the quotation marks is missing + else if(line.find(QRegExp("^msgid\\s*\"?.*\"?$")) != -1) + { + part=Msgid; + + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^msgid\\s*\"?")); + line.remove(QRegExp("\"$")); + + (*(_msgid).begin())=line; + + if(!line.isEmpty()) + recoverableError=true; + } + else + { + kdDebug(KBABEL) << "no comment, msgctxt or msgid found after a comment: " << line << endl; + error=true; + break; + } + } + else if(part==Comment) + { + if(line.isEmpty() && _obsolete ) return OK; + if(line.isEmpty() ) + continue; + else if(line.startsWith("#~")) + { + _comment+=("\n"+line); + _obsolete=true; + } + else if(line.startsWith("#")) + { + _comment+=("\n"+line); + } + else if(line.find(QRegExp("^msgctxt\\s*\".*\"$")) != -1) + { + part=Msgctxt; + + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^msgctxt\\s*\"")); + line.remove(QRegExp("\"$")); + _msgctxt=line; + seenMsgctxt=true; + } + else if(line.find(QRegExp("^msgid\\s*\".*\"$")) != -1) + { + part=Msgid; + + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^msgid\\s*\"")); + line.remove(QRegExp("\"$")); + + (*(_msgid).begin())=line; + } + // one of the quotation marks is missing + else if(line.find(QRegExp("^msgid\\s*\"?.*\"?$")) != -1) + { + part=Msgid; + + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^msgid\\s*\"?")); + line.remove(QRegExp("\"$")); + + (*(_msgid).begin())=line; + + if(!line.isEmpty()) + recoverableError=true; + } + else + { + kdDebug(KBABEL) << "no comment or msgid found after a comment while parsing: " << _comment << endl; + error=true; + break; + } + } + else if(part==Msgctxt) + { + if(line.isEmpty()) + continue; + else if(line.find(QRegExp("^\".*\\n?\"$")) != -1) + { + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^\"")); + line.remove(QRegExp("\"$")); + + // add Msgctxt line to item + if(_msgctxt.isEmpty()) + _msgctxt=line; + else + _msgctxt+=("\n"+line); + } + else if(line.find(QRegExp("^msgid\\s*\".*\"$")) != -1) + { + part=Msgid; + + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^msgid\\s*\"")); + line.remove(QRegExp("\"$")); + + (*(_msgid).begin())=line; + } + // one of the quotation marks is missing + else if(line.find(QRegExp("^msgid\\s*\"?.*\"?$")) != -1) + { + part=Msgid; + + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^msgid\\s*\"?")); + line.remove(QRegExp("\"$")); + + (*(_msgid).begin())=line; + + if(!line.isEmpty()) + recoverableError=true; + } + else + { + kdDebug(KBABEL) << "no msgid found after a msgctxt while parsing: " << _msgctxt << endl; + error=true; + break; + } + } + else if(part==Msgid) + { + if(line.isEmpty()) + continue; + else if(line.find(QRegExp("^\".*\\n?\"$")) != -1) + { + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^\"")); + line.remove(QRegExp("\"$")); + + QStringList::Iterator it; + if(_gettextPluralForm) + it = _msgid.fromLast(); + else + it = _msgid.begin(); + + // add Msgid line to item + if((*it).isEmpty()) + (*it)=line; + else + (*it)+=("\n"+line); + } + else if(line.find(QRegExp("^msgid_plural\\s*\".*\"$")) != -1) + { + part=Msgid; + _gettextPluralForm = true; + + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^msgid_plural\\s*\"")); + line.remove(QRegExp("\"$")); + + _msgid.append(line); + } + // one of the quotation marks is missing + else if(line.find(QRegExp("^msgid_plural\\s*\"?.*\"?$")) != -1) + { + part=Msgid; + _gettextPluralForm = true; + + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^msgid_plural\\s*\"?")); + line.remove(QRegExp("\"$")); + + _msgid.append(line); + + if(!line.isEmpty()) + recoverableError=true; + } + else if(!_gettextPluralForm + && (line.find(QRegExp("^msgstr\\s*\".*\\n?\"$")) != -1)) + { + part=Msgstr; + + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^msgstr\\s*\"?")); + line.remove(QRegExp("\"$")); + + (*msgstrIt)=line; + } + else if(!_gettextPluralForm + && line.find(QRegExp("^msgstr\\s*\"?.*\\n?\"?$")) != -1) + { + part=Msgstr; + + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^msgstr\\s*\"?")); + line.remove(QRegExp("\"$")); + + (*msgstrIt)=line; + + if(!line.isEmpty()) + recoverableError=true; + } + else if( _gettextPluralForm + && (line.find(QRegExp("^msgstr\\[0\\]\\s*\".*\\n?\"$")) != -1)) + { + part=Msgstr; + + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^msgstr\\[0\\]\\s*\"?")); + line.remove(QRegExp("\"$")); + + (*msgstrIt)=line; + } + else if( _gettextPluralForm + && (line.find(QRegExp("^msgstr\\[0\\]\\s*\"?.*\\n?\"?$")) != -1)) + { + part=Msgstr; + + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^msgstr\\[0\\]\\s*\"?")); + line.remove(QRegExp("\"$")); + + (*msgstrIt)=line; + + if(!line.isEmpty()) + recoverableError=true; + } + else if ( line.startsWith( "#" ) ) + { + // ### TODO: could this be considered recoverable? + kdDebug(KBABEL) << "comment found after a msgid while parsing: " << _msgid.first() << endl; + error=true; + break; + } + else if ( line.startsWith( "msgid" ) ) + { + kdDebug(KBABEL) << "Another msgid found after a msgid while parsing: " << _msgid.first() << endl; + error=true; + break; + } + // a line of the msgid with a missing quotation mark + else if(line.find(QRegExp("^\"?.+\\n?\"?$")) != -1) + { + recoverableError=true; + + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^\"")); + line.remove(QRegExp("\"$")); + + QStringList::Iterator it; + if( _gettextPluralForm ) + it = _msgid.fromLast(); + else + it = _msgid.begin(); + + // add Msgid line to item + if((*it).isEmpty()) + (*it)=line; + else + (*it)+=("\n"+line); + } + else + { + kdDebug(KBABEL) << "no msgstr found after a msgid while parsing: " << _msgid.first() << endl; + error=true; + break; + } + } + else if(part==Msgstr) + { + if(line.isEmpty()) + continue; + // another line of the msgstr + else if(line.find(QRegExp("^\".*\\n?\"$")) != -1) + { + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^\"")); + line.remove(QRegExp("\"$")); + + if((*msgstrIt).isEmpty()) + (*msgstrIt)=line; + else + (*msgstrIt)+=("\n"+line); + } + else if( _gettextPluralForm + && (line.find(QRegExp("^msgstr\\[[0-9]+\\]\\s*\".*\\n?\"$")) != -1)) + { + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^msgstr\\[[0-9]+\\]\\s*\"?")); + line.remove(QRegExp("\"$")); + + msgstrIt=_msgstr.append(line); + } + else if( _gettextPluralForm + && (line.find(QRegExp("^msgstr\\[[0-9]\\]\\s*\"?.*\\n?\"?$")) != -1)) + { + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^msgstr\\[[0-9]\\]\\s*\"?")); + line.remove(QRegExp("\"$")); + + msgstrIt=_msgstr.append(line); + + if(!line.isEmpty()) + recoverableError=true; + } + else if((line.find(QRegExp("^\\s*msgid")) != -1) || (line.find(QRegExp("^\\s*#")) != -1)) + { + // We have read successfully one entry, so end loop. + stream.device()->at(pos);// reset position in stream to beginning of this line + break; + } + else if(line.startsWith("msgstr")) + { + kdDebug(KBABEL) << "Another msgstr found after a msgstr while parsing: " << _msgstr.last() << endl; + error=true; + break; + } + // another line of the msgstr with a missing quotation mark + else if(line.find(QRegExp("^\"?.+\\n?\"?$")) != -1) + { + recoverableError=true; + + // remove quotes at beginning and the end of the lines + line.remove(QRegExp("^\"")); + line.remove(QRegExp("\"$")); + + if((*msgstrIt).isEmpty()) + (*msgstrIt)=line; + else + (*msgstrIt)+=("\n"+line); + } + else + { + kdDebug(KBABEL) << "no msgid or comment found after a msgstr while parsing: " << _msgstr.last() << endl; + error=true; + break; + } + } + } + +/* + if(_gettextPluralForm) + { + kdDebug(KBABEL) << "gettext plural form:\n" + << "msgid:\n" << _msgid.first() << "\n" + << "msgid_plural:\n" << _msgid.last() << "\n" << endl; + int counter=0; + for(QStringList::Iterator it = _msgstr.begin(); it != _msgstr.end(); ++it) + { + kdDebug(KBABEL) << "msgstr[" << counter << "]:\n" + << (*it) << endl; + counter++; + } + } + */ + + //kdDebug( KBABEL ) << k_funcinfo << " NEAR RETURN" << endl; + if(error) + return PARSE_ERROR; + else if(recoverableError) + return RECOVERED_PARSE_ERROR; + else + { + return OK; + } +} diff --git a/kbabel/filters/gettext/gettextimport.h b/kbabel/filters/gettext/gettextimport.h new file mode 100644 index 00000000..e28ec790 --- /dev/null +++ b/kbabel/filters/gettext/gettextimport.h @@ -0,0 +1,70 @@ +/* **************************************************************************** + This file is part of KBabel + + Copyright (C) 2002 by Stanislav Visnovsky + <visnovsky@kde.org> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. + +**************************************************************************** */ +#ifndef GETTEXTIMPORTPLUGIN_H +#define GETTEXTIMPORTPLUGIN_H + +#include <catalogfileplugin.h> + +#include <qstringlist.h> + +class KURL; +class QFile; +class QTextCodec; + +/* **************************************************************************** + The class for importing GNU gettext PO files. As an extra information, + it stores the list of all obsolete entries. +**************************************************************************** */ + +class GettextImportPlugin: public KBabel::CatalogImportPlugin +{ +public: + GettextImportPlugin(QObject* parent, const char* name, const QStringList &); + virtual KBabel::ConversionStatus load(const QString& file, const QString& mimetype); + virtual const QString id() { return "GNU gettext"; } + +private: + QTextCodec* codecForArray(QByteArray& arary, bool* hadCodec); + KBabel::ConversionStatus readHeader(QTextStream& stream); + KBabel::ConversionStatus readEntry(QTextStream& stream); + + // description of the last read entry + QString _msgctxt; + QStringList _msgid; + QStringList _msgstr; + QString _comment; + bool _gettextPluralForm; + bool _obsolete; +}; + +#endif diff --git a/kbabel/filters/gettext/kbabel_gettext_export.desktop b/kbabel/filters/gettext/kbabel_gettext_export.desktop new file mode 100644 index 00000000..30cf0df5 --- /dev/null +++ b/kbabel/filters/gettext/kbabel_gettext_export.desktop @@ -0,0 +1,53 @@ +[Desktop Entry] +Type=Service +Name=KBabel GNU Gettext Export Filter +Name[bg]=Филтър за експортиране на GNU Gettext - KBabel +Name[br]=Sil ezporzh GNU Gettext evit KBabel +Name[bs]=KBabel GNU Gettext filter za izvoz +Name[ca]=Filtre KBabel per exportar Gettext de GNU +Name[cs]=Exportní filtr do formátu Gettext pro KBabel +Name[cy]=Hidl Allforio GNU Gettext KBabel +Name[da]=KBabel GNU Gettext eksportfilter +Name[de]=GNU Gettext-Exportfilter für KBabel +Name[el]=Φίλτρο εξαγωγής GNU Gettext του KBabel +Name[es]=Filtro de exportación KBabel GNU Gettext +Name[et]=KBabeli GNU Gettexti ekspordifilter +Name[eu]=KBabel GNU gettext esportazio iragazkia +Name[fa]=پالایۀ صادرات KBabel GNU Gettext +Name[fi]=KBabel GNU Gettext -vientisuodatin +Name[fr]=filtre d'exportation GNU Gettext pour KBabel +Name[ga]=Scagaire Easpórtála Gettext GNU le haghaidh KBabel +Name[gl]=Filtro de exportación de GNU/gettext para KBabel +Name[hi]=के-बेबल ग्नू गेट-टेक्स्ट निर्यात फ़िल्टर +Name[hu]=KBabel GNU Gettext exportszűrő +Name[is]=KBabel GNU Gettext útflutningssía +Name[it]=Filtro di esportazione di GNU Gettext per KBabel +Name[ja]=KBabel GNU Gettext エクスポートフィルタ +Name[ka]=KBabel GNU Gettext ექსპორტის ფილტრი +Name[kk]=KBabel GNU Gettext экспорттау сүзгісі +Name[lt]=KBabel GNU Gettext eksportavimo filtras +Name[ms]=Penapis Eksport KBabel GNU Gettext +Name[nb]=GNU Gettext-eksportfilter for KBabel +Name[nds]=KBabel-Exportfilter för GNU Gettext +Name[ne]=केब्याबल जीएनयू गेटटेक्स्ट निर्यात फिल्टर +Name[nl]=KBabel GNU Gettext Exportfilter +Name[nn]=GNU Gettext-eksportfilter for KBabel +Name[pa]=KBabel GNU Gettext ਨਿਰਯਾਤ ਫਿਲਟਰ +Name[pl]=Filtr KBabel do eksportu do formatu GNU Gettext +Name[pt]=Filtro de Exportação do Gettext da GNU para o KBabel +Name[pt_BR]=Filtro de Exportação GNU Gettext do KBabel +Name[ru]=Фильтр экспорта сообщений GNU Gettext +Name[sk]=Exportný filter GNU gettext pre KBabel +Name[sl]=Izvozni filter GNU Gettext za KBabel +Name[sr]=KBabel-ов филтер за извоз у GNU Gettext +Name[sr@Latn]=KBabel-ov filter za izvoz u GNU Gettext +Name[sv]=Kbabel exportfilter för GNU Gettext +Name[ta]=Kபாபேல் GNU உரை எடு ஏற்றுமதி வடிகட்டி +Name[tg]=Филтри содироти хабарҳо GNU Gettext +Name[tr]=KBabel GNU Gettext Dışa Aktarma Süzgeci +Name[uk]=Фільтр експорту GNU Gettext для KBabel +Name[zh_CN]=KBabel GNU Gettext 导出过滤器 +Name[zh_TW]=KBabel GNU Gettext 匯出過濾器 +X-KDE-Library=kbabel_gettextexport +X-KDE-Export=application/x-gettext +ServiceTypes=KBabelFilter diff --git a/kbabel/filters/gettext/kbabel_gettext_import.desktop b/kbabel/filters/gettext/kbabel_gettext_import.desktop new file mode 100644 index 00000000..c6b1b293 --- /dev/null +++ b/kbabel/filters/gettext/kbabel_gettext_import.desktop @@ -0,0 +1,53 @@ +[Desktop Entry] +Type=Service +Name=KBabel GNU Gettext Import Filter +Name[bg]=Филтър за импортиране на GNU Gettext - KBabel +Name[br]=Sil enporzh GNU Gettext evit KBabel +Name[bs]=KBabel GNU Gettext filter za uvoz +Name[ca]=Filtre KBabel per importar Gettext de GNU +Name[cs]=Importní filtr Gettext pro KBabel +Name[cy]=Hidl Mewnforio GNU Gettext KBabel +Name[da]=KBabel GNU Gettext Import-filter +Name[de]=GNU Gettext-Importfilter für KBabel +Name[el]=Φίλτρο εισαγωγής GNU Gettext του KBabel +Name[es]=Filtro de importación KBabel GNU Gettext +Name[et]=KBabeli GNU Gettexti impordifilter +Name[eu]=KBabel GNU gettext inportazio iragazkia +Name[fa]=پالایۀ واردات KBabel GNU Gettext +Name[fi]=KBabel GNU Gettext -tuontisuodatin +Name[fr]=Filtre d'importation GNU Gettext pour KBabel +Name[ga]=Scagaire Iompórtála Gettext GNU le haghaidh KBabel +Name[gl]=Filtro de importación de GNU/gettext para KBabel +Name[hi]=के-बेबल ग्नू गेट-टेक्स्ट आयात फ़िल्टर +Name[hu]=KBabel GNU Gettext importszűrő +Name[is]=KBabel GNU Gettext innflutningssía +Name[it]=Filtro di importazione di GNU Gettext per KBabel +Name[ja]=KBabel GNU Gettext インポートフィルタ +Name[ka]=KBabel GNU Gettext იმპორის ფილტრი +Name[kk]=KBabel GNU Gettext импорттау сүзгісі +Name[lt]=KBabel GNU Gettext importavimo filtras +Name[ms]=Penapis Import KBabel GNU Gettext +Name[nb]=GNU Gettext-importfilter for KBabel +Name[nds]=KBabel-Importfilter för GNU Gettext +Name[ne]=केब्याबल जीएनयू गेटटेक्स्ट आयात फिल्टर +Name[nl]=KBabel GNU Gettext Importfilter +Name[nn]=GNU Gettext-importfilter for KBabel +Name[pa]=KBabel GNU Gettext ਅਯਾਤ ਫਿਲਟਰ +Name[pl]=Filtr KBabel do importu z formatu GNU Gettext +Name[pt]=Filtro de Importação de Gettext da GNU para o KBabel +Name[pt_BR]=Filtro de Importação GNU Gettext para o KBabel +Name[ru]=Фильтр импорта сообщений GNU Gettext +Name[sk]=Importný filter GNU gettext pre KBabel +Name[sl]=Uvozni filter GNU Gettext za KBabel +Name[sr]=KBabel-ов филтер за увоз из GNU Gettext-а +Name[sr@Latn]=KBabel-ov filter za uvoz iz GNU Gettext-a +Name[sv]=Kbabel importfilter för GNU Gettext +Name[ta]=Kபாபேல் GNU உரை எடு இறக்குமதி வடிகட்டி +Name[tg]=Филтри воридоти хабарҳо GNU Gettext +Name[tr]=KBabel GNU Gettext İçe Aktarma Süzgeci +Name[uk]=Фільтр імпорту GNU Gettext для KBabel +Name[zh_CN]=KBabel GNU Gettext 导入过滤器 +Name[zh_TW]=KBabel GNU Gettext 匯入過濾器 +X-KDE-Library=kbabel_gettextimport +X-KDE-Import=application/x-gettext +ServiceTypes=KBabelFilter |