From 112df455217c6e50304c037df481de1acefd1e9b Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Tue, 1 Jul 2025 12:54:00 +1000 Subject: [PATCH] chore: make helper tools more robust, translation helpers (#6396) --- radio/src/fonts/lvgl/make_fonts.sh | 274 ++++++++++++------- radio/src/translations/pl.h | 1 + tools/cfn_sorter.sh | 50 ++-- tools/check_translations.py | 351 +++++++++++++++++++++++++ tools/update-companion-translations.sh | 82 ++++++ 5 files changed, 645 insertions(+), 113 deletions(-) create mode 100755 tools/check_translations.py create mode 100755 tools/update-companion-translations.sh diff --git a/radio/src/fonts/lvgl/make_fonts.sh b/radio/src/fonts/lvgl/make_fonts.sh index f74d86d31..45e26ff89 100755 --- a/radio/src/fonts/lvgl/make_fonts.sh +++ b/radio/src/fonts/lvgl/make_fonts.sh @@ -1,14 +1,35 @@ #!/bin/bash -LVGLDIR="../../thirdparty/lvgl" -TTF_DIR="../" +# Exit on any error, undefined variables, or pipe failures +set -euo pipefail -SYMBOLS_FONT="${LVGLDIR}/scripts/built_in_font/FontAwesome5-Solid+Brands+Regular.woff" +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RADIO_SRC_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" +TRANSLATIONS_DIR="${RADIO_SRC_DIR}/translations" + +SYMBOLS_FONT="${RADIO_SRC_DIR}/thirdparty/lvgl/scripts/built_in_font/FontAwesome5-Solid+Brands+Regular.woff" +# test if realpath supports --relative-to as macOS BSD version doesn't +if realpath --relative-to="." "." >/dev/null 2>&1; then + # GNU realpath with --relative-to support + SYMBOLS_FONT_REL="$(realpath --relative-to="${SCRIPT_DIR}" "${SYMBOLS_FONT}")" +else + # BSD realpath or no realpath - use hardcoded relative path + SYMBOLS_FONT_REL="../../thirdparty/lvgl/scripts/built_in_font/FontAwesome5-Solid+Brands+Regular.woff" +fi SYMBOLS="61441,61448,61451,61452,61453,61457,61459,61461,61465,61468,61473,61478,61479,61480,61502,61507,61512,61515,61516,61517,61521,61522,61523,61524,61543,61544,61550,61552,61553,61556,61559,61560,61561,61563,61587,61589,61636,61637,61639,61641,61664,61671,61674,61683,61724,61732,61787,61931,62016,62017,62018,62019,62020,62087,62099,62212,62189,62810,63426,63650" EXTRA_FONT="EdgeTX/extra.ttf" EXTRA_SYM="0x88-0x96" +# https://yeun.github.io/open-arrow/ +ARROWS_FONT="EdgeTX/OpenArrow-Regular.woff" +# 0x80: right, 0x81: left, 0x82: up, 0x83: down +ARROWS="0x21E8=>0x80,0x21E6=>0x81,0x21E7=>0x82,0x21E9=>0x83" + +LATIN_FONT="Roboto/Roboto-Regular.ttf" +LATIN_FONT_BOLD="Roboto/Roboto-Bold.ttf" + ASCII="0x20-0x7F" DEGREE="0xB0" BULLET="0x2022" @@ -17,21 +38,38 @@ LATIN1_EXT_A="0x100-0x17F" LATIN1="${LATIN1_SUPPLEMENT},${LATIN1_EXT_A}" COMPARE="0x2265" -TW_SYMBOLS=$(python3 get_char_ck.py ../../translations/tw.h) -CN_SYMBOLS=$(python3 get_char_ck.py ../../translations/cn.h) -JP_SYMBOLS=$(python3 get_char_jp.py ../../translations/jp.h) -HE_SYMBOLS=$(python3 get_char_he.py ../../translations/he.h) -KO_SYMBOLS=$(python3 get_char_ko.py ../../translations/ko.h) -RU_SYMBOLS=$(python3 get_char_cyrillic.py ../../translations/ru.h) -UA_SYMBOLS=$(python3 get_char_cyrillic.py ../../translations/ua.h) +# LV_SYMBOL_CHARGE, LV_SYMBOL_NEW_LINE, LV_SYMBOL_SD_CARD, LV_SYMBOL_CLOSE +# LV_SYMBOL_FILE, LV_SYMBOL_OK, LV_SYMBOL_WIFI, LV_SYMBOL_USB +BL_SYMBOLS="61671,63650,63426,61453,61787,61452,61931,62087" -# https://yeun.github.io/open-arrow/ -ARROWS_FONT="EdgeTX/OpenArrow-Regular.woff" -# 0x80: right, 0x81: left, 0x82: up, 0x83: down -ARROWS="0x21E8=>0x80,0x21E6=>0x81,0x21E7=>0x82,0x21E9=>0x83" +check_dependencies() { + # Check if lv_font_conv is available + if ! command -v lv_font_conv >/dev/null 2>&1; then + echo "ERROR: lv_font_conv not found. Please install it from https://github.com/lvgl/lv_font_conv or npm registry" >&2 + exit 1 + fi + + # Check if we're on macOS and provide helpful info about realpath + if [[ "$(uname)" == "Darwin" ]]; then + if ! realpath --relative-to="." "." >/dev/null 2>&1; then + echo "INFO: Using BSD realpath (no --relative-to support). Using hardcoded relative paths." >&2 + echo "INFO: To install GNU coreutils (optional): brew install coreutils" >&2 + fi + fi +} -LATIN_FONT="Roboto/Roboto-Regular.ttf" -LATIN_FONT_BOLD="Roboto/Roboto-Bold.ttf" +get_translation_symbols() { + TW_SYMBOLS=$(python3 get_char_ck.py "${TRANSLATIONS_DIR}/tw.h" 2>/dev/null || echo "") + CN_SYMBOLS=$(python3 get_char_ck.py "${TRANSLATIONS_DIR}/cn.h" 2>/dev/null || echo "") + JP_SYMBOLS=$(python3 get_char_jp.py "${TRANSLATIONS_DIR}/jp.h" 2>/dev/null || echo "") + HE_SYMBOLS=$(python3 get_char_he.py "${TRANSLATIONS_DIR}/he.h" 2>/dev/null || echo "") + KO_SYMBOLS=$(python3 get_char_ko.py "${TRANSLATIONS_DIR}/ko.h" 2>/dev/null || echo "") + RU_SYMBOLS=$(python3 get_char_cyrillic.py "${TRANSLATIONS_DIR}/ru.h" 2>/dev/null || echo "") + UA_SYMBOLS=$(python3 get_char_cyrillic.py "${TRANSLATIONS_DIR}/ua.h" 2>/dev/null || echo "") + + # Export variables for later use + export TW_SYMBOLS CN_SYMBOLS JP_SYMBOLS HE_SYMBOLS KO_SYMBOLS RU_SYMBOLS UA_SYMBOLS +} function make_font() { local name=$1 @@ -42,20 +80,29 @@ function make_font() { local dir=$6 local chars=$7 - lv_font_conv --no-prefilter --bpp 4 --size ${size} \ - --font ${TTF_DIR}${latin_ttf} -r ${ASCII},${DEGREE},${BULLET},${COMPARE} \ - --font ${TTF_DIR}${ttf} -r ${chars} \ - --font EdgeTX/extra.ttf -r ${EXTRA_SYM} \ - --font ${ARROWS_FONT} -r ${ARROWS} \ - --font ${SYMBOLS_FONT} -r ${SYMBOLS} \ - --format lvgl -o ${dir}/lv_font_${name}_${sfx}.c --force-fast-kern-format --no-compress + echo "Creating font: ${name}_${sfx} (size: ${size})" + + # Use relative paths for lv_font_conv to avoid absolute paths in generated files + lv_font_conv --no-prefilter --bpp 4 --size "${size}" \ + --font "../${latin_ttf}" -r "${ASCII},${DEGREE},${BULLET},${COMPARE}" \ + --font "../${ttf}" -r "${chars}" \ + --font "${EXTRA_FONT}" -r "${EXTRA_SYM}" \ + --font "${ARROWS_FONT}" -r "${ARROWS}" \ + --font "${SYMBOLS_FONT_REL}" -r "${SYMBOLS}" \ + --format lvgl -o "${dir}/lv_font_${name}_${sfx}.c" --force-fast-kern-format --no-compress } function compress_font() { local name=$1 - gcc -I ../../thirdparty lz4_font.cpp ../../thirdparty/lz4/lz4hc.c ../../thirdparty/lz4/lz4.c -o lz4_font - ./lz4_font ${name} + # Compile the compression tool + gcc -I "${RADIO_SRC_DIR}/thirdparty" \ + "${SCRIPT_DIR}/lz4_font.cpp" \ + "${RADIO_SRC_DIR}/thirdparty/lz4/lz4hc.c" \ + "${RADIO_SRC_DIR}/thirdparty/lz4/lz4.c" \ + -o "${SCRIPT_DIR}/lz4_font" + + "${SCRIPT_DIR}/lz4_font" "${name}" } function make_font_lz4() { @@ -67,14 +114,17 @@ function make_font_lz4() { local dir=$6 local chars=$7 - lv_font_conv --no-prefilter --bpp 4 --size ${size} \ - --font ${TTF_DIR}${latin_ttf} -r ${ASCII},${DEGREE},${BULLET},${COMPARE} \ - --font ${TTF_DIR}${ttf} -r ${chars} \ - --font EdgeTX/extra.ttf -r ${EXTRA_SYM} \ - --font ${ARROWS_FONT} -r ${ARROWS} \ - --font ${SYMBOLS_FONT} -r ${SYMBOLS} \ - --format lvgl -o lv_font.inc --force-fast-kern-format --no-compress - compress_font ${dir}/lv_font_${name}_${sfx} + echo "Creating compressed font: ${name}_${sfx} (size: ${size})" + + # Use relative paths for lv_font_conv + lv_font_conv --no-prefilter --bpp 4 --size "${size}" \ + --font "../${latin_ttf}" -r "${ASCII},${DEGREE},${BULLET},${COMPARE}" \ + --font "../${ttf}" -r "${chars}" \ + --font "${EXTRA_FONT}" -r "${EXTRA_SYM}" \ + --font "${ARROWS_FONT}" -r "${ARROWS}" \ + --font "${SYMBOLS_FONT_REL}" -r "${SYMBOLS}" \ + --format lvgl -o "lv_font.inc" --force-fast-kern-format --no-compress + compress_font "${dir}/lv_font_${name}_${sfx}" } function make_font_w_extra_sym() { @@ -86,12 +136,14 @@ function make_font_w_extra_sym() { local dir=$6 local chars=$7 - lv_font_conv --no-prefilter --bpp 4 --size ${size} \ - --font ${TTF_DIR}${latin_ttf} -r ${ASCII},${DEGREE} \ - --font ${TTF_DIR}${ttf} -r ${chars} \ - --font EdgeTX/extra.ttf -r ${EXTRA_SYM} \ - --format lvgl -o lv_font.inc --force-fast-kern-format --no-compress - compress_font ${dir}/lv_font_${name}_${sfx} + echo "Creating font with extra symbols: ${name}_${sfx} (size: ${size})" + + lv_font_conv --no-prefilter --bpp 4 --size "${size}" \ + --font "../${latin_ttf}" -r "${ASCII},${DEGREE}" \ + --font "../${ttf}" -r "${chars}" \ + --font "${EXTRA_FONT}" -r "${EXTRA_SYM}" \ + --format lvgl -o "lv_font.inc" --force-fast-kern-format --no-compress + compress_font "${dir}/lv_font_${name}_${sfx}" } function make_font_no_sym() { @@ -103,11 +155,13 @@ function make_font_no_sym() { local dir=$6 local chars=$7 - lv_font_conv --no-prefilter --bpp 4 --size ${size} \ - --font ${TTF_DIR}${latin_ttf} -r ${ASCII},${DEGREE} \ - --font ${TTF_DIR}${ttf} -r ${chars} \ - --format lvgl -o lv_font.inc --force-fast-kern-format --no-compress - compress_font ${dir}/lv_font_${name}_${sfx} + echo "Creating font without symbols: ${name}_${sfx} (size: ${size})" + + lv_font_conv --no-prefilter --bpp 4 --size "${size}" \ + --font "../${latin_ttf}" -r "${ASCII},${DEGREE}" \ + --font "../${ttf}" -r "${chars}" \ + --format lvgl -o "lv_font.inc" --force-fast-kern-format --no-compress + compress_font "${dir}/lv_font_${name}_${sfx}" } function make_font_no_sym_no_trans() { @@ -117,26 +171,26 @@ function make_font_no_sym_no_trans() { local sfx=$4 local dir=$5 - lv_font_conv --no-prefilter --bpp 4 --size ${size} \ - --font ${TTF_DIR}${latin_ttf} -r ${ASCII},${DEGREE} \ - --format lvgl -o lv_font.inc --force-fast-kern-format --no-compress - compress_font ${dir}/lv_font_${name}_${sfx} + echo "Creating basic font: ${name}_${sfx} (size: ${size})" + + lv_font_conv --no-prefilter --bpp 4 --size "${size}" \ + --font "../${latin_ttf}" -r "${ASCII},${DEGREE}" \ + --format lvgl -o "lv_font.inc" --force-fast-kern-format --no-compress + compress_font "${dir}/lv_font_${name}_${sfx}" } -# LV_SYMBOL_CHARGE, LV_SYMBOL_NEW_LINE, LV_SYMBOL_SD_CARD, LV_SYMBOL_CLOSE -# LV_SYMBOL_FILE, LV_SYMBOL_OK, LV_SYMBOL_WIFI, LV_SYMBOL_USB -BL_SYMBOLS="61671,63650,63426,61453,61787,61452,61931,62087" - function make_bootloader_font() { local name=$1 local ttf=$2 local size=$3 local dir=$4 - lv_font_conv --no-prefilter --bpp 1 --size ${size} --no-compress \ - --font ${TTF_DIR}${ttf} -r ${ASCII} \ - --font ${SYMBOLS_FONT} -r ${BL_SYMBOLS} \ - --format lvgl -o ${dir}/lv_font_${name}.c --force-fast-kern-format --no-compress + echo "Creating bootloader font: ${name} (size: ${size})" + + lv_font_conv --no-prefilter --bpp 1 --size "${size}" --no-compress \ + --font "../${ttf}" -r "${ASCII}" \ + --font "${SYMBOLS_FONT_REL}" -r "${BL_SYMBOLS}" \ + --format lvgl -o "${dir}/lv_font_${name}.c" --force-fast-kern-format --no-compress } function make_font_set() { @@ -145,49 +199,79 @@ function make_font_set() { local ttf_bold=$3 local chars=$4 - make_font_lz4 "${name}" ${LATIN_FONT} "${ttf_normal}" 9 "XXS" "std" ${chars} - make_font_lz4 "${name}" ${LATIN_FONT} "${ttf_normal}" 13 "XS" "std" ${chars} - make_font "${name}" ${LATIN_FONT} "${ttf_normal}" 16 "STD" "std" ${chars} - make_font_lz4 "${name}_bold" ${LATIN_FONT_BOLD} "${ttf_bold}" 16 "STD" "std" ${chars} - make_font_w_extra_sym "${name}" ${LATIN_FONT} "${ttf_normal}" 24 "L" "std" ${chars} - make_font_no_sym "${name}_bold" ${LATIN_FONT_BOLD} "${ttf_bold}" 32 "XL" "std" ${chars} + if [[ -z "$chars" ]]; then + echo "WARNING: No characters found for ${name} font set. Skipping." >&2 + return 0 + fi - # 320x240 LCD fonts - make_font_lz4 "${name}" ${LATIN_FONT} "${ttf_normal}" 8 "XXS" "sml" ${chars} - make_font_lz4 "${name}" ${LATIN_FONT} "${ttf_normal}" 10 "XS" "sml" ${chars} - make_font "${name}" ${LATIN_FONT} "${ttf_normal}" 13 "STD" "sml" ${chars} - make_font_lz4 "${name}_bold" ${LATIN_FONT_BOLD} "${ttf_bold}" 13 "STD" "sml" ${chars} - make_font_w_extra_sym "${name}" ${LATIN_FONT} "${ttf_normal}" 19 "L" "sml" ${chars} - make_font_no_sym "${name}_bold" ${LATIN_FONT_BOLD} "${ttf_bold}" 25 "XL" "sml" ${chars} + echo "Creating font set for: ${name}" - # 800x480 LCD fonts - make_font_lz4 "${name}" ${LATIN_FONT} "${ttf_normal}" 13 "XXS" "lrg" ${chars} - make_font_lz4 "${name}" ${LATIN_FONT} "${ttf_normal}" 19 "XS" "lrg" ${chars} - make_font "${name}" ${LATIN_FONT} "${ttf_normal}" 24 "STD" "lrg" ${chars} - make_font_lz4 "${name}_bold" ${LATIN_FONT_BOLD} "${ttf_bold}" 24 "STD" "lrg" ${chars} - make_font_w_extra_sym "${name}" ${LATIN_FONT} "${ttf_normal}" 36 "L" "lrg" ${chars} - make_font_no_sym "${name}_bold" ${LATIN_FONT_BOLD} "${ttf_bold}" 48 "XL" "lrg" ${chars} + # Standard LCD fonts (480x272, 480x320, 320x480) + make_font_lz4 "${name}" "${LATIN_FONT}" "${ttf_normal}" 9 "XXS" "std" "${chars}" + make_font_lz4 "${name}" "${LATIN_FONT}" "${ttf_normal}" 13 "XS" "std" "${chars}" + make_font "${name}" "${LATIN_FONT}" "${ttf_normal}" 16 "STD" "std" "${chars}" + make_font_lz4 "${name}_bold" "${LATIN_FONT_BOLD}" "${ttf_bold}" 16 "STD" "std" "${chars}" + make_font_w_extra_sym "${name}" "${LATIN_FONT}" "${ttf_normal}" 24 "L" "std" "${chars}" + make_font_no_sym "${name}_bold" "${LATIN_FONT_BOLD}" "${ttf_bold}" 32 "XL" "std" "${chars}" + + # Small LCD fonts (320x240) + make_font_lz4 "${name}" "${LATIN_FONT}" "${ttf_normal}" 8 "XXS" "sml" "${chars}" + make_font_lz4 "${name}" "${LATIN_FONT}" "${ttf_normal}" 10 "XS" "sml" "${chars}" + make_font "${name}" "${LATIN_FONT}" "${ttf_normal}" 13 "STD" "sml" "${chars}" + make_font_lz4 "${name}_bold" "${LATIN_FONT_BOLD}" "${ttf_bold}" 13 "STD" "sml" "${chars}" + make_font_w_extra_sym "${name}" "${LATIN_FONT}" "${ttf_normal}" 19 "L" "sml" "${chars}" + make_font_no_sym "${name}_bold" "${LATIN_FONT_BOLD}" "${ttf_bold}" 25 "XL" "sml" "${chars}" + + # Large LCD fonts (800x480) + make_font_lz4 "${name}" "${LATIN_FONT}" "${ttf_normal}" 13 "XXS" "lrg" "${chars}" + make_font_lz4 "${name}" "${LATIN_FONT}" "${ttf_normal}" 19 "XS" "lrg" "${chars}" + make_font "${name}" "${LATIN_FONT}" "${ttf_normal}" 24 "STD" "lrg" "${chars}" + make_font_lz4 "${name}_bold" "${LATIN_FONT_BOLD}" "${ttf_bold}" 24 "STD" "lrg" "${chars}" + make_font_w_extra_sym "${name}" "${LATIN_FONT}" "${ttf_normal}" 36 "L" "lrg" "${chars}" + make_font_no_sym "${name}_bold" "${LATIN_FONT_BOLD}" "${ttf_bold}" 48 "XL" "lrg" "${chars}" } -# Bootloader font -make_bootloader_font "bl" "Roboto/Roboto-Regular-BL.ttf" 16 "std" # 480x272, 480x320, 320x480 -make_bootloader_font "bl" "Roboto/Roboto-Regular-BL.ttf" 14 "sml" # 320x240 -make_bootloader_font "bl" "Roboto/Roboto-Regular-BL.ttf" 24 "lrg" # 800x480 +# Main execution starts here +main() { + echo "Starting font generation..." -# XXL fonts (no translation chars) -make_font_no_sym_no_trans "en_bold" ${LATIN_FONT_BOLD} 64 "XXL" "std" -make_font_no_sym_no_trans "en_bold" ${LATIN_FONT_BOLD} 50 "XXL" "sml" -make_font_no_sym_no_trans "en_bold" ${LATIN_FONT_BOLD} 96 "XXL" "lrg" + # Change to script directory for python and other scripts to work correctly + cd "${SCRIPT_DIR}" -# Language fonts -make_font_set "en" ${LATIN_FONT} ${LATIN_FONT_BOLD} "${LATIN1}" -make_font_set "tw" "Noto/NotoSansCJKsc-Regular.otf" "Noto/NotoSansCJKsc-Bold.otf" "${TW_SYMBOLS}" -make_font_set "cn" "Noto/NotoSansCJKsc-Regular.otf" "Noto/NotoSansCJKsc-Bold.otf" "${CN_SYMBOLS}" -make_font_set "jp" "Noto/NotoSansCJKsc-Regular.otf" "Noto/NotoSansCJKsc-Bold.otf" "${JP_SYMBOLS}" -make_font_set "he" "Arimo/Arimo-Regular.ttf" "Arimo/Arimo-Bold.ttf" "${HE_SYMBOLS}" -make_font_set "ru" "Arimo/Arimo-Regular.ttf" "Arimo/Arimo-Bold.ttf" "${RU_SYMBOLS}" -make_font_set "ua" "Arimo/Arimo-Regular.ttf" "Arimo/Arimo-Bold.ttf" "${UA_SYMBOLS}" -make_font_set "ko" "Nanum/NanumBarunpenR.ttf" "Nanum/NanumBarunpenB.ttf" "${KO_SYMBOLS}" + # Check dependencies and setup + check_dependencies + get_translation_symbols + + # Bootloader fonts + echo "Generating bootloader fonts..." + make_bootloader_font "bl" "Roboto/Roboto-Regular-BL.ttf" 16 "std" # 480x272, 480x320, 320x480 + make_bootloader_font "bl" "Roboto/Roboto-Regular-BL.ttf" 14 "sml" # 320x240 + make_bootloader_font "bl" "Roboto/Roboto-Regular-BL.ttf" 24 "lrg" # 800x480 -rm lv_font.inc -rm lz4_font + # XXL fonts (no translation chars) + echo "Generating XXL fonts..." + make_font_no_sym_no_trans "en_bold" "${LATIN_FONT_BOLD}" 64 "XXL" "std" + make_font_no_sym_no_trans "en_bold" "${LATIN_FONT_BOLD}" 50 "XXL" "sml" + make_font_no_sym_no_trans "en_bold" "${LATIN_FONT_BOLD}" 96 "XXL" "lrg" + + # Language fonts + echo "Generating language font sets..." + make_font_set "en" "${LATIN_FONT}" "${LATIN_FONT_BOLD}" "${LATIN1}" + make_font_set "tw" "Noto/NotoSansCJKsc-Regular.otf" "Noto/NotoSansCJKsc-Bold.otf" "${TW_SYMBOLS}" + make_font_set "cn" "Noto/NotoSansCJKsc-Regular.otf" "Noto/NotoSansCJKsc-Bold.otf" "${CN_SYMBOLS}" + make_font_set "jp" "Noto/NotoSansCJKsc-Regular.otf" "Noto/NotoSansCJKsc-Bold.otf" "${JP_SYMBOLS}" + make_font_set "he" "Arimo/Arimo-Regular.ttf" "Arimo/Arimo-Bold.ttf" "${HE_SYMBOLS}" + make_font_set "ru" "Arimo/Arimo-Regular.ttf" "Arimo/Arimo-Bold.ttf" "${RU_SYMBOLS}" + make_font_set "ua" "Arimo/Arimo-Regular.ttf" "Arimo/Arimo-Bold.ttf" "${UA_SYMBOLS}" + make_font_set "ko" "Nanum/NanumBarunpenR.ttf" "Nanum/NanumBarunpenB.ttf" "${KO_SYMBOLS}" + + # Clean up temporary files + echo "Cleaning up temporary files..." + rm -f "${SCRIPT_DIR}/lv_font.inc" + rm -f "${SCRIPT_DIR}/lz4_font" + + echo "Font generation completed successfully!" +} + +# Run main function +main "$@" diff --git a/radio/src/translations/pl.h b/radio/src/translations/pl.h index 16f16e9c7..88fedd786 100644 --- a/radio/src/translations/pl.h +++ b/radio/src/translations/pl.h @@ -288,6 +288,7 @@ #define TR_SWITCH "Przełą" #define TR_FUNCTION_SWITCHES "Ustawiane przełączniki" #define TR_FS_COLOR_LIST "Custom","Off","White","Red","Green","Yellow","Orange","Blue","Pink" +#define TR_GROUP "Group" #define TR_GROUP_ALWAYS_ON "Always on" #define TR_FS_ON_COLOR TR("ON:","ON Color") #define TR_FS_OFF_COLOR TR("OFF:","OFF Color") diff --git a/tools/cfn_sorter.sh b/tools/cfn_sorter.sh index b58d23658..9bd9ef22a 100755 --- a/tools/cfn_sorter.sh +++ b/tools/cfn_sorter.sh @@ -29,6 +29,18 @@ set -e # zh_TW.UTF-8 / zh_TW.utf8 # uk_UA.UTF-8 / uk_UA.utf8 +# Get the directory where this script is located and the project root +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +# Define paths relative to project root +TOOLS_DIR="${PROJECT_ROOT}/tools" +RADIO_SRC_DIR="${PROJECT_ROOT}/radio/src" +CFN_SORTER_CPP="${TOOLS_DIR}/cfn_sorter.cpp" +COPYRIGHT_HEADER="${TOOLS_DIR}/copyright-header.txt" +OUTPUT_FILE="${RADIO_SRC_DIR}/cfn_sort.cpp" +EXECUTABLE="${SCRIPT_DIR}/a.out" + # Determine the compiler to use if command -v g++ >/dev/null 2>&1; then CXX=g++ @@ -49,38 +61,40 @@ compile_and_append() { echo "Compiling ${translation_macro} (${lng_macro}) ..." - $CXX -std=c++11 -lstdc++ -Wfatal-errors -D${lng_macro} tools/cfn_sorter.cpp + # Change to script directory for compilation + cd "${SCRIPT_DIR}" + $CXX -std=c++11 -lstdc++ -Wfatal-errors -D${lng_macro} "${CFN_SORTER_CPP}" { if [ "$condition" = "else" ]; then echo "#${condition}" else echo "#${condition} defined(${translation_macro})" fi - ./a.out - } >> radio/src/cfn_sort.cpp + "${EXECUTABLE}" + } >> "${OUTPUT_FILE}" } -rm radio/src/cfn_sort.cpp +# Ensure output directory exists +mkdir -p "${RADIO_SRC_DIR}" -# Copyright header -cat tools/copyright-header.txt > radio/src/cfn_sort.cpp +# Remove existing output file +rm -f "${OUTPUT_FILE}" -# Start of cfn_sort.cpp -echo "Compiling TRANSLATIONS_CN (LNG_CN) ..." -$CXX -std=c++11 -lstdc++ -Wfatal-errors -DLNG_CN tools/cfn_sorter.cpp -{ -cat <> radio/src/cfn_sort.cpp +# Start the file with boilerplate copyright header +cat "${COPYRIGHT_HEADER}" > "${OUTPUT_FILE}" + +# Add the rest of the file header +cat <> "${OUTPUT_FILE}" // This file is auto-generated via cfn_sorter.sh. Do not edit. #include "dataconstants.h" Functions cfn_sorted[] = { -#if defined(TRANSLATIONS_CN) EOF -./a.out -} >> radio/src/cfn_sort.cpp -# Languages +# Languages - compile and append each translation +compile_and_append "LNG_CN" "TRANSLATIONS_CN" "if" + compile_and_append "LNG_CZ" "TRANSLATIONS_CZ" "elif" compile_and_append "LNG_DA" "TRANSLATIONS_DA" "elif" compile_and_append "LNG_DE" "TRANSLATIONS_DE" "elif" @@ -99,11 +113,10 @@ compile_and_append "LNG_SE" "TRANSLATIONS_SE" "elif" compile_and_append "LNG_TW" "TRANSLATIONS_TW" "elif" compile_and_append "LNG_UA" "TRANSLATIONS_UA" "elif" -# Final else case compile_and_append "LNG_EN" "TRANSLATIONS_EN" "else" # End of cfn_sort.cpp -cat <> radio/src/cfn_sort.cpp +cat <> "${OUTPUT_FILE}" #endif }; @@ -116,4 +129,5 @@ uint8_t getFuncSortIdx(uint8_t func) } EOF -[ -f ./a.out ] && rm ./a.out +# Clean up +[ -f "${EXECUTABLE}" ] && rm "${EXECUTABLE}" diff --git a/tools/check_translations.py b/tools/check_translations.py new file mode 100755 index 000000000..f9f6b8b63 --- /dev/null +++ b/tools/check_translations.py @@ -0,0 +1,351 @@ +#!/usr/bin/env python3 +""" +Translation Checker for EdgeTX Translations + +This script can check: +1. Bootloader translations (bl_translations.h) - unified file with multiple languages +2. Individual language translation files (*.h) - separate files per language + +It ensures that all translation languages have the same set of translation strings defined. +""" + +import re +import sys +import os +from pathlib import Path +from collections import defaultdict +from typing import Dict, Set, List, Tuple, Optional +import argparse +import glob + +class TranslationChecker: + def __init__(self): + self.bootloader_translations = defaultdict(set) # language -> set of bootloader translation keys + self.language_translations = defaultdict(set) # language -> set of language translation keys + self.bootloader_keys = set() + self.language_keys = set() + self.checked_files = [] + + def find_translations_directory(self, start_path: str) -> Optional[Path]: + """Find the translations directory by searching up from the given path.""" + current_path = Path(start_path).resolve() + + # If the provided path is already the translations directory + if current_path.name == "translations" and current_path.is_dir(): + return current_path + + # If the provided path is a file in translations directory + if current_path.parent.name == "translations": + return current_path.parent + + # Search up the directory tree + while current_path != current_path.parent: + translations_path = current_path / "radio" / "src" / "translations" + if translations_path.exists() and translations_path.is_dir(): + return translations_path + current_path = current_path.parent + + return None + + def parse_bootloader_file(self, file_path: Path): + """Parse the bootloader translation file (bl_translations.h).""" + if not file_path.exists(): + print(f"Warning: File {file_path} does not exist") + return False + + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Split content into lines for easier processing + lines = content.split('\n') + current_language = None + in_translation_block = False + conditional_depth = 0 + + for i, line in enumerate(lines): + line = line.strip() + + # Skip empty lines and comments + if not line or line.startswith('//'): + continue + + # Check for translation language blocks + translation_match = re.match(r'#(?:if|elif)\s+defined\(TRANSLATIONS_([A-Z]+)\)', line) + if translation_match: + current_language = translation_match.group(1) + in_translation_block = True + conditional_depth = 0 + continue + + # Check for else block (default/English) + if re.match(r'#else', line) and in_translation_block and conditional_depth == 0: + current_language = "EN" # Default language + continue + + # Track conditional compilation depth + if re.match(r'#if', line) and in_translation_block: + conditional_depth += 1 + continue + elif re.match(r'#endif', line) and in_translation_block: + if conditional_depth > 0: + conditional_depth -= 1 + else: + # This is the end of the translation block + in_translation_block = False + current_language = None + continue + + # Skip non-translation lines + if not in_translation_block or not current_language: + continue + + # Parse #define statements + define_match = re.match(r'#define\s+(TR_BL_\w+)', line) + if define_match: + key = define_match.group(1) + self.bootloader_translations[current_language].add(key) + self.bootloader_keys.add(key) + + self.checked_files.append(str(file_path)) + return True + + def parse_language_file(self, file_path: Path) -> Optional[str]: + """Parse an individual language translation file (e.g., en.h, fr.h).""" + if not file_path.exists(): + return None + + # Extract language code from filename + language = file_path.stem.upper() + + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + content = f.read() + + # Find all #define TR_ statements + define_matches = re.findall(r'#define\s+(TR_\w+)', content) + + for key in define_matches: + self.language_translations[language].add(key) + self.language_keys.add(key) + + self.checked_files.append(str(file_path)) + return language + + def check_bootloader_translations(self, translations_dir: Path) -> bool: + """Check bootloader translations.""" + bl_file = translations_dir / "bl_translations.h" + return self.parse_bootloader_file(bl_file) + + def check_language_translations(self, translations_dir: Path) -> List[str]: + """Check individual language translation files.""" + languages_found = [] + + # Look for .h files that are language files (excluding special files) + exclude_files = {"bl_translations.h", "untranslated.h"} + + for h_file in translations_dir.glob("*.h"): + if h_file.name in exclude_files: + continue + + # Skip files that are clearly not language files + if h_file.name.startswith("tts_"): + continue + + language = self.parse_language_file(h_file) + if language: + languages_found.append(language) + + return languages_found + + def analyze(self) -> Dict[str, any]: + """Analyze translations and return results.""" + bootloader_languages = list(self.bootloader_translations.keys()) + language_languages = list(self.language_translations.keys()) + + results = { + "bootloader": { + "languages": bootloader_languages, + "total_keys": len(self.bootloader_keys), + "missing_keys": defaultdict(set), + "extra_keys": defaultdict(set), + "summary": {} + }, + "language_files": { + "languages": language_languages, + "total_keys": len(self.language_keys), + "missing_keys": defaultdict(set), + "extra_keys": defaultdict(set), + "summary": {} + }, + "checked_files": self.checked_files + } + + # Analyze bootloader translations + for lang in bootloader_languages: + lang_keys = self.bootloader_translations[lang] + results["bootloader"]["missing_keys"][lang] = self.bootloader_keys - lang_keys + results["bootloader"]["extra_keys"][lang] = lang_keys - self.bootloader_keys + results["bootloader"]["summary"][lang] = { + "total": len(lang_keys), + "missing": len(results["bootloader"]["missing_keys"][lang]), + "extra": len(results["bootloader"]["extra_keys"][lang]) + } + + # Analyze language file translations + for lang in language_languages: + lang_keys = self.language_translations[lang] + results["language_files"]["missing_keys"][lang] = self.language_keys - lang_keys + results["language_files"]["extra_keys"][lang] = lang_keys - self.language_keys + results["language_files"]["summary"][lang] = { + "total": len(lang_keys), + "missing": len(results["language_files"]["missing_keys"][lang]), + "extra": len(results["language_files"]["extra_keys"][lang]) + } + + return results + + def print_report(self, verbose=False): + """Print a detailed report of the translation analysis.""" + results = self.analyze() + + has_bootloader = bool(results["bootloader"]["languages"]) + has_language_files = bool(results["language_files"]["languages"]) + + if not has_bootloader and not has_language_files: + print("No translation files found or processed.") + return + + print("EdgeTX Translation Analysis") + print("=" * 50) + + if verbose: + print(f"Checked files: {len(results['checked_files'])}") + for file_path in results['checked_files']: + print(f" - {file_path}") + print() + + # Report bootloader translations + if has_bootloader: + self._print_section_report("Bootloader Translations (bl_translations.h)", + results["bootloader"], self.bootloader_keys, verbose) + + # Report language file translations + if has_language_files: + self._print_section_report("Language File Translations (*.h)", + results["language_files"], self.language_keys, verbose) + + def _print_section_report(self, title: str, section_results: Dict, all_keys: Set, verbose: bool): + """Print report for a specific section (bootloader or language files).""" + print(f"\n{title}") + print("-" * len(title)) + print(f"Total unique translation keys: {section_results['total_keys']}") + print(f"Languages found: {', '.join(sorted(section_results['languages']))}") + print() + + # Summary table + print("Summary by Language:") + print("-" * 60) + print(f"{'Language':<12} {'Total':<8} {'Missing':<10} {'Extra':<8}") + print("-" * 60) + + for lang in sorted(section_results['languages']): + summary = section_results['summary'][lang] + print(f"{lang:<12} {summary['total']:<8} {summary['missing']:<10} {summary['extra']:<8}") + + print() + + # Detailed missing keys report + has_issues = False + for lang in sorted(section_results['languages']): + missing = section_results['missing_keys'][lang] + extra = section_results['extra_keys'][lang] + + if missing or extra: + has_issues = True + print(f"Issues for {lang}:") + + if missing: + print(f" Missing keys ({len(missing)}):") + for key in sorted(missing): + print(f" - {key}") + + if extra: + print(f" Extra keys ({len(extra)}):") + for key in sorted(extra): + print(f" + {key}") + print() + + if not has_issues: + print("✅ All languages have consistent translation keys!") + else: + print("❌ Translation inconsistencies found!") + + # Show all translation keys for reference (only if verbose) + if verbose: + print(f"\nAll Translation Keys ({len(all_keys)}):") + print("-" * 40) + for key in sorted(all_keys): + print(f" {key}") + print() + +def main(): + parser = argparse.ArgumentParser( + description="Check EdgeTX translation consistency", + epilog=""" +Examples: + # Check bootloader translations only + python3 check_translations.py --bootloader + + # Check individual language files only + python3 check_translations.py --languages + + # Check both (default) + python3 check_translations.py + + # Check with verbose output + python3 check_translations.py -v + + # Specify path to translations directory or EdgeTX root + python3 check_translations.py /path/to/edgetx/radio/src/translations + """, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument("path", nargs="?", default=".", + help="Path to translations directory, EdgeTX root, or current directory") + parser.add_argument("-v", "--verbose", action="store_true", + help="Show all translation keys") + parser.add_argument("--bootloader", action="store_true", + help="Check only bootloader translations (bl_translations.h)") + parser.add_argument("--languages", action="store_true", + help="Check only individual language files (*.h)") + + args = parser.parse_args() + + checker = TranslationChecker() + + # Find translations directory + translations_dir = checker.find_translations_directory(args.path) + if not translations_dir: + print(f"Error: Could not find translations directory from path: {args.path}") + print("Please specify a path to the EdgeTX repository root or translations directory.") + sys.exit(1) + + print(f"Using translations directory: {translations_dir}") + print() + + # Determine what to check + check_bootloader = args.bootloader or not args.languages + check_languages = args.languages or not args.bootloader + + if check_bootloader: + if not checker.check_bootloader_translations(translations_dir): + print("Warning: Could not process bootloader translations") + + if check_languages: + languages_found = checker.check_language_translations(translations_dir) + if not languages_found: + print("Warning: No individual language translation files found") + + checker.print_report(verbose=args.verbose) + +if __name__ == "__main__": + main() diff --git a/tools/update-companion-translations.sh b/tools/update-companion-translations.sh new file mode 100755 index 000000000..eec1773b5 --- /dev/null +++ b/tools/update-companion-translations.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +# Stops on first error, echo on +set -e +set -x + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +. "$SCRIPT_DIR/build-common.sh" + +# Determine number of CPU cores for parallel builds +if [ -f /usr/bin/nproc ]; then + num_cpus=$(nproc) +elif [ "$(uname)" = "Darwin" ]; then + num_cpus=$(sysctl -n hw.ncpu) +elif [ -f /usr/sbin/sysctl ]; then + num_cpus=$(sysctl -n hw.logicalcpu) +else + num_cpus=2 +fi +: "${JOBS:=$num_cpus}" + +# Parse command line arguments +while [ $# -gt 0 ] +do + case "$1" in + --jobs=*) + JOBS="${1#*=}";; + -j*) + JOBS="${1#*j}";; + -h|--help) + echo "Usage: $0 [-j|--jobs=]" + echo "Build companion translations using tx16s target configuration" + echo "" + echo "Options:" + echo " -j, --jobs= Number of parallel jobs (default: $num_cpus)" + echo " -h, --help Show this help message" + exit 0;; + -*) + echo >&2 "usage: $0 [-j|--jobs=]" + echo >&2 "Use -h or --help for more information" + exit 1;; + *) + echo >&2 "usage: $0 [-j|--jobs=]" + echo >&2 "Use -h or --help for more information" + exit 1;; + esac + shift +done + +# Project root directory (one level up from tools) +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +# Set common build options +: "${BUILD_TYPE:=Release}" +: "${COMMON_OPTIONS:="-DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_RULE_MESSAGES=OFF -Wno-dev"}" + +BUILD_OPTIONS="${COMMON_OPTIONS} " + +echo "Building companion translations with tx16s target configuration..." + +# Get tx16s target build options from build-common.sh +if ! get_target_build_options "tx16s"; then + echo "Error: Failed to find a match for target 'tx16s'" + exit 1 +fi + +# Ensure we're in the project root +cd "${PROJECT_ROOT}" + +# Create and enter build directory +rm -rf build +mkdir build +cd build + +echo "Configuring build with options: ${BUILD_OPTIONS}" +cmake ${BUILD_OPTIONS} "${PROJECT_ROOT}" + +echo "Configuring native build..." +cmake --build . --target native-configure + +echo "Building companion translations with ${JOBS} parallel jobs..." +make -j"${JOBS}" -C native companion_translations