# -- # Copyright (C) 2001-2019 OTRS AG, https://otrs.com/ # -- # This software comes with ABSOLUTELY NO WARRANTY. For details, see # the enclosed file COPYING for license information (GPL). If you # did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt. # -- package Kernel::System::Console::Command::Dev::Tools::TranslationsUpdate; use strict; use warnings; use parent qw(Kernel::System::Console::BaseCommand); use File::Basename; use File::Copy; use Lingua::Translit; use Pod::Strip; use Kernel::Language; our @ObjectDependencies = ( 'Kernel::Config', 'Kernel::System::DateTime', 'Kernel::System::Encode', 'Kernel::System::Main', 'Kernel::System::Storable', 'Kernel::System::SysConfig', ); sub Configure { my ( $Self, %Param ) = @_; $Self->Description('Update the OTRS translation files.'); $Self->AddOption( Name => 'language', Description => "Which language to use, omit to update all languages.", Required => 0, HasValue => 1, ValueRegex => qr/.*/smx, ); $Self->AddOption( Name => 'module-directory', Description => "Translate the OTRS module in the given directory.", Required => 0, HasValue => 1, ValueRegex => qr/.*/smx, ); $Self->AddOption( Name => 'generate-po', Description => "Generate PO (translation content) files. This is only needed if a module is not yet available in Transifex to force initial creation of the gettext files.", Required => 0, HasValue => 0, ); my $Name = $Self->Name(); $Self->AdditionalHelp(<<"EOF"); Translating OTRS Make sure that you have a clean system with a current configuration. No modules may be installed or linked into the system! otrs.Console.pl $Name --language ... Translating Extension Modules Make sure that you have a clean system with a current configuration. The module that needs to be translated has to be installed or linked into the system, but only this one! otrs.Console.pl $Name --language ... --module-directory ... EOF return; } my $BreakLineAfterChars = 60; sub Run { my ( $Self, %Param ) = @_; my @Languages; my $LanguageOption = $Self->GetOption('language'); my $Home = $Kernel::OM->Get('Kernel::Config')->Get('Home'); # check params if ( !$LanguageOption ) { my %DefaultUsedLanguages = %{ $Kernel::OM->Get('Kernel::Config')->Get('DefaultUsedLanguages') }; @Languages = sort keys %DefaultUsedLanguages; @Languages = grep { $_ ne 'en' } @Languages; # ignore en*.pm files } else { push @Languages, $LanguageOption; if ( !-f "$Home/Kernel/Language/$Languages[0].pm" ) { $Self->PrintError("No core translation file: $Languages[0]!"); return $Self->ExitCodeError(); } } $Self->Print("Starting...\n\n"); # Gather some statistics my %Stats; for my $Language (@Languages) { $Self->HandleLanguage( Language => $Language, Module => $Self->GetOption('module-directory'), WritePO => $Self->GetOption('generate-po'), Stats => \%Stats, ); } $Self->Print("\nTranslation statistics:\n"); for my $Language ( sort { ( $Stats{$b}->{Translated} // 0 ) <=> ( $Stats{$a}->{Translated} // 0 ) } keys %Stats ) { my $Strings = $Stats{$Language}->{Total}; my $Translations = $Stats{$Language}->{Translated} // 0; $Self->Print( "\t" . sprintf( "%7s", $Language ) . ": " ); $Self->Print( sprintf( "%02d", int( ( $Translations / $Strings ) * 100 ) ) ); $Self->Print( sprintf( "%% (%4d/%4d)\n", $Translations, $Strings ) ); } $Self->Print("\nDone.\n"); return $Self->ExitCodeOk(); } my @OriginalTranslationStrings; # Remember which strings came from JavaScript my %UsedInJS; sub HandleLanguage { my ( $Self, %Param ) = @_; my $Language = $Param{Language}; my $Module = $Param{Module} || ''; my $ModuleDirectory = $Module; my $LanguageFile; my $TargetFile; my $TargetPOTFile; my $TargetPOFile; my $IsSubTranslation; my $DefaultTheme = $Kernel::OM->Get('Kernel::Config')->Get('DefaultTheme'); # We need to map internal codes to the official ones used by Transifex my %TransifexLanguagesMap = ( sr_Cyrl => 'sr', sr_Latn => 'sr', ); my $TransifexLanguage = $TransifexLanguagesMap{$Language} // $Language; my $Home = $Kernel::OM->Get('Kernel::Config')->Get('Home'); if ( !$Module ) { $LanguageFile = "$Home/Kernel/Language/$Language.pm"; $TargetFile = "$Home/Kernel/Language/$Language.pm"; $TargetPOTFile = "$Home/i18n/otrs/otrs.pot"; $TargetPOFile = "$Home/i18n/otrs/otrs.$TransifexLanguage.po"; } else { $IsSubTranslation = 1; # extract module name from module path $Module = basename $Module; # remove underscores and/or version numbers and following from module name # i.e. FAQ_2_0 or FAQ20 $Module =~ s/((_|\-)?(\d+))+$//gix; # save module directory in target file $TargetFile = "$ModuleDirectory/Kernel/Language/${Language}_$Module.pm"; $TargetPOTFile = "$ModuleDirectory/i18n/$Module/$Module.pot"; $TargetPOFile = "$ModuleDirectory/i18n/$Module/$Module.$TransifexLanguage.po"; } my $WritePOT = $Param{WritePO} || -e $TargetPOTFile; if ( !-w $TargetFile ) { if ( -w $TargetPOFile ) { $Self->Print( "Creating missing file $TargetFile\n" ); } else { $Self->PrintError("Ignoring missing file $TargetFile!"); return; } } if ( !@OriginalTranslationStrings ) { $Self->Print( "Extracting source strings, this can take a moment...\n" ); # open .tt files and write new translation file my %UsedWords; my $TemplatesDirectory = $IsSubTranslation ? "$ModuleDirectory/Kernel/Output/HTML/Templates/$DefaultTheme" : "$Home/Kernel/Output/HTML/Templates/$DefaultTheme"; my @TemplateList; if ( -d $TemplatesDirectory ) { @TemplateList = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead( Directory => $TemplatesDirectory, Filter => '*.tt', Recursive => 1, ); } my $CustomTemplatesDir = "$ModuleDirectory/Custom/Kernel/Output/HTML/Templates/$DefaultTheme"; if ( $IsSubTranslation && -d $CustomTemplatesDir ) { my @CustomTemplateList = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead( Directory => $CustomTemplatesDir, Filter => '*.tt', Recursive => 1, ); push @TemplateList, @CustomTemplateList; } for my $File (@TemplateList) { my $ContentRef = $Kernel::OM->Get('Kernel::System::Main')->FileRead( Location => $File, Mode => 'utf8', ); if ( !ref $ContentRef ) { die "Can't open $File: $!"; } my $Content = ${$ContentRef}; $File =~ s{^.*/(.+?)\.tt}{$1}smx; # do translation $Content =~ s{ Translate\( \s* (["'])(.*?)(? "Template: $File", Source => $Word, }; } ''; }egx; } # Add strings from .html.tmpl files (JavaScript templates). my $JSTemplatesDirectory = $IsSubTranslation ? "$ModuleDirectory/Kernel/Output/JavaScript/Templates/$DefaultTheme" : "$Home/Kernel/Output/JavaScript/Templates/$DefaultTheme"; my @JSTemplateList; if ( -d $JSTemplatesDirectory ) { @JSTemplateList = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead( Directory => $JSTemplatesDirectory, Filter => '*.html.tmpl', Recursive => 1, ); } my $CustomJSTemplatesDir = "$ModuleDirectory/Custom/Kernel/Output/JavaScript/Templates/$DefaultTheme"; if ( $IsSubTranslation && -d $CustomJSTemplatesDir ) { my @CustomJSTemplateList = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead( Directory => $CustomJSTemplatesDir, Filter => '*.html.tmpl', Recursive => 1, ); push @JSTemplateList, @CustomJSTemplateList; } for my $File (@JSTemplateList) { my $ContentRef = $Kernel::OM->Get('Kernel::System::Main')->FileRead( Location => $File, Mode => 'utf8', ); if ( !ref $ContentRef ) { die "Can't open $File: $!"; } my $Content = ${$ContentRef}; $File =~ s{^.*/(.+?)\.html\.tmpl}{$1}smx; # Find strings marked for translation. $Content =~ s{ \{\{ \s* (["'])(.*?)(? "JS Template: $File", Source => $Word, }; } # Also save that this string was used in JS (for later use in Loader). $UsedInJS{$Word} = 1; ''; }egx; } # add translatable strings from Perl code my $PerlModuleDirectory = $IsSubTranslation ? "$ModuleDirectory/Kernel" : "$Home/Kernel"; my @PerlModuleList; if ( -d $PerlModuleDirectory ) { @PerlModuleList = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead( Directory => $PerlModuleDirectory, Filter => '*.pm', Recursive => 1, ); } # include Custom folder for modules my $CustomKernelDir = "$ModuleDirectory/Custom/Kernel"; if ( $IsSubTranslation && -d $CustomKernelDir ) { my @CustomPerlModuleList = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead( Directory => $CustomKernelDir, Filter => '*.pm', Recursive => 1, ); push @PerlModuleList, @CustomPerlModuleList; } # include var/packagesetup folder for modules my $PackageSetupDir = "$ModuleDirectory/var/packagesetup"; if ( $IsSubTranslation && -d $PackageSetupDir ) { my @PackageSetupModuleList = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead( Directory => $PackageSetupDir, Filter => '*.pm', Recursive => 1, ); push @PerlModuleList, @PackageSetupModuleList; } FILE: for my $File (@PerlModuleList) { next FILE if ( $File =~ m{cpan-lib}xms ); next FILE if ( $File =~ m{Kernel/Config/Files}xms ); my $ContentRef = $Kernel::OM->Get('Kernel::System::Main')->FileRead( Location => $File, Mode => 'utf8', ); if ( !ref $ContentRef ) { die "Can't open $File: $!"; } $File =~ s{^.*/(Kernel/)}{$1}smx; $File =~ s{^.*/(var/packagesetup/)}{$1}smx; my $Content = ${$ContentRef}; # Remove POD my $PodStrip = Pod::Strip->new(); $PodStrip->replace_with_comments(1); my $Code; $PodStrip->output_string( \$Code ); $PodStrip->parse_string_document($Content); # Purge all comments $Code =~ s{^ \s* # .*? \n}{\n}xmsg; # do translation $Code =~ s{ (?: ->Translate | Translatable ) \( \s* (["'])(.*?)(? "Perl Module: $File", Source => $Word, }; } ''; }egx; } # add translatable strings from DB XML my @DBXMLFiles = "$Home/scripts/database/otrs-initial_insert.xml"; if ($IsSubTranslation) { @DBXMLFiles = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead( Directory => "$ModuleDirectory", Filter => '*.sopm', ); } FILE: for my $File (@DBXMLFiles) { my $ContentRef = $Kernel::OM->Get('Kernel::System::Main')->FileRead( Location => $File, Mode => 'utf8', ); if ( !ref $ContentRef ) { die "Can't open $File: $!"; } $File =~ s{^.*/(scripts/)}{$1}smx; my $Content = ${$ContentRef}; # do translation $Content =~ s{ ]+Translatable="1"[^>]*>(.*?) } { my $Word = $1 // ''; if ($Word && !$UsedWords{$Word}++) { if ($IsSubTranslation) { $File =~ s{^.*/(.+\.sopm)}{$1}smx; } push @OriginalTranslationStrings, { Location => "Database XML Definition: $File", Source => $Word, }; } ''; }egx; } # add translatable strings from JavaScript code my $JSDirectory = $IsSubTranslation ? "$ModuleDirectory/var/httpd/htdocs/js" : "$Home/var/httpd/htdocs/js"; my @JSFileList; if ( -d $JSDirectory ) { @JSFileList = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead( Directory => $JSDirectory, Filter => '*.js', Recursive => 1, ); } FILE: for my $File (@JSFileList) { my $ContentRef = $Kernel::OM->Get('Kernel::System::Main')->FileRead( Location => $File, Mode => 'utf8', ); if ( !ref $ContentRef ) { die "Can't open $File: $!"; } # skip js cache files next FILE if ( $File =~ m{\/js\/js-cache\/}xmsg ); my $Content = ${$ContentRef}; # skip thirdparty files without custom markers if ( $File =~ m{\/js\/thirdparty\/}xmsg ) { next FILE if ( $Content !~ m{\/\/\s*OTRS}xmsg ); } $File =~ s{^.*/(.+?)\.js}{$1}smx; # Purge all comments $Content =~ s{^ \s* // .*? \n}{\n}xmsg; # do translation $Content =~ s{ (?: Core.Language.Translate ) \( \s* (["'])(.*?)(? "JS File: $File", Source => $Word, }; } # also save that this string was used in JS (for later use in Loader) $UsedInJS{$Word} = 1; ''; }egx; } # add translatable strings from SysConfig my @Strings = $Kernel::OM->Get('Kernel::System::SysConfig')->ConfigurationTranslatableStrings(); STRING: for my $String ( sort @Strings ) { next STRING if !$String || $UsedWords{$String}++; push @OriginalTranslationStrings, { Location => 'SysConfig', Source => $String, }; } } if ($IsSubTranslation) { $Self->Print( "Processing language $Language template files from $Module, writing output to $TargetFile\n" ); } else { $Self->Print( "Processing language $Language template files, writing output to $TargetFile\n" ); } # Language file, which only contains the OTRS core translations my $LanguageCoreObject = Kernel::Language->new( UserLanguage => $Language, TranslationFile => 1, ); # Language file, which contains all translations my $LanguageObject = Kernel::Language->new( UserLanguage => $Language, ); # Helpers for SR Cyr2Lat Transliteration my $TranslitObject; my $TranslitLanguageCoreObject; my $TranslitLanguageObject; my %TranslitLanguagesMap = ( sr_Latn => { SourceLanguage => 'sr_Cyrl', TranslitTable => 'ISO/R 9', }, ); if ( $TranslitLanguagesMap{$Language} ) { $TranslitObject = new Lingua::Translit( $TranslitLanguagesMap{$Language}->{TranslitTable} ); ## no critic $TranslitLanguageCoreObject = Kernel::Language->new( UserLanguage => $TranslitLanguagesMap{$Language}->{SourceLanguage}, TranslationFile => 1, ); $TranslitLanguageObject = Kernel::Language->new( UserLanguage => $TranslitLanguagesMap{$Language}->{SourceLanguage}, ); } my %POTranslations; if ( $WritePOT || $Param{WritePO} ) { %POTranslations = $Self->LoadPOFile( TargetPOFile => $TargetPOFile, ); } my @TranslationStrings; STRING: for my $OriginalTranslationString (@OriginalTranslationStrings) { my $String = $OriginalTranslationString->{Source}; # skip if we translate a module and the word already exists in the core translation next STRING if $IsSubTranslation && exists $LanguageCoreObject->{Translation}->{$String}; my $Translation; # transliterate word from existing translation if language supports it if ( $TranslitLanguagesMap{$Language} ) { $Translation = ( $IsSubTranslation ? $TranslitLanguageObject : $TranslitLanguageCoreObject )->{Translation} ->{$String}; $Translation = $TranslitObject->translit($Translation) || ''; } # lookup for existing translation else { $Translation = $POTranslations{$String} || ( $IsSubTranslation ? $LanguageObject : $LanguageCoreObject )->{Translation} ->{$String}; $Translation ||= ''; } push @TranslationStrings, { Location => $OriginalTranslationString->{Location}, Source => $String, Translation => $Translation, }; $Param{Stats}->{ $Param{Language} }->{Total}++; $Param{Stats}->{ $Param{Language} }->{Translated}++ if $Translation; } if ( $WritePOT && !$Self->{POTFileWritten}++ ) { $Self->WritePOTFile( TranslationStrings => \@TranslationStrings, TargetPOTFile => $TargetPOTFile, Module => $Module, ); } if ( $Param{WritePO} ) { $Self->WritePOFile( TranslationStrings => \@TranslationStrings, TargetPOTFile => $TargetPOTFile, TargetPOFile => $TargetPOFile, Module => $Module, ); } $Self->WritePerlLanguageFile( IsSubTranslation => $IsSubTranslation, LanguageCoreObject => $LanguageCoreObject, Language => $Language, Module => $Module, LanguageFile => $LanguageFile, TargetFile => $TargetFile, TranslationStrings => \@TranslationStrings, UsedInJS => \%UsedInJS, ); return 1; } sub LoadPOFile { my ( $Self, %Param ) = @_; return if !-e $Param{TargetPOFile}; $Kernel::OM->Get('Kernel::System::Main')->Require('Locale::PO') || die "Could not load Locale::PO"; my $POEntries = Locale::PO->load_file_asarray( $Param{TargetPOFile} ); my %POTranslations; ENTRY: for my $Entry ( @{$POEntries} ) { if ( $Entry->msgstr() ) { my $Source = $Entry->dequote( $Entry->msgid() ); $Source =~ s/\\{2}/\\/g; $Kernel::OM->Get('Kernel::System::Encode')->EncodeInput( \$Source ); my $Translation = $Entry->dequote( $Entry->msgstr() ); $Translation =~ s/\\{2}/\\/g; $Kernel::OM->Get('Kernel::System::Encode')->EncodeInput( \$Translation ); $POTranslations{$Source} = $Translation; } } return %POTranslations; } sub WritePOFile { my ( $Self, %Param ) = @_; $Kernel::OM->Get('Kernel::System::Main')->Require('Locale::PO') || die "Could not load Locale::PO"; if ( !-e $Param{TargetPOFile} ) { File::Copy::copy( $Param{TargetPOTFile}, $Param{TargetPOFile} ) || die "Could not copy $Param{TargetPOTFile} to $Param{TargetPOFile}: $!"; } my $POEntries = Locale::PO->load_file_asarray( $Param{TargetPOFile} ); my %POLookup; for my $Entry ( @{$POEntries} ) { my $Source = $Entry->dequote( $Entry->msgid() ); $Source =~ s/\\{2}/\\/g; $Kernel::OM->Get('Kernel::System::Encode')->EncodeInput( \$Source ); $POLookup{$Source} = $Entry; } for my $String ( @{ $Param{TranslationStrings} } ) { my $Source = $String->{Source}; $Source =~ s/\\/\\\\/g; $Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Source ); my $Translation = $String->{Translation}; $Translation =~ s/\\/\\\\/g; $Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Translation ); # Is there an entry in the PO already? if ( exists $POLookup{ $String->{Source} } ) { # Yes, update it $POLookup{ $String->{Source} }->msgstr($Translation); $POLookup{ $String->{Source} }->automatic( $String->{Location} ); } else { # No PO entry yet, create one. push @{$POEntries}, Locale::PO->new( -msgid => $Source, -msgstr => $Translation, -automatic => $String->{Location}, ); } } # Theoretically we could now also check for removed strings, but since the translations # are handled by transifex, this will not be needed as Transifex will handle that for us. Locale::PO->save_file_fromarray( $Param{TargetPOFile}, $POEntries ) || die "Could not save file $Param{TargetPOFile}: $!"; return 1; } sub WritePOTFile { my ( $Self, %Param ) = @_; my @POTEntries; $Kernel::OM->Get('Kernel::System::Main')->Require('Locale::PO') || die "Could not load Locale::PO"; my $Package = $Param{Module} // 'OTRS'; # build creation date, only YEAR-MO-DA HO:MI is needed without seconds my $CreationDate = $Kernel::OM->Create('Kernel::System::DateTime')->Format( Format => '%Y-%m-%d %H:%M+0000' ); push @POTEntries, Locale::PO->new( -msgid => '', -msgstr => "Project-Id-Version: $Package\n" . "POT-Creation-Date: $CreationDate\n" . "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" . "Last-Translator: FULL NAME \n" . "Language-Team: LANGUAGE \n" . "Language: \n" . "MIME-Version: 1.0\n" . "Content-Type: text/plain; charset=UTF-8\n" . "Content-Transfer-Encoding: 8bit\n", ); for my $String ( @{ $Param{TranslationStrings} } ) { my $Source = $String->{Source}; $Source =~ s/\\/\\\\/g; $Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Source ); push @POTEntries, Locale::PO->new( -msgid => $Source, -msgstr => '', -automatic => $String->{Location}, ); } Locale::PO->save_file_fromarray( $Param{TargetPOTFile}, \@POTEntries ) || die "Could not save file $Param{TargetPOTFile}: $!"; return; } sub WritePerlLanguageFile { my ( $Self, %Param ) = @_; my $LanguageCoreObject = $Param{LanguageCoreObject}; my $Indent = ' ' x 8; # 8 spaces for core files if ( $Param{IsSubTranslation} ) { $Indent = ' ' x 4; # 4 spaces for module files } my $Data = ''; my ( $StringsTotal, $StringsTranslated ); my $PreviousLocation = ''; for my $String ( @{ $Param{TranslationStrings} } ) { if ( $PreviousLocation ne $String->{Location} ) { $Data .= "\n"; $Data .= $Indent . "# $String->{Location}\n"; $PreviousLocation = $String->{Location}; } $StringsTotal++; if ( $String->{Translation} ) { $StringsTranslated++; } # Escape ' signs in strings my $Key = $String->{Source}; $Key =~ s/'/\\'/g; my $Translation = $String->{Translation}; $Translation =~ s/'/\\'/g; if ( $Param{IsSubTranslation} ) { if ( index( $Key, "\n" ) > -1 || length($Key) < $BreakLineAfterChars ) { $Data .= $Indent . "\$Self->{Translation}->{'$Key'} = '$Translation';\n"; } else { $Data .= $Indent . "\$Self->{Translation}->{'$Key'} =\n"; $Data .= $Indent . ' ' . "'$Translation';\n"; } } else { if ( index( $Key, "\n" ) > -1 || length($Key) < $BreakLineAfterChars ) { $Data .= $Indent . "'$Key' => '$Translation',\n"; } else { $Data .= $Indent . "'$Key' =>\n"; $Data .= $Indent . ' ' . "'$Translation',\n"; } } } # add data structure for JS translations my $JSData = " \$Self->{JavaScriptStrings} = [\n"; if ( $Param{IsSubTranslation} ) { $JSData = ' push @{ $Self->{JavaScriptStrings} // [] }, (' . "\n"; } for my $String ( sort keys %{ $Param{UsedInJS} // {} } ) { my $Key = $String; $Key =~ s/'/\\'/g; $JSData .= $Indent . "'" . $Key . "',\n"; } if ( $Param{IsSubTranslation} ) { $JSData .= " );\n"; } else { $JSData .= " ];\n"; } my %MetaData; my $NewOut = ''; # translating a module if ( $Param{IsSubTranslation} ) { # needed for cvs check-in filter my $Separator = "# --"; $NewOut = <<"EOF"; $Separator # Copyright (C) 2001-2019 OTRS AG, https://otrs.com/ $Separator # This software comes with ABSOLUTELY NO WARRANTY. For details, see # the enclosed file COPYING for license information (GPL). If you # did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt. $Separator package Kernel::Language::$Param{Language}_$Param{Module}; use strict; use warnings; use utf8; sub Data { my \$Self = shift; $Data $JSData } 1; EOF } # translating the core else { ## no critic open( my $In, '<', $Param{LanguageFile} ) || die "Can't open: $Param{LanguageFile}\n"; ## use critic while (<$In>) { my $Line = $_; $Kernel::OM->Get('Kernel::System::Encode')->EncodeInput( \$Line ); if ( !$MetaData{DataPrinted} ) { $NewOut .= $Line; } if ( $_ =~ /\$\$START\$\$/ && !$MetaData{DataPrinted} ) { $MetaData{DataPrinted} = 1; $NewOut .= " # possible charsets\n"; $NewOut .= " \$Self->{Charset} = ["; for ( $LanguageCoreObject->GetPossibleCharsets() ) { $NewOut .= "'$_', "; } $NewOut .= "];\n"; my $Completeness = 0; if ($StringsTranslated) { $Completeness = $StringsTranslated / $StringsTotal; } $NewOut .= <<"EOF"; # date formats (\%A=WeekDay;\%B=LongMonth;\%T=Time;\%D=Day;\%M=Month;\%Y=Year;) \$Self->{DateFormat} = '$LanguageCoreObject->{DateFormat}'; \$Self->{DateFormatLong} = '$LanguageCoreObject->{DateFormatLong}'; \$Self->{DateFormatShort} = '$LanguageCoreObject->{DateFormatShort}'; \$Self->{DateInputFormat} = '$LanguageCoreObject->{DateInputFormat}'; \$Self->{DateInputFormatLong} = '$LanguageCoreObject->{DateInputFormatLong}'; \$Self->{Completeness} = $Completeness; # csv separator \$Self->{Separator} = '$LanguageCoreObject->{Separator}'; \$Self->{DecimalSeparator} = '$LanguageCoreObject->{DecimalSeparator}'; \$Self->{ThousandSeparator} = '$LanguageCoreObject->{ThousandSeparator}'; EOF if ( $LanguageCoreObject->{TextDirection} ) { $NewOut .= <<"EOF"; # TextDirection rtl or ltr \$Self->{TextDirection} = '$LanguageCoreObject->{TextDirection}'; EOF } $NewOut .= <<"EOF"; \$Self->{Translation} = { $Data }; EOF $NewOut .= $JSData . "\n"; } if ( $_ =~ /\$\$STOP\$\$/ ) { $NewOut .= $Line; $MetaData{DataPrinted} = 0; } } close $In; } my $TargetFile = $Param{TargetFile}; if ( -e $TargetFile ) { rename( $TargetFile, "$TargetFile.old" ) || die $!; } $Kernel::OM->Get('Kernel::System::Main')->FileWrite( Location => $TargetFile, Content => \$NewOut, Mode => 'utf8', # binmode|utf8 ); return 1; } 1;