diff --git a/_meta.lua b/_meta.lua index e5a687f..a4ce6e3 100644 --- a/_meta.lua +++ b/_meta.lua @@ -1,6 +1,7 @@ +local _ = require("highlightsync_gettext") return { name = "HighlightSync", - fullname = "HighlightSync", - description = "HighlightSync is a plugin for KOReader that synchronizes and merges your highlights, notes, and bookmarks across multiple devices or cloud backup locations.", - version = "0.7.2", -- ← Add this line! + fullname = _("HighlightSync"), + description = _("HighlightSync is a plugin for KOReader that synchronizes and merges your highlights, notes, and bookmarks across multiple devices or cloud backup locations."), + version = "0.7.2", } diff --git a/highlightsync_gettext.lua b/highlightsync_gettext.lua new file mode 100644 index 0000000..dde0da8 --- /dev/null +++ b/highlightsync_gettext.lua @@ -0,0 +1,294 @@ +-- This module is adapted from assistant.koplugin/assistant_gettext.lua +-- which is in turn based on koreader frontend/gettext.lua +-- +--[[-- +A pure Lua implementation of a gettext subset for highlightsync.koplugin. +--]] + +local logger = require("logger") + +local GetText = { + context = {}, + translation = {}, + current_lang = "C", + dirname = "l10n", + textdomain = "koreader", + plural_default = "n != 1", +} + +local GetText_mt = { + __index = {} +} + +GetText.wrapUntranslated_nowrap = function(text) return text end +GetText.wrapUntranslated = GetText.wrapUntranslated_nowrap + +function GetText_mt.__call(gettext, msgid) + return gettext.translation[msgid] and gettext.translation[msgid][0] or gettext.translation[msgid] or gettext.wrapUntranslated(msgid) +end + +local function c_escape(what_full, what) + if what == "\n" then return "" + elseif what == "a" then return "\a" + elseif what == "b" then return "\b" + elseif what == "f" then return "\f" + elseif what == "n" then return "\n" + elseif what == "r" then return "\r" + elseif what == "t" then return "\t" + elseif what == "v" then return "\v" + elseif what == "0" then return "\0" + else + return what_full + end +end + +local function logicalCtoLua(logical_str) + logical_str = logical_str:gsub("&&", "and") + logical_str = logical_str:gsub("!=", "~=") + logical_str = logical_str:gsub("||", "or") + return logical_str +end + +local function getDefaultPlural(n) + if n ~= 1 then + return 1 + else + return 0 + end +end + +local function getPluralFunc(pl_tests, nplurals, plural_default) + local plural_func_str = "return function(n) if " + + if #pl_tests > 1 then + for i = 1, #pl_tests do + local pl_test = pl_tests[i] + pl_test = logicalCtoLua(pl_test) + + if i > 1 and tonumber(pl_test) == nil then + pl_test = " elseif "..pl_test + end + if tonumber(pl_test) ~= nil then + pl_test = " else return "..pl_test + end + pl_test = pl_test:gsub("?", " then return") + + plural_func_str = plural_func_str..pl_test + end + plural_func_str = plural_func_str.." end end" + else + local pl_test = pl_tests[1] + if pl_test == plural_default then + return getDefaultPlural + end + if tonumber(pl_test) ~= nil then + plural_func_str = "return function(n) return "..pl_test.." end" + else + pl_test = logicalCtoLua(pl_test) + plural_func_str = "return function(n) if "..pl_test.." then return 1 else return 0 end end" + end + end + logger.dbg("gettext: plural function", plural_func_str) + return loadstring(plural_func_str)() +end + +local function addTranslation(msgctxt, msgid, msgstr, n) + local unescaped_string = string.gsub(msgstr, "(\\(.))", c_escape) + if msgctxt and msgctxt ~= "" then + if not GetText.context[msgctxt] then + GetText.context[msgctxt] = {} + end + if n then + if not GetText.context[msgctxt][msgid] then + GetText.context[msgctxt][msgid] = {} + end + GetText.context[msgctxt][msgid][n] = unescaped_string ~= "" and unescaped_string or nil + else + GetText.context[msgctxt][msgid] = unescaped_string ~= "" and unescaped_string or nil + end + else + if n then + if not GetText.translation[msgid] then + GetText.translation[msgid] = {} + end + GetText.translation[msgid][n] = unescaped_string ~= "" and unescaped_string or nil + else + GetText.translation[msgid] = unescaped_string ~= "" and unescaped_string or nil + end + end +end + +function GetText_mt.__index.changeLang(new_lang) + GetText.context = {} + GetText.translation = {} + GetText.current_lang = "C" + + if new_lang == "C" or new_lang == nil or new_lang == "" + or new_lang:match("^en_US") == "en_US" then + return + end + + -- Strip encoding suffix if present (e.g., "zh_CN.UTF-8" -> "zh_CN") + local dot_pos = new_lang:find("%.") + if dot_pos then + new_lang = new_lang:sub(1, dot_pos - 1) + end + + local file = GetText.dirname .. "/" .. new_lang .. "/" .. GetText.textdomain .. ".po" + local po = io.open(file, "r") + + if not po then + logger.dbg("highlightsync: cannot open translation file:", file) + return false + end + logger.info("highlightsync: successfully opened translation file:", file) + print("highlightsync: successfully opened translation file:", file) + + local data = {} + local fuzzy = false + local headers + local what = nil + while true do + local line = po:read("*l") + if line then + line = line:gsub("\r$", "") + end + if line == nil or line == "" then + if data.msgid and data.msgid_plural and data["msgstr[0]"] then + for k, v in pairs(data) do + local n = tonumber(k:match("msgstr%[([0-9]+)%]")) + local msgstr = v + + if n and msgstr then + addTranslation(data.msgctxt, data.msgid, msgstr, n) + end + end + elseif data.msgid and data.msgstr and data.msgstr ~= "" then + if not headers and data.msgid == "" then + headers = data.msgstr + local plural_forms = data.msgstr:match("Plural%-Forms: (.*)") + + -- Guard against missing Plural-Forms header + if not plural_forms then + GetText.getPlural = getDefaultPlural + else + local nplurals = plural_forms:match("nplurals=([0-9]+);") or 2 + local plurals = plural_forms:match("plural=%((.*)%);") + + if plurals == "n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && n % 10 == 0) ? 2 : 3)" then + plurals = "n == 1 ? 0 : (n == 2) ? 1 : (n > 10 && n % 10 == 0) ? 2 : 3" + end + if plurals == "n % 10 == 0 || n % 100 >= 11 && n % 100 <= 19) ? 0 : ((n % 10 == 1 && n % 100 != 11) ? 1 : 2" then + plurals = "n % 10 == 0 || n % 100 >= 11 && n % 100 <= 19 ? 0 : (n % 10 == 1 && n % 100 != 11) ? 1 : 2" + end + if plurals == "n == 1) ? 0 : ((n == 0 || n != 1 && n % 100 >= 1 && n % 100 <= 19) ? 1 : 2" then + plurals = "n == 1 ? 0 : (n == 0 || n != 1 && n % 100 >= 1 && n % 100 <= 19) ? 1 : 2" + end + + if not plurals then + plurals = plural_forms:match("plural=(.*);") + end + + if plurals and plurals:find("[^n!=%%<>&:()|?0-9 ]") then + plurals = GetText.plural_default + end + + local pl_tests = {} + if plurals then + for pl_test in plurals:gmatch("[^:]+") do + table.insert(pl_tests, pl_test) + end + end + + GetText.getPlural = getPluralFunc(pl_tests, nplurals, GetText.plural_default) + if not GetText.getPlural then + GetText.getPlural = getDefaultPlural + end + end + end + + addTranslation(data.msgctxt, data.msgid, data.msgstr) + end + if line == nil then break end + data = {} + what = nil + else + if not line:match("^#") then + local w, s = line:match("^%s*([%a_%[%]0-9]+)%s+\"(.*)\"%s*$") + if w then + what = w + else + s = line:match("^%s*\"(.*)\"%s*$") + end + if what and s and not fuzzy then + -- Single-pass string unescape (more efficient than 3 separate gsub) + s = s:gsub("\\([n\"\\\\])", function(c) + if c == "n" then return "\n" + elseif c == '"' then return '"' + else return "\\" + end + end) + data[what] = (data[what] or "") .. s + elseif what and s == "" and fuzzy then + -- Ignore fuzzy entries + else + fuzzy = false + end + elseif line:match("#, fuzzy") then + fuzzy = true + end + end + end + po:close() + GetText.current_lang = new_lang +end + +GetText_mt.__index.getPlural = getDefaultPlural + +function GetText_mt.__index.ngettext(msgid, msgid_plural, n) + local plural = GetText.getPlural(n) + + if plural == 0 then + return GetText.translation[msgid] and GetText.translation[msgid][plural] or GetText.wrapUntranslated(msgid) + else + return GetText.translation[msgid] and GetText.translation[msgid][plural] or GetText.wrapUntranslated(msgid_plural) + end +end + +function GetText_mt.__index.npgettext(msgctxt, msgid, msgid_plural, n) + local plural = GetText.getPlural(n) + + if plural == 0 then + return GetText.context[msgctxt] and GetText.context[msgctxt][msgid] and GetText.context[msgctxt][msgid][plural] or GetText.wrapUntranslated(msgid) + else + return GetText.context[msgctxt] and GetText.context[msgctxt][msgid] and GetText.context[msgctxt][msgid][plural] or GetText.wrapUntranslated(msgid_plural) + end +end + +function GetText_mt.__index.pgettext(msgctxt, msgid) + return GetText.context[msgctxt] and GetText.context[msgctxt][msgid] or GetText.wrapUntranslated(msgid) +end + +setmetatable(GetText, GetText_mt) + +-- highlightsync.koplugin specific setup +GetText.dirname = require("datastorage"):getDataDir() .. "/plugins/highlightsync.koplugin/l10n" + +local function loadCurrentLang() + local ok, err = pcall(function() + local lang = require("gettext").current_lang + if lang:match("^zh_Hans") then + lang = "zh_CN" + elseif lang:match("^zh_Hant") then + lang = "zh_TW" + end + GetText.changeLang(lang) + end) + if not ok then + logger.err("highlightsync: ERROR in loadCurrentLang:", err) + end +end + +loadCurrentLang() + +return GetText diff --git a/l10n/zh_CN/koreader.po b/l10n/zh_CN/koreader.po new file mode 100644 index 0000000..3381dc3 --- /dev/null +++ b/l10n/zh_CN/koreader.po @@ -0,0 +1,61 @@ +# Highlight Sync 插件简体中文翻译 +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# 本文件与 PACKAGE 包使用相同许可证。 +# +msgid "" +msgstr "Project-Id-Version: highlightsync.koplugin\n" + "Report-Msgid-Bugs-To: \n" + "POT-Creation-Date: 2025-01-01 12:00+0800\n" + "PO-Revision-Date: 2025-01-01 12:00+0800\n" + "Last-Translator: \n" + "Language-Team: \n" + "Language: 简体中文 (zh_CN)\n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=UTF-8\n" + "Content-Transfer-Encoding: 8bit\n" + "Plural-Forms: nplurals=1; plural=0;\n" + +#: ../_meta.lua:5 +msgid "Synchronize book highlights across devices using cloud storage (WebDAV/Dropbox)." +msgstr "使用云存储(WebDAV/Dropbox)跨设备同步书籍高亮。" + +#: ../main.lua:69 +msgid "Highlight Sync" +msgstr "高亮同步" + +#: ../main.lua:119 +msgid "Syncing book highlights. This may take a while." +msgstr "正在同步书籍高亮,请稍候。" + +#: ../main.lua:142 +msgid "Sync Cloud" +msgstr "云端设置" + +#: ../main.lua:162 +msgid "Delete" +msgstr "删除" + +#: ../main.lua:166 +msgid "Delete server info?" +msgstr "删除服务器信息?" + +#: ../main.lua:167 +msgid "Cancel" +msgstr "取消" + +#: ../main.lua:179 +msgid "Edit" +msgstr "编辑" + +#: ../main.lua:186 +msgid "Close" +msgstr "关闭" + +#: ../main.lua:193 +#, lua-format +msgid "Cloud storage:\n%1\n\nFolder path:\n%2\n\nSet up the same cloud folder on each device to sync across your devices." +msgstr "云存储:\n%1\n\n文件夹路径:\n%2\n\n在每台设备上设置相同的云文件夹以实现跨设备同步。" + +#: ../main.lua:205 +msgid "Sync Highlights" +msgstr "同步" diff --git a/l10n/zh_TW/koreader.po b/l10n/zh_TW/koreader.po new file mode 100644 index 0000000..2554d48 --- /dev/null +++ b/l10n/zh_TW/koreader.po @@ -0,0 +1,61 @@ +# Highlight Sync 插件繁體中文翻譯 +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# 本文件與 PACKAGE 包使用相同許可證。 +# +msgid "" +msgstr "Project-Id-Version: highlightsync.koplugin\n" + "Report-Msgid-Bugs-To: \n" + "POT-Creation-Date: 2025-01-01 12:00+0800\n" + "PO-Revision-Date: 2025-01-01 12:00+0800\n" + "Last-Translator: \n" + "Language-Team: \n" + "Language: 繁體中文 (zh_TW)\n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=UTF-8\n" + "Content-Transfer-Encoding: 8bit\n" + "Plural-Forms: nplurals=1; plural=0;\n" + +#: ../_meta.lua:5 +msgid "Synchronize book highlights across devices using cloud storage (WebDAV/Dropbox)." +msgstr "使用雲端儲存(WebDAV/Dropbox)跨裝置同步書籍高亮。" + +#: ../main.lua:69 +msgid "Highlight Sync" +msgstr "高亮同步" + +#: ../main.lua:119 +msgid "Syncing book highlights. This may take a while." +msgstr "正在同步書籍高亮,請稍候。" + +#: ../main.lua:142 +msgid "Sync Cloud" +msgstr "雲端設置" + +#: ../main.lua:162 +msgid "Delete" +msgstr "刪除" + +#: ../main.lua:166 +msgid "Delete server info?" +msgstr "刪除伺服器資訊?" + +#: ../main.lua:167 +msgid "Cancel" +msgstr "取消" + +#: ../main.lua:179 +msgid "Edit" +msgstr "編輯" + +#: ../main.lua:186 +msgid "Close" +msgstr "關閉" + +#: ../main.lua:193 +#, lua-format +msgid "Cloud storage:\n%1\n\nFolder path:\n%2\n\nSet up the same cloud folder on each device to sync across your devices." +msgstr "雲端儲存:\n%1\n\n資料夾路徑:\n%2\n\n在每台裝置上設置相同的雲端資料夾以實現跨裝置同步。" + +#: ../main.lua:205 +msgid "Sync Highlights" +msgstr "同步" diff --git a/main.lua b/main.lua index 73db389..9fe9f4f 100644 --- a/main.lua +++ b/main.lua @@ -6,7 +6,7 @@ local FFIUtil = require("ffi/util") local T = FFIUtil.template local InfoMessage = require("ui/widget/infomessage") local WidgetContainer = require("ui/widget/container/widgetcontainer") -local _ = require("gettext") +local _ = require("highlightsync_gettext") local SyncService = require("frontend/apps/cloudstorage/syncservice") local Merge = require("merge") local rapidjson = require("rapidjson")