c59097b0ac
- i18n loader at apps/readest.koplugin/i18n.lua: isolated callable,
falls back to KOReader's gettext when a string is untranslated
- Translation catalog at locales/<i18next-code>/translation.po for 31
languages, mirroring apps/readest-app/public/locales/
- scripts/extract-i18n.js (scan _("...") and _([[...]]); preserve
existing, drop obsolete, add new) and scripts/apply-translations.js
(bulk import from /tmp/koplugin-translations/<lang>.json)
- Mirror apps/readest-app SyncInfoDialog: rename showMetaHashInfo to
showSyncInfo, dialog title "Sync Info", new Last Synced row computed
as max(last_synced_at_config, last_synced_at_notes) from doc_settings
- syncconfig.lua / syncannotations.lua mark per-book sync timestamps
on push/pull success
- Rename "Meta Hash" -> "Book Fingerprint" in koplugin and
apps/readest-app SyncInfoDialog.tsx; translations propagated to
all readest-app locales
- "book config" -> "reading progress" wording across user-facing
strings (matches QiuYukang fork terminology)
- Replace "Log out as " / "Login failed: " concat prefixes with
T(_("...%1..."), arg) placeholder pattern (RTL / verb-final friendly)
- pnpm lint:lua: luajit -b syntax check across koplugin .lua files;
soft-skips when luajit is missing locally; CI installs luajit and
runs the check unconditionally
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
150 lines
4.2 KiB
Lua
150 lines
4.2 KiB
Lua
local logger = require("logger")
|
|
local koreader_gettext = require("gettext")
|
|
|
|
local GetText = {
|
|
translation = {},
|
|
loaded_lang = nil,
|
|
}
|
|
|
|
local function unescapePoString(s)
|
|
s = s:gsub("\\n", "\n")
|
|
s = s:gsub("\\t", "\t")
|
|
s = s:gsub("\\r", "\r")
|
|
s = s:gsub('\\"', '"')
|
|
s = s:gsub("\\\\", "\\")
|
|
return s
|
|
end
|
|
|
|
local function loadPoFile(path)
|
|
local file = io.open(path, "r")
|
|
if not file then
|
|
logger.dbg("Readest i18n: cannot open translation file:", path)
|
|
return false
|
|
end
|
|
|
|
local entry = {}
|
|
local current
|
|
|
|
local function flush()
|
|
if entry.msgid and entry.msgid ~= "" and entry.msgstr and entry.msgstr ~= "" then
|
|
GetText.translation[entry.msgid] = entry.msgstr
|
|
end
|
|
entry = {}
|
|
current = nil
|
|
end
|
|
|
|
for line in file:lines() do
|
|
if line == "" then
|
|
flush()
|
|
elseif not line:match("^#") then
|
|
local key, value = line:match('^%s*(msgid)%s+"(.*)"%s*$')
|
|
if not key then
|
|
key, value = line:match('^%s*(msgstr)%s+"(.*)"%s*$')
|
|
end
|
|
if key then
|
|
current = key
|
|
entry[current] = unescapePoString(value)
|
|
else
|
|
value = line:match('^%s*"(.*)"%s*$')
|
|
if current and value then
|
|
entry[current] = (entry[current] or "") .. unescapePoString(value)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
flush()
|
|
file:close()
|
|
|
|
logger.info("Readest i18n: loaded translation file:", path)
|
|
return true
|
|
end
|
|
|
|
local function getPluginDir()
|
|
local info = debug.getinfo(1, "S")
|
|
local source = info and info.source
|
|
if source and source:sub(1, 1) == "@" then
|
|
source = source:sub(2)
|
|
end
|
|
return source and source:match("(.*/)") or "./"
|
|
end
|
|
|
|
local plugin_dir = getPluginDir()
|
|
|
|
-- KOReader stores locales with an underscore separator (e.g. zh_CN, pt_BR);
|
|
-- our catalog mirrors readest-app's i18next layout which uses hyphens (zh-CN).
|
|
local function getLanguage()
|
|
local lang = G_reader_settings and G_reader_settings:readSetting("language")
|
|
if type(lang) == "string" and lang ~= "" and lang ~= "C" then
|
|
return (lang:gsub("%..*", ""):gsub("_", "-"))
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function initTranslation()
|
|
GetText.translation = {}
|
|
|
|
local lang = getLanguage()
|
|
GetText.loaded_lang = lang
|
|
if not lang then return end
|
|
|
|
local candidates = { lang }
|
|
local base_lang = lang:match("^(%a+)-")
|
|
if base_lang then
|
|
table.insert(candidates, base_lang)
|
|
end
|
|
if lang:match("^zh") and lang ~= "zh-CN" then
|
|
table.insert(candidates, "zh-CN")
|
|
end
|
|
|
|
for _, candidate in ipairs(candidates) do
|
|
local path = plugin_dir .. "locales/" .. candidate .. "/translation.po"
|
|
if loadPoFile(path) then
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
local function translate(msgid)
|
|
local lang = getLanguage()
|
|
if lang ~= GetText.loaded_lang then
|
|
initTranslation()
|
|
end
|
|
return GetText.translation[msgid]
|
|
end
|
|
|
|
setmetatable(GetText, {
|
|
__call = function(_, msgid)
|
|
return translate(msgid) or koreader_gettext(msgid)
|
|
end,
|
|
})
|
|
|
|
function GetText.ngettext(msgid, msgid_plural, n)
|
|
local translated = translate(n == 1 and msgid or msgid_plural)
|
|
if translated then return translated end
|
|
if koreader_gettext.ngettext then
|
|
return koreader_gettext.ngettext(msgid, msgid_plural, n)
|
|
end
|
|
return n == 1 and GetText(msgid) or GetText(msgid_plural)
|
|
end
|
|
|
|
function GetText.pgettext(msgctxt, msgid)
|
|
local translated = translate(msgctxt .. "\004" .. msgid) or translate(msgid)
|
|
if translated then return translated end
|
|
if koreader_gettext.pgettext then
|
|
return koreader_gettext.pgettext(msgctxt, msgid)
|
|
end
|
|
return GetText(msgid)
|
|
end
|
|
|
|
function GetText.npgettext(msgctxt, msgid, msgid_plural, n)
|
|
local selected = n == 1 and msgid or msgid_plural
|
|
local translated = translate(msgctxt .. "\004" .. selected) or translate(selected)
|
|
if translated then return translated end
|
|
if koreader_gettext.npgettext then
|
|
return koreader_gettext.npgettext(msgctxt, msgid, msgid_plural, n)
|
|
end
|
|
return GetText.ngettext(msgid, msgid_plural, n)
|
|
end
|
|
|
|
return GetText
|