tsstorage.cpp 16.5 KB
Newer Older
1
/*
2
Copyright 2008-2014 Nick Shaforostoff <shaforostoff@kde.ru>
Simon Depiets's avatar
Simon Depiets committed
3
                2018-2019 by Simon Depiets <sdepiets@gmail.com>
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

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) version 3 or any later version
accepted by the membership of KDE e.V. (or its successor approved
by the membership of KDE e.V.), which shall act as a proxy
defined in Section 14 of version 3 of the license.

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, see <http://www.gnu.org/licenses/>.
*/

#include "tsstorage.h"

Luigi Toscano's avatar
Luigi Toscano committed
24 25
#include "lokalize_debug.h"

26 27 28 29 30 31 32 33 34
#include "gettextheader.h"
#include "project.h"
#include "version.h"
#include "prefs_lokalize.h"

#include <QProcess>
#include <QString>
#include <QMap>
#include <QDomDocument>
35
#include <QElapsedTimer>
36 37 38 39
#include <QPair>
#include <QList>
#include <QXmlSimpleReader>

40 41
#include <klocalizedstring.h>

Nick Shaforostoff's avatar
Nick Shaforostoff committed
42 43 44 45 46
#ifdef Q_OS_WIN
#define U QLatin1String
#else
#define U QStringLiteral
#endif
47

Nick Shaforostoff's avatar
Nick Shaforostoff committed
48
//static const char* const noyes[]={"no","yes"};
49

Simon Depiets's avatar
Simon Depiets committed
50 51
static const QString names[] = {U("source"), U("translation"), U("oldsource"), U("translatorcomment"), U("comment"), U("name"), U("numerus")};
enum TagNames                {SourceTag, TargetTag, OldSourceTag, NoteTag, DevNoteTag, NameTag, PluralTag};
52

Simon Depiets's avatar
Simon Depiets committed
53 54
static const QString attrnames[] = {U("location"), U("type"), U("obsolete")};
enum AttrNames                   {LocationAttr, TypeAttr, ObsoleteAttr};
Nick Shaforostoff's avatar
Nick Shaforostoff committed
55

Simon Depiets's avatar
Simon Depiets committed
56 57
static const QString attrvalues[] = {U("obsolete"), U("vanished")};
enum AttValues                    {ObsoleteVal, VanishedVal};
58 59

TsStorage::TsStorage()
Simon Depiets's avatar
Simon Depiets committed
60
    : CatalogStorage()
61 62 63 64 65 66 67 68 69 70 71 72
{
}

int TsStorage::capabilities() const
{
    return 0;//MultipleNotes;
}

//BEGIN OPEN/SAVE

int TsStorage::load(QIODevice* device)
{
73
    QElapsedTimer chrono; chrono.start();
74 75 76


    QXmlSimpleReader reader;
Simon Depiets's avatar
Simon Depiets committed
77 78
    reader.setFeature(QStringLiteral("http://qt-project.org/xml/features/report-whitespace-only-CharData"), true);
    reader.setFeature(QStringLiteral("http://xml.org/sax/features/namespaces"), false);
79 80 81 82
    QXmlInputSource source(device);

    QString errorMsg;
    int errorLine;//+errorColumn;
Simon Depiets's avatar
Simon Depiets committed
83
    bool success = m_doc.setContent(&source, &reader, &errorMsg, &errorLine/*,errorColumn*/);
84

Simon Depiets's avatar
Simon Depiets committed
85 86 87
    if (!success) {
        qCWarning(LOKALIZE_LOG) << "parse error" << errorMsg << errorLine;
        return errorLine + 1;
88 89 90
    }


Simon Depiets's avatar
Simon Depiets committed
91 92 93 94
    QDomElement file = m_doc.elementsByTagName(QStringLiteral("TS")).at(0).toElement();
    m_sourceLangCode = file.attribute(QStringLiteral("sourcelanguage"));
    m_targetLangCode = file.attribute(QStringLiteral("language"));
    m_numberOfPluralForms = numberOfPluralFormsForLangCode(m_targetLangCode);
95 96 97 98 99

    //Create entry mapping.
    //Along the way: for langs with more than 2 forms
    //we create any form-entries additionally needed

Simon Depiets's avatar
Simon Depiets committed
100
    entries = m_doc.elementsByTagName(QStringLiteral("message"));
101

Simon Depiets's avatar
Simon Depiets committed
102
    qCWarning(LOKALIZE_LOG) << chrono.elapsed() << "secs, " << entries.size() << "entries";
103 104 105 106 107
    return 0;
}

bool TsStorage::save(QIODevice* device, bool belongsToProject)
{
108
    Q_UNUSED(belongsToProject)
109
    QTextStream stream(device);
Simon Depiets's avatar
Simon Depiets committed
110
    m_doc.save(stream, 4);
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
    return true;
}
//END OPEN/SAVE

//BEGIN STORAGE TRANSLATION

int TsStorage::size() const
{
    //return m_map.size();

    return entries.size();
}




/**
 * helper structure used during XLIFF XML walk-through
 */
Simon Depiets's avatar
Simon Depiets committed
130 131
struct TsContentEditingData {
    enum ActionType {Get, DeleteText, InsertText, CheckLength};
132 133 134 135 136 137 138

    QString stringToInsert;
    int pos;
    int lengthOfStringToRemove;
    ActionType actionType;

    ///Get
Simon Depiets's avatar
Simon Depiets committed
139 140 141 142
    TsContentEditingData(ActionType type = Get)
        : pos(-1)
        , lengthOfStringToRemove(-1)
        , actionType(type)
143 144 145
    {}

    ///DeleteText
Nick Shaforostoff's avatar
Nick Shaforostoff committed
146
    TsContentEditingData(int p, int l)
Simon Depiets's avatar
Simon Depiets committed
147 148 149
        : pos(p)
        , lengthOfStringToRemove(l)
        , actionType(DeleteText)
150 151 152
    {}

    ///InsertText
Simon Depiets's avatar
Simon Depiets committed
153 154 155 156 157
    TsContentEditingData(int p, const QString& s)
        : stringToInsert(s)
        , pos(p)
        , lengthOfStringToRemove(-1)
        , actionType(InsertText)
158 159 160
    {}
};

Nick Shaforostoff's avatar
Nick Shaforostoff committed
161
static QString doContent(QDomElement elem, int startingPos, TsContentEditingData* data);
162 163

/**
Nick Shaforostoff's avatar
Nick Shaforostoff committed
164
 * walks through XLIFF XML and performs actions depending on TsContentEditingData:
165 166 167 168
 * - reads content
 * - deletes content, or
 * - inserts content
 */
Simon Depiets's avatar
Simon Depiets committed
169
static QString content(QDomElement elem, TsContentEditingData* data = 0)
170 171 172 173
{
    return doContent(elem, 0, data);
}

Nick Shaforostoff's avatar
Nick Shaforostoff committed
174
static QString doContent(QDomElement elem, int startingPos, TsContentEditingData* data)
175 176 177 178 179 180
{
    //actually startingPos is current pos

    QString result;

    if (elem.isNull()
Simon Depiets's avatar
Simon Depiets committed
181
        || (!result.isEmpty() && data && data->actionType == TsContentEditingData::CheckLength))
182 183
        return QString();

Simon Depiets's avatar
Simon Depiets committed
184
    bool seenCharacterDataAfterElement = false;
185 186

    QDomNode n = elem.firstChild();
Simon Depiets's avatar
Simon Depiets committed
187 188 189 190 191 192 193 194 195
    while (!n.isNull()) {
        if (n.isCharacterData()) {
            seenCharacterDataAfterElement = true;

            QDomCharacterData c = n.toCharacterData();
            QString cData = c.data();

            if (data && data->pos != -1 &&
                data->pos >= startingPos && data->pos <= startingPos + cData.size()) {
196
                // time to do some action! ;)
Simon Depiets's avatar
Simon Depiets committed
197
                int localStartPos = data->pos - startingPos;
198 199

                //BEGIN DELETE TEXT
Simon Depiets's avatar
Simon Depiets committed
200 201
                if (data->actionType == TsContentEditingData::DeleteText) { //(data->lengthOfStringToRemove!=-1)
                    if (localStartPos + data->lengthOfStringToRemove > cData.size()) {
202
                        //text is fragmented into several QDomCharacterData
Simon Depiets's avatar
Simon Depiets committed
203
                        int localDelLen = cData.size() - localStartPos;
Luigi Toscano's avatar
Luigi Toscano committed
204
                        //qCWarning(LOKALIZE_LOG)<<"text is fragmented into several QDomCharacterData. localDelLen:"<<localDelLen<<"cData:"<<cData;
Simon Depiets's avatar
Simon Depiets committed
205
                        c.deleteData(localStartPos, localDelLen);
206
                        //setup data for future iterations
Simon Depiets's avatar
Simon Depiets committed
207
                        data->lengthOfStringToRemove = data->lengthOfStringToRemove - localDelLen;
208
                        //data->pos=startingPos;
Luigi Toscano's avatar
Luigi Toscano committed
209
                        //qCWarning(LOKALIZE_LOG)<<"\tsetup:"<<data->pos<<data->lengthOfStringToRemove;
Simon Depiets's avatar
Simon Depiets committed
210
                    } else {
Luigi Toscano's avatar
Luigi Toscano committed
211
                        //qCWarning(LOKALIZE_LOG)<<"simple delete"<<localStartPos<<data->lengthOfStringToRemove;
Simon Depiets's avatar
Simon Depiets committed
212 213
                        c.deleteData(localStartPos, data->lengthOfStringToRemove);
                        data->actionType = TsContentEditingData::CheckLength;
214 215 216 217 218
                        return QString('a');//so it exits 100%
                    }
                }
                //END DELETE TEXT
                //INSERT
Simon Depiets's avatar
Simon Depiets committed
219 220 221
                else if (data->actionType == TsContentEditingData::InsertText) {
                    c.insertData(localStartPos, data->stringToInsert);
                    data->actionType = TsContentEditingData::CheckLength;
222 223
                    return QString('a');//so it exits 100%
                }
Simon Depiets's avatar
Simon Depiets committed
224
                cData = c.data();
225 226 227
            }
            //else
            //    if (data&&data->pos!=-1/*&& n.nextSibling().isNull()*/)
Luigi Toscano's avatar
Luigi Toscano committed
228
            //        qCWarning(LOKALIZE_LOG)<<"arg!"<<startingPos<<"data->pos"<<data->pos;
229 230

            result += cData;
Simon Depiets's avatar
Simon Depiets committed
231
            startingPos += cData.size();
232 233 234
        }
        n = n.nextSibling();
    }
Simon Depiets's avatar
Simon Depiets committed
235
    if (!seenCharacterDataAfterElement) {
236
        //add empty charData child so that user could add some text
Simon Depiets's avatar
Simon Depiets committed
237
        elem.appendChild(elem.ownerDocument().createTextNode(QString()));
238 239 240 241 242 243 244 245 246 247 248 249
    }

    return result;
}



//flat-model interface (ignores XLIFF grouping)

CatalogString TsStorage::catalogString(QDomElement contentElement) const
{
    CatalogString catalogString;
Nick Shaforostoff's avatar
Nick Shaforostoff committed
250
    TsContentEditingData data(TsContentEditingData::Get);
Simon Depiets's avatar
Simon Depiets committed
251
    catalogString.string = content(contentElement, &data);
252 253 254 255 256
    return catalogString;
}

CatalogString TsStorage::catalogString(const DocPosition& pos) const
{
Simon Depiets's avatar
Simon Depiets committed
257
    return catalogString(pos.part == DocPosition::Target ? targetForPos(pos) : sourceForPos(pos.entry));
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
}

CatalogString TsStorage::targetWithTags(DocPosition pos) const
{
    return catalogString(targetForPos(pos));
}
CatalogString TsStorage::sourceWithTags(DocPosition pos) const
{
    return catalogString(sourceForPos(pos.entry));
}

QString TsStorage::source(const DocPosition& pos) const
{
    return content(sourceForPos(pos.entry));
}
QString TsStorage::target(const DocPosition& pos) const
{
    return content(targetForPos(pos));
}

278
QString TsStorage::sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const
279
{
280
    QString str = source(pos);
281
    if (truncateFirstLine) {
282 283 284 285 286
        int truncatePos = str.indexOf("\n");
        if (truncatePos != -1)
            str.truncate(truncatePos);
    }
    return str;
287
}
288
QString TsStorage::targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const
289
{
290
    QString str = target(pos);
291
    if (truncateFirstLine) {
292 293 294 295 296
        int truncatePos = str.indexOf("\n");
        if (truncatePos != -1)
            str.truncate(truncatePos);
    }
    return str;
297 298
}

299 300 301

void TsStorage::targetDelete(const DocPosition& pos, int count)
{
Simon Depiets's avatar
Simon Depiets committed
302 303
    TsContentEditingData data(pos.offset, count);
    content(targetForPos(pos), &data);
304 305 306 307
}

void TsStorage::targetInsert(const DocPosition& pos, const QString& arg)
{
Simon Depiets's avatar
Simon Depiets committed
308 309
    qCWarning(LOKALIZE_LOG) << pos.entry << arg;
    QDomElement targetEl = targetForPos(pos);
310
    //BEGIN add <*target>
Simon Depiets's avatar
Simon Depiets committed
311 312 313 314 315 316
    if (targetEl.isNull()) {
        QDomNode unitEl = unitForPos(pos.entry);
        QDomNode refNode = unitEl.firstChildElement(names[SourceTag]);
        targetEl = unitEl.insertAfter(m_doc.createElement(names[TargetTag]), refNode).toElement();

        if (pos.entry < size()) {
317 318 319 320 321 322 323
            targetEl.appendChild(m_doc.createTextNode(arg));//i bet that pos.offset is 0 ;)
            return;
        }
    }
    //END add <*target>
    if (arg.isEmpty()) return; //means we were called just to add <taget> tag

Simon Depiets's avatar
Simon Depiets committed
324 325
    TsContentEditingData data(pos.offset, arg);
    content(targetEl, &data);
326 327 328 329 330 331 332 333 334 335 336 337 338
}

void TsStorage::setTarget(const DocPosition& pos, const QString& arg)
{
    Q_UNUSED(pos);
    Q_UNUSED(arg);
//TODO
}


QVector<AltTrans> TsStorage::altTrans(const DocPosition& pos) const
{
    QVector<AltTrans> result;
339

Simon Depiets's avatar
Simon Depiets committed
340
    QString oldsource = content(unitForPos(pos.entry).firstChildElement(names[OldSourceTag]));
341
    if (!oldsource.isEmpty())
Simon Depiets's avatar
Simon Depiets committed
342
        result << AltTrans(CatalogString(oldsource), i18n("Previous source value, saved by lupdate tool"));
343

344 345 346 347 348 349 350 351
    return result;
}


QStringList TsStorage::sourceFiles(const DocPosition& pos) const
{
    QStringList result;

352
    QDomElement elem = unitForPos(pos.entry).firstChildElement(attrnames[LocationAttr]);
Simon Depiets's avatar
Simon Depiets committed
353 354 355 356 357 358 359
    while (!elem.isNull()) {
        QString sourcefile = elem.attribute(QStringLiteral("filename"));
        QString linenumber = elem.attribute(QStringLiteral("line"));
        if (!(sourcefile.isEmpty() && linenumber.isEmpty()))
            result.append(sourcefile + ':' + linenumber);

        elem = elem.nextSiblingElement(attrnames[LocationAttr]);
360 361 362 363 364 365 366 367 368 369 370
    }
    //qSort(result);

    return result;
}

QVector<Note> TsStorage::notes(const DocPosition& pos) const
{
    QVector<Note> result;

    QDomElement elem = unitForPos(pos.entry).firstChildElement(names[NoteTag]);
Simon Depiets's avatar
Simon Depiets committed
371
    while (!elem.isNull()) {
372
        Note note;
Simon Depiets's avatar
Simon Depiets committed
373
        note.content = elem.text();
374 375
        result.append(note);

Simon Depiets's avatar
Simon Depiets committed
376
        elem = elem.nextSiblingElement(names[NoteTag]);
377 378 379 380 381 382
    }
    return result;
}

QVector<Note> TsStorage::developerNotes(const DocPosition& pos) const
{
383 384 385
    QVector<Note> result;

    QDomElement elem = unitForPos(pos.entry).firstChildElement(names[DevNoteTag]);
Simon Depiets's avatar
Simon Depiets committed
386
    while (!elem.isNull()) {
387
        Note note;
Simon Depiets's avatar
Simon Depiets committed
388
        note.content = elem.text();
389 390
        result.append(note);

Simon Depiets's avatar
Simon Depiets committed
391
        elem = elem.nextSiblingElement(names[DevNoteTag]);
392 393
    }
    return result;
394 395 396 397
}

Note TsStorage::setNote(DocPosition pos, const Note& note)
{
Luigi Toscano's avatar
Luigi Toscano committed
398
    //qCWarning(LOKALIZE_LOG)<<int(pos.form)<<note.content;
Simon Depiets's avatar
Simon Depiets committed
399
    QDomElement unit = unitForPos(pos.entry);
400 401
    QDomElement elem;
    Note oldNote;
Simon Depiets's avatar
Simon Depiets committed
402 403 404
    if (pos.form == -1 && !note.content.isEmpty()) {
        QDomElement ref = unit.lastChildElement(names[NoteTag]);
        elem = unit.insertAfter(m_doc.createElement(names[NoteTag]), ref).toElement();
405
        elem.appendChild(m_doc.createTextNode(QString()));
Simon Depiets's avatar
Simon Depiets committed
406 407
    } else {
        QDomNodeList list = unit.elementsByTagName(names[NoteTag]);
408
        //if (pos.form==-1) pos.form=list.size()-1;
Simon Depiets's avatar
Simon Depiets committed
409
        if (pos.form < list.size()) {
410
            elem = unit.elementsByTagName(names[NoteTag]).at(pos.form).toElement();
Simon Depiets's avatar
Simon Depiets committed
411
            oldNote.content = elem.text();
412 413 414 415 416
        }
    }

    if (elem.isNull()) return oldNote;

Simon Depiets's avatar
Simon Depiets committed
417 418 419
    if (!elem.text().isEmpty()) {
        TsContentEditingData data(0, elem.text().size());
        content(elem, &data);
420 421
    }

Simon Depiets's avatar
Simon Depiets committed
422 423 424 425
    if (!note.content.isEmpty()) {
        TsContentEditingData data(0, note.content);
        content(elem, &data);
    } else
426 427 428 429 430 431 432 433 434
        unit.removeChild(elem);

    return oldNote;
}

QStringList TsStorage::context(const DocPosition& pos) const
{
    QStringList result;

Simon Depiets's avatar
Simon Depiets committed
435 436
    QDomElement unit = unitForPos(pos.entry);
    QDomElement context = unit.parentNode().toElement();
437 438 439
    //if (context.isNull())
    //    return result;

Simon Depiets's avatar
Simon Depiets committed
440
    QDomElement name = context.firstChildElement(names[NameTag]);
441 442
    if (name.isNull())
        return result;
Simon Depiets's avatar
Simon Depiets committed
443

444 445 446 447 448 449 450 451 452 453 454 455
    result.append(name.text());
    return result;
}

QStringList TsStorage::matchData(const DocPosition& pos) const
{
    Q_UNUSED(pos);
    return QStringList();
}

QString TsStorage::id(const DocPosition& pos) const
{
Simon Depiets's avatar
Simon Depiets committed
456
    QString result = source(pos);
457
    result.remove('\n');
Simon Depiets's avatar
Simon Depiets committed
458
    QStringList ctxt = context(pos);
459 460 461 462 463 464 465
    if (ctxt.size())
        result.prepend(ctxt.first());
    return result;
}

bool TsStorage::isPlural(const DocPosition& pos) const
{
Simon Depiets's avatar
Simon Depiets committed
466
    QDomElement unit = unitForPos(pos.entry);
467 468 469 470 471 472

    return unit.hasAttribute(names[PluralTag]);
}

void TsStorage::setApproved(const DocPosition& pos, bool approved)
{
473
    targetInsert(pos, QString()); //adds <target> if needed
Simon Depiets's avatar
Simon Depiets committed
474 475
    QDomElement target = unitForPos(pos.entry).firstChildElement(names[TargetTag]); //asking directly to bypass plural state detection
    if (target.attribute(attrnames[TypeAttr]) == attrvalues[ObsoleteVal])
476
        return;
477
    if (approved)
478
        target.removeAttribute(attrnames[TypeAttr]);
479
    else
Simon Depiets's avatar
Simon Depiets committed
480
        target.setAttribute(attrnames[TypeAttr], QStringLiteral("unfinished"));
481 482 483 484
}

bool TsStorage::isApproved(const DocPosition& pos) const
{
Simon Depiets's avatar
Simon Depiets committed
485 486
    QDomElement target = unitForPos(pos.entry).firstChildElement(names[TargetTag]);
    return !target.hasAttribute(attrnames[TypeAttr]) || target.attribute(attrnames[TypeAttr]) == attrvalues[VanishedVal];
487 488
}

489 490
bool TsStorage::isObsolete(int entry) const
{
Simon Depiets's avatar
Simon Depiets committed
491 492 493
    QDomElement target = unitForPos(entry).firstChildElement(names[TargetTag]);
    QString v = target.attribute(attrnames[TypeAttr]);
    return v == attrvalues[ObsoleteVal] || v == attrvalues[VanishedVal];
494 495
}

496 497
bool TsStorage::isEmpty(const DocPosition& pos) const
{
Nick Shaforostoff's avatar
Nick Shaforostoff committed
498
    TsContentEditingData data(TsContentEditingData::CheckLength);
Simon Depiets's avatar
Simon Depiets committed
499
    return content(targetForPos(pos), &data).isEmpty();
500 501 502 503
}

bool TsStorage::isEquivTrans(const DocPosition& pos) const
{
Nick Shaforostoff's avatar
Nick Shaforostoff committed
504
    Q_UNUSED(pos)
505 506 507 508 509
    return true;//targetForPos(pos.entry).attribute("equiv-trans")!="no";
}

void TsStorage::setEquivTrans(const DocPosition& pos, bool equivTrans)
{
Nick Shaforostoff's avatar
Nick Shaforostoff committed
510 511
    Q_UNUSED(pos)
    Q_UNUSED(equivTrans)
512 513 514 515 516 517 518 519 520 521
    //targetForPos(pos.entry).setAttribute("equiv-trans",noyes[equivTrans]);
}

QDomElement TsStorage::unitForPos(int pos) const
{
    return entries.at(pos).toElement();
}

QDomElement TsStorage::targetForPos(DocPosition pos) const
{
Simon Depiets's avatar
Simon Depiets committed
522 523
    QDomElement unit = unitForPos(pos.entry);
    QDomElement translation = unit.firstChildElement(names[TargetTag]);
524 525
    if (!unit.hasAttribute(names[PluralTag]))
        return translation;
Simon Depiets's avatar
Simon Depiets committed
526 527 528 529 530 531

    if (pos.form == -1) pos.form = 0;

    QDomNodeList forms = translation.elementsByTagName(QStringLiteral("numerusform"));
    while (pos.form >= forms.size())
        translation.appendChild(unit.ownerDocument().createElement(QStringLiteral("numerusform")));
532 533 534 535 536 537 538 539
    return forms.at(pos.form).toElement();
}

QDomElement TsStorage::sourceForPos(int pos) const
{
    return unitForPos(pos).firstChildElement(names[SourceTag]);
}

Nick Shaforostoff's avatar
Nick Shaforostoff committed
540 541
void TsStorage::setTargetLangCode(const QString& langCode)
{
Simon Depiets's avatar
Simon Depiets committed
542
    m_targetLangCode = langCode;
Nick Shaforostoff's avatar
Nick Shaforostoff committed
543

Simon Depiets's avatar
Simon Depiets committed
544 545 546
    QDomElement file = m_doc.elementsByTagName(QStringLiteral("TS")).at(0).toElement();
    if (m_targetLangCode != file.attribute(QStringLiteral("language")).replace('-', '_')) {
        QString l = langCode;
Nick Shaforostoff's avatar
Nick Shaforostoff committed
547 548 549 550 551
        file.setAttribute(QStringLiteral("language"), l.replace('_', '-'));
    }
}


552 553 554
//END STORAGE TRANSLATION