From 4580211ff5f991507c9207970bc3a0b860eb0004 Mon Sep 17 00:00:00 2001 From: Cyano Hao Date: Tue, 25 Jun 2024 16:50:15 +0800 Subject: [PATCH] add localization support --- .gitignore | 3 + lib/intl/include/intl.hpp | 428 ++++++++++++++++++++++++++++++++++++++ package/common.sh | 2 + package/dev.sh | 1 + package/linux-amd64.sh | 1 + package/mac-arm.sh | 1 + package/mac-x86.sh | 1 + package/windows-32.sh | 1 + package/windows-arm64.sh | 1 + package/windows-x64.sh | 1 + po/update.sh | 19 ++ po/zh_CN.po | 96 +++++++++ src/merger/intl.hpp | 347 ++++++++++++++++++++++++++++++ src/merger/merge-otd.cpp | 99 ++++----- xmake.lua | 2 +- 15 files changed, 955 insertions(+), 48 deletions(-) create mode 100644 lib/intl/include/intl.hpp create mode 100755 po/update.sh create mode 100644 po/zh_CN.po create mode 100644 src/merger/intl.hpp diff --git a/.gitignore b/.gitignore index b2c173b..266c86d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ __pycache__ /build /release /compile_commands.json +/po/*.mo +/po/*.po~ +/po/*.pot diff --git a/lib/intl/include/intl.hpp b/lib/intl/include/intl.hpp new file mode 100644 index 0000000..9aefa08 --- /dev/null +++ b/lib/intl/include/intl.hpp @@ -0,0 +1,428 @@ +/** + * Based on libintl lite (https://github.com/j-jorge/libintl-lite) + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class Translator { + + constexpr static bool isBigEndian = std::endian::native == std::endian::big; + + struct MoHeader { + uint32_t magic; + uint32_t version; + uint32_t nStrings; + uint32_t offsetOrig; + uint32_t offsetTrans; + uint32_t sizeOrigStringsArray; + uint32_t sizeTranslatedStringsArray; + }; + + class MessageCatalog { + private: + MessageCatalog(const MessageCatalog &); + MessageCatalog &operator=(const MessageCatalog &); + + uint32_t numberOfStrings; + const std::string *sortedOrigStringsArray; + const std::string *translatedStringsArray; + + public: + // The ownership of these arrays is transfered to the created message + // catalog object! + // Does not throw exceptions! + MessageCatalog(uint32_t numberOfStrings, + const std::string *sortedOrigStringsArray, + const std::string *translatedStringsArray) + : numberOfStrings(numberOfStrings), + sortedOrigStringsArray(sortedOrigStringsArray), + translatedStringsArray(translatedStringsArray) {} + + ~MessageCatalog() { + delete[] this->sortedOrigStringsArray; + delete[] this->translatedStringsArray; + } + + // Returns NULL, if the original string was not found. + // Does not throw exceptions! + const std::string *getTranslatedStrPtr(const std::string &orig) const { + const std::string *lastSortedOrigStringEndIter = + this->sortedOrigStringsArray + this->numberOfStrings; + const std::string *origStrPtr = + std::lower_bound(this->sortedOrigStringsArray, + lastSortedOrigStringEndIter, orig); + + if (!origStrPtr || (origStrPtr == lastSortedOrigStringEndIter) || + (*origStrPtr != orig)) { + return NULL; + } else { + return &this->translatedStringsArray + [origStrPtr - this->sortedOrigStringsArray]; + } + } + + // Helper functions for handling numbers and char array conversions: + + static inline uint32_t + charArrayToUInt32(const char uint32CharArray[4]) { + return *reinterpret_cast(uint32CharArray); + } + + static inline bool readUIn32FromFile(FILE *fileHandle, + uint32_t &outReadUInt32) { + char uint32CharArray[4]; + if ((fread(uint32CharArray, 1, 4, fileHandle)) != 4) { + return false; + } + + outReadUInt32 = charArrayToUInt32(uint32CharArray); + return true; + } + + // RAII classes: + + template class ArrayGurard { + private: + ArrayGurard(const ArrayGurard &); + ArrayGurard &operator=(const ArrayGurard &); + + T *&arrayRef; + bool released; + + public: + explicit ArrayGurard(T *&arrayRef) + : arrayRef(arrayRef), released(false) {} + + ~ArrayGurard() { + if (!this->released) { + delete[] this->arrayRef; + } + } + + const T *release() { + this->released = true; + return this->arrayRef; + } + }; + + class CloseFileHandleGuard { + private: + CloseFileHandleGuard(const CloseFileHandleGuard &); + CloseFileHandleGuard &operator=(const CloseFileHandleGuard &); + + FILE *&fileHandleRef; + + public: + explicit CloseFileHandleGuard(FILE *&fileHandleRef) + : fileHandleRef(fileHandleRef) {} + + ~CloseFileHandleGuard() { + if (this->fileHandleRef) { + fclose(this->fileHandleRef); + } + } + }; + + // Helper function to load strings from a .mo file and stores them in a + // given array + + static bool + loadMoFileStringsToArray(FILE *moFile, uint32_t numberOfStrings, + uint32_t stringsTableOffsetFromFileBegin, + std::string *outStringsFromMoFileArray) { + if (fseek(moFile, stringsTableOffsetFromFileBegin, SEEK_SET) != 0) + return false; + + uint32_t *stringsLengthsArray = NULL; + ArrayGurard stringsLengthsArrayGuard(stringsLengthsArray); + stringsLengthsArray = new uint32_t[numberOfStrings]; + if (!stringsLengthsArray) { + return false; + } + + uint32_t firstStringOffset; + uint32_t lastStringOffset; + { + uint32_t currentStringLength; + uint32_t currentStringOffset; + for (uint32_t i = 0; i < numberOfStrings; i++) { + if (!readUIn32FromFile(moFile, currentStringLength)) + return false; + if (!readUIn32FromFile(moFile, currentStringOffset)) + return false; + + stringsLengthsArray[i] = currentStringLength; + + if (i == 0) { + firstStringOffset = currentStringOffset; + } + + if (i == (numberOfStrings - 1)) { + lastStringOffset = currentStringOffset; + } + } + } + + { + char *stringCharsArray = NULL; + ArrayGurard stringCharsArrayGuard(stringCharsArray); + + uint32_t stringCharsArraySize = + lastStringOffset + + stringsLengthsArray[numberOfStrings - 1] + 1 - + firstStringOffset; + if (stringCharsArraySize == 0) { + return false; + } + + if (fseek(moFile, firstStringOffset, SEEK_SET) != 0) + return false; + stringCharsArray = new char[stringCharsArraySize]; + if (!stringCharsArray) { + return false; + } + if (fread(stringCharsArray, 1, stringCharsArraySize, moFile) != + stringCharsArraySize) + return false; + + const char *stringsCharsArrayIter = stringCharsArray; + for (uint32_t i = 0; i < numberOfStrings; i++) { + const char *currentStrEndIter = + stringsCharsArrayIter + stringsLengthsArray[i]; + outStringsFromMoFileArray[i] = + std::string(stringsCharsArrayIter, currentStrEndIter); + stringsCharsArrayIter = + currentStrEndIter + + 1 /* skip the NULL char at the end of the string */; + } + } + + return true; + } + + static std::vector buildMoFilePaths(const char *domain) { + const char *pLanguage = getenv("LANGUAGE"); + std::string languages = pLanguage ? pLanguage : ""; + std::vector paths{""}; + std::string slash = "/"; + const size_t length = languages.size(); + size_t pos = 0; + size_t colon = 0; + while (pos != length) { + colon = languages.find(':', pos); + std::string lang = languages.substr(pos, colon - pos); + if (!lang.empty()) { + paths.emplace_back(slash + lang + "/LC_MESSAGES/" + domain + + ".mo"); + } + if (colon == std::string::npos) { + break; + } else { + pos = colon + 1; + } + } + return paths; + } + + bool loadMessageCatalog(const char *domain, const char *moFilesRoot) { + if (!moFilesRoot || !domain) { + return false; + } + + FILE *moFile = NULL; + CloseFileHandleGuard closeFileHandleGuard(moFile); + + for (const std::string &path : buildMoFilePaths(domain)) { + std::string fullPath = moFilesRoot + path; + moFile = fopen(fullPath.c_str(), "rb"); + if (loadMessageCatalogFile(domain, moFile) == true) { + return true; + } + } + + return false; + } + + bool loadMessageCatalogFile(const char *domain, FILE *moFile) { + try { + if (sizeof(uint32_t) != 4) { + return false; + } + + if (!moFile || !domain) { + return false; + } + + uint32_t magicNumber; + if (!readUIn32FromFile(moFile, magicNumber)) + return false; + if (magicNumber != 0x950412de) + return false; + + uint32_t fileFormatRevision; + if (!readUIn32FromFile(moFile, fileFormatRevision)) + return false; + if (fileFormatRevision != 0) + return false; + + uint32_t numberOfStrings; + if (!readUIn32FromFile(moFile, numberOfStrings)) + return false; + if (numberOfStrings == 0) { + return true; + } + + uint32_t offsetOrigTable; + if (!readUIn32FromFile(moFile, offsetOrigTable)) + return false; + + uint32_t offsetTransTable; + if (!readUIn32FromFile(moFile, offsetTransTable)) + return false; + + std::string *sortedOrigStringsArray = NULL; + ArrayGurard sortedOrigStringsArrayGuard( + sortedOrigStringsArray); + sortedOrigStringsArray = new std::string[numberOfStrings]; + if (!sortedOrigStringsArray) { + return false; + } + + if (!loadMoFileStringsToArray(moFile, numberOfStrings, + offsetOrigTable, + sortedOrigStringsArray)) + return false; + + std::string *translatedStringsArray = NULL; + ArrayGurard translatedStringsArrayGuard( + translatedStringsArray); + translatedStringsArray = new std::string[numberOfStrings]; + if (!translatedStringsArray) { + return false; + } + + if (!loadMoFileStringsToArray(moFile, numberOfStrings, + offsetTransTable, + translatedStringsArray)) + return false; + + MessageCatalog *newMessageCatalogPtr = + new MessageCatalog(numberOfStrings, sortedOrigStringsArray, + translatedStringsArray); + if (!newMessageCatalogPtr) + return false; + sortedOrigStringsArrayGuard.release(); + translatedStringsArrayGuard.release(); + + char *domainDup = strdup(domain); + if (!domainDup) + return false; + closeLoadedMessageCatalog(domain); + loadedMessageCatalogPtrsByDomain[domainDup] = + newMessageCatalogPtr; + + return true; + } catch (...) { + return false; + } + } + + bool bindtextdomain(const char *domain, const char *moFilePath) { + return loadMessageCatalog(domain, moFilePath); + } + + void closeLoadedMessageCatalog(const char *domain) { + if (domain) { + for (auto i = loadedMessageCatalogPtrsByDomain.begin(); + i != loadedMessageCatalogPtrsByDomain.end(); i++) { + if (strcmp(i->first, domain) == 0) { + free(i->first); + delete i->second; + loadedMessageCatalogPtrsByDomain.erase(i); + return; + } + } + } + } + + void closeAllLoadedMessageCatalogs() { + for (auto i = loadedMessageCatalogPtrsByDomain.begin(); + i != loadedMessageCatalogPtrsByDomain.end(); i++) { + free(i->first); + delete i->second; + } + loadedMessageCatalogPtrsByDomain.clear(); + free(currentDefaultDomain); + currentDefaultDomain = NULL; + } + }; + + inline static char *currentDefaultDomain = NULL; + inline static std::map + loadedMessageCatalogPtrsByDomain; + + const char *gettext(const char *origStr) { return gettext(NULL, origStr); } + + const char *gettext(const char *domain, const char *origStr) { + if (!origStr) { + return NULL; + } + + if (!domain) { + if (currentDefaultDomain) { + domain = currentDefaultDomain; + } else { + return origStr; + } + } + + const MessageCatalog *msgCat = NULL; + for (auto i = loadedMessageCatalogPtrsByDomain.begin(); + !msgCat && (i != loadedMessageCatalogPtrsByDomain.end()); i++) { + if (strcmp(i->first, domain) == 0) { + msgCat = i->second; + } + } + + if (!msgCat) { + return origStr; + } + + const std::string *translatedStrPtr = + msgCat->getTranslatedStrPtr(origStr); + if (translatedStrPtr) { + return translatedStrPtr->c_str(); + } else { + return origStr; + } + } + + const char *gettext(const char *origStr, const char *origStrPlural, + unsigned long n) { + if (n == 1) { + return gettext(origStr); + } else { + return gettext(origStrPlural); + } + } + + const char *gettext(const char *domain, const char *origStr, + const char *origStrPlural, unsigned long n) { + if (n == 1) { + return gettext(domain, origStr); + } else { + return gettext(domain, origStrPlural); + } + } +}; + +inline std::unique_ptr gTranslator; diff --git a/package/common.sh b/package/common.sh index 7e90530..e75b163 100755 --- a/package/common.sh +++ b/package/common.sh @@ -11,6 +11,7 @@ function package_sc() { cp "$_merge_otd" "$_otfccbuild" "$_otfccdump" release/$_Dist/ cp font/$_cjk.ttf release/$_Dist/cjk.ttf cp font/$_latin.ttf release/$_Dist/latin.ttf + cp po/zh_CN.mo release/$_Dist/zh_CN.mo case $_platform in unix) @@ -49,6 +50,7 @@ function package_tc() { cp "$_merge_otd" "$_otfccbuild" "$_otfccdump" release/$_Dist/ cp font/$_cjk.ttf release/$_Dist/cjk.ttf cp font/$_latin.ttf release/$_Dist/latin.ttf + cp po/zh_CN.mo release/$_Dist/zh_CN.mo case $_platform in unix) diff --git a/package/dev.sh b/package/dev.sh index c861910..1738c45 100755 --- a/package/dev.sh +++ b/package/dev.sh @@ -9,6 +9,7 @@ xmake build source build/config.sh VERSION=$VERSION-dev +po/update.sh source package/common.sh export _platform="unix" diff --git a/package/linux-amd64.sh b/package/linux-amd64.sh index 17a2d5c..469fc0c 100755 --- a/package/linux-amd64.sh +++ b/package/linux-amd64.sh @@ -9,6 +9,7 @@ xmake build source build/config.sh VERSION=$VERSION-linux-amd64 +po/update.sh source package/common.sh export _platform="unix" diff --git a/package/mac-arm.sh b/package/mac-arm.sh index bf24d50..17c34a3 100755 --- a/package/mac-arm.sh +++ b/package/mac-arm.sh @@ -9,6 +9,7 @@ xmake build source build/config.sh VERSION=$VERSION-mac-arm +po/update.sh source package/common.sh export _platform="unix" diff --git a/package/mac-x86.sh b/package/mac-x86.sh index 3d0876f..40d1080 100755 --- a/package/mac-x86.sh +++ b/package/mac-x86.sh @@ -9,6 +9,7 @@ xmake build source build/config.sh VERSION=$VERSION-mac-x86 +po/update.sh source package/common.sh export _platform="unix" diff --git a/package/windows-32.sh b/package/windows-32.sh index 8fb9290..cc1fe5c 100755 --- a/package/windows-32.sh +++ b/package/windows-32.sh @@ -9,6 +9,7 @@ xmake build source build/config.sh VERSION=$VERSION-windows-32 +po/update.sh source package/common.sh export _platform="windows" diff --git a/package/windows-arm64.sh b/package/windows-arm64.sh index fbf44ca..3ec4e4b 100755 --- a/package/windows-arm64.sh +++ b/package/windows-arm64.sh @@ -9,6 +9,7 @@ xmake build source build/config.sh VERSION=$VERSION-windows-arm64 +po/update.sh source package/common.sh export _platform="windows" diff --git a/package/windows-x64.sh b/package/windows-x64.sh index a4b82a2..e74cf0e 100755 --- a/package/windows-x64.sh +++ b/package/windows-x64.sh @@ -9,6 +9,7 @@ xmake build source build/config.sh VERSION=$VERSION-windows-x64 +po/update.sh source package/common.sh export _platform="windows" diff --git a/po/update.sh b/po/update.sh new file mode 100755 index 0000000..431d3e4 --- /dev/null +++ b/po/update.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +xgettext --c++ --from-code=UTF-8 --keyword=_ --no-location --omit-header --output=po/unified.pot \ + src/merger/merge-otd.cpp \ + src/otfcc-driver/otfccbuild.c \ + src/otfcc-driver/otfccdump.c + +for lang in zh_CN +do + if [[ ! -f po/$lang.po ]] + then + msginit --locale=$lang --no-translator --input=po/unified.pot --output=po/$lang.po + else + msgmerge --update --sort-output po/$lang.po po/unified.pot + fi + msgfmt -o po/$lang.mo po/$lang.po +done diff --git a/po/zh_CN.po b/po/zh_CN.po new file mode 100644 index 0000000..5587e65 --- /dev/null +++ b/po/zh_CN.po @@ -0,0 +1,96 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#, c++-format +msgid "Failed to load file {0}" +msgstr "读取文件 {0} 失败" + +msgid "Output otd file. If omit, override the first input otd file." +msgstr "输出 otd 文件路径。如果不指定,则覆盖第一个输入 otd 文件。" + +msgid "" +"Set font family and style, formatted as \"Font Name;Weight;Width;Slope\".\n" +"If unset, the font family and style will be automatically derived from the " +"original font name.\n" +"\n" +"Values for weight:\n" +"+ Number: 100 to 950, inclusive.\n" +"+ The following keywords (case insensitive, ignore hyphens; synonyms are " +"listed in parentheses):\n" +"+ - Thin = 100 (UltraLight)\n" +"+ - ExtraLight = 200\n" +"+ - Light = 300\n" +"+ - SemiLight = 350 (DemiLight)\n" +"+ - Normal = 372\n" +"+ - Regular = 400 (Roman, \"\")\n" +"+ - Book = 450\n" +"+ - Medium = 500\n" +"+ - SemiBold = 600 (Demi, DemiBold)\n" +"+ - Bold = 700\n" +"+ - ExtraBold = 800\n" +"+ - Black = 900 (Heavy, UltraBold)\n" +"+ - ExtraBlack = 950\n" +"\n" +"Values for width:\n" +"+ Number: 1 to 9, inclusive.\n" +"+ The following keywords (case insensitive, ignore hyphens; synonyms are " +"listed in parentheses):\n" +"+ - UltraCondensed = 1\n" +"+ - ExtraCondensed = 2\n" +"+ - Condensed = 3\n" +"+ - SemiCondensed = 4\n" +"+ - Normal = 5 (\"\")\n" +"+ - SemiExtended = 6 (SemiExpanded)\n" +"+ - Extended = 7 (Expanded)\n" +"+ - ExtraExtended = 8 (ExtraExpanded)\n" +"+ - UltraExtended = 9 (UltraExpanded)\n" +"\n" +"Values for slope (case insensitive; synonyms are listed in parentheses):\n" +"- Upright (Normal, Roman, Unslanted, \"\")\n" +"- Italic (Italized)\n" +"- Oblique (Slant)\n" +msgstr "" +"指定字体家族名和样式,格式为 \"字体名;字重;宽度;倾斜\".\n" +"如果不指定,则自动根据原字体名合成新的字体名。\n" +"\n" +"字重的取值范围:\n" +"+ 数字:100 至 950 之间的整数(含两端点)。\n" +"+ 下列单词(不区分大小写,忽略连字符;括号内的单词视作左边单词的同义词):\n" +"+ - Thin = 100 (UltraLight)\n" +"+ - ExtraLight = 200\n" +"+ - Light = 300\n" +"+ - SemiLight = 350 (DemiLight)\n" +"+ - Normal = 372\n" +"+ - Regular = 400 (Roman, \"\")\n" +"+ - Book = 450\n" +"+ - Medium = 500\n" +"+ - SemiBold = 600 (Demi, DemiBold)\n" +"+ - Bold = 700\n" +"+ - ExtraBold = 800\n" +"+ - Black = 900 (Heavy, UltraBold)\n" +"+ - ExtraBlack = 950\n" +"\n" +"宽度的取值范围:\n" +"+ 数字:1 至 9 之间的整数(含两端点)。\n" +"+ 下列单词(不区分大小写,忽略连字符;括号内的单词视作左边单词的同义词):\n" +"+ - UltraCondensed = 1\n" +"+ - ExtraCondensed = 2\n" +"+ - Condensed = 3\n" +"+ - SemiCondensed = 4\n" +"+ - Normal = 5 (\"\")\n" +"+ - SemiExtended = 6 (SemiExpanded)\n" +"+ - Extended = 7 (Expanded)\n" +"+ - ExtraExtended = 8 (ExtraExpanded)\n" +"+ - UltraExtended = 9 (UltraExpanded)\n" +"\n" +"倾斜的取值范围(不区分大小写;括号内的单词视作左边单词的同义词):\n" +"- Upright (Normal, Roman, Unslanted, \"\")\n" +"- Italic (Italized)\n" +"- Oblique (Slant)\n" diff --git a/src/merger/intl.hpp b/src/merger/intl.hpp new file mode 100644 index 0000000..0d53259 --- /dev/null +++ b/src/merger/intl.hpp @@ -0,0 +1,347 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#elif defined(__linux__) +#include +#include +#elif defined(__APPLE__) +#include +#include +#endif + +#include +#include + +#ifdef _WIN32 +#include +#endif + +class Translator { + public: + Translator() { + auto langs = getLanguageList(); + auto appDir = getAppDir(); + + for (auto lang : langs) { + auto moPath = fmt::format("{}/{}.mo", appDir, lang); + if (loadMoFile(moPath)) + return; + } + } + Translator(const Translator &) = delete; + Translator &operator=(const Translator &) = delete; + + const char *gettext(const char *orig) { + if (translations.empty() || !orig) + return orig; + + auto it = + std::lower_bound(translations.begin(), translations.end(), orig); + + if (it != translations.end() && it->orig == orig) + return it->trans.begin(); + else + return orig; + } + + private: + struct Translation { + std::string_view orig; + std::string_view trans; + + bool operator<(const Translation &rhs) const { return orig < rhs.orig; } + + bool operator<(const char *rhs) const { return orig < rhs; } + }; + + private: + std::string moFile; + std::vector translations; + + private: + struct MoHeader { + uint32_t magic; + uint32_t version; + uint32_t nStrings; + uint32_t offsetOrig; + uint32_t offsetTrans; + uint32_t sizeHash; + uint32_t offSetHash; + }; + + struct MoStr { + uint32_t length; + uint32_t offset; + + bool valid(const std::string &moFile) const { + return offset + length <= moFile.size(); + } + + std::string_view toStringView(const std::string &moFile) const { + return {moFile.data() + offset, length}; + } + }; + + bool loadMoFile(const std::string &filename) { + std::string moFile = loadFile(filename); + if (moFile.size() < sizeof(MoHeader)) + return false; + + MoHeader *header = reinterpret_cast(moFile.data()); + if (header->magic != 0x950412de) + return false; + if (header->version != 0) + return false; + + if (moFile.size() < header->offsetOrig + header->nStrings * 8) + return false; + MoStr *origs = + reinterpret_cast(moFile.data() + header->offsetOrig); + + if (moFile.size() < header->offsetTrans + header->nStrings * 8) + return false; + MoStr *trans = + reinterpret_cast(moFile.data() + header->offsetTrans); + + std::vector translations; + translations.reserve(header->nStrings); + for (uint32_t i = 0; i < header->nStrings; i++) { + if (!origs[i].valid(moFile) || !trans[i].valid(moFile)) + return false; + + translations.emplace_back(origs[i].toStringView(moFile), + trans[i].toStringView(moFile)); + } + + std::sort(translations.begin(), translations.end()); + + this->moFile.swap(moFile); + this->translations.swap(translations); + return true; + } + + static std::string loadFile(const std::string &filename) { + FILE *fp = nowide::fopen(filename.c_str(), "rb"); + if (!fp) + return {}; + fseek(fp, 0, SEEK_END); + size_t size = ftell(fp); + fseek(fp, 0, SEEK_SET); + std::string result(size, 0); + fread(result.data(), 1, size, fp); + fclose(fp); + return result; + } + + struct LangCode { + std::string lang; + std::string region; + std::string script; + + std::string toGnu() const { + std::string result = lang; + if (!region.empty()) + result += "-" + region; + if (!script.empty()) + result += "-" + script; + return result; + } + + std::vector extendedGnu() const { + std::vector result; + if (!script.empty()) { + if (!region.empty()) + result.push_back(toGnu()); // 1. perfetch match + result.push_back(lang + "@" + script); // 2. lang & script + } + if (!region.empty()) + result.push_back(lang + "_" + region); // 3. lang & region + result.push_back(lang); // 4. lang + return result; + } + + static LangCode fromGnu(std::string langStr) { + // remove encoding: zh_CN.UTF-8 + auto pos3 = langStr.find('.'); + if (pos3 != std::string::npos) + langStr.erase(pos3); + + LangCode result; + auto pos2 = langStr.find('@'); + if (pos2 != std::string::npos) { + result.script = langStr.substr(pos2 + 1); + langStr = langStr.substr(0, pos2); + } + auto pos1 = langStr.find('_'); + if (pos1 != std::string::npos) { + result.region = langStr.substr(pos1 + 1); + langStr = langStr.substr(0, pos1); + } + result.lang = langStr; + return result; + } + + static LangCode fromWin32(std::string langStr) { + // ref. + // https://learn.microsoft.com/en-us/windows/win32/intl/language-names + + // remove supplemental: en-US-x-fabricam + if (auto pos = langStr.find("-x-"); pos != std::string::npos) + langStr.erase(pos); + + LangCode result; + auto pos1 = langStr.find("-"); + result.lang = langStr.substr(0, pos1); + if (pos1 == std::string::npos) + return result; // neutral + + langStr = langStr.substr(pos1 + 1); + auto pos2 = langStr.find("-"); + if (pos2 != std::string::npos) { + result.script = langStr.substr(0, pos2); + result.script[0] = toupper(result.script[0]); + result.region = langStr.substr(pos2 + 1); + } else + result.region = langStr; + return result; + } + }; + + static std::vector getLanguageList() { + std::vector result; + std::set added; + + auto add = [&result, &added](const LangCode &langCode) { + for (auto lang : langCode.extendedGnu()) + if (added.find(lang) == added.end()) { + result.push_back(lang); + added.insert(lang); + } + }; + + // LANGUAGE=zh_CN:en_US + if (const char *langStr = getenv("LANGUAGE"); langStr) { + auto langs = split(langStr); + for (auto lang : langs) + add(LangCode::fromGnu(lang)); + } + + // LC_ALL=zh_CN.UTF-8 + if (const char *lang = getenv("LC_ALL"); lang) + add(LangCode::fromGnu(lang)); + + // LC_MESSAGES=zh_CN.UTF-8 + if (const char *lang = getenv("LC_MESSAGES"); lang) + add(LangCode::fromGnu(lang)); + + // LANG=zh_CN.UTF-8 + if (const char *lang = getenv("LANG"); lang) + add(LangCode::fromGnu(lang)); + +#ifdef _WIN32 + for (auto lang : getWin32LanguageList()) + add(LangCode::fromWin32(lang)); +#endif + + return result; + } + + static std::vector split(const std::string &str, + char sep = ':') { + std::vector result; + size_t pos = 0; + while (pos < str.size()) { + size_t next = str.find(sep, pos); + if (next == std::string::npos) + next = str.size(); + size_t length = next - pos; + if (length > 0) + result.push_back(str.substr(pos, length)); + pos = next + 1; + } + return result; + } + +#ifdef _WIN32 + static std::vector getWin32LanguageList() { + static auto pGetUserPreferredUILanguages = + reinterpret_cast( + GetProcAddress(GetModuleHandleW(L"kernel32.dll"), + "GetUserPreferredUILanguages")); + if (!pGetUserPreferredUILanguages) + return {}; + + constexpr int bufLen = 4096; + wchar_t wbuf[bufLen]; + ULONG len = bufLen; + ULONG numLangs; + if (!pGetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numLangs, wbuf, + &len)) + return {}; + + char buf[bufLen]; + for (int i = 0; i < len; i++) + buf[i] = wbuf[i]; + + std::vector result; + char *pos = buf; + while (*pos) { + std::string lang = std::string(pos); + pos += lang.size() + 1; + result.push_back(lang); + } + return result; + } +#endif + + static std::string getAppDir() { + // ref. + // https://stackoverflow.com/questions/1023306/finding-current-executables-path-without-proc-self-exe +#if defined(_WIN32) + wchar_t buf[MAX_PATH]; + if (DWORD len = GetModuleFileNameW(NULL, buf, MAX_PATH); len) { + std::wstring res(buf, len); + auto pos = res.find_last_of('\\'); + if (pos != std::wstring::npos) + return nowide::narrow(res.substr(0, pos)); + } +#elif defined(__linux__) + char buf[PATH_MAX]; + if (ssize_t len = readlink("/proc/self/exe", buf, PATH_MAX); + len != -1) { + std::string res(buf, len); + auto pos = res.find_last_of('/'); + if (pos != std::string::npos) + return res.substr(0, pos); + } +#elif defined(__APPLE__) + char buf[PATH_MAX]; + uint32_t len = PATH_MAX; + if (_NSGetExecutablePath(buf, &len) == 0) { + std::string res(buf, len); + auto pos = res.find_last_of('/'); + if (pos != std::string::npos) + return res.substr(0, pos); + } +#endif + return {}; + } +}; + +inline std::unique_ptr gTranslator; + +inline const char *_(const char *origStr) { + return gTranslator ? gTranslator->gettext(origStr) : origStr; +} diff --git a/src/merger/merge-otd.cpp b/src/merger/merge-otd.cpp index 84f4ade..ca4168c 100644 --- a/src/merger/merge-otd.cpp +++ b/src/merger/merge-otd.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -15,7 +16,9 @@ #include #include #include +#include +#include "intl.hpp" #include "invisible.hpp" #include "merge-name.h" #include "ps2tt.h" @@ -23,18 +26,10 @@ using json = nlohmann::json; -#if __cpp_char8_t >= 201811L - -inline auto &operator<<(std::ostream &os, const char8_t *u8str) { - return os << reinterpret_cast(u8str); -} - -#endif - std::string LoadFile(const std::string &u8filename) { nowide::ifstream file(u8filename); if (!file) { - nowide::cerr << u8"读取文件 " << u8filename << u8" 失败\n" << std::endl; + spdlog::error(fmt::runtime(_("Failed to load file {0}")), u8filename); throw std::runtime_error("failed to load file"); } std::string result{std::istreambuf_iterator(file), @@ -176,7 +171,9 @@ json MergeCodePage(std::vector cpranges) { } int main(int argc, char *u8argv[]) { - nowide::args _{argc, u8argv}; + nowide::args args{argc, u8argv}; + + gTranslator = std::make_unique(); std::string outputPath; @@ -184,51 +181,59 @@ int main(int argc, char *u8argv[]) { std::string baseFileName; std::vector appendFileNames; - auto cli = ({ using namespace clipp; ((option("-o", "--output") & value("out.otd", outputPath)) % - "输出 otd 文件路径。如果不指定,则覆盖第一个输入 otd 文件。", + _("Output otd file. If omit, override the first input otd " + "file."), (option("-n", "--name") & value("Font Name;Weight;Width;Slope", overrideNameStyle)) % - R"+(指定字体家族名和样式。如果不指定,则自动根据原字体名合成新的字体名。 -格式:"字体名;字重;宽度;倾斜" -字重的取值范围 - 数字:100 至 950 之间的整数(含两端点)。 - 下列单词(不区分大小写,忽略连字符;括号内的单词视作左边单词的同义词): -  Thin = 100(UltraLight) -  ExtraLight = 200 -  Light = 300 -  SemiLight = 350(DemiLight) -  Normal = 372 -  Regular = 400(Roman、"") -  Book = 450 -  Medium = 500 -  SemiBold = 600(Demi、DemiBold) -  Bold = 700 -  ExtraBold = 800 -  Black = 900(Heavy、UltraBold) -  ExtraBlack = 950 -宽度的取值范围 - 数字:1 至 9 之间的整数(含两端点)。 - 下列单词(不区分大小写,忽略连字符;括号内的单词视作左边单词的同义词): -  UltraCondensed = 1 -  ExtraCondensed = 2 -  Condensed = 3 -  SemiCondensed = 4 -  Normal = 5("") -  SemiExtended = 6(SemiExpanded) -  Extended = 7(Expanded) -  ExtraExtended = 8(ExtraExpanded) -  UltraExtended = 9(UltraExpanded) -倾斜的取值范围 - 下列单词(不区分大小写;括号内的单词视作左边单词的同义词): -  Upright(Normal、Roman、Unslanted、"") -  Italic (Italized) -  Oblique(Slant))+", + _("Set font family and style, formatted as \"Font " + "Name;Weight;Width;Slope\".\n" + "If unset, the font family and style will be automatically " + "derived from the original font name.\n" + "\n" + "Values for weight:\n" + "+ Number: 100 to 950, inclusive.\n" + "+ The following keywords (case insensitive, ignore hyphens; " + "synonyms are listed in parentheses):\n" + "+ - Thin = 100 (UltraLight)\n" + "+ - ExtraLight = 200\n" + "+ - Light = 300\n" + "+ - SemiLight = 350 (DemiLight)\n" + "+ - Normal = 372\n" + "+ - Regular = 400 (Roman, \"\")\n" + "+ - Book = 450\n" + "+ - Medium = 500\n" + "+ - SemiBold = 600 (Demi, DemiBold)\n" + "+ - Bold = 700\n" + "+ - ExtraBold = 800\n" + "+ - Black = 900 (Heavy, UltraBold)\n" + "+ - ExtraBlack = 950\n" + "\n" + "Values for width:\n" + "+ Number: 1 to 9, inclusive.\n" + "+ The following keywords (case insensitive, ignore hyphens; " + "synonyms are listed in parentheses):\n" + "+ - UltraCondensed = 1\n" + "+ - ExtraCondensed = 2\n" + "+ - Condensed = 3\n" + "+ - SemiCondensed = 4\n" + "+ - Normal = 5 (\"\")\n" + "+ - SemiExtended = 6 (SemiExpanded)\n" + "+ - Extended = 7 (Expanded)\n" + "+ - ExtraExtended = 8 (ExtraExpanded)\n" + "+ - UltraExtended = 9 (UltraExpanded)\n" + "\n" + "Values for slope (case insensitive; synonyms are listed in " + "parentheses):\n" + "- Upright (Normal, Roman, Unslanted, \"\")\n" + "- Italic (Italized)\n" + "- Oblique (Slant)\n"), value("base.otd", baseFileName), values("append.otd", appendFileNames)); }); + if (!clipp::parse(argc, u8argv, cli) || appendFileNames.empty()) { nowide::cout << clipp::make_man_page(cli, "merge-otd") << std::endl; return EXIT_FAILURE; diff --git a/xmake.lua b/xmake.lua index 1272744..164edc1 100644 --- a/xmake.lua +++ b/xmake.lua @@ -19,7 +19,7 @@ rule("static_binary") target("merge-otd") set_kind("binary") add_rules("static_binary") - add_deps("clipp", "json", "nowide") + add_deps("clipp", "fmt", "json", "nowide", "spdlog") add_files("src/merger/*.cpp") target("otfccbuild")