Локализация приложения
Цель лекции: Научиться создавать приложения с поддержкой нескольких языков пользовательского интерфейса
Независимо от того, собираетесь ли вы выпускать ваш программу под свободной лицензией, либо как коммерческий продукт, наверняка вы стремитесь к тому, чтобы она получила как можно более широкое распространение. Понятно, что возможность менять язык интерфейса приложения и наличие переведённой на различные языки документации потенциально значительно расширяют число её пользователей.
Локализация приложения — это его адаптация к языковым и культурным особенностям страны или стран, отличных от места разработки программы, в которых оно будет использоваться. Локализация включает в себя перевод интерфейса программы, её документации, адаптацию алгоритмов алфавитной сортировки и предоставления данных (например, отображение разделителя дробной части десятичных дробей). В этой главе мы остановимся лишь на переводе интерфейса приложения.
Реализовать эту задачу можно двумя различными способами.
Например, можно поступить так, как мы это сделали в "Элементы управления. Компоненты меню" , когда переведённые строки вставляются прямо в код. Однако, во-первых, это неправильно идеологически, т.к. нарушает принцип инкапсуляции: либо переводчик должен получить доступ к исходным текстам программы, либо новый перевод должен переносить в код программист. Кроме того, тем самым резко затрудняется добавление новых языков: ведь для этого программу придётся каждый раз компилировать заново.
Поэтому большее распространение получил второй способ, когда программа считывает строки перевода из файла, соответствующему языку, выбранному пользователем. При добавлении новых файлов перевода в заданный каталог в меню выбора языков программы появляются новые пункты. Подобный подход, помимо облегчения добавления новых языков интерфейса, позволяет разделить труд переводчика и программиста.
В библиотеке Juce для реализации последнего подхода существует специальный класс, LocalisedStrings. Объект этого класса загружает строки перевода из текстового файла и заменяет ими эквивалентные строки, используемые в приложении.
Файл перевода имеет следующий формат:
language: Russian countries: ru "hello" = "привет" "goodbye" = "до свидания"
В том случае, если в текст локализации требуется включить кавычки, в файл перевода необходимо вставить двойные кавычки ("").
Для замены в программе интерфейсных строк строками перевода можно воспользоваться двумя способами.
Во-первых, класс LocalisedStrings включает метод const String LocalisedStrings::translate(const String& text) const, который заменяет строки интерфейса программы их локализованными версиями. В том случае, если в файле перевода не окажется подходящей строки, то будет возвращена оригинальная.
Во-вторых, в Juce определён макрос TRANS(text), который пытается перевести строку text с помощью метода static const String LocalisedStrings::translateWithCurrentMappings(const String& text) класса LocalisedStrings ( пример 21.1).
#define TRANS(stringLiteral) LocalisedStrings::translateWithCurrentMappings(stringLiteralЛистинг 21.1. Определение макроса TEXT
Метод пытается перевести строку text, получаемую в качестве параметра, используя заданное отображение (mapping). Отображение задаётся с помощью метода static void LocalisedStrings::setCurrentMappings(LocalisedStrings* newTranslations).
В том случае, если никакого отображения не было задано, translateWithCurrentMappings возвращает оригинальный текст.
Рассмотрим оба способа локализации интерфейса приложения на простых примерах.
Перевод строк интерфейса с помощью макроса TRANS
Первая демонстрационная программа будет выводить на ярлык приветствующую мир надпись, а также включать закрывающую кнопку ( рис. 21.1).
Строки для перевода надписей на ярлыке и кнопке наша программа будет загружать из файла набора, который мы подготовим заранее. Выбор файла с тем или иным языком перевода будет осуществляться в зависимости от языковых настроек целевой операционной системы.
Собственно перевод будет осуществлять глобальный объект класса LocalisedStrings, указатель на который мы будем передавать в метод setCurrentMappings в качестве параметра. Этот указатель будет удаляться автоматически после того, как в нём отпадёт необходимость, поэтому во избежание утечек памяти нам необходимо воспользоваться паттерном одиночки (singleton). Напишем класс TTranslator, унаследовав его от LocalisedStrings ( пример 21.2).
#ifndef _TTranslator_h_ #define _TTranslator_h_ //-------------------------------------------------- #include "../JuceLibraryCode/JuceHeader.h" //-------------------------------------------------- class TTranslator : public LocalisedStrings { static TTranslator* pSelf; protected: TTranslator(const File FileToLoad) : LocalisedStrings(FileToLoad){} public: static TTranslator* pInstance(const File FileToLoad) { if(!pSelf) pSelf = new TTranslator(FileToLoad); return pSelf; } }; //-------------------------------------------------- TTranslator* TTranslator::pSelf = NULL; //-------------------------------------------------- #endifЛистинг 21.2. Класс TTranslator (файл TTranslator.h)
Теперь мы можем задать в качестве отображения (mapping) для перевода интерфейса программы посредством макроса TRANS строки, загруженные из файла перевода ( пример 21.3). Его название соответствует первым двум буквам локали, т.е. в случае, если языком операционной системы является русский (локаль ru_RU), то файл перевода будет называться "ru.lng". Разумеется, для имени файла вы можете выбрать и другое расширение.
#include "TCentralComponent.h" #include "TTranslator.h" #include <locale> //------------------------------------------------------ TCentralComponent::TCentralComponent() : Component ("Central Component"), pHelloLabel(0), pCloseButton(0) { // Получаем первые две буквы текущей локали String sLocale(setlocale(LC_ALL, "")); sLocale = sLocale.substring(0, 2); sLocale = sLocale.toLowerCase(); // Находим путь к исполняемому файлу нашей программы File Path = Path.getSpecialLocation(File::currentExecutableFile); // Получаем путь к программе String sPath = Path.getFullPathName(); sPath = sPath.trimCharactersAtEnd(Path.getFileName()); // Получаем полное имя файла перевода, исходя из текущей локали sPath += sLocale; sPath += ".lng"; // Загружаем строки файл перевода... File Translations(sPath); // и устанавливаем их в качестве отображения (mapping) LocalisedStrings::setCurrentMappings(TTranslator::pInstance(Translations)); // Переводим интерфейс программы с помощью макроса TRANS pHelloLabel = new Label("Hello Label", TRANS("Hello world!")); addAndMakeVisible(pHelloLabel); pHelloLabel->setFont(Font(38.0000f, Font::bold)); pHelloLabel->setJustificationType(Justification::centred); pHelloLabel->setEditable(false, false, false); pHelloLabel->setColour(Label::textColourId, Colours::blue); pHelloLabel->setColour(TextEditor::backgroundColourId, Colours::azure); pCloseButton = new TextButton("Close Button"); addAndMakeVisible(pCloseButton); pCloseButton->setButtonText(TRANS("Close")); pCloseButton->addListener(this); setSize (400, 200); }Листинг 21.3. Реализация конструктора класса компонента содержимого TCentralComponent (файл TCentralComponent.cpp)
Перевод строк интерфейса с помощью метода translate
Недостатком вышеприведённого примера является то, что интерфейс программы переводится только один раз; при этом невозможно выбирать язык приложения вручную, что неудобно в случаях, когда его требуется запускать на операционной системе с локалью, отличной от родного языка пользователя.
Данный недостаток исправлен во втором примере. В нём, как и в предыдущем, язык интерфейса по умолчанию выбирается на основании текущей локали операционной системы, однако есть возможность использовать и другие файлы перевода, выбирая соответствующий язык в группе радиокнопок ( рис. 21.2).
Смена языка интерфейса будет осуществляться в функции-члене класса компонента содержимого void TCentralComponent::LanguageChange(LocalisedStrings& sTr), принимающей в качестве параметра "переводчика" - объект класса LocalisedStrings ( пример 21.4).
void TCentralComponent::LanguageChange(LocalisedStrings& sTr) { // Переводим интерфейс приложения с помощью нового "переводчика" pLanguageGroup->setText(sTr.translate("Languages")); pHelloLabel->setText(sTr.translate(L"Hello world!"), false); pCloseButton->setButtonText(sTr.translate(L"Close")); }Листинг 21.4. Реализация метода LanguageChange класса компонента содержимого TCentralComponent (файл TCentralComponent.cpp)
Для облегчения создания нового "переводчика", исходя из пути к файлу перевода для того или иного языка, предусмотрим в нашем классе метод LocalisedStrings TCentralComponent::NewTranslator(String sPath) ( пример 21.5).
LocalisedStrings TCentralComponent::NewTranslator(String sPath) { // Получаем файл перевода, исходя из пути к нему File Translations(sPath); // Создаём новый "переводчик" LocalisedStrings sTr(Translations); return sTr; }Листинг 21.5. Реализация метода NewTranslator класса компонента содержимого TCentralComponent (файл TCentralComponent.cpp
Теперь мы можем выполнить собственно перевод интерфейса посредством нашего метода LanguageChange, который вызывается в конструкторе класса компонента содержимого, а также при выборе пользователем радиокнопки в группе "Языки" ( пример 21.6).
#include "TCentralComponent.h" #include <locale> //-------------------------------------------------- #define tr(s) String::fromUTF8(s) //-------------------------------------------------- TCentralComponent::TCentralComponent() : Component("Central Component"), pLanguageGroup(0), pEnglishButton(0), pRussianButton(0), pItalianButton(0), pHelloLabel(0), pCloseButton(0) { String sLocale(setlocale(LC_ALL, "")); sLocale = sLocale.substring(0, 2); sLocale = sLocale.toLowerCase(); File Path = Path.getSpecialLocation(File::currentExecutableFile); String sPath = Path.getFullPathName(); sPath = sPath.trimCharactersAtEnd(Path.getFileName()); sPath += sLocale; sPath += ".lng"; pLanguageGroup = new GroupComponent(L"LanguageGroup", L"Languages"); addAndMakeVisible(pLanguageGroup); pEnglishButton = new ToggleButton(L"English Button"); pEnglishButton->setButtonText(L"English"); pEnglishButton->setRadioGroupId(1234); pEnglishButton->addListener(this); addAndMakeVisible(pEnglishButton); // Если язык операционной системы английский, отмечаем радиокнопку if(sLocale == "en") pEnglishButton->setToggleState(true, false); pRussianButton = new ToggleButton(L"Russian Button"); pRussianButton->setButtonText(tr("Русский")); pRussianButton->setRadioGroupId(1234); pRussianButton->addListener(this); addAndMakeVisible(pRussianButton); // Если язык операционной системы русский, отмечаем радиокнопку if(sLocale == "ru") pRussianButton->setToggleState(true, false); pItalianButton = new ToggleButton(L"Italian Button"); pItalianButton->setButtonText(L"Italiano"); pItalianButton->setRadioGroupId(1234); pItalianButton->addListener(this); addAndMakeVisible(pItalianButton); // Если язык операционной системы итальянский, отмечаем радиокнопку if(sLocale == "it") pItalianButton->setToggleState(true, false); pHelloLabel = new Label(L"Hello Label", L"Hello world!"); pHelloLabel->setFont(Font (40.0000f, Font::bold)); pHelloLabel->setJustificationType(Justification::centred); pHelloLabel->setEditable(false, false, false); pHelloLabel->setColour(Label::textColourId, Colours::blue); pHelloLabel->setColour(Label::backgroundColourId, Colours::azure); addAndMakeVisible(pHelloLabel); pCloseButton = new TextButton(L"Close Button"); pCloseButton->addListener(this); addAndMakeVisible(pCloseButton); setSize(600, 250); // Создаём нового "переводчика" LocalisedStrings sTr = NewTranslator(sPath); // Смена языка интерфейса LanguageChange(sTr); } //--------------------------------------------- void TCentralComponent::buttonClicked(Button* pButton) { File Path = Path.getSpecialLocation(File::currentExecutableFile); String sPath = Path.getFullPathName(); sPath = sPath.trimCharactersAtEnd(Path.getFileName()); // Если нажата кнопка "Закрыть"... if(pButton == pCloseButton) { // выходим из программы JUCEApplication::quit(); } // Задаём путь к файлу перевода, // исходя из выбранного пользователем языка интерфейса else if(pButton == pEnglishButton) { sPath += "en.lng"; } else if(pButton == pRussianButton) { sPath += "ru.lng"; } else if(pButton == pItalianButton) { sPath += "it.lng"; } // Создаём нового "переводчика"... LocalisedStrings sTr = NewTranslator(sPath); // ...и с его помощью выполняем перевод LanguageChange(sTr); }//------------------------------------------------------Листинг 21.6. Часть реализация класса компонента содержимого TCentralComponent (файл TCentralComponent.cpp)
Понятно, что подобным образом можно реализовать динамически меняющееся меню "Языки", в котором новые пункты будут появляться автоматически при добавлении новых файлов перевода в заданный каталог. При этом названия языков — пунктов меню можно получать непосредственно из файлов перевода посредством метода const String LocalisedStrings::getLanguageName() const. Кроме того, в распространяемых приложениях необходимо предусмотреть обработку ситуации, когда первые буквы локали и названия целевого языка не совпадают (например, pl_PL и Polsky).
Краткие итоги
Для локализации приложения в Juce используется класс LocalisedStrins. Для перевода строк интерфейса программы можно либо обратиться к методу translateWithCurrentMappings класса (макрос TRANS), либо к его же методу translate.
Упражнение
Напишите простейший текстовый редактор с меню, включая контекстное и панелью инструментов. Предусмотрите возможность динамической (на этапе выполнения программы) смены языка её интерфейса.
Дополнительные материалы
Архив с исходными текстами примеров Вы можете скачать здесь