mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-02-20 15:35:05 +00:00
i18n implementation basics
This commit is contained in:
parent
6c524ac191
commit
f28c76eb53
7
Data/data/i18n/core/en-GB.i18n
Normal file
7
Data/data/i18n/core/en-GB.i18n
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
OK
|
||||||
|
Yes
|
||||||
|
No
|
||||||
|
Cancel
|
||||||
|
Retry
|
||||||
|
Close
|
||||||
|
Open
|
||||||
7
Data/data/i18n/core/en-US.i18n
Normal file
7
Data/data/i18n/core/en-US.i18n
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
OK
|
||||||
|
Yes
|
||||||
|
No
|
||||||
|
Cancel
|
||||||
|
Retry
|
||||||
|
Close
|
||||||
|
Open
|
||||||
7
Data/data/i18n/core/fr-FR.i18n
Normal file
7
Data/data/i18n/core/fr-FR.i18n
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
OK
|
||||||
|
Oui
|
||||||
|
Non
|
||||||
|
Annuler
|
||||||
|
Réessayer
|
||||||
|
Fermer
|
||||||
|
Ouvrir
|
||||||
7
Data/data/i18n/core/nl-BE.i18n
Normal file
7
Data/data/i18n/core/nl-BE.i18n
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
OK
|
||||||
|
Ja
|
||||||
|
Nee
|
||||||
|
Annuleren
|
||||||
|
Opnieuw
|
||||||
|
Sluiten
|
||||||
|
Openen
|
||||||
7
Data/data/i18n/core/nl-NL.i18n
Normal file
7
Data/data/i18n/core/nl-NL.i18n
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
OK
|
||||||
|
Ja
|
||||||
|
Nee
|
||||||
|
Annuleren
|
||||||
|
Opnieuw
|
||||||
|
Sluiten
|
||||||
|
Openen
|
||||||
3
Data/data/i18n/launcher/en-GB.i18n
Normal file
3
Data/data/i18n/launcher/en-GB.i18n
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Apps
|
||||||
|
Files
|
||||||
|
Settings
|
||||||
3
Data/data/i18n/launcher/en-US.i18n
Normal file
3
Data/data/i18n/launcher/en-US.i18n
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Apps
|
||||||
|
Files
|
||||||
|
Settings
|
||||||
3
Data/data/i18n/launcher/fr-FR.i18n
Normal file
3
Data/data/i18n/launcher/fr-FR.i18n
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Appli
|
||||||
|
Fichiers
|
||||||
|
Réglages
|
||||||
3
Data/data/i18n/launcher/nl-BE.i18n
Normal file
3
Data/data/i18n/launcher/nl-BE.i18n
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Apps
|
||||||
|
Bestanden
|
||||||
|
Instellingen
|
||||||
3
Data/data/i18n/launcher/nl-NL.i18n
Normal file
3
Data/data/i18n/launcher/nl-NL.i18n
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Apps
|
||||||
|
Bestanden
|
||||||
|
Instellingen
|
||||||
17
Tactility/Include/Tactility/i18n/Core.h
Normal file
17
Tactility/Include/Tactility/i18n/Core.h
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// WARNING: This file is auto-generated. Do not edit manually.
|
||||||
|
|
||||||
|
namespace tt::i18n::core {
|
||||||
|
|
||||||
|
enum class Text {
|
||||||
|
OK = 0,
|
||||||
|
YES = 1,
|
||||||
|
NO = 2,
|
||||||
|
CANCEL = 3,
|
||||||
|
RETRY = 4,
|
||||||
|
CLOSE = 5,
|
||||||
|
OPEN = 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
24
Tactility/Include/Tactility/i18n/I18n.h
Normal file
24
Tactility/Include/Tactility/i18n/I18n.h
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace tt::i18n {
|
||||||
|
|
||||||
|
class IndexedText {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
virtual ~IndexedText() = default;
|
||||||
|
|
||||||
|
virtual const std::string& get(int index) const = 0;
|
||||||
|
|
||||||
|
template <typename EnumType>
|
||||||
|
const std::string& get(EnumType value) const { return get(static_cast<int>(value)); }
|
||||||
|
|
||||||
|
const std::string& operator[](const int index) const { return get(index); }
|
||||||
|
};
|
||||||
|
|
||||||
|
std::shared_ptr<IndexedText> loadIndexedText(const std::string& path);
|
||||||
|
|
||||||
|
}
|
||||||
13
Tactility/Include/Tactility/i18n/Launcher.h
Normal file
13
Tactility/Include/Tactility/i18n/Launcher.h
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// WARNING: This file is auto-generated. Do not edit manually.
|
||||||
|
|
||||||
|
namespace tt::i18n::launcher {
|
||||||
|
|
||||||
|
enum class Text {
|
||||||
|
APPS = 0,
|
||||||
|
FILES = 1,
|
||||||
|
SETTINGS = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@ -2,15 +2,18 @@
|
|||||||
#include "Tactility/app/AppRegistration.h"
|
#include "Tactility/app/AppRegistration.h"
|
||||||
#include "Tactility/service/loader/Loader.h"
|
#include "Tactility/service/loader/Loader.h"
|
||||||
|
|
||||||
|
#include "Tactility/i18n/Launcher.h"
|
||||||
|
|
||||||
#include <Tactility/Tactility.h>
|
#include <Tactility/Tactility.h>
|
||||||
|
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
#include <Tactility/BootProperties.h>
|
#include <Tactility/BootProperties.h>
|
||||||
|
#include <Tactility/i18n/I18n.h>
|
||||||
#define TAG "launcher"
|
|
||||||
|
|
||||||
namespace tt::app::launcher {
|
namespace tt::app::launcher {
|
||||||
|
|
||||||
|
constexpr auto* TAG = "Launcher";
|
||||||
|
|
||||||
static void onAppPressed(TT_UNUSED lv_event_t* e) {
|
static void onAppPressed(TT_UNUSED lv_event_t* e) {
|
||||||
auto* appId = (const char*)lv_event_get_user_data(e);
|
auto* appId = (const char*)lv_event_get_user_data(e);
|
||||||
service::loader::startApp(appId);
|
service::loader::startApp(appId);
|
||||||
@ -52,8 +55,11 @@ static lv_obj_t* createAppButton(lv_obj_t* parent, const char* title, const char
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LauncherApp : public App {
|
class LauncherApp : public App {
|
||||||
|
std::shared_ptr<i18n::IndexedText> launcherText = i18n::loadIndexedText("/data/i18n/launcher");
|
||||||
|
|
||||||
void onCreate(TT_UNUSED AppContext& app) override {
|
void onCreate(TT_UNUSED AppContext& app) override {
|
||||||
|
assert(launcherText != nullptr);
|
||||||
|
|
||||||
BootProperties boot_properties;
|
BootProperties boot_properties;
|
||||||
if (loadBootProperties(boot_properties) && !boot_properties.autoStartAppId.empty()) {
|
if (loadBootProperties(boot_properties) && !boot_properties.autoStartAppId.empty()) {
|
||||||
TT_LOG_I(TAG, "Starting %s", boot_properties.autoStartAppId.c_str());
|
TT_LOG_I(TAG, "Starting %s", boot_properties.autoStartAppId.c_str());
|
||||||
@ -87,9 +93,14 @@ class LauncherApp : public App {
|
|||||||
auto apps_icon_path = paths->getSystemPathLvgl("icon_apps.png");
|
auto apps_icon_path = paths->getSystemPathLvgl("icon_apps.png");
|
||||||
auto files_icon_path = paths->getSystemPathLvgl("icon_files.png");
|
auto files_icon_path = paths->getSystemPathLvgl("icon_files.png");
|
||||||
auto settings_icon_path = paths->getSystemPathLvgl("icon_settings.png");
|
auto settings_icon_path = paths->getSystemPathLvgl("icon_settings.png");
|
||||||
createAppButton(wrapper, "Apps", apps_icon_path.c_str(), "AppList", 0);
|
|
||||||
createAppButton(wrapper, "Files", files_icon_path.c_str(), "Files", padding);
|
const auto& apps_title = launcherText->get(i18n::launcher::Text::APPS);
|
||||||
createAppButton(wrapper, "Settings", settings_icon_path.c_str(), "Settings", padding);
|
const auto& files_title = launcherText->get(i18n::launcher::Text::FILES);
|
||||||
|
const auto& settings_title = launcherText->get(i18n::launcher::Text::SETTINGS);
|
||||||
|
|
||||||
|
createAppButton(wrapper, apps_title.c_str(), apps_icon_path.c_str(), "AppList", 0);
|
||||||
|
createAppButton(wrapper, files_title.c_str(), files_icon_path.c_str(), "Files", padding);
|
||||||
|
createAppButton(wrapper, settings_title.c_str(), settings_icon_path.c_str(), "Settings", padding);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
85
Tactility/Source/i18n/I18n.cpp
Normal file
85
Tactility/Source/i18n/I18n.cpp
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
#include "Tactility/i18n/I18n.h"
|
||||||
|
#include "Tactility/file/FileLock.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace tt::i18n {
|
||||||
|
|
||||||
|
constexpr auto* TAG = "I18n";
|
||||||
|
static std::string ERROR_RESULT = "TRANSLATION_ERROR";
|
||||||
|
|
||||||
|
class IndexedTextImplementation : IndexedText {
|
||||||
|
|
||||||
|
std::vector<std::string> data;
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
explicit IndexedTextImplementation(std::vector<std::string> data) : data(std::move(data)) {}
|
||||||
|
|
||||||
|
const std::string& get(const int index) const override {
|
||||||
|
if (index < data.size()) {
|
||||||
|
return data[index];
|
||||||
|
} else {
|
||||||
|
return ERROR_RESULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::string getDesiredLocale() {
|
||||||
|
// TODO: Implement locale settings
|
||||||
|
return "nl-NL";
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string getFallbackLocale() {
|
||||||
|
// TODO: Implement locale settings
|
||||||
|
return "en-GB";
|
||||||
|
}
|
||||||
|
|
||||||
|
static FILE* openI18nFile(const std::string& path) {
|
||||||
|
auto locale = getDesiredLocale();
|
||||||
|
auto desired_file_path = std::format("{}/{}.i18n", path, locale);
|
||||||
|
auto* file = fopen(desired_file_path.c_str(), "r");
|
||||||
|
if (file == nullptr) {
|
||||||
|
auto fallback_locale = getFallbackLocale();
|
||||||
|
TT_LOG_W(TAG, "Translations not found for %s at %s", locale.c_str(), desired_file_path.c_str());
|
||||||
|
auto fallback_file_path = std::format("{}/{}.i18n", path, getFallbackLocale());
|
||||||
|
file = fopen(fallback_file_path.c_str(), "r");
|
||||||
|
if (file == nullptr) {
|
||||||
|
TT_LOG_W(TAG, "Fallback translations not found for %s at %s", fallback_locale.c_str(), fallback_file_path.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<IndexedText> loadIndexedText(const std::string& path) {
|
||||||
|
std::vector<std::string> data;
|
||||||
|
|
||||||
|
// We lock on folder level, because file is TBD
|
||||||
|
file::withLock<void>(path, [&path, &data] {
|
||||||
|
auto* file = openI18nFile(path);
|
||||||
|
if (file != nullptr) {
|
||||||
|
char line[1024];
|
||||||
|
// TODO: move to file::readLines(filePath, skipEndline, callback)
|
||||||
|
while (fgets(line, sizeof(line), file) != nullptr) {
|
||||||
|
// Strip newline
|
||||||
|
size_t line_length = strlen(line);
|
||||||
|
if (line_length > 0 && line[line_length - 1] == '\n') {
|
||||||
|
line[line_length - 1] = '\0';
|
||||||
|
}
|
||||||
|
// Publish
|
||||||
|
data.push_back(line);
|
||||||
|
}
|
||||||
|
fclose(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto result = std::make_shared<IndexedTextImplementation>(data);
|
||||||
|
return std::reinterpret_pointer_cast<IndexedText>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
2
Translations/.gitignore
vendored
Normal file
2
Translations/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.csv
|
||||||
|
*.ods#
|
||||||
25
Translations/README.md
Normal file
25
Translations/README.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Translations
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Install [LibreOffice Calc](https://libreoffice.org/)
|
||||||
|
- Ensure you have Python 3 installed (Python 2 is not supported)
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
To add new translations or edit existing ones, please follow these steps:
|
||||||
|
|
||||||
|
1. Edit `Translations.ods` (see chapter below)
|
||||||
|
2. In LibreOffice Calc, select the relevant tab that you want to export
|
||||||
|
3. Click on `File` -> `Save a copy...` and save the file as `[tabname].csv` (without the "[]")
|
||||||
|
4. Repeat step 2 and 3 for all tabs that you updated
|
||||||
|
5. Run `python generate-all.py`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Do not commit the CSV files
|
||||||
|
- When editing the ODS file, make sure you don't paste in formatted data (use CTRL+Shift+V instead of CTRL+V)
|
||||||
|
- ODS export settings:
|
||||||
|
- Field delimiter: `,`
|
||||||
|
- String delimiter: `"`
|
||||||
|
- Encoding: `UTF-8`
|
||||||
BIN
Translations/Translations.ods
Normal file
BIN
Translations/Translations.ods
Normal file
Binary file not shown.
12
Translations/generate-all.py
Normal file
12
Translations/generate-all.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
def generate(csvFile, headerFile, headerNamespace, dataPath):
|
||||||
|
if os.path.isfile(csvFile):
|
||||||
|
print(f"Generating {headerFile}")
|
||||||
|
os.system(f"python generate.py {csvFile} {headerFile} {headerNamespace} {dataPath}")
|
||||||
|
else:
|
||||||
|
print(f"Skipping {headerFile} (not found)")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
generate("system.csv", "../Tactility/Include/Tactility/i18n/Core.h", "tt::i18n::core", "../Data/data/i18n/core")
|
||||||
|
generate("launcher.csv", "../Tactility/Include/Tactility/i18n/Launcher.h", "tt::i18n::launcher", "../Data/data/i18n/launcher")
|
||||||
90
Translations/generate.py
Normal file
90
Translations/generate.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import csv
|
||||||
|
import sys
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
def load_csv(path: str, delimiter: str = ",", quotechar: str = '"', encoding: str = "utf-8", skip_header: bool = False) -> List[List[str]]:
|
||||||
|
"""
|
||||||
|
Load a CSV file into a list of rows, where each row is a list of strings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to the CSV file.
|
||||||
|
delimiter: Field delimiter character.
|
||||||
|
quotechar: Quote character.
|
||||||
|
encoding: File encoding.
|
||||||
|
skip_header: If True, skip the first row (header).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of rows (list of lists of strings).
|
||||||
|
"""
|
||||||
|
rows: List[List[str]] = []
|
||||||
|
with open(path, "r", encoding=encoding, newline="") as f:
|
||||||
|
reader = csv.reader(f, delimiter=delimiter, quotechar=quotechar)
|
||||||
|
if skip_header:
|
||||||
|
next(reader, None)
|
||||||
|
for row in reader:
|
||||||
|
rows.append(row)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def print_help():
|
||||||
|
print("Usage: python generate.py [csv_file] [header_file_path] [header_namespace] [i18n_directory]\n\n")
|
||||||
|
print("\t[csv_file] the CSV file containing the translations, exported from the .ods file")
|
||||||
|
print("\t[header_file_path] the path to the header file to be generated")
|
||||||
|
print("\t[header_namespace] the C++ namespace to use for the generated header file")
|
||||||
|
print("\t[i18n_directory] the directory where the .i18n files will be generated")
|
||||||
|
|
||||||
|
def open_i18n_files(row, i18n_path):
|
||||||
|
result = []
|
||||||
|
for i in range(1, len(row)):
|
||||||
|
filepath = f"{i18n_path}/{row[i]}.i18n"
|
||||||
|
print(f"Opening {filepath}")
|
||||||
|
file = open(filepath, "w")
|
||||||
|
result.append(file)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def close_i18n_files(files):
|
||||||
|
for file in files:
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
def generate_header(filepath, rows, namespace):
|
||||||
|
file = open(filepath, "w")
|
||||||
|
file.write("#pragma once\n\n")
|
||||||
|
file.write("// WARNING: This file is auto-generated. Do not edit manually.\n\n")
|
||||||
|
file.write(f"namespace {namespace}")
|
||||||
|
file.write(" {\n\n")
|
||||||
|
file.write("enum class Text {\n")
|
||||||
|
for i in range(1, len(rows)):
|
||||||
|
key = rows[i][0].upper()
|
||||||
|
file.write(f" {key} = {i - 1},\n")
|
||||||
|
file.write("};\n")
|
||||||
|
file.write("\n}\n")
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
def translate(rows, language_index, file):
|
||||||
|
for i in range(1, len(rows)):
|
||||||
|
value = rows[i][language_index]
|
||||||
|
if value == "":
|
||||||
|
value = f"{rows[i][0]}_untranslated"
|
||||||
|
file.write(value)
|
||||||
|
file.write("\n")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if "--help" in sys.argv:
|
||||||
|
print_help()
|
||||||
|
sys.exit()
|
||||||
|
if len(sys.argv) != 5:
|
||||||
|
print_help()
|
||||||
|
sys.exit()
|
||||||
|
csv_file = sys.argv[1]
|
||||||
|
header_path = sys.argv[2]
|
||||||
|
header_namespace = sys.argv[3]
|
||||||
|
i18n_path = sys.argv[4]
|
||||||
|
rows = load_csv(csv_file)
|
||||||
|
if len(rows) == 0:
|
||||||
|
print("Error: CSV file is empty.")
|
||||||
|
sys.exit(1)
|
||||||
|
generate_header(header_path, rows, header_namespace)
|
||||||
|
i18n_files = open_i18n_files(rows[0], i18n_path)
|
||||||
|
for i in range(0, len(i18n_files)):
|
||||||
|
i18n_file = i18n_files[i]
|
||||||
|
translate(rows, i + 1, i18n_file)
|
||||||
|
close_i18n_files(i18n_files)
|
||||||
Loading…
x
Reference in New Issue
Block a user