| @@ -179,6 +179,7 @@ int main(int argc, char* argv[]) { | |||||
| exit(1); | exit(1); | ||||
| } | } | ||||
| } | } | ||||
| string::init(); | |||||
| // Check existence of the system res/ directory | // Check existence of the system res/ directory | ||||
| std::string resDir = asset::system("res"); | std::string resDir = asset::system("res"); | ||||
| @@ -27,6 +27,8 @@ extern bool isPlugin; | |||||
| // Persistent state, serialized to settings.json. | // Persistent state, serialized to settings.json. | ||||
| /** ISO 639-1 language code for string translations. */ | |||||
| extern std::string language; | |||||
| /** Launches Rack without loading plugins or the autosave patch. Always set to false when settings are saved. */ | /** Launches Rack without loading plugins or the autosave patch. Always set to false when settings are saved. */ | ||||
| extern bool safeMode; | extern bool safeMode; | ||||
| /** vcvrack.com user token */ | /** vcvrack.com user token */ | ||||
| @@ -131,5 +131,10 @@ struct Version { | |||||
| }; | }; | ||||
| std::string translate(const std::string& id); | |||||
| std::string translate(const std::string& id, const std::string& language); | |||||
| void init(); | |||||
| } // namespace string | } // namespace string | ||||
| } // namespace rack | } // namespace rack | ||||
| @@ -20,6 +20,7 @@ bool devMode = false; | |||||
| bool headless = false; | bool headless = false; | ||||
| bool isPlugin = false; | bool isPlugin = false; | ||||
| std::string language = "en"; | |||||
| bool safeMode = false; | bool safeMode = false; | ||||
| std::string token; | std::string token; | ||||
| bool windowMaximized = false; | bool windowMaximized = false; | ||||
| @@ -126,6 +127,8 @@ void destroy() { | |||||
| json_t* toJson() { | json_t* toJson() { | ||||
| json_t* rootJ = json_object(); | json_t* rootJ = json_object(); | ||||
| json_object_set_new(rootJ, "language", json_string(language.c_str())); | |||||
| // Always disable safe mode when settings are saved. | // Always disable safe mode when settings are saved. | ||||
| json_object_set_new(rootJ, "safeMode", json_boolean(false)); | json_object_set_new(rootJ, "safeMode", json_boolean(false)); | ||||
| @@ -281,6 +284,10 @@ json_t* toJson() { | |||||
| } | } | ||||
| void fromJson(json_t* rootJ) { | void fromJson(json_t* rootJ) { | ||||
| json_t* languageJ = json_object_get(rootJ, "language"); | |||||
| if (languageJ) | |||||
| language = json_string_value(languageJ); | |||||
| json_t* safeModeJ = json_object_get(rootJ, "safeMode"); | json_t* safeModeJ = json_object_get(rootJ, "safeMode"); | ||||
| if (safeModeJ) { | if (safeModeJ) { | ||||
| // If safe mode is enabled (e.g. by command line flag), don't disable it when loading. | // If safe mode is enabled (e.g. by command line flag), don't disable it when loading. | ||||
| @@ -3,6 +3,7 @@ | |||||
| #include <ctime> | #include <ctime> | ||||
| #include <cctype> // for tolower and toupper | #include <cctype> // for tolower and toupper | ||||
| #include <algorithm> // for transform and equal | #include <algorithm> // for transform and equal | ||||
| #include <map> | |||||
| #include <libgen.h> // for dirname and basename | #include <libgen.h> // for dirname and basename | ||||
| #include <stdarg.h> | #include <stdarg.h> | ||||
| @@ -11,6 +12,9 @@ | |||||
| #endif | #endif | ||||
| #include <string.hpp> | #include <string.hpp> | ||||
| #include <settings.hpp> | |||||
| #include <system.hpp> | |||||
| #include <asset.hpp> | |||||
| namespace rack { | namespace rack { | ||||
| @@ -322,6 +326,76 @@ bool Version::operator<(const Version& other) { | |||||
| } | } | ||||
| /** {language: {id: string}} */ | |||||
| static std::map<std::string, std::map<std::string, std::string>> translations; | |||||
| static void loadTranslations() { | |||||
| translations.clear(); | |||||
| std::string translationsDir = asset::system("translations"); | |||||
| for (std::string filename : system::getEntries(translationsDir)) { | |||||
| if (system::getExtension(filename) != ".json") | |||||
| continue; | |||||
| std::string language = system::getStem(filename); | |||||
| std::string path = system::join(translationsDir, filename); | |||||
| INFO("Loading translation %s from %s", language.c_str(), path.c_str()); | |||||
| FILE* file = std::fopen(path.c_str(), "r"); | |||||
| if (!file) { | |||||
| WARN("Cannot open translation file %s", path.c_str()); | |||||
| continue; | |||||
| } | |||||
| DEFER({std::fclose(file);}); | |||||
| json_error_t error; | |||||
| json_t* rootJ = json_loadf(file, 0, &error); | |||||
| if (!rootJ) { | |||||
| WARN("Translation file has invalid JSON at %d:%d %s", error.line, error.column, error.text); | |||||
| continue; | |||||
| } | |||||
| DEFER({json_decref(rootJ);}); | |||||
| auto& translation = translations[language]; | |||||
| // Load JSON | |||||
| const char* id; | |||||
| json_t* strJ; | |||||
| json_object_foreach(rootJ, id, strJ) { | |||||
| std::string s(json_string_value(strJ), json_string_length(strJ)); | |||||
| translation[id] = s; | |||||
| } | |||||
| } | |||||
| } | |||||
| std::string translate(const std::string& id) { | |||||
| std::string s = translate(id, settings::language); | |||||
| if (!s.empty()) | |||||
| return s; | |||||
| // English fallback | |||||
| if (settings::language != "en") { | |||||
| return translate(id, "en"); | |||||
| } | |||||
| return ""; | |||||
| } | |||||
| std::string translate(const std::string& id, const std::string& language) { | |||||
| auto it = translations.find(language); | |||||
| if (it == translations.end()) | |||||
| return ""; | |||||
| const auto& translation = it->second; | |||||
| return get(translation, id, ""); | |||||
| } | |||||
| void init() { | |||||
| loadTranslations(); | |||||
| } | |||||
| } // namespace string | } // namespace string | ||||
| } // namespace rack | } // namespace rack | ||||
| @@ -0,0 +1,3 @@ | |||||
| { | |||||
| "language": "English" | |||||
| } | |||||