This commit is contained in:
1F616EMO 2024-12-29 23:50:10 +08:00
commit 20ec4c7faa
No known key found for this signature in database
GPG Key ID: EF52EFA8E05859B2
15 changed files with 797 additions and 0 deletions

85
.luacheckrc Normal file
View File

@ -0,0 +1,85 @@
read_globals = {
"DIR_DELIM",
"INIT",
"core",
"dump",
"dump2",
"Raycast",
"Settings",
"PseudoRandom",
"PerlinNoise",
"VoxelManip",
"SecureRandom",
"VoxelArea",
"PerlinNoiseMap",
"PcgRandom",
"ItemStack",
"AreaStore",
"vector",
table = {
fields = {
"copy",
"indexof",
"insert_all",
"key_value_swap",
"shuffle",
}
},
string = {
fields = {
"split",
"trim",
}
},
math = {
fields = {
"hypot",
"sign",
"factorial"
}
},
}
files["babelfish_core"] = {
globals = {
"babelfish",
}
}
files["babelfish_engine_lingva"] = {
read_globals = {
"babelfish",
}
}
files["babelfish_chat"] = {
read_globals = {
"babelfish",
"beerchat",
}
}
files["babelfish_chat_history"] = {
read_globals = {
"babelfish",
"beerchat",
}
}
files["babelfish_preferred_langauge"] = {
globals = {
"babelfish",
}
}
files["babelfish_private_chat"] = {
read_globals = {
"babelfish",
}
}

41
README.md Normal file
View File

@ -0,0 +1,41 @@
# BabelFish... but done in another way
This mod allows Luanti players to communicate across language barriers.
## ... but why not the original BabalFish mod?
BabelFish was a great mod for breaking langauge barrier between players speaking different languages. However, it was unmaintained for 7 years, and many code became messy and inefficient. This rewrite is a drop-in replacement for most end users, and provides more method for developers to interact with BabelFish. Notable changes include:
* Guessing preferred language from the player's client language code
* Handles [Beerchat](https://content.luanti.org/packages/mt-mods/beerchat/) properly
* Shipped with [Lingva Translate](https://github.com/thedaviddelta/lingva-translate) support (Yandex Translate port will be avaliable soon)
* Register new translation engine with new mods instead of adding files into the core mod
## How to use?
Avaliable in `babelfish_core` mod:
* Use `/bbcodes` to list all avaliable languages and their alias.
Avaliable in `babelfish_chat` mod:
* Write `%<language code>` in a message to boardcast translation to other players
* e.g. "Hello %fr" would yield "Bonjour"
* Unlike the original BabelFish, you must leave spaces between the tag and other texts.
Avaliable in `babelfish_preferred_langauge` mod:
* Your preferred language is guessed when you first join the server.
* Fallbacks to English if your language is not supported.
* Use `/bblang <language code>` to set your preferred language.
* Use `/bblang` to check your preferred language.
Avaliable in `babelfish_chat_history` mod:
* Use `/babel <username>` to translate the last message sent by a user
* (Beerchat only) Use `/babel <username> <channel>` to translate the last message sent by a user on a channel
* If channel is unspecified, defaults to the executer's channel.
Avaliable in `babelfish_private_chat`
* User `/bbmsg <username> <message>` to send private messages to a player in their preferred language.

65
babelfish_chat/init.lua Normal file
View File

@ -0,0 +1,65 @@
-- babelfish_redo/babelfish_chat/init.lua
-- Translate by writing %<code>
-- Copyright (C) 2016 Tai "DuCake" Kedzierski
-- Copyright (C) 2024 1F616EMO
-- SPDX-License-Identifier: AGPL-3.0-or-later
local S = core.get_translator("babelfish_chat")
local function check_message(message)
local _, _, targetlang = message:find("%%([a-zA-Z-_]+)")
if targetlang then
local targetphrase = message:gsub("%%" .. targetlang, '', 1)
local new_targetlang = babelfish.validate_language(targetlang)
if not new_targetlang then
return false, targetlang
end
return new_targetlang, targetphrase
end
return false
end
local dosend
local function process(name, message, arg1)
local targetlang, targetphrase = check_message(message)
if not targetlang then
if targetphrase == 1 then
return core.chat_send_player(name, S("@1 is not a valid language.", targetphrase))
end
return
end
babelfish.translate("auto", targetlang, targetphrase, function(succeed, translated)
if not succeed then
if core.get_player_by_name(name) then
return core.chat_send_player(name, S("Could not translate message: @1", translated))
end
return
end
return dosend(name, translated, arg1)
end)
end
if core.global_exists("beerchat") then
dosend = function(name, translated, channel)
return beerchat.send_on_channel({
name = name,
channel = channel,
message = "[" .. babelfish.get_engine_label() .. "]: " .. translated,
_supress_babelfish_redo = true,
})
end
beerchat.register_callback("before_send_on_channel", function(name, msg)
if msg._supress_babelfish_redo then return end
local message = msg.message
return process(name, message, msg.channel)
end)
else
dosend = function(name, translated)
return core.chat_send_all(core.format_chat_message(name,
"[" .. babelfish.get_engine_label() .. "]: " .. translated))
end
core.register_on_chat_message(process)
end

5
babelfish_chat/mod.conf Normal file
View File

@ -0,0 +1,5 @@
name = babelfish_chat
title = Babelfish Redo: Chatroom Translation
description = Translate by writing %<code>
depends = babelfish_core
optional_depends = beerchat

View File

@ -0,0 +1,105 @@
-- babelfish_redo/babelfish_chat_history/init.lua
-- Translate messages in chat history
-- Copyright (C) 2016 Tai "DuCake" Kedzierski
-- Copyright (C) 2024 1F616EMO
-- SPDX-License-Identifier: AGPL-3.0-or-later
local S = core.get_translator("babelfish_chat_history")
---@type { [string]: { [string]: string } }
local chat_history = {}
local main_channel = "main"
local function record_message(name, channel, message)
if not chat_history[channel] then
chat_history[channel] = {}
end
chat_history[channel][name] = message
end
core.register_on_leaveplayer(function(player)
local name = player:get_player_name()
for channel, chn_data in pairs(chat_history) do
chn_data[name] = nil
if channel ~= main_channel and not next(chn_data) then
chat_history[channel] = nil
end
end
end)
local get_channel
local cmd_param
local is_player_subscribed_to_channel
if core.global_exists("beerchat") then
main_channel = beerchat.main_channel_name
beerchat.register_callback("on_send_on_channel", function(name, msg)
record_message(name, msg.channel, msg.message)
end)
cmd_param = S("<player name> [<channel name>]")
get_channel = function(name)
local channel = beerchat.get_player_channel(name)
if channel then
return channel
else
return beerchat.fix_player_channel(name, true)
end
end
is_player_subscribed_to_channel = beerchat.is_player_subscribed_to_channel
else
core.register_on_chat_message(function(name, message)
record_message(name, main_channel, message)
end)
cmd_param = S("<player name>")
get_channel = function() return main_channel end
is_player_subscribed_to_channel = function() return true end
end
core.register_chatcommand("babel", {
description = S("Translate last message sent by a player"),
params = cmd_param,
func = function(name, param)
local player = core.get_player_by_name(name)
if not player then
return false, S("You must be online to run this command.")
end
local target_lang = babelfish.get_player_preferred_language(name)
if not target_lang then
return false, S("Error while obtaining default language.")
end
local args = string.split(param, " ")
if not args[1] then
return false
end
local target_player, channel = args[1], args[2] or get_channel(name)
if not channel then
return false, S("Failed to get channel.")
end
if not is_player_subscribed_to_channel(name, channel) then
return false, S("You are not allowed to read messages from channel #@1!", channel)
end
if not (chat_history[channel] and chat_history[channel][target_player]) then
return false, S("@1 haven't sent anythign on @2.",
target_player, channel == main_channel and S("the main channel") or ("#" .. channel))
end
babelfish.translate("auto", target_lang, chat_history[channel][target_player], function(succeeded, translated)
if not core.get_player_by_name(name) then return end
if not succeeded then
return core.chat_send_player(name, S("Failed to get translation: @1", translated))
end
return core.chat_send_player(name,
"[" .. babelfish.get_engine_label() .. " #" .. channel .. " " .. target_player .. "]: " .. translated)
end)
return true
end,
})

View File

@ -0,0 +1,5 @@
name = babelfist_chat_history
title = Babelfish Redo: Chat History
description = Translate messages in chat history
depends = babelfish_core, babelfish_preferred_language
optional_depends = beerchat

177
babelfish_core/init.lua Normal file
View File

@ -0,0 +1,177 @@
-- babelfish_redo/babelfish_core/init.lua
-- High leve API for translating texts using one of the Babelfish enginess
-- Copyright (C) 2016 Tai "DuCake" Kedzierski
-- Copyright (C) 2024 1F616EMO
-- SPDX-License-Identifier: AGPL-3.0-or-later
local S = core.get_translator("babelfish_core")
babelfish = {}
local registered_on_engine_ready = {}
---Run function when the engine is ready, or if it is already ready, run it now.
---@param func fun()
function babelfish.register_on_engine_ready(func)
if not registered_on_engine_ready then
return func()
end
registered_on_engine_ready[#registered_on_engine_ready+1] = func
end
---@alias BabelFishCallback fun(succeed: boolean, string_or_err: string)
---@class (exact) BabelFishEngine
---@field translate fun(source: string, target: string, query: string, callback: BabelFishCallback)
---@field language_codes { [string]: string }
---@field language_alias { [string]: string }?
---@field mt_language_map { [string] : string }?
---@field compliance string?
---@field engine_label string?
local babelfish_engine
---Register a translate engine
---@param engine_def BabelFishEngine
function babelfish.register_engine(engine_def)
local mod_name = core.get_current_modname() or "??"
assert(type(engine_def) == "table",
"Invalid `engine_def` type (expected table, got " .. type(engine_def) .. ")")
assert(type(engine_def.translate) == "function",
"Invalid `engine_def.translate` type (expected function, got " .. type(engine_def.translate) .. ")")
assert(type(engine_def.language_codes) == "table",
"Invalid `engine_def.language_codes` type (expected table, got " .. type(engine_def.language_codes) .. ")")
if engine_def.language_alias == nil then
engine_def.language_alias = {}
else
assert(type(engine_def.language_alias) == "table",
"Invalid `engine_def.language_alias` type (expected table or nil, got "
.. type(engine_def.language_alias) .. ")")
end
if engine_def.mt_language_map == nil then
engine_def.mt_language_map = {}
else
assert(type(engine_def.mt_language_map) == "table",
"Invalid `engine_def.mt_language_map` type (expected table or nil, got "
.. type(engine_def.mt_language_map) .. ")")
end
if engine_def.engine_label == nil then
engine_def.engine_label = mod_name
else
assert(type(engine_def.engine_label) == "string",
"Invalid `engine_def.engine_label` type (expected string or nil, got "
.. type(engine_def.engine_label) .. ")")
end
if engine_def.compliance == nil then
engine_def.compliance = S("Translations are powered by @1", engine_def.engine_label)
else
assert(type(engine_def.compliance) == "string",
"Invalid `engine_def.compliance` type (expected string or nil, got "
.. type(engine_def.compliance) .. ")")
end
engine_def.mod_name = mod_name
babelfish_engine = engine_def
babelfish.register_engine = function()
return error("[babelfish_core] Attempt to registered more than one BabelFish engine "
.. "(already registered by " .. engine_def.mod_name .. ")")
end
for _, func in ipairs(registered_on_engine_ready) do
func()
end
registered_on_engine_ready = nil
end
core.register_on_mods_loaded(function()
if not babelfish_engine then
return error("[babelfish_core] Please enable one (and only one) BabelFish engine mod.")
end
end)
---Translate a given text
---@param source string Source language code. If `"auto"`, detect the language automatically.
---@param target string Target language code.
---@param query string String to translate.
---@param callback BabelFishCallback Callback to run after finishing (or failing) a request
function babelfish.translate(source, target, query, callback)
assert(type(source) == "string",
"Invalid `source` type (expected string or nil, got " .. type(source) .. ")")
assert(type(target) == "string",
"Invalid `target` type (expected string, got " .. type(target) .. ")")
assert(type(query) == "string",
"Invalid `query` type (expected string, got " .. type(query) .. ")")
assert(source == "auto" or babelfish_engine.language_codes[source],
"Attempt to translate from unsupported language " .. source)
assert(babelfish_engine.language_codes[target],
"Attempt to translate from unsupported language " .. target)
return babelfish_engine.translate(source, target, query, callback)
end
---Check whether a given language code is valid, and resolve any alias
---@param language string?
---@return string
---@nodiscard
function babelfish.validate_language(language)
if language == nil then
return "auto"
end
language = babelfish_engine.language_alias[language] or language
return babelfish_engine.language_codes[language] and language or nil
end
---Get name of a language
---@param language string
---@return string?
function babelfish.get_language_name(language)
if language == "auto" then
return S("Detect automatically")
end
return babelfish_engine.language_codes[language]
end
---Get language map: MT language code -> engine lanaguage code
---@return { [string]: string }
function babelfish.get_mt_language_map()
return table.copy(babelfish_engine.mt_language_map)
end
---Get language codes
---@return { [string]: string }
function babelfish.get_language_codes()
return table.copy(babelfish_engine.language_codes)
end
---Get engine compliance
---@return string
function babelfish.get_compliance()
return babelfish_engine.compliance
end
---Get engine engine_label
---@return string
function babelfish.get_engine_label()
return babelfish_engine.engine_label
end
core.register_chatcommand("bbcodes", {
description = S("List avaliable language codes"),
func = function ()
local lines = {}
for code, name in pairs(babelfish_engine.language_codes) do
lines[#lines+1] = code .. ": " .. name
local alias = {}
for src, dst in pairs(babelfish_engine.language_alias) do
if dst == code then
alias[#alias+1] = src
end
end
if #alias ~= 0 then
lines[#lines] = lines[#lines] .. " " .. S("(Alias: @1)", table.concat(alias, ", "))
end
end
return true, table.concat(lines, "\n")
end
})

3
babelfish_core/mod.conf Normal file
View File

@ -0,0 +1,3 @@
name = babelfish_core
title = Babelfish Redo: Core
description = High leve API for translating texts using one of the Babelfish engines

View File

@ -0,0 +1,162 @@
-- babelfish_redo/babelfish_engine_lingva/init.lua
-- Google Translate via the Lingva frontend
-- Copyright (C) 2016 Tai "DuCake" Kedzierski
-- Copyright (C) 2024 1F616EMO
-- SPDX-License-Identifier: AGPL-3.0-or-later
local http = assert(core.request_http_api(),
"Could not get HTTP API table. Add babelfish_engine_lingva to secure.http_mods")
local S = core.get_translator("babelfish_engine_lingva")
local engine_status = "init"
local language_codes = {}
local language_alias = {}
local serviceurl = core.settings:get("babelfish_engine_lingva.serviceurl")
if not serviceurl then
serviceurl = "https://lingva.ml/api/graphql"
core.log("warning",
"[babelfish_engine_lingva] babelfish_engine_lingva.serviceurl not specified, " ..
"using official instance (https://lingva.ml/api/graphql)")
end
local function graphql_fetch(query, func)
return http.fetch({
url = serviceurl,
method = "POST",
timeout = 10,
extra_headers = { "accept: application/graphql-response+json;charset=utf-8, application/json;charset=utf-8" },
post_data = core.write_json({
query = query
}),
}, function(responce)
if not responce.succeeded then
core.log("error", "[babelfish_engine_lingva] Error on requesting " .. query .. ": " .. dump(responce))
return func(false)
end
local data, err = core.parse_json(responce.data, nil, true)
if not data then
core.log("error", "[babelfish_engine_lingva] Error on requesting " .. query .. ": " .. err)
core.log("error", "[babelfish_engine_lingva] Raw data: " .. responce.data)
return func(false)
end
if data.errors then
core.log("error", "[babelfish_engine_lingva] Error on requesting " .. query .. ": ")
for i, error in ipairs(data.errors) do
local location_string = "?"
if error.locations then
local location_strings = {}
for _, location in ipairs(error.locations) do
location_strings[#location_strings + 1] = location.line .. ":" .. location.column
end
location_string = table.concat(location_strings, ", ")
end
core.log("error", string.format("[babelfish_engine_lingva] (%d/%d) Line(s) %s: %s (%s)",
i, #data.errors,
location_string, error.message, error.extensions and error.extensions.code or "UNKNOWN"))
if error.extensions and error.extensions.stacktrace then
core.log("error", "[babelfish_engine_lingva]Stacktrace:")
for _, line in ipairs(error.extensions.stacktrace) do
core.log("error", "[babelfish_engine_lingva] \t" .. line)
end
end
end
end
if not data.data then
return func(false)
end
return func(data.data)
end)
end
do
local valid_alias = {
["zh_HANT"] = {
"zht",
"zh-tw",
"zh-hant",
},
["zh"] = {
"zhs",
"zh-cn",
"zh-hans",
},
}
graphql_fetch("{languages{code,name}}", function(data)
if not data then
engine_status = "error"
return
end
local langs_got = {}
local alias_log_strings = {}
-- We assume all langauge supports bidirectional translation
for _, langdata in ipairs(data.languages) do
if langdata.code ~= "auto" then
language_codes[langdata.code] = langdata.name
langs_got[#langs_got + 1] = langdata.code
if valid_alias[langdata.code] then
for _, alias in ipairs(valid_alias[langdata.code]) do
language_alias[alias] = langdata.code
alias_log_strings[#alias_log_strings + 1] =
alias .. " -> " .. langdata.code
end
end
end
end
core.log("action", "[babelfish_engine_lingva] Got language list: " .. table.concat(langs_got, ", "))
core.log("action", "[babelfish_engine_lingva] Got language alias: " .. table.concat(alias_log_strings, "; "))
engine_status = "ready"
end)
end
---Function for translating a given text
---@param source string Source language code. If `"auto"`, detect the language automatically.
---@param target string Target language code.
---@param query string String to translate.
---@param callback BabelFishCallback Callback to run after finishing (or failing) a request
local function translate(source, target, query, callback)
if engine_status == "error" then
return callback(false, S("Engine error while initializing."))
elseif engine_status == "init" then
return callback(false, S("Engine not yet initialized."))
end
query = string.gsub(query, "\"", "\\\"")
graphql_fetch(
"{translation(source: \"" .. source .. "\", target: \"" .. target ..
"\", query: \"" .. query .. "\"){target{text}}}",
function(data)
if data then
return callback(true, data.translation.target.text)
end
return callback(false, S("Error getting translation"))
end)
end
local mt_language_map = {
["es_US"] = "es",
["lzh"] = "zh_HANT",
["zh_CN"] = "zh",
["zh_TW"] = "zh_HANT",
["sr_Cyrl"] = "sr",
["sr_Latn"] = "sr",
}
babelfish.register_engine({
translate = translate,
language_codes = language_codes,
language_alias = language_alias,
mt_language_map = mt_language_map,
compliance = nil, -- S("Translations are powered by Lingva"),
engine_label = "Lingva Translate",
})

View File

@ -0,0 +1,4 @@
name = babelfish_engine_lingva
title = Babelfish Redo: Lingva Engine
description = Google Translate via the Lingva frontend
depends = babelfish_core

View File

@ -0,0 +1,97 @@
-- babelfish_redo/babelfish_preferred_language/init.lua
-- Set and get player preferred languages
-- Copyright (C) 2016 Tai "DuCake" Kedzierski
-- Copyright (C) 2024 1F616EMO
-- SPDX-License-Identifier: AGPL-3.0-or-later
local S = core.get_translator("babelfish_preferred_language")
local language_map
local fallback_lang
babelfish.register_on_engine_ready(function()
language_map = babelfish.get_mt_language_map()
local settings_fallback_lang = core.settings:get("babelfish_preferred_language.fallback_lang")
fallback_lang = babelfish.validate_language(settings_fallback_lang)
if not fallback_lang or fallback_lang == "auto" then
core.log("error", "Invalid fallback language, using en")
fallback_lang = "en" -- out last hope
end
end)
---Guess the player's preferred language from player information
---@param name string
---@return string
function babelfish.guess_player_preferred_language(name)
local player_info = core.get_player_information(name)
if not player_info then return fallback_lang end
local lang_code = player_info.lang_code
lang_code = language_map[lang_code] or lang_code
lang_code = babelfish.validate_language(lang_code)
if not lang_code or lang_code == "auto" then
return fallback_lang
end
return lang_code
end
---Get a player's preferred lanaguage
---@param name string
---@return string
function babelfish.get_player_preferred_language(name)
local player = core.get_player_by_name(name)
if not player then return end
local meta = player:get_meta()
local preferred_language = meta:get_string("babelfish:preferred_language")
preferred_language = babelfish.validate_language(preferred_language)
if not preferred_language or preferred_language == "auto" then
preferred_language = babelfish.guess_player_preferred_language(name)
if not preferred_language then return end
meta:set_string("babelfish:preferred_language", preferred_language)
end
return preferred_language
end
---Set a player's preferred language
---@param name string
---@param lang string
function babelfish.set_player_preferred_languag(name, lang)
local player = core.get_player_by_name(name)
if not player then return end
local meta = player:get_meta()
return meta:set_string("babelfish:preferred_language", lang)
end
core.register_on_joinplayer(function(player)
-- Beautiful hack to update or generate preferred language
return babelfish.get_player_preferred_language(player:get_player_name())
end)
core.register_chatcommand("bblang", {
descriptio = S("Get or set preferred language"),
params = S("[<language code>]"),
func = function(name, param)
if param == "" then
local lang = babelfish.get_player_preferred_language(name)
return true, S("Preferred language: @1", lang and babelfish.get_language_name(lang) or S("Unknown"))
end
local lang = babelfish.validate_language(param)
if not lang or lang == "auto" then
return false, S("Invalid language code: @1", param)
end
local player = core.get_player_by_name(name)
if not player then
return false, S("You must be online to run this command.")
end
babelfish.set_player_preferred_languag(name, lang)
return true, S("Preferred language set to @1.", babelfish.get_language_name(lang))
end,
})

View File

@ -0,0 +1,4 @@
name = babelfish_preferred_language
title = Babelfish Redo: Preferred Language
description = Set and get player preferred languages
depends = babelfish_core

View File

@ -0,0 +1,38 @@
-- babelfish_redo/babelfish_private_chat/init.lua
-- Translate private chats
-- Copyright (C) 2016 Tai "DuCake" Kedzierski
-- Copyright (C) 2024 1F616EMO
-- SPDX-License-Identifier: AGPL-3.0-or-later
local S = core.get_translator("babelfish_private_chat")
core.register_chatcommand("bbmsg", {
params = core.translate("__builtin", "<name> <message>"),
description = S("Send a direct message to a player in their preferred langauge"),
privs = { shout = true },
func = function(name, param)
local sendto, message = param:match("^(%S+)%s(.+)$")
if not sendto then
return false
end
if not core.get_player_by_name(sendto) then
return false, core.translate("__builtin", "The player @1 is not online.", sendto)
end
local target_lang = babelfish.get_player_preferred_language(sendto)
babelfish.translate("auto", target_lang, message, function(succeeded, translated)
if not succeeded then
if core.get_player_by_name(name) then
return core.chat_send_player(name, S("Failed to get translation."))
end
return
end
core.log("action", "DM from " .. name .. " to " .. sendto
.. ": " .. translated)
core.chat_send_player(sendto, core.translate("__builtin", "DM from @1: @2",
name, "[" .. babelfish.get_engine_label() .. "]: " .. translated))
end)
return true, core.translate("__builtin", "Message sent.")
end,
})

View File

@ -0,0 +1,4 @@
name = babelfish_private_chat
title = Babelfish Redo: Private Chat
description = Translate private chats
depends = babelfish_core, babelfish_preferred_language

2
modpack.conf Normal file
View File

@ -0,0 +1,2 @@
title = Babelfish Redo
description = Translate chat messages into other languages