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/service/loader/Loader.h"
|
||||
|
||||
#include "Tactility/i18n/Launcher.h"
|
||||
|
||||
#include <Tactility/Tactility.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <Tactility/BootProperties.h>
|
||||
|
||||
#define TAG "launcher"
|
||||
#include <Tactility/i18n/I18n.h>
|
||||
|
||||
namespace tt::app::launcher {
|
||||
|
||||
constexpr auto* TAG = "Launcher";
|
||||
|
||||
static void onAppPressed(TT_UNUSED lv_event_t* e) {
|
||||
auto* appId = (const char*)lv_event_get_user_data(e);
|
||||
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 {
|
||||
std::shared_ptr<i18n::IndexedText> launcherText = i18n::loadIndexedText("/data/i18n/launcher");
|
||||
|
||||
void onCreate(TT_UNUSED AppContext& app) override {
|
||||
assert(launcherText != nullptr);
|
||||
|
||||
BootProperties boot_properties;
|
||||
if (loadBootProperties(boot_properties) && !boot_properties.autoStartAppId.empty()) {
|
||||
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 files_icon_path = paths->getSystemPathLvgl("icon_files.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);
|
||||
createAppButton(wrapper, "Settings", settings_icon_path.c_str(), "Settings", padding);
|
||||
|
||||
const auto& apps_title = launcherText->get(i18n::launcher::Text::APPS);
|
||||
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