summaryrefslogtreecommitdiff
path: root/Data/BuiltIn/Libraries/lua-addons/addons/translate/translate.lua
diff options
context:
space:
mode:
Diffstat (limited to 'Data/BuiltIn/Libraries/lua-addons/addons/translate/translate.lua')
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/translate/translate.lua398
1 files changed, 398 insertions, 0 deletions
diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/translate/translate.lua b/Data/BuiltIn/Libraries/lua-addons/addons/translate/translate.lua
new file mode 100644
index 0000000..a33bbda
--- /dev/null
+++ b/Data/BuiltIn/Libraries/lua-addons/addons/translate/translate.lua
@@ -0,0 +1,398 @@
+--Copyright (c) 2014~2020, Byrthnoth
+--All rights reserved.
+
+--Redistribution and use in source and binary forms, with or without
+--modification, are permitted provided that the following conditions are met:
+
+-- * Redistributions of source code must retain the above copyright
+-- notice, this list of conditions and the following disclaimer.
+-- * Redistributions in binary form must reproduce the above copyright
+-- notice, this list of conditions and the following disclaimer in the
+-- documentation and/or other materials provided with the distribution.
+-- * Neither the name of <addon name> nor the
+-- names of its contributors may be used to endorse or promote products
+-- derived from this software without specific prior written permission.
+
+--THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+--ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+--WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+--DISCLAIMED. IN NO EVENT SHALL <your name> BE LIABLE FOR ANY
+--DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+--(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+--LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+--ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+--(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+--SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+_addon.name = 'Translate'
+_addon.version = '2.0.0.0'
+_addon.author = 'Byrth'
+_addon.commands = {'trans','translate'}
+
+
+language = 'english'
+trans_list = {}
+res = require 'resources'
+require 'sets'
+require 'lists'
+require 'pack'
+require 'strings'
+require 'katakana_to_romanji'
+search_comment = {ts = 0, reg = L{}, translated = false}
+
+handled_resources = S{
+ 'ability_recasts',
+ 'auto_translates',
+ 'buffs',
+ 'days',
+ 'elements',
+ 'items',
+ 'job_abilities',
+ 'job_traits',
+ 'jobs',
+ 'key_items',
+ 'monster_abilities',
+ 'monstrosity',
+ 'moon_phases',
+ 'races',
+ 'regions',
+ 'skills',
+ 'spells',
+ 'titles',
+ 'weapon_skills',
+ 'weather',
+ 'zones'
+ }
+
+green_open = string.char(0xEF,0x27)
+red_close = string.char(0xEF,0x28)
+
+green_col = ''--string.char(0x1E,2)
+rcol = ''--string.char(0x1E,1)
+
+local temp_str
+
+function to_a_code(num)
+ local first_byte,second_byte = math.floor(num/256),num%256
+ if first_byte == 0 or second_byte == 0 then return nil end
+ return string.char(0xFD,2,2,first_byte,second_byte,0xFD):escape()
+ -- 0xFD,2,2,8,37,0xFD :: 37 = %
+end
+
+function to_item_code(id)
+ local first_byte,second_byte = math.floor(id/256),id%256
+ local t = 0x07
+ if first_byte == 0 then
+ t = 0x09
+ first_byte = 0xFF
+ elseif second_byte == 0 then
+ t = 0x0A
+ second_byte = 0xFF
+ end
+ return string.char(0xFD,t,2,first_byte,second_byte,0xFD):escape()
+end
+
+function to_ki_code(id)
+ local first_byte,second_byte = math.floor(id/256),id%256
+ local t = 0x13
+ if first_byte == 0 then
+ t = 0x15
+ first_byte = 0xFF
+ elseif second_byte == 0 then
+ t = 0x16
+ second_byte = 0xFF
+ end
+ return string.char(0xFD,t,2,first_byte,second_byte,0xFD):escape()
+end
+
+function sanity_check(ja)
+ return (ja and string.len(ja) > 0 and ja ~= '%.')
+end
+
+function load_dict(dict)
+ for _,res_line in pairs(dict) do
+ local jp = windower.to_shift_jis(res_line.ja or ''):escape()
+ local jps = windower.to_shift_jis(res_line.jas or ''):escape()
+ if sanity_check(jp) and sanity_check(res_line.en) and jp~= res_line.en:escape() then
+ trans_list[jp] = green_col..res_line.en..rcol
+ end
+ if sanity_check(jps) and sanity_check(res_line.ens) and jps ~= res_line.ens:escape() then
+ trans_list[jps] = green_col..res_line.ens..rcol
+ end
+ end
+end
+
+load_dict(katakana_to_romanji)
+
+for res_name in pairs(handled_resources) do
+ local resource = res[res_name] or {}
+ if res_name == 'auto_translates' then
+ for autotranslate_code,res_line in pairs(resource) do
+ local jp = windower.to_shift_jis(res_line.ja or ''):escape()
+ if sanity_check(jp) and not trans_list[jp] and jp ~= res_line.en:escape() then
+ trans_list[jp] = to_a_code(autotranslate_code)
+ end
+ end
+ elseif res_name == 'items' then
+ for id,res_line in pairs(resource) do
+ local jp = windower.to_shift_jis(res_line.ja or ''):escape()
+ if sanity_check(jp) and not trans_list[jp] and jp ~= res_line.en:escape() then
+ trans_list[jp] = to_item_code(id)
+ end
+ end
+ elseif res_name == 'key_items' then
+ for id,res_line in pairs(resource) do
+ local jp = windower.to_shift_jis(res_line.ja or ''):escape()
+ if sanity_check(jp) and not trans_list[jp] and jp ~= res_line.en:escape() then
+ trans_list[jp] = to_ki_code(id)
+ end
+ end
+ elseif res_name == 'jobs' then
+ for _,res_line in pairs(resource) do
+ local jp = windower.to_shift_jis(res_line.ja or ''):escape()
+ local jps = windower.to_shift_jis(res_line.jas or ''):escape()
+ if sanity_check(jp) and sanity_check(res_line.en) and jp ~= res_line.en:escape() then
+ trans_list[jp] = green_col..res_line.en..rcol:escape()
+ end
+ if sanity_check(jps) and sanity_check(res_line.ens) and jp ~= res_line.en:escape() and jps ~= res_line.ens:escape() and res_line.ens ~= 'PUP' then
+ trans_list[jps] = green_col..res_line.ens..rcol:escape()
+ end
+ end
+ else
+ for _,res_line in pairs(resource) do
+ local jp = windower.to_shift_jis(res_line.ja or ''):escape()
+ local jps = windower.to_shift_jis(res_line.jas or ''):escape()
+ if sanity_check(jp) and not trans_list[jp] and sanity_check(res_line.en) and jp ~= res_line.en:escape() then
+ trans_list[jp] = green_col..res_line.en..rcol:escape()
+ end
+ if sanity_check(jps) and not trans_list[jps] and sanity_check(res_line.ens) and jp ~= res_line.en:escape() and jps ~= res_line.ens:escape() then
+ trans_list[jps] = green_col..res_line.ens..rcol:escape()
+ end
+ end
+ end
+end
+
+local custom_dict_names = S(windower.get_dir(windower.addon_path..'dicts/')):filter(string.endswith-{'.lua'}):map(string.sub-{1, -5})
+for dict_name in pairs(custom_dict_names) do
+ local dict = dofile(windower.addon_path..'dicts/'..dict_name..'.lua')
+ if dict then
+ load_dict(dict)
+ end
+end
+
+function print_bytes(str)
+ local c = ''
+ local i = 1
+ while i <= #str do
+ c = c..' '..str:byte(i)
+ i = i + 1
+ end
+ return c
+end
+
+trans_list[string.char(0x46)] = nil
+trans_list['\.'] = nil
+
+
+windower.register_event('incoming chunk',function(id,orgi,modi,is_injected,is_blocked)
+ if id == 0x17 and not is_injected and not is_blocked then
+ local out_text = modi:unpack('z',0x18)
+
+ out_text = translate_phrase(out_text)
+
+ if not out_text then return end
+
+ if show_original then windower.add_to_chat(8,modi:sub(9,0x17):unpack('z',1)..'[Original]: '..modi:unpack('z',0x18)) end
+ while #out_text > 0 do
+ local boundary = get_boundary_length(out_text,151)
+ local len = math.ceil((boundary+1+23)/2) -- Make sure there is at least one nul after the string
+ local out_pack = string.char(0x17,len)..modi:sub(3,0x17)..out_text:sub(1,boundary)
+
+ -- zero pad it
+ while #out_pack < len*2 do
+ out_pack = out_pack..string.char(0)
+ end
+ windower.packets.inject_incoming(0x17,out_pack)
+ out_text = out_text:sub(boundary+1)
+ end
+ return true
+ end
+end)
+
+function translate_phrase(out_text)
+ local matches,match_bool = {},false
+ local function make_matches(catch)
+ -- build a table of matches indexed by their length
+ local esc = catch:escape()
+ if not sanity_check(esc) then return end
+
+
+
+ if not matches[#catch] then
+ matches[#catch] = {}
+ end
+ matches[#catch][#matches[#catch]+1] = esc
+ match_bool = true
+ end
+ for jp,en in pairs(trans_list) do
+ out_text:gsub(jp,make_matches)
+ end
+
+ if not match_bool then return end
+
+ local order = {}
+ for len,_ in pairs(matches) do
+ local c,found = 1,false
+ while c <= #order do
+ if len > order[c] then
+ table.insert(order,c,len)
+ found = true
+ break
+ end
+ c = c + 1
+ end
+ if c > #order then order[c] = len end
+ end
+
+ for _,ind in ipairs(order) do
+ for _,option in ipairs(matches[ind]) do
+ out_text = sjis_gsub(out_text,unescape(option),unescape(trans_list[option]))
+ end
+ end
+ return out_text
+end
+
+function get_boundary_length(str,limit)
+ -- If it is already short enough, return it
+ if #str <= limit then return #str end
+
+ local lim = 0
+ for i= 1,#str do
+ local c_byte = str:byte(i)
+ if c_byte == 0xFD then
+ i = i + 6
+ elseif ( (c_byte > 0x7F and c_byte <= 0xA0) or c_byte >= 0xE0) then
+ i = i + 2
+ else
+ i = i + 1
+ end
+ if i > limit then
+ return lim
+ else
+ lim = i
+ end
+ end
+
+ -- Otherwise, try to pick a spot to split that will not interfere with command codes and such
+--[[ local boundary = limit
+ for i=limit-5,limit do
+ local c_byte = str:byte(i)
+ if c_byte ==0xFD then
+ -- 0xFD: Autotranslate code, 6 bytes
+ boundary = i-1
+ break
+ elseif c_byte == 0xEF and str:byte(i+1) == 0x27 then
+ -- Opening green (
+ boundary = i-1
+ break
+ elseif i == limit and ( (c_byte > 0x7F and c_byte <= 0xA0) or c_byte >= 0xE0) then
+ -- Double-byte shift_JIS character
+ boundary = i-1
+ break
+ end
+ end
+ return boundary]]
+end
+
+windower.register_event('addon command', function(...)
+ local commands = {...}
+ if not commands[1] then return end
+ if commands[1]:lower() == 'show' then
+ if commands[2] and commands[2]:lower() == 'original' then
+ show_original=not show_original
+ if show_original then
+ print('Translate: Showing the original text line.')
+ else
+ print('Translate: Hiding the original text line.')
+ end
+ end
+ elseif commands[1]:lower() == 'eval' and commands[2] then
+ table.remove(commands,1)
+ assert(loadstring(table.concat(commands, ' ')))()
+ end
+end)
+
+windower.register_event('incoming text',function(org,mod,ocol,mcol,blk)
+ if not blk and ocol == 204 then
+ local ret = translate_phrase(mod)
+ temp_str = ret or org
+ if ret then
+ if show_original then
+ windower.add_to_chat:schedule(0.3, 8,'[Original]: '..org)
+ end
+ mod = ret
+ if org == temp_str then
+ blk = true
+ temp_str = ''
+ end
+ return blk and blk or mod
+ end
+ end
+end)
+
+function unescape(str)
+ return (str:gsub('%%([%%%%^%$%*%(%)%.%+%?%-%]%[])','%1'))
+end
+
+
+-- Two problems with how I currently do this:
+-- 1: It is possible to have something like 0x94, (0x92, 0x8B,) 0xE1, which are two JP characters that contain a third.
+-- 2: It is possible to have a gsub replace something with an autotranslate code, which then causes a later dictionary
+-- option to match part of the replacement.
+-- If I solve #1, will #2 be an issue? No, it should not be.
+
+function sjis_gsub(str,pattern,rep)
+ if not (type(rep) == 'function' or type(rep) == 'string') then return str end
+ local str_len,pat_len,ret_str = string.len(str),string.len(pattern),str
+ local i = 1
+ while i<=str_len-pat_len+1 do
+ local c_byte = str:byte(i)
+ if str:sub(i,i+pat_len-1) == pattern then
+ if type(rep) == 'function' then
+ ret_str = rep(pattern) or str
+ -- No recursion for functions at the moment, because this addon doesn't need it
+ return
+ elseif type(rep) == 'string' then
+ if i ~= 1 then
+ -- Not the beginning
+ ret_str = str:sub(1,i-1)..rep
+ else
+ -- The beginning
+ ret_str = rep
+ end
+ if i+pat_len <= str_len-pat_len+1 then
+ -- i == 13, pat_len == 2, str_len == 16
+ -- Match is characters 13 and 14. Could conceivably match again to characters 15 and 16.
+
+ -- Send the remainder of the string back through recursively.
+ return ret_str..sjis_gsub(str:sub(i+pat_len),pattern,rep)
+ elseif i+pat_len <= str_len then
+ -- i == 14, pat_len == 2, str_len == 16
+ -- Match is characters 14 and 15, so 16 can't possibly be a match but needs to be stuck on there
+ return ret_str..str:sub(i+pat_len)
+ else
+ -- i == 15, pat_len == 2, str_len == 16
+ -- Match is characters 15 and 16, so no further addition is necessary
+ return ret_str
+ end
+ end
+ elseif c_byte == 0xFD then
+ i = i + 6
+ elseif ( (c_byte > 0x7F and c_byte <= 0xA0) or c_byte >= 0xE0) then
+ i = i + 2
+ else
+ i = i + 1
+ end
+ end
+ return ret_str
+end