1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
10 #include <rtl/ustring.hxx>
11 #include <rtl/strbuf.hxx>
13 #include <sal/log.hxx>
21 #include <string_view>
26 /** Container of po entry
28 Provide all file operations related to LibreOffice specific
29 po entry and store it's attributes.
34 OStringBuffer m_sExtractCom
;
35 std::vector
<OString
> m_sReferences
;
38 OString m_sMsgIdPlural
;
40 std::vector
<OString
> m_sMsgStrPlural
;
48 const std::vector
<OString
>& getReference() const { return m_sReferences
; }
49 const OString
& getMsgCtxt() const { return m_sMsgCtxt
; }
50 const OString
& getMsgId() const { return m_sMsgId
; }
51 const OString
& getMsgStr() const { return m_sMsgStr
; }
52 bool isFuzzy() const { return m_bFuzzy
; }
53 bool isNull() const { return m_bNull
; }
55 void setExtractCom(std::string_view rExtractCom
)
57 m_sExtractCom
= rExtractCom
;
59 void setReference(const OString
& rReference
)
61 m_sReferences
.push_back(rReference
);
63 void setMsgCtxt(const OString
& rMsgCtxt
)
65 m_sMsgCtxt
= rMsgCtxt
;
67 void setMsgId(const OString
& rMsgId
)
71 void setMsgStr(const OString
& rMsgStr
)
76 void writeToFile(std::ofstream
& rOFStream
) const;
77 void readFromFile(std::ifstream
& rIFStream
);
82 // Convert a normal string to msg/po output string
83 OString
lcl_GenMsgString(std::string_view rString
)
85 if ( rString
.empty() )
90 helper::escapeAll(rString
,"\n""\t""\r""\\""\"","\\n""\\t""\\r""\\\\""\\\"") +
93 while((nIndex
=sResult
.indexOf("\\n",nIndex
))!=-1)
95 if( !sResult
.match("\\\\n", nIndex
-1) &&
96 nIndex
!=sResult
.getLength()-3)
98 sResult
= sResult
.replaceAt(nIndex
,2,"\\n\"\n\"");
103 if ( sResult
.indexOf('\n') != -1 )
104 return "\"\"\n" + sResult
;
109 // Convert msg string to normal form
110 OString
lcl_GenNormString(std::string_view rString
)
114 rString
.substr(1,rString
.size()-2),
115 "\\n""\\t""\\r""\\\\""\\\"",
116 "\n""\t""\r""\\""\"");
120 GenPoEntry::GenPoEntry()
122 , m_bCFormat( false )
127 void GenPoEntry::writeToFile(std::ofstream
& rOFStream
) const
129 if ( rOFStream
.tellp() != std::ofstream::pos_type( 0 ))
130 rOFStream
<< std::endl
;
131 if ( !m_sExtractCom
.isEmpty() )
134 << m_sExtractCom
.toString().replaceAll("\n"_ostr
,"\n#. "_ostr
) << std::endl
;
135 for(const auto& rReference
: m_sReferences
)
136 rOFStream
<< "#: " << rReference
<< std::endl
;
138 rOFStream
<< "#, fuzzy" << std::endl
;
140 rOFStream
<< "#, c-format" << std::endl
;
141 if ( !m_sMsgCtxt
.isEmpty() )
142 rOFStream
<< "msgctxt "
143 << lcl_GenMsgString(m_sMsgCtxt
)
145 rOFStream
<< "msgid "
146 << lcl_GenMsgString(m_sMsgId
) << std::endl
;
147 if ( !m_sMsgIdPlural
.isEmpty() )
148 rOFStream
<< "msgid_plural "
149 << lcl_GenMsgString(m_sMsgIdPlural
)
151 if ( !m_sMsgStrPlural
.empty() )
152 for(auto & line
: m_sMsgStrPlural
)
153 rOFStream
<< line
.copy(0,10) << lcl_GenMsgString(line
.subView(10)) << std::endl
;
155 rOFStream
<< "msgstr "
156 << lcl_GenMsgString(m_sMsgStr
) << std::endl
;
159 void GenPoEntry::readFromFile(std::ifstream
& rIFStream
)
161 *this = GenPoEntry();
162 OString
* pLastMsg
= nullptr;
164 getline(rIFStream
,sTemp
);
165 if( rIFStream
.eof() || sTemp
.empty() )
170 while(!rIFStream
.eof())
172 OString
sLine(sTemp
.data(),sTemp
.length());
173 if (sLine
.startsWith("#. "))
175 if( !m_sExtractCom
.isEmpty() )
177 m_sExtractCom
.append("\n");
179 m_sExtractCom
.append(sLine
.subView(3));
181 else if (sLine
.startsWith("#: "))
183 m_sReferences
.push_back(sLine
.copy(3));
185 else if (sLine
.startsWith("#, fuzzy"))
189 else if (sLine
.startsWith("#, c-format"))
193 else if (sLine
.startsWith("msgctxt "))
195 m_sMsgCtxt
= lcl_GenNormString(sLine
.subView(8));
196 pLastMsg
= &m_sMsgCtxt
;
198 else if (sLine
.startsWith("msgid "))
200 m_sMsgId
= lcl_GenNormString(sLine
.subView(6));
201 pLastMsg
= &m_sMsgId
;
203 else if (sLine
.startsWith("msgid_plural "))
205 m_sMsgIdPlural
= lcl_GenNormString(sLine
.subView(13));
206 pLastMsg
= &m_sMsgIdPlural
;
208 else if (sLine
.startsWith("msgstr "))
210 m_sMsgStr
= lcl_GenNormString(sLine
.subView(7));
211 pLastMsg
= &m_sMsgStr
;
213 else if (sLine
.startsWith("msgstr["))
215 // assume there are no more than 10 plural forms...
216 // and that plural strings are never split to multi-line in po
217 m_sMsgStrPlural
.push_back(sLine
.subView(0,10) + lcl_GenNormString(sLine
.subView(10)));
219 else if (sLine
.startsWith("\"") && pLastMsg
)
222 if (!m_sReferences
.empty())
224 sReference
= m_sReferences
.front();
226 if (pLastMsg
!= &m_sMsgCtxt
|| sLine
!= Concat2View("\"" + sReference
+ "\\n\""))
228 *pLastMsg
+= lcl_GenNormString(sLine
);
233 getline(rIFStream
,sTemp
);
238 : m_bIsInitialized( false )
243 std::string_view rSourceFile
, std::string_view rResType
, std::string_view rGroupId
,
244 std::string_view rLocalId
, std::string_view rHelpText
,
245 const OString
& rText
, const TYPE eType
)
246 : m_bIsInitialized( false )
248 if( rSourceFile
.empty() )
250 else if ( rResType
.empty() )
252 else if ( rGroupId
.empty() )
254 else if ( rText
.isEmpty() )
256 else if ( rHelpText
.size() == 5 )
259 m_pGenPo
.reset( new GenPoEntry() );
260 size_t idx
= rSourceFile
.rfind('/');
261 if (idx
== std::string_view::npos
)
263 OString
sReference(rSourceFile
.substr(idx
+1));
264 m_pGenPo
->setReference(sReference
);
269 (rLocalId
.empty() ? OString() : OString::Concat(rLocalId
) + "\n") +
273 sMsgCtxt
+= ".text"; break;
275 sMsgCtxt
+= ".quickhelptext"; break;
277 sMsgCtxt
+= ".title"; break;
278 // Default case is unneeded because the type of eType has only three element
280 m_pGenPo
->setMsgCtxt(sMsgCtxt
);
281 m_pGenPo
->setMsgId(rText
);
282 m_pGenPo
->setExtractCom(Concat2View(
283 ( !rHelpText
.empty() ? OString::Concat(rHelpText
) + "\n" : OString()) +
284 genKeyId( m_pGenPo
->getReference().front() + rGroupId
+ rLocalId
+ rResType
+ rText
) ));
285 m_bIsInitialized
= true;
292 PoEntry::PoEntry( const PoEntry
& rPo
)
293 : m_pGenPo( rPo
.m_pGenPo
? new GenPoEntry( *(rPo
.m_pGenPo
) ) : nullptr )
294 , m_bIsInitialized( rPo
.m_bIsInitialized
)
298 PoEntry
& PoEntry::operator=(const PoEntry
& rPo
)
308 *m_pGenPo
= *(rPo
.m_pGenPo
);
312 m_pGenPo
.reset( new GenPoEntry( *(rPo
.m_pGenPo
) ) );
319 m_bIsInitialized
= rPo
.m_bIsInitialized
;
323 PoEntry
& PoEntry::operator=(PoEntry
&& rPo
) noexcept
325 m_pGenPo
= std::move(rPo
.m_pGenPo
);
326 m_bIsInitialized
= std::move(rPo
.m_bIsInitialized
);
330 OString
const & PoEntry::getSourceFile() const
332 assert( m_bIsInitialized
);
333 return m_pGenPo
->getReference().front();
336 OString
PoEntry::getGroupId() const
338 assert( m_bIsInitialized
);
339 return m_pGenPo
->getMsgCtxt().getToken(0,'\n');
342 OString
PoEntry::getLocalId() const
344 assert( m_bIsInitialized
);
345 const OString sMsgCtxt
= m_pGenPo
->getMsgCtxt();
346 if (sMsgCtxt
.indexOf('\n')==sMsgCtxt
.lastIndexOf('\n'))
349 return sMsgCtxt
.getToken(1,'\n');
352 OString
PoEntry::getResourceType() const
354 assert( m_bIsInitialized
);
355 const OString sMsgCtxt
= m_pGenPo
->getMsgCtxt();
356 if (sMsgCtxt
.indexOf('\n')==sMsgCtxt
.lastIndexOf('\n'))
357 return sMsgCtxt
.getToken(1,'\n').getToken(0,'.');
359 return sMsgCtxt
.getToken(2,'\n').getToken(0,'.');
362 PoEntry::TYPE
PoEntry::getType() const
364 assert( m_bIsInitialized
);
365 const OString sMsgCtxt
= m_pGenPo
->getMsgCtxt();
366 const OString sType
= sMsgCtxt
.copy( sMsgCtxt
.lastIndexOf('.') + 1 );
368 (sType
== "text" || sType
== "quickhelptext" || sType
== "title") );
369 if ( sType
== "text" )
371 else if ( sType
== "quickhelptext" )
372 return TQUICKHELPTEXT
;
377 bool PoEntry::isFuzzy() const
379 assert( m_bIsInitialized
);
380 return m_pGenPo
->isFuzzy();
383 // Get message context
384 const OString
& PoEntry::getMsgCtxt() const
386 assert( m_bIsInitialized
);
387 return m_pGenPo
->getMsgCtxt();
391 // Get translation string in merge format
392 OString
const & PoEntry::getMsgId() const
394 assert( m_bIsInitialized
);
395 return m_pGenPo
->getMsgId();
398 // Get translated string in merge format
399 const OString
& PoEntry::getMsgStr() const
401 assert( m_bIsInitialized
);
402 return m_pGenPo
->getMsgStr();
406 bool PoEntry::IsInSameComp(const PoEntry
& rPo1
,const PoEntry
& rPo2
)
408 assert( rPo1
.m_bIsInitialized
&& rPo2
.m_bIsInitialized
);
409 return ( rPo1
.getSourceFile() == rPo2
.getSourceFile() &&
410 rPo1
.getGroupId() == rPo2
.getGroupId() &&
411 rPo1
.getLocalId() == rPo2
.getLocalId() &&
412 rPo1
.getResourceType() == rPo2
.getResourceType() );
415 OString
PoEntry::genKeyId(const OString
& rGenerator
)
417 sal_uInt32 nCRC
= rtl_crc32(0, rGenerator
.getStr(), rGenerator
.getLength());
418 // Use simple ASCII characters, exclude I, l, 1 and O, 0 to avoid confusing IDs
419 static const char sSymbols
[] =
420 "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
422 for( short nKeyInd
= 0; nKeyInd
< 5; ++nKeyInd
)
424 sKeyId
[nKeyInd
] = sSymbols
[(nCRC
& 63) % strlen(sSymbols
)];
433 // Get actual time in "YEAR-MO-DA HO:MI+ZONE" form
434 OString
lcl_GetTime()
436 time_t aNow
= time(nullptr);
437 struct tm
* pNow
= localtime(&aNow
);
439 strftime( pBuff
, sizeof pBuff
, "%Y-%m-%d %H:%M%z", pNow
);
444 // when updating existing files (pocheck), reuse provided po-header
445 PoHeader::PoHeader( std::string_view rExtSrc
, const OString
& rPoHeaderMsgStr
)
446 : m_pGenPo( new GenPoEntry() )
447 , m_bIsInitialized( false )
449 m_pGenPo
->setExtractCom(Concat2View(OString::Concat("extracted from ") + rExtSrc
));
450 m_pGenPo
->setMsgStr(rPoHeaderMsgStr
);
451 m_bIsInitialized
= true;
454 PoHeader::PoHeader( std::string_view rExtSrc
)
455 : m_pGenPo( new GenPoEntry() )
456 , m_bIsInitialized( false )
458 m_pGenPo
->setExtractCom(Concat2View(OString::Concat("extracted from ") + rExtSrc
));
460 "Project-Id-Version: PACKAGE VERSION\n"
461 "Report-Msgid-Bugs-To: https://bugs.libreoffice.org/enter_bug.cgi?"
462 "product=LibreOffice&bug_status=UNCONFIRMED&component=UI\n"
463 "POT-Creation-Date: " + lcl_GetTime() +
464 "\nPO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
465 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
466 "Language-Team: LANGUAGE <LL@li.org>\n"
467 "MIME-Version: 1.0\n"
468 "Content-Type: text/plain; charset=UTF-8\n"
469 "Content-Transfer-Encoding: 8bit\n"
470 "X-Accelerator-Marker: ~\n"
471 "X-Generator: LibreOffice\n");
472 m_bIsInitialized
= true;
475 PoHeader::~PoHeader()
479 PoOfstream::PoOfstream()
480 : m_bIsAfterHeader( false )
484 PoOfstream::PoOfstream(const OString
& rFileName
, OpenMode aMode
)
485 : m_bIsAfterHeader( false )
487 open( rFileName
, aMode
);
490 PoOfstream::~PoOfstream()
498 void PoOfstream::open(const OString
& rFileName
, OpenMode aMode
)
503 m_aOutPut
.open( rFileName
.getStr(),
504 std::ios_base::out
| std::ios_base::trunc
);
505 m_bIsAfterHeader
= false;
507 else if( aMode
== APP
)
509 m_aOutPut
.open( rFileName
.getStr(),
510 std::ios_base::out
| std::ios_base::app
);
511 m_bIsAfterHeader
= m_aOutPut
.tellp() != std::ofstream::pos_type( 0 );
515 void PoOfstream::close()
521 void PoOfstream::writeHeader(const PoHeader
& rPoHeader
)
523 assert( isOpen() && !m_bIsAfterHeader
&& rPoHeader
.m_bIsInitialized
);
524 rPoHeader
.m_pGenPo
->writeToFile( m_aOutPut
);
525 m_bIsAfterHeader
= true;
528 void PoOfstream::writeEntry( const PoEntry
& rPoEntry
)
530 assert( isOpen() && m_bIsAfterHeader
&& rPoEntry
.m_bIsInitialized
);
531 rPoEntry
.m_pGenPo
->writeToFile( m_aOutPut
);
537 // Check the validity of read entry
538 bool lcl_CheckInputEntry(const GenPoEntry
& rEntry
)
540 // stock button labels don't have a reference/sourcefile - they are not extracted from ui files
541 // (explicitly skipped by solenv/bin/uiex) but instead inserted by l10ntools/source/localize.cxx
542 // into all module templates (see d5d905b480c2a9b1db982f2867e87b5c230d1ab9)
543 return !rEntry
.getMsgCtxt().isEmpty() &&
544 (rEntry
.getMsgCtxt() == "stock" || !rEntry
.getReference().empty()) &&
545 !rEntry
.getMsgId().isEmpty();
550 PoIfstream::PoIfstream()
555 PoIfstream::PoIfstream(const OString
& rFileName
)
561 PoIfstream::~PoIfstream()
569 void PoIfstream::open( const OString
& rFileName
, OString
& rPoHeader
)
572 m_aInPut
.open( rFileName
.getStr(), std::ios_base::in
);
574 // capture header, updating timestamp and generator
576 std::getline(m_aInPut
,sTemp
);
577 while( !sTemp
.empty() && !m_aInPut
.eof() )
579 std::getline(m_aInPut
,sTemp
);
580 OString
sLine(sTemp
.data(),sTemp
.length());
581 if (sLine
.startsWith("\"PO-Revision-Date"))
582 rPoHeader
+= "PO-Revision-Date: " + lcl_GetTime() + "\n";
583 else if (sLine
.startsWith("\"X-Generator"))
584 rPoHeader
+= "X-Generator: LibreOffice\n";
585 else if (sLine
.startsWith("\""))
586 rPoHeader
+= lcl_GenNormString(sLine
);
591 void PoIfstream::open( const OString
& rFileName
)
594 m_aInPut
.open( rFileName
.getStr(), std::ios_base::in
);
598 std::getline(m_aInPut
,sTemp
);
599 while( !sTemp
.empty() && !m_aInPut
.eof() )
601 std::getline(m_aInPut
,sTemp
);
606 void PoIfstream::close()
612 void PoIfstream::readEntry( PoEntry
& rPoEntry
)
614 assert( isOpen() && !eof() );
616 aGenPo
.readFromFile( m_aInPut
);
617 if( aGenPo
.isNull() )
620 rPoEntry
= PoEntry();
624 if( lcl_CheckInputEntry(aGenPo
) )
626 if( rPoEntry
.m_pGenPo
)
628 *(rPoEntry
.m_pGenPo
) = std::move(aGenPo
);
632 rPoEntry
.m_pGenPo
.reset( new GenPoEntry(std::move(aGenPo
)) );
634 rPoEntry
.m_bIsInitialized
= true;
638 SAL_WARN("l10ntools", "Parse problem with entry: " << aGenPo
.getMsgStr());
639 throw PoIfstream::Exception();
644 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */