#!/usr/bin/env bash

define() { IFS='\n' read -r -d '' ${1} || true ; }

myname="${0##*/}"

define pod <<"=cut"

=encoding utf-8

=head1 NAME

    xlate - TRANSlate CLI front-end for App::Greple::xlate module

=head1 SYNOPSIS

    xlate [ options ] -t LANG FILE [ greple options ]
        -h   help
        -v   show version
        -d   debug
        -n   dry-run
        -a   use API
        -c   just check translation area
        -r   refresh cache
        -u   force update cache
        -s   silent mode
        -t # target language (required, no default)
        -b # base language (informational)
        -e # translation engine (*deepl, gpt3, gpt4, gpt4o)
        -p # pattern string to determine translation area
        -f # pattern file to determine translation area
        -o # output format (*xtxt, cm, ifdef, space, space+, colon)
        -x # file containing mask patterns
        -w # wrap line by # width
        -m # max length per API call
        -l # show library files (XLATE.mk, xlate.el)
        --   end of option
        N.B. default is marked as *

    Make options
        -M   run make
        -n   dry-run

    Docker options
        -D * run xlate on the container with the same parameters
        -C * execute following command on the container, or run shell
        -L * use the live container
        N.B. These options terminate option handling

        -W   mount current working directory
        -H   mount home directory
        -V # specify mount directory
        -U   do not mount
        -R   mount read-only
        -B   run container in batch mode
        -N   specify the name of live container
        -K   kill and remove live container
        -E # specify an environment variable to be inherited
        -I # docker image or version (default: tecolicom/xlate:version)

    Control Files:
        *.LANG    translation languages
        *.FORMAT  translation format (xtxt, cm, ifdef, colon, space)
        *.ENGINE  translation engine (deepl, gpt3, gpt4, gpt4o)

=head1 VERSION

    Version 0.9914

=cut

[[ $pod =~ Version\ +([0-9.]+) ]] && my_version=${BASH_REMATCH[1]}

git_topdir() {
    : git_topdir
    [[ $(git rev-parse --is-inside-work-tree 2>/dev/null) == true ]] || return
    local         dir="$(git rev-parse --show-superproject-working-tree)"
    [[ $dir ]] || dir="$(git rev-parse --show-toplevel)"
    echo "$dir"
}

get_ip() {
    : get_ip
    local ip=$(ifconfig 2>/dev/null | awk '/inet /{print $2}' | tail -1)
    echo "$ip"
}

dist_dir() {
    perl -MFile::Share=:all -E "say dist_dir '$1'"
}

warn() {
    [[ $quiet ]] && return
    echo "$@" >&2
}

topdir=$(git_topdir)
pwd=$(pwd)
owner=tecolicom
image=
repository=$owner/$myname
hostname=
mount=yes
remove=yes
interactive=yes
container=
version=
workdir=/work
mount_mode=rw
read_only=
display=
localtime=/etc/localtime
givenname=
detach=
volume=
volumes=()
ports=()
others=()

ENV=(
    LANG TZ
    HTTP_PROXY HTTPS_PROXY
    http_proxy https_proxy
    TERM_PROGRAM TERM_BGCOLOR COLORTERM
    DEEPL_AUTH_KEY
    OPENAI_API_KEY
    ANTHROPIC_API_KEY
    LLM_PERPLEXITY_KEY
)

container_name() {
    : container_name
    local name
    if [[ $givenname ]]
    then
	name="$givenname"
    else
	if [[ $image =~ (.*/)?([-_.a-zA-Z]+) ]]
	then name=${BASH_REMATCH[2]}
	else name=$myname
	fi
	[[ $volume ]] && name+=.${volume##*/}
    fi
    echo "$name"
}

docker_find() {
    : docker_find
    local OPT OPTARG OPTIND status id
    while getopts "s:" OPT
    do
	case $OPT in
	    s) status+=("$OPTARG") ;;
	esac
    done
    shift $((OPTIND - 1))
    id=$(docker ps -a -q ${status[@]/#/-f status=} -f name="^/$1\$")
    [[ $id ]] && echo "$id" || return 1
}

docker_status() {
    docker inspect --format='{{.State.Status}}' $1
}


##
## read .xlaterc
##
declare -a rcpath=(.)
[[ $topdir && $topdir != $pwd ]] && rcpath+=($topdir)
[[ $HOME != $pwd ]] && rcpath+=($HOME)
for dir in ${rcpath[@]}
do
    rc="$dir/.xlaterc"
    [[ -r $rc ]] || continue
    mapfile -t OPTS < <(grep -v ^# "$rc")
    warn "$myname: ${OPTS[@]}"
    set -- ${OPTS[@]} "$@"
done

##
## Docker
##
if [[ $topdir && $topdir != $pwd ]]
then
    volume="$topdir"
fi
while getopts :dq${DOCKER_OPT:=dJ:URAI:E:WHP:O:BV:GN:KSALCD} OPT
do
    [[ $XLATE_RUNNING_ON_DOCKER ]] && break
    case $OPT in
	d) debug=yes ; set -x ;;
	q) quiet=yes ;;
	I)
	    if [[ $OPTARG =~ ^:(.+)$ ]]
	    then
		version="${BASH_REMATCH[1]}"
	    else 
		image="$OPTARG"
	    fi
	    ;;
	E) ENV+=("$OPTARG") ;;
	P) ports+=("$OPTARG") ;;
	O) others+=("$OPTARG") ;;
	B) interactive= ;;

	# mount related options
	J) workdir="$OPTARG" ;;
	U) mount= ;;
	R) mount_mode=ro ;;
	V) volumes+=("$OPTARG") ;;
	W|H|G)
	    case $OPT in
		W)  volume="$pwd" ;;
		H)  volume="$HOME"
		    ENV+=(HOME=$workdir)
		    ;;
		G)  ;;
	    esac
	    ;;

	N) givenname="$OPTARG" ;;
	K)
	    : ${container:=$(container_name)}
	    if id=$(docker_find "$container")
	    then
		docker rm -f "$container" | sed -e "s/\$/ ($id) is removed/"
	    fi
	    [[ ${@:$OPTIND:1} =~ L ]] && (( OPTIND++ )) || exit
	    ;&
	L)
	    : ${container:=$(container_name)}
	    if id=$(docker_find "$container")
	    then
		status=$(docker_status $id)
		case $status in
		    exited)
			warn start the "$container ($id)"
			id=$(docker start $id) || exit 1
			;;
		    paused)
			warn unpause the "$container ($id)"
			id=$(docker unpause $id) || exit 1
			;;
		    running)
			;;
		    *)
			warn unknown status $status of "$container ($id)"
			;;
		esac
	    fi
	    if id=$(docker_find -s running "$container")
	    then
		shift $((OPTIND - 1))
		if (( $# > 0 ))
		then
		    declare -a dockopt
		    [ $interactive ] && dockopt+=(--interactive)
		    [ $interactive ] && [ -t 0 ] && dockopt+=(--tty)
		    export DOCKER_CLI_HINTS=false
		    exec docker exec ${dockopt[@]} $id "$@" || exit 1
		else
		    warn attach to the "$container ($id)"
		    exec docker attach $id || exit 1
		fi
		exit
	    fi
	    warn create live conainer \"$container\"
	    remove=
	    ;&
	C|D)
	    if [[ $mount && $volume && $volume != $pwd ]]
	    then
		warn "Mount $volume to $workdir"
		[[ ${pwd#$volume/} != $pwd ]] && relative="${pwd#$volume/}"
	    fi
	    if [[ $DISPLAY && "${ip:=$(get_ip)}" ]]
	    then
		display="$ip:0"
	    fi
	    if [[ ! $version && $my_version =~ ^[0-9]+\.[0-9_]+$ ]]
	    then
		version=$my_version
	    fi
	    declare -a command
	    case "$OPT" in
		D) command=(xlate) ;;
		*) shift $((OPTIND - 1)) ;;
	    esac
	    command+=("$@")
	    : ${image:=${repository}${version:+:$version}}
	    : ${hostname:=$(<<< ${image} sed -e s:.*/:: -e s/:.*//)}
	    declare -a dockopt=(
		-e XLATE_RUNNING_ON_DOCKER=1
		--init
	    )
	    [[ $interactive ]]  && dockopt+=(--interactive)
	    [[ $interactive ]]  && [[ -t 0 ]] && dockopt+=(--tty)
	    [[ $remove ]]       && dockopt+=(--rm)
	    [[ $remove ]]       || dockopt+=(${container:+--name "$container"})
	    [[ -e $localtime ]] && dockopt+=(-v $localtime:$localtime:ro)
	    [[ $mount ]]        && dockopt+=(
		    -v "${volume:-$pwd}:${workdir}:${mount_mode}" 
		    -w "${workdir}${relative:+/${relative}}"
		)
	    dockopt+=(
		${detach:+--detach}
		${read_only:+--read-only}
		${hostname:+--hostname "${hostname}"}
		${display:+-e DISPLAY="$display"}
		${ports[@]:+${ports[@]/#/-p }}
		${volumes[@]:+${volumes[@]/#/-v }}
		${others[@]}
		${ENV[@]/#/-e }
	    )
	    exec=(docker run "${dockopt[@]}" "${image}" "${command[@]}")
	    exec "${exec[@]}" || exit 1
	    ;;
    esac
done
OPTIND=1

share=$(dist_dir App-Greple-xlate)

declare -a pattern

while getopts dq"${DOCKER_OPT}"'Macrusne:w:o:b:t:m:x:g:p:f:l:hv' OPT
do
    case $OPT in
	[$DOCKER_OPT]) : ;;
	d)     debug=yes ; set -x ;;
	q)     quiet=yes ;;
	M)  run_make=yes ;;
	a)   use_api=yes ;;
	c)     check=yes ;;
	r)   refresh=yes ;;
	u)    update=yes ;;
	s)    silent=yes ;;
	n)    dryrun=yes ;;
	e)    engine="$OPTARG" ;;
	w)      wrap="$OPTARG" ;;
	o)    format="$OPTARG" ;;
	b) base_lang="$OPTARG" ;;
	t)  tgt_lang="$OPTARG" ;;
	m)       max="$OPTARG" ;;
	x)  maskfile="$OPTARG" ;;
	g)  glossary="$OPTARG" ;;
	p)  pattern+=(-E "$OPTARG") ;;
	f)  pattern+=(-f "$OPTARG") ;;
	l)
	    file="$share/$OPTARG"
	    if [[ -f $file ]]
	    then
		cat $file
	    else
		echo "$share"
		ls -1 $share | sed 's/^/\t/'
	    fi
	    exit
	    ;;
	h)
	    sed -r \
		-e '/^$/N' \
		-e '/^\n*(#|=encoding)/d' \
		-e 's/^(\n*)=[a-z]+[0-9]* */\1/' \
		-e '/Version/q' \
		<<< $pod
	    exit
	    ;;
	v)
	    echo $my_version
	    exit
	    ;;
    esac
done	
shift $((OPTIND - 1))

find_gmake() {
    local make
    (( $# == 0 )) && set make gmake gnumake
    for make in "$@"
    do
	[[ $("$make" --version) =~ GNU ]] && { echo "$make"; return 0; }
    done
    return 1
}

##
## Make
##
if [[ $run_make == yes ]]
then
    declare -a opt
    for m in "$@"
    do
	if [[ -f $m ]]
	then
	    # GNU Make behaves differently in different versions with
	    # respect to double-quoted strings and spaces within them.
	    files="${files:+$files|||}$m"
	else
	    opt+=("$m")
	fi
    done
    unset MAKELEVEL
    gmake=$(find_gmake) || { warn "GNU Make is required."; exit 1; }
    exec $gmake -f $share/XLATE.mk \
	 ${dryrun:+-n} \
	 XLATE_LANG=\""$tgt_lang"\" \
	 XLATE_DEBUG=$debug \
	 XLATE_MAXLEN=$max \
	 XLATE_USEAPI=$use_api \
	 XLATE_UPDATE=$update \
	 ${engine:+XLATE_ENGINE=\""$engine"\"} \
	 ${format:+XLATE_FORMAT=\""$format"\"} \
	 ${files:+XLATE_FILES=\""$files"\"} \
	 ${opt[@]} \
    || exit 1
fi

if [[ ! $tgt_lang ]]
then
    warn "$myname: -t option is required."
    exit 1
fi

: ${format:=xtxt}
: ${engine:=deepl}

if [[ $format =~ ^(.+)-fold$ ]]
then
    format=${BASH_REMATCH[1]}
    : ${wrap:=76}
fi

declare -a module option

module+=(-Mxlate) 
option+=(--xlate-engine="$engine")
option+=(--xlate-to="$tgt_lang" --xlate-format="$format" --xlate-cache=yes)
option+=(--all)

[[ $base_lang      ]] && option+=(--xlate-from "$base_lang")
[[ $use_api == yes ]] || use_clipboard=yes
[[ $check   == yes ]] || option+=(--xlate${use_clipboard:+-labor})
[[ $wrap           ]] && option+=(--xlate-fold-line --xlate-fold-width=$wrap)
[[ $debug   == yes ]] && option+=(-dmo)
[[ $refresh == yes ]] && option+=(--xlate-cache=clear)
[[ $update  == yes ]] && option+=(--xlate-update)
[[ $silent  == yes ]] && option+=(--no-xlate-progress)
[[ $max            ]] && option+=(--xlate-maxlen="$max")
[[ $maskfile       ]] && option+=(--xlate-setopt "maskfile=$maskfile")
[[ $glossary       ]] && option+=(--xlate-glossary "$glossary")

declare -a area
case $1 in
    *.txt)
	area=(-E '^(.+\n)+')
	;;
    *.md)
	area=(-E '(?x) ^[-+#].*\n | ^\h+\K.*\n | ^(.+\n)+ ')
	;;
    *.pm|*.pod)
	module+=(-Mperl)
	option+=(--pod)
	option+=(--exclude '^=head\d +(VERSION|AUTHOR|LICENSE|COPYRIGHT|SEE.?ALSO).*\n(?s:.*?)(?=^=|\z)')
	area=(-E '^([\w\pP].+\n)+')
	;;
    *.doc|*.docx|*.pptx|*.xlsx)
	module+=(-Mmsdoc)
	;&
    *.stxt)
	option+=(--exclude '^\[.*\b(doc|docx|pptx|xlsx)\b.*\]\n')
	area=(-E '^.+\n')
	;;
    *)
	area=(-E '^(.+\n)+')
	;;
esac

if (( ${#pattern[@]} > 0))
then
    option+=("${pattern[@]}")
else
    option+=("${area[@]}")
fi

exec=(greple "${module[@]}" "${option[@]}" "$@")
[[ $dryrun ]] && exec=(echo "${exec[@]}")

exec "${exec[@]}"

exit 1

: <<'=cut'

=head1 DESCRIPTION

B<XLATE> is a versatile command-line tool designed as a user-friendly
frontend for the B<greple> C<-Mxlate> module, simplifying the process
of multilingual automatic translation using various API services.  It
streamlines the interaction with the underlying module, making it
easier for users to handle diverse translation needs across multiple
file formats and languages.

A key feature of B<xlate> is its seamless integration with Docker
environments, allowing users to quickly set up and use the tool
without complex environment configurations.  This Docker support
ensures consistency across different systems and simplifies
deployment, benefiting both individual users and teams working on
translation projects.

B<xlate> supports various document formats, including C<.docx>,
C<.pptx>, and C<.md> files, and offers multiple output formats to suit
different requirements.  By combining Docker capabilities with
built-in make functionality, B<xlate> enables powerful automation of
translation workflows.  This combination facilitates efficient batch
processing of multiple files, streamlined project management, and easy
integration into continuous integration/continuous deployment (CI/CD)
pipelines, significantly enhancing productivity in large-scale
localization efforts.

=head2 Basic Usage

To translate a file, use the following command:

    xlate -t <target_language> <file>

For example, to translate a file from English to Japanese:

    xlate -t JA example.txt

=head2 Translation Engines

xlate supports multiple translation engines.  Use the -e option to
specify the engine:

    xlate -e deepl -t JA example.txt

Available engines: deepl, gpt3, gpt4, gpt4o

=head2 Output Formats

Various output formats are supported. Use the -o option to specify the format:

    xlate -o cm -t JA example.txt

Available formats: xtxt, cm, ifdef, space, space+, colon

=head2 Docker Support

B<xlate> offers seamless integration with Docker, providing a powerful
and flexible environment for translation tasks.  This approach
combines the strengths of xlate's translation capabilities with
Docker's containerization benefits.

=head3 Key Concepts

=over 4

=item B<Git Friendly>

If you are working in a git environment, the git top directory is
automatically mounted, which works seamlessly with git commands.
Otherwise the current directory is mounted.

=item B<Containerized Environment>

By running xlate in a Docker container, you ensure a consistent and
isolated environment for all translation tasks.  This eliminates
issues related to system dependencies or conflicting software
versions.

=item B<Integration with Make>

The Docker functionality can be combined with xlate's B<make> feature,
allowing for complex, multi-file translation projects to be managed
efficiently within a containerized environment. For example:

    xlate -DM -t 'EN FR DE' project_files/*.docx

This command runs B<xlate> in a Docker container, utilizing make to
process multiple files with specified target languages.

=item B<Environment Variable Handling>

With the ability to pass specific environment variables into the
container (C<-E>), you can easily manage API keys and other
configuration settings without modifying the container itself.

=back

=head2 Make Support

xlate utilizes GNU Make for automating and managing translation tasks.
This feature is particularly useful for handling translations of
multiple files or translations to different languages.

To use the make feature:

    xlate -M [options] [target]

xlate provides a specialized Makefile (F<XLATE.mk>) that defines
translation tasks and rules.  This file is located in the xlate
library directory and is automatically used when the -M option is
specified.

Example usage:

    xlate -M -t 'EN FR DE' document.docx

This command will use make to translate document.docx to English,
French, and German, following the rules defined in XLATE.mk.

The C<-n> option can be used with C<-M> for a dry-run, showing what
actions would be taken without actually performing the translations:

    xlate -M -n -t 'EN FR DE' document.docx

Users can customize the translation process using parameter files:

=over 4

=item F<*.LANG>:

Specifies target languages for a specific file

=item F<*.FORMAT>:

Defines output formats for a specific file

=item F<*.ENGINE>:

Selects the translation engine for a specific file

=back

For more detailed information on the make functionality and available
rules, refer to the F<XLATE.mk> file in the xlate library directory.

=head2 XLATERC File

The F<.xlaterc> file allows you to set default options for the
C<xlate> command.  This file is searched in the user's home directory,
Git top directory and the current directory.  If found, its contents
are applied before any command-line options.

Each line in the C<.xlaterc> file should contain a valid C<xlate>
command option.  Lines starting with C<#> are treated as comments and
ignored.

For example, if the following line is set in F<.xlaterc>, C<xlate>
will use the specified container image when docker is run.

    -I tecolicom/texlive-groff-ja:v1.35

=head1 OPTIONS

=over 7

=item B<-h>

Show help message.

=item B<-v>

Show version information.

=item B<-d>

Enable debug mode.

=item B<-n>

Perform a dry-run without making any changes.

=item B<-a>

Use API for translation.

=item B<-c>

Check translation area without performing translation.

=item B<-r>

Refresh the translation cache.

=item B<-u>

Force update of the translation cache.

=item B<-s>

Run in silent mode.

=item B<-t> I<lang>

Specify the target language (required).

=item B<-b> I<lang>

Specify the base language (optional).

=item B<-e> I<engine>

Specify the translation engine to use.

=item B<-p> I<pattern>

Specify a pattern to determine the translation area.
See L<App::Greple::xlate/NORMALIZATION>.

=item B<-f> I<file>

Specify a file containing patterns to determine the translation area.
See L<App::Greple::xlate/NORMALIZATION>.

=item B<-o> I<format>

Specify the output format.

=item B<-x> I<file>

Specify a file containing mask patterns.
See L<App::Greple::xlate/MASKING>.

=item B<-w> I<width>

Wrap lines at the specified width.

=item B<-m> I<length>

Specify the maximum length per API call.

=item B<-l> I<file>

Show library files (XLATE.mk, xlate.el).

=back

=head2 MAKE OPTIONS

=over 7

=item B<-M>

Run make.

=item B<-n>

Dry-run.

=back

=head2 DOCKER OPTIONS

Docker feature is invoked by the C<-D>, C<-C> or C<-L> option.
Once any of these options appear, subsequent options are not
interpreted, so they should always be the last of the Docker-related
options.

=over 7

=item B<-D> I<options>

Run B<xlate> script on the Docker container with the rest of the
parameters.

=item B<-C> [ I<command> ]

Execute the following command on the Docker container, or run a shell
if no command is provided.

=item B<-L> [ I<command> ]

When executed without arguments, option C<-L> attaches to a running
container (performs C<docker attach>).  If the container does not
exist, a new container is created, and if there is a stopped
container, it is restarted before attaching.

If executed with command arguments, the command is executed on the
running container (performs C<docker exec>).  If the container does
not exist, it creates a new container executing the given command.
Therefore, the next time it attaches, it connects to the container
that executes that command.

Target container is distinguished by name.  The default container name
is C<xlate>.  The image name (the part after the last slash in the
Docker image name) is used as the container name.  If a directory to
mount is specified, the name of that directory is added after a dot
(C<.>).  For example, if you run C<ubuntu:latest> image with mounting
home directory (C<-H>), the container name will be C<ubuntu.yourname>.

=begin comment

Naturally, the other docker options only apply the first time the
container is launched.  Use the C<-K> option to remove the existing
container before starting.  The C<-KA> option can be used to restart
the container.

=end comment

=back

When running in a Docker environment, the L<git(1)> top directory is
mounted if you are in a directory under git, otherwise current
directory is mounted.  The working directory inside the container will
be set to the corresponding location within that tree.

=over 7

=item B<-W>

Mount current working directory.

=item B<-H>

Mount user's home directory.  The environment variable C<HOME> is set
to the mount point.

=begin comment

=item B<-G>

Mount L<git(1)> top-level directory.  This is the default behavior, so
it does virtually nothing.

=end comment

=item B<-U>

Do not mount any directory.

=item B<-R>

Mount directory as read-only.

=item B<-V> I<from>:I<to>

Specify an additional directory to be mounted.
Repeatable.

=item B<-E> I<name>[=I<value>]

Specify environment variable to be inherited in Docker.
Repeatable.

=item B<-I> I<image>

Specify Docker image name.  If it begins with a colon (C<:>), it is
treated as a version of the default image.

Setting an empty string will invalidate any previous settings and use
the default image.

=begin comment

=item B<-J> I<directory>

Specify the working directory (default: C</work>).

=end comment

=item B<-B>

Run the container in batch mode.  Specifically, run C<docker> command
without the C<--interactive> and C<--tty> options.

=item B<-N>

Specifies the name of the live container explicitly.  Once you have
created a container named C<xlate>, you can connect to it with just
the C<-L> option.

=item B<-K>

Kill and remove the existing live container.  If a container with the
specified name (default is C<xlate>) exists, it will be stopped and
removed.

=begin comment

If specified as C<-KL>, then a new container is launched after the
existing container is removed.

=end comment

=back

=head1 ENVIRONMENT

=over 4

=item DEEPL_AUTH_KEY

DeepL API key.

=item OPENAI_API_KEY

OpenAI API key.

=item ANTHROPIC_API_KEY

Anthropic API key.

=item LLM_PERPLEXITY_KEY

Perplexity API key.

=back

=head1 FILES

=over 4

=item F<*.LANG>

Specifies translation languages.

=item F<*.FORMAT>

Specifies translation format.

=item F<*.ENGINE>

Specifies translation engine.

=back

=head1 EXAMPLES

1. Translate a Word document to English:

   xlate -DMa -t EN-US example.docx

2. Translate to multiple languages and formats:

   xlate -M -o 'xtxt ifdef' -t 'EN-US KO ZH' example.docx

3. Run a command in Docker container:

   xlate -C sdif -V --nocdif example.EN-US.cm

4. Translate without using API (via clipboard):

   xlate -t JA README.md

=head1 SEE ALSO

L<App::Greple::xlate>

=head1 AUTHOR

Kazumasa Utashiro

=head1 LICENSE

Copyright © 2023-2025 Kazumasa Utashiro.

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut

#  LocalWords:  xlate ubuntu
