1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
4 *
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/.
8 */
9
10 #include <rtl/ustring.hxx>
11 #include <rtl/crc.h>
12 #include <sal/log.hxx>
13
14 #include <cstring>
15 #include <ctime>
16 #include <cassert>
17
18 #include <vector>
19 #include <string>
20
21 #include <po.hxx>
22 #include <helper.hxx>
23
24 /** Container of po entry
25
26 Provide all file operations related to LibreOffice specific
27 po entry and store it's attributes.
28 */
29 class GenPoEntry
30 {
31 private:
32 OStringBuffer m_sExtractCom;
33 std::vector<OString> m_sReferences;
34 OString m_sMsgCtxt;
35 OString m_sMsgId;
36 OString m_sMsgIdPlural;
37 OString m_sMsgStr;
38 std::vector<OString> m_sMsgStrPlural;
39 bool m_bFuzzy;
40 bool m_bCFormat;
41 bool m_bNull;
42
43 public:
44 GenPoEntry();
45
getReference() const46 const std::vector<OString>& getReference() const { return m_sReferences; }
getMsgCtxt() const47 const OString& getMsgCtxt() const { return m_sMsgCtxt; }
getMsgId() const48 const OString& getMsgId() const { return m_sMsgId; }
getMsgStr() const49 const OString& getMsgStr() const { return m_sMsgStr; }
isFuzzy() const50 bool isFuzzy() const { return m_bFuzzy; }
isNull() const51 bool isNull() const { return m_bNull; }
52
setExtractCom(const OString & rExtractCom)53 void setExtractCom(const OString& rExtractCom)
54 {
55 m_sExtractCom = rExtractCom;
56 }
setReference(const OString & rReference)57 void setReference(const OString& rReference)
58 {
59 m_sReferences.push_back(rReference);
60 }
setMsgCtxt(const OString & rMsgCtxt)61 void setMsgCtxt(const OString& rMsgCtxt)
62 {
63 m_sMsgCtxt = rMsgCtxt;
64 }
setMsgId(const OString & rMsgId)65 void setMsgId(const OString& rMsgId)
66 {
67 m_sMsgId = rMsgId;
68 }
setMsgStr(const OString & rMsgStr)69 void setMsgStr(const OString& rMsgStr)
70 {
71 m_sMsgStr = rMsgStr;
72 }
73
74 void writeToFile(std::ofstream& rOFStream) const;
75 void readFromFile(std::ifstream& rIFStream);
76 };
77
78 namespace
79 {
80 // Convert a normal string to msg/po output string
lcl_GenMsgString(const OString & rString)81 OString lcl_GenMsgString(const OString& rString)
82 {
83 if ( rString.isEmpty() )
84 return "\"\"";
85
86 OString sResult =
87 "\"" +
88 helper::escapeAll(rString,"\n""\t""\r""\\""\"","\\n""\\t""\\r""\\\\""\\\"") +
89 "\"";
90 sal_Int32 nIndex = 0;
91 while((nIndex=sResult.indexOf("\\n",nIndex))!=-1)
92 {
93 if( !sResult.match("\\\\n", nIndex-1) &&
94 nIndex!=sResult.getLength()-3)
95 {
96 sResult = sResult.replaceAt(nIndex,2,"\\n\"\n\"");
97 }
98 ++nIndex;
99 }
100
101 if ( sResult.indexOf('\n') != -1 )
102 return "\"\"\n" + sResult;
103
104 return sResult;
105 }
106
107 // Convert msg string to normal form
lcl_GenNormString(const OString & rString)108 OString lcl_GenNormString(const OString& rString)
109 {
110 return
111 helper::unEscapeAll(
112 rString.copy(1,rString.getLength()-2),
113 "\\n""\\t""\\r""\\\\""\\\"",
114 "\n""\t""\r""\\""\"");
115 }
116 }
117
GenPoEntry()118 GenPoEntry::GenPoEntry()
119 : m_sExtractCom( OString() )
120 , m_sReferences( std::vector<OString>() )
121 , m_sMsgCtxt( OString() )
122 , m_sMsgId( OString() )
123 , m_sMsgIdPlural( OString() )
124 , m_sMsgStr( OString() )
125 , m_sMsgStrPlural( std::vector<OString>() )
126 , m_bFuzzy( false )
127 , m_bCFormat( false )
128 , m_bNull( false )
129 {
130 }
131
writeToFile(std::ofstream & rOFStream) const132 void GenPoEntry::writeToFile(std::ofstream& rOFStream) const
133 {
134 if ( rOFStream.tellp() != std::ofstream::pos_type( 0 ))
135 rOFStream << std::endl;
136 if ( !m_sExtractCom.isEmpty() )
137 rOFStream
138 << "#. "
139 << m_sExtractCom.toString().replaceAll("\n","\n#. ") << std::endl;
140 for(const auto& rReference : m_sReferences)
141 rOFStream << "#: " << rReference << std::endl;
142 if ( m_bFuzzy )
143 rOFStream << "#, fuzzy" << std::endl;
144 if ( m_bCFormat )
145 rOFStream << "#, c-format" << std::endl;
146 if ( !m_sMsgCtxt.isEmpty() )
147 rOFStream << "msgctxt "
148 << lcl_GenMsgString(m_sMsgCtxt)
149 << std::endl;
150 rOFStream << "msgid "
151 << lcl_GenMsgString(m_sMsgId) << std::endl;
152 if ( !m_sMsgIdPlural.isEmpty() )
153 rOFStream << "msgid_plural "
154 << lcl_GenMsgString(m_sMsgIdPlural)
155 << std::endl;
156 if ( !m_sMsgStrPlural.empty() )
157 for(auto & line : m_sMsgStrPlural)
158 rOFStream << line.copy(0,10) << lcl_GenMsgString(line.copy(10)) << std::endl;
159 else
160 rOFStream << "msgstr "
161 << lcl_GenMsgString(m_sMsgStr) << std::endl;
162 }
163
readFromFile(std::ifstream & rIFStream)164 void GenPoEntry::readFromFile(std::ifstream& rIFStream)
165 {
166 *this = GenPoEntry();
167 OString* pLastMsg = nullptr;
168 std::string sTemp;
169 getline(rIFStream,sTemp);
170 if( rIFStream.eof() || sTemp.empty() )
171 {
172 m_bNull = true;
173 return;
174 }
175 while(!rIFStream.eof())
176 {
177 OString sLine(sTemp.data(),sTemp.length());
178 if (sLine.startsWith("#. "))
179 {
180 if( !m_sExtractCom.isEmpty() )
181 {
182 m_sExtractCom.append("\n");
183 }
184 m_sExtractCom.append(sLine.copy(3));
185 }
186 else if (sLine.startsWith("#: "))
187 {
188 m_sReferences.push_back(sLine.copy(3));
189 }
190 else if (sLine.startsWith("#, fuzzy"))
191 {
192 m_bFuzzy = true;
193 }
194 else if (sLine.startsWith("#, c-format"))
195 {
196 m_bCFormat = true;
197 }
198 else if (sLine.startsWith("msgctxt "))
199 {
200 m_sMsgCtxt = lcl_GenNormString(sLine.copy(8));
201 pLastMsg = &m_sMsgCtxt;
202 }
203 else if (sLine.startsWith("msgid "))
204 {
205 m_sMsgId = lcl_GenNormString(sLine.copy(6));
206 pLastMsg = &m_sMsgId;
207 }
208 else if (sLine.startsWith("msgid_plural "))
209 {
210 m_sMsgIdPlural = lcl_GenNormString(sLine.copy(13));
211 pLastMsg = &m_sMsgIdPlural;
212 }
213 else if (sLine.startsWith("msgstr "))
214 {
215 m_sMsgStr = lcl_GenNormString(sLine.copy(7));
216 pLastMsg = &m_sMsgStr;
217 }
218 else if (sLine.startsWith("msgstr["))
219 {
220 // assume there are no more than 10 plural forms...
221 // and that plural strings are never split to multi-line in po
222 m_sMsgStrPlural.push_back(sLine.copy(0,10) + lcl_GenNormString(sLine.copy(10)));
223 }
224 else if (sLine.startsWith("\"") && pLastMsg)
225 {
226 OString sReference;
227 if (!m_sReferences.empty())
228 {
229 sReference = m_sReferences.front();
230 }
231 if (pLastMsg != &m_sMsgCtxt || sLine != "\"" + sReference + "\\n\"")
232 {
233 *pLastMsg += lcl_GenNormString(sLine);
234 }
235 }
236 else
237 break;
238 getline(rIFStream,sTemp);
239 }
240 }
241
PoEntry()242 PoEntry::PoEntry()
243 : m_bIsInitialized( false )
244 {
245 }
246
PoEntry(const OString & rSourceFile,const OString & rResType,const OString & rGroupId,const OString & rLocalId,const OString & rHelpText,const OString & rText,const TYPE eType)247 PoEntry::PoEntry(
248 const OString& rSourceFile, const OString& rResType, const OString& rGroupId,
249 const OString& rLocalId, const OString& rHelpText,
250 const OString& rText, const TYPE eType )
251 : m_bIsInitialized( false )
252 {
253 if( rSourceFile.isEmpty() )
254 throw NOSOURCFILE;
255 else if ( rResType.isEmpty() )
256 throw NORESTYPE;
257 else if ( rGroupId.isEmpty() )
258 throw NOGROUPID;
259 else if ( rText.isEmpty() )
260 throw NOSTRING;
261 else if ( rHelpText.getLength() == 5 )
262 throw WRONGHELPTEXT;
263
264 m_pGenPo.reset( new GenPoEntry() );
265 OString sReference = rSourceFile.copy(rSourceFile.lastIndexOf('/')+1);
266 m_pGenPo->setReference(sReference);
267
268 OString sMsgCtxt =
269 sReference + "\n" +
270 rGroupId + "\n" +
271 (rLocalId.isEmpty() ? OString() : rLocalId + "\n") +
272 rResType;
273 switch(eType){
274 case TTEXT:
275 sMsgCtxt += ".text"; break;
276 case TQUICKHELPTEXT:
277 sMsgCtxt += ".quickhelptext"; break;
278 case TTITLE:
279 sMsgCtxt += ".title"; break;
280 // Default case is unneeded because the type of eType has only three element
281 }
282 m_pGenPo->setMsgCtxt(sMsgCtxt);
283 m_pGenPo->setMsgId(rText);
284 m_pGenPo->setExtractCom(
285 ( !rHelpText.isEmpty() ? rHelpText + "\n" : OString()) +
286 genKeyId( m_pGenPo->getReference().front() + rGroupId + rLocalId + rResType + rText ) );
287 m_bIsInitialized = true;
288 }
289
~PoEntry()290 PoEntry::~PoEntry()
291 {
292 }
293
PoEntry(const PoEntry & rPo)294 PoEntry::PoEntry( const PoEntry& rPo )
295 : m_pGenPo( rPo.m_pGenPo ? new GenPoEntry( *(rPo.m_pGenPo) ) : nullptr )
296 , m_bIsInitialized( rPo.m_bIsInitialized )
297 {
298 }
299
operator =(const PoEntry & rPo)300 PoEntry& PoEntry::operator=(const PoEntry& rPo)
301 {
302 if( this == &rPo )
303 {
304 return *this;
305 }
306 if( rPo.m_pGenPo )
307 {
308 if( m_pGenPo )
309 {
310 *m_pGenPo = *(rPo.m_pGenPo);
311 }
312 else
313 {
314 m_pGenPo.reset( new GenPoEntry( *(rPo.m_pGenPo) ) );
315 }
316 }
317 else
318 {
319 m_pGenPo.reset();
320 }
321 m_bIsInitialized = rPo.m_bIsInitialized;
322 return *this;
323 }
324
operator =(PoEntry && rPo)325 PoEntry& PoEntry::operator=(PoEntry&& rPo) noexcept
326 {
327 m_pGenPo = std::move(rPo.m_pGenPo);
328 m_bIsInitialized = std::move(rPo.m_bIsInitialized);
329 return *this;
330 }
331
getSourceFile() const332 OString const & PoEntry::getSourceFile() const
333 {
334 assert( m_bIsInitialized );
335 return m_pGenPo->getReference().front();
336 }
337
getGroupId() const338 OString PoEntry::getGroupId() const
339 {
340 assert( m_bIsInitialized );
341 return m_pGenPo->getMsgCtxt().getToken(0,'\n');
342 }
343
getLocalId() const344 OString PoEntry::getLocalId() const
345 {
346 assert( m_bIsInitialized );
347 const OString sMsgCtxt = m_pGenPo->getMsgCtxt();
348 if (sMsgCtxt.indexOf('\n')==sMsgCtxt.lastIndexOf('\n'))
349 return OString();
350 else
351 return sMsgCtxt.getToken(1,'\n');
352 }
353
getResourceType() const354 OString PoEntry::getResourceType() const
355 {
356 assert( m_bIsInitialized );
357 const OString sMsgCtxt = m_pGenPo->getMsgCtxt();
358 if (sMsgCtxt.indexOf('\n')==sMsgCtxt.lastIndexOf('\n'))
359 return sMsgCtxt.getToken(1,'\n').getToken(0,'.');
360 else
361 return sMsgCtxt.getToken(2,'\n').getToken(0,'.');
362 }
363
getType() const364 PoEntry::TYPE PoEntry::getType() const
365 {
366 assert( m_bIsInitialized );
367 const OString sMsgCtxt = m_pGenPo->getMsgCtxt();
368 const OString sType = sMsgCtxt.copy( sMsgCtxt.lastIndexOf('.') + 1 );
369 assert(
370 (sType == "text" || sType == "quickhelptext" || sType == "title") );
371 if ( sType == "text" )
372 return TTEXT;
373 else if ( sType == "quickhelptext" )
374 return TQUICKHELPTEXT;
375 else
376 return TTITLE;
377 }
378
isFuzzy() const379 bool PoEntry::isFuzzy() const
380 {
381 assert( m_bIsInitialized );
382 return m_pGenPo->isFuzzy();
383 }
384
385 // Get message context
getMsgCtxt() const386 const OString& PoEntry::getMsgCtxt() const
387 {
388 assert( m_bIsInitialized );
389 return m_pGenPo->getMsgCtxt();
390
391 }
392
393 // Get translation string in merge format
getMsgId() const394 OString const & PoEntry::getMsgId() const
395 {
396 assert( m_bIsInitialized );
397 return m_pGenPo->getMsgId();
398 }
399
400 // Get translated string in merge format
getMsgStr() const401 const OString& PoEntry::getMsgStr() const
402 {
403 assert( m_bIsInitialized );
404 return m_pGenPo->getMsgStr();
405
406 }
407
IsInSameComp(const PoEntry & rPo1,const PoEntry & rPo2)408 bool PoEntry::IsInSameComp(const PoEntry& rPo1,const PoEntry& rPo2)
409 {
410 assert( rPo1.m_bIsInitialized && rPo2.m_bIsInitialized );
411 return ( rPo1.getSourceFile() == rPo2.getSourceFile() &&
412 rPo1.getGroupId() == rPo2.getGroupId() &&
413 rPo1.getLocalId() == rPo2.getLocalId() &&
414 rPo1.getResourceType() == rPo2.getResourceType() );
415 }
416
genKeyId(const OString & rGenerator)417 OString PoEntry::genKeyId(const OString& rGenerator)
418 {
419 sal_uInt32 nCRC = rtl_crc32(0, rGenerator.getStr(), rGenerator.getLength());
420 // Use simple ASCII characters, exclude I, l, 1 and O, 0 to avoid confusing IDs
421 static const char sSymbols[] =
422 "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
423 char sKeyId[6];
424 for( short nKeyInd = 0; nKeyInd < 5; ++nKeyInd )
425 {
426 sKeyId[nKeyInd] = sSymbols[(nCRC & 63) % strlen(sSymbols)];
427 nCRC >>= 6;
428 }
429 sKeyId[5] = '\0';
430 return sKeyId;
431 }
432
433 namespace
434 {
435 // Get actual time in "YEAR-MO-DA HO:MI+ZONE" form
lcl_GetTime()436 OString lcl_GetTime()
437 {
438 time_t aNow = time(nullptr);
439 struct tm* pNow = localtime(&aNow);
440 char pBuff[50];
441 strftime( pBuff, sizeof pBuff, "%Y-%m-%d %H:%M%z", pNow );
442 return pBuff;
443 }
444 }
445
446 // when updating existing files (pocheck), reuse provided po-header
PoHeader(const OString & rExtSrc,const OString & rPoHeaderMsgStr)447 PoHeader::PoHeader( const OString& rExtSrc, const OString& rPoHeaderMsgStr )
448 : m_pGenPo( new GenPoEntry() )
449 , m_bIsInitialized( false )
450 {
451 m_pGenPo->setExtractCom("extracted from " + rExtSrc);
452 m_pGenPo->setMsgStr(rPoHeaderMsgStr);
453 m_bIsInitialized = true;
454 }
455
PoHeader(const OString & rExtSrc)456 PoHeader::PoHeader( const OString& rExtSrc )
457 : m_pGenPo( new GenPoEntry() )
458 , m_bIsInitialized( false )
459 {
460 m_pGenPo->setExtractCom("extracted from " + rExtSrc);
461 m_pGenPo->setMsgStr(
462 "Project-Id-Version: PACKAGE VERSION\n"
463 "Report-Msgid-Bugs-To: https://bugs.libreoffice.org/enter_bug.cgi?"
464 "product=LibreOffice&bug_status=UNCONFIRMED&component=UI\n"
465 "POT-Creation-Date: " + lcl_GetTime() +
466 "\nPO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
467 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
468 "Language-Team: LANGUAGE <LL@li.org>\n"
469 "MIME-Version: 1.0\n"
470 "Content-Type: text/plain; charset=UTF-8\n"
471 "Content-Transfer-Encoding: 8bit\n"
472 "X-Accelerator-Marker: ~\n"
473 "X-Generator: LibreOffice\n");
474 m_bIsInitialized = true;
475 }
476
~PoHeader()477 PoHeader::~PoHeader()
478 {
479 }
480
PoOfstream()481 PoOfstream::PoOfstream()
482 : m_aOutPut()
483 , m_bIsAfterHeader( false )
484 {
485 }
486
PoOfstream(const OString & rFileName,OpenMode aMode)487 PoOfstream::PoOfstream(const OString& rFileName, OpenMode aMode )
488 : m_aOutPut()
489 , m_bIsAfterHeader( false )
490 {
491 open( rFileName, aMode );
492 }
493
~PoOfstream()494 PoOfstream::~PoOfstream()
495 {
496 if( isOpen() )
497 {
498 close();
499 }
500 }
501
open(const OString & rFileName,OpenMode aMode)502 void PoOfstream::open(const OString& rFileName, OpenMode aMode )
503 {
504 assert( !isOpen() );
505 if( aMode == TRUNC )
506 {
507 m_aOutPut.open( rFileName.getStr(),
508 std::ios_base::out | std::ios_base::trunc );
509 m_bIsAfterHeader = false;
510 }
511 else if( aMode == APP )
512 {
513 m_aOutPut.open( rFileName.getStr(),
514 std::ios_base::out | std::ios_base::app );
515 m_bIsAfterHeader = m_aOutPut.tellp() != std::ofstream::pos_type( 0 );
516 }
517 }
518
close()519 void PoOfstream::close()
520 {
521 assert( isOpen() );
522 m_aOutPut.close();
523 }
524
writeHeader(const PoHeader & rPoHeader)525 void PoOfstream::writeHeader(const PoHeader& rPoHeader)
526 {
527 assert( isOpen() && !m_bIsAfterHeader && rPoHeader.m_bIsInitialized );
528 rPoHeader.m_pGenPo->writeToFile( m_aOutPut );
529 m_bIsAfterHeader = true;
530 }
531
writeEntry(const PoEntry & rPoEntry)532 void PoOfstream::writeEntry( const PoEntry& rPoEntry )
533 {
534 assert( isOpen() && m_bIsAfterHeader && rPoEntry.m_bIsInitialized );
535 rPoEntry.m_pGenPo->writeToFile( m_aOutPut );
536 }
537
538 namespace
539 {
540
541 // Check the validity of read entry
lcl_CheckInputEntry(const GenPoEntry & rEntry)542 bool lcl_CheckInputEntry(const GenPoEntry& rEntry)
543 {
544 return !rEntry.getReference().empty() &&
545 !rEntry.getMsgCtxt().isEmpty() &&
546 !rEntry.getMsgId().isEmpty();
547 }
548
549 }
550
PoIfstream()551 PoIfstream::PoIfstream()
552 : m_aInPut()
553 , m_bEof( false )
554 {
555 }
556
PoIfstream(const OString & rFileName)557 PoIfstream::PoIfstream(const OString& rFileName)
558 : m_aInPut()
559 , m_bEof( false )
560 {
561 open( rFileName );
562 }
563
~PoIfstream()564 PoIfstream::~PoIfstream()
565 {
566 if( isOpen() )
567 {
568 close();
569 }
570 }
571
open(const OString & rFileName,OString & rPoHeader)572 void PoIfstream::open( const OString& rFileName, OString& rPoHeader )
573 {
574 assert( !isOpen() );
575 m_aInPut.open( rFileName.getStr(), std::ios_base::in );
576
577 // capture header, updating timestamp and generator
578 std::string sTemp;
579 std::getline(m_aInPut,sTemp);
580 while( !sTemp.empty() && !m_aInPut.eof() )
581 {
582 std::getline(m_aInPut,sTemp);
583 OString sLine(sTemp.data(),sTemp.length());
584 if (sLine.startsWith("\"PO-Revision-Date"))
585 rPoHeader += "PO-Revision-Date: " + lcl_GetTime() + "\n";
586 else if (sLine.startsWith("\"X-Generator"))
587 rPoHeader += "X-Generator: LibreOffice\n";
588 else if (sLine.startsWith("\""))
589 rPoHeader += lcl_GenNormString(sLine);
590 }
591 m_bEof = false;
592 }
593
open(const OString & rFileName)594 void PoIfstream::open( const OString& rFileName )
595 {
596 assert( !isOpen() );
597 m_aInPut.open( rFileName.getStr(), std::ios_base::in );
598
599 // Skip header
600 std::string sTemp;
601 std::getline(m_aInPut,sTemp);
602 while( !sTemp.empty() && !m_aInPut.eof() )
603 {
604 std::getline(m_aInPut,sTemp);
605 }
606 m_bEof = false;
607 }
608
close()609 void PoIfstream::close()
610 {
611 assert( isOpen() );
612 m_aInPut.close();
613 }
614
readEntry(PoEntry & rPoEntry)615 void PoIfstream::readEntry( PoEntry& rPoEntry )
616 {
617 assert( isOpen() && !eof() );
618 GenPoEntry aGenPo;
619 aGenPo.readFromFile( m_aInPut );
620 if( aGenPo.isNull() )
621 {
622 m_bEof = true;
623 rPoEntry = PoEntry();
624 }
625 else
626 {
627 if( lcl_CheckInputEntry(aGenPo) )
628 {
629 if( rPoEntry.m_pGenPo )
630 {
631 *(rPoEntry.m_pGenPo) = aGenPo;
632 }
633 else
634 {
635 rPoEntry.m_pGenPo.reset( new GenPoEntry( aGenPo ) );
636 }
637 rPoEntry.m_bIsInitialized = true;
638 }
639 else
640 {
641 SAL_WARN("l10ntools", "Parse problem with entry: " << aGenPo.getMsgStr());
642 throw PoIfstream::Exception();
643 }
644 }
645 }
646
647 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
648