diff options
Diffstat (limited to 'l10ntools/source/po.cxx')
-rw-r--r-- | l10ntools/source/po.cxx | 644 |
1 files changed, 644 insertions, 0 deletions
diff --git a/l10ntools/source/po.cxx b/l10ntools/source/po.cxx new file mode 100644 index 000000000..62d09e1e7 --- /dev/null +++ b/l10ntools/source/po.cxx @@ -0,0 +1,644 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <rtl/ustring.hxx> +#include <rtl/strbuf.hxx> +#include <rtl/crc.h> +#include <sal/log.hxx> + +#include <cstring> +#include <ctime> +#include <cassert> + +#include <vector> +#include <string> +#include <string_view> + +#include <po.hxx> +#include <helper.hxx> + +/** Container of po entry + + Provide all file operations related to LibreOffice specific + po entry and store it's attributes. +*/ +class GenPoEntry +{ +private: + OStringBuffer m_sExtractCom; + std::vector<OString> m_sReferences; + OString m_sMsgCtxt; + OString m_sMsgId; + OString m_sMsgIdPlural; + OString m_sMsgStr; + std::vector<OString> m_sMsgStrPlural; + bool m_bFuzzy; + bool m_bCFormat; + bool m_bNull; + +public: + GenPoEntry(); + + const std::vector<OString>& getReference() const { return m_sReferences; } + const OString& getMsgCtxt() const { return m_sMsgCtxt; } + const OString& getMsgId() const { return m_sMsgId; } + const OString& getMsgStr() const { return m_sMsgStr; } + bool isFuzzy() const { return m_bFuzzy; } + bool isNull() const { return m_bNull; } + + void setExtractCom(std::string_view rExtractCom) + { + m_sExtractCom = rExtractCom; + } + void setReference(const OString& rReference) + { + m_sReferences.push_back(rReference); + } + void setMsgCtxt(const OString& rMsgCtxt) + { + m_sMsgCtxt = rMsgCtxt; + } + void setMsgId(const OString& rMsgId) + { + m_sMsgId = rMsgId; + } + void setMsgStr(const OString& rMsgStr) + { + m_sMsgStr = rMsgStr; + } + + void writeToFile(std::ofstream& rOFStream) const; + void readFromFile(std::ifstream& rIFStream); +}; + +namespace +{ + // Convert a normal string to msg/po output string + OString lcl_GenMsgString(std::string_view rString) + { + if ( rString.empty() ) + return "\"\""; + + OString sResult = + "\"" + + helper::escapeAll(rString,"\n""\t""\r""\\""\"","\\n""\\t""\\r""\\\\""\\\"") + + "\""; + sal_Int32 nIndex = 0; + while((nIndex=sResult.indexOf("\\n",nIndex))!=-1) + { + if( !sResult.match("\\\\n", nIndex-1) && + nIndex!=sResult.getLength()-3) + { + sResult = sResult.replaceAt(nIndex,2,"\\n\"\n\""); + } + ++nIndex; + } + + if ( sResult.indexOf('\n') != -1 ) + return "\"\"\n" + sResult; + + return sResult; + } + + // Convert msg string to normal form + OString lcl_GenNormString(std::string_view rString) + { + return + helper::unEscapeAll( + rString.substr(1,rString.size()-2), + "\\n""\\t""\\r""\\\\""\\\"", + "\n""\t""\r""\\""\""); + } +} + +GenPoEntry::GenPoEntry() + : m_bFuzzy( false ) + , m_bCFormat( false ) + , m_bNull( false ) +{ +} + +void GenPoEntry::writeToFile(std::ofstream& rOFStream) const +{ + if ( rOFStream.tellp() != std::ofstream::pos_type( 0 )) + rOFStream << std::endl; + if ( !m_sExtractCom.isEmpty() ) + rOFStream + << "#. " + << m_sExtractCom.toString().replaceAll("\n","\n#. ") << std::endl; + for(const auto& rReference : m_sReferences) + rOFStream << "#: " << rReference << std::endl; + if ( m_bFuzzy ) + rOFStream << "#, fuzzy" << std::endl; + if ( m_bCFormat ) + rOFStream << "#, c-format" << std::endl; + if ( !m_sMsgCtxt.isEmpty() ) + rOFStream << "msgctxt " + << lcl_GenMsgString(m_sMsgCtxt) + << std::endl; + rOFStream << "msgid " + << lcl_GenMsgString(m_sMsgId) << std::endl; + if ( !m_sMsgIdPlural.isEmpty() ) + rOFStream << "msgid_plural " + << lcl_GenMsgString(m_sMsgIdPlural) + << std::endl; + if ( !m_sMsgStrPlural.empty() ) + for(auto & line : m_sMsgStrPlural) + rOFStream << line.copy(0,10) << lcl_GenMsgString(line.subView(10)) << std::endl; + else + rOFStream << "msgstr " + << lcl_GenMsgString(m_sMsgStr) << std::endl; +} + +void GenPoEntry::readFromFile(std::ifstream& rIFStream) +{ + *this = GenPoEntry(); + OString* pLastMsg = nullptr; + std::string sTemp; + getline(rIFStream,sTemp); + if( rIFStream.eof() || sTemp.empty() ) + { + m_bNull = true; + return; + } + while(!rIFStream.eof()) + { + OString sLine(sTemp.data(),sTemp.length()); + if (sLine.startsWith("#. ")) + { + if( !m_sExtractCom.isEmpty() ) + { + m_sExtractCom.append("\n"); + } + m_sExtractCom.append(sLine.subView(3)); + } + else if (sLine.startsWith("#: ")) + { + m_sReferences.push_back(sLine.copy(3)); + } + else if (sLine.startsWith("#, fuzzy")) + { + m_bFuzzy = true; + } + else if (sLine.startsWith("#, c-format")) + { + m_bCFormat = true; + } + else if (sLine.startsWith("msgctxt ")) + { + m_sMsgCtxt = lcl_GenNormString(sLine.subView(8)); + pLastMsg = &m_sMsgCtxt; + } + else if (sLine.startsWith("msgid ")) + { + m_sMsgId = lcl_GenNormString(sLine.subView(6)); + pLastMsg = &m_sMsgId; + } + else if (sLine.startsWith("msgid_plural ")) + { + m_sMsgIdPlural = lcl_GenNormString(sLine.subView(13)); + pLastMsg = &m_sMsgIdPlural; + } + else if (sLine.startsWith("msgstr ")) + { + m_sMsgStr = lcl_GenNormString(sLine.subView(7)); + pLastMsg = &m_sMsgStr; + } + else if (sLine.startsWith("msgstr[")) + { + // assume there are no more than 10 plural forms... + // and that plural strings are never split to multi-line in po + m_sMsgStrPlural.push_back(sLine.subView(0,10) + lcl_GenNormString(sLine.subView(10))); + } + else if (sLine.startsWith("\"") && pLastMsg) + { + OString sReference; + if (!m_sReferences.empty()) + { + sReference = m_sReferences.front(); + } + if (pLastMsg != &m_sMsgCtxt || sLine != OStringConcatenation("\"" + sReference + "\\n\"")) + { + *pLastMsg += lcl_GenNormString(sLine); + } + } + else + break; + getline(rIFStream,sTemp); + } + } + +PoEntry::PoEntry() + : m_bIsInitialized( false ) +{ +} + +PoEntry::PoEntry( + std::string_view rSourceFile, std::string_view rResType, std::string_view rGroupId, + std::string_view rLocalId, std::string_view rHelpText, + const OString& rText, const TYPE eType ) + : m_bIsInitialized( false ) +{ + if( rSourceFile.empty() ) + throw NOSOURCFILE; + else if ( rResType.empty() ) + throw NORESTYPE; + else if ( rGroupId.empty() ) + throw NOGROUPID; + else if ( rText.isEmpty() ) + throw NOSTRING; + else if ( rHelpText.size() == 5 ) + throw WRONGHELPTEXT; + + m_pGenPo.reset( new GenPoEntry() ); + size_t idx = rSourceFile.rfind('/'); + if (idx == std::string_view::npos) + idx = 0; + OString sReference(rSourceFile.substr(idx+1)); + m_pGenPo->setReference(sReference); + + OString sMsgCtxt = + sReference + "\n" + + rGroupId + "\n" + + (rLocalId.empty() ? OString() : OString::Concat(rLocalId) + "\n") + + rResType; + switch(eType){ + case TTEXT: + sMsgCtxt += ".text"; break; + case TQUICKHELPTEXT: + sMsgCtxt += ".quickhelptext"; break; + case TTITLE: + sMsgCtxt += ".title"; break; + // Default case is unneeded because the type of eType has only three element + } + m_pGenPo->setMsgCtxt(sMsgCtxt); + m_pGenPo->setMsgId(rText); + m_pGenPo->setExtractCom(OStringConcatenation( + ( !rHelpText.empty() ? OString::Concat(rHelpText) + "\n" : OString()) + + genKeyId( m_pGenPo->getReference().front() + rGroupId + rLocalId + rResType + rText ) )); + m_bIsInitialized = true; +} + +PoEntry::~PoEntry() +{ +} + +PoEntry::PoEntry( const PoEntry& rPo ) + : m_pGenPo( rPo.m_pGenPo ? new GenPoEntry( *(rPo.m_pGenPo) ) : nullptr ) + , m_bIsInitialized( rPo.m_bIsInitialized ) +{ +} + +PoEntry& PoEntry::operator=(const PoEntry& rPo) +{ + if( this == &rPo ) + { + return *this; + } + if( rPo.m_pGenPo ) + { + if( m_pGenPo ) + { + *m_pGenPo = *(rPo.m_pGenPo); + } + else + { + m_pGenPo.reset( new GenPoEntry( *(rPo.m_pGenPo) ) ); + } + } + else + { + m_pGenPo.reset(); + } + m_bIsInitialized = rPo.m_bIsInitialized; + return *this; +} + +PoEntry& PoEntry::operator=(PoEntry&& rPo) noexcept +{ + m_pGenPo = std::move(rPo.m_pGenPo); + m_bIsInitialized = std::move(rPo.m_bIsInitialized); + return *this; +} + +OString const & PoEntry::getSourceFile() const +{ + assert( m_bIsInitialized ); + return m_pGenPo->getReference().front(); +} + +OString PoEntry::getGroupId() const +{ + assert( m_bIsInitialized ); + return m_pGenPo->getMsgCtxt().getToken(0,'\n'); +} + +OString PoEntry::getLocalId() const +{ + assert( m_bIsInitialized ); + const OString sMsgCtxt = m_pGenPo->getMsgCtxt(); + if (sMsgCtxt.indexOf('\n')==sMsgCtxt.lastIndexOf('\n')) + return OString(); + else + return sMsgCtxt.getToken(1,'\n'); +} + +OString PoEntry::getResourceType() const +{ + assert( m_bIsInitialized ); + const OString sMsgCtxt = m_pGenPo->getMsgCtxt(); + if (sMsgCtxt.indexOf('\n')==sMsgCtxt.lastIndexOf('\n')) + return sMsgCtxt.getToken(1,'\n').getToken(0,'.'); + else + return sMsgCtxt.getToken(2,'\n').getToken(0,'.'); +} + +PoEntry::TYPE PoEntry::getType() const +{ + assert( m_bIsInitialized ); + const OString sMsgCtxt = m_pGenPo->getMsgCtxt(); + const OString sType = sMsgCtxt.copy( sMsgCtxt.lastIndexOf('.') + 1 ); + assert( + (sType == "text" || sType == "quickhelptext" || sType == "title") ); + if ( sType == "text" ) + return TTEXT; + else if ( sType == "quickhelptext" ) + return TQUICKHELPTEXT; + else + return TTITLE; +} + +bool PoEntry::isFuzzy() const +{ + assert( m_bIsInitialized ); + return m_pGenPo->isFuzzy(); +} + +// Get message context +const OString& PoEntry::getMsgCtxt() const +{ + assert( m_bIsInitialized ); + return m_pGenPo->getMsgCtxt(); + +} + +// Get translation string in merge format +OString const & PoEntry::getMsgId() const +{ + assert( m_bIsInitialized ); + return m_pGenPo->getMsgId(); +} + +// Get translated string in merge format +const OString& PoEntry::getMsgStr() const +{ + assert( m_bIsInitialized ); + return m_pGenPo->getMsgStr(); + +} + +bool PoEntry::IsInSameComp(const PoEntry& rPo1,const PoEntry& rPo2) +{ + assert( rPo1.m_bIsInitialized && rPo2.m_bIsInitialized ); + return ( rPo1.getSourceFile() == rPo2.getSourceFile() && + rPo1.getGroupId() == rPo2.getGroupId() && + rPo1.getLocalId() == rPo2.getLocalId() && + rPo1.getResourceType() == rPo2.getResourceType() ); +} + +OString PoEntry::genKeyId(const OString& rGenerator) +{ + sal_uInt32 nCRC = rtl_crc32(0, rGenerator.getStr(), rGenerator.getLength()); + // Use simple ASCII characters, exclude I, l, 1 and O, 0 to avoid confusing IDs + static const char sSymbols[] = + "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"; + char sKeyId[6]; + for( short nKeyInd = 0; nKeyInd < 5; ++nKeyInd ) + { + sKeyId[nKeyInd] = sSymbols[(nCRC & 63) % strlen(sSymbols)]; + nCRC >>= 6; + } + sKeyId[5] = '\0'; + return OString(sKeyId); +} + +namespace +{ + // Get actual time in "YEAR-MO-DA HO:MI+ZONE" form + OString lcl_GetTime() + { + time_t aNow = time(nullptr); + struct tm* pNow = localtime(&aNow); + char pBuff[50]; + strftime( pBuff, sizeof pBuff, "%Y-%m-%d %H:%M%z", pNow ); + return OString(pBuff); + } +} + +// when updating existing files (pocheck), reuse provided po-header +PoHeader::PoHeader( std::string_view rExtSrc, const OString& rPoHeaderMsgStr ) + : m_pGenPo( new GenPoEntry() ) + , m_bIsInitialized( false ) +{ + m_pGenPo->setExtractCom(OStringConcatenation(OString::Concat("extracted from ") + rExtSrc)); + m_pGenPo->setMsgStr(rPoHeaderMsgStr); + m_bIsInitialized = true; +} + +PoHeader::PoHeader( std::string_view rExtSrc ) + : m_pGenPo( new GenPoEntry() ) + , m_bIsInitialized( false ) +{ + m_pGenPo->setExtractCom(OStringConcatenation(OString::Concat("extracted from ") + rExtSrc)); + m_pGenPo->setMsgStr( + "Project-Id-Version: PACKAGE VERSION\n" + "Report-Msgid-Bugs-To: https://bugs.libreoffice.org/enter_bug.cgi?" + "product=LibreOffice&bug_status=UNCONFIRMED&component=UI\n" + "POT-Creation-Date: " + lcl_GetTime() + + "\nPO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" + "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" + "Language-Team: LANGUAGE <LL@li.org>\n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=UTF-8\n" + "Content-Transfer-Encoding: 8bit\n" + "X-Accelerator-Marker: ~\n" + "X-Generator: LibreOffice\n"); + m_bIsInitialized = true; +} + +PoHeader::~PoHeader() +{ +} + +PoOfstream::PoOfstream() + : m_bIsAfterHeader( false ) +{ +} + +PoOfstream::PoOfstream(const OString& rFileName, OpenMode aMode ) + : m_bIsAfterHeader( false ) +{ + open( rFileName, aMode ); +} + +PoOfstream::~PoOfstream() +{ + if( isOpen() ) + { + close(); + } +} + +void PoOfstream::open(const OString& rFileName, OpenMode aMode ) +{ + assert( !isOpen() ); + if( aMode == TRUNC ) + { + m_aOutPut.open( rFileName.getStr(), + std::ios_base::out | std::ios_base::trunc ); + m_bIsAfterHeader = false; + } + else if( aMode == APP ) + { + m_aOutPut.open( rFileName.getStr(), + std::ios_base::out | std::ios_base::app ); + m_bIsAfterHeader = m_aOutPut.tellp() != std::ofstream::pos_type( 0 ); + } +} + +void PoOfstream::close() +{ + assert( isOpen() ); + m_aOutPut.close(); +} + +void PoOfstream::writeHeader(const PoHeader& rPoHeader) +{ + assert( isOpen() && !m_bIsAfterHeader && rPoHeader.m_bIsInitialized ); + rPoHeader.m_pGenPo->writeToFile( m_aOutPut ); + m_bIsAfterHeader = true; +} + +void PoOfstream::writeEntry( const PoEntry& rPoEntry ) +{ + assert( isOpen() && m_bIsAfterHeader && rPoEntry.m_bIsInitialized ); + rPoEntry.m_pGenPo->writeToFile( m_aOutPut ); +} + +namespace +{ + +// Check the validity of read entry +bool lcl_CheckInputEntry(const GenPoEntry& rEntry) +{ + // stock button labels don't have a reference/sourcefile - they are not extracted from ui files + // (explicitly skipped by solenv/bin/uiex) but instead inserted by l10ntools/source/localize.cxx + // into all module templates (see d5d905b480c2a9b1db982f2867e87b5c230d1ab9) + return !rEntry.getMsgCtxt().isEmpty() && + (rEntry.getMsgCtxt() == "stock" || !rEntry.getReference().empty()) && + !rEntry.getMsgId().isEmpty(); +} + +} + +PoIfstream::PoIfstream() + : m_bEof( false ) +{ +} + +PoIfstream::PoIfstream(const OString& rFileName) + : m_bEof( false ) +{ + open( rFileName ); +} + +PoIfstream::~PoIfstream() +{ + if( isOpen() ) + { + close(); + } +} + +void PoIfstream::open( const OString& rFileName, OString& rPoHeader ) +{ + assert( !isOpen() ); + m_aInPut.open( rFileName.getStr(), std::ios_base::in ); + + // capture header, updating timestamp and generator + std::string sTemp; + std::getline(m_aInPut,sTemp); + while( !sTemp.empty() && !m_aInPut.eof() ) + { + std::getline(m_aInPut,sTemp); + OString sLine(sTemp.data(),sTemp.length()); + if (sLine.startsWith("\"PO-Revision-Date")) + rPoHeader += "PO-Revision-Date: " + lcl_GetTime() + "\n"; + else if (sLine.startsWith("\"X-Generator")) + rPoHeader += "X-Generator: LibreOffice\n"; + else if (sLine.startsWith("\"")) + rPoHeader += lcl_GenNormString(sLine); + } + m_bEof = false; +} + +void PoIfstream::open( const OString& rFileName ) +{ + assert( !isOpen() ); + m_aInPut.open( rFileName.getStr(), std::ios_base::in ); + + // Skip header + std::string sTemp; + std::getline(m_aInPut,sTemp); + while( !sTemp.empty() && !m_aInPut.eof() ) + { + std::getline(m_aInPut,sTemp); + } + m_bEof = false; +} + +void PoIfstream::close() +{ + assert( isOpen() ); + m_aInPut.close(); +} + +void PoIfstream::readEntry( PoEntry& rPoEntry ) +{ + assert( isOpen() && !eof() ); + GenPoEntry aGenPo; + aGenPo.readFromFile( m_aInPut ); + if( aGenPo.isNull() ) + { + m_bEof = true; + rPoEntry = PoEntry(); + } + else + { + if( lcl_CheckInputEntry(aGenPo) ) + { + if( rPoEntry.m_pGenPo ) + { + *(rPoEntry.m_pGenPo) = aGenPo; + } + else + { + rPoEntry.m_pGenPo.reset( new GenPoEntry( aGenPo ) ); + } + rPoEntry.m_bIsInitialized = true; + } + else + { + SAL_WARN("l10ntools", "Parse problem with entry: " << aGenPo.getMsgStr()); + throw PoIfstream::Exception(); + } + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |