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

View File

@@ -32,6 +32,10 @@ function(znc_add_executable name)
add_executable("${name}" ${ARGN})
set(_all_targets "${_all_targets};${name}" CACHE INTERNAL "")
endfunction()
function(znc_add_custom_target name)
add_custom_target("${name}" ${ARGN})
set(_all_targets "${_all_targets};${name}" CACHE INTERNAL "")
endfunction()
list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
@@ -175,6 +179,24 @@ if(WANT_TCL)
endif()
endif()
tristate_option(I18N "Native language support (i18n)")
if(WANT_I18N)
find_package(Boost ${TRISTATE_I18N_REQUIRED} COMPONENTS locale)
find_package(Gettext ${TRISTATE_I18N_REQUIRED})
endif()
if(Boost_LOCALE_FOUND AND GETTEXT_MSGFMT_EXECUTABLE)
set(HAVE_I18N true)
else()
set(HAVE_I18N false)
endif()
if(HAVE_I18N AND GETTEXT_MSGMERGE_EXECUTABLE)
find_program(XGETTEXT_EXECUTABLE xgettext)
if(XGETTEXT_EXECUTABLE)
add_custom_target(translation)
endif()
endif()
# poll() is broken on Mac OS, it fails with POLLNVAL for pipe()s.
if(APPLE)
set(CSOCK_USE_POLL false)
@@ -321,6 +343,7 @@ summary_line("Tcl " "${TCL_FOUND}")
summary_line("Cyrus " "${CYRUS_FOUND}")
summary_line("Charset " "${ICU_FOUND}")
summary_line("Zlib " "${ZLIB_FOUND}")
summary_line("i18n " "${HAVE_I18N}")
include(render_framed_multiline)
render_framed_multiline("${summary_lines}")

View File

@@ -53,7 +53,7 @@ LIB_SRCS := ZNCString.cpp Csocket.cpp znc.cpp IRCNetwork.cpp User.cpp IRCSock.c
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 version.cpp Query.cpp \
SSLVerifyHost.cpp Message.cpp
SSLVerifyHost.cpp Message.cpp Translation.cpp
LIB_SRCS := $(addprefix src/,$(LIB_SRCS))
BIN_SRCS := src/main.cpp
LIB_OBJS := $(patsubst %cpp,%o,$(LIB_SRCS))

75
cmake/translation.cmake Normal file
View File

@@ -0,0 +1,75 @@
#
# 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(CMakeParseArguments)
function(translation)
cmake_parse_arguments(arg "" "FULL;SHORT" "SOURCES;TMPLDIRS" ${ARGN})
set(short "${arg_SHORT}")
file(GLOB all_po "${short}.*.po")
if(XGETTEXT_EXECUTABLE)
set(params)
foreach(i ${arg_SOURCES})
list(APPEND params "--explicit_sources=${i}")
endforeach()
foreach(i ${arg_TMPLDIRS})
list(APPEND params "--tmpl_dirs=${i}")
endforeach()
add_custom_target("translation_${short}"
COMMAND "${PROJECT_SOURCE_DIR}/translation_pot.py"
"--include_dir=${CMAKE_CURRENT_SOURCE_DIR}/.."
"--strip_prefix=${PROJECT_SOURCE_DIR}/"
"--tmp_prefix=${CMAKE_CURRENT_BINARY_DIR}/${short}"
"--output=${CMAKE_CURRENT_SOURCE_DIR}/${short}.pot"
${params}
VERBATIM)
foreach(one_po ${all_po})
add_custom_command(TARGET "translation_${short}" POST_BUILD
COMMAND "${GETTEXT_MSGMERGE_EXECUTABLE}"
--update --quiet --backup=none "${one_po}" "${short}.pot"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
VERBATIM)
endforeach()
add_dependencies(translation "translation_${short}")
endif()
if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${short}.pot")
return()
endif()
znc_add_custom_target("po_${short}")
foreach(one_po ${all_po})
get_filename_component(longext "${one_po}" EXT)
if(NOT longext MATCHES "^\\.([a-zA-Z_]+)\\.po$")
message(WARNING "Unrecognized translation file ${one_po}")
continue()
endif()
set(lang "${CMAKE_MATCH_1}")
add_custom_command(OUTPUT "${short}.${lang}.gmo"
COMMAND "${GETTEXT_MSGFMT_EXECUTABLE}"
-D "${CMAKE_CURRENT_SOURCE_DIR}"
-o "${short}.${lang}.gmo"
"${short}.${lang}.po"
DEPENDS "${short}.${lang}.po"
VERBATIM)
add_custom_target("po_${short}_${lang}" DEPENDS "${short}.${lang}.gmo")
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${short}.${lang}.gmo"
DESTINATION "${CMAKE_INSTALL_LOCALEDIR}/${lang}/LC_MESSAGES"
RENAME "${arg_FULL}.mo")
add_dependencies("po_${short}" "po_${short}_${lang}")
endforeach()
endfunction()

54
cmake/translation_tmpl.py Executable file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
#
# 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.
#
import argparse
import glob
import os
import re
parser = argparse.ArgumentParser(
description='Extract translateable strings from .tmpl files')
parser.add_argument('--directory', action='store')
parser.add_argument('--output', action='store')
args = parser.parse_args()
pattern = re.compile(r'<\?\s*(?:FORMAT|(PLURAL))\s+(?:CTX="([^"]+?)"\s+)?"([^"]+?)"(?(1)\s+"([^"]+?)"|).*?\?>')
result = []
for fname in glob.iglob(args.directory + '/*.tmpl'):
fbase = os.path.basename(fname)
with open(fname) as f:
for linenum, line in enumerate(f):
for x in pattern.finditer(line):
text, plural, context = x.group(3), x.group(4), x.group(2)
result.append('#: {}:{}'.format(fbase, linenum + 1))
if context:
result.append('msgctxt "{}"'.format(context))
result.append('msgid "{}"'.format(text))
if plural:
result.append('msgid_plural "{}"'.format(plural))
result.append('msgstr[0] ""')
result.append('msgstr[1] ""')
else:
result.append('msgstr ""')
result.append('')
if result:
with open(args.output, 'w') as f:
for line in result:
print(line, file=f)

View File

@@ -82,6 +82,7 @@ tristate('swig')
tristate('cyrus')
tristate('charset', 'ICU')
tristate('tcl')
tristate('i18n')
class HandlePython(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):

View File

@@ -23,6 +23,7 @@
#include <znc/Threads.h>
#include <znc/Message.h>
#include <znc/main.h>
#include <znc/Translation.h>
#include <functional>
#include <set>
#include <queue>
@@ -58,20 +59,24 @@ class CModInfo;
#define ZNC_EXPORT_LIB_EXPORT
#endif
#define MODCOMMONDEFS(CLASS, DESCRIPTION, TYPE) \
extern "C" { \
ZNC_EXPORT_LIB_EXPORT bool ZNCModInfo(double dCoreVersion, \
CModInfo& Info); \
ZNC_EXPORT_LIB_EXPORT bool ZNCModInfo(double dCoreVersion, \
CModInfo& Info) { \
if (dCoreVersion != VERSION) return false; \
Info.SetDescription(DESCRIPTION); \
Info.SetDefaultType(TYPE); \
Info.AddType(TYPE); \
Info.SetLoader(TModLoad<CLASS>); \
TModInfo<CLASS>(Info); \
return true; \
} \
#define MODCOMMONDEFS(CLASS, DESCRIPTION, TYPE) \
extern "C" { \
ZNC_EXPORT_LIB_EXPORT bool ZNCModInfo(double dCoreVersion, \
CModInfo& Info); \
ZNC_EXPORT_LIB_EXPORT bool ZNCModInfo(double dCoreVersion, \
CModInfo& Info) { \
if (dCoreVersion != VERSION) return false; \
auto t = [&](const CString& sEnglish, const CString& sContext = "") { \
return sEnglish.empty() ? "" : Info.t(sEnglish, sContext); \
}; \
t(CString()); /* Don't warn about unused t */ \
Info.SetDescription(DESCRIPTION); \
Info.SetDefaultType(TYPE); \
Info.AddType(TYPE); \
Info.SetLoader(TModLoad<CLASS>); \
TModInfo<CLASS>(Info); \
return true; \
} \
}
/** Instead of writing a constructor, you should call this macro. It accepts all
@@ -273,6 +278,9 @@ class CModInfo {
void SetLoader(ModLoader fLoader) { m_fLoader = fLoader; }
void SetDefaultType(EModuleType eType) { m_eDefaultType = eType; }
// !Setters
CString t(const CString& sEnglish, const CString& sContext = "") const;
private:
protected:
std::set<EModuleType> m_seType;
@@ -316,16 +324,18 @@ class CModCommand {
const CString& sArgs, const CString& sDesc);
CModCommand(const CString& sCmd, CmdFunc func, const CString& sArgs,
const CString& sDesc);
CModCommand(const CString& sCmd, CmdFunc func, const CString& sArgs,
const CDelayedTranslation& dDesc);
/** Copy constructor, needed so that this can be saved in a std::map.
* @param other Object to copy from.
*/
CModCommand(const CModCommand& other);
CModCommand(const CModCommand& other) = default;
/** Assignment operator, needed so that this can be saved in a std::map.
* @param other Object to copy from.
*/
CModCommand& operator=(const CModCommand& other);
CModCommand& operator=(const CModCommand& other) = default;
/** Initialize a CTable so that it can be used with AddHelp().
* @param Table The instance of CTable to initialize.
@@ -341,7 +351,7 @@ class CModCommand {
const CString& GetCommand() const { return m_sCmd; }
CmdFunc GetFunction() const { return m_pFunc; }
const CString& GetArgs() const { return m_sArgs; }
const CString& GetDescription() const { return m_sDesc; }
CString GetDescription() const;
void Call(const CString& sLine) const { m_pFunc(sLine); }
@@ -350,6 +360,8 @@ class CModCommand {
CmdFunc m_pFunc;
CString m_sArgs;
CString m_sDesc;
CDelayedTranslation m_dDesc;
bool m_bTranslating;
};
/** The base class for your own ZNC modules.
@@ -1093,6 +1105,10 @@ class CModule {
bool AddCommand(const CString& sCmd, const CString& sArgs,
const CString& sDesc,
std::function<void(const CString& sLine)> func);
/// @return True if the command was successfully added.
bool AddCommand(const CString& sCmd, const CString& sArgs,
const CDelayedTranslation& dDesc,
std::function<void(const CString& sLine)> func);
/// @return True if the command was successfully removed.
bool RemCommand(const CString& sCmd);
/// @return The CModCommand instance or nullptr if none was found.
@@ -1267,6 +1283,17 @@ class CModule {
CModInfo::EModuleType eType);
// !Global Modules
#ifndef SWIG
// Translation
CString t(const CString& sEnglish, const CString& sContext = "") const;
CInlineFormatMessage f(const CString& sEnglish,
const CString& sContext = "") const;
CInlineFormatMessage p(const CString& sEnglish, const CString& sEnglishes,
int iNum, const CString& sContext = "") const;
CDelayedTranslation d(const CString& sEnglish,
const CString& sContext = "") const;
#endif
protected:
CModInfo::EModuleType m_eType;
CString m_sDescription;
@@ -1285,6 +1312,7 @@ class CModule {
CString m_sSavePath;
CString m_sArgs;
CString m_sModPath;
CTranslationDomainRefHolder m_Translation;
private:
MCString

View File

@@ -20,10 +20,11 @@
#include <znc/zncconfig.h>
#include <znc/Csocket.h>
#include <znc/Threads.h>
#include <znc/Translation.h>
class CModule;
class CZNCSock : public Csock {
class CZNCSock : public Csock, public CCoreTranslationMixin {
public:
CZNCSock(int timeout = 60);
CZNCSock(const CString& sHost, u_short port, int timeout = 60);
@@ -267,6 +268,18 @@ class CSocket : public CZNCSock {
// Getters
CModule* GetModule() const;
// !Getters
#ifndef SWIG
// Translation. As opposed to CCoreTranslationMixin, this one uses module.mo
CString t(const CString& sEnglish, const CString& sContext = "") const;
CInlineFormatMessage f(const CString& sEnglish,
const CString& sContext = "") const;
CInlineFormatMessage p(const CString& sEnglish, const CString& sEnglishes,
int iNum, const CString& sContext) const;
CDelayedTranslation d(const CString& sEnglish,
const CString& sContext = "") const;
#endif
private:
protected:
CModule*

91
include/znc/Translation.h Normal file
View File

@@ -0,0 +1,91 @@
/*
* 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.
*/
#ifndef ZNC_TRANSLATION_H
#define ZNC_TRANSLATION_H
#include <znc/ZNCString.h>
#include <unordered_map>
// All instances of modules share single message map using this class stored in
// CZNC.
class CTranslation {
public:
static CTranslation& Get();
CString Singular(const CString& sDomain, const CString& sContext,
const CString& sEnglish);
CString Plural(const CString& sDomain, const CString& sContext,
const CString& sEnglish, const CString& sEnglishes,
int iNum);
void PushLanguage(const CString& sLanguage);
void PopLanguage();
void NewReference(const CString& sDomain);
void DelReference(const CString& sDomain);
private:
const std::locale& LoadTranslation(const CString& sDomain);
std::unordered_map<CString /* domain */,
std::unordered_map<CString /* language */, std::locale>>
m_Translations;
VCString m_sLanguageStack;
std::unordered_map<CString /* domain */, int> m_miReferences;
};
struct CLanguageScope {
explicit CLanguageScope(const CString& sLanguage);
~CLanguageScope();
};
struct CTranslationDomainRefHolder {
explicit CTranslationDomainRefHolder(const CString& sDomain);
~CTranslationDomainRefHolder();
private:
CString m_sDomain;
};
// This is inspired by boost::locale::message, but without boost
class CDelayedTranslation {
public:
CDelayedTranslation() = default;
CDelayedTranslation(const CString& sDomain, const CString& sContext,
const CString& sEnglish)
: m_sDomain(sDomain), m_sContext(sContext), m_sEnglish(sEnglish) {}
CString Resolve() const;
private:
CString m_sDomain;
CString m_sContext;
CString m_sEnglish;
};
// Used by everything except modules.
// CModule defines its own version of these functions.
class CCoreTranslationMixin {
protected:
static CString t(const CString& sEnglish, const CString& sContext = "");
static CInlineFormatMessage f(const CString& sEnglish,
const CString& sContext = "");
static CInlineFormatMessage p(const CString& sEnglish,
const CString& sEnglishes, int iNum,
const CString& sContext = "");
static CDelayedTranslation d(const CString& sEnglish,
const CString& sContext = "");
};
#endif

View File

@@ -143,6 +143,7 @@ class CUser {
bool SetQueryBufferSize(unsigned int u, bool bForce = false);
void SetAutoClearChanBuffer(bool b);
void SetAutoClearQueryBuffer(bool b);
bool SetLanguage(const CString& s);
void SetBeingDeleted(bool b) { m_bBeingDeleted = b; }
void SetTimestampFormat(const CString& s) { m_sTimestampFormat = s; }
@@ -200,6 +201,7 @@ class CUser {
unsigned int JoinTries() const { return m_uMaxJoinTries; }
unsigned int MaxJoins() const { return m_uMaxJoins; }
CString GetSkinName() const;
CString GetLanguage() const;
unsigned int MaxNetworks() const { return m_uMaxNetworks; }
unsigned int MaxQueryBuffers() const { return m_uMaxQueryBuffers; }
// !Getters
@@ -253,6 +255,7 @@ class CUser {
unsigned int m_uMaxQueryBuffers;
unsigned int m_uMaxJoins;
CString m_sSkinName;
CString m_sLanguage;
CModules* m_pModules;

View File

@@ -663,4 +663,38 @@ class MCString : public std::map<CString, CString> {
virtual CString& Decode(CString& sValue) const;
};
namespace std {
template <>
struct hash<CString> : hash<std::string> {};
}
// Make translateable messages easy to write:
// _f("Foo is {1}")(foo)
class CInlineFormatMessage {
public:
explicit CInlineFormatMessage(const CString& sFormat)
: m_sFormat(sFormat) {}
explicit CInlineFormatMessage(CString&& sFormat)
: m_sFormat(std::move(sFormat)) {}
template <typename... Args>
CString operator()(const Args&... args) const {
MCString values;
apply(values, 1, args...);
return CString::NamedFormat(m_sFormat, values);
}
private:
template <typename Arg, typename... Rest>
void apply(MCString& values, int index, const Arg& arg,
const Rest&... rest) const {
values[CString(index)] = CString(arg);
apply(values, index + 1, rest...);
}
void apply(MCString& values, int index) const {}
CString m_sFormat;
};
#endif // !ZNCSTRING_H

View File

@@ -22,6 +22,7 @@
#include <znc/Modules.h>
#include <znc/Socket.h>
#include <znc/Listener.h>
#include <znc/Translation.h>
#include <mutex>
#include <map>
#include <list>
@@ -298,6 +299,7 @@ class CZNC {
TCacheMap<CString> m_sConnectThrottle;
bool m_bProtectWebSessions;
bool m_bHideVersion;
CTranslationDomainRefHolder m_Translation;
};
#endif // !ZNC_H

View File

@@ -32,6 +32,7 @@
#cmakedefine HAVE_LIBSSL 1
#cmakedefine HAVE_IPV6 1
#cmakedefine HAVE_ZLIB 1
#cmakedefine HAVE_I18N 1
#cmakedefine CSOCK_USE_POLL 1
#cmakedefine HAVE_GETOPT_LONG 1
@@ -49,5 +50,6 @@
#define _MODDIR_ "@CMAKE_INSTALL_FULL_LIBDIR@/znc"
#define _DATADIR_ "@CMAKE_INSTALL_FULL_DATADIR@/znc"
#define LOCALE_DIR "@CMAKE_INSTALL_FULL_LOCALEDIR@"
#endif /* ZNCCONFIG_H */

View File

@@ -75,10 +75,19 @@ else()
set(moddisable_modtcl true)
endif()
set(actual_modules)
foreach(modpath ${all_modules})
string(REGEX MATCH "/([-a-zA-Z0-9_]+)\\.([a-z]+)$" unused "${modpath}")
if(NOT "${modpath}" MATCHES "/([-a-zA-Z0-9_]+)\\.([a-z]+)$")
continue()
endif()
set(mod "${CMAKE_MATCH_1}")
set(modtype "${CMAKE_MATCH_2}")
if(mod STREQUAL "CMakeLists" OR mod STREQUAL "Makefile")
continue()
endif()
list(APPEND actual_modules "${modpath}")
set(modenabled true)
@@ -107,5 +116,9 @@ foreach(modpath ${all_modules})
endif()
endforeach()
if(HAVE_I18N)
add_subdirectory(po)
endif()
install(DIRECTORY data/
DESTINATION "${CMAKE_INSTALL_DATADIR}/znc/modules")

24
modules/po/CMakeLists.txt Normal file
View File

@@ -0,0 +1,24 @@
#
# 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)
foreach(modpath ${actual_modules})
get_filename_component(mod "${modpath}" NAME_WE)
get_filename_component(module "${modpath}" NAME)
translation(SHORT "${mod}" FULL "znc-${mod}" SOURCES "${module}"
TMPLDIRS "${CMAKE_CURRENT_SOURCE_DIR}/../data/${mod}/tmpl")
endforeach()

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}

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");

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);
}

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);
}

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--;

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
src/Translation.cpp Normal file
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);
}

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);

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
src/po/CMakeLists.txt Normal file
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})

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;

81
translation_pot.py Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
#
# 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.
#
import argparse
import glob
import os
import re
import subprocess
parser = argparse.ArgumentParser()
parser.add_argument('--include_dir', action='store')
parser.add_argument('--explicit_sources', action='append')
parser.add_argument('--tmpl_dirs', action='append')
parser.add_argument('--strip_prefix', action='store')
parser.add_argument('--tmp_prefix', action='store')
parser.add_argument('--output', action='store')
args = parser.parse_args()
pot_list = []
# .cpp
main_pot = args.tmp_prefix + '_main.pot'
subprocess.check_call(['xgettext',
'--omit-header',
'-D', args.include_dir,
'-o', main_pot,
'--keyword=t:1,1t', '--keyword=t:1,2c,2t',
'--keyword=f:1,1t', '--keyword=f:1,2c,2t',
'--keyword=p:1,2,3t', '--keyword=p:1,2,4c,4t',
'--keyword=d:1,1t', '--keyword=d:1,2c,2t',
] + args.explicit_sources)
if os.path.isfile(main_pot):
pot_list.append(main_pot)
# .tmpl
tmpl_pot = args.tmp_prefix + '_tmpl.pot'
tmpl_uniq_pot = args.tmp_prefix + '_tmpl_uniq.pot'
tmpl = []
pattern = re.compile(r'<\?\s*(?:FORMAT|(PLURAL))\s+(?:CTX="([^"]+?)"\s+)?"([^"]+?)"(?(1)\s+"([^"]+?)"|).*?\?>')
for tmpl_dir in args.tmpl_dirs:
for fname in glob.iglob(tmpl_dir + '/*.tmpl'):
fbase = fname[len(args.strip_prefix):]
with open(fname) as f:
for linenum, line in enumerate(f):
for x in pattern.finditer(line):
text, plural, context = x.group(3), x.group(4), x.group(2)
tmpl.append('#: {}:{}'.format(fbase, linenum + 1))
if context:
tmpl.append('msgctxt "{}"'.format(context))
tmpl.append('msgid "{}"'.format(text))
if plural:
tmpl.append('msgid_plural "{}"'.format(plural))
tmpl.append('msgstr[0] ""')
tmpl.append('msgstr[1] ""')
else:
tmpl.append('msgstr ""')
tmpl.append('')
if tmpl:
with open(tmpl_pot, 'w') as f:
for line in tmpl:
print(line, file=f)
subprocess.check_call(['msguniq', '-o', tmpl_uniq_pot, tmpl_pot])
pot_list.append(tmpl_uniq_pot)
# combine
if pot_list:
subprocess.check_call(['msgcat', '-o', args.output] + pot_list)