Files
webmin/bin/manipulate-language
2020-02-28 16:38:17 +03:00

834 lines
30 KiB
Perl
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env perl
# manipulate-language - Automatic translation and/or transcoding language files for all or specific modules
use strict;
use warnings;
use 5.010;
use File::Spec;
use File::Basename;
use File::Find;
use JSON::PP;
use HTTP::Tiny;
use HTML::Entities;
use List::MoreUtils qw(any);
use Cwd qw(cwd);
use Encode qw/encode decode/;
use Encode::Detect::Detector;
use Term::ANSIColor qw(:constants);
use Getopt::Long qw(:config permute pass_through);
use Pod::Usage;
sub main
{
my %opt;
GetOptions(
'help|h' => \$opt{'help'},
'type|w:s' => \$opt{'type'},
'modules|m:s' => \$opt{'modules'},
'language-target|t:s' => \$opt{'language-target'},
'language-source|s:s' => \$opt{'language-source'},
'language-source-encoding|e:s' => \$opt{'language-source-encoding'},
'no-translate|o!' => \$opt{'no-translate'},
# 'action|a:s' => \$opt{'action'}, ####################################
# 'key|k:s' => \$opt{'key'}, ########### coming soon! ###########
# 'value|d:s' => \$opt{'value'}, ####################################
'overwrite|f!' => \$opt{'overwrite'},
'log|l:s' => \$opt{'log'},
'verbose|v:i' => \$opt{'verbose'});
my %data;
# Print help and exit
pod2usage(0) if ($opt{'help'});
# Enforce verbose output by default
if (!defined($opt{'verbose'})) {
$opt{'verbose'} = 1;
}
# Get current path and make workaround to run from sub dir
my $path = cwd;
if ($path =~ /\/bin$/) {
$path = Cwd::realpath('..');
}
my $type = -d "$path/ulang" ? 'ulang' : 'lang';
# Load Webmin lib
require("$path/web-lib-funcs.pl");
# Enforce user specific target: language directory, config.info or module.info file {lang|ulang|config|module}
$opt{'type'} && ($type = $opt{'type'});
# Store path and type
$data{'path'} = $path;
$data{'type'} = $type;
# Check if we can get Google Translate API token
$data{'token'} = get_google_translate_token();
if (!$data{'token'}) {
$opt{'no-translate'} = 1;
say YELLOW . "Translation will not be done as Google Translate API key is missing." . RESET;
}
# Get list of languages from `lang_list.txt` file
$data{'languages_source_list'} = list_languages_local(\%data);
$data{'languages_source_list_codes'} = [map {$_->{'lang'}} @{ $data{'languages_source_list'} }];
# Define base language. Default is set English
$opt{'language-source'} ||= 'en';
my $language_source = $opt{'language-source'};
# What language encoding do we expect on human translated files
$opt{'language-source-encoding'} ||= 'utf-8';
# Find out which modules to update, if exist
my $modules = $opt{'modules'};
my @modules;
if ($modules) {
my @ml = split(',', $modules);
foreach my $module (@ml) {
my $exists = source_data(\%data, \%opt, $module);
if ($exists) {
push(@modules, $module);
}
}
@modules = sort @modules;
} else {
my $found;
find(
{
wanted => sub {
$found = $File::Find::name;
if (!-l $found && ($found =~ /$type\/$language_source$/ || $found =~ /$type\.info$/)) {
$found =~ s/^$path\///g;
$found =~ s/\/$type\/$language_source//g;
$found =~ s/\/$type\.info//g;
$found =~ s/\/$language_source//g;
$found =~ s/\/$language_source//g;
if ($found !~ /^_/) {
push(@modules, $found);
}
}
},
},
$path);
@modules = sort @modules;
}
$data{'modules'} = \@modules;
# Define target language(s) or translate all
$opt{'language-target'} = [$opt{'language-target'} ? split(',', $opt{'language-target'}) : ()];
if (@{ $opt{'language-target'} }) {
my @bad_languages;
for my $language (@{ $opt{'language-target'} }) {
push(@bad_languages, $language) if !any {$_ =~ /^$language$/} @{ $data{'languages_source_list_codes'} };
}
if (@bad_languages) {
errors('language-target', join(',', @bad_languages));
exit;
}
}
# Define source language
if (!any {$_ =~ /^$language_source/} @{ $data{'languages_source_list_codes'} }) {
errors('language-source', $language_source);
exit;
}
# User interactions
talk('affected', \%opt, \%data);
# Log the output. Start
$opt{'log'} ||= "/tmp/manipulate-language-@{[time()]}.log";
open $data{'out'}, ">", "$opt{'log'}" or die RED, "Error creating log: $!\n", RESET, "\n";
# Execute force transcode/translate
if ($opt{'overwrite'}) {
talk('overwrite-pre', \%opt, \%data);
if (prompt('next')) {
talk('overwrite-post', \%opt, \%data);
overwrite(\%opt, \%data);
}
exit;
}
# Log the output. End
close $data{'out'};
return 0;
}
main();
# The following is used to get correct language files based on old language table.
# This function, should not be really needed, unless compiling languages with
# Webmin version prior to 1.950 (first time), as Webmin 1.950+ is going to
# use new langauge map and have all related language files in UTF-8 already
sub language_map
{
my ($code) = @_;
my %language_map = (
# Use plain language codes
'ja' => 'ja_JP.euc',
'ko' => 'ko_KR.euc',
'ms' => 'ms_MY',
'ru' => 'ru_RU',
'uk' => 'uk_UA',
'zh' => 'zh_CN',
'zh_TW' => 'zh_TW.Big5',
# Czech is `cs` not `cz`, as there is no `cz` at all
'cs' => 'cz',
# Slovenian is `sl` not `si`, as `si` is Sinhala
'sl' => 'si',
# Greek is `el` not `gr`
'el' => 'gr',);
if ($language_map{$code}) {
$code = $language_map{$code};
}
return $code;
}
# Always check, if strings that are about to be translated haven't been
# translated by human already, and if so, extract them with correct
# encoding, to be further converted to UTF-8 and stored on destination file
sub language_source_file
{
my ($opt, $data, %data) = @_;
my $language_code = $data{'language-code'};
my %language_source_file;
my $language_source_file;
my $language_source_file_encoding = 'utf-8';
if ($opt->{'language-source-encoding'} eq 'map') {
$language_code = language_map($language_code);
}
# Set prefix in case of processing `config.info` or `module.info`
my $language_source_file_target_ = "/";
if ($opt->{'type'} =~ /config|module/) {
$language_source_file_target_ = "/$opt->{'type'}.info.";
}
# Get already translated language strings from current directory
my $language_source_file_target = "$data{'module-path'}$language_source_file_target_$language_code";
if (-r $language_source_file_target) {
$language_source_file = $language_source_file_target;
}
# Supply proper encoding for existing language files
if ($language_source_file) {
talk_log(("" . GREEN . " .. Found human translated file for $data{'language-name'} ($language_code)" . RESET . ""),
$data, 1);
read_file("$language_source_file", \%language_source_file);
# Force encoding based on map
if ($opt->{'language-source-encoding'} eq 'map') {
my $map_auto;
my $code = $data{'language-code'};
if ($code eq 'ja') {
$language_source_file_encoding = "euc-jp";
} elsif ($code eq 'ko') {
$language_source_file_encoding = "euc-kr";
} elsif ($code eq 'ru' || $code eq 'bg' || $code eq 'uk') {
$language_source_file_encoding = "cp1251";
} elsif ($code eq 'ca' ||
$code eq 'fr' ||
$code eq 'hr' ||
$code eq 'lt' ||
$code eq 'no')
{
$language_source_file_encoding = "cp1252";
} elsif ($code eq 'cs' ||
$code eq 'sk' ||
$code eq 'pl' ||
$code eq 'sl' ||
$code eq 'hu')
{
$language_source_file_encoding = "iso-8859-2";
} elsif ($code eq 'tr') {
$language_source_file_encoding = "iso-8859-9";
} elsif ($code eq 'he') {
$language_source_file_encoding = "iso-8859-8-i";
} elsif ($code eq 'th') {
$language_source_file_encoding = "tis-620";
} elsif ($code eq 'zh') {
$language_source_file_encoding = "gb2312";
} elsif ($code eq 'zh_TW') {
$language_source_file_encoding = "big5";
} else {
my $language_source_file_data = read_file_contents($language_source_file_target);
my $detected_encoding = Encode::Detect::Detector::detect($language_source_file_data);
if ($detected_encoding) {
$language_source_file_encoding = $detected_encoding;
$map_auto = " (auto)";
} else {
$language_source_file_encoding = 'utf-8';
$map_auto = " (auto enforced)";
}
}
talk_log(
("" . CYAN . " .. Force file encoding to \`$language_source_file_encoding\`" .
($map_auto // '') . " as derived from language map" . RESET . ""
),
$data,
1);
}
# Figure out encoding automatically
elsif ($opt->{'language-source-encoding'} eq 'auto') {
my $language_source_file_data = read_file_contents($language_source_file_target);
$language_source_file_encoding = Encode::Detect::Detector::detect($language_source_file_data);
if (!$language_source_file_encoding) {
$language_source_file_encoding =
ask(' ' . BRIGHT_RED . '.. Cannot detect encoding, enter it manually' . RESET . '');
talk_log(
("" . BRIGHT_RED .
" .. Manually set file encoding to \`$language_source_file_encoding\`" . RESET . ""),
$data,
1);
} else {
talk_log(
("" . MAGENTA .
" .. Automatically detected file encoding is \`$language_source_file_encoding\`" . RESET . ""
),
$data,
1);
}
} else {
$language_source_file_encoding = $opt->{'language-source-encoding'};
talk_log(("" . MAGENTA . " .. Set file encoding to default \`$language_source_file_encoding\`" . RESET . ""),
$data, 1);
}
} else {
say YELLOW, " .. No human translated file has be found for $data{'language-name'} ($language_code)", RESET;
}
return (\%language_source_file, $language_source_file_encoding);
}
sub language_transcode
{
my ($string, $encoding) = @_;
eval {$string = decode($encoding, $string)};
if ($@) {
say "Error found: $@";
if (!prompt('next')) {
exit;
}
}
decode_entities($string);
$string = encode('utf-8', $string);
return $string;
}
sub source_data
{
my ($data, $opt, $module) = @_;
my ($language_source, $type, $target, $source_file, $source_file_, $exists);
$language_source = $opt->{'language-source'};
$type = $data->{'type'};
if ($type eq 'config' || $type eq 'module') {
$language_source = $language_source ne 'en' ? "$type.info.$language_source" : "$type.info";
}
$target = "$data->{'path'}/$module";
$source_file = "$target/$type/$language_source";
$source_file_ = "$target/$language_source";
$exists = (-r $source_file && !-l $source_file) || (-r $source_file_ && !-l $source_file_) || 0;
if (-r $source_file && !-l $source_file) {
$target .= "/$type";
} elsif (-r $source_file_ && !-l $source_file_) {
$source_file = $source_file_;
}
return ($exists, $source_file, $target);
}
# Returns an array of supported languages,
# taken from Webmin's lang_list.txt file.
sub list_languages_local
{
my ($data) = @_;
my ($key, $value, $options, $language, @languages);
open(my $LANG, "$data->{'path'}/lang_list.txt");
while (<$LANG>) {
if (/^(.*=\w+)\s+(.*)/) {
$language = { 'desc' => $2 };
foreach $options (split(/,/, $1)) {
if ($options =~ /^([^=]+)=(.*)$/) {
$key = $1;
$value = $2;
$key =~ s/^\s+|\s+$//g;
$value =~ s/^\s+|\s+$//g;
$language->{$key} = $value;
}
}
push(@languages, $language);
}
}
close($LANG);
@languages = sort {$a->{'desc'} cmp $b->{'desc'}} @languages;
return wantarray ? @languages : \@languages;
}
# Prepare the string that is going to be sent to translator
sub translate_substitute
{
my ($value) = @_;
$value = language_transcode($value, 'utf-8');
$value =~ s/(?<!['|"|`|“|«|=])(\$(\d+))/<gtt class=notranslate>\$$2<\/gtt>/g;
return $value;
}
# Perform white sorcery on returned string from translator,
# as it seems to be breaking quite a lot of things
sub translated_substitute
{
my ($translated) = @_;
$translated =~ s/<\/[ ]*(\w+)>/<\/$1>/g;
$translated =~ s/<[ ](.*?)[ ]/<$1/g;
$translated =~ s/<gtt.*?>.*?(\d+).*?<.*?>/\$$1/gi;
$translated =~ s/<[ ]*(?:(\?\w+|(\w+).*?))[ ]*>(.*?)<.*?>/<$1>$3<\/$2>/g;
$translated =~ s/([ ]*)\$[ ]*(\d+)/$1\$$2/g;
$translated =~ s/(['|"|`|“|«])[ ]*\$(\d+)[ ]*(['|"|`|“|»])/$1\$$2$3/g;
$translated =~ s/(<.*?>)[ ]*(.*?)[ ]*(<.*?>)/$1$2$3/g;
$translated =~ s/\$(\d+)[ ]*([:|])[ ]*\$(\d+)[ ]*/ \$$1:\$$3 /g;
$translated =~ s/\$(\d+)[ ]*[ ]*\$(\d+)/\$$1:\$$2/g;
$translated =~ s/(\p{L})\$(\d+)[ ]+/$1 \$$2 /g;
$translated =~ s/(\p{L})[ ]+([:|]){2}[ ]+(\p{L})/$1::$3/g;
$translated =~ s/([ ]+)/ /g;
$translated =~ s/(.)[ ]+\./$1./g;
return $translated;
}
# Make actual translation using Google Translate API
sub translate
{
my ($token, $source, $target, $value) = @_;
# Replace language code to match what translator expects
$target =~ s/_/-/;
# Prevent service unavailable error
sleep 1;
my $tr;
my $rsp = "https://translation.googleapis.com/language/translate/v2?q=@{[urlize($value)]}";
my $rsh = { 'Authorization' => "Bearer \"@{[trim($token)]}\"",
'User-Agent' => 'curl/7.29.1',
'Content-Type' => 'application/json; charset=utf-8', };
my $rsc = "{ 'source': '" . $source . "', 'target': '" . $target . "', 'format': 'text'}";
my $rs = HTTP::Tiny->new()->request('POST' => $rsp, { 'headers' => $rsh, 'content' => $rsc });
my $ts = $rs->{'success'};
my $tc = $rs->{'content'};
if ($ts) {
$tr = JSON::PP->new->decode($rs->{'content'});
}
return ($ts, $tc, $tr);
}
# Run transcoding or translation of module(s) for the very first time
sub overwrite
{
my ($opt, $data) = @_;
my ($module, $language);
my $path = $data->{'path'};
my $type = $data->{'type'};
my $token = $data->{'token'};
my $modules = $data->{'modules'};
my $language_source = $opt->{'language-source'};
my $language_source_encoding = $opt->{'language-source-encoding'};
my $language_target = $opt->{'language-target'};
foreach $module (@{$modules}) {
my (%template);
my ($exists, $mfile, $mpath) = source_data($data, $opt, $module);
# Get source, base language file
read_file($mfile, \%template);
talk_log(("Transcoding/translating " . BLUE BOLD, $module, RESET . " module .."), $data, 1);
foreach $language (@{ $data->{'languages_source_list'} }) {
# Get target language code
my $code = $language->{'lang'};
# Skip translating source, base language
next if ($code eq $language_source);
# Process only user defined languages or do all
if (@{$language_target}) {
next if (!any {$_ =~ /^$code$/} @{$language_target});
}
# Get other target language attributes
my $name = $language->{'desc'};
my $rtl = $language->{'rtl'};
my %language;
my %language_auto;
talk_log(("" . YELLOW . " .. Processing $name ($code) language .." . RESET . ""), $data, $opt->{'verbose'});
my ($language_source_file, $language_source_file_encoding) =
language_source_file($opt,
$data,
('language-name' => $name,
'language-code' => $code,
'module-path' => $mpath,
'module-name' => $module,
'translation-target' => $type
));
while (my ($key, $value) = each %template) {
# Don't add special keys
next if ($key eq '__norefs');
# Modify `$key` when type is set to "module", otherwise keep default
my $key_ = $key;
my $key__ = $key;
# When processing `module.info` file, ignore most keys
# and add suffix to allowed to return expected format
if ($type eq 'module') {
if ($key !~ /name|desc|longdesc/) {
next;
}
my $code_ = $code;
if ($opt->{'language-source-encoding'} eq 'map') {
$code_ = language_map($code);
$key_ = "${key}_${code_}";
}
$key__ = "${key}_${code}";
}
# Human translated strings must be added to original `$code` file, e.g. `de` after transcoding
if ($language_source_file && $language_source_file->{$key_}) {
talk_log(
("" . DARK . " .. found and transcoded human translated value for \`$key\` key to" . RESET . ""),
$data, $opt->{'verbose'});
# We need to re-encode the string first, depending on which human translated file is being
# used (iso or UTF-8). We still need to convert all back and forth to get rid of ugly &#... escapes
$language_source_file->{$key_} =
language_transcode($language_source_file->{$key_}, $language_source_file_encoding);
talk_log(" — " . WHITE . " $language_source_file->{$key_}" . RESET . "", $data,
$opt->{'verbose'});
# Add a string to main language file
$language{$key__} = $language_source_file->{$key_};
}
# Machine translated strings must be added to `$code.auto` file, e.g. `de.auto`
else {
# Add original value, from source language, to `$code.auto` file without
# performing actual translation. It's useful when you want to translate it manually
# and simply need to find missing language strings on targeted file. Besides, `config.info`
# options are never translated (so far), just as languages that use right-to-left writing
if ($opt->{'no-translate'} || $opt->{'type'} eq 'config' || $rtl && $value =~ /\$(\d+)/) {
talk_log(("" . DARK . " .. stored original value for \`$key\` which is" . RESET . ""),
$data, $opt->{'verbose'});
talk_log(" — " . CYAN . " $value" . RESET . "", $data, $opt->{'verbose'});
$language_auto{$key__} = $value;
next;
}
# Add translated string
talk_log(("" . BRIGHT_MAGENTA . " .. translated value for \`$key\` to" . RESET . ""),
$data, $opt->{'verbose'});
# Prepare sent text
my $_value = $value;
$value = translate_substitute($value);
# Run actual translation
my ($translate_status, $translate_response, $translated) =
translate($token, $language_source, $code, $value);
if ($translate_status) {
$translated = $translated->{'data'}->{'translations'}[0]->{'translatedText'};
talk_log(" — $key --> $_value", $data, $opt->{'verbose'});
if ($opt->{'verbose'} == 2) {
say " .. $key <-- $value";
say " .. $key --> $translated";
}
$translated = translated_substitute($translated);
if ($opt->{'verbose'} == 2) {
say " .. $key <-> $translated";
}
# Store translated string
$language_auto{$key__} = $translated;
talk_log(" — $key <-- $translated", $data, $opt->{'verbose'});
} else {
print RED, "Error: Google Translator - $translate_response", RESET;
if (!prompt('next')) {
say YELLOW, "Terminating..", RESET;
exit;
}
}
}
}
# If we are converting from old times, where there were different encodings and
# file extensions for it, we need to delete/clear all old, no longer needed files
# e.g. will be deleted: uk_UA, zh_TW.Big5, ja_JP.euc, ko_KR.euc and etc.
if ($opt->{'language-source-encoding'} eq 'map') {
my $found;
find(
{
wanted => sub {
$found = $File::Find::name;
my $code_ = language_map($code);
if ($code ne $code_ &&
( $found eq "$mpath/$code_" ||
($opt->{'type'} &&
$opt->{'type'} =~ /config|module/ &&
($found eq "$mpath/$opt->{'type'}.info.$code_"))))
{
unlink($found);
$found =~ s/$mpath\///;
talk_log(
("" . YELLOW .
" .. Found no longer used language file ($found) and deleted .." . RESET . ""
),
$data,
1);
}
},
},
$mpath);
}
# Prepend file name for `config.info` or `module.info` modes
if ($opt->{'type'} =~ /config|module/) {
$code = "$opt->{'type'}.info.$code";
}
# Write transcoded/translated file
write_file($mpath . "/$code", \%language);
write_file($mpath . "/$code.auto", \%language_auto);
}
say GREEN, ".. done ", RESET;
}
}
sub get_google_translate_token
{
my $gc = `gcloud -v 2>&1`;
if (!$gc // $gc !~ /Google Cloud SDK/) {
errors('gcloud-missing');
return 0;
}
my $token = `gcloud auth application-default print-access-token`;
if ($token =~ /ERROR:/) {
errors('gcloud-error', $token);
return 0;
} else {
return $token;
}
}
sub prompt
{
my ($q) = @_;
if ($q eq 'next') {
return prompt("Do you want to proceed?");
}
my $p = sub {
my ($q) = @_;
local $| = 1;
print DARK, "$q", RESET;
chomp(my $a = <STDIN>);
return $a;
};
my $a = &$p("$q (y/N): ");
return lc($a) eq 'y';
}
sub ask
{
my ($q) = @_;
my $p = sub {
my ($q) = @_;
local $| = 1;
print DARK, "$q", RESET;
chomp(my $a = <STDIN>);
return $a;
};
my $a = &$p("$q: ");
return $a;
}
sub errors
{
my ($e, $error) = @_;
my $message;
if ($e eq 'language-target') {
$message =
"Error: Using \`$error\` as a target language(s) is refused. The value(s) should match to one of the language codes from the language list file.";
}
if ($e eq 'language-source') {
$message =
"Error: Using \`$error\` as a source language is refused. The value should match to one of the language codes from the language list file.";
}
if ($e eq 'gcloud-missing') {
$message =
"Error: Command \`gcloud\` to manage Google Cloud Platform resources and developer workflow is not available.",;
}
if ($e eq 'gcloud-error') {
$message = "Error: $error";
}
say RED, $message, RESET;
}
sub talk_log
{
my ($what, $data, $output_to_console) = @_;
# Output to console
if ($output_to_console) {
say $what;
}
# Store to log
my $out = $data->{'out'};
$what =~ s/\[\d+m//g;
say $out $what;
}
sub talk
{
my ($what, $opt, $data) = @_;
if ($what eq 'affected') {
my $affected = "@{$data->{'modules'}}";
$affected =~ s/\b(lang)\b/(lang)/;
if ($affected) {
say GREEN, "Affected modules: ", RESET, YELLOW BOLD, $affected, RESET;
exit if (!prompt('next'));
} else {
say RED, "Error: No modules found to operate on", RESET;
exit;
}
say GREEN, "Affected languages: ", RESET, YELLOW BOLD,
"" . ("@{$opt->{'language-target'}}" || "@{$data->{'languages_source_list_codes'}}") . "", RESET;
exit if (!prompt('next'));
}
if ($what eq 'overwrite-pre') {
say RED, "Warning! ", RESET, WHITE,
"The following operation will force-translate and overwrite\nmentioned languages in all mentioned modules listed above, using ",
YELLOW BOLD, $opt->{'language-source'}, RESET, "\nas a template language. The following directory " . YELLOW BOLD,
$data->{'path'}, RESET,
"\nwith human translated module strings, will be used instead of machine\ntranslations. The operation log will be written in "
. YELLOW BOLD, $opt->{'log'}, RESET, " file.", RESET;
}
if ($what eq 'overwrite-post') {
say BLUE, "Force re-translating all languages, for all modules ..", RESET;
}
}
sub trim
{
my $s = shift;
$s =~ s/^\s+|\s+$//g;
return $s;
}
=pod
=head1 NAME
manipulate-language
=head1 DESCRIPTION
Manage Webmin/Usermin module language files (lang|ulang|help|config|module), to perform transcoding, translation, spell check and add/remove language strings.
=head1 SYNOPSIS
manipulate-language [options]
=head1 OPTIONS
=over
=item --help, -h
Give this help list.
Examples of usage:
manipulate-language --overwrite -m=acl,apache -t=de,ru -o -e=map -w=lang -l=acl,apache:for-de,ru.log
=item --type <lang|ulang|help|config|module>, -w <lang|ulang|help|config|module>
Type of target to use for operations. Either <lang>, <ulang>, <help> directories or language info file, as <config> or <module>. Default is set to "lang" for Webmin and "ulang" for Usermin.
=item --modules, -m
Comma separated list of modules to operate on. Default is to operate on all available modules.
=item --language-target, -t
Comma separated list of affected language codes to operate on. Default is to operate on all languages defined in language list.
=item --language-source, -s
Language to use as a base language. Default is set English.
=item --language-source-encoding, -e
Encoding of human translated language files. Default is set to "utf-8". Available options are <utf-8>, <auto>, <map>. Option "auto" is used to detect encoding automatically, while option "map" is used to use old-time encoding map, where each language had different encoding, and nowadays, should not be used, as all language files are going to be encoded in "utf-8" already.
=item --no-translate, -o
Do not perform translation but still save currently untranslated strings to separate file, with extension ".auto". It's useful, when you need to extract missing, untranslated language stirings from the main language file.
=item --overwrite, -f
Use this only when you run transcoding or translation of module(s) for the very first time. This operation uses base language (default is English) and transcodes/translates all missing strings for given type.
=item --log, -l
Saves complete operation log. By default, log file is saved to "/tmp/manipulate-language-{timestamp}.txt" file.
=item --verbose, -v
Verbosely print processed files and provide detailed output. Be detault, verbose output is enabled.
=back
=head1 LICENSE AND COPYRIGHT
Copyright 2020 Ilia Rostovtsev <programming@rostovtsev.io>