Add framework for translating ZNC to different languages

This commit is contained in:
Alexey Sokolov
2016-01-21 08:19:20 +00:00
parent 10785ee90e
commit 8eeeaf71a0
26 changed files with 797 additions and 54 deletions
+10 -4
View File
@@ -19,13 +19,13 @@ if(CMAKE_VERSION VERSION_LESS 3.2)
set_source_files_properties("versionc.cpp" PROPERTIES GENERATED true)
endif()
znc_add_library(znclib ${lib_type} "ZNCString.cpp" "znc.cpp" "IRCNetwork.cpp"
set(znc_cpp "ZNCString.cpp" "znc.cpp" "IRCNetwork.cpp" "Translation.cpp"
"IRCSock.cpp" "Client.cpp" "Chan.cpp" "Nick.cpp" "Server.cpp"
"Modules.cpp" "MD5.cpp" "Buffer.cpp" "Utils.cpp" "FileUtils.cpp"
"HTTPSock.cpp" "Template.cpp" "ClientCommand.cpp" "Socket.cpp"
"SHA256.cpp" "WebModules.cpp" "Listener.cpp" "Config.cpp" "ZNCDebug.cpp"
"Threads.cpp" "Query.cpp" "SSLVerifyHost.cpp" "Message.cpp" "Csocket.cpp"
"versionc.cpp" "User.cpp")
"Threads.cpp" "Query.cpp" "SSLVerifyHost.cpp" "Message.cpp" "User.cpp")
znc_add_library(znclib ${lib_type} ${znc_cpp} "Csocket.cpp" "versionc.cpp")
znc_add_executable(znc "main.cpp")
target_link_libraries(znc PRIVATE znclib)
@@ -76,6 +76,10 @@ if(ICU_FOUND)
target_link_libraries(znclib ${ICU_LDFLAGS})
list(APPEND znc_include_dirs ${ICU_INCLUDE_DIRS})
endif()
if(Boost_FOUND)
target_link_libraries(znclib ${Boost_LIBRARIES})
list(APPEND znc_include_dirs ${Boost_INCLUDE_DIRS})
endif()
target_include_directories(znc PUBLIC ${znc_include_dirs})
target_include_directories(znclib PUBLIC ${znc_include_dirs})
@@ -91,7 +95,9 @@ set_target_properties(znclib PROPERTIES
OUTPUT_NAME "znc"
SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
if(HAVE_I18N)
add_subdirectory(po)
endif()
install(TARGETS znc ${install_lib}
+1
View File
@@ -94,6 +94,7 @@ void CClient::SendRequiredPasswordNotice() {
}
void CClient::ReadLine(const CString& sData) {
CLanguageScope user_lang(GetUser() ? GetUser()->GetLanguage() : "");
CString sLine = sData;
sLine.TrimRight("\n\r");
+61 -21
View File
@@ -138,6 +138,7 @@ CModule::CModule(ModHandle pDLL, CUser* pUser, CIRCNetwork* pNetwork,
m_sSavePath(""),
m_sArgs(""),
m_sModPath(""),
m_Translation("znc-" + sModName),
m_mssRegistry(),
m_vSubPages(),
m_mCommands() {
@@ -516,6 +517,13 @@ bool CModule::AddCommand(const CString& sCmd, const CString& sArgs,
return AddCommand(std::move(cmd));
}
bool CModule::AddCommand(const CString& sCmd, const CString& sArgs,
const CDelayedTranslation& dDesc,
std::function<void(const CString& sLine)> func) {
CModCommand cmd(sCmd, std::move(func), sArgs, dDesc);
return AddCommand(std::move(cmd));
}
void CModule::AddHelpCommand() {
AddCommand("Help", &CModule::HandleHelpCommand, "search",
"Generate this output");
@@ -1595,6 +1603,8 @@ bool CModules::LoadModule(const CString& sModule, const CString& sArgs,
sRetMsg = "Unable to find module [" + sModule + "]";
return false;
}
Info.SetName(sModule);
Info.SetPath(sModPath);
ModHandle p =
OpenModule(sModule, sModPath, bVersionMismatch, Info, sRetMsg);
@@ -1754,14 +1764,13 @@ bool CModules::GetModPathInfo(CModInfo& ModInfo, const CString& sModule,
const CString& sModPath, CString& sRetMsg) {
bool bVersionMismatch;
ModHandle p =
OpenModule(sModule, sModPath, bVersionMismatch, ModInfo, sRetMsg);
if (!p) return false;
ModInfo.SetName(sModule);
ModInfo.SetPath(sModPath);
ModHandle p =
OpenModule(sModule, sModPath, bVersionMismatch, ModInfo, sRetMsg);
if (!p) return false;
if (bVersionMismatch) {
ModInfo.SetDescription(
"--- Version mismatch, recompile this module. ---");
@@ -1910,6 +1919,7 @@ ModHandle CModules::OpenModule(const CString& sModule, const CString& sModPath,
return nullptr;
}
CTranslationDomainRefHolder translation("znc-" + sModule);
typedef bool (*InfoFP)(double, CModInfo&);
InfoFP ZNCModInfo = (InfoFP)dlsym(p, "ZNCModInfo");
@@ -1930,32 +1940,32 @@ ModHandle CModules::OpenModule(const CString& sModule, const CString& sModPath,
return p;
}
CModCommand::CModCommand() : m_sCmd(), m_pFunc(nullptr), m_sArgs(), m_sDesc() {}
CModCommand::CModCommand()
: m_sCmd(), m_pFunc(nullptr), m_sArgs(), m_sDesc(), m_bTranslating(false) {}
CModCommand::CModCommand(const CString& sCmd, CModule* pMod, ModCmdFunc func,
const CString& sArgs, const CString& sDesc)
: m_sCmd(sCmd),
m_pFunc([pMod, func](const CString& sLine) { (pMod->*func)(sLine); }),
m_sArgs(sArgs),
m_sDesc(sDesc) {}
m_sDesc(sDesc),
m_bTranslating(false) {}
CModCommand::CModCommand(const CString& sCmd, CmdFunc func,
const CString& sArgs, const CString& sDesc)
: m_sCmd(sCmd), m_pFunc(std::move(func)), m_sArgs(sArgs), m_sDesc(sDesc) {}
: m_sCmd(sCmd),
m_pFunc(std::move(func)),
m_sArgs(sArgs),
m_sDesc(sDesc),
m_bTranslating(false) {}
CModCommand::CModCommand(const CModCommand& other)
: m_sCmd(other.m_sCmd),
m_pFunc(other.m_pFunc),
m_sArgs(other.m_sArgs),
m_sDesc(other.m_sDesc) {}
CModCommand& CModCommand::operator=(const CModCommand& other) {
m_sCmd = other.m_sCmd;
m_pFunc = other.m_pFunc;
m_sArgs = other.m_sArgs;
m_sDesc = other.m_sDesc;
return *this;
}
CModCommand::CModCommand(const CString& sCmd, CmdFunc func,
const CString& sArgs, const CDelayedTranslation& dDesc)
: m_sCmd(sCmd),
m_pFunc(std::move(func)),
m_sArgs(sArgs),
m_dDesc(dDesc),
m_bTranslating(true) {}
void CModCommand::InitHelp(CTable& Table) {
Table.AddColumn("Command");
@@ -1967,3 +1977,33 @@ void CModCommand::AddHelp(CTable& Table) const {
Table.SetCell("Command", GetCommand() + " " + GetArgs());
Table.SetCell("Description", GetDescription());
}
CString CModCommand::GetDescription() const {
return m_bTranslating ? m_dDesc.Resolve() : m_sDesc;
}
CString CModule::t(const CString& sEnglish, const CString& sContext) const {
return CTranslation::Get().Singular("znc-" + GetModName(), sContext,
sEnglish);
}
CInlineFormatMessage CModule::f(const CString& sEnglish,
const CString& sContext) const {
return CInlineFormatMessage(t(sEnglish, sContext));
}
CInlineFormatMessage CModule::p(const CString& sEnglish,
const CString& sEnglishes, int iNum,
const CString& sContext) const {
return CInlineFormatMessage(CTranslation::Get().Plural(
"znc-" + GetModName(), sContext, sEnglish, sEnglishes, iNum));
}
CDelayedTranslation CModule::d(const CString& sEnglish,
const CString& sContext) const {
return CDelayedTranslation("znc-" + GetModName(), sContext, sEnglish);
}
CString CModInfo::t(const CString& sEnglish, const CString& sContext) const {
return CTranslation::Get().Singular("znc-" + GetName(), sContext, sEnglish);
}
+21
View File
@@ -607,3 +607,24 @@ void CIRCSocket::IcuExtFromUCallback(UConverterFromUnicodeArgs* fromArgs,
err);
}
#endif
CString CSocket::t(const CString& sEnglish, const CString& sContext) const {
return GetModule()->t(sEnglish, sContext);
}
CInlineFormatMessage CSocket::f(const CString& sEnglish,
const CString& sContext) const {
return GetModule()->f(sEnglish, sContext);
}
CInlineFormatMessage CSocket::p(const CString& sEnglish,
const CString& sEnglishes, int iNum,
const CString& sContext) const {
return GetModule()->p(sEnglish, sEnglishes, iNum, sContext);
}
CDelayedTranslation CSocket::d(const CString& sEnglish,
const CString& sContext) const {
return GetModule()->d(sEnglish, sContext);
}
+55 -3
View File
@@ -17,6 +17,7 @@
#include <znc/Template.h>
#include <znc/FileUtils.h>
#include <znc/ZNCDebug.h>
#include <znc/Translation.h>
#include <algorithm>
using std::stringstream;
@@ -313,6 +314,10 @@ bool CTemplate::Print(const CString& sFileName, ostream& oOut) {
bool bLoopBreak = false;
bool bExit = false;
// Single template works across multiple translation domains, e.g. .tmpl of
// a module can INC'lude Footer.tmpl from core
CString sI18N;
while (File.ReadLine(sLine)) {
CString sOutput;
bool bFoundATag = false;
@@ -420,6 +425,12 @@ bool CTemplate::Print(const CString& sFileName, ostream& oOut) {
} else if (sAction.Equals("SETBLOCK")) {
sSetBlockVar = sArgs;
bInSetBlock = true;
} else if (sAction.Equals("ENDSETBLOCK")) {
CString sName = sSetBlockVar.Token(0);
(*this)[sName] += sOutput;
sOutput = "";
bInSetBlock = false;
sSetBlockVar = "";
} else if (sAction.Equals("EXPAND")) {
sOutput += ExpandFile(sArgs, true);
} else if (sAction.Equals("VAR")) {
@@ -536,6 +547,50 @@ bool CTemplate::Print(const CString& sFileName, ostream& oOut) {
}
} else if (sAction.Equals("REM")) {
uSkip++;
} else if (sAction.Equals("I18N")) {
sI18N = sArgs;
} else if (sAction.Equals("FORMAT") ||
sAction.Equals("PLURAL")) {
bool bHaveContext = false;
if (sArgs.TrimPrefix("CTX=")) {
bHaveContext = true;
}
VCString vsArgs;
sArgs.QuoteSplit(vsArgs);
CString sEnglish, sEnglishes, sContext;
int idx = 0;
if (bHaveContext && vsArgs.size() > idx) {
sContext = vsArgs[idx];
idx++;
}
if (vsArgs.size() > idx) {
sEnglish = vsArgs[idx];
idx++;
}
CString sFormat;
if (sAction.Equals("PLURAL")) {
CString sEnglishes;
int iNum = 0;
if (vsArgs.size() > idx) {
sEnglishes = vsArgs[idx];
idx++;
}
if (vsArgs.size() > idx) {
iNum = GetValue(vsArgs[idx], true).ToInt();
idx++;
}
sFormat = CTranslation::Get().Plural(
sI18N, sContext, sEnglish, sEnglishes, iNum);
} else {
sFormat = CTranslation::Get().Singular(
sI18N, sContext, sEnglish);
}
MCString msParams;
for (int i = 0; i + idx < vsArgs.size(); ++i) {
msParams[CString(i + 1)] =
GetValue(vsArgs[i + idx], false);
}
sOutput += CString::NamedFormat(sFormat, msParams);
} else {
bNotFound = true;
}
@@ -557,9 +612,6 @@ bool CTemplate::Print(const CString& sFileName, ostream& oOut) {
if (uSkip) {
uSkip--;
}
} else if (sAction.Equals("ENDSETBLOCK")) {
bInSetBlock = false;
sSetBlockVar = "";
} else if (sAction.Equals("ENDLOOP")) {
if (bLoopCont && uSkip == 1) {
uSkip--;
-2
View File
@@ -29,8 +29,6 @@ static const size_t MAX_IDLE_THREADS = 3;
static const size_t MAX_TOTAL_THREADS = 20;
CThreadPool& CThreadPool::Get() {
// Beware! The following is not thread-safe! This function must
// be called once any thread is started.
static CThreadPool pool;
return pool;
}
+128
View File
@@ -0,0 +1,128 @@
/*
* Copyright (C) 2004-2016 ZNC, see the NOTICE file for details.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <znc/Translation.h>
#ifdef HAVE_I18N
#include <boost/locale.hpp>
#endif
CTranslation& CTranslation::Get() {
static CTranslation translation;
return translation;
}
CString CTranslation::Singular(const CString& sDomain, const CString& sContext,
const CString& sEnglish) {
#ifdef HAVE_I18N
const std::locale& loc = LoadTranslation(sDomain);
return boost::locale::translate(sContext, sEnglish).str(loc);
#else
return sEnglish;
#endif
}
CString CTranslation::Plural(const CString& sDomain, const CString& sContext,
const CString& sEnglish, const CString& sEnglishes,
int iNum) {
#ifdef HAVE_I18N
const std::locale& loc = LoadTranslation(sDomain);
return boost::locale::translate(sContext, sEnglish, sEnglishes, iNum)
.str(loc);
#else
if (iNum == 1) {
return sEnglish;
} else {
return sEnglishes;
}
#endif
}
const std::locale& CTranslation::LoadTranslation(const CString& sDomain) {
CString sLanguage = m_sLanguageStack.empty() ? "" : m_sLanguageStack.back();
#ifdef HAVE_I18N
// Not using built-in support for multiple domains in single std::locale
// via overloaded call to .str() because we need to be able to reload
// translations from disk independently when a module gets updated
auto& domain = m_Translations[sDomain];
auto lang_it = domain.find(sLanguage);
if (lang_it == domain.end()) {
boost::locale::generator gen;
gen.add_messages_path(LOCALE_DIR);
gen.add_messages_domain(sDomain);
std::tie(lang_it, std::ignore) =
domain.emplace(sLanguage, gen(sLanguage + ".UTF-8"));
}
return lang_it->second;
#else
// dummy, it's not used anyway
return std::locale::classic();
#endif
}
void CTranslation::PushLanguage(const CString& sLanguage) {
m_sLanguageStack.push_back(sLanguage);
}
void CTranslation::PopLanguage() { m_sLanguageStack.pop_back(); }
void CTranslation::NewReference(const CString& sDomain) {
m_miReferences[sDomain]++;
}
void CTranslation::DelReference(const CString& sDomain) {
if (!--m_miReferences[sDomain]) {
m_Translations.erase(sDomain);
}
}
CString CCoreTranslationMixin::t(const CString& sEnglish,
const CString& sContext) {
return CTranslation::Get().Singular("znc", sContext, sEnglish);
}
CInlineFormatMessage CCoreTranslationMixin::f(const CString& sEnglish,
const CString& sContext) {
return CInlineFormatMessage(t(sEnglish, sContext));
}
CInlineFormatMessage CCoreTranslationMixin::p(const CString& sEnglish,
const CString& sEnglishes,
int iNum,
const CString& sContext) {
return CInlineFormatMessage(CTranslation::Get().Plural(
"znc", sContext, sEnglish, sEnglishes, iNum));
}
CDelayedTranslation CCoreTranslationMixin::d(const CString& sEnglish,
const CString& sContext) {
return CDelayedTranslation("znc", sContext, sEnglish);
}
CLanguageScope::CLanguageScope(const CString& sLanguage) {
CTranslation::Get().PushLanguage(sLanguage);
}
CLanguageScope::~CLanguageScope() { CTranslation::Get().PopLanguage(); }
CString CDelayedTranslation::Resolve() const {
return CTranslation::Get().Singular(m_sDomain, m_sContext, m_sEnglish);
}
CTranslationDomainRefHolder::CTranslationDomainRefHolder(const CString& sDomain)
: m_sDomain(sDomain) {
CTranslation::Get().NewReference(sDomain);
}
CTranslationDomainRefHolder::~CTranslationDomainRefHolder() {
CTranslation::Get().DelReference(m_sDomain);
}
+18
View File
@@ -272,6 +272,9 @@ bool CUser::ParseConfig(CConfig* pConfig, CString& sError) {
}
}
}
if (pConfig->FindStringEntry("language", sValue)) {
SetLanguage(sValue);
}
pConfig->FindStringEntry("pass", sValue);
// There are different formats for this available:
// Pass = <plain text>
@@ -749,6 +752,7 @@ bool CUser::Clone(const CUser& User, CString& sErrorRet, bool bCloneNetworks) {
SetMaxQueryBuffers(User.MaxQueryBuffers());
SetMaxJoins(User.MaxJoins());
SetClientEncoding(User.GetClientEncoding());
SetLanguage(User.GetLanguage());
// Allowed Hosts
m_ssAllowedHosts.clear();
@@ -1057,6 +1061,7 @@ CConfig CUser::ToConfig() const {
config.AddKeyValuePair("MaxQueryBuffers", CString(m_uMaxQueryBuffers));
config.AddKeyValuePair("MaxJoins", CString(m_uMaxJoins));
config.AddKeyValuePair("ClientEncoding", GetClientEncoding());
config.AddKeyValuePair("Language", GetLanguage());
// Allow Hosts
if (!m_ssAllowedHosts.empty()) {
@@ -1398,6 +1403,18 @@ bool CUser::SetStatusPrefix(const CString& s) {
return false;
}
bool CUser::SetLanguage(const CString& s) {
// They look like ru_RU
for (char c : s) {
if (isalpha(c) || c == '_') {
} else {
return false;
}
}
m_sLanguage = s;
return true;
}
// !Setters
// Getters
@@ -1462,6 +1479,7 @@ bool CUser::AutoClearQueryBuffer() const { return m_bAutoClearQueryBuffer; }
// CString CUser::GetSkinName() const { return (!m_sSkinName.empty()) ?
// m_sSkinName : CZNC::Get().GetSkinName(); }
CString CUser::GetSkinName() const { return m_sSkinName; }
CString CUser::GetLanguage() const { return m_sLanguage; }
const CString& CUser::GetUserPath() const {
if (!CFile::Exists(m_sUserPath)) {
CDir::MakeDir(m_sUserPath);
+2
View File
@@ -672,6 +672,8 @@ CWebSock::EPageReqResult CWebSock::OnPageRequestInternal(const CString& sURI,
m_sUser = GetSession()->GetUser()->GetUserName();
m_bLoggedIn = true;
}
CLanguageScope user_language(
m_bLoggedIn ? GetSession()->GetUser()->GetLanguage() : "");
// Handle the static pages that don't require a login
if (sURI == "/") {
+25
View File
@@ -0,0 +1,25 @@
#
# Copyright (C) 2004-2016 ZNC, see the NOTICE file for details.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
include(translation)
set(tmpl_dirs)
file(GLOB skins "${PROJECT_SOURCE_DIR}/webskins/*")
foreach(skin ${skins})
list(APPEND tmpl_dirs "${skin}/tmpl")
endforeach()
translation(SHORT "znc" FULL "znc" SOURCES ${znc_cpp}
TMPLDIRS ${tmpl_dirs})
+12 -4
View File
@@ -75,7 +75,8 @@ CZNC::CZNC()
m_uiForceEncoding(0),
m_sConnectThrottle(),
m_bProtectWebSessions(true),
m_bHideVersion(false) {
m_bHideVersion(false),
m_Translation("znc") {
if (!InitCsocket()) {
CUtils::PrintError("Could not initialize Csocket!");
exit(-1);
@@ -160,11 +161,17 @@ CString CZNC::GetCompileOptionsString() {
#else
"no"
#endif
", build: "
", build: "
#ifdef BUILD_WITH_CMAKE
"cmake"
"cmake"
#else
"autoconf"
"autoconf"
#endif
", i18n: "
#ifdef HAVE_I18N
"yes"
#else
"no"
#endif
;
}
@@ -1419,6 +1426,7 @@ void CZNC::Broadcast(const CString& sMessage, bool bAdminOnly, CUser* pSkipUser,
if (bAdminOnly && !it.second->IsAdmin()) continue;
if (it.second != pSkipUser) {
// TODO: translate message to user's language
CString sMsg = sMessage;
bool bContinue = false;