first release of translation facility

* runtime - loading and translating
 * design-time - works with *.ts file (pip_tr utility)
 * compile-time - CMake macro for compile *.ts
This commit is contained in:
2024-11-05 13:49:00 +03:00
parent 73ed51e3d4
commit 57f8c1313e
52 changed files with 1571 additions and 480 deletions

View File

@@ -17,14 +17,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "parser.h"
#include "picli.h"
#include "pidir.h"
#include "pifile.h"
#include "piiostream.h"
#include "pijson.h"
#include "pivaluetree.h"
#include "pivaluetree_conversions.h"
#include "ts_file.h"
#include "piliterals_string.h"
#include "pitranslator.h"
#include "pitranslator_p.h"
using namespace PICoutManipulators;
@@ -42,12 +41,17 @@ void header() {
piCout << Bold << "PIP Translator";
piCout << Cyan << "Version" << Bold << PIPVersion() << NewLine;
piCout << Green << Bold << "Usage:" << Default
<< "\"pip_tr [-hHqn] -l <lang> -o <output_file> <file1/dir1> [<file2/dir2>] [<file3/dir3>] [...]\"" << NewLine;
<< "\"pip_tr -P [-hHqnr] -l <lang> -o <output_file> <file1/dir1> [<file2/dir2>] [<file3/dir3>] [...]\"" << NewLine
<< "\"pip_tr -C [-hHq] -o <output_file> <ts_file>\"" << NewLine;
}
void usage() {
header();
piCout << Green << Bold << "Details:";
piCout << Bold << "Mode";
piCout << "-P --Parse " << Green << "- parse sources and create/update *.ts file";
piCout << "-C --Compile " << Green << "- compile *.ts file";
piCout << "";
piCout << Bold << "Debug control";
piCout << "-h " << Green << "- display this message and exit";
piCout << "-H " << Green << "- display details help";
@@ -56,10 +60,12 @@ void usage() {
piCout << Bold << "Output control";
piCout << "-l <lang> " << Green << "- translation language (e.g. en_US, ru_RU)";
piCout << "-n, --no-obsolete " << Green << "- drop unused translations in output file";
piCout << "-o <output_file> " << Green << "- output file for translation (QtLinguist *.ts file)";
piCout << "-o <output_file> " << Green
<< "- output file for translation (QtLinguist *.ts file for parse mode or binary file for compile mode)";
piCout << "";
piCout << Bold << "Input control";
piCout << "<file/dir> " << Green << "- add file or dir translation";
piCout << "-r, --recursive " << Green << "- scan directories recursively";
piCout << "<file/dir> " << Green << "- add source file/dir for parse mode or *.ts file for compile mode";
}
void help() {
@@ -67,105 +73,10 @@ void help() {
piCout << help_string;
}
PIString fromCode(const PIString & in) {
return in.replacedAll("\\n", '\n').replaceAll("\\r", '\r').replaceAll("\\t", '\t');
}
void gatherStrings(TSFile::Content & content, const PIString & file, const PIString & file_loc) {
PIString source, context;
int pos = -1, ppos = 0, line = -1;
int len = 0;
auto isCLetter = [](const PIChar c) { return c.isAlpha() || c.isDigit() || c == '_'; };
static const PIStringList keyword({"PITranslator::tr", "piTr"});
for (const auto & kw: keyword) {
for (;;) {
source.clear();
context.clear();
pos = file.find(kw, pos + 1);
if (pos < 0) break;
if (pos > 0) {
auto pc = file[pos - 1];
if (isCLetter(pc) || pc == ':') {
pos += kw.size_s();
continue;
}
}
pos += kw.size_s();
if (pos < file.size_s() - 1) {
auto pc = file[pos];
if (isCLetter(pc) || pc == ':') {
pos += kw.size_s();
continue;
}
}
// piCout << "PITranslator" << pos;
pos = file.find('(', pos);
if (pos < 0) break;
// piCout << "(" << pos;
pos += 1;
pos = file.findRange('"', '"', '\\', pos, &len);
if (pos < 0) break;
// piCout << "\"" << pos;
source = file.mid(pos, len);
pos += len + 1;
line = file.lineNumber(pos);
ppos = pos;
while (pos < file.size_s()) {
if (!file[pos].isSpace() && file[pos] != '\n' && file[pos] != '\r') break;
++pos;
}
if (pos < file.size_s()) {
// piCout << "check comma" << file[pos];
if (file[pos] == ',') {
pos += 1;
pos = file.findRange('"', '"', '\\', pos, &len);
// piCout << "check range" << file.mid(pos, len);
if (pos >= 0) {
context = file.mid(pos, len);
pos += len + 1;
ppos = pos;
}
}
}
content[context].confirm(fromCode(source), file_loc, line);
pos = ppos;
piCout << "Context = \"" << context << "\", message = \"" << source << "\"";
}
}
auto & ec(content[""]);
pos = -1;
for (;;) {
source.clear();
pos = file.find('"', pos + 1);
if (pos < 0) break;
pos = file.findRange('"', '"', '\\', pos, &len);
if (pos < 0) break;
// piCout << "\"" << pos;
source = file.mid(pos, len);
pos += len + 1;
if (file.mid(pos, 3) == "_tr") {
pos += 3;
if (pos < file.size_s() - 1) {
auto pc = file[pos];
if (isCLetter(pc) || pc == ':') {
continue;
}
}
ec.confirm(fromCode(source), file_loc, file.lineNumber(pos));
piCout << "_tr = \"" << source << "\"";
}
}
}
enum Mode {
mParse = 1,
mCompile = 2,
};
int main(int argc, char * argv[]) {
PICLI cli(argc, argv);
@@ -177,48 +88,99 @@ int main(int argc, char * argv[]) {
cli.addArgument("Help");
cli.addArgument("quiet");
cli.addArgument("no-obsolete");
cli.addArgument("recursive");
cli.addArgument("Parse");
cli.addArgument("Compile");
if (cli.hasArgument("Help")) {
help();
return 0;
}
if (cli.hasArgument("help") || cli.argumentValue("output").isEmpty() || cli.argumentValue("language").isEmpty() ||
cli.optionalArguments().isEmpty()) {
int mode = 0;
if (cli.hasArgument("Parse")) mode |= mParse;
if (cli.hasArgument("Compile")) mode |= mCompile;
if (cli.hasArgument("help") || cli.argumentValue("output").isEmpty() || cli.optionalArguments().isEmpty() ||
(mode != mParse && mode != mCompile)) {
usage();
return 0;
}
if (mode == mParse && cli.argumentValue("language").isEmpty()) {
usage();
return 0;
}
piDebug = !cli.hasArgument("quiet");
PIString out_path = cli.argumentValue("output");
PIStringList files;
const static PIStringList ext({"h", "hpp", "cpp", "cxx"});
for (const PIString & a: cli.optionalArguments()) {
if (PIDir::isExists(a)) {
auto dl = PIDir(a).allEntries();
for (const auto & i: dl) {
if (!i.isFile()) continue;
if (ext.contains(i.extension().toLowerCase().trim())) files << i.path;
if (mode == mParse) {
piCout << Cyan << Bold << "Reading ts file ...";
auto content = TSFile::read(out_path);
if (content.lang.isEmpty()) content.lang = cli.argumentValue("language");
content.markAllMissing();
piCout << Cyan << Bold << "Reading done";
bool recursive = cli.hasArgument("recursive");
PIStringList files;
const static PIStringList ext({"h", "hpp", "cpp", "cxx"});
for (const PIString & a: cli.optionalArguments()) {
if (PIDir::isExists(a)) {
auto dl = recursive ? PIDir(a).allEntries() : PIDir(a).entries();
for (const auto & i: dl) {
if (!i.isFile()) continue;
if (ext.contains(i.extension().toLowerCase().trim())) files << i.path;
}
} else {
if (PIFile::isExists(a)) files << a;
}
} else {
if (PIFile::isExists(a)) files << a;
}
piCout << Cyan << Bold << "Read" << files.size_s() << "files ...";
PITimeMeasurer tm;
PIDir out_dir(PIFile::FileInfo(out_path).dir());
int count_all = 0, count_new = 0;
for (const auto & p: files) {
PIString file_loc = out_dir.relative(p);
PIFile f(p, PIIODevice::ReadOnly);
gatherStrings(content, PIString::fromUTF8(f.readAll()), file_loc, count_all, count_new);
}
auto elapsed = tm.elapsed();
piCout << Cyan << Bold
<< "Reading done, found %1 strings (%2 new) in %3 ms"_a.arg(count_all).arg(count_new).arg((int)elapsed.toMilliseconds());
piCout << Cyan << Bold << "Writing ts file ...";
if (!TSFile::write(out_path, content, cli.hasArgument("no-obsolete"))) return 1;
piCout << Cyan << Bold << "Writing done";
} else if (mode == mCompile) {
piCout << Cyan << Bold << "Reading ts file ...";
auto content = TSFile::read(cli.optionalArguments().front());
if (content.lang.isEmpty()) content.lang = cli.argumentValue("language");
piCout << Cyan << Bold << "Reading done";
int wcount = 0;
PITranslatorPrivate::Translation t;
t.lang = content.lang;
auto cit = content.contexts.makeIterator();
while (cit.next()) {
auto * c = t.createContext(cit.key());
auto mit = cit.value().messages.makeIterator();
while (mit.next()) {
const auto & m(mit.value());
if (m.type[TSFile::Message::Unfinished] || m.type[TSFile::Message::Missing]) continue;
c->add(m.source, m.translation);
++wcount;
// piCout << "tr" << m.source << "->" << m.translation;
}
}
PIFile outf(out_path, PIIODevice::WriteOnly);
if (!outf.isOpened()) {
piCerr << "Can`t open file \"%1\"!"_tr("TSFile").arg(outf.path());
return 1;
}
outf.clear();
piCout << Cyan << Bold << "Writing bin file ...";
PITranslatorPrivate::writeHeader(&outf);
outf.write(t.save());
piCout << Cyan << Bold << "Writing done (%1 strings)"_a.arg(wcount);
}
piCout << Cyan << Bold << "Reading ts file ...";
auto content = TSFile::read(out_path);
piCout << Cyan << Bold << "Reading done";
piCout << Cyan << Bold << "Read" << files.size_s() << "files ...";
PIDir out_dir(PIFile::FileInfo(out_path).dir());
for (const auto & p: files) {
PIString file_loc = out_dir.relative(p);
PIFile f(p, PIIODevice::ReadOnly);
gatherStrings(content, PIString::fromUTF8(f.readAll()), file_loc);
}
piCout << Cyan << Bold << "Reading done";
piCout << Cyan << Bold << "Writing ts file ...";
if (!TSFile::write(out_path, content, cli.argumentValue("language"), cli.hasArgument("no-obsolete"))) return 1;
piCout << Cyan << Bold << "Writing done";
return 0;
}