From 37f5fd7689567699aba107bdfa761c40ada563bf Mon Sep 17 00:00:00 2001 From: Didier 'OdyX' Raboud Date: Fri, 25 Oct 2013 15:18:18 +0200 Subject: [PATCH 1/8] Add CMake-Po4a-based infrastructure to allow po-based translation of Colobot help- and scene-description (level) files --- levels-i18n/CMakeLists.txt | 114 ++++++++++ levels-i18n/scripts/01_create_po4a_file.sh | 42 ++++ levels-i18n/scripts/02_run_po4a.sh | 10 + levels-i18n/scripts/03_inject_translations.sh | 28 +++ .../scripts/04_move_helpfiles_to_paths.sh | 42 ++++ ...05_move_untranslated_helpfiles_to_paths.sh | 14 ++ .../perllib/Locale/Po4a/Colobothelp.pm | 194 ++++++++++++++++++ .../perllib/Locale/Po4a/Colobotlevel.pm | 93 +++++++++ 8 files changed, 537 insertions(+) create mode 100644 levels-i18n/CMakeLists.txt create mode 100755 levels-i18n/scripts/01_create_po4a_file.sh create mode 100755 levels-i18n/scripts/02_run_po4a.sh create mode 100755 levels-i18n/scripts/03_inject_translations.sh create mode 100755 levels-i18n/scripts/04_move_helpfiles_to_paths.sh create mode 100755 levels-i18n/scripts/05_move_untranslated_helpfiles_to_paths.sh create mode 100644 levels-i18n/scripts/perllib/Locale/Po4a/Colobothelp.pm create mode 100644 levels-i18n/scripts/perllib/Locale/Po4a/Colobotlevel.pm diff --git a/levels-i18n/CMakeLists.txt b/levels-i18n/CMakeLists.txt new file mode 100644 index 00000000..1a0c1e28 --- /dev/null +++ b/levels-i18n/CMakeLists.txt @@ -0,0 +1,114 @@ +cmake_minimum_required(VERSION 2.8) + +# Meta-infrastructure to allow po-based translation of Colobot help files and scene-description (level) files. + +# set(LEVELS_I18N_PATH ${CMAKE_CURRENT_SOURCE_DIR}) + +# set(LEVEL_CODENAME "scene100") +# set(SCENEFILE "scene100/scene.txt") +# set(SCENEDEST "scene101.txt") # If unset , use the same name as SCENEFILE +# set(PODIR "scene100/po/") +# set(HELPDIR "scene100/help/") +# set(HELPDEST "") + +project(colobot-level-${LEVEL_CODENAME} NONE) + +if(NOT DEFINED COLOBOT_INSTALL_DATA_DIR) + set(COLOBOT_INSTALL_DATA_DIR ${CMAKE_INSTALL_PREFIX}/share/games/colobot CACHE PATH "Colobot shared data directory") +endif() +set(LEVEL_INSTALL_DATA_DIR ${COLOBOT_INSTALL_DATA_DIR}/levels/) +set(HELP_INSTALL_DATA_DIR ${COLOBOT_INSTALL_DATA_DIR}/help/) + +# Translate translatable material +find_program(PO4A po4a) + +### STEP 3 # Inject translations in levels +set(_levels_i18n_target "levels_i18n") + +file(GLOB levelfiles "${CMAKE_CURRENT_SOURCE_DIR}/${SCENEFILE}") +foreach(levelfile ${levelfiles}) + if(DEFINED SCENEDEST) + get_filename_component(_scenedest ${SCENEDEST} NAME) + else() + get_filename_component(_scenedest ${levelfile} NAME) + endif() + get_filename_component(_levelfile ${levelfile} NAME_WE) + if(PO4A AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${PODIR}") + add_custom_command(OUTPUT ${_levels_i18n_target}/${_scenedest} + COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/${_levels_i18n_target} + COMMAND ${LEVELS_I18N_PATH}/scripts/03_inject_translations.sh ${levelfile} ${CMAKE_CURRENT_BINARY_DIR}/${LEVEL_CODENAME}/${_levelfile} ${PODIR} > ${CMAKE_CURRENT_BINARY_DIR}/${_levels_i18n_target}/${_scenedest} + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/po4a-ran-${LEVEL_CODENAME} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "${LEVEL_CODENAME}: Inject translation in ${_scenedest}" + ) + else() + add_custom_command(OUTPUT ${_levels_i18n_target}/${_scenedest} + COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/${_levels_i18n_target} + COMMAND cp ${levelfile} ${CMAKE_CURRENT_BINARY_DIR}/${_levels_i18n_target}/${_scenedest} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "${LEVEL_CODENAME}: Move untranslatable ${_scenedest}" + ) + endif() + add_custom_target(level_i18n-${LEVEL_CODENAME}_${_levelfile} ALL DEPENDS ${_levels_i18n_target}/${_scenedest}) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${_levels_i18n_target}/${_scenedest} + DESTINATION ${LEVEL_INSTALL_DATA_DIR}) +endforeach() + +file(GLOB helpfiles "${CMAKE_CURRENT_SOURCE_DIR}/${HELPDIR}/E/*.txt") +list(SORT helpfiles) + +if(PO4A AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${PODIR}") + foreach(helpfile ${helpfiles}) + get_filename_component(_helpfile_we ${helpfile} NAME_WE) + get_filename_component(_helpfile ${helpfile} NAME) + + ### STEP 4 # Put translated help files in the correct place + set(_help_i18n_target "help_i18n_${LEVEL_CODENAME}_${_helpfile_we}") + add_custom_command(OUTPUT moved-all-${LEVEL_CODENAME}-${_helpfile} + COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/${_help_i18n_target} + COMMAND ${LEVELS_I18N_PATH}/scripts/04_move_helpfiles_to_paths.sh ${LEVEL_CODENAME} ${LEVEL_CODENAME}-help/${_helpfile} ${_help_i18n_target} '${HELPDIR}' '${HELPDEST}' ${CMAKE_CURRENT_SOURCE_DIR} > moved-all-${LEVEL_CODENAME}-${_helpfile} + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/po4a-ran-${LEVEL_CODENAME} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "${LEVEL_CODENAME}: Move ${_helpfile} translations around" + ) + add_custom_target(help_i18n-${LEVEL_CODENAME}_${_helpfile_we} ALL DEPENDS moved-all-${LEVEL_CODENAME}-${_helpfile}) + install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${_help_i18n_target}/ + DESTINATION ${HELP_INSTALL_DATA_DIR}) + + endforeach() +elseif(DEFINED HELPDIR AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${HELPDIR}") + set(_help_i18n_target "help_i18n_${LEVEL_CODENAME}") + add_custom_command(OUTPUT moved-all-${LEVEL_CODENAME} + COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/${_help_i18n_target} + COMMAND ${LEVELS_I18N_PATH}/scripts/05_move_untranslated_helpfiles_to_paths.sh '${_help_i18n_target}' '${HELPDEST}' '${CMAKE_CURRENT_SOURCE_DIR}/${HELPDIR}' > moved-all-${LEVEL_CODENAME}-${_helpfile} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "${LEVEL_CODENAME}: Move untranslatable help files" + ) + add_custom_target(help_i18n-${LEVEL_CODENAME} ALL DEPENDS moved-all-${LEVEL_CODENAME}) + install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${_help_i18n_target}/ + DESTINATION ${HELP_INSTALL_DATA_DIR}) + +endif() + + +#### STEP 1 # Create po4a.cfg file out of the list of files +add_custom_command(OUTPUT po4a-${LEVEL_CODENAME}.cfg + COMMAND ${LEVELS_I18N_PATH}/scripts/01_create_po4a_file.sh ${CMAKE_CURRENT_SOURCE_DIR} ${PODIR} ${LEVEL_CODENAME} '${SCENEFILE}' '${HELPDIR}/E' > ${CMAKE_CURRENT_BINARY_DIR}/po4a-${LEVEL_CODENAME}.cfg + COMMENT "${LEVEL_CODENAME}: Create po4a configuration file" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + +if(PO4A AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${PODIR}) + #### STEP 2 # Run po4a + add_custom_command(OUTPUT po4a-ran-${LEVEL_CODENAME} + COMMAND ${LEVELS_I18N_PATH}/scripts/02_run_po4a.sh ${LEVELS_I18N_PATH} ${CMAKE_CURRENT_BINARY_DIR}/po4a-${LEVEL_CODENAME}.cfg + COMMAND touch po4a-ran-${LEVEL_CODENAME} + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/po4a-${LEVEL_CODENAME}.cfg + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "${LEVEL_CODENAME}: Run po4a to generate the translated files out of the .po files") + add_custom_target(po4a-${LEVEL_CODENAME} DEPENDS po4a-ran-${LEVEL_CODENAME}) +else() + message(STATUS "${LEVEL_CODENAME}: No translation needed or possible") + file(GLOB levelfiles "${CMAKE_CURRENT_SOURCE_DIR}/levels/*.txt") + install(FILES ${levelfiles} DESTINATION ${LEVEL_INSTALL_DATA_DIR}/) +endif() diff --git a/levels-i18n/scripts/01_create_po4a_file.sh b/levels-i18n/scripts/01_create_po4a_file.sh new file mode 100755 index 00000000..f1650487 --- /dev/null +++ b/levels-i18n/scripts/01_create_po4a_file.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +set -e + +# MUST be run from ${CMAKE_CURRENT_BINARY_DIR} + +srcdir=$1 # absolute +PODIR=$2 # relative +LEVEL_CODENAME=$3 # relative +SCENEFILE=$4 # relative +HELPDIR=$5 # relative + +rm -Rf $LEVEL_CODENAME-po +ln -s $srcdir/$PODIR $LEVEL_CODENAME-po +echo "[po_directory] $LEVEL_CODENAME-po" + +# Create a pseudo file for the translation of the language code +echo "[type:text] ${LEVEL_CODENAME}.languagecode \$lang:${LEVEL_CODENAME}.\$lang.languagecode" +echo "E" > ${LEVEL_CODENAME}.languagecode + +# Create symlink for relative paths in po4a +mkdir -p $LEVEL_CODENAME + +if [ -n "$SCENEFILE" ]; then + # Levels are precompiled, they are already in the current dir + for scene in $(cd $srcdir/; ls $SCENEFILE); do + scene_=$(basename $scene .txt) + $(cd $LEVEL_CODENAME; ln -s $srcdir/$scene $scene_.txt) + echo "[type:colobotlevel] $LEVEL_CODENAME/$scene_.txt \$lang:$LEVEL_CODENAME/$scene_.\$lang.txt" + done +fi + +# Create symlink for relative paths in po4a +mkdir -p $LEVEL_CODENAME-help + +if [ -d $srcdir/$HELPDIR ]; then + for helpfile in $(cd $srcdir/$HELPDIR; ls *.txt); do + helpfile_=$(basename $helpfile .txt) + $(cd $LEVEL_CODENAME-help; ln -s $srcdir/$HELPDIR/$helpfile $helpfile_.txt) + echo "[type:colobothelp] $LEVEL_CODENAME-help/$helpfile_.txt \$lang:$LEVEL_CODENAME-help/$helpfile_.\$lang.txt" + done +fi diff --git a/levels-i18n/scripts/02_run_po4a.sh b/levels-i18n/scripts/02_run_po4a.sh new file mode 100755 index 00000000..96030d1c --- /dev/null +++ b/levels-i18n/scripts/02_run_po4a.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +set -e + +LEVELS_I18N_PATH=$1 +PO4A_FILE=$2 + +export PERLLIB=${LEVELS_I18N_PATH}/scripts/perllib + +po4a -k100 -v -f $PO4A_FILE diff --git a/levels-i18n/scripts/03_inject_translations.sh b/levels-i18n/scripts/03_inject_translations.sh new file mode 100755 index 00000000..de41b501 --- /dev/null +++ b/levels-i18n/scripts/03_inject_translations.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +set -e + +levelfile=$1 +translations_prefix=$2 +PODIR=$3 + +rootfilename=$(basename $translations_prefix) + +# Autodetect translated languages +script_path=$(dirname $0) +linguas=$(cd $PODIR/; ls *.po | sed -e 's/\.po$//g') + +# Make sure we take english (first, but it's not really important) +for lang in en $linguas; do + dotlang=".$lang" + if [ "$lang" = "en" ]; then + dotlang=""; + fi + i18nfile=$translations_prefix$dotlang.txt + if [ -f $i18nfile ]; then + sed -n '/^Title/p;/^Resume/p;/^ScriptName/p' $i18nfile + fi +done +echo "// End of level headers translations" +echo "" +sed -e '/^Title/d;/^Resume/d;/^ScriptName/d' $levelfile diff --git a/levels-i18n/scripts/04_move_helpfiles_to_paths.sh b/levels-i18n/scripts/04_move_helpfiles_to_paths.sh new file mode 100755 index 00000000..45b7c372 --- /dev/null +++ b/levels-i18n/scripts/04_move_helpfiles_to_paths.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +set -e + +codename=$1 +orig_file=$2 +dest_dir=$3 +HELPDIR=$4 +HELPDEST=$5 +CMAKE_CURRENT_SOURCE_DIR=$6 + +orig_l10n_file=$(basename $orig_file .txt) +orig_l10n_dir=$(dirname $orig_file) + +# Get all language codes +for lcodef in $(ls ${codename}.*.languagecode); do + # Get the one-letter language code + LCODE=$(cat $lcodef) + lang=$(echo $lcodef | sed -e 's/.*\.\(.*\)\.languagecode/\1/') + # Foreach, rename the source files + mkdir -p $dest_dir/$LCODE/$HELPDEST + + orig_trans=$CMAKE_CURRENT_SOURCE_DIR/$HELPDIR/$LCODE/$orig_l10n_file.txt + + # Copy the translated file to the correct pathi + if [ -e $orig_l10n_dir/$orig_l10n_file.$lang.txt ]; then + cp -Lf $orig_l10n_dir/$orig_l10n_file.$lang.txt $dest_dir/$LCODE/$HELPDEST/$orig_l10n_file.txt + # Mark the source file that the translation is supposed to replace # Replace false by true to make it happen + if [ -e $orig_trans ] && false; then + sed -e '/Obsoleted translation/d;/This translated file/d' $orig_trans >$orig_trans.temp + echo "\\\\b; Obsoleted translation\nThis translated file has been replaced by it's PO-based counterpart and should be removed.\n" > $orig_trans + cat $orig_trans.temp >> $orig_trans + rm $orig_trans.temp + fi + elif [ -e $orig_trans ]; then + cp -Lf $orig_trans $dest_dir/$LCODE/$HELPDEST/$orig_l10n_file.txt + fi +done + +mkdir -p $dest_dir/E/$HELPDEST +# Copy the english file to the correct path too +cp -Lf $orig_file $dest_dir/E/$HELPDEST/$orig_l10n_file.txt diff --git a/levels-i18n/scripts/05_move_untranslated_helpfiles_to_paths.sh b/levels-i18n/scripts/05_move_untranslated_helpfiles_to_paths.sh new file mode 100755 index 00000000..150fe8da --- /dev/null +++ b/levels-i18n/scripts/05_move_untranslated_helpfiles_to_paths.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +set -e + +HELP_I18N_TARGET=$1 +HELPDEST=$2 +HELPDIR_FULL=$3 + +for langdir in $(ls ${HELPDIR_FULL}); do + if [ "$langdir" != "po" ]; then + mkdir -p ${HELP_I18N_TARGET}/$langdir/${HELPDEST} + cp ${HELPDIR_FULL}/$langdir/* ${HELP_I18N_TARGET}/$langdir/${HELPDEST}/ + fi +done diff --git a/levels-i18n/scripts/perllib/Locale/Po4a/Colobothelp.pm b/levels-i18n/scripts/perllib/Locale/Po4a/Colobothelp.pm new file mode 100644 index 00000000..8e3b5151 --- /dev/null +++ b/levels-i18n/scripts/perllib/Locale/Po4a/Colobothelp.pm @@ -0,0 +1,194 @@ +# Locale::Po4a::Colobothelp -- Convert Colobot help files +# +# This program is free software; you may redistribute it and/or modify it +# under the terms of GPLv3. +# + +use Locale::Po4a::TransTractor qw(process new); +use Locale::Po4a::Common; +use Locale::Po4a::Text; + +package Locale::Po4a::Colobothelp; + +use 5.006; +use strict; +use warnings; + +require Exporter; + +use vars qw(@ISA @EXPORT $AUTOLOAD); +@ISA = qw(Locale::Po4a::TransTractor); +@EXPORT = qw(); + +my @comments = (); + +sub initialize {} + +sub parse { + my $self = shift; + my ($line,$ref); + my $paragraph=""; + my $wrapped_mode = 1; + my $s_mode = 0; + my $expect_header = 1; + my $end_of_paragraph = 0; + ($line,$ref)=$self->shiftline(); + while (defined($line)) { + chomp($line); + $self->{ref}="$ref"; + ($paragraph,$wrapped_mode,$s_mode,$expect_header,$end_of_paragraph) = parse_colobothelp($self,$line,$ref,$paragraph,$wrapped_mode,$s_mode,$expect_header,$end_of_paragraph); + if ($end_of_paragraph) { + do_paragraph($self,offlink($paragraph),$wrapped_mode); + $paragraph=""; + $wrapped_mode = 1; + $end_of_paragraph = 0; + } + ($line,$ref)=$self->shiftline(); + } + if (length $paragraph) { + $paragraph =~ s/\n$//; + do_paragraph($self,$paragraph,$wrapped_mode); + $self->pushline("\n"); + } +} + +sub parse_colobothelp { + my ($self,$line,$ref,$paragraph,$wrapped_mode,$s_mode,$expect_header,$end_of_paragraph) = @_; + + if (($s_mode == 1) and ($line !~ /^\\s;/)) { + # Process the end of \s; blocks + $s_mode = 0; + # substr removes the last superfluous \n + my $s_block = onlink($self->translate(substr(offlink($paragraph),0,-1),$ref,"\\s; block (usually verbatim code)")); + $s_block =~ s/(\n|^)/$1\\s;/g; + $self->pushline($s_block."\n"); + $paragraph=""; + $wrapped_mode = 0; + } + + if ( $line =~ /^\s*$/ + or $line =~ m/^\\[nctr];$/) { + # Break paragraphs on lines containing only spaces or any of \n; \c; \t; \r; (alone) + + # Drop the latest EOL to avoid having it in the translation + my $dropped_eol = ($paragraph =~ s/\n$//); + do_paragraph($self,$paragraph,$wrapped_mode); + $self->pushline("\n") if $dropped_eol; # Therefore only add it back if it was removed + $paragraph=""; + $wrapped_mode = 0; + $self->pushline($line."\n"); + } elsif ($line =~ s/^(\\s;)//) { + # Lines starting with \s; are special (yellow-background, usually code-block) + # Break paragraph before them + if($s_mode == 0) { + $s_mode = 1; + my $dropped_eol = ($paragraph =~ s/\n$//); + do_paragraph($self,$paragraph,$wrapped_mode); + $self->pushline("\n") if $dropped_eol; # Therefore only add it back if it was removed + $paragraph=""; + $wrapped_mode = 0; + } + $paragraph .= $line."\n"; + } elsif ($line =~ s/^(\\[bt];)//) { + # Break paragraphs on \b; or \t; headers + do_paragraph($self,$paragraph,$wrapped_mode); + $paragraph=""; + $wrapped_mode = 1; + + $self->pushline($1.onlink($self->translate(offlink($line),$ref,"$1 header")."\n")); + } elsif ($line =~ /^\\image (.*) (\d*) (\d*);$/) { + # Discard lines with \image name lx ly; tags + do_paragraph($self,$paragraph,$wrapped_mode); + $paragraph=""; + $wrapped_mode = 1; + + $self->pushline("\\image ".$self->translate($1,$ref,'Image filename')." $2 $3;\n"); + } elsif ( $line =~ /^=+$/ + or $line =~ /^_+$/ + or $line =~ /^-+$/) { + $wrapped_mode = 0; + $paragraph .= $line."\n"; + do_paragraph($self,$paragraph,$wrapped_mode); + $paragraph=""; + $wrapped_mode = 1; + } elsif ($line =~ s/^(\s*)([0-9]\)|[o-])(\s*)//) { + # Break paragraphs on lines starting with either number + parenthesis or any of o- + space + do_paragraph($self,$paragraph,$wrapped_mode); + $paragraph=""; + $wrapped_mode = 1; + + $self->pushline("$1$2$3".onlink($self->translate(offlink($line),$ref,"Bullet: '$2'")."\n")); + } else { + # All paragraphs are non-wrap paragraphs by default + $wrapped_mode = 0; + undef $self->{bullet}; + undef $self->{indent}; + $paragraph .= $line."\n"; + } + return ($paragraph,$wrapped_mode,$s_mode,$expect_header,$end_of_paragraph); +} + +sub offlink { + my ($paragraph) = @_; + # Put \const;Code\norm; sequences into pseudo-HTML tags + $paragraph =~ s#\\(const|type|token);([^\\;]*?)\\norm;#$2#g; + # Transform CBot links \l;text\u target; into pseudo-HTML text + $paragraph =~ s#\\l;(.*?)\\u ([^;]*?);#$1#g; + # Cleanup pseudo-html targets separated by \\ to have a single character | + $paragraph =~ s###g; + # Put \c;Code\n; sequences into pseudo-HTML tags + $paragraph =~ s#\\c;([^\\;]*?)\\n;#$1#g; + # Replace remnants of \s; \c; \b; or \n; as pseudo xHTML tags + $paragraph =~ s#\\([scbn]);#<$1/>#g; + # Replace remnants of \const; \type; \token or \norm; as pseudo xHTML tags + $paragraph =~ s#\\(const|type|token|norm);#<$1/>#g; + # Replace \button $id; as pseudo xHTML