536 lines
22 KiB
Bash
536 lines
22 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
#
|
||
|
|
# gKeyMapper - everything that touches po/*.po, po/gkeymapper.pot and the compiled
|
||
|
|
# catalogs, in one place, so a translation change is a command instead of a list of
|
||
|
|
# steps to remember. The rules behind it are written down in po/README.md.
|
||
|
|
#
|
||
|
|
# Usage: scripts/i18n.sh <command> [arguments]
|
||
|
|
# extract regenerate po/gkeymapper.pot from the sources
|
||
|
|
# update [lang...] merge the pot into the catalogs: new strings come in
|
||
|
|
# untranslated, existing translations are kept
|
||
|
|
# check validate the pot, the catalogs and the language list
|
||
|
|
# status per-language overview (counts + compiled catalog)
|
||
|
|
# todo <lang> what is still untranslated or fuzzy, with the source
|
||
|
|
# locations from the pot
|
||
|
|
# new <lang> [name] create po/<lang>.po from the pot (name = Language-Team,
|
||
|
|
# e.g. "Italiano"); "new en" writes the identity catalog
|
||
|
|
# help this text
|
||
|
|
#
|
||
|
|
# Only gettext tools and coreutils are used. The tools can be overridden from the
|
||
|
|
# environment, like the Makefile's variables: XGETTEXT=... scripts/i18n.sh extract
|
||
|
|
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||
|
|
# Everything is addressed relative to the repository root and run from there: the
|
||
|
|
# source locations xgettext writes into the pot ("src/main.cpp:582") and the file
|
||
|
|
# names the gettext tools print are relative to the current directory, and absolute
|
||
|
|
# paths would end up in the pot and in every message this script shows.
|
||
|
|
cd -- "$ROOT" || exit 1
|
||
|
|
PO_DIR="po"
|
||
|
|
POT="$PO_DIR/gkeymapper.pot"
|
||
|
|
LOCALE_DIR="locale"
|
||
|
|
PACKAGE="gkeymapper"
|
||
|
|
# What the catalogs carry in Project-Id-Version (see po/*.po).
|
||
|
|
PO_VERSION="0.1"
|
||
|
|
|
||
|
|
XGETTEXT=${XGETTEXT:-xgettext}
|
||
|
|
MSGFMT=${MSGFMT:-msgfmt}
|
||
|
|
MSGMERGE=${MSGMERGE:-msgmerge}
|
||
|
|
MSGATTRIB=${MSGATTRIB:-msgattrib}
|
||
|
|
MSGCAT=${MSGCAT:-msgcat}
|
||
|
|
|
||
|
|
# The counts and message lists below are parsed, so the tools have to answer in
|
||
|
|
# English, whatever the session's language is.
|
||
|
|
export LC_ALL=C
|
||
|
|
|
||
|
|
# A temp file for a pot, removed on every exit - also on the error paths, where it
|
||
|
|
# is the half-written file that must not stay behind.
|
||
|
|
POT_TMP=""
|
||
|
|
trap 'rm -f "${POT_TMP:-}"' EXIT
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# helpers
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
die() {
|
||
|
|
printf 'i18n: %s\n' "$*" >&2
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
|
||
|
|
note() {
|
||
|
|
printf '%s\n' "$*"
|
||
|
|
}
|
||
|
|
|
||
|
|
require_tools() {
|
||
|
|
local tool
|
||
|
|
for tool in "$@"; do
|
||
|
|
command -v "$tool" >/dev/null 2>&1 ||
|
||
|
|
die "$tool not found - install gettext (see README, Requirements)"
|
||
|
|
done
|
||
|
|
}
|
||
|
|
|
||
|
|
# Every source that may hold translatable strings: the GUI, which is the only
|
||
|
|
# program with gettext set up (src/main.cpp) - the same two directories the Makefile
|
||
|
|
# compiles into it. The daemon is English on purpose (see `check`), so its sources
|
||
|
|
# are not scanned. The pot is generated from this list, in this order, so `extract`
|
||
|
|
# is reproducible: a hand-ordered file list once made the pot's message order depend
|
||
|
|
# on the machine it ran on.
|
||
|
|
sources() {
|
||
|
|
local file
|
||
|
|
for file in "$ROOT"/src/*.cpp "$ROOT"/shared/*.cpp; do
|
||
|
|
if [ -e "$file" ]; then printf '%s\n' "${file#"$ROOT"/}"; fi
|
||
|
|
done
|
||
|
|
}
|
||
|
|
|
||
|
|
# The languages that have a catalog, i.e. po/*.po - the same list the Makefile
|
||
|
|
# compiles and installs.
|
||
|
|
languages() {
|
||
|
|
local file
|
||
|
|
for file in "$PO_DIR"/*.po; do
|
||
|
|
if [ -e "$file" ]; then basename "$file" .po; fi
|
||
|
|
done | sort
|
||
|
|
}
|
||
|
|
|
||
|
|
# The messages of a po/pot, one per line and multi-line strings joined: msgcat does
|
||
|
|
# the joining, so `_("a" "b")` in the sources and the pot entry are the same string.
|
||
|
|
# The header (empty msgid) is left out.
|
||
|
|
msgids() {
|
||
|
|
"$MSGCAT" --stringtable-output "$1" 2>/dev/null |
|
||
|
|
sed -n 's/^"\(.*\)" = .*$/\1/p' | sed '/^$/d'
|
||
|
|
}
|
||
|
|
|
||
|
|
# translated/fuzzy/untranslated of one catalog, into three globals. msgfmt prints
|
||
|
|
# them as a comma separated list ("81 translated messages, 1 untranslated
|
||
|
|
# message.") and leaves the parts out that are zero, so the list is split first -
|
||
|
|
# a pattern like ".*\([0-9]\+\) untranslated" would stop at the last digit of a
|
||
|
|
# count and read 82 as 2.
|
||
|
|
stats() {
|
||
|
|
local out parts
|
||
|
|
out=$("$MSGFMT" --statistics -o /dev/null "$1" 2>&1) || die "$MSGFMT rejected $1"
|
||
|
|
parts=$(printf '%s\n' "$out" | tr ',' '\n')
|
||
|
|
translated=$(printf '%s\n' "$parts" | sed -n 's/^ *\([0-9]\+\) translated.*/\1/p'); : "${translated:=0}"
|
||
|
|
fuzzy=$(printf '%s\n' "$parts" | sed -n 's/^ *\([0-9]\+\) fuzzy.*/\1/p'); : "${fuzzy:=0}"
|
||
|
|
untranslated=$(printf '%s\n' "$parts" | sed -n 's/^ *\([0-9]\+\) untranslated.*/\1/p'); : "${untranslated:=0}"
|
||
|
|
}
|
||
|
|
|
||
|
|
# The `Language:` header of a catalog, so a copy-pasted file is noticed.
|
||
|
|
po_language() {
|
||
|
|
sed -n 's/^"Language: \(.*\)\\n"$/\1/p' "$1"
|
||
|
|
}
|
||
|
|
|
||
|
|
# The POT-Creation-Date of a po/pot. It does not decide whether a file is up to
|
||
|
|
# date (the message set does); it tells whether a merge happened after the last
|
||
|
|
# extract.
|
||
|
|
pot_date() {
|
||
|
|
sed -n 's/^"POT-Creation-Date: \(.*\)\\n"$/\1/p' "$1"
|
||
|
|
}
|
||
|
|
|
||
|
|
# One line per entry whose msgstr is not the msgid again - the rule for po/en.po,
|
||
|
|
# which is an identity catalog because English is the source language. Compared as
|
||
|
|
# raw (still escaped) text: both sides come from the same file, so the escaping
|
||
|
|
# matches. Plurals need more than this and are not used anywhere (no ngettext).
|
||
|
|
identity_mismatches() {
|
||
|
|
awk '
|
||
|
|
function quoted(line) { sub(/^[^"]*"/, "", line); sub(/"$/, "", line); return line }
|
||
|
|
function check() { if (id != "" && str != id) { print id; mismatches++ } }
|
||
|
|
/^msgid_plural|^msgstr\[/ { print "plural entries are not supported"; exit 2 }
|
||
|
|
/^msgid / { check(); id = quoted($0); str = ""; in_id = 1; next }
|
||
|
|
/^msgstr / { str = quoted($0); in_id = 0; next }
|
||
|
|
/^"/ { if (in_id) id = id quoted($0); else str = str quoted($0); next }
|
||
|
|
/^#/ { next }
|
||
|
|
/^[[:space:]]*$/ { next }
|
||
|
|
END { check(); exit (mismatches > 0 ? 1 : 0) }
|
||
|
|
' "$1"
|
||
|
|
}
|
||
|
|
|
||
|
|
# The pot as xgettext writes it right now, into the given file. `_()` marks
|
||
|
|
# strings that are translated when they are used, `N_()` the ones that are only
|
||
|
|
# collected (the help text is assembled later on); both are named explicitly,
|
||
|
|
# because a gettext that does not know `_` by itself is not unheard of.
|
||
|
|
extract_to() {
|
||
|
|
local out="$1" file
|
||
|
|
require_tools "$XGETTEXT"
|
||
|
|
local -a files=()
|
||
|
|
while IFS= read -r file; do files+=("$file"); done < <(sources)
|
||
|
|
[ "${#files[@]}" -gt 0 ] || die "no sources found under src/ or shared/"
|
||
|
|
|
||
|
|
"$XGETTEXT" --from-code=UTF-8 --language=C++ --keyword=_ --keyword=N_ \
|
||
|
|
--output="$out" "${files[@]}" || die "$XGETTEXT failed"
|
||
|
|
|
||
|
|
# Nothing or header-only means the extraction is broken, never that the program
|
||
|
|
# has no strings. Checked before the pot is replaced, so a broken gettext
|
||
|
|
# cannot empty a good pot.
|
||
|
|
[ -s "$out" ] || die "$XGETTEXT produced no output (is gettext working? $XGETTEXT --version)"
|
||
|
|
[ "$(grep -c '^msgid "' "$out")" -gt 1 ] ||
|
||
|
|
die "$XGETTEXT extracted no message - are the keywords right?"
|
||
|
|
}
|
||
|
|
|
||
|
|
# The pot without its header entry and without source locations: the entries of a
|
||
|
|
# fresh catalog. With identity=1 every msgstr repeats its msgid (po/en.po).
|
||
|
|
pot_body() {
|
||
|
|
awk -v identity="$2" '
|
||
|
|
NR == 1 { skipping = 1 }
|
||
|
|
skipping { if ($0 ~ /^$/) skipping = 0; next }
|
||
|
|
/^#:/ { next }
|
||
|
|
/^msgid / {
|
||
|
|
if (identity) { buffer = $0; sub(/^msgid/, "msgstr", buffer); collecting = 1 }
|
||
|
|
print; next
|
||
|
|
}
|
||
|
|
/^"/ { if (collecting) buffer = buffer "\n" $0; print; next }
|
||
|
|
/^msgstr ""$/ {
|
||
|
|
if (collecting) { print buffer; collecting = 0; next }
|
||
|
|
print; next
|
||
|
|
}
|
||
|
|
{ print }
|
||
|
|
' "$1"
|
||
|
|
}
|
||
|
|
|
||
|
|
# The language codes the GUI offers (src/optionsdialog.cpp, without the leading
|
||
|
|
# "" for "follow the session"). A catalog that is not in this list cannot be
|
||
|
|
# picked; a code without a catalog is a window that stays English.
|
||
|
|
gui_language_codes() {
|
||
|
|
sed -n 's/.*kLanguageCodes\[\] *= *{\(.*\)};.*/\1/p' "src/optionsdialog.cpp" |
|
||
|
|
tr ',' '\n' | tr -d ' "' | sed '/^$/d'
|
||
|
|
}
|
||
|
|
|
||
|
|
# The labels of that dropdown: one more than the codes, the first being
|
||
|
|
# "System language".
|
||
|
|
gui_language_labels() {
|
||
|
|
sed -n '/languageOptions\[\] *= *{/,/nullptr/p' "src/optionsdialog.cpp" |
|
||
|
|
sed 's/.*{//; s/nullptr.*//' | tr ',' '\n' | grep -c '[^[:space:]]'
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# commands
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
cmd_extract() {
|
||
|
|
POT_TMP=$(mktemp "${TMPDIR:-/tmp}/$PACKAGE-pot.XXXXXX")
|
||
|
|
extract_to "$POT_TMP"
|
||
|
|
|
||
|
|
if [ -f "$POT" ]; then
|
||
|
|
local added gone
|
||
|
|
added=$(comm -13 <(msgids "$POT" | sort) <(msgids "$POT_TMP" | sort))
|
||
|
|
gone=$(comm -23 <(msgids "$POT" | sort) <(msgids "$POT_TMP" | sort))
|
||
|
|
note "po/gkeymapper.pot: $(msgids "$POT_TMP" | wc -l) messages"
|
||
|
|
[ -z "$added" ] || { note "new:"; printf '%s\n' "$added" | sed 's/^/ /'; }
|
||
|
|
[ -z "$gone" ] || { note "gone:"; printf '%s\n' "$gone" | sed 's/^/ /'; }
|
||
|
|
else
|
||
|
|
note "po/gkeymapper.pot: $(msgids "$POT_TMP" | wc -l) messages"
|
||
|
|
fi
|
||
|
|
|
||
|
|
mv "$POT_TMP" "$POT"
|
||
|
|
POT_TMP="" # moved, nothing to clean up
|
||
|
|
note "wrote po/gkeymapper.pot - now: scripts/i18n.sh update"
|
||
|
|
}
|
||
|
|
|
||
|
|
cmd_update() {
|
||
|
|
local lang po
|
||
|
|
require_tools "$MSGMERGE" "$MSGFMT"
|
||
|
|
[ -f "$POT" ] || die "$POT is missing - run: scripts/i18n.sh extract"
|
||
|
|
|
||
|
|
local -a targets
|
||
|
|
if [ "$#" -gt 0 ]; then targets=("$@"); else mapfile -t targets < <(languages); fi
|
||
|
|
[ "${#targets[@]}" -gt 0 ] || die "no po/*.po to update"
|
||
|
|
|
||
|
|
for lang in "${targets[@]}"; do
|
||
|
|
po="$PO_DIR/$lang.po"
|
||
|
|
[ -f "$po" ] || die "$po does not exist (a new language: scripts/i18n.sh new $lang)"
|
||
|
|
# --no-location: the catalogs carry no "#: file:line" comments on purpose -
|
||
|
|
# they are noise for translators and churn whenever a line moves. The
|
||
|
|
# locations live in the pot, and `todo` shows them from there.
|
||
|
|
"$MSGMERGE" --quiet --update --no-location "$po" "$POT"
|
||
|
|
# msgmerge keeps the previous file as <po>~; a backup in git is noise.
|
||
|
|
rm -f "$po~"
|
||
|
|
stats "$po"
|
||
|
|
note "po/$lang.po: $translated translated, $fuzzy fuzzy, $untranslated untranslated"
|
||
|
|
done
|
||
|
|
}
|
||
|
|
|
||
|
|
cmd_check() {
|
||
|
|
local lang po code out failures=0 stale_dates=""
|
||
|
|
require_tools "$MSGFMT" "$MSGATTRIB" "$MSGCAT" "$MSGMERGE" "$XGETTEXT"
|
||
|
|
[ -f "$POT" ] || die "$POT is missing"
|
||
|
|
|
||
|
|
local langs
|
||
|
|
langs=$(languages)
|
||
|
|
[ -n "$langs" ] || die "no po/*.po found"
|
||
|
|
|
||
|
|
# 1. Is the pot what the sources say? A message that was added or changed
|
||
|
|
# without touching the pot is what this catches - the source locations only
|
||
|
|
# drift when a line moves, which is worth a note, not a failure.
|
||
|
|
POT_TMP=$(mktemp "${TMPDIR:-/tmp}/$PACKAGE-pot.XXXXXX")
|
||
|
|
extract_to "$POT_TMP"
|
||
|
|
local added gone
|
||
|
|
added=$(comm -13 <(msgids "$POT" | sort) <(msgids "$POT_TMP" | sort))
|
||
|
|
gone=$(comm -23 <(msgids "$POT" | sort) <(msgids "$POT_TMP" | sort))
|
||
|
|
if [ -n "$added" ] || [ -n "$gone" ]; then
|
||
|
|
note "po/gkeymapper.pot is out of date:"
|
||
|
|
[ -z "$added" ] || { note " in the sources, not in the pot:"; printf '%s\n' "$added" | sed 's/^/ /'; }
|
||
|
|
[ -z "$gone" ] || { note " in the pot, not in the sources:"; printf '%s\n' "$gone" | sed 's/^/ /'; }
|
||
|
|
note " fix: scripts/i18n.sh extract && scripts/i18n.sh update"
|
||
|
|
failures=$((failures + 1))
|
||
|
|
elif ! diff -q <(grep -v '^"POT-Creation-Date:' "$POT_TMP") \
|
||
|
|
<(grep -v '^"POT-Creation-Date:' "$POT") >/dev/null; then
|
||
|
|
note "po/gkeymapper.pot has the same messages, but an older extract (line numbers, order)"
|
||
|
|
note " only the locations in 'todo' are affected - scripts/i18n.sh extract refreshes them"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# 2. Every catalog: parsable, complete, translated, and named what it says.
|
||
|
|
for lang in $langs; do
|
||
|
|
po="$PO_DIR/$lang.po"
|
||
|
|
|
||
|
|
# -c checks the format strings (%s, %d, ...) and the entry structure.
|
||
|
|
if ! out=$("$MSGFMT" -c -o /dev/null "$po" 2>&1); then
|
||
|
|
note "$lang: $MSGFMT -c failed:"
|
||
|
|
printf '%s\n' "$out" | sed 's/^/ /'
|
||
|
|
failures=$((failures + 1))
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Does the catalog define the same messages as the pot? Compared as two
|
||
|
|
# lists rather than with msgcmp, which would also report every untranslated
|
||
|
|
# entry and drown the real problem (the counts below say what is missing).
|
||
|
|
local missing extra
|
||
|
|
missing=$(comm -23 <(msgids "$POT" | sort) <(msgids "$po" | sort))
|
||
|
|
extra=$(comm -13 <(msgids "$POT" | sort) <(msgids "$po" | sort))
|
||
|
|
if [ -n "$missing" ]; then
|
||
|
|
note "$lang: $po does not define:"
|
||
|
|
printf '%s\n' "$missing" | sed 's/^/ /'
|
||
|
|
failures=$((failures + 1))
|
||
|
|
fi
|
||
|
|
if [ -n "$extra" ]; then
|
||
|
|
note "$lang: $po defines messages the pot does not know:"
|
||
|
|
printf '%s\n' "$extra" | sed 's/^/ /'
|
||
|
|
failures=$((failures + 1))
|
||
|
|
fi
|
||
|
|
|
||
|
|
stats "$po"
|
||
|
|
if [ "$untranslated" -ne 0 ]; then
|
||
|
|
note "$lang: $untranslated untranslated message(s) - scripts/i18n.sh todo $lang"
|
||
|
|
failures=$((failures + 1))
|
||
|
|
fi
|
||
|
|
if [ "$fuzzy" -ne 0 ]; then
|
||
|
|
note "$lang: $fuzzy fuzzy message(s) - those are shown in English"
|
||
|
|
failures=$((failures + 1))
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Strings that vanished from the program: msgmerge keeps them as "#~"
|
||
|
|
# entries. Not an error, but stale food for the next translator.
|
||
|
|
local obsolete=0
|
||
|
|
obsolete=$(grep -c '^#~' "$po" || true)
|
||
|
|
[ "$obsolete" -eq 0 ] ||
|
||
|
|
note "$lang: $obsolete obsolete entries (gone from the sources; msgattrib --no-obsolete drops them)"
|
||
|
|
|
||
|
|
if [ "$(po_language "$po")" != "$lang" ]; then
|
||
|
|
note "$lang: the header says Language: $(po_language "$po")"
|
||
|
|
failures=$((failures + 1))
|
||
|
|
fi
|
||
|
|
|
||
|
|
# English has a catalog of its own (an explicit language has to work on a
|
||
|
|
# German desktop) and it is an identity one.
|
||
|
|
if [ "$lang" = "en" ]; then
|
||
|
|
if ! out=$(identity_mismatches "$po"); then
|
||
|
|
note "en: po/en.po is not an identity catalog:"
|
||
|
|
printf '%s\n' "$out" | head -20 | sed 's/^/ /'
|
||
|
|
failures=$((failures + 1))
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
# A note, not a failure: it only means the file was not merged after the
|
||
|
|
# last extract, so its header carries an older date than the pot. Collected
|
||
|
|
# into one line - it says nothing about a single language.
|
||
|
|
if [ "$(pot_date "$po")" != "$(pot_date "$POT")" ]; then
|
||
|
|
stale_dates="$stale_dates $lang"
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
[ -z "$stale_dates" ] ||
|
||
|
|
note "not merged against the current pot:$stale_dates (scripts/i18n.sh update)"
|
||
|
|
|
||
|
|
# 3. The languages the GUI offers (src/optionsdialog.cpp): a catalog that cannot
|
||
|
|
# be selected is dead weight, a code without a catalog is an English window
|
||
|
|
# with a German label.
|
||
|
|
local codes
|
||
|
|
codes=$(gui_language_codes)
|
||
|
|
[ -n "$codes" ] || die "could not read kLanguageCodes[] from src/optionsdialog.cpp"
|
||
|
|
for lang in $langs; do
|
||
|
|
if ! grep -qx "$lang" <<<"$codes"; then
|
||
|
|
note "po/$lang.po exists, but $lang is not offered in the options window"
|
||
|
|
note " add it to kLanguageCodes[] and languageOptions[] (src/optionsdialog.cpp)"
|
||
|
|
failures=$((failures + 1))
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
for code in $codes; do
|
||
|
|
if [ ! -f "$PO_DIR/$code.po" ]; then
|
||
|
|
note "the options window offers $code, but po/$code.po is missing"
|
||
|
|
failures=$((failures + 1))
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
if [ "$(gui_language_labels)" -ne "$(( $(printf '%s\n' "$codes" | wc -l) + 1 ))" ]; then
|
||
|
|
note "kLanguageCodes[] and languageOptions[] differ in length (src/optionsdialog.cpp)"
|
||
|
|
failures=$((failures + 1))
|
||
|
|
fi
|
||
|
|
|
||
|
|
# 4. Strings marked in the daemon. It has no gettext setup (no setlocale, no
|
||
|
|
# bindtextdomain - see daemon/main.cpp), so a `_()` there would end up in the
|
||
|
|
# pot, get translated, and still show English forever. The GUI is the
|
||
|
|
# translated program.
|
||
|
|
local daemon_marked
|
||
|
|
daemon_marked=$(grep -lE '(^|[^[:alnum:]_])_\("|N_\("' daemon/*.cpp 2>/dev/null || true)
|
||
|
|
if [ -n "$daemon_marked" ]; then
|
||
|
|
note "the daemon is not translated, but these files mark strings:"
|
||
|
|
printf '%s\n' "$daemon_marked" | sed 's/^/ /'
|
||
|
|
note " either drop the marker, or give the daemon its own gettext setup and"
|
||
|
|
note " add daemon/ to sources() in scripts/i18n.sh"
|
||
|
|
failures=$((failures + 1))
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ "$failures" -gt 0 ]; then
|
||
|
|
note "i18n: $failures problem(s)"
|
||
|
|
return 1
|
||
|
|
fi
|
||
|
|
note "i18n: $(printf '%s\n' "$langs" | wc -l) languages, $(msgids "$POT" | wc -l) messages, everything in sync"
|
||
|
|
}
|
||
|
|
|
||
|
|
cmd_status() {
|
||
|
|
local lang po mo mo_state
|
||
|
|
require_tools "$MSGFMT" "$MSGCAT"
|
||
|
|
[ -f "$POT" ] || die "$POT is missing"
|
||
|
|
printf '%-4s %-8s %10s %6s %11s %-9s %s\n' \
|
||
|
|
lang messages translated fuzzy untranslated catalog file
|
||
|
|
for lang in $(languages); do
|
||
|
|
po="$PO_DIR/$lang.po"
|
||
|
|
stats "$po"
|
||
|
|
mo="$LOCALE_DIR/$lang/LC_MESSAGES/$PACKAGE.mo"
|
||
|
|
mo_state="not built"
|
||
|
|
if [ -f "$mo" ]; then
|
||
|
|
# `make translations` rebuilds a catalog that is older than its po.
|
||
|
|
if [ "$mo" -nt "$po" ]; then mo_state="current"; else mo_state="stale"; fi
|
||
|
|
fi
|
||
|
|
printf '%-4s %-8s %10s %6s %11s %-9s %s\n' "$lang" \
|
||
|
|
"$((translated + fuzzy + untranslated))" "$translated" "$fuzzy" "$untranslated" \
|
||
|
|
"$mo_state" "po/$lang.po"
|
||
|
|
done
|
||
|
|
printf '\npot: %s messages, %s\n' "$(msgids "$POT" | wc -l)" "$(pot_date "$POT")"
|
||
|
|
}
|
||
|
|
|
||
|
|
cmd_todo() {
|
||
|
|
local lang="${1:-}"
|
||
|
|
[ -n "$lang" ] || die "usage: scripts/i18n.sh todo <lang>"
|
||
|
|
local po="$PO_DIR/$lang.po"
|
||
|
|
[ -f "$po" ] || die "$po does not exist"
|
||
|
|
require_tools "$MSGMERGE" "$MSGATTRIB"
|
||
|
|
|
||
|
|
# Merging the pot into the catalog is what puts the source locations next to
|
||
|
|
# the messages - the catalogs themselves have none (see cmd_update).
|
||
|
|
local open
|
||
|
|
open=$("$MSGMERGE" --quiet "$po" "$POT" | "$MSGATTRIB" --untranslated --no-obsolete)
|
||
|
|
if [ -n "$open" ]; then
|
||
|
|
# A valid po comment, so the output can be kept as a file and translated.
|
||
|
|
note "# --- untranslated in po/$lang.po ---"
|
||
|
|
printf '%s\n' "$open"
|
||
|
|
else
|
||
|
|
note "po/$lang.po: nothing untranslated"
|
||
|
|
fi
|
||
|
|
|
||
|
|
local fuzzy
|
||
|
|
fuzzy=$("$MSGMERGE" --quiet "$po" "$POT" | "$MSGATTRIB" --only-fuzzy --no-obsolete)
|
||
|
|
if [ -n "$fuzzy" ]; then
|
||
|
|
note
|
||
|
|
note "# --- fuzzy: shown in English until reviewed, the old text is kept as a comment ---"
|
||
|
|
printf '%s\n' "$fuzzy"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
cmd_new() {
|
||
|
|
local lang="${1:-}" team="${2:-}"
|
||
|
|
[ -n "$lang" ] || die "usage: scripts/i18n.sh new <lang> [Language-Team]"
|
||
|
|
case "$lang" in
|
||
|
|
*[!a-zA-Z_@.-]*) die "unexpected language code: $lang" ;;
|
||
|
|
esac
|
||
|
|
[ -f "$POT" ] || die "$POT is missing - run: scripts/i18n.sh extract"
|
||
|
|
local po="$PO_DIR/$lang.po"
|
||
|
|
[ -e "$po" ] && die "$po already exists"
|
||
|
|
if [ -z "$team" ]; then
|
||
|
|
# The two catalogs for a language that has been here from the start carry
|
||
|
|
# the English name of the language; anything else falls back to its code.
|
||
|
|
case "$lang" in
|
||
|
|
en) team="English" ;;
|
||
|
|
*) team="$lang" ;;
|
||
|
|
esac
|
||
|
|
fi
|
||
|
|
|
||
|
|
{
|
||
|
|
if [ "$lang" = "en" ]; then
|
||
|
|
printf '# English translation for %s.\n' "$PACKAGE"
|
||
|
|
printf '# English is the source language; this catalog keeps the strings explicit.\n'
|
||
|
|
else
|
||
|
|
printf '# %s translation for %s.\n' "$team" "$PACKAGE"
|
||
|
|
printf '# Copyright (C) %s\n' "$(date '+%Y')"
|
||
|
|
printf '# This file is distributed under the same license as the %s package.\n' "$PACKAGE"
|
||
|
|
fi
|
||
|
|
printf '#\n'
|
||
|
|
# The header in the shape the other catalogs use, so a new language does not
|
||
|
|
# start out looking different.
|
||
|
|
cat <<EOF
|
||
|
|
msgid ""
|
||
|
|
msgstr ""
|
||
|
|
"Project-Id-Version: $PACKAGE $PO_VERSION\n"
|
||
|
|
"Report-Msgid-Bugs-To: \n"
|
||
|
|
"POT-Creation-Date: $(pot_date "$POT")\n"
|
||
|
|
"PO-Revision-Date: $(date '+%Y-%m-%d %H:%M%z')\n"
|
||
|
|
"Last-Translator: \n"
|
||
|
|
"Language-Team: $team\n"
|
||
|
|
"Language: $lang\n"
|
||
|
|
"MIME-Version: 1.0\n"
|
||
|
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||
|
|
"Content-Transfer-Encoding: 8bit\n"
|
||
|
|
EOF
|
||
|
|
printf '\n'
|
||
|
|
# The entries come from the pot: it defines what has to be translated, and
|
||
|
|
# every msgstr in it is already empty.
|
||
|
|
pot_body "$POT" "$([ "$lang" = "en" ] && echo 1 || echo 0)"
|
||
|
|
} >"$po"
|
||
|
|
|
||
|
|
note "wrote po/$lang.po"
|
||
|
|
if [ "$lang" != "en" ]; then
|
||
|
|
note "next:"
|
||
|
|
note " translate it, then: scripts/i18n.sh check && make translations"
|
||
|
|
note " offer it in the options window, or nobody can pick it:"
|
||
|
|
note " src/optionsdialog.cpp: kLanguageCodes[] and languageOptions[]"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
usage() {
|
||
|
|
cat <<'EOF'
|
||
|
|
scripts/i18n.sh <command> [arguments]
|
||
|
|
|
||
|
|
extract regenerate po/gkeymapper.pot from the sources
|
||
|
|
update [lang...] merge the pot into the catalogs: new strings come in
|
||
|
|
untranslated, existing translations are kept
|
||
|
|
check validate the pot, the catalogs and the language list
|
||
|
|
status per-language overview (counts + compiled catalog)
|
||
|
|
todo <lang> what is still untranslated or fuzzy, with the source
|
||
|
|
locations from the pot
|
||
|
|
new <lang> [name] create po/<lang>.po from the pot (name = Language-Team,
|
||
|
|
e.g. "Italiano"); "new en" writes the identity catalog
|
||
|
|
help this text
|
||
|
|
|
||
|
|
The rules behind the commands are in po/README.md. gettext tools are used from
|
||
|
|
PATH; override them like the Makefile's variables, e.g. XGETTEXT=/usr/bin/xgettext
|
||
|
|
EOF
|
||
|
|
}
|
||
|
|
|
||
|
|
command="${1:-help}"
|
||
|
|
shift || true
|
||
|
|
case "$command" in
|
||
|
|
extract|pot) cmd_extract "$@" ;;
|
||
|
|
update) cmd_update "$@" ;;
|
||
|
|
check) cmd_check "$@" ;;
|
||
|
|
status) cmd_status "$@" ;;
|
||
|
|
todo) cmd_todo "$@" ;;
|
||
|
|
new) cmd_new "$@" ;;
|
||
|
|
help|-h|--help) usage ;;
|
||
|
|
*) usage; exit 1 ;;
|
||
|
|
esac
|