diff --git a/Documentation/Development.md b/Documentation/Development.md
index b9f94b2e..65e50955 100644
--- a/Documentation/Development.md
+++ b/Documentation/Development.md
@@ -1,30 +1,86 @@
# Development
-Building this software can be performed two ways, using the STMCubeIDE or using command line tools.
+Building this software can be performed two ways: using the STM32CubeIDE or using command line tools.
-## STM Cube IDE
+## STM32CubeIDE
-The easiest way to start working using the STM Cube IDE is to create a new project for the STM32F103RCTx.
-Then once this is created remove the auto-generated source code.
-Next drag the contents of the `source` folder into the project and choose to link to files.
-You will need to update the build settings for include paths & point to the new `.ld` linker file
+The easiest way to start working with the STM32CubeIDE is to create a new project for the STM32F103RCTx.
+Once this is created, remove the auto-generated source code.
+Next, drag the contents of the `source` folder into the project and choose to link to files.
+You will need to update the build settings for include paths and point to the new `.ld` linker file.
-## Developing with command line tools & building a release
+## Command line tools and building a release
-In the `source` folder there is a makefile that can be used to build the repository using command line tools.
-when running the `make` command, specify which model of the device & the language you would like to use.
+In the `source` folder there is a `Makefile` that can be used to build the repository using command line tools.
+When running the `make` command, specify which model of the device and the language(s) you would like to use.
-`make -j8 lang=EN model=TS80`
+### macOS
-To build a release instead, run the build.sh script. This will update translations and also build every language for both TS100 and TS80 models.
+Use the following steps to set up a build environment for IronOS on the command line (in Terminal).
+
+1. [Follow steps 1 – 3 here to install the toolchain](https://github.com/glegrain/STM32-with-macOS#0---installing-the-toolchain) needed to compile for STM32 microcontrollers.
+2. Install `python`:
+```
+brew install python
+```
+3. (Optional) Update `pip` so it doesn't warn you about being out-of-date:
+```
+python3 -m pip install --upgrade pip
+```
+4. Change to the `source` directory:
+```
+cd source
+```
+5. Create a Python virtual environment for IronOS named `ironos-venv` to keep your Python installation clean:
+```
+python3 -m venv ironos-venv
+```
+6. Activate the Python virtual environment:
+```
+source ironos-venv/bin/activate
+```
+7. Install the dependencies required to run `make-translation.py`:
+```
+pip install bdflib
+```
+8. All done! See some examples below for how you can build your own IronOS.
+
+### Examples
+
+To build a single language Simplified Chinese firmware for the TS80P with 8 simultaneous jobs:
+```
+make -j8 model=TS80P firmware-ZH_CN
+```
+
+To build a European multi-language firmware for the Pinecil with as many simultaneous jobs as there are logical processors on Linux:
+```
+make -j$(nproc) model=Pinecil firmware-multi_European
+```
+
+To build a Cyrillic compressed multi-language firmware for the Pinecil with as many simultaneous jobs as there are logical processors on macOS:
+```
+make -j$(sysctl -n hw.logicalcpu) model=Pinecil firmware-multi_compressed_Bulgarian+Russian+Serbian+Ukrainian
+```
+
+To build a custom multi-language firmware including English and Simplified Chinese for the TS80:
+```
+make -j8 model=TS80 custom_multi_langs="EN ZH_CN" firmware-multi_Custom
+```
+
+To build a custom compressed multi-language firmware including German, Spanish, and French for the TS100 (note if `model` is unspecified, it will default to `TS100`):
+```
+make -j8 custom_multi_langs="DE ES FR" firmware-multi_compressed_Custom
+```
+
+To build a release instead, run the `build.sh` script. This will update translations and also build every language for all device models. For macOS users, replace `make -j$(nproc)` in the script with `make -j$(sysctl -n hw.logicalcpu)` before running.
## Updating languages
-To update the language translation files & associated font map, execute the `make_translation.py` code from the translations directory.
+To update the language translation files and their associated font maps, execute the `make_translation.py` code from the `Translations` directory.
## Building Pinecil
-I highly recommend using the command line tools and using docker to run the compiler.
-It's a bit more fussy on setup than the STM tooling and this is by far the easiest way.
-If you _need_ an IDE I have used [Nuclei's IDE](https://nucleisys.com/download.php)
-And follow same idea as the STM Cube IDE notes above.
+I highly recommend using the command line tools and using Docker to run the compiler.
+It's a bit more fussy on setup than the STM tooling, and this is by far the easiest way.
+If you _need_ an IDE I have used [Nuclei's IDE](https://nucleisys.com/download.php).
+Follow the same idea as the STM Cube IDE notes above.
diff --git a/Translations/TranslationEditor.html b/Translations/TranslationEditor.html
index 91b982b7..2b57d344 100644
--- a/Translations/TranslationEditor.html
+++ b/Translations/TranslationEditor.html
@@ -308,6 +308,7 @@
diff --git a/Translations/font_tables.py b/Translations/font_tables.py
index e2f8cff9..94c3b210 100755
--- a/Translations/font_tables.py
+++ b/Translations/font_tables.py
@@ -326,6 +326,70 @@ def get_font_map_latin_extended() -> Dict[str, bytes]:
return font
+def get_font_map_greek() -> Dict[str, bytes]:
+ font = {
+ # U+0370..U+03FF Greek and Coptic
+ "Έ": b"\x06\xFC\xFC\x8C\x8C\x8C\x8C\x8C\x8C\x0C\x0C\x00\x00\x3F\x3F\x31\x31\x31\x31\x31\x31\x30\x30\x00",
+ "Α": b"\x00\x00\x00\xE0\xFC\x1F\x1F\xFC\xE0\x00\x00\x00\x00\x38\x3F\x07\x06\x06\x06\x06\x07\x3F\x38\x00",
+ "Β": b"\x00\xFF\xFF\xC3\xC3\xC3\xC3\xE7\xFE\xBC\x00\x00\x00\x3F\x3F\x30\x30\x30\x30\x30\x39\x1F\x0F\x00",
+ "Γ": b"\x00\xFF\xFF\x03\x03\x03\x03\x03\x03\x03\x03\x00\x00\x3F\x3F\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ "Δ": b"\x00\x00\x00\xE0\xFC\x1F\x1F\xFC\xE0\x00\x00\x00\x00\x38\x3F\x37\x30\x30\x30\x30\x37\x3F\x38\x00",
+ "Ε": b"\x00\xFF\xFF\xC3\xC3\xC3\xC3\xC3\xC3\x03\x03\x00\x00\x3F\x3F\x30\x30\x30\x30\x30\x30\x30\x30\x00",
+ "Ζ": b"\x00\x03\x03\x03\x03\xC3\xE3\x33\x1F\x0F\x03\x00\x00\x30\x3C\x3E\x33\x31\x30\x30\x30\x30\x30\x00",
+ "Η": b"\x00\xFF\xFF\xC0\xC0\xC0\xC0\xC0\xC0\xFF\xFF\x00\x00\x3F\x3F\x00\x00\x00\x00\x00\x00\x3F\x3F\x00",
+ "Θ": b"\x00\xF0\xFC\x0E\xC7\xC3\xC3\xC7\x0E\xFC\xF0\x00\x00\x03\x0F\x1C\x38\x30\x30\x38\x1C\x0F\x03\x00",
+ "Ι": b"\x00\x00\x00\x03\x03\xFF\xFF\x03\x03\x00\x00\x00\x00\x00\x00\x30\x30\x3F\x3F\x30\x30\x00\x00\x00",
+ "Κ": b"\x00\xFF\xFF\xC0\xE0\xF0\x38\x1C\x0E\x07\x03\x00\x00\x3F\x3F\x00\x01\x03\x07\x0E\x1C\x38\x30\x00",
+ "Λ": b"\x00\x00\x00\xE0\xFC\x1F\x1F\xFC\xE0\x00\x00\x00\x00\x38\x3F\x07\x00\x00\x00\x00\x07\x3F\x38\x00",
+ "Μ": b"\x00\xFF\xFF\x1E\x78\xE0\xE0\x78\x1E\xFF\xFF\x00\x00\x3F\x3F\x00\x00\x01\x01\x00\x00\x3F\x3F\x00",
+ "Ν": b"\x00\xFF\xFF\x0E\x38\xF0\xC0\x00\x00\xFF\xFF\x00\x00\x3F\x3F\x00\x00\x00\x03\x07\x1C\x3F\x3F\x00",
+ "Ξ": b"\x00\x03\x03\xC3\xC3\xC3\xC3\xC3\xC3\x03\x03\x00\x00\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x00",
+ "Ο": b"\x00\xF0\xFC\x0E\x07\x03\x03\x07\x0E\xFC\xF0\x00\x00\x03\x0F\x1C\x38\x30\x30\x38\x1C\x0F\x03\x00",
+ "Π": b"\x00\xFF\xFF\x03\x03\x03\x03\x03\x03\xFF\xFF\x00\x00\x3F\x3F\x00\x00\x00\x00\x00\x00\x3F\x3F\x00",
+ "Ρ": b"\x00\xFF\xFF\x83\x83\x83\x83\x83\xC7\xFE\x7C\x00\x00\x3F\x3F\x01\x01\x01\x01\x01\x01\x00\x00\x00",
+ "Σ": b"\x00\x03\x0F\x1F\x33\xE3\xE3\x03\x03\x03\x03\x00\x00\x30\x3C\x3E\x33\x31\x30\x30\x30\x30\x30\x00",
+ "Τ": b"\x00\x03\x03\x03\x03\xFF\xFF\x03\x03\x03\x03\x00\x00\x00\x00\x00\x00\x3F\x3F\x00\x00\x00\x00\x00",
+ "Υ": b"\x00\x03\x0F\x3C\xF0\xC0\xC0\xF0\x3C\x0F\x03\x00\x00\x00\x00\x00\x00\x3F\x3F\x00\x00\x00\x00\x00",
+ "Φ": b"\x00\xF8\xFC\x0E\x06\xFF\xFF\x06\x0E\xFC\xF8\x00\x00\x03\x07\x0E\x0C\x3F\x3F\x0C\x0E\x07\x03\x00",
+ "Χ": b"\x00\x03\x0F\x3C\xF0\xC0\xC0\xF0\x3C\x0F\x03\x00\x00\x30\x3C\x0F\x03\x00\x00\x03\x0F\x3C\x30\x00",
+ "Ψ": b"\x00\x3F\x7F\xE0\xC0\xFF\xFF\xC0\xE0\x7F\x3F\x00\x00\x00\x00\x00\x00\x3F\x3F\x00\x00\x00\x00\x00",
+ "Ω": b"\x00\xF0\xFC\x0E\x07\x03\x03\x07\x0E\xFC\xF0\x00\x00\x63\x6F\x7C\x70\x00\x00\x70\x7C\x6F\x63\x00",
+ "ά": b"\x00\x80\xC0\xE0\x60\x6C\x6E\x66\xC0\xE0\xE0\x00\x00\x0F\x1F\x38\x30\x30\x30\x30\x18\x3F\x3F\x20",
+ "έ": b"\x00\xE0\xF0\x30\x30\x36\x37\x33\x30\x70\x60\x00\x00\x1D\x3F\x33\x33\x33\x33\x33\x33\x38\x18\x00",
+ "ή": b"\x00\xE0\xE0\xC0\x60\x6C\x6E\xE6\xE0\xC0\x00\x00\x00\x1F\x1F\x00\x00\x00\x00\x00\x7F\x7F\x00\x00",
+ "ί": b"\x00\x00\x00\xEC\xEE\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1F\x3F\x30\x30\x30\x00\x00\x00\x00",
+ "α": b"\x00\x80\xC0\xE0\x60\x60\x60\x60\xC0\xE0\xE0\x00\x00\x0F\x1F\x38\x30\x30\x30\x30\x18\x3F\x3F\x20",
+ "β": b"\x00\x00\xC0\xE0\x30\x10\x10\x30\xE0\xC0\x00\x00\x00\x00\xFF\xFF\x21\x21\x21\x33\x3F\x1E\x00\x00",
+ "γ": b"\x00\x60\xE0\x80\x00\x00\x00\x00\x80\xE0\x60\x00\x00\x00\x01\x07\x1E\xF8\xF8\x1E\x07\x01\x00\x00",
+ "δ": b"\x00\x83\xC7\xEF\x7F\x7B\x73\x63\xE3\xC3\x83\x00\x00\x0F\x1F\x38\x30\x30\x30\x30\x38\x1F\x0F\x00",
+ "ε": b"\x00\xE0\xF0\x30\x30\x30\x30\x30\x30\x70\x60\x00\x00\x1D\x3F\x33\x33\x33\x33\x33\x33\x38\x18\x00",
+ "ζ": b"\x00\x83\xC3\xE3\x63\x63\x63\x73\x3F\x1F\x00\x00\x00\x0F\x1F\x38\x30\x30\x30\xF0\xE0\x00\x00\x00",
+ "η": b"\x00\xE0\xE0\xC0\x60\x60\x60\xE0\xE0\xC0\x00\x00\x00\x1F\x1F\x00\x00\x00\x00\x00\x7F\x7F\x00\x00",
+ "θ": b"\x00\xF0\xF8\x1C\x8C\x8C\x8C\x8C\x1C\xF8\xF0\x00\x00\x0F\x1F\x38\x31\x31\x31\x31\x38\x1F\x0F\x00",
+ "ι": b"\x00\x00\x00\xE0\xE0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1F\x3F\x30\x30\x30\x00\x00\x00\x00",
+ "κ": b"\x00\x00\xC0\xC0\x00\x00\x80\xC0\xC0\x00\x00\x00\x00\x00\x3F\x3F\x06\x0F\x1F\x39\x30\x00\x00\x00",
+ "λ": b"\x00\x00\x00\x00\xC0\xE0\xE0\xC0\x00\x00\x00\x00\x00\x30\x3C\x0F\x03\x00\x00\x03\x0F\x3C\x30\x00",
+ "μ": b"\x00\xF0\xF0\x00\x00\x00\x00\x00\xF0\xF0\x00\x00\x00\xFF\xFF\x0E\x0C\x0C\x0C\x06\x0F\x0F\x00\x00",
+ "ν": b"\x00\x60\xE0\x80\x00\x00\x00\x00\x80\xE0\x60\x00\x00\x00\x01\x07\x1E\x38\x38\x1E\x07\x01\x00\x00",
+ "ξ": b"\x00\x3C\xFE\xE7\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x0F\x1F\x39\x30\x30\xF0\xE0\x00\x00\x00\x00",
+ "ο": b"\x00\x80\xC0\xE0\x60\x60\x60\x60\xE0\xC0\x80\x00\x00\x0F\x1F\x38\x30\x30\x30\x30\x38\x1F\x0F\x00",
+ "π": b"\x00\x60\xE0\xE0\x60\x60\x60\xE0\xE0\x60\x00\x00\x00\x00\x3F\x3F\x00\x00\x00\x3F\x3F\x30\x00\x00",
+ "ρ": b"\x00\xE0\xE0\x60\x60\x60\x60\x60\xE0\xC0\x80\x00\x00\xFF\xFF\x0C\x18\x18\x18\x18\x1C\x0F\x07\x00",
+ "ς": b"\x00\x80\xC0\xE0\x60\x60\x60\xE0\xC0\x80\x00\x00\x00\x0F\x1F\x38\x30\x30\xF0\xE0\x03\x03\x00\x00",
+ "σ": b"\x00\x80\xC0\xE0\x60\x60\x60\xE0\xE0\xE0\x60\x00\x00\x0F\x1F\x38\x30\x30\x30\x38\x1F\x0F\x00\x00",
+ "τ": b"\x00\x60\x60\xE0\xE0\x60\x60\x60\x00\x00\x00\x00\x00\x00\x00\x1F\x3F\x30\x30\x30\x00\x00\x00\x00",
+ "υ": b"\x00\xE0\xE0\x00\x00\x00\x00\x00\xE0\xE0\x00\x00\x00\x0F\x1F\x38\x30\x30\x30\x38\x1F\x0F\x00\x00",
+ "φ": b"\x00\xC0\xE0\x70\x30\xF8\xF8\x30\x70\xE0\xC0\x00\x00\x07\x0F\x1C\x18\x7F\x7F\x18\x1C\x0F\x07\x00",
+ "χ": b"\x00\x70\xF0\xC0\x80\x00\x80\xC0\xF0\x70\x00\x00\x00\x70\x78\x1D\x0F\x07\x0F\x1D\x78\x70\x00\x00",
+ "ψ": b"\x00\xE0\xE0\x00\x00\xE0\xE0\x00\x00\xE0\xE0\x00\x00\x07\x0F\x1C\x18\x7F\x7F\x18\x1C\x0F\x07\x00",
+ "ω": b"\x00\xC0\xE0\x00\x00\x00\x00\x00\x00\xE0\xC0\x00\x00\x0F\x1F\x38\x38\x1E\x1E\x38\x38\x1F\x0F\x00",
+ "ό": b"\x00\x80\xC0\xE0\x60\x6C\x6E\x66\xE0\xC0\x80\x00\x00\x0F\x1F\x38\x30\x30\x30\x30\x38\x1F\x0F\x00",
+ "ύ": b"\x00\xE0\xE0\x00\x00\x0C\x0E\x06\xE0\xE0\x00\x00\x00\x0F\x1F\x38\x30\x30\x30\x38\x1F\x0F\x00\x00",
+ "ώ": b"\x00\xC0\xE0\x00\x00\x18\x1C\x0C\x00\xE0\xC0\x00\x00\x0F\x1F\x38\x38\x1E\x1E\x38\x38\x1F\x0F\x00",
+ }
+ return font
+
+
def get_font_map_cyrillic() -> Dict[str, bytes]:
font = {
# U+0400..U+04FF Cyrillic
@@ -750,6 +814,70 @@ def get_small_font_map_latin_extended() -> Dict[str, bytes]:
return font
+def get_small_font_map_greek() -> Dict[str, bytes]:
+ font = {
+ # U+0370..U+03FF Greek and Coptic
+ "Έ": b"\x03\x7F\x49\x49\x49\x41",
+ "Α": b"\x7e\x09\x09\x09\x7e\x00",
+ "Β": b"\x7f\x49\x49\x49\x36\x00",
+ "Γ": b"\x7f\x01\x01\x01\x01\x00",
+ "Δ": b"\x70\x4C\x43\x4C\x70\x00",
+ "Ε": b"\x7f\x49\x49\x49\x41\x00",
+ "Ζ": b"\x61\x51\x49\x45\x43\x00",
+ "Η": b"\x7f\x08\x08\x08\x7f\x00",
+ "Θ": b"\x3E\x49\x49\x49\x3E\x00",
+ "Ι": b"\x00\x41\x7f\x41\x00\x00",
+ "Κ": b"\x7f\x08\x14\x22\x41\x00",
+ "Λ": b"\x70\x0C\x03\x0C\x70\x00",
+ "Μ": b"\x7f\x02\x0c\x02\x7f\x00",
+ "Ν": b"\x7f\x04\x08\x10\x7f\x00",
+ "Ξ": b"\x41\x49\x49\x49\x41\x00",
+ "Ο": b"\x3e\x41\x41\x41\x3e\x00",
+ "Π": b"\x7F\x01\x01\x01\x7F\x00",
+ "Ρ": b"\x7f\x09\x09\x09\x06\x00",
+ "Σ": b"\x63\x55\x49\x41\x41\x00",
+ "Τ": b"\x01\x01\x7f\x01\x01\x00",
+ "Υ": b"\x07\x08\x70\x08\x07\x00",
+ "Φ": b"\x0c\x12\x7f\x12\x0c\x00",
+ "Χ": b"\x63\x14\x08\x14\x63\x00",
+ "Ψ": b"\x07\x08\x7F\x08\x07\x00",
+ "Ω": b"\x5E\x61\x01\x61\x5E\x00",
+ "ά": b"\x38\x45\x45\x38\x7C\x40",
+ "έ": b"\x28\x55\x55\x44\x28\x00",
+ "ή": b"\x04\x79\x05\x04\xF8\x00",
+ "ί": b"\x04\x3D\x41\x40\x00\x00",
+ "α": b"\x38\x44\x44\x38\x7C\x40",
+ "β": b"\x7E\x21\x25\x25\x1A\x00",
+ "γ": b"\x0C\x10\x60\x10\x0C\x00",
+ "δ": b"\x30\x4B\x45\x49\x30\x00",
+ "ε": b"\x28\x54\x54\x44\x28\x00",
+ "ζ": b"\x00\x31\x49\x45\xC3\x00",
+ "η": b"\x04\x78\x04\x04\xF8\x00",
+ "θ": b"\x3E\x49\x49\x49\x3E\x00",
+ "ι": b"\x04\x3C\x40\x40\x00\x00",
+ "κ": b"\x00\x7C\x10\x28\x44\x00",
+ "λ": b"\x70\x0A\x04\x08\x70\x00",
+ "μ": b"\xFC\x10\x10\x3C\x20\x00",
+ "ν": b"\x1C\x20\x40\x20\x1C\x00",
+ "ξ": b"\x36\x49\x49\xC9\x00\x00",
+ "ο": b"\x38\x44\x44\x44\x38\x00",
+ "π": b"\x04\x7C\x04\x7C\x44\x00",
+ "ρ": b"\xF8\x24\x24\x24\x18\x00",
+ "ς": b"\x38\x44\x44\xC4\x04\x00",
+ "σ": b"\x38\x44\x44\x44\x3C\x04",
+ "τ": b"\x04\x3C\x44\x44\x00\x00",
+ "υ": b"\x3C\x40\x40\x40\x3C\x00",
+ "φ": b"\x18\x24\x7e\x24\x18\x00",
+ "χ": b"\x44\x24\x38\x48\x44\x00",
+ "ψ": b"\x1C\x20\x7C\x20\x1C\x00",
+ "ω": b"\x38\x44\x30\x44\x38\x00",
+ "ό": b"\x38\x45\x45\x44\x38\x00",
+ "ύ": b"\x3C\x41\x41\x40\x3C\x00",
+ "ώ": b"\x38\x45\x31\x44\x38\x00",
+ }
+ return font
+
+
def get_small_font_map_cyrillic() -> Dict[str, bytes]:
font = {
# U+0400..U+04FF Cyrillic
@@ -859,6 +987,7 @@ NAME_ASCII_BASIC: Final = "ascii_basic"
NAME_LATIN_EXTENDED: Final = "latin_extended"
NAME_CYRILLIC: Final = "cyrillic"
NAME_CJK: Final = "cjk"
+NAME_GREEK: Final = "greek"
def get_font_maps_for_name(
@@ -870,5 +999,7 @@ def get_font_maps_for_name(
return get_font_map_latin_extended(), get_small_font_map_latin_extended()
elif font_name == NAME_CYRILLIC:
return get_font_map_cyrillic(), get_small_font_map_cyrillic()
+ elif font_name == NAME_GREEK:
+ return get_font_map_greek(), get_small_font_map_greek()
else:
raise ValueError("Invalid font name")
diff --git a/Translations/make_translation.py b/Translations/make_translation.py
index 99145216..085a3ec9 100755
--- a/Translations/make_translation.py
+++ b/Translations/make_translation.py
@@ -1,1370 +1,1370 @@
-#!/usr/bin/env python3
-
-import argparse
-import functools
-import json
-import hashlib
-import logging
-import os
-import pickle
-import re
-import subprocess
-import sys
-from datetime import datetime
-from itertools import chain
-from pathlib import Path
-from typing import BinaryIO, Dict, List, Optional, TextIO, Tuple, Union
-from dataclasses import dataclass
-
-from bdflib import reader as bdfreader
-from bdflib.model import Font, Glyph
-
-import font_tables
-import brieflz
-import objcopy
-
-logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
-
-HERE = Path(__file__).resolve().parent
-
-
-@functools.lru_cache(maxsize=None)
-def cjk_font() -> Font:
- with open(os.path.join(HERE, "wqy-bitmapsong/wenquanyi_9pt.bdf"), "rb") as f:
- return bdfreader.read_bdf(f)
-
-
-# Loading a single JSON file
-def load_json(filename: str, skip_first_line: bool) -> dict:
- with open(filename) as f:
- if skip_first_line:
- f.readline()
- return json.loads(f.read())
-
-
-def get_language_unqiue_id(language_ascii_name: str):
- """
- Given a language code, it will return a unique (enough) uint16_t id code
- When we have a collision here we can tweak this, but language list should be fairly stable from now on
- """
- return (
- int(hashlib.sha1(language_ascii_name.encode("utf-8")).hexdigest(), 16) % 0xFFFF
- )
-
-
-def read_translation(json_root: Union[str, Path], lang_code: str) -> dict:
- filename = f"translation_{lang_code}.json"
-
- file_with_path = os.path.join(json_root, filename)
-
- try:
- lang = load_json(file_with_path, skip_first_line=False)
- except json.decoder.JSONDecodeError as e:
- logging.error(f"Failed to decode {filename}")
- logging.exception(str(e))
- sys.exit(2)
-
- validate_langcode_matches_content(filename, lang)
-
- return lang
-
-
-def validate_langcode_matches_content(filename: str, content: dict) -> None:
- # Extract lang code from file name
- lang_code = filename[12:-5].upper()
- # ...and the one specified in the JSON file...
- try:
- lang_code_from_json = content["languageCode"]
- except KeyError:
- lang_code_from_json = "(missing)"
-
- # ...cause they should be the same!
- if lang_code != lang_code_from_json:
- raise ValueError(
- f"Invalid languageCode {lang_code_from_json} in file {filename}"
- )
-
-
-def write_start(f: TextIO):
- f.write(
- "// WARNING: THIS FILE WAS AUTO GENERATED BY make_translation.py. PLEASE DO NOT EDIT.\n"
- )
- f.write("\n")
- f.write('#include "Translation.h"\n')
-
-
-def get_constants(build_version: str) -> List[Tuple[str, str]]:
- # Extra constants that are used in the firmware that are shared across all languages
- return [
- ("SymbolPlus", "+"),
- ("SymbolMinus", "-"),
- ("SymbolSpace", " "),
- ("SymbolDot", "."),
- ("SymbolDegC", "C"),
- ("SymbolDegF", "F"),
- ("SymbolMinutes", "M"),
- ("SymbolSeconds", "S"),
- ("SymbolWatts", "W"),
- ("SymbolVolts", "V"),
- ("SymbolDC", "DC"),
- ("SymbolCellCount", "S"),
- ("SymbolVersionNumber", build_version),
- ]
-
-
-def get_debug_menu() -> List[str]:
- return [
- datetime.today().strftime("%d-%m-%y"),
- "HW G ",
- "HW M ",
- "HW P ",
- "Time ",
- "Move ",
- "RTip ",
- "CTip ",
- "CHan ",
- "Vin ",
- "ACC ",
- "PWR ",
- "Max ",
- ]
-
-
-def get_accel_names_list() -> List[str]:
- return [
- "Scanning",
- "None",
- "MMA8652FC",
- "LIS2DH12",
- "BMA223",
- "MSA301",
- "SC7A20",
- ]
-
-
-def get_power_source_list() -> List[str]:
- return [
- "DC",
- "QC",
- "PD",
- ]
-
-
-def get_letter_counts(
- defs: dict, lang: dict, build_version: str
-) -> Tuple[List[str], Dict[str, int]]:
- text_list = []
- # iterate over all strings
- obj = lang["menuOptions"]
- for mod in defs["menuOptions"]:
- eid = mod["id"]
- text_list.append(obj[eid]["desc"])
-
- obj = lang["messages"]
- for mod in defs["messages"]:
- eid = mod["id"]
- if eid not in obj:
- text_list.append(mod["default"])
- else:
- text_list.append(obj[eid])
-
- obj = lang["messagesWarn"]
- for mod in defs["messagesWarn"]:
- eid = mod["id"]
- if isinstance(obj[eid], list):
- text_list.append(obj[eid][0])
- text_list.append(obj[eid][1])
- else:
- text_list.append(obj[eid])
-
- obj = lang["characters"]
-
- for mod in defs["characters"]:
- eid = mod["id"]
- text_list.append(obj[eid])
-
- obj = lang["menuOptions"]
- for mod in defs["menuOptions"]:
- eid = mod["id"]
- if isinstance(obj[eid]["text2"], list):
- text_list.append(obj[eid]["text2"][0])
- text_list.append(obj[eid]["text2"][1])
- else:
- text_list.append(obj[eid]["text2"])
-
- obj = lang["menuGroups"]
- for mod in defs["menuGroups"]:
- eid = mod["id"]
- if isinstance(obj[eid]["text2"], list):
- text_list.append(obj[eid]["text2"][0])
- text_list.append(obj[eid]["text2"][1])
- else:
- text_list.append(obj[eid]["text2"])
-
- obj = lang["menuGroups"]
- for mod in defs["menuGroups"]:
- eid = mod["id"]
- text_list.append(obj[eid]["desc"])
- constants = get_constants(build_version)
- for x in constants:
- text_list.append(x[1])
- text_list.extend(get_debug_menu())
- text_list.extend(get_accel_names_list())
- text_list.extend(get_power_source_list())
-
- # collapse all strings down into the composite letters and store totals for these
-
- symbol_counts: dict[str, int] = {}
- for line in text_list:
- line = line.replace("\n", "").replace("\r", "")
- line = line.replace("\\n", "").replace("\\r", "")
- if line:
- for letter in line:
- symbol_counts[letter] = symbol_counts.get(letter, 0) + 1
- # swap to Big -> little sort order
- symbols_by_occurrence = [
- x[0]
- for x in sorted(
- symbol_counts.items(), key=lambda kv: (kv[1], kv[0]), reverse=True
- )
- ]
- return symbols_by_occurrence, symbol_counts
-
-
-def get_cjk_glyph(sym: str) -> bytes:
- glyph: Glyph = cjk_font()[ord(sym)]
-
- data = glyph.data
- src_left, src_bottom, src_w, src_h = glyph.get_bounding_box()
- dst_w = 12
- dst_h = 16
-
- # The source data is a per-row list of ints. The first item is the bottom-
- # most row. For each row, the LSB is the right-most pixel.
- # Here, (x, y) is the coordinates with origin at the top-left.
- def get_cell(x: int, y: int) -> bool:
- # Adjust x coordinates by actual bounding box.
- adj_x = x - src_left
- if adj_x < 0 or adj_x >= src_w:
- return False
- # Adjust y coordinates by actual bounding box, then place the glyph
- # baseline 3px above the bottom edge to make it centre-ish.
- # This metric is optimized for WenQuanYi Bitmap Song 9pt and assumes
- # each glyph is to be placed in a 12x12px box.
- adj_y = y - (dst_h - src_h - src_bottom - 3)
- if adj_y < 0 or adj_y >= src_h:
- return False
- if data[src_h - adj_y - 1] & (1 << (src_w - adj_x - 1)):
- return True
- else:
- return False
-
- # A glyph in the font table is divided into upper and lower parts, each by
- # 8px high. Each byte represents half if a column, with the LSB being the
- # top-most pixel. The data goes from the left-most to the right-most column
- # of the top half, then from the left-most to the right-most column of the
- # bottom half.
- bs = bytearray()
- for block in range(2):
- for c in range(dst_w):
- b = 0
- for r in range(8):
- if get_cell(c, r + 8 * block):
- b |= 0x01 << r
- bs.append(b)
- return bytes(bs)
-
-
-def get_bytes_from_font_index(index: int) -> bytes:
- """
- Converts the font table index into its corresponding bytes
- """
-
- # We want to be able to use more than 254 symbols (excluding \x00 null
- # terminator and \x01 new-line) in the font table but without making all
- # the chars take 2 bytes. To do this, we use \xF1 to \xFF as lead bytes
- # to designate double-byte chars, and leave the remaining as single-byte
- # chars.
- #
- # For the sake of sanity, \x00 always means the end of string, so we skip
- # \xF1\x00 and others in the mapping.
- #
- # Mapping example:
- #
- # 0x02 => 2
- # 0x03 => 3
- # ...
- # 0xEF => 239
- # 0xF0 => 240
- # 0xF1 0x01 => 1 * 0xFF - 15 + 1 = 241
- # 0xF1 0x02 => 1 * 0xFF - 15 + 2 = 242
- # ...
- # 0xF1 0xFF => 1 * 0xFF - 15 + 255 = 495
- # 0xF2 0x01 => 2 * 0xFF - 15 + 1 = 496
- # ...
- # 0xF2 0xFF => 2 * 0xFF - 15 + 255 = 750
- # 0xF3 0x01 => 3 * 0xFF - 15 + 1 = 751
- # ...
- # 0xFF 0xFF => 15 * 0xFF - 15 + 255 = 4065
-
- if index < 0:
- raise ValueError("index must be positive")
- page = (index + 0x0E) // 0xFF
- if page > 0x0F:
- raise ValueError("page value out of range")
- if page == 0:
- return bytes([index])
- else:
- # Into extended range
- # Leader is 0xFz where z is the page number
- # Following char is the remainder
- leader = page + 0xF0
- value = ((index + 0x0E) % 0xFF) + 0x01
-
- if leader > 0xFF or value > 0xFF:
- raise ValueError("value is out of range")
- return bytes([leader, value])
-
-
-def bytes_to_escaped(b: bytes) -> str:
- return "".join((f"\\x{i:02X}" for i in b))
-
-
-def bytes_to_c_hex(b: bytes) -> str:
- return ", ".join((f"0x{i:02X}" for i in b)) + ","
-
-
-@dataclass
-class FontMap:
- font12: Dict[str, bytes]
- font06: Dict[str, Optional[bytes]]
-
-
-@dataclass
-class FontMapsPerFont:
- font12_maps: Dict[str, Dict[str, bytes]]
- font06_maps: Dict[str, Dict[str, Optional[bytes]]]
- sym_lists: Dict[str, List[str]]
-
-
-def get_font_map_per_font(text_list: List[str], fonts: List[str]) -> FontMapsPerFont:
- pending_sym_set = set(text_list)
- if len(pending_sym_set) != len(text_list):
- raise ValueError("`text_list` contains duplicated symbols")
-
- if fonts[0] != font_tables.NAME_ASCII_BASIC:
- raise ValueError(
- f'First item in `fonts` must be "{font_tables.NAME_ASCII_BASIC}"'
- )
-
- total_symbol_count = len(text_list)
- # \x00 is for NULL termination and \x01 is for newline, so the maximum
- # number of symbols allowed is as follow (see also the comments in
- # `get_bytes_from_font_index`):
- if total_symbol_count > (0x10 * 0xFF - 15) - 2: # 4063
- raise ValueError(
- f"Error, too many used symbols for this version (total {total_symbol_count})"
- )
-
- logging.info(f"Generating fonts for {total_symbol_count} symbols")
-
- # Collect font bitmaps by the defined font order:
- font12_maps: Dict[str, Dict[str, bytes]] = {}
- font06_maps: Dict[str, Dict[str, Optional[bytes]]] = {}
- sym_lists: Dict[str, List[str]] = {}
- for font in fonts:
- font12_maps[font] = {}
- font12_map = font12_maps[font]
- font06_maps[font] = {}
- font06_map = font06_maps[font]
- sym_lists[font] = []
- sym_list = sym_lists[font]
-
- if len(pending_sym_set) == 0:
- logging.warning(
- f"Font {font} not used because all symbols already have font bitmaps"
- )
- continue
-
- if font == font_tables.NAME_CJK:
- is_cjk = True
- else:
- is_cjk = False
- font12: Dict[str, bytes]
- font06: Dict[str, bytes]
- font12, font06 = font_tables.get_font_maps_for_name(font)
-
- for sym in text_list:
- if sym not in pending_sym_set:
- continue
- if is_cjk:
- font12_line = get_cjk_glyph(sym)
- if font12_line is None:
- continue
- font06_line = None
- else:
- try:
- font12_line = font12[sym]
- font06_line = font06[sym]
- except KeyError:
- continue
- font12_map[sym] = font12_line
- font06_map[sym] = font06_line
- sym_list.append(sym)
- pending_sym_set.remove(sym)
-
- if len(sym_list) == 0:
- logging.warning(f"Font {font} not used by any symbols on the list")
- if len(pending_sym_set) > 0:
- raise KeyError(f"Symbols not found in specified fonts: {pending_sym_set}")
-
- return FontMapsPerFont(font12_maps, font06_maps, sym_lists)
-
-
-def get_forced_first_symbols() -> List[str]:
- forced_first_symbols = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
- return forced_first_symbols
-
-
-def get_sym_list_and_font_map(
- text_list: List[str], fonts: List[str]
-) -> Tuple[List[str], Dict[str, List[str]], FontMap]:
- font_maps = get_font_map_per_font(text_list, fonts)
- font12_maps = font_maps.font12_maps
- font06_maps = font_maps.font06_maps
-
- # Build the full font maps
- font12_map = {}
- font06_map = {}
- for font in fonts:
- font12_map.update(font12_maps[font])
- font06_map.update(font06_maps[font])
-
- # Collect all symbols by the original symbol order, but also making sure
- # all symbols with only large font must be placed after all symbols with
- # both small and large fonts
- sym_list_both_fonts = []
- sym_list_large_only = []
- for sym in text_list:
- if font06_map[sym] is None:
- sym_list_large_only.append(sym)
- else:
- sym_list_both_fonts.append(sym)
- sym_list = sym_list_both_fonts + sym_list_large_only
-
- return sym_list, font_maps.sym_lists, FontMap(font12_map, font06_map)
-
-
-def build_symbol_conversion_map(sym_list: List[str]) -> Dict[str, bytes]:
- forced_first_symbols = get_forced_first_symbols()
- if sym_list[: len(forced_first_symbols)] != forced_first_symbols:
- raise ValueError("Symbol list does not start with forced_first_symbols.")
-
- # the text list is sorted
- # allocate out these in their order as number codes
- symbol_map: Dict[str, bytes] = {"\n": bytes([1])}
- index = 2 # start at 2, as 0= null terminator,1 = new line
-
- # Assign symbol bytes by font index
- for index, sym in enumerate(sym_list, index):
- assert sym not in symbol_map
- symbol_map[sym] = get_bytes_from_font_index(index)
-
- return symbol_map
-
-
-def make_font_table_cpp(
- sym_list: List[str], font_map: FontMap, symbol_map: Dict[str, bytes]
-) -> str:
- output_table = make_font_table_named_cpp(
- "USER_FONT_12", sym_list, font_map.font12, symbol_map
- )
- output_table += make_font_table_06_cpp(sym_list, font_map, symbol_map)
- return output_table
-
-
-def make_font_table_named_cpp(
- name: Optional[str],
- sym_list: List[str],
- font_map: Dict[str, bytes],
- symbol_map: Dict[str, bytes],
-) -> str:
- output_table = ""
- if name:
- output_table = f"const uint8_t {name}[] = {{\n"
- for sym in sym_list:
- output_table += f"{bytes_to_c_hex(font_map[sym])}//{bytes_to_escaped(symbol_map[sym])} -> {sym}\n"
- if name:
- output_table += f"}}; // {name}\n"
- return output_table
-
-
-def make_font_table_06_cpp(
- sym_list: List[str], font_map: FontMap, symbol_map: Dict[str, bytes]
-) -> str:
- output_table = "const uint8_t USER_FONT_6x8[] = {\n"
- for sym in sym_list:
- font_bytes = font_map.font06[sym]
- if font_bytes:
- font_line = bytes_to_c_hex(font_bytes)
- else:
- font_line = "// " # placeholder
- output_table += f"{font_line}//{bytes_to_escaped(symbol_map[sym])} -> {sym}\n"
- output_table += "};\n"
- return output_table
-
-
-def convert_string_bytes(symbol_conversion_table: Dict[str, bytes], text: str) -> bytes:
- # convert all of the symbols from the string into bytes for their content
- output_string = b""
- for c in text.replace("\\r", "").replace("\\n", "\n"):
- if c not in symbol_conversion_table:
- logging.error(f"Missing font definition for {c}")
- sys.exit(1)
- else:
- output_string += symbol_conversion_table[c]
- return output_string
-
-
-def convert_string(symbol_conversion_table: Dict[str, bytes], text: str) -> str:
- # convert all of the symbols from the string into escapes for their content
- return bytes_to_escaped(convert_string_bytes(symbol_conversion_table, text))
-
-
-def escape(string: str) -> str:
- return json.dumps(string, ensure_ascii=False)
-
-
-def write_bytes_as_c_array(
- f: TextIO, name: str, data: bytes, indent: int = 2, bytes_per_line: int = 16
-) -> None:
- f.write(f"const uint8_t {name}[] = {{\n")
- for i in range(0, len(data), bytes_per_line):
- f.write(" " * indent)
- f.write(", ".join((f"0x{b:02X}" for b in data[i : i + bytes_per_line])))
- f.write(",\n")
- f.write(f"}}; // {name}\n\n")
-
-
-@dataclass
-class LanguageData:
- langs: List[dict]
- defs: dict
- build_version: str
- sym_list: List[str]
- sym_lists_by_font: Dict[str, List[str]]
- font_map: FontMap
-
-
-def prepare_language(lang: dict, defs: dict, build_version: str) -> LanguageData:
- language_code: str = lang["languageCode"]
- logging.info(f"Preparing language data for {language_code}")
- # Iterate over all of the text to build up the symbols & counts
- text_list, _ = get_letter_counts(defs, lang, build_version)
- # From the letter counts, need to make a symbol translator & write out the font
- fonts = lang["fonts"]
-
- forced_first_symbols = get_forced_first_symbols()
-
- # We enforce that numbers come first.
- text_list = forced_first_symbols + [
- x for x in text_list if x not in forced_first_symbols
- ]
-
- sym_list, sym_lists_by_font, font_map = get_sym_list_and_font_map(text_list, fonts)
- return LanguageData(
- [lang], defs, build_version, sym_list, sym_lists_by_font, font_map
- )
-
-
-def prepare_languages(
- langs: List[dict], defs: dict, build_version: str
-) -> LanguageData:
- language_codes: List[str] = [lang["languageCode"] for lang in langs]
- logging.info(f"Preparing language data for {language_codes}")
-
- forced_first_symbols = get_forced_first_symbols()
-
- all_fonts = [
- font_tables.NAME_ASCII_BASIC,
- font_tables.NAME_LATIN_EXTENDED,
- font_tables.NAME_CYRILLIC,
- font_tables.NAME_CJK,
- ]
-
- # Build the full font maps
- font12_map = {}
- font06_map = {}
- # Calculate total symbol counts per font:
- total_sym_counts: Dict[str, Dict[str, int]] = {}
- for lang in langs:
- text_list, sym_counts = get_letter_counts(defs, lang, build_version)
- fonts = lang["fonts"]
- text_list = forced_first_symbols + [
- x for x in text_list if x not in forced_first_symbols
- ]
- font_maps = get_font_map_per_font(text_list, fonts)
- for font in fonts:
- font12_map.update(font_maps.font12_maps[font])
- font06_map.update(font_maps.font06_maps[font])
- for font, font_sym_list in font_maps.sym_lists.items():
- font_total_sym_counts = total_sym_counts.get(font, {})
- for sym in font_sym_list:
- font_total_sym_counts[sym] = font_total_sym_counts.get(
- sym, 0
- ) + sym_counts.get(sym, 0)
- total_sym_counts[font] = font_total_sym_counts
-
- sym_lists_by_font: Dict[str, List[str]] = {}
- combined_sym_list = []
- for font in all_fonts:
- if font not in total_sym_counts:
- continue
- # swap to Big -> little sort order
- current_sym_list = [
- x[0]
- for x in sorted(
- total_sym_counts[font].items(),
- key=lambda kv: (kv[1], kv[0]),
- reverse=True,
- )
- ]
- if font == font_tables.NAME_ASCII_BASIC:
- # We enforce that numbers come first.
- current_sym_list = forced_first_symbols + [
- x for x in current_sym_list if x not in forced_first_symbols
- ]
- sym_lists_by_font[font] = current_sym_list
- combined_sym_list.extend(current_sym_list)
-
- return LanguageData(
- langs,
- defs,
- build_version,
- combined_sym_list,
- sym_lists_by_font,
- FontMap(font12_map, font06_map),
- )
-
-
-def write_language(
- data: LanguageData,
- f: TextIO,
- strings_bin: Optional[bytes] = None,
- compress_font: bool = False,
-) -> None:
- if len(data.langs) > 1:
- raise ValueError("More than 1 languages are provided")
- lang = data.langs[0]
- defs = data.defs
- build_version = data.build_version
- sym_list = data.sym_list
- font_map = data.font_map
-
- symbol_conversion_table = build_symbol_conversion_map(sym_list)
-
- language_code: str = lang["languageCode"]
- logging.info(f"Generating block for {language_code}")
-
- try:
- lang_name = lang["languageLocalName"]
- except KeyError:
- lang_name = language_code
-
- if strings_bin or compress_font:
- f.write('#include "brieflz.h"\n')
-
- f.write(f"\n// ---- {lang_name} ----\n\n")
-
- if not compress_font:
- font_table_text = make_font_table_cpp(
- sym_list, font_map, symbol_conversion_table
- )
- f.write(font_table_text)
- f.write(
- "const FontSection FontSectionsData[] = {\n"
- " {\n"
- " .symbol_start = 2,\n"
- f" .symbol_end = {len(sym_list) + 2},\n"
- " .font12_start_ptr = USER_FONT_12,\n"
- " .font06_start_ptr = USER_FONT_6x8,\n"
- " },\n"
- "};\n"
- "const FontSection *const FontSections = FontSectionsData;\n"
- "const uint8_t FontSectionsCount = sizeof(FontSectionsData) / sizeof(FontSectionsData[0]);\n"
- )
- else:
- font12_uncompressed = bytearray()
- for sym in sym_list:
- font12_uncompressed.extend(font_map.font12[sym])
- font12_compressed = brieflz.compress(bytes(font12_uncompressed))
- logging.info(
- f"Font table 12x16 compressed from {len(font12_uncompressed)} to {len(font12_compressed)} bytes (ratio {len(font12_compressed) / len(font12_uncompressed):.3})"
- )
- write_bytes_as_c_array(f, "font_12x16_brieflz", font12_compressed)
- font_table_text = make_font_table_06_cpp(
- sym_list, font_map, symbol_conversion_table
- )
- f.write(font_table_text)
- f.write(
- f"static uint8_t font_out_buffer[{len(font12_uncompressed)}];\n"
- "const FontSection FontSectionsData[] = {\n"
- " {\n"
- " .symbol_start = 2,\n"
- f" .symbol_end = {len(sym_list) + 2},\n"
- " .font12_start_ptr = font_out_buffer,\n"
- " .font06_start_ptr = USER_FONT_6x8,\n"
- " },\n"
- "};\n"
- "const FontSection *const FontSections = FontSectionsData;\n"
- "const uint8_t FontSectionsCount = sizeof(FontSectionsData) / sizeof(FontSectionsData[0]);\n"
- )
-
- f.write(f"\n// ---- {lang_name} ----\n\n")
-
- translation_common_text = get_translation_common_text(
- defs, symbol_conversion_table, build_version
- )
- f.write(translation_common_text)
- f.write(
- f"const bool HasFahrenheit = {('true' if lang.get('tempUnitFahrenheit', True) else 'false')};\n\n"
- )
-
- if not strings_bin:
- translation_strings_and_indices_text = get_translation_strings_and_indices_text(
- lang, defs, symbol_conversion_table
- )
- f.write(translation_strings_and_indices_text)
- f.write(
- "const TranslationIndexTable *Tr = &translation.indices;\n"
- "const char *TranslationStrings = translation.strings;\n\n"
- )
- else:
- compressed = brieflz.compress(strings_bin)
- logging.info(
- f"Strings compressed from {len(strings_bin)} to {len(compressed)} bytes (ratio {len(compressed) / len(strings_bin):.3})"
- )
- write_bytes_as_c_array(f, "translation_data_brieflz", compressed)
- f.write(
- f"static uint8_t translation_data_out_buffer[{len(strings_bin)}] __attribute__((__aligned__(2)));\n\n"
- "const TranslationIndexTable *Tr = reinterpret_cast(translation_data_out_buffer);\n"
- "const char *TranslationStrings = reinterpret_cast(translation_data_out_buffer) + sizeof(TranslationIndexTable);\n\n"
- )
-
- if not strings_bin and not compress_font:
- f.write("void prepareTranslations() {}\n\n")
- else:
- f.write("void prepareTranslations() {\n")
- if compress_font:
- f.write(
- " blz_depack_srcsize(font_12x16_brieflz, font_out_buffer, sizeof(font_12x16_brieflz));\n"
- )
- if strings_bin:
- f.write(
- " blz_depack_srcsize(translation_data_brieflz, translation_data_out_buffer, sizeof(translation_data_brieflz));\n"
- )
- f.write("}\n\n")
-
- sanity_checks_text = get_translation_sanity_checks_text(defs)
- f.write(sanity_checks_text)
-
-
-def write_languages(
- data: LanguageData,
- f: TextIO,
- strings_obj_path: Optional[str] = None,
- compress_font: bool = False,
-) -> None:
- defs = data.defs
- build_version = data.build_version
- combined_sym_list = data.sym_list
- sym_lists_by_font = data.sym_lists_by_font
- font_map = data.font_map
-
- symbol_conversion_table = build_symbol_conversion_map(combined_sym_list)
-
- language_codes: List[str] = [lang["languageCode"] for lang in data.langs]
- logging.info(f"Generating block for {language_codes}")
-
- lang_names = [
- lang.get("languageLocalName", lang["languageCode"]) for lang in data.langs
- ]
-
- f.write('#include "Translation_multi.h"')
-
- f.write(f"\n// ---- {lang_names} ----\n\n")
-
- max_decompressed_font_size = 0
- if not compress_font:
- font_table_text = ""
- font_section_info_text = (
- "const FontSectionDataInfo FontSectionDataInfos[] = {\n"
- )
- for font, current_sym_list in sym_lists_by_font.items():
- font_table_text += f"const uint8_t font_table_data_{font}[] = {{\n"
- font_table_text += "// 12x16:\n"
- font_table_text += make_font_table_named_cpp(
- None,
- current_sym_list,
- font_map.font12,
- symbol_conversion_table,
- )
- if font != font_tables.NAME_CJK:
- font_table_text += "// 6x8:\n"
- font_table_text += make_font_table_named_cpp(
- None,
- current_sym_list,
- font_map.font06, # type: ignore[arg-type]
- symbol_conversion_table,
- )
- font_table_text += f"}}; // font_table_data_{font}\n"
- current_sym_start = combined_sym_list.index(current_sym_list[0]) + 2
- font_section_info_text += (
- " {\n"
- f" .symbol_start = {current_sym_start},\n"
- f" .symbol_count = {len(current_sym_list)},\n"
- f" .data_size = sizeof(font_table_data_{font}),\n"
- " .data_is_compressed = false,\n"
- f" .data_ptr = font_table_data_{font},\n"
- " },\n"
- )
-
- f.write(font_table_text)
- font_section_info_text += (
- "};\n"
- "const uint8_t FontSectionDataCount = sizeof(FontSectionDataInfos) / sizeof(FontSectionDataInfos[0]);\n\n"
- )
- f.write(font_section_info_text)
- f.write(
- "FontSection DynamicFontSections[4] = {};\n"
- "const FontSection *const FontSections = DynamicFontSections;\n"
- "const uint8_t FontSectionsCount = sizeof(DynamicFontSections) / sizeof(DynamicFontSections[0]);\n"
- )
- else:
- font_section_info_text = (
- "const FontSectionDataInfo FontSectionDataInfos[] = {\n"
- )
- for font, current_sym_list in sym_lists_by_font.items():
- current_sym_start = combined_sym_list.index(current_sym_list[0]) + 2
- font_uncompressed = bytearray()
- for sym in current_sym_list:
- font_uncompressed.extend(font_map.font12[sym])
- if font != font_tables.NAME_CJK:
- for sym in current_sym_list:
- font_uncompressed.extend(font_map.font06[sym]) # type: ignore[arg-type]
- font_compressed = brieflz.compress(bytes(font_uncompressed))
- logging.info(
- f"Font table for {font} compressed from {len(font_uncompressed)} to {len(font_compressed)} bytes (ratio {len(font_compressed) / len(font_uncompressed):.3})"
- )
- max_decompressed_font_size += len(font_uncompressed)
- write_bytes_as_c_array(f, f"font_data_brieflz_{font}", font_compressed)
- font_section_info_text += (
- " {\n"
- f" .symbol_start = {current_sym_start},\n"
- f" .symbol_count = {len(current_sym_list)},\n"
- f" .data_size = sizeof(font_data_brieflz_{font}),\n"
- " .data_is_compressed = true,\n"
- f" .data_ptr = font_data_brieflz_{font},\n"
- " },\n"
- )
- font_section_info_text += (
- "};\n"
- "const uint8_t FontSectionDataCount = sizeof(FontSectionDataInfos) / sizeof(FontSectionDataInfos[0]);\n\n"
- )
- f.write(font_section_info_text)
- f.write(
- "FontSection DynamicFontSections[4] = {};\n"
- "const FontSection *const FontSections = DynamicFontSections;\n"
- "const uint8_t FontSectionsCount = sizeof(DynamicFontSections) / sizeof(DynamicFontSections[0]);\n"
- )
-
- f.write(f"\n// ---- {lang_names} ----\n\n")
-
- translation_common_text = get_translation_common_text(
- defs, symbol_conversion_table, build_version
- )
- f.write(translation_common_text)
- f.write(
- f"const bool HasFahrenheit = {('true' if any([lang.get('tempUnitFahrenheit', True) for lang in data.langs]) else 'false')};\n\n"
- )
-
- max_decompressed_translation_size = 0
- if not strings_obj_path:
- for lang in data.langs:
- lang_code = lang["languageCode"]
- translation_strings_and_indices_text = (
- get_translation_strings_and_indices_text(
- lang, defs, symbol_conversion_table, suffix=f"_{lang_code}"
- )
- )
- f.write(translation_strings_and_indices_text)
- f.write("const LanguageMeta LanguageMetas[] = {\n")
- for lang in data.langs:
- lang_code = lang["languageCode"]
- lang_id = get_language_unqiue_id(lang_code)
- f.write(
- " {\n"
- f" .uniqueID = {lang_id},\n"
- f" .translation_data = reinterpret_cast(&translation_{lang_code}),\n"
- f" .translation_size = sizeof(translation_{lang_code}),\n"
- f" .translation_is_compressed = false,\n"
- " },\n"
- )
- f.write("};\n")
- else:
- for lang in data.langs:
- lang_code = lang["languageCode"]
- sym_name = objcopy.cpp_var_to_section_name(f"translation_{lang_code}")
- strings_bin = objcopy.get_binary_from_obj(strings_obj_path, sym_name)
- if len(strings_bin) == 0:
- raise ValueError(f"Output for {sym_name} is empty")
- max_decompressed_translation_size = max(
- max_decompressed_translation_size, len(strings_bin)
- )
- compressed = brieflz.compress(strings_bin)
- logging.info(
- f"Strings for {lang_code} compressed from {len(strings_bin)} to {len(compressed)} bytes (ratio {len(compressed) / len(strings_bin):.3})"
- )
- write_bytes_as_c_array(
- f, f"translation_data_brieflz_{lang_code}", compressed
- )
- f.write("const LanguageMeta LanguageMetas[] = {\n")
- for lang in data.langs:
- lang_code = lang["languageCode"]
- lang_id = get_language_unqiue_id(lang_code)
- f.write(
- " {\n"
- f" .uniqueID = {lang_id},\n"
- f" .translation_data = translation_data_brieflz_{lang_code},\n"
- f" .translation_size = sizeof(translation_data_brieflz_{lang_code}),\n"
- f" .translation_is_compressed = true,\n"
- " },\n"
- )
- f.write("};\n")
- f.write(
- "const uint8_t LanguageCount = sizeof(LanguageMetas) / sizeof(LanguageMetas[0]);\n\n"
- f"alignas(TranslationData) uint8_t translation_data_out_buffer[{max_decompressed_translation_size + max_decompressed_font_size}];\n"
- "const uint16_t translation_data_out_buffer_size = sizeof(translation_data_out_buffer);\n\n"
- )
-
- sanity_checks_text = get_translation_sanity_checks_text(defs)
- f.write(sanity_checks_text)
-
-
-def get_translation_common_text(
- defs: dict, symbol_conversion_table: Dict[str, bytes], build_version
-) -> str:
- translation_common_text = ""
-
- # Write out firmware constant options
- constants = get_constants(build_version)
- for x in constants:
- translation_common_text += f'const char* {x[0]} = "{convert_string(symbol_conversion_table, x[1])}";//{x[1]} \n'
- translation_common_text += "\n"
-
- # Debug Menu
- translation_common_text += "const char* DebugMenu[] = {\n"
-
- for c in get_debug_menu():
- translation_common_text += (
- f'\t "{convert_string(symbol_conversion_table, c)}",//{c} \n'
- )
- translation_common_text += "};\n\n"
-
- # accel names
- translation_common_text += "const char* AccelTypeNames[] = {\n"
-
- for c in get_accel_names_list():
- translation_common_text += (
- f'\t "{convert_string(symbol_conversion_table, c)}",//{c} \n'
- )
- translation_common_text += "};\n\n"
-
- # power source types
- translation_common_text += "const char* PowerSourceNames[] = {\n"
-
- for c in get_power_source_list():
- translation_common_text += (
- f'\t "{convert_string(symbol_conversion_table, c)}",//{c} \n'
- )
- translation_common_text += "};\n\n"
-
- return translation_common_text
-
-
-@dataclass
-class TranslationItem:
- info: str
- str_index: int
-
-
-def get_translation_strings_and_indices_text(
- lang: dict, defs: dict, symbol_conversion_table: Dict[str, bytes], suffix: str = ""
-) -> str:
- str_table: List[str] = []
- str_group_messages: List[TranslationItem] = []
- str_group_messageswarn: List[TranslationItem] = []
- str_group_characters: List[TranslationItem] = []
- str_group_settingdesc: List[TranslationItem] = []
- str_group_settingshortnames: List[TranslationItem] = []
- str_group_settingmenuentries: List[TranslationItem] = []
- str_group_settingmenuentriesdesc: List[TranslationItem] = []
-
- eid: str
-
- # ----- Reading SettingsDescriptions
- obj = lang["menuOptions"]
-
- for index, mod in enumerate(defs["menuOptions"]):
- eid = mod["id"]
- str_group_settingdesc.append(
- TranslationItem(f"[{index:02d}] {eid}", len(str_table))
- )
- str_table.append(obj[eid]["desc"])
-
- # ----- Reading Message strings
-
- obj = lang["messages"]
-
- for mod in defs["messages"]:
- eid = mod["id"]
- source_text = ""
- if "default" in mod:
- source_text = mod["default"]
- if eid in obj:
- source_text = obj[eid]
- str_group_messages.append(TranslationItem(eid, len(str_table)))
- str_table.append(source_text)
-
- obj = lang["messagesWarn"]
-
- for mod in defs["messagesWarn"]:
- eid = mod["id"]
- if isinstance(obj[eid], list):
- if not obj[eid][1]:
- source_text = obj[eid][0]
- else:
- source_text = obj[eid][0] + "\n" + obj[eid][1]
- else:
- source_text = "\n" + obj[eid]
- str_group_messageswarn.append(TranslationItem(eid, len(str_table)))
- str_table.append(source_text)
-
- # ----- Reading Characters
-
- obj = lang["characters"]
-
- for mod in defs["characters"]:
- eid = mod["id"]
- str_group_characters.append(TranslationItem(eid, len(str_table)))
- str_table.append(obj[eid])
-
- # ----- Reading SettingsDescriptions
- obj = lang["menuOptions"]
-
- for index, mod in enumerate(defs["menuOptions"]):
- eid = mod["id"]
- if isinstance(obj[eid]["text2"], list):
- if not obj[eid]["text2"][1]:
- source_text = obj[eid]["text2"][0]
- else:
- source_text = obj[eid]["text2"][0] + "\n" + obj[eid]["text2"][1]
- else:
- source_text = "\n" + obj[eid]["text2"]
- str_group_settingshortnames.append(
- TranslationItem(f"[{index:02d}] {eid}", len(str_table))
- )
- str_table.append(source_text)
-
- # ----- Reading Menu Groups
- obj = lang["menuGroups"]
-
- for index, mod in enumerate(defs["menuGroups"]):
- eid = mod["id"]
- if isinstance(obj[eid]["text2"], list):
- if not obj[eid]["text2"][1]:
- source_text = obj[eid]["text2"][0]
- else:
- source_text = obj[eid]["text2"][0] + "\n" + obj[eid]["text2"][1]
- else:
- source_text = "\n" + obj[eid]["text2"]
- str_group_settingmenuentries.append(
- TranslationItem(f"[{index:02d}] {eid}", len(str_table))
- )
- str_table.append(source_text)
-
- # ----- Reading Menu Groups Descriptions
- obj = lang["menuGroups"]
-
- for index, mod in enumerate(defs["menuGroups"]):
- eid = mod["id"]
- str_group_settingmenuentriesdesc.append(
- TranslationItem(f"[{index:02d}] {eid}", len(str_table))
- )
- str_table.append(obj[eid]["desc"])
-
- @dataclass
- class RemappedTranslationItem:
- str_index: int
- str_start_offset: int = 0
-
- # ----- Perform suffix merging optimization:
- #
- # We sort the backward strings so that strings with the same suffix will
- # be next to each other, e.g.:
- # "ef\0",
- # "cdef\0",
- # "abcdef\0",
- backward_sorted_table: List[Tuple[int, str, bytes]] = sorted(
- (
- (i, s, bytes(reversed(convert_string_bytes(symbol_conversion_table, s))))
- for i, s in enumerate(str_table)
- ),
- key=lambda x: x[2],
- )
- str_remapping: List[Optional[RemappedTranslationItem]] = [None] * len(str_table)
- for i, (str_index, source_str, converted) in enumerate(backward_sorted_table[:-1]):
- j = i
- while backward_sorted_table[j + 1][2].startswith(converted):
- j += 1
- if j + 1 == len(backward_sorted_table):
- break
- if j != i:
- str_remapping[str_index] = RemappedTranslationItem(
- str_index=backward_sorted_table[j][0],
- str_start_offset=len(backward_sorted_table[j][2]) - len(converted),
- )
-
- # ----- Write the string table:
- str_offsets = [-1] * len(str_table)
- offset = 0
- write_null = False
- # NOTE: Cannot specify C99 designator here due to GCC (g++) bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=55227
- translation_strings_text = " /* .strings = */ {\n"
- for i, source_str in enumerate(str_table):
- if str_remapping[i] is not None:
- continue
- if write_null:
- translation_strings_text += ' "\\0"\n'
- write_null = True
- # Find what items use this string
- str_used_by = [i] + [
- j for j, r in enumerate(str_remapping) if r and r.str_index == i
- ]
- for j in str_used_by:
- for group, pre_info in [
- (str_group_messages, "messages"),
- (str_group_messageswarn, "messagesWarn"),
- (str_group_characters, "characters"),
- (str_group_settingdesc, "SettingsDescriptions"),
- (str_group_settingshortnames, "SettingsShortNames"),
- (str_group_settingmenuentries, "SettingsMenuEntries"),
- (str_group_settingmenuentriesdesc, "SettingsMenuEntriesDescriptions"),
- ]:
- for item in group:
- if item.str_index == j:
- translation_strings_text += (
- f" // - {pre_info} {item.info}\n"
- )
- if j == i:
- translation_strings_text += (
- f" // {offset: >4}: {escape(source_str)}\n"
- )
- str_offsets[j] = offset
- else:
- remapped = str_remapping[j]
- assert remapped is not None
- translation_strings_text += f" // {offset + remapped.str_start_offset: >4}: {escape(str_table[j])}\n"
- str_offsets[j] = offset + remapped.str_start_offset
- converted_bytes = convert_string_bytes(symbol_conversion_table, source_str)
- translation_strings_text += f' "{bytes_to_escaped(converted_bytes)}"'
- str_offsets[i] = offset
- # Add the length and the null terminator
- offset += len(converted_bytes) + 1
- translation_strings_text += "\n }, // .strings\n\n"
-
- str_total_bytes = offset
-
- def get_offset(idx: int) -> int:
- assert str_offsets[idx] >= 0
- return str_offsets[idx]
-
- translation_indices_text = " .indices = {\n"
-
- # ----- Write the messages string indices:
- for group in [str_group_messages, str_group_messageswarn, str_group_characters]:
- for item in group:
- translation_indices_text += f" .{item.info} = {get_offset(item.str_index)}, // {escape(str_table[item.str_index])}\n"
- translation_indices_text += "\n"
-
- # ----- Write the settings index tables:
- for group, name in [
- (str_group_settingdesc, "SettingsDescriptions"),
- (str_group_settingshortnames, "SettingsShortNames"),
- (str_group_settingmenuentries, "SettingsMenuEntries"),
- (str_group_settingmenuentriesdesc, "SettingsMenuEntriesDescriptions"),
- ]:
- max_len = 30
- translation_indices_text += f" .{name} = {{\n"
- for item in group:
- translation_indices_text += f" /* {item.info.ljust(max_len)[:max_len]} */ {get_offset(item.str_index)}, // {escape(str_table[item.str_index])}\n"
- translation_indices_text += f" }}, // {name}\n\n"
-
- translation_indices_text += " }, // .indices\n\n"
-
- return (
- "struct {\n"
- " TranslationIndexTable indices;\n"
- f" char strings[{str_total_bytes}];\n"
- f"}} const translation{suffix} = {{\n"
- + translation_indices_text
- + translation_strings_text
- + f"}}; // translation{suffix}\n\n"
- )
-
-
-def get_translation_sanity_checks_text(defs: dict) -> str:
- sanity_checks_text = "\n// Verify SettingsItemIndex values:\n"
- for i, mod in enumerate(defs["menuOptions"]):
- eid = mod["id"]
- sanity_checks_text += (
- f"static_assert(static_cast(SettingsItemIndex::{eid}) == {i});\n"
- )
- sanity_checks_text += f"static_assert(static_cast(SettingsItemIndex::NUM_ITEMS) == {len(defs['menuOptions'])});\n"
- return sanity_checks_text
-
-
-def read_version() -> str:
- with open(HERE.parent / "source" / "version.h") as version_file:
- for line in version_file:
- if re.findall(r"^.*(?<=(#define)).*(?<=(BUILD_VERSION))", line):
- matches = re.findall(r"\"(.+?)\"", line)
- if matches:
- version = matches[0]
- try:
- version += f".{subprocess.check_output(['git', 'rev-parse', '--short=7', 'HEAD']).strip().decode('ascii').upper()}"
- # --short=7: the shorted hash with 7 digits. Increase/decrease if needed!
- except OSError:
- version += " git"
- return version
-
-
-def parse_args() -> argparse.Namespace:
- parser = argparse.ArgumentParser()
- parser.add_argument(
- "--output-pickled",
- help="Write pickled language data for later reuse",
- type=argparse.FileType("wb"),
- required=False,
- dest="output_pickled",
- )
- parser.add_argument(
- "--input-pickled",
- help="Use previously generated pickled language data",
- type=argparse.FileType("rb"),
- required=False,
- dest="input_pickled",
- )
- parser.add_argument(
- "--strings-obj",
- help="Use generated TranslationData by extracting from object file",
- type=argparse.FileType("rb"),
- required=False,
- dest="strings_obj",
- )
- parser.add_argument(
- "--compress-font",
- help="Compress the font table",
- action="store_true",
- required=False,
- dest="compress_font",
- )
- parser.add_argument(
- "--output", "-o", help="Target file", type=argparse.FileType("w"), required=True
- )
- parser.add_argument(
- "languageCodes",
- metavar="languageCode",
- nargs="+",
- help="Language(s) to generate",
- )
- return parser.parse_args()
-
-
-def main() -> None:
- json_dir = HERE
-
- args = parse_args()
- if args.input_pickled and args.output_pickled:
- logging.error("error: Both --output-pickled and --input-pickled are specified")
- sys.exit(1)
-
- language_data: LanguageData
- if args.input_pickled:
- logging.info(f"Reading pickled language data from {args.input_pickled.name}...")
- language_data = pickle.load(args.input_pickled)
- language_codes = [lang["languageCode"] for lang in language_data.langs]
- if language_codes != args.languageCodes:
- logging.error(
- f"error: languageCode {args.languageCode} does not match language data {language_codes}"
- )
- sys.exit(1)
- logging.info(f"Read language data for {language_codes}")
- logging.info(f"Build version: {language_data.build_version}")
- else:
- try:
- build_version = read_version()
- except FileNotFoundError:
- logging.error("error: Could not find version info ")
- sys.exit(1)
-
- logging.info(f"Build version: {build_version}")
- logging.info(f"Making {args.languageCodes} from {json_dir}")
-
- defs_ = load_json(os.path.join(json_dir, "translations_def.js"), True)
- if len(args.languageCodes) == 1:
- lang_ = read_translation(json_dir, args.languageCodes[0])
- language_data = prepare_language(lang_, defs_, build_version)
- else:
- langs_ = [
- read_translation(json_dir, lang_code)
- for lang_code in args.languageCodes
- ]
- language_data = prepare_languages(langs_, defs_, build_version)
-
- out_ = args.output
- write_start(out_)
- if len(language_data.langs) == 1:
- if args.strings_obj:
- sym_name = objcopy.cpp_var_to_section_name("translation")
- strings_bin = objcopy.get_binary_from_obj(args.strings_obj.name, sym_name)
- if len(strings_bin) == 0:
- raise ValueError(f"Output for {sym_name} is empty")
- write_language(
- language_data,
- out_,
- strings_bin=strings_bin,
- compress_font=args.compress_font,
- )
- else:
- write_language(language_data, out_, compress_font=args.compress_font)
- else:
- if args.strings_obj:
- write_languages(
- language_data,
- out_,
- strings_obj_path=args.strings_obj.name,
- compress_font=args.compress_font,
- )
- else:
- write_languages(language_data, out_, compress_font=args.compress_font)
-
- if args.output_pickled:
- logging.info(f"Writing pickled data to {args.output_pickled.name}")
- pickle.dump(language_data, args.output_pickled)
-
- logging.info("Done")
-
-
-if __name__ == "__main__":
- main()
+#!/usr/bin/env python3
+
+import argparse
+import functools
+import json
+import hashlib
+import logging
+import os
+import pickle
+import re
+import subprocess
+import sys
+from datetime import datetime
+from itertools import chain
+from pathlib import Path
+from typing import BinaryIO, Dict, List, Optional, TextIO, Tuple, Union
+from dataclasses import dataclass
+
+from bdflib import reader as bdfreader
+from bdflib.model import Font, Glyph
+
+import font_tables
+import brieflz
+import objcopy
+
+logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
+
+HERE = Path(__file__).resolve().parent
+
+
+@functools.lru_cache(maxsize=None)
+def cjk_font() -> Font:
+ with open(os.path.join(HERE, "wqy-bitmapsong/wenquanyi_9pt.bdf"), "rb") as f:
+ return bdfreader.read_bdf(f)
+
+
+# Loading a single JSON file
+def load_json(filename: str, skip_first_line: bool) -> dict:
+ with open(filename) as f:
+ if skip_first_line:
+ f.readline()
+ return json.loads(f.read())
+
+
+def get_language_unqiue_id(language_ascii_name: str):
+ """
+ Given a language code, it will return a unique (enough) uint16_t id code
+ When we have a collision here we can tweak this, but language list should be fairly stable from now on
+ """
+ return (
+ int(hashlib.sha1(language_ascii_name.encode("utf-8")).hexdigest(), 16) % 0xFFFF
+ )
+
+
+def read_translation(json_root: Union[str, Path], lang_code: str) -> dict:
+ filename = f"translation_{lang_code}.json"
+
+ file_with_path = os.path.join(json_root, filename)
+
+ try:
+ lang = load_json(file_with_path, skip_first_line=False)
+ except json.decoder.JSONDecodeError as e:
+ logging.error(f"Failed to decode {filename}")
+ logging.exception(str(e))
+ sys.exit(2)
+
+ validate_langcode_matches_content(filename, lang)
+
+ return lang
+
+
+def validate_langcode_matches_content(filename: str, content: dict) -> None:
+ # Extract lang code from file name
+ lang_code = filename[12:-5].upper()
+ # ...and the one specified in the JSON file...
+ try:
+ lang_code_from_json = content["languageCode"]
+ except KeyError:
+ lang_code_from_json = "(missing)"
+
+ # ...cause they should be the same!
+ if lang_code != lang_code_from_json:
+ raise ValueError(
+ f"Invalid languageCode {lang_code_from_json} in file {filename}"
+ )
+
+
+def write_start(f: TextIO):
+ f.write(
+ "// WARNING: THIS FILE WAS AUTO GENERATED BY make_translation.py. PLEASE DO NOT EDIT.\n"
+ )
+ f.write("\n")
+ f.write('#include "Translation.h"\n')
+
+
+def get_constants(build_version: str) -> List[Tuple[str, str]]:
+ # Extra constants that are used in the firmware that are shared across all languages
+ return [
+ ("SymbolPlus", "+"),
+ ("SymbolMinus", "-"),
+ ("SymbolSpace", " "),
+ ("SymbolDot", "."),
+ ("SymbolDegC", "C"),
+ ("SymbolDegF", "F"),
+ ("SymbolMinutes", "M"),
+ ("SymbolSeconds", "S"),
+ ("SymbolWatts", "W"),
+ ("SymbolVolts", "V"),
+ ("SymbolDC", "DC"),
+ ("SymbolCellCount", "S"),
+ ("SymbolVersionNumber", build_version),
+ ]
+
+
+def get_debug_menu() -> List[str]:
+ return [
+ datetime.today().strftime("%d-%m-%y"),
+ "HW G ",
+ "HW M ",
+ "HW P ",
+ "Time ",
+ "Move ",
+ "RTip ",
+ "CTip ",
+ "CHan ",
+ "Vin ",
+ "ACC ",
+ "PWR ",
+ "Max ",
+ ]
+
+
+def get_accel_names_list() -> List[str]:
+ return [
+ "Scanning",
+ "None",
+ "MMA8652FC",
+ "LIS2DH12",
+ "BMA223",
+ "MSA301",
+ "SC7A20",
+ ]
+
+
+def get_power_source_list() -> List[str]:
+ return [
+ "DC",
+ "QC",
+ "PD",
+ ]
+
+
+def get_letter_counts(
+ defs: dict, lang: dict, build_version: str
+) -> Tuple[List[str], Dict[str, int]]:
+ text_list = []
+ # iterate over all strings
+ obj = lang["menuOptions"]
+ for mod in defs["menuOptions"]:
+ eid = mod["id"]
+ text_list.append(obj[eid]["desc"])
+
+ obj = lang["messages"]
+ for mod in defs["messages"]:
+ eid = mod["id"]
+ if eid not in obj:
+ text_list.append(mod["default"])
+ else:
+ text_list.append(obj[eid])
+
+ obj = lang["messagesWarn"]
+ for mod in defs["messagesWarn"]:
+ eid = mod["id"]
+ if isinstance(obj[eid], list):
+ text_list.append(obj[eid][0])
+ text_list.append(obj[eid][1])
+ else:
+ text_list.append(obj[eid])
+
+ obj = lang["characters"]
+
+ for mod in defs["characters"]:
+ eid = mod["id"]
+ text_list.append(obj[eid])
+
+ obj = lang["menuOptions"]
+ for mod in defs["menuOptions"]:
+ eid = mod["id"]
+ if isinstance(obj[eid]["text2"], list):
+ text_list.append(obj[eid]["text2"][0])
+ text_list.append(obj[eid]["text2"][1])
+ else:
+ text_list.append(obj[eid]["text2"])
+
+ obj = lang["menuGroups"]
+ for mod in defs["menuGroups"]:
+ eid = mod["id"]
+ if isinstance(obj[eid]["text2"], list):
+ text_list.append(obj[eid]["text2"][0])
+ text_list.append(obj[eid]["text2"][1])
+ else:
+ text_list.append(obj[eid]["text2"])
+
+ obj = lang["menuGroups"]
+ for mod in defs["menuGroups"]:
+ eid = mod["id"]
+ text_list.append(obj[eid]["desc"])
+ constants = get_constants(build_version)
+ for x in constants:
+ text_list.append(x[1])
+ text_list.extend(get_debug_menu())
+ text_list.extend(get_accel_names_list())
+ text_list.extend(get_power_source_list())
+
+ # collapse all strings down into the composite letters and store totals for these
+
+ symbol_counts: dict[str, int] = {}
+ for line in text_list:
+ line = line.replace("\n", "").replace("\r", "")
+ line = line.replace("\\n", "").replace("\\r", "")
+ if line:
+ for letter in line:
+ symbol_counts[letter] = symbol_counts.get(letter, 0) + 1
+ # swap to Big -> little sort order
+ symbols_by_occurrence = [
+ x[0]
+ for x in sorted(
+ symbol_counts.items(), key=lambda kv: (kv[1], kv[0]), reverse=True
+ )
+ ]
+ return symbols_by_occurrence, symbol_counts
+
+
+def get_cjk_glyph(sym: str) -> bytes:
+ glyph: Glyph = cjk_font()[ord(sym)]
+
+ data = glyph.data
+ src_left, src_bottom, src_w, src_h = glyph.get_bounding_box()
+ dst_w = 12
+ dst_h = 16
+
+ # The source data is a per-row list of ints. The first item is the bottom-
+ # most row. For each row, the LSB is the right-most pixel.
+ # Here, (x, y) is the coordinates with origin at the top-left.
+ def get_cell(x: int, y: int) -> bool:
+ # Adjust x coordinates by actual bounding box.
+ adj_x = x - src_left
+ if adj_x < 0 or adj_x >= src_w:
+ return False
+ # Adjust y coordinates by actual bounding box, then place the glyph
+ # baseline 3px above the bottom edge to make it centre-ish.
+ # This metric is optimized for WenQuanYi Bitmap Song 9pt and assumes
+ # each glyph is to be placed in a 12x12px box.
+ adj_y = y - (dst_h - src_h - src_bottom - 3)
+ if adj_y < 0 or adj_y >= src_h:
+ return False
+ if data[src_h - adj_y - 1] & (1 << (src_w - adj_x - 1)):
+ return True
+ else:
+ return False
+
+ # A glyph in the font table is divided into upper and lower parts, each by
+ # 8px high. Each byte represents half if a column, with the LSB being the
+ # top-most pixel. The data goes from the left-most to the right-most column
+ # of the top half, then from the left-most to the right-most column of the
+ # bottom half.
+ bs = bytearray()
+ for block in range(2):
+ for c in range(dst_w):
+ b = 0
+ for r in range(8):
+ if get_cell(c, r + 8 * block):
+ b |= 0x01 << r
+ bs.append(b)
+ return bytes(bs)
+
+
+def get_bytes_from_font_index(index: int) -> bytes:
+ """
+ Converts the font table index into its corresponding bytes
+ """
+
+ # We want to be able to use more than 254 symbols (excluding \x00 null
+ # terminator and \x01 new-line) in the font table but without making all
+ # the chars take 2 bytes. To do this, we use \xF1 to \xFF as lead bytes
+ # to designate double-byte chars, and leave the remaining as single-byte
+ # chars.
+ #
+ # For the sake of sanity, \x00 always means the end of string, so we skip
+ # \xF1\x00 and others in the mapping.
+ #
+ # Mapping example:
+ #
+ # 0x02 => 2
+ # 0x03 => 3
+ # ...
+ # 0xEF => 239
+ # 0xF0 => 240
+ # 0xF1 0x01 => 1 * 0xFF - 15 + 1 = 241
+ # 0xF1 0x02 => 1 * 0xFF - 15 + 2 = 242
+ # ...
+ # 0xF1 0xFF => 1 * 0xFF - 15 + 255 = 495
+ # 0xF2 0x01 => 2 * 0xFF - 15 + 1 = 496
+ # ...
+ # 0xF2 0xFF => 2 * 0xFF - 15 + 255 = 750
+ # 0xF3 0x01 => 3 * 0xFF - 15 + 1 = 751
+ # ...
+ # 0xFF 0xFF => 15 * 0xFF - 15 + 255 = 4065
+
+ if index < 0:
+ raise ValueError("index must be positive")
+ page = (index + 0x0E) // 0xFF
+ if page > 0x0F:
+ raise ValueError("page value out of range")
+ if page == 0:
+ return bytes([index])
+ else:
+ # Into extended range
+ # Leader is 0xFz where z is the page number
+ # Following char is the remainder
+ leader = page + 0xF0
+ value = ((index + 0x0E) % 0xFF) + 0x01
+
+ if leader > 0xFF or value > 0xFF:
+ raise ValueError("value is out of range")
+ return bytes([leader, value])
+
+
+def bytes_to_escaped(b: bytes) -> str:
+ return "".join((f"\\x{i:02X}" for i in b))
+
+
+def bytes_to_c_hex(b: bytes) -> str:
+ return ", ".join((f"0x{i:02X}" for i in b)) + ","
+
+
+@dataclass
+class FontMap:
+ font12: Dict[str, bytes]
+ font06: Dict[str, Optional[bytes]]
+
+
+@dataclass
+class FontMapsPerFont:
+ font12_maps: Dict[str, Dict[str, bytes]]
+ font06_maps: Dict[str, Dict[str, Optional[bytes]]]
+ sym_lists: Dict[str, List[str]]
+
+
+def get_font_map_per_font(text_list: List[str], fonts: List[str]) -> FontMapsPerFont:
+ pending_sym_set = set(text_list)
+ if len(pending_sym_set) != len(text_list):
+ raise ValueError("`text_list` contains duplicated symbols")
+
+ if fonts[0] != font_tables.NAME_ASCII_BASIC:
+ raise ValueError(
+ f'First item in `fonts` must be "{font_tables.NAME_ASCII_BASIC}"'
+ )
+
+ total_symbol_count = len(text_list)
+ # \x00 is for NULL termination and \x01 is for newline, so the maximum
+ # number of symbols allowed is as follow (see also the comments in
+ # `get_bytes_from_font_index`):
+ if total_symbol_count > (0x10 * 0xFF - 15) - 2: # 4063
+ raise ValueError(
+ f"Error, too many used symbols for this version (total {total_symbol_count})"
+ )
+
+ logging.info(f"Generating fonts for {total_symbol_count} symbols")
+
+ # Collect font bitmaps by the defined font order:
+ font12_maps: Dict[str, Dict[str, bytes]] = {}
+ font06_maps: Dict[str, Dict[str, Optional[bytes]]] = {}
+ sym_lists: Dict[str, List[str]] = {}
+ for font in fonts:
+ font12_maps[font] = {}
+ font12_map = font12_maps[font]
+ font06_maps[font] = {}
+ font06_map = font06_maps[font]
+ sym_lists[font] = []
+ sym_list = sym_lists[font]
+
+ if len(pending_sym_set) == 0:
+ logging.warning(
+ f"Font {font} not used because all symbols already have font bitmaps"
+ )
+ continue
+
+ if font == font_tables.NAME_CJK:
+ is_cjk = True
+ else:
+ is_cjk = False
+ font12: Dict[str, bytes]
+ font06: Dict[str, bytes]
+ font12, font06 = font_tables.get_font_maps_for_name(font)
+
+ for sym in text_list:
+ if sym not in pending_sym_set:
+ continue
+ if is_cjk:
+ font12_line = get_cjk_glyph(sym)
+ if font12_line is None:
+ continue
+ font06_line = None
+ else:
+ try:
+ font12_line = font12[sym]
+ font06_line = font06[sym]
+ except KeyError:
+ continue
+ font12_map[sym] = font12_line
+ font06_map[sym] = font06_line
+ sym_list.append(sym)
+ pending_sym_set.remove(sym)
+
+ if len(sym_list) == 0:
+ logging.warning(f"Font {font} not used by any symbols on the list")
+ if len(pending_sym_set) > 0:
+ raise KeyError(f"Symbols not found in specified fonts: {pending_sym_set}")
+
+ return FontMapsPerFont(font12_maps, font06_maps, sym_lists)
+
+
+def get_forced_first_symbols() -> List[str]:
+ forced_first_symbols = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
+ return forced_first_symbols
+
+
+def get_sym_list_and_font_map(
+ text_list: List[str], fonts: List[str]
+) -> Tuple[List[str], Dict[str, List[str]], FontMap]:
+ font_maps = get_font_map_per_font(text_list, fonts)
+ font12_maps = font_maps.font12_maps
+ font06_maps = font_maps.font06_maps
+
+ # Build the full font maps
+ font12_map = {}
+ font06_map = {}
+ for font in fonts:
+ font12_map.update(font12_maps[font])
+ font06_map.update(font06_maps[font])
+
+ # Collect all symbols by the original symbol order, but also making sure
+ # all symbols with only large font must be placed after all symbols with
+ # both small and large fonts
+ sym_list_both_fonts = []
+ sym_list_large_only = []
+ for sym in text_list:
+ if font06_map[sym] is None:
+ sym_list_large_only.append(sym)
+ else:
+ sym_list_both_fonts.append(sym)
+ sym_list = sym_list_both_fonts + sym_list_large_only
+
+ return sym_list, font_maps.sym_lists, FontMap(font12_map, font06_map)
+
+
+def build_symbol_conversion_map(sym_list: List[str]) -> Dict[str, bytes]:
+ forced_first_symbols = get_forced_first_symbols()
+ if sym_list[: len(forced_first_symbols)] != forced_first_symbols:
+ raise ValueError("Symbol list does not start with forced_first_symbols.")
+
+ # the text list is sorted
+ # allocate out these in their order as number codes
+ symbol_map: Dict[str, bytes] = {"\n": bytes([1])}
+ index = 2 # start at 2, as 0= null terminator,1 = new line
+
+ # Assign symbol bytes by font index
+ for index, sym in enumerate(sym_list, index):
+ assert sym not in symbol_map
+ symbol_map[sym] = get_bytes_from_font_index(index)
+
+ return symbol_map
+
+
+def make_font_table_cpp(
+ sym_list: List[str], font_map: FontMap, symbol_map: Dict[str, bytes]
+) -> str:
+ output_table = make_font_table_named_cpp(
+ "USER_FONT_12", sym_list, font_map.font12, symbol_map
+ )
+ output_table += make_font_table_06_cpp(sym_list, font_map, symbol_map)
+ return output_table
+
+
+def make_font_table_named_cpp(
+ name: Optional[str],
+ sym_list: List[str],
+ font_map: Dict[str, bytes],
+ symbol_map: Dict[str, bytes],
+) -> str:
+ output_table = ""
+ if name:
+ output_table = f"const uint8_t {name}[] = {{\n"
+ for sym in sym_list:
+ output_table += f"{bytes_to_c_hex(font_map[sym])}//{bytes_to_escaped(symbol_map[sym])} -> {sym}\n"
+ if name:
+ output_table += f"}}; // {name}\n"
+ return output_table
+
+
+def make_font_table_06_cpp(
+ sym_list: List[str], font_map: FontMap, symbol_map: Dict[str, bytes]
+) -> str:
+ output_table = "const uint8_t USER_FONT_6x8[] = {\n"
+ for sym in sym_list:
+ font_bytes = font_map.font06[sym]
+ if font_bytes:
+ font_line = bytes_to_c_hex(font_bytes)
+ else:
+ font_line = "// " # placeholder
+ output_table += f"{font_line}//{bytes_to_escaped(symbol_map[sym])} -> {sym}\n"
+ output_table += "};\n"
+ return output_table
+
+
+def convert_string_bytes(symbol_conversion_table: Dict[str, bytes], text: str) -> bytes:
+ # convert all of the symbols from the string into bytes for their content
+ output_string = b""
+ for c in text.replace("\\r", "").replace("\\n", "\n"):
+ if c not in symbol_conversion_table:
+ logging.error(f"Missing font definition for {c}")
+ sys.exit(1)
+ else:
+ output_string += symbol_conversion_table[c]
+ return output_string
+
+
+def convert_string(symbol_conversion_table: Dict[str, bytes], text: str) -> str:
+ # convert all of the symbols from the string into escapes for their content
+ return bytes_to_escaped(convert_string_bytes(symbol_conversion_table, text))
+
+
+def escape(string: str) -> str:
+ return json.dumps(string, ensure_ascii=False)
+
+
+def write_bytes_as_c_array(
+ f: TextIO, name: str, data: bytes, indent: int = 2, bytes_per_line: int = 16
+) -> None:
+ f.write(f"const uint8_t {name}[] = {{\n")
+ for i in range(0, len(data), bytes_per_line):
+ f.write(" " * indent)
+ f.write(", ".join((f"0x{b:02X}" for b in data[i : i + bytes_per_line])))
+ f.write(",\n")
+ f.write(f"}}; // {name}\n\n")
+
+
+@dataclass
+class LanguageData:
+ langs: List[dict]
+ defs: dict
+ build_version: str
+ sym_list: List[str]
+ sym_lists_by_font: Dict[str, List[str]]
+ font_map: FontMap
+
+
+def prepare_language(lang: dict, defs: dict, build_version: str) -> LanguageData:
+ language_code: str = lang["languageCode"]
+ logging.info(f"Preparing language data for {language_code}")
+ # Iterate over all of the text to build up the symbols & counts
+ text_list, _ = get_letter_counts(defs, lang, build_version)
+ # From the letter counts, need to make a symbol translator & write out the font
+ fonts = lang["fonts"]
+
+ forced_first_symbols = get_forced_first_symbols()
+
+ # We enforce that numbers come first.
+ text_list = forced_first_symbols + [
+ x for x in text_list if x not in forced_first_symbols
+ ]
+
+ sym_list, sym_lists_by_font, font_map = get_sym_list_and_font_map(text_list, fonts)
+ return LanguageData(
+ [lang], defs, build_version, sym_list, sym_lists_by_font, font_map
+ )
+
+
+def prepare_languages(
+ langs: List[dict], defs: dict, build_version: str
+) -> LanguageData:
+ language_codes: List[str] = [lang["languageCode"] for lang in langs]
+ logging.info(f"Preparing language data for {language_codes}")
+
+ forced_first_symbols = get_forced_first_symbols()
+
+ all_fonts = [
+ font_tables.NAME_ASCII_BASIC,
+ font_tables.NAME_LATIN_EXTENDED,
+ font_tables.NAME_CYRILLIC,
+ font_tables.NAME_CJK,
+ ]
+
+ # Build the full font maps
+ font12_map = {}
+ font06_map = {}
+ # Calculate total symbol counts per font:
+ total_sym_counts: Dict[str, Dict[str, int]] = {}
+ for lang in langs:
+ text_list, sym_counts = get_letter_counts(defs, lang, build_version)
+ fonts = lang["fonts"]
+ text_list = forced_first_symbols + [
+ x for x in text_list if x not in forced_first_symbols
+ ]
+ font_maps = get_font_map_per_font(text_list, fonts)
+ for font in fonts:
+ font12_map.update(font_maps.font12_maps[font])
+ font06_map.update(font_maps.font06_maps[font])
+ for font, font_sym_list in font_maps.sym_lists.items():
+ font_total_sym_counts = total_sym_counts.get(font, {})
+ for sym in font_sym_list:
+ font_total_sym_counts[sym] = font_total_sym_counts.get(
+ sym, 0
+ ) + sym_counts.get(sym, 0)
+ total_sym_counts[font] = font_total_sym_counts
+
+ sym_lists_by_font: Dict[str, List[str]] = {}
+ combined_sym_list = []
+ for font in all_fonts:
+ if font not in total_sym_counts:
+ continue
+ # swap to Big -> little sort order
+ current_sym_list = [
+ x[0]
+ for x in sorted(
+ total_sym_counts[font].items(),
+ key=lambda kv: (kv[1], kv[0]),
+ reverse=True,
+ )
+ ]
+ if font == font_tables.NAME_ASCII_BASIC:
+ # We enforce that numbers come first.
+ current_sym_list = forced_first_symbols + [
+ x for x in current_sym_list if x not in forced_first_symbols
+ ]
+ sym_lists_by_font[font] = current_sym_list
+ combined_sym_list.extend(current_sym_list)
+
+ return LanguageData(
+ langs,
+ defs,
+ build_version,
+ combined_sym_list,
+ sym_lists_by_font,
+ FontMap(font12_map, font06_map),
+ )
+
+
+def write_language(
+ data: LanguageData,
+ f: TextIO,
+ strings_bin: Optional[bytes] = None,
+ compress_font: bool = False,
+) -> None:
+ if len(data.langs) > 1:
+ raise ValueError("More than 1 languages are provided")
+ lang = data.langs[0]
+ defs = data.defs
+ build_version = data.build_version
+ sym_list = data.sym_list
+ font_map = data.font_map
+
+ symbol_conversion_table = build_symbol_conversion_map(sym_list)
+
+ language_code: str = lang["languageCode"]
+ logging.info(f"Generating block for {language_code}")
+
+ try:
+ lang_name = lang["languageLocalName"]
+ except KeyError:
+ lang_name = language_code
+
+ if strings_bin or compress_font:
+ f.write('#include "brieflz.h"\n')
+
+ f.write(f"\n// ---- {lang_name} ----\n\n")
+
+ if not compress_font:
+ font_table_text = make_font_table_cpp(
+ sym_list, font_map, symbol_conversion_table
+ )
+ f.write(font_table_text)
+ f.write(
+ "const FontSection FontSectionsData[] = {\n"
+ " {\n"
+ " .symbol_start = 2,\n"
+ f" .symbol_end = {len(sym_list) + 2},\n"
+ " .font12_start_ptr = USER_FONT_12,\n"
+ " .font06_start_ptr = USER_FONT_6x8,\n"
+ " },\n"
+ "};\n"
+ "const FontSection *const FontSections = FontSectionsData;\n"
+ "const uint8_t FontSectionsCount = sizeof(FontSectionsData) / sizeof(FontSectionsData[0]);\n"
+ )
+ else:
+ font12_uncompressed = bytearray()
+ for sym in sym_list:
+ font12_uncompressed.extend(font_map.font12[sym])
+ font12_compressed = brieflz.compress(bytes(font12_uncompressed))
+ logging.info(
+ f"Font table 12x16 compressed from {len(font12_uncompressed)} to {len(font12_compressed)} bytes (ratio {len(font12_compressed) / len(font12_uncompressed):.3})"
+ )
+ write_bytes_as_c_array(f, "font_12x16_brieflz", font12_compressed)
+ font_table_text = make_font_table_06_cpp(
+ sym_list, font_map, symbol_conversion_table
+ )
+ f.write(font_table_text)
+ f.write(
+ f"static uint8_t font_out_buffer[{len(font12_uncompressed)}];\n"
+ "const FontSection FontSectionsData[] = {\n"
+ " {\n"
+ " .symbol_start = 2,\n"
+ f" .symbol_end = {len(sym_list) + 2},\n"
+ " .font12_start_ptr = font_out_buffer,\n"
+ " .font06_start_ptr = USER_FONT_6x8,\n"
+ " },\n"
+ "};\n"
+ "const FontSection *const FontSections = FontSectionsData;\n"
+ "const uint8_t FontSectionsCount = sizeof(FontSectionsData) / sizeof(FontSectionsData[0]);\n"
+ )
+
+ f.write(f"\n// ---- {lang_name} ----\n\n")
+
+ translation_common_text = get_translation_common_text(
+ defs, symbol_conversion_table, build_version
+ )
+ f.write(translation_common_text)
+ f.write(
+ f"const bool HasFahrenheit = {('true' if lang.get('tempUnitFahrenheit', True) else 'false')};\n\n"
+ )
+
+ if not strings_bin:
+ translation_strings_and_indices_text = get_translation_strings_and_indices_text(
+ lang, defs, symbol_conversion_table
+ )
+ f.write(translation_strings_and_indices_text)
+ f.write(
+ "const TranslationIndexTable *Tr = &translation.indices;\n"
+ "const char *TranslationStrings = translation.strings;\n\n"
+ )
+ else:
+ compressed = brieflz.compress(strings_bin)
+ logging.info(
+ f"Strings compressed from {len(strings_bin)} to {len(compressed)} bytes (ratio {len(compressed) / len(strings_bin):.3})"
+ )
+ write_bytes_as_c_array(f, "translation_data_brieflz", compressed)
+ f.write(
+ f"static uint8_t translation_data_out_buffer[{len(strings_bin)}] __attribute__((__aligned__(2)));\n\n"
+ "const TranslationIndexTable *Tr = reinterpret_cast(translation_data_out_buffer);\n"
+ "const char *TranslationStrings = reinterpret_cast(translation_data_out_buffer) + sizeof(TranslationIndexTable);\n\n"
+ )
+
+ if not strings_bin and not compress_font:
+ f.write("void prepareTranslations() {}\n\n")
+ else:
+ f.write("void prepareTranslations() {\n")
+ if compress_font:
+ f.write(
+ " blz_depack_srcsize(font_12x16_brieflz, font_out_buffer, sizeof(font_12x16_brieflz));\n"
+ )
+ if strings_bin:
+ f.write(
+ " blz_depack_srcsize(translation_data_brieflz, translation_data_out_buffer, sizeof(translation_data_brieflz));\n"
+ )
+ f.write("}\n\n")
+
+ sanity_checks_text = get_translation_sanity_checks_text(defs)
+ f.write(sanity_checks_text)
+
+
+def write_languages(
+ data: LanguageData,
+ f: TextIO,
+ strings_obj_path: Optional[str] = None,
+ compress_font: bool = False,
+) -> None:
+ defs = data.defs
+ build_version = data.build_version
+ combined_sym_list = data.sym_list
+ sym_lists_by_font = data.sym_lists_by_font
+ font_map = data.font_map
+
+ symbol_conversion_table = build_symbol_conversion_map(combined_sym_list)
+
+ language_codes: List[str] = [lang["languageCode"] for lang in data.langs]
+ logging.info(f"Generating block for {language_codes}")
+
+ lang_names = [
+ lang.get("languageLocalName", lang["languageCode"]) for lang in data.langs
+ ]
+
+ f.write('#include "Translation_multi.h"')
+
+ f.write(f"\n// ---- {lang_names} ----\n\n")
+
+ max_decompressed_font_size = 0
+ if not compress_font:
+ font_table_text = ""
+ font_section_info_text = (
+ "const FontSectionDataInfo FontSectionDataInfos[] = {\n"
+ )
+ for font, current_sym_list in sym_lists_by_font.items():
+ font_table_text += f"const uint8_t font_table_data_{font}[] = {{\n"
+ font_table_text += "// 12x16:\n"
+ font_table_text += make_font_table_named_cpp(
+ None,
+ current_sym_list,
+ font_map.font12,
+ symbol_conversion_table,
+ )
+ if font != font_tables.NAME_CJK:
+ font_table_text += "// 6x8:\n"
+ font_table_text += make_font_table_named_cpp(
+ None,
+ current_sym_list,
+ font_map.font06, # type: ignore[arg-type]
+ symbol_conversion_table,
+ )
+ font_table_text += f"}}; // font_table_data_{font}\n"
+ current_sym_start = combined_sym_list.index(current_sym_list[0]) + 2
+ font_section_info_text += (
+ " {\n"
+ f" .symbol_start = {current_sym_start},\n"
+ f" .symbol_count = {len(current_sym_list)},\n"
+ f" .data_size = sizeof(font_table_data_{font}),\n"
+ " .data_is_compressed = false,\n"
+ f" .data_ptr = font_table_data_{font},\n"
+ " },\n"
+ )
+
+ f.write(font_table_text)
+ font_section_info_text += (
+ "};\n"
+ "const uint8_t FontSectionDataCount = sizeof(FontSectionDataInfos) / sizeof(FontSectionDataInfos[0]);\n\n"
+ )
+ f.write(font_section_info_text)
+ f.write(
+ "FontSection DynamicFontSections[4] = {};\n"
+ "const FontSection *const FontSections = DynamicFontSections;\n"
+ "const uint8_t FontSectionsCount = sizeof(DynamicFontSections) / sizeof(DynamicFontSections[0]);\n"
+ )
+ else:
+ font_section_info_text = (
+ "const FontSectionDataInfo FontSectionDataInfos[] = {\n"
+ )
+ for font, current_sym_list in sym_lists_by_font.items():
+ current_sym_start = combined_sym_list.index(current_sym_list[0]) + 2
+ font_uncompressed = bytearray()
+ for sym in current_sym_list:
+ font_uncompressed.extend(font_map.font12[sym])
+ if font != font_tables.NAME_CJK:
+ for sym in current_sym_list:
+ font_uncompressed.extend(font_map.font06[sym]) # type: ignore[arg-type]
+ font_compressed = brieflz.compress(bytes(font_uncompressed))
+ logging.info(
+ f"Font table for {font} compressed from {len(font_uncompressed)} to {len(font_compressed)} bytes (ratio {len(font_compressed) / len(font_uncompressed):.3})"
+ )
+ max_decompressed_font_size += len(font_uncompressed)
+ write_bytes_as_c_array(f, f"font_data_brieflz_{font}", font_compressed)
+ font_section_info_text += (
+ " {\n"
+ f" .symbol_start = {current_sym_start},\n"
+ f" .symbol_count = {len(current_sym_list)},\n"
+ f" .data_size = sizeof(font_data_brieflz_{font}),\n"
+ " .data_is_compressed = true,\n"
+ f" .data_ptr = font_data_brieflz_{font},\n"
+ " },\n"
+ )
+ font_section_info_text += (
+ "};\n"
+ "const uint8_t FontSectionDataCount = sizeof(FontSectionDataInfos) / sizeof(FontSectionDataInfos[0]);\n\n"
+ )
+ f.write(font_section_info_text)
+ f.write(
+ "FontSection DynamicFontSections[4] = {};\n"
+ "const FontSection *const FontSections = DynamicFontSections;\n"
+ "const uint8_t FontSectionsCount = sizeof(DynamicFontSections) / sizeof(DynamicFontSections[0]);\n"
+ )
+
+ f.write(f"\n// ---- {lang_names} ----\n\n")
+
+ translation_common_text = get_translation_common_text(
+ defs, symbol_conversion_table, build_version
+ )
+ f.write(translation_common_text)
+ f.write(
+ f"const bool HasFahrenheit = {('true' if any([lang.get('tempUnitFahrenheit', True) for lang in data.langs]) else 'false')};\n\n"
+ )
+
+ max_decompressed_translation_size = 0
+ if not strings_obj_path:
+ for lang in data.langs:
+ lang_code = lang["languageCode"]
+ translation_strings_and_indices_text = (
+ get_translation_strings_and_indices_text(
+ lang, defs, symbol_conversion_table, suffix=f"_{lang_code}"
+ )
+ )
+ f.write(translation_strings_and_indices_text)
+ f.write("const LanguageMeta LanguageMetas[] = {\n")
+ for lang in data.langs:
+ lang_code = lang["languageCode"]
+ lang_id = get_language_unqiue_id(lang_code)
+ f.write(
+ " {\n"
+ f" .uniqueID = {lang_id},\n"
+ f" .translation_data = reinterpret_cast(&translation_{lang_code}),\n"
+ f" .translation_size = sizeof(translation_{lang_code}),\n"
+ f" .translation_is_compressed = false,\n"
+ " },\n"
+ )
+ f.write("};\n")
+ else:
+ for lang in data.langs:
+ lang_code = lang["languageCode"]
+ sym_name = objcopy.cpp_var_to_section_name(f"translation_{lang_code}")
+ strings_bin = objcopy.get_binary_from_obj(strings_obj_path, sym_name)
+ if len(strings_bin) == 0:
+ raise ValueError(f"Output for {sym_name} is empty")
+ max_decompressed_translation_size = max(
+ max_decompressed_translation_size, len(strings_bin)
+ )
+ compressed = brieflz.compress(strings_bin)
+ logging.info(
+ f"Strings for {lang_code} compressed from {len(strings_bin)} to {len(compressed)} bytes (ratio {len(compressed) / len(strings_bin):.3})"
+ )
+ write_bytes_as_c_array(
+ f, f"translation_data_brieflz_{lang_code}", compressed
+ )
+ f.write("const LanguageMeta LanguageMetas[] = {\n")
+ for lang in data.langs:
+ lang_code = lang["languageCode"]
+ lang_id = get_language_unqiue_id(lang_code)
+ f.write(
+ " {\n"
+ f" .uniqueID = {lang_id},\n"
+ f" .translation_data = translation_data_brieflz_{lang_code},\n"
+ f" .translation_size = sizeof(translation_data_brieflz_{lang_code}),\n"
+ f" .translation_is_compressed = true,\n"
+ " },\n"
+ )
+ f.write("};\n")
+ f.write(
+ "const uint8_t LanguageCount = sizeof(LanguageMetas) / sizeof(LanguageMetas[0]);\n\n"
+ f"alignas(TranslationData) uint8_t translation_data_out_buffer[{max_decompressed_translation_size + max_decompressed_font_size}];\n"
+ "const uint16_t translation_data_out_buffer_size = sizeof(translation_data_out_buffer);\n\n"
+ )
+
+ sanity_checks_text = get_translation_sanity_checks_text(defs)
+ f.write(sanity_checks_text)
+
+
+def get_translation_common_text(
+ defs: dict, symbol_conversion_table: Dict[str, bytes], build_version
+) -> str:
+ translation_common_text = ""
+
+ # Write out firmware constant options
+ constants = get_constants(build_version)
+ for x in constants:
+ translation_common_text += f'const char* {x[0]} = "{convert_string(symbol_conversion_table, x[1])}";//{x[1]} \n'
+ translation_common_text += "\n"
+
+ # Debug Menu
+ translation_common_text += "const char* DebugMenu[] = {\n"
+
+ for c in get_debug_menu():
+ translation_common_text += (
+ f'\t "{convert_string(symbol_conversion_table, c)}",//{c} \n'
+ )
+ translation_common_text += "};\n\n"
+
+ # accel names
+ translation_common_text += "const char* AccelTypeNames[] = {\n"
+
+ for c in get_accel_names_list():
+ translation_common_text += (
+ f'\t "{convert_string(symbol_conversion_table, c)}",//{c} \n'
+ )
+ translation_common_text += "};\n\n"
+
+ # power source types
+ translation_common_text += "const char* PowerSourceNames[] = {\n"
+
+ for c in get_power_source_list():
+ translation_common_text += (
+ f'\t "{convert_string(symbol_conversion_table, c)}",//{c} \n'
+ )
+ translation_common_text += "};\n\n"
+
+ return translation_common_text
+
+
+@dataclass
+class TranslationItem:
+ info: str
+ str_index: int
+
+
+def get_translation_strings_and_indices_text(
+ lang: dict, defs: dict, symbol_conversion_table: Dict[str, bytes], suffix: str = ""
+) -> str:
+ str_table: List[str] = []
+ str_group_messages: List[TranslationItem] = []
+ str_group_messageswarn: List[TranslationItem] = []
+ str_group_characters: List[TranslationItem] = []
+ str_group_settingdesc: List[TranslationItem] = []
+ str_group_settingshortnames: List[TranslationItem] = []
+ str_group_settingmenuentries: List[TranslationItem] = []
+ str_group_settingmenuentriesdesc: List[TranslationItem] = []
+
+ eid: str
+
+ # ----- Reading SettingsDescriptions
+ obj = lang["menuOptions"]
+
+ for index, mod in enumerate(defs["menuOptions"]):
+ eid = mod["id"]
+ str_group_settingdesc.append(
+ TranslationItem(f"[{index:02d}] {eid}", len(str_table))
+ )
+ str_table.append(obj[eid]["desc"])
+
+ # ----- Reading Message strings
+
+ obj = lang["messages"]
+
+ for mod in defs["messages"]:
+ eid = mod["id"]
+ source_text = ""
+ if "default" in mod:
+ source_text = mod["default"]
+ if eid in obj:
+ source_text = obj[eid]
+ str_group_messages.append(TranslationItem(eid, len(str_table)))
+ str_table.append(source_text)
+
+ obj = lang["messagesWarn"]
+
+ for mod in defs["messagesWarn"]:
+ eid = mod["id"]
+ if isinstance(obj[eid], list):
+ if not obj[eid][1]:
+ source_text = obj[eid][0]
+ else:
+ source_text = obj[eid][0] + "\n" + obj[eid][1]
+ else:
+ source_text = "\n" + obj[eid]
+ str_group_messageswarn.append(TranslationItem(eid, len(str_table)))
+ str_table.append(source_text)
+
+ # ----- Reading Characters
+
+ obj = lang["characters"]
+
+ for mod in defs["characters"]:
+ eid = mod["id"]
+ str_group_characters.append(TranslationItem(eid, len(str_table)))
+ str_table.append(obj[eid])
+
+ # ----- Reading SettingsDescriptions
+ obj = lang["menuOptions"]
+
+ for index, mod in enumerate(defs["menuOptions"]):
+ eid = mod["id"]
+ if isinstance(obj[eid]["text2"], list):
+ if not obj[eid]["text2"][1]:
+ source_text = obj[eid]["text2"][0]
+ else:
+ source_text = obj[eid]["text2"][0] + "\n" + obj[eid]["text2"][1]
+ else:
+ source_text = "\n" + obj[eid]["text2"]
+ str_group_settingshortnames.append(
+ TranslationItem(f"[{index:02d}] {eid}", len(str_table))
+ )
+ str_table.append(source_text)
+
+ # ----- Reading Menu Groups
+ obj = lang["menuGroups"]
+
+ for index, mod in enumerate(defs["menuGroups"]):
+ eid = mod["id"]
+ if isinstance(obj[eid]["text2"], list):
+ if not obj[eid]["text2"][1]:
+ source_text = obj[eid]["text2"][0]
+ else:
+ source_text = obj[eid]["text2"][0] + "\n" + obj[eid]["text2"][1]
+ else:
+ source_text = "\n" + obj[eid]["text2"]
+ str_group_settingmenuentries.append(
+ TranslationItem(f"[{index:02d}] {eid}", len(str_table))
+ )
+ str_table.append(source_text)
+
+ # ----- Reading Menu Groups Descriptions
+ obj = lang["menuGroups"]
+
+ for index, mod in enumerate(defs["menuGroups"]):
+ eid = mod["id"]
+ str_group_settingmenuentriesdesc.append(
+ TranslationItem(f"[{index:02d}] {eid}", len(str_table))
+ )
+ str_table.append(obj[eid]["desc"])
+
+ @dataclass
+ class RemappedTranslationItem:
+ str_index: int
+ str_start_offset: int = 0
+
+ # ----- Perform suffix merging optimization:
+ #
+ # We sort the backward strings so that strings with the same suffix will
+ # be next to each other, e.g.:
+ # "ef\0",
+ # "cdef\0",
+ # "abcdef\0",
+ backward_sorted_table: List[Tuple[int, str, bytes]] = sorted(
+ (
+ (i, s, bytes(reversed(convert_string_bytes(symbol_conversion_table, s))))
+ for i, s in enumerate(str_table)
+ ),
+ key=lambda x: x[2],
+ )
+ str_remapping: List[Optional[RemappedTranslationItem]] = [None] * len(str_table)
+ for i, (str_index, source_str, converted) in enumerate(backward_sorted_table[:-1]):
+ j = i
+ while backward_sorted_table[j + 1][2].startswith(converted):
+ j += 1
+ if j + 1 == len(backward_sorted_table):
+ break
+ if j != i:
+ str_remapping[str_index] = RemappedTranslationItem(
+ str_index=backward_sorted_table[j][0],
+ str_start_offset=len(backward_sorted_table[j][2]) - len(converted),
+ )
+
+ # ----- Write the string table:
+ str_offsets = [-1] * len(str_table)
+ offset = 0
+ write_null = False
+ # NOTE: Cannot specify C99 designator here due to GCC (g++) bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=55227
+ translation_strings_text = " /* .strings = */ {\n"
+ for i, source_str in enumerate(str_table):
+ if str_remapping[i] is not None:
+ continue
+ if write_null:
+ translation_strings_text += ' "\\0"\n'
+ write_null = True
+ # Find what items use this string
+ str_used_by = [i] + [
+ j for j, r in enumerate(str_remapping) if r and r.str_index == i
+ ]
+ for j in str_used_by:
+ for group, pre_info in [
+ (str_group_messages, "messages"),
+ (str_group_messageswarn, "messagesWarn"),
+ (str_group_characters, "characters"),
+ (str_group_settingdesc, "SettingsDescriptions"),
+ (str_group_settingshortnames, "SettingsShortNames"),
+ (str_group_settingmenuentries, "SettingsMenuEntries"),
+ (str_group_settingmenuentriesdesc, "SettingsMenuEntriesDescriptions"),
+ ]:
+ for item in group:
+ if item.str_index == j:
+ translation_strings_text += (
+ f" // - {pre_info} {item.info}\n"
+ )
+ if j == i:
+ translation_strings_text += (
+ f" // {offset: >4}: {escape(source_str)}\n"
+ )
+ str_offsets[j] = offset
+ else:
+ remapped = str_remapping[j]
+ assert remapped is not None
+ translation_strings_text += f" // {offset + remapped.str_start_offset: >4}: {escape(str_table[j])}\n"
+ str_offsets[j] = offset + remapped.str_start_offset
+ converted_bytes = convert_string_bytes(symbol_conversion_table, source_str)
+ translation_strings_text += f' "{bytes_to_escaped(converted_bytes)}"'
+ str_offsets[i] = offset
+ # Add the length and the null terminator
+ offset += len(converted_bytes) + 1
+ translation_strings_text += "\n }, // .strings\n\n"
+
+ str_total_bytes = offset
+
+ def get_offset(idx: int) -> int:
+ assert str_offsets[idx] >= 0
+ return str_offsets[idx]
+
+ translation_indices_text = " .indices = {\n"
+
+ # ----- Write the messages string indices:
+ for group in [str_group_messages, str_group_messageswarn, str_group_characters]:
+ for item in group:
+ translation_indices_text += f" .{item.info} = {get_offset(item.str_index)}, // {escape(str_table[item.str_index])}\n"
+ translation_indices_text += "\n"
+
+ # ----- Write the settings index tables:
+ for group, name in [
+ (str_group_settingdesc, "SettingsDescriptions"),
+ (str_group_settingshortnames, "SettingsShortNames"),
+ (str_group_settingmenuentries, "SettingsMenuEntries"),
+ (str_group_settingmenuentriesdesc, "SettingsMenuEntriesDescriptions"),
+ ]:
+ max_len = 30
+ translation_indices_text += f" .{name} = {{\n"
+ for item in group:
+ translation_indices_text += f" /* {item.info.ljust(max_len)[:max_len]} */ {get_offset(item.str_index)}, // {escape(str_table[item.str_index])}\n"
+ translation_indices_text += f" }}, // {name}\n\n"
+
+ translation_indices_text += " }, // .indices\n\n"
+
+ return (
+ "struct {\n"
+ " TranslationIndexTable indices;\n"
+ f" char strings[{str_total_bytes}];\n"
+ f"}} const translation{suffix} = {{\n"
+ + translation_indices_text
+ + translation_strings_text
+ + f"}}; // translation{suffix}\n\n"
+ )
+
+
+def get_translation_sanity_checks_text(defs: dict) -> str:
+ sanity_checks_text = "\n// Verify SettingsItemIndex values:\n"
+ for i, mod in enumerate(defs["menuOptions"]):
+ eid = mod["id"]
+ sanity_checks_text += (
+ f"static_assert(static_cast(SettingsItemIndex::{eid}) == {i});\n"
+ )
+ sanity_checks_text += f"static_assert(static_cast(SettingsItemIndex::NUM_ITEMS) == {len(defs['menuOptions'])});\n"
+ return sanity_checks_text
+
+
+def read_version() -> str:
+ with open(HERE.parent / "source" / "version.h") as version_file:
+ for line in version_file:
+ if re.findall(r"^.*(?<=(#define)).*(?<=(BUILD_VERSION))", line):
+ matches = re.findall(r"\"(.+?)\"", line)
+ if matches:
+ version = matches[0]
+ try:
+ version += f".{subprocess.check_output(['git', 'rev-parse', '--short=7', 'HEAD']).strip().decode('ascii').upper()}"
+ # --short=7: the shorted hash with 7 digits. Increase/decrease if needed!
+ except OSError:
+ version += " git"
+ return version
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--output-pickled",
+ help="Write pickled language data for later reuse",
+ type=argparse.FileType("wb"),
+ required=False,
+ dest="output_pickled",
+ )
+ parser.add_argument(
+ "--input-pickled",
+ help="Use previously generated pickled language data",
+ type=argparse.FileType("rb"),
+ required=False,
+ dest="input_pickled",
+ )
+ parser.add_argument(
+ "--strings-obj",
+ help="Use generated TranslationData by extracting from object file",
+ type=argparse.FileType("rb"),
+ required=False,
+ dest="strings_obj",
+ )
+ parser.add_argument(
+ "--compress-font",
+ help="Compress the font table",
+ action="store_true",
+ required=False,
+ dest="compress_font",
+ )
+ parser.add_argument(
+ "--output", "-o", help="Target file", type=argparse.FileType("w"), required=True
+ )
+ parser.add_argument(
+ "languageCodes",
+ metavar="languageCode",
+ nargs="+",
+ help="Language(s) to generate",
+ )
+ return parser.parse_args()
+
+
+def main() -> None:
+ json_dir = HERE
+
+ args = parse_args()
+ if args.input_pickled and args.output_pickled:
+ logging.error("error: Both --output-pickled and --input-pickled are specified")
+ sys.exit(1)
+
+ language_data: LanguageData
+ if args.input_pickled:
+ logging.info(f"Reading pickled language data from {args.input_pickled.name}...")
+ language_data = pickle.load(args.input_pickled)
+ language_codes = [lang["languageCode"] for lang in language_data.langs]
+ if language_codes != args.languageCodes:
+ logging.error(
+ f"error: languageCode {args.languageCode} does not match language data {language_codes}"
+ )
+ sys.exit(1)
+ logging.info(f"Read language data for {language_codes}")
+ logging.info(f"Build version: {language_data.build_version}")
+ else:
+ try:
+ build_version = read_version()
+ except FileNotFoundError:
+ logging.error("error: Could not find version info ")
+ sys.exit(1)
+
+ logging.info(f"Build version: {build_version}")
+ logging.info(f"Making {args.languageCodes} from {json_dir}")
+
+ defs_ = load_json(os.path.join(json_dir, "translations_def.js"), True)
+ if len(args.languageCodes) == 1:
+ lang_ = read_translation(json_dir, args.languageCodes[0])
+ language_data = prepare_language(lang_, defs_, build_version)
+ else:
+ langs_ = [
+ read_translation(json_dir, lang_code)
+ for lang_code in args.languageCodes
+ ]
+ language_data = prepare_languages(langs_, defs_, build_version)
+
+ out_ = args.output
+ write_start(out_)
+ if len(language_data.langs) == 1:
+ if args.strings_obj:
+ sym_name = objcopy.cpp_var_to_section_name("translation")
+ strings_bin = objcopy.get_binary_from_obj(args.strings_obj.name, sym_name)
+ if len(strings_bin) == 0:
+ raise ValueError(f"Output for {sym_name} is empty")
+ write_language(
+ language_data,
+ out_,
+ strings_bin=strings_bin,
+ compress_font=args.compress_font,
+ )
+ else:
+ write_language(language_data, out_, compress_font=args.compress_font)
+ else:
+ if args.strings_obj:
+ write_languages(
+ language_data,
+ out_,
+ strings_obj_path=args.strings_obj.name,
+ compress_font=args.compress_font,
+ )
+ else:
+ write_languages(language_data, out_, compress_font=args.compress_font)
+
+ if args.output_pickled:
+ logging.info(f"Writing pickled data to {args.output_pickled.name}")
+ pickle.dump(language_data, args.output_pickled)
+
+ logging.info("Done")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/Translations/translation_BE.json b/Translations/translation_BE.json
new file mode 100644
index 00000000..4a848704
--- /dev/null
+++ b/Translations/translation_BE.json
@@ -0,0 +1,213 @@
+{
+ "languageCode": "BE",
+ "languageLocalName": "Беларуская",
+ "fonts": ["ascii_basic", "latin_extended", "cyrillic"],
+ "messages": {
+ "SettingsCalibrationDone": "Каліброўка закончана!",
+ "SettingsCalibrationWarning": "Перад тым, як працягнуць, каліласка, упэўнецеся, што джала мае комнатную тэмпературу!",
+ "SettingsResetWarning": "Вы ўпэннены, што жадаеце зкінуць налады да першапачатковых значэнняў?",
+ "UVLOWarningString": "НАПРУГА--",
+ "UndervoltageString": "Нізкая напруга",
+ "InputVoltageString": "Сілкаванне В: ",
+ "WarningTipTempString": "Тэмп. джала: ",
+ "BadTipString": "ДЖАЛА--",
+ "SleepingSimpleString": "Zzzz",
+ "SleepingAdvancedString": "Чаканне...",
+ "WarningSimpleString": "ГОРАЧА!",
+ "WarningAdvancedString": "!!! ГАРАЧАЕ !!!\n!!! ДЖАЛА !!!",
+ "SleepingTipAdvancedString": "Джала:",
+ "IdleTipString": "Джала:",
+ "IdleSetString": " ->",
+ "TipDisconnectedString": "ДЖАЛА АДЛУЧАНА",
+ "SolderingAdvancedPowerPrompt": "Сілкаванне: ",
+ "OffString": "Выкл.",
+ "YourGainMessage": "Прырост:"
+ },
+ "messagesWarn": {
+ "ResetOKMessage": "Скід OK",
+ "SettingsResetMessage": ["Налады", "зкінуты!"],
+ "NoAccelerometerMessage": ["Ня вызначаны", "акселерометр!"],
+ "NoPowerDeliveryMessage": ["Няма USB-PD IC", "выяўлены!"],
+ "LockingKeysString": "ЗАМКНУТЫ",
+ "UnlockingKeysString": "АДЫМКНУТЫ",
+ "WarningKeysLockedString": "!ЗАМКНУТЫ!",
+ "WarningThermalRunaway": ["Цеплавы", "Уцякач"]
+ },
+ "characters": {
+ "SettingRightChar": "П",
+ "SettingLeftChar": "Л",
+ "SettingAutoChar": "А",
+ "SettingOffChar": "O",
+ "SettingSlowChar": "М",
+ "SettingMediumChar": "С",
+ "SettingFastChar": "Х",
+ "SettingStartNoneChar": "В",
+ "SettingStartSolderingChar": "П",
+ "SettingStartSleepChar": "Ч",
+ "SettingStartSleepOffChar": "К",
+ "SettingSensitivityOff": "А",
+ "SettingSensitivityLow": "Н",
+ "SettingSensitivityMedium": "С",
+ "SettingSensitivityHigh": "В",
+ "SettingLockDisableChar": "А",
+ "SettingLockBoostChar": "Т",
+ "SettingLockFullChar": "П",
+ "SettingNAChar": "Н/Д"
+ },
+ "menuGroups": {
+ "PowerMenu": {
+ "text2": ["Налады", "сілкавання"],
+ "desc": "Крыніца сілкавання і мінімальная напруга на ячэйку"
+ },
+ "SolderingMenu": {
+ "text2": ["Налады", "пайкі"],
+ "desc": "Налады пайкі"
+ },
+ "PowerSavingMenu": {
+ "text2": ["Рэжымы", "сну"],
+ "desc": "Налады рэжыму чакання"
+ },
+ "UIMenu": {
+ "text2": ["Налады", "інтэрфейсу"],
+ "desc": "Карыстальніцкія налады знешняга выгляду"
+ },
+ "AdvancedMenu": {
+ "text2": ["Дадатковыя", "налады"],
+ "desc": "Дадатковыя налады"
+ }
+ },
+ "menuOptions": {
+ "DCInCutoff": {
+ "text2": ["Крыніца", "сілкавання"],
+ "desc": "Крыніца сілкавання. Усталюе напругу адсечкі. (DC 10В) (S 3,3В на ячэйку, без абмежавання магутнасці)"
+ },
+ "SleepTemperature": {
+ "text2": ["Тэмп.", "чакання"],
+ "desc": "Тэмпература рэжыму чакання"
+ },
+ "SleepTimeout": {
+ "text2": ["Таймаўт", "чакання"],
+ "desc": "Час да пераходу ў рэжым чакання (Хвіліны | Секунды)"
+ },
+ "ShutdownTimeout": {
+ "text2": ["Таймаут", "выключэння"],
+ "desc": "Час да адключэння паяльніка (Хвіліны)"
+ },
+ "MotionSensitivity": {
+ "text2": ["Адчувальнасць", "акселерометра"],
+ "desc": "Адчувальнасць акселерометра (0=Выкл. | 1=Мін. | ... | 9=Макс.)"
+ },
+ "TemperatureUnit": {
+ "text2": ["Адзінкі", "тэмпературы"],
+ "desc": "Адзінкі вымярэння тэмпературы (C=Цэльcія | F=Фарэнгейта)"
+ },
+ "AdvancedIdle": {
+ "text2": ["Падрабязны", "рэжым чакання"],
+ "desc": "Адлюстроўваць дэталёвую инфармацыю паменьшаным шрыфтом на экране чакання"
+ },
+ "DisplayRotation": {
+ "text2": ["Арыентацыя", "экрану"],
+ "desc": "Арыентацыя экрану (П=Правая рука | Л=Левая рука | А=Аўта)"
+ },
+ "BoostTemperature": {
+ "text2": ["t° турба", "рэжыму"],
+ "desc": "Тэмпература джала ў турба-рэжыме"
+ },
+ "AutoStart": {
+ "text2": ["Аўта", "старт"],
+ "desc": "Рэжым, у якім запускаецца паяльнік пры падачы сілкавання (В=Выкл. | П=Пайка | Ч=Чаканне | К=Чаканне пры комн. тэмп.)"
+ },
+ "CooldownBlink": {
+ "text2": ["Мігценне t°", "пры астуджэнні"],
+ "desc": "Міргаць тэмпературай на экране астуджэння, пакуль джага яшчэ гарачае"
+ },
+ "TemperatureCalibration": {
+ "text2": ["Каліброўка", "тэмпературы"],
+ "desc": "Каліброўка тэрмадатчыка джала"
+ },
+ "SettingsReset": {
+ "text2": ["Скід", "наладаў"],
+ "desc": "Скід наладаў да першапачатковых значэнняў"
+ },
+ "VoltageCalibration": {
+ "text2": ["Каліброўка", "напругі"],
+ "desc": "Каліброўка ўваходнай напругі (падоўжаны націск для выхаду)"
+ },
+ "AdvancedSoldering": {
+ "text2": ["Падрабязны", "экран пайкі"],
+ "desc": "Паказваць дэталёвую інформацыю на экране пайкі"
+ },
+ "ScrollingSpeed": {
+ "text2": ["Хуткацсь", "тексту"],
+ "desc": "Хуткасць гартання тэксту (М=марудна | Х=хутка)"
+ },
+ "QCMaxVoltage": {
+ "text2": ["Магутнасць", "сілкавання"],
+ "desc": "Магутнасць выкарыстоўваемай крыніцы сілкавання"
+ },
+ "PDNegTimeout": {
+ "text2": ["PD", "прыпынак"],
+ "desc": "Час чакання ўзгаднення PD з крокам 100 мс для сумяшчальнасці з некаторымі зараднымі зараднымі прыладамі QC (0: адключана)"
+ },
+ "PowerLimit": {
+ "text2": ["Межы", "магутнасці"],
+ "desc": "Максімальная магутнасць, якую можа выкарыстоўваць паяльнік (Ватт)"
+ },
+ "ReverseButtonTempChange": {
+ "text2": ["Інвертаваць", "кнопкі"],
+ "desc": "Інвертаваць кнопкі вымярэння тэмпературы"
+ },
+ "TempChangeShortStep": {
+ "text2": ["Крок тэмп.", "кар. нац."],
+ "desc": "Крок вымярэння тэмпературы пры кароткім націску кнопак"
+ },
+ "TempChangeLongStep": {
+ "text2": ["Крок тэмп.", "пад. нац."],
+ "desc": "Крок вымярэння тэмпературы пры падоўжаным націску кнопак"
+ },
+ "PowerPulsePower": {
+ "text2": ["Сіла імп.", "сілкав. Вт"],
+ "desc": "Сіла імпульса щтрымливаючага ад сну павербанку ці іншай крыніцы сілкавання"
+ },
+ "HallEffSensitivity": {
+ "text2": ["Эфект Хола", "адчувальнасць"],
+ "desc": "Узровень адчувальнасці датчыка хола ў рэжыме сну (А=Адключана | Н=Нізкі | С=Сярэдні | В=Высокі)"
+ },
+ "LockingMode": {
+ "text2": ["Дазволіць", "блок. кнопак"],
+ "desc": "Пры рабоце падоўжаны націск дзьвух кнопак блакуе іх (А=Адключана | Т=Толькі турба | П=Поўная блакіроўка)"
+ },
+ "MinVolCell": {
+ "text2": ["Мін.", "напр."],
+ "desc": "Мінімальная дазволеная напруга на ячэйку (3S: 3 - 3,7V | 4S: 2,4 - 3,7V)"
+ },
+ "AnimLoop": {
+ "text2": ["Зацыкленая", "анімацыя"],
+ "desc": "Зацыкленая анімацыя гузікаў у галоўным меню"
+ },
+ "AnimSpeed": {
+ "text2": ["Хуткасць", "анімацыі"],
+ "desc": "Хуткасць анімацыі гузікаў у галоўным меню (Мілісекунды) (А=Адключана | Н=Нізкі | С=Сярэдні | В=Высокі)"
+ },
+ "PowerPulseWait": {
+ "text2": ["Імпульс магутнасці", "час чакання"],
+ "desc": "Час чакання перад запускам кожнага імпульсу няспання (x 2.5 с)"
+ },
+ "PowerPulseDuration": {
+ "text2": ["Імпульс магутнасці", "працягласць"],
+ "desc": "Працягласць імпульсу няспання (x 250 мс)"
+ },
+ "LanguageSwitch": {
+ "text2": ["Мова:", " BY Беларуская"],
+ "desc": ""
+ },
+ "Brightness": {
+ "text2": ["Экран", "Яркасць"],
+ "desc": "Адрэгулюйце кантраснасць / яркасць OLED-экрана"
+ },
+ "ColourInversion": {
+ "text2": ["Экран", "Інвертаваць"],
+ "desc": "Інвертаваць колеры OLED-экрана"
+ }
+ }
+}
diff --git a/Translations/translation_EL.json b/Translations/translation_EL.json
new file mode 100644
index 00000000..10d4ae86
--- /dev/null
+++ b/Translations/translation_EL.json
@@ -0,0 +1,340 @@
+{
+ "languageCode": "EL",
+ "languageLocalName": "Greek",
+ "fonts": [
+ "ascii_basic",
+ "greek"
+ ],
+ "tempUnitFahrenheit": true,
+ "messages": {
+ "SettingsCalibrationWarning": "Πριν προχωρήσετε, παρακαλώ σιγουρευτείτε πως η μύτη βρίσκεται σε θερμοκρασία δωματίου!",
+ "SettingsResetWarning": "Σίγουρα θέλετε επαναφορά αρχικών ρυθμίσεων;",
+ "UVLOWarningString": "Χαμηλ DC",
+ "UndervoltageString": "Υπόταση",
+ "InputVoltageString": "Είσοδος V: ",
+ "SleepingSimpleString": "Zzzz",
+ "SleepingAdvancedString": "Υπνος...",
+ "SleepingTipAdvancedString": "Μύτη:",
+ "IdleTipString": "Μύτη:",
+ "IdleSetString": " Set:",
+ "TipDisconnectedString": "ΧΩΡΙΣ ΜΥΤΗ",
+ "SolderingAdvancedPowerPrompt": "Ενέργεια: ",
+ "OffString": "Απ."
+ },
+ "messagesWarn": {
+ "ResetOKMessage": "Επαν. OK",
+ "SettingsResetMessage": [
+ "Κάποιες ρυθμ.",
+ "άλλαξαν"
+ ],
+ "NoAccelerometerMessage": [
+ "Δεν εντοπίστηκε",
+ "επιταχυνσιόμετρο"
+ ],
+ "NoPowerDeliveryMessage": [
+ "Δεν εντοπίστηκε",
+ "κύκλωμα USB-PD"
+ ],
+ "LockingKeysString": "ΚΛΕΙΔ.",
+ "UnlockingKeysString": "ΞΕΚΛΕΙΔ.",
+ "WarningKeysLockedString": [
+ "ΚΛΕΙΔΩΜΕΝΑ",
+ "ΠΛΗΚΤΡΑ!"
+ ],
+ "WarningThermalRunaway": [
+ "Θερμική",
+ "Φυγή"
+ ]
+ },
+ "characters": {
+ "SettingRightChar": "R",
+ "SettingLeftChar": "L",
+ "SettingAutoChar": "Α",
+ "SettingOffChar": "0",
+ "SettingSlowChar": "Α",
+ "SettingMediumChar": "Μ",
+ "SettingFastChar": "Γ",
+ "SettingStartNoneChar": "0",
+ "SettingStartSolderingChar": "Κ",
+ "SettingStartSleepChar": "Ζ",
+ "SettingStartSleepOffChar": "Υ",
+ "SettingSensitivityOff": "0",
+ "SettingSensitivityLow": "Χ",
+ "SettingSensitivityMedium": "Μ",
+ "SettingSensitivityHigh": "Υ",
+ "SettingLockDisableChar": "Α",
+ "SettingLockBoostChar": "B",
+ "SettingLockFullChar": "Π",
+ "SettingNAChar": "Δ/Δ"
+ },
+ "menuGroups": {
+ "PowerMenu": {
+ "text2": [
+ "Ρυθμίσεις",
+ "ενέργειας"
+ ],
+ "desc": "Ρυθμίσεις ενέργειας"
+ },
+ "SolderingMenu": {
+ "text2": [
+ "Ρυθμίσεις",
+ "κόλλησης"
+ ],
+ "desc": "Ρυθμίσεις κόλλησης"
+ },
+ "PowerSavingMenu": {
+ "text2": [
+ "Λειτουργία",
+ "ύπνου"
+ ],
+ "desc": "Ρυθμίσεις εξοικ. ενέργ."
+ },
+ "UIMenu": {
+ "text2": [
+ "Διεπαφή",
+ "χρήστη"
+ ],
+ "desc": "Ρυθμίσεις διεπαφής χρήστη"
+ },
+ "AdvancedMenu": {
+ "text2": [
+ "Προηγμένες",
+ "ρυθμίσεις"
+ ],
+ "desc": "Προηγμένες ρυθμίσεις"
+ }
+ },
+ "menuOptions": {
+ "DCInCutoff": {
+ "text2": [
+ "Πηγή",
+ "ενέργειας"
+ ],
+ "desc": "Πηγή ενέργειας. Oρισμός τάσης απενεργοποίησης. (DC 10V) (S 3.3V ανα κυψέλη, απενεργοποίηση ενεργειακού ορίου)"
+ },
+ "SleepTemperature": {
+ "text2": [
+ "Θερμοκρ.",
+ "ύπνου"
+ ],
+ "desc": "Θερμοκρασία μύτης σε λειτ. ύπνου"
+ },
+ "SleepTimeout": {
+ "text2": [
+ "Έναρξη",
+ "ύπνου"
+ ],
+ "desc": "Χρονικό διάστημα πρίν την ενεργοποίηση λειτουργίας ύπνου (Δ=δευτ. | Λ=λεπτά)"
+ },
+ "ShutdownTimeout": {
+ "text2": [
+ "Έναρξη",
+ "απενεργ."
+ ],
+ "desc": "Χρονικό διάστημα πρίν την απενεργοποίηση του κολλητηριού (Λ=λεπτά)"
+ },
+ "MotionSensitivity": {
+ "text2": [
+ "Ευαισθησία",
+ "κίνησης"
+ ],
+ "desc": "0=off | 1=λιγότερο ευαίσθητο | ... | 9=περισσότερο ευαίσθητο"
+ },
+ "TemperatureUnit": {
+ "text2": [
+ "Μονάδες",
+ "θερμοκρασίας"
+ ],
+ "desc": "C=Κελσίου | F=Φαρενάιτ"
+ },
+ "AdvancedIdle": {
+ "text2": [
+ "Λεπτομερής",
+ "οθ. αδράνειας"
+ ],
+ "desc": "Προβολή λεπτομερών πληροφοριών σε μικρότερη γραμματοσειρά στην οθόνη αδράνειας"
+ },
+ "DisplayRotation": {
+ "text2": [
+ "Διάταξη",
+ "οθόνης"
+ ],
+ "desc": "R=δεξιόχειρες | L=αριστερόχειρες | Α=αυτόματο"
+ },
+ "BoostTemperature": {
+ "text2": [
+ "Θερμοκ.",
+ "boost"
+ ],
+ "desc": "Θερμοκρασία στη \"λειτουργία boost\""
+ },
+ "AutoStart": {
+ "text2": [
+ "Ζέσταμα",
+ "κατά την εν."
+ ],
+ "desc": "0=off | Κ=θερμ. κόλλησης | Z=αναμονή σε θερμοκρασία ύπνου μέχρι την κίνηση | Υ=αναμονή χωρίς ζέσταμα μέχρι την κίνηση"
+ },
+ "CooldownBlink": {
+ "text2": [
+ "Αναβοσβήσιμο",
+ "ψύξης"
+ ],
+ "desc": "Αναβοσβήσιμο της ενδειξης θερμοκρασίας κατά την παύση θέρμανσης όταν η μύτη είναι ακόμα καυτή"
+ },
+ "TemperatureCalibration": {
+ "text2": [
+ "Βαθμονόμηση",
+ "θερμοκρασίας;"
+ ],
+ "desc": "Βαθμονόμηση αρχικής θερμοκρασίας μύτης"
+ },
+ "SettingsReset": {
+ "text2": [
+ "Επαναφορά",
+ "εργ. ρυθμίσεων;"
+ ],
+ "desc": "Επαναφορά στις προεπιλεγμένες ρυθμίσεις"
+ },
+ "VoltageCalibration": {
+ "text2": [
+ "Βαθμονόμηση",
+ "τάσης εισόδου;"
+ ],
+ "desc": "Έναρξη βαθμονόμησης τάσης εισόδου (κράτημα για έξοδο)"
+ },
+ "AdvancedSoldering": {
+ "text2": [
+ "Λεπτομερής",
+ "οθ. κόλλησης"
+ ],
+ "desc": "Προβολή λεπτομερών πληροφοριών σε μικρότερη γραμματοσειρά στην οθόνη κόλλησης"
+ },
+ "ScrollingSpeed": {
+ "text2": [
+ "Ταχύτητα",
+ "κύλισης"
+ ],
+ "desc": "Ταχύτητα κύλισης κειμένου (Α=αργά | Γ=γρήγορα)"
+ },
+ "QCMaxVoltage": {
+ "text2": [
+ "Τάση",
+ "QC"
+ ],
+ "desc": "Μέγιστη τάση QC που να ζητά το κολλητήρι από το τροφοδοτικό"
+ },
+ "PDNegTimeout": {
+ "text2": [
+ "χρονικό όριο",
+ "PD"
+ ],
+ "desc": "Χρονικό όριο διαπραγμάτευσης PD σε βήματα 100ms για συμβατότητα με κάποιους φορτιστές QC"
+ },
+ "PowerLimit": {
+ "text2": [
+ "Ενεργειακό",
+ "όριο"
+ ],
+ "desc": "Μέγιστη ενέργεια που μπορεί να χρησιμοποιεί το κολλητήρι (W=watt)"
+ },
+ "ReverseButtonTempChange": {
+ "text2": [
+ "Αντιστροφή",
+ "πλήκτρων + -"
+ ],
+ "desc": "Αντιστροφή διάταξης πλήκτρων στη ρύθμιση θερμοκρασίας"
+ },
+ "TempChangeShortStep": {
+ "text2": [
+ "Αλλαγή θερμοκ.",
+ "στιγμιαίο"
+ ],
+ "desc": "Βήμα αλλαγής θερμοκρασίας σε στιγμιαίο πάτημα πλήκτρου"
+ },
+ "TempChangeLongStep": {
+ "text2": [
+ "Αλλαγή θερμοκ.",
+ "παρατεταμένο"
+ ],
+ "desc": "Βήμα αλλαγής θερμοκρασίας σε παρατεταμένο πάτημα πλήκτρου"
+ },
+ "PowerPulsePower": {
+ "text2": [
+ "Παλμός",
+ "ενέργειας"
+ ],
+ "desc": "Ένταση ενέργειας παλμού διατήρησης λειτουργίας (watt)"
+ },
+ "HallEffSensitivity": {
+ "text2": [
+ "Ευαισθ. αισθ. ",
+ "φαιν. Hall"
+ ],
+ "desc": "Ευαισθησία του αισθητήρα φαινομένου Hall για εντοπισμό αδράνειας (0=off | Χ=χαμηλή | Μ=μέτρια | Υ=υψηλή)"
+ },
+ "LockingMode": {
+ "text2": [
+ "Κλείδωμα",
+ "πλήκτρων"
+ ],
+ "desc": "Κατά την κόλληση, κρατήστε και τα δύο πλήκτρα για κλείδωμα (A=απενεργοποίηση | B=μόνο λειτ. boost | Π=πλήρες κλείδωμα)"
+ },
+ "MinVolCell": {
+ "text2": [
+ "Ελάχιστη",
+ "τάση"
+ ],
+ "desc": "Ελάχιστη επιτρεπτή τάση ανα κυψέλη (3 σε σειρά: 3 - 3.7V | 4-6 σε σειρά: 2.4 - 3.7V)"
+ },
+ "AnimLoop": {
+ "text2": [
+ "Επανάληψη",
+ "κιν. εικονιδ."
+ ],
+ "desc": "Επανάληψη κίνησης εικονιδίων στο αρχικό μενού"
+ },
+ "AnimSpeed": {
+ "text2": [
+ "Ταχύτητα",
+ "κιν. εικονιδ."
+ ],
+ "desc": "Ρυθμός κίνησης εικονιδίων στο μενού (0=off | Α=αργός | Μ=μέτριος | Γ=γρήγορος"
+ },
+ "PowerPulseWait": {
+ "text2": [
+ "Καθυστέρηση",
+ "παλμού ενέργ."
+ ],
+ "desc": "Καθυστέρηση πριν την ενεργοποίση παλμού διατήρησης λειτουργίας (x 2.5s)"
+ },
+ "PowerPulseDuration": {
+ "text2": [
+ "Διάρκεια",
+ "παλμού ενέργ."
+ ],
+ "desc": "Διάρκεια παλμού διατήρησης ενέργειας (x 250ms)"
+ },
+ "LanguageSwitch": {
+ "text2": [
+ "Γλώσσα",
+ " GR Ελληνικά"
+ ],
+ "desc": "Τρέχουσα γλώσσα λογισμικού"
+ },
+ "Brightness": {
+ "text2": [
+ "Αντίθεση",
+ "οθόνης"
+ ],
+ "desc": "Ρύθμιση φωτεινότητας οθόνης OLED"
+ },
+ "ColourInversion": {
+ "text2": [
+ "Αντιστροφή",
+ "χρωμάτων"
+ ],
+ "desc": "Αντιστροφή χρωμάτων οθόνης OLED"
+ }
+ }
+}
\ No newline at end of file
diff --git a/Translations/translation_YUE_HK.json b/Translations/translation_YUE_HK.json
index d5b8ed78..8def9cbb 100644
--- a/Translations/translation_YUE_HK.json
+++ b/Translations/translation_YUE_HK.json
@@ -4,7 +4,7 @@
"fonts": ["ascii_basic", "cjk"],
"tempUnitFahrenheit": true,
"messages": {
- "SettingsCalibrationWarning": "開始温度校正之前,請先確定辣雞咀係處於室温!",
+ "SettingsCalibrationWarning": "開始温度校正之前,請先確定焫雞咀係處於室温!",
"SettingsResetWarning": "你係咪確定要將全部設定重設到預設值?",
"UVLOWarningString": "電壓過低",
"UndervoltageString": "Undervoltage",
@@ -21,8 +21,8 @@
"messagesWarn": {
"ResetOKMessage": "已重設!",
"SettingsResetMessage": "設定已被重設!",
- "NoAccelerometerMessage": ["No accelerometer", "detected!"],
- "NoPowerDeliveryMessage": ["No USB-PD IC", "detected!"],
+ "NoAccelerometerMessage": "未能偵測加速度計",
+ "NoPowerDeliveryMessage": "未能偵測PD晶片",
"LockingKeysString": "已鎖定",
"UnlockingKeysString": "已解除鎖定",
"WarningKeysLockedString": "!撳掣鎖定!",
@@ -78,7 +78,7 @@
},
"SleepTemperature": {
"text2": "待機温度",
- "desc": "喺待機模式時嘅辣雞咀温度"
+ "desc": "喺待機模式時嘅焫雞咀温度"
},
"SleepTimeout": {
"text2": "待機延時",
@@ -114,11 +114,11 @@
},
"CooldownBlink": {
"text2": "降温時閃爍",
- "desc": "停止加熱之後,當辣雞咀仲係熱嗰陣閃爍畫面"
+ "desc": "停止加熱之後,當焫雞咀仲係熱嗰陣閃爍畫面"
},
"TemperatureCalibration": {
"text2": "温度校正?",
- "desc": "開始校正辣雞咀温度位移"
+ "desc": "開始校正焫雞咀温度位移"
},
"SettingsReset": {
"text2": "全部重設?",
@@ -146,7 +146,7 @@
},
"PowerLimit": {
"text2": "功率限制",
- "desc": "限制辣雞可用嘅最大功率 "
+ "desc": "限制焫雞可用嘅最大功率 "
},
"ReverseButtonTempChange": {
"text2": "反轉加減掣",
diff --git a/Translations/translation_ZH_CN.json b/Translations/translation_ZH_CN.json
index 18a6527a..7c5c0333 100644
--- a/Translations/translation_ZH_CN.json
+++ b/Translations/translation_ZH_CN.json
@@ -4,27 +4,27 @@
"fonts": ["ascii_basic", "cjk"],
"tempUnitFahrenheit": true,
"messages": {
- "SettingsCalibrationWarning": "开始温度校正前,请先确定铬铁头正处于室温!",
- "SettingsResetWarning": "你是否确定要将全部设置重置为默认值?",
+ "SettingsCalibrationWarning": "开始温度校正前,请先确定烙铁头正处于室温!",
+ "SettingsResetWarning": "你是否确定要将全部设定重置为默认值?",
"UVLOWarningString": "电压过低",
"UndervoltageString": "Undervoltage",
- "InputVoltageString": "Input V: ",
+ "InputVoltageString": "VIN: ",
"SleepingSimpleString": "Zzzz",
- "SleepingAdvancedString": "Sleeping...",
- "SleepingTipAdvancedString": "Tip:",
- "IdleTipString": "Tip:",
- "IdleSetString": " Set:",
- "TipDisconnectedString": "NO TIP",
+ "SleepingAdvancedString": "Zzzz...",
+ "SleepingTipAdvancedString": "--->",
+ "IdleTipString": "--->",
+ "IdleSetString": " /",
+ "TipDisconnectedString": "<-X-",
"SolderingAdvancedPowerPrompt": "Power: ",
"OffString": "关"
},
"messagesWarn": {
"ResetOKMessage": "已重置!",
- "SettingsResetMessage": "设置已被重置!",
- "NoAccelerometerMessage": ["No accelerometer", "detected!"],
- "NoPowerDeliveryMessage": ["No USB-PD IC", "detected!"],
+ "SettingsResetMessage": "设定已被重置!",
+ "NoAccelerometerMessage": "未检测到加速度计",
+ "NoPowerDeliveryMessage": "未检测到PD电路",
"LockingKeysString": "已锁定",
- "UnlockingKeysString": "已解除锁定",
+ "UnlockingKeysString": "已解锁",
"WarningKeysLockedString": "!按键锁定!",
"WarningThermalRunaway": "加热失控"
},
@@ -63,78 +63,78 @@
"desc": "自动待机省电设置"
},
"UIMenu": {
- "text2": "使用者介面",
- "desc": "使用者介面设置"
+ "text2": "用户界面",
+ "desc": "用户界面设置"
},
"AdvancedMenu": {
- "text2": "进阶设置",
- "desc": "进阶设置"
+ "text2": "高级设置",
+ "desc": "高级设置"
}
},
"menuOptions": {
"DCInCutoff": {
- "text2": "电源",
- "desc": "输入电源;设置自动停机电压 "
+ "text2": "下限电压",
+ "desc": "设置自动停机电压 "
},
"SleepTemperature": {
"text2": "待机温度",
- "desc": "于待机模式时的铬铁头温度"
+ "desc": "待机模式时的烙铁头温度"
},
"SleepTimeout": {
- "text2": "待机延时",
- "desc": "自动进入待机模式前的闲置等候时间 "
+ "text2": "待机超时",
+ "desc": "自动进入待机模式前的等候时间 "
},
"ShutdownTimeout": {
"text2": "自动关机",
- "desc": "自动关机前的闲置等候时间 "
+ "desc": "自动关机前的等候时间 "
},
"MotionSensitivity": {
"text2": "动作灵敏度",
"desc": "0=禁用 | 1=最低灵敏度 | ... | 9=最高灵敏度"
},
"TemperatureUnit": {
- "text2": "温标",
+ "text2": "温度单位",
"desc": "C=摄氏 | F=华氏"
},
"AdvancedIdle": {
- "text2": "详细闲置画面",
- "desc": "于闲置画面以英语小字显示详细信息"
+ "text2": "闲置画面详情",
+ "desc": "闲置画面以英语小字体显示详情"
},
"DisplayRotation": {
- "text2": "画面方向",
- "desc": "右=使用右手 | 左=使用左手 | 自=自动"
+ "text2": "显示方向",
+ "desc": "右=右手 | 左=左手 | 自=自动"
},
"BoostTemperature": {
"text2": "增热温度",
- "desc": "于增热模式时使用的温度"
+ "desc": "增热模式时使用的温度"
},
"AutoStart": {
- "text2": "自动启用",
- "desc": "开机时自动启用 <无=禁用 | 焊=焊接模式 | 待=待机模式 | 室=室温待机>"
+ "text2": "自动启动",
+ "desc": "开机时自动启动 <无=禁用 | 焊=焊接模式 | 待=待机模式 | 室=室温待机>"
},
"CooldownBlink": {
- "text2": "降温时闪烁",
- "desc": "停止加热之后,当铬铁头仍处于高温时闪烁画面"
+ "text2": "降温时闪显",
+ "desc": "停止加热之后,闪动温度显示提醒烙铁头仍处于高温状态"
},
"TemperatureCalibration": {
"text2": "温度校正?",
- "desc": "开始校正铬铁头温度位移"
+ "desc": "开始校正烙铁头温度偏移"
},
"SettingsReset": {
"text2": "全部重置?",
- "desc": "将所有设置重置为默认值"
+ "desc": "将所有设定重置为默认值"
},
"VoltageCalibration": {
"text2": "输入电压校正?",
- "desc": "开始校正VIN输入电压 <长按以退出>"
+ "desc": "开始校正输入电压(VIN)<长按以退出>"
},
"AdvancedSoldering": {
- "text2": "详细焊接画面",
- "desc": "于焊接模式画面以英语小字显示详细信息"
+ "text2": "焊接画面详情",
+ "desc": "焊接模式画面以英语小字体显示详请"
},
"ScrollingSpeed": {
- "text2": "卷动速度",
- "desc": "解说文字的卷动速度"
+ "text2": "滚动速度",
+ "desc": "解说文字的滚动速度"
},
"QCMaxVoltage": {
"text2": "QC电压",
@@ -142,55 +142,55 @@
},
"PDNegTimeout": {
"text2": "PD超时",
- "desc": "设定USB PD协议交涉的超时时限;为兼容某些QC电源而设 "
+ "desc": "设定USB-PD协议交涉的超时时限;为兼容某些QC电源而设 "
},
"PowerLimit": {
"text2": "功率限制",
- "desc": "限制铬铁可用的最大功率 "
+ "desc": "限制烙铁可用的最大功率 "
},
"ReverseButtonTempChange": {
"text2": "调换加减键",
- "desc": "调校温度时调换加减键的方向"
+ "desc": "调校温度时更换加减键的方向"
},
"TempChangeShortStep": {
- "text2": "温度调整 短",
- "desc": "调校温度时短按一下的温度变幅"
+ "text2": "短按温度调整",
+ "desc": "调校温度时短按按键的温度变幅"
},
"TempChangeLongStep": {
- "text2": "温度调整 长",
+ "text2": "长按温度调整",
"desc": "调校温度时长按按键的温度变幅"
},
"PowerPulsePower": {
"text2": "电源脉冲",
- "desc": "为保持电源唤醒而通电所用的功率 "
+ "desc": "为保持电源处于唤醒状态所用的功率 "
},
"HallEffSensitivity": {
"text2": "磁场灵敏度",
- "desc": "磁场感应器用作启动待机模式的灵敏度 <关=禁用 | 低=最低灵敏度 | 中=中等灵敏度 | 高=最高灵敏度>"
+ "desc": "霍尔效应传感器用作启动待机模式的灵敏度 <关=禁用 | 低=低灵敏度 | 中=中灵敏度 | 高=高灵敏度>"
},
"LockingMode": {
"text2": "按键锁定",
- "desc": "于焊接模式时,同时长按两个按键启用按键锁定 <无=禁用 | 增=只容许增热模式 | 全=锁定全部>"
+ "desc": "焊接模式时,同时长按两个按键启用按键锁定 <无=禁用 | 增=只容许增热模式 | 全=完全锁定>"
},
"MinVolCell": {
"text2": "最低电压",
- "desc": "每颗电池的最低可用电压 <伏特> <3S: 3.0V - 3.7V, 4/5/6S: 2.4V - 3.7V>"
+ "desc": "每节电池的最低允许电压 <3S: 3.0V - 3.7V, 4/5/6S: 2.4V - 3.7V>"
},
"AnimLoop": {
"text2": "动画循环",
- "desc": "循环显示功能表图示动画"
+ "desc": "主菜单中循环播放功能图标动画"
},
"AnimSpeed": {
"text2": "动画速度",
- "desc": "功能表图示动画的速度 <关=不显示动画 | 慢=慢速 | 中=中速 | 快=快速>"
+ "desc": "主菜单中功能图标动画的播放速度 <关=不显示动画 | 慢=慢速 | 中=中速 | 快=快速>"
},
"PowerPulseWait": {
"text2": "电源脉冲间隔",
- "desc": "为保持电源唤醒,每次通电之间的间隔时间 "
+ "desc": "为保持电源处于唤醒状态,每次通电之间的间隔时间 "
},
"PowerPulseDuration": {
"text2": "电源脉冲时长",
- "desc": "为保持电源唤醒,每次通电脉冲的时间长度 "
+ "desc": "为保持电源处于唤醒状态,每次通电脉冲的时间长度 "
},
"LanguageSwitch": {
"text2": "语言:简体中文",
@@ -198,11 +198,11 @@
},
"Brightness": {
"text2": "屏幕亮度",
- "desc": "设定OLED屏幕的亮度"
+ "desc": "调整OLED屏幕的亮度"
},
"ColourInversion": {
- "text2": "螢幕反轉色",
- "desc": "反转OLED屏幕的黑白色彩"
+ "text2": "反转屏幕颜色",
+ "desc": "反转OLED黑/白屏幕"
}
}
}
diff --git a/Translations/translation_ZH_TW.json b/Translations/translation_ZH_TW.json
index 34727748..b28835e5 100644
--- a/Translations/translation_ZH_TW.json
+++ b/Translations/translation_ZH_TW.json
@@ -4,7 +4,7 @@
"fonts": ["ascii_basic", "cjk"],
"tempUnitFahrenheit": true,
"messages": {
- "SettingsCalibrationWarning": "開始溫度校正前,請先確定鉻鐵頭正處於室溫!",
+ "SettingsCalibrationWarning": "開始溫度校正前,請先確定烙鐵頭正處於室溫!",
"SettingsResetWarning": "你是否確定要將全部設定重設到預設值?",
"UVLOWarningString": "電壓過低",
"UndervoltageString": "Undervoltage",
@@ -21,8 +21,8 @@
"messagesWarn": {
"ResetOKMessage": "已重設!",
"SettingsResetMessage": "設定已被重設!",
- "NoAccelerometerMessage": ["No accelerometer", "detected!"],
- "NoPowerDeliveryMessage": ["No USB-PD IC", "detected!"],
+ "NoAccelerometerMessage": "未能偵測加速度計",
+ "NoPowerDeliveryMessage": "未能偵測PD晶片",
"LockingKeysString": "已鎖定",
"UnlockingKeysString": "已解除鎖定",
"WarningKeysLockedString": "!按鍵鎖定!",
@@ -78,7 +78,7 @@
},
"SleepTemperature": {
"text2": "待機溫度",
- "desc": "於待機模式時的鉻鐵頭溫度"
+ "desc": "於待機模式時的烙鐵頭溫度"
},
"SleepTimeout": {
"text2": "待機延時",
@@ -114,11 +114,11 @@
},
"CooldownBlink": {
"text2": "降溫時閃爍",
- "desc": "停止加熱之後,當鉻鐵頭仍處於高溫時閃爍畫面"
+ "desc": "停止加熱之後,當烙鐵頭仍處於高溫時閃爍畫面"
},
"TemperatureCalibration": {
"text2": "溫度校正?",
- "desc": "開始校正鉻鐵頭溫度位移"
+ "desc": "開始校正烙鐵頭溫度位移"
},
"SettingsReset": {
"text2": "全部重設?",
@@ -146,7 +146,7 @@
},
"PowerLimit": {
"text2": "功率限制",
- "desc": "限制鉻鐵可用的最大功率 "
+ "desc": "限制烙鐵可用的最大功率 "
},
"ReverseButtonTempChange": {
"text2": "調換加減鍵",
diff --git a/source/Makefile b/source/Makefile
index dfae3739..51373f1b 100644
--- a/source/Makefile
+++ b/source/Makefile
@@ -1,4 +1,3 @@
-
ifndef model
model:=TS100
endif
@@ -11,18 +10,30 @@ ifneq ($(model),$(filter $(model),$(ALL_MODELS)))
$(error Invalid model '$(model)', valid options are: $(ALL_MODELS))
endif
+# output folder
+HEXFILE_DIR=Hexfile
+# temporary objects folder
+OUTPUT_DIR_BASE=Objects
+OUTPUT_DIR=Objects/$(model)
+
ALL_LANGUAGES=BG CS DA DE EN ES FI FR HR HU IT JA_JP LT NL NL_BE NO PL PT RU SK SL SR_CYRL SR_LATN SV TR UK YUE_HK ZH_CN ZH_TW
LANGUAGE_GROUP_CJK_LANGS=EN JA_JP YUE_HK ZH_TW ZH_CN
LANGUAGE_GROUP_CJK_NAME=Chinese+Japanese
+ifdef custom_multi_langs
+RUN_SHELL_CMD := $(shell rm -Rf {Core/Gen,$(OUTPUT_DIR)/Core/Gen,$(HEXFILE_DIR)/*_Custom.*})
+LANGUAGE_GROUP_CUSTOM_LANGS=$(custom_multi_langs)
+LANGUAGE_GROUP_CUSTOM_NAME=Custom
+endif
+
LANGUAGE_GROUP_CYRILLIC_LANGS=EN BG RU SR_CYRL SR_LATN UK
LANGUAGE_GROUP_CYRILLIC_NAME=Bulgarian+Russian+Serbian+Ukrainian
LANGUAGE_GROUP_EUR_LANGS=EN $(filter-out $(LANGUAGE_GROUP_CJK_LANGS) $(LANGUAGE_GROUP_CYRILLIC_LANGS),$(ALL_LANGUAGES))
LANGUAGE_GROUP_EUR_NAME=European
-LANGUAGE_GROUPS=CJK CYRILLIC EUR
+LANGUAGE_GROUPS=CUSTOM CJK CYRILLIC EUR
# Defines for host tools
@@ -184,12 +195,7 @@ $(shell find $(SOURCE_CORE_DIR) -type f -name '*.cpp') \
$(shell find $(SOURCE_DRIVERS_DIR) -path $(PD_DRIVER_TESTS_DIR) -prune -false -o -type f -name '*.cpp') \
$(shell find $(DEVICE_BSP_DIR) -type f -name '*.cpp') \
$(shell find $(SOURCE_MIDDLEWARES_DIR) -type f -name '*.cpp')
-# output folder
-HEXFILE_DIR=Hexfile
-# temporary objects folder
-OUTPUT_DIR_BASE=Objects
-OUTPUT_DIR=Objects/$(model)
# code optimisation ------------------------------------------------------------
OPTIM=-Os -flto -finline-small-functions -findirect-inlining -fdiagnostics-color -ffunction-sections -fdata-sections -fshort-enums