diff --git a/data/strings/en.ini b/data/strings/en.ini index de9dc80de..ffdf9e187 100644 --- a/data/strings/en.ini +++ b/data/strings/en.ini @@ -829,6 +829,7 @@ section_experimental = Experimental general = General screen_scaling = Screen Scaling: ui_scaling = UI Elements Scaling: +language = Language: gpu_acceleration = GPU acceleration gpu_acceleration_tooltip = Check this option to enable hardware acceleration show_menu_bar = Show Aseprite menu bar diff --git a/data/widgets/options.xml b/data/widgets/options.xml index 48a8ac48e..92871e7d4 100644 --- a/data/widgets/options.xml +++ b/data/widgets/options.xml @@ -28,24 +28,25 @@ - - - - + + diff --git a/src/app/app.cpp b/src/app/app.cpp index a8cf2ab53..869a891c3 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -24,6 +24,7 @@ #include "app/file/file_formats_manager.h" #include "app/file_system.h" #include "app/gui_xml.h" +#include "app/i18n/strings.h" #include "app/ini_file.h" #include "app/log.h" #include "app/modules.h" @@ -193,8 +194,10 @@ void App::initialize(const AppOptions& options) if (isGui()) { LOG("APP: GUI mode\n"); - // Setup the GUI cursor and redraw screen + // Load main language (which might be in an extension) + Strings::instance()->loadCurrentLanguage(); + // Setup the GUI cursor and redraw screen ui::set_use_native_cursors(preferences().cursor.useNativeCursor()); ui::set_mouse_cursor_scale(preferences().cursor.cursorScale()); ui::set_mouse_cursor(kArrowCursor); diff --git a/src/app/commands/cmd_options.cpp b/src/app/commands/cmd_options.cpp index 1f778d1f2..3d4b74810 100644 --- a/src/app/commands/cmd_options.cpp +++ b/src/app/commands/cmd_options.cpp @@ -41,6 +41,7 @@ namespace app { +static const char* kSectionGeneralId = "section_general"; static const char* kSectionBgId = "section_bg"; static const char* kSectionGridId = "section_grid"; static const char* kSectionThemeId = "section_theme"; @@ -375,6 +376,11 @@ public: onChangeGridScope(); sectionListbox()->selectIndex(m_curSection); + // Refill languages combobox when extensions are enabled/disabled + m_extLanguagesChanges = + App::instance()->extensions().LanguagesChange.connect( + base::Bind(&OptionsWindow::refillLanguages, this)); + // Reload themes when extensions are enabled/disabled m_extThemesChanges = App::instance()->extensions().ThemesChange.connect( @@ -386,6 +392,10 @@ public: } void saveConfig() { + // Update language + Strings::instance()->setCurrentLanguage( + language()->getItemText(language()->getSelectedItemIndex())); + m_pref.general.autoshowTimeline(autotimeline()->isSelected()); m_pref.general.rewindOnStop(rewindOnStop()->isSelected()); m_globPref.timeline.firstFrame(firstFrame()->textInt()); @@ -603,8 +613,13 @@ private: panel()->showChild(findChild(item->getValue().c_str())); m_curSection = sectionListbox()->getSelectedIndex(); - if (item->getValue() == kSectionBgId) + // General section + if (item->getValue() == kSectionGeneralId) + loadLanguages(); + // Background section + else if (item->getValue() == kSectionBgId) onChangeBgScope(); + // Grid section else if (item->getValue() == kSectionGridId) onChangeGridScope(); // Load themes @@ -744,6 +759,25 @@ private: } } + void refillLanguages() { + language()->removeAllItems(); + loadLanguages(); + } + + void loadLanguages() { + // Languages already loaded + if (language()->getItemCount() > 0) + return; + + Strings* strings = Strings::instance(); + std::string curLang = strings->currentLanguage(); + for (const std::string& lang : strings->availableLanguages()) { + int i = language()->addItem(lang); + if (lang == curLang) + language()->setSelectedItemIndex(i); + } + } + void reloadThemes() { while (themeList()->firstChild()) delete themeList()->lastChild(); @@ -1091,6 +1125,7 @@ private: DocumentPreferences& m_docPref; DocumentPreferences* m_curPref; int& m_curSection; + obs::scoped_connection m_extLanguagesChanges; obs::scoped_connection m_extThemesChanges; std::string m_restoreThisTheme; int m_restoreScreenScaling; diff --git a/src/app/extensions.cpp b/src/app/extensions.cpp index 90636060b..f00ee46d3 100644 --- a/src/app/extensions.cpp +++ b/src/app/extensions.cpp @@ -220,6 +220,11 @@ Extension::~Extension() it.second.destroyMatrix(); } +void Extension::addLanguage(const std::string& id, const std::string& path) +{ + m_languages[id] = path; +} + void Extension::addTheme(const std::string& id, const std::string& path) { m_themes[id] = path; @@ -419,6 +424,19 @@ Extensions::~Extensions() delete ext; } +std::string Extensions::languagePath(const std::string& langId) +{ + for (auto ext : m_extensions) { + if (!ext->isEnabled()) // Ignore disabled extensions + continue; + + auto it = ext->languages().find(langId); + if (it != ext->languages().end()) + return it->second; + } + return std::string(); +} + std::string Extensions::themePath(const std::string& themeId) { for (auto ext : m_extensions) { @@ -630,6 +648,24 @@ Extension* Extensions::loadExtension(const std::string& path, auto contributes = json["contributes"]; if (contributes.is_object()) { + // Languages + auto languages = contributes["languages"]; + if (languages.is_array()) { + for (const auto& lang : languages.array_items()) { + std::string langId = lang["id"].string_value(); + std::string langPath = lang["path"].string_value(); + + // The path must be always relative to the extension + langPath = base::join_path(path, langPath); + + LOG("EXT: New language '%s' in '%s'\n", + langId.c_str(), + langPath.c_str()); + + extension->addLanguage(langId, langPath); + } + } + // Themes auto themes = contributes["themes"]; if (themes.is_array()) { @@ -695,6 +731,7 @@ Extension* Extensions::loadExtension(const std::string& path, void Extensions::generateExtensionSignals(Extension* extension) { + if (extension->hasLanguages()) LanguagesChange(extension); if (extension->hasThemes()) ThemesChange(extension); if (extension->hasPalettes()) PalettesChange(extension); if (extension->hasDitheringMatrices()) DitheringMatricesChange(extension); diff --git a/src/app/extensions.h b/src/app/extensions.h index 207938df9..1ae831e68 100644 --- a/src/app/extensions.h +++ b/src/app/extensions.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2017 David Capello +// Copyright (C) 2017-2018 David Capello // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -67,9 +67,11 @@ namespace app { const std::string& version() const { return m_version; } const std::string& displayName() const { return m_displayName; } + const ExtensionItems& languages() const { return m_languages; } const ExtensionItems& themes() const { return m_themes; } const ExtensionItems& palettes() const { return m_palettes; } + void addLanguage(const std::string& id, const std::string& path); void addTheme(const std::string& id, const std::string& path); void addPalette(const std::string& id, const std::string& path); void addDitheringMatrix(const std::string& id, @@ -81,6 +83,7 @@ namespace app { bool canBeDisabled() const; bool canBeUninstalled() const; + bool hasLanguages() const { return !m_languages.empty(); } bool hasThemes() const { return !m_themes.empty(); } bool hasPalettes() const { return !m_palettes.empty(); } bool hasDitheringMatrices() const { return !m_ditheringMatrices.empty(); } @@ -92,6 +95,7 @@ namespace app { bool isCurrentTheme() const; bool isDefaultTheme() const; + ExtensionItems m_languages; ExtensionItems m_themes; ExtensionItems m_palettes; std::map m_ditheringMatrices; @@ -121,6 +125,7 @@ namespace app { Extension* installCompressedExtension(const std::string& zipFn, const ExtensionInfo& info); + std::string languagePath(const std::string& langId); std::string themePath(const std::string& themeId); std::string palettePath(const std::string& palId); ExtensionItems palettes() const; @@ -128,6 +133,7 @@ namespace app { std::vector ditheringMatrices(); obs::signal NewExtension; + obs::signal LanguagesChange; obs::signal ThemesChange; obs::signal PalettesChange; obs::signal DitheringMatricesChange; diff --git a/src/app/i18n/strings.cpp b/src/app/i18n/strings.cpp index 8663d6291..2e1c8397e 100644 --- a/src/app/i18n/strings.cpp +++ b/src/app/i18n/strings.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2016, 2017 David Capello +// Copyright (C) 2016-2018 David Capello // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -10,8 +10,11 @@ #include "app/i18n/strings.h" +#include "app/app.h" +#include "app/extensions.h" #include "app/pref/preferences.h" #include "app/resource_finder.h" +#include "app/ui/main_window.h" #include "app/xml_document.h" #include "app/xml_exception.h" #include "base/fs.h" @@ -19,6 +22,8 @@ namespace app { +static const char* kDefLanguage = "en"; + // static Strings* Strings::instance() { @@ -30,16 +35,101 @@ Strings* Strings::instance() Strings::Strings() { - const std::string lang = Preferences::instance().general.language(); - LOG("I18N: Loading strings/%s.ini file\n", lang.c_str()); + loadLanguage(kDefLanguage); +} +std::set Strings::availableLanguages() const +{ + std::set result; + + // Add languages in data/strings/ ResourceFinder rf; - rf.includeDataDir(("strings/" + lang + ".ini").c_str()); - if (!rf.findFirst()) - throw base::Exception("strings/" + lang + ".txt was not found"); + rf.includeDataDir("strings"); + while (rf.next()) { + if (!base::is_directory(rf.filename())) + continue; + for (const auto& fn : base::list_files(rf.filename())) { + const std::string langId = base::get_file_title(fn); + result.insert(langId); + } + } + + // Add languages in extensions + for (const auto& ext : App::instance()->extensions()) { + if (ext->isEnabled() && + ext->hasLanguages()) { + for (const auto& langId : ext->languages()) + result.insert(langId.first); + } + } + + ASSERT(result.find(kDefLanguage) != result.end); + return result; +} + +std::string Strings::currentLanguage() const +{ + return Preferences::instance().general.language(); +} + +void Strings::setCurrentLanguage(const std::string& langId) +{ + // Do nothing (same language) + if (currentLanguage() == langId) + return; + + Preferences::instance().general.language(langId); + loadLanguage(langId); + + // Reload menus + App::instance()->mainWindow()->reloadMenus(); +} + +// Called when extensions are available +void Strings::loadCurrentLanguage() +{ + std::string langId = currentLanguage(); + if (langId != kDefLanguage) + loadLanguage(langId); +} + +void Strings::loadLanguage(const std::string& langId) +{ + m_strings.clear(); + loadStringsFromDataDir(kDefLanguage); + if (langId != kDefLanguage) { + loadStringsFromDataDir(langId); + loadStringsFromExtension(langId); + } +} + +void Strings::loadStringsFromDataDir(const std::string& langId) +{ + // Load the English language file from the Aseprite data directory (so we have the most update list of strings) + LOG("I18N: Loading strings/%s.ini file\n", langId.c_str()); + ResourceFinder rf; + rf.includeDataDir(("strings/" + langId + ".ini").c_str()); + if (!rf.findFirst()) { + LOG("strings/%s.ini was not found", langId.c_str()); + return; + } + + loadStringsFromFile(rf.filename()); +} + +void Strings::loadStringsFromExtension(const std::string& langId) +{ + Extensions& exts = App::instance()->extensions(); + std::string fn = exts.languagePath(langId); + if (!fn.empty() && base::is_file(fn)) + loadStringsFromFile(fn); +} + +void Strings::loadStringsFromFile(const std::string& fn) +{ cfg::CfgFile cfg; - cfg.load(rf.filename()); + cfg.load(fn); std::vector sections; std::vector keys; diff --git a/src/app/i18n/strings.h b/src/app/i18n/strings.h index 52c9ff182..8e3326a10 100644 --- a/src/app/i18n/strings.h +++ b/src/app/i18n/strings.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2016-2017 David Capello +// Copyright (C) 2016-2018 David Capello // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -8,9 +8,9 @@ #define APP_I18N_STRINGS_INCLUDED #pragma once +#include #include #include -#include #include "strings.ini.h" @@ -23,9 +23,19 @@ namespace app { const std::string& translate(const char* id) const; + void loadCurrentLanguage(); + std::set availableLanguages() const; + std::string currentLanguage() const; + void setCurrentLanguage(const std::string& langId); + private: Strings(); + void loadLanguage(const std::string& langId); + void loadStringsFromDataDir(const std::string& langId); + void loadStringsFromExtension(const std::string& langId); + void loadStringsFromFile(const std::string& fn); + mutable std::unordered_map m_strings; };