AzDesign Localization
Authors: AzDesign
Version: 1.0
Type: Multilingual Game Tool
Key Term: Game Utility
IntroductionDo you want people can play your games without any language restriction ? This script add a simple functionality to store dialogues in different languages. Each dialogue has different IDs and the game will search its text according to the what language has been set. The language files itself are simple .txt file, which everyone can add and modify it freely without having to learn specific scripting skills. Grab the demo now
Features1. Easy to create and modify language files, all you need is a notepad++ (recommended). The file will be saved as common .txt
2. Support non-standard characters such as chinese and japanese characters
3. A debug feature that will detect any text error such as
bad/duplicate IDs and
text missing/too big/too many lines.
4. Automatically add lines after specific word that have reached the maximum message window width limit.
5. Different configuration which affect the script behavior such as :
a.
Dialogue search method :
-Cached : Upon game start or language changes, all dialogue files will be stored in memory and sorted by their IDs for faster access
-Streaming : Always re-read the file for specific dialogue. You change the language file and look at the result directly while the game running
b.
Localization error behavior :
-Strict : Halt and exit upon receiving text error. Intended for final release which make sure the dialogue files are in normal condition.
-Loose : Tolerate most error for debugging purpose. Both method always show
which line produce this error c.
Line splitting method :
-Word : Adding new line after specific
word that have reach width limit, best for most language
-Char : Adding new line after specific
character that have reach width limit, best for japanese language
Spoilers
DemoPlease make sure to set the library to RGSS103J, otherwise, the script will hung up. The demo has already set with RGSS103JPlease make sure to set your system locale into japanese to display non-standard character properlyhttp://www.mediafire.com/?vscc64d5y1wqy5h
Script#-----------------------------------------------------------#
#------------AzDesign Localization Script V.1---------------#
#------------------my.az.design@gmail.com-------------------#
#---------------License : Creative Commons :----------------#
#-----Attribution-NonCommercial-ShareAlike 3.0 Unported-----#
#-----------------------------------------------------------#
module Localization
#delimiter for each dialogue pairs
LOCALIZATION_DELIMITER = '='
#default language in case there are no selected language available
LOCALIZATION_DEFAULT = 'ENG'
#line number location for storing localization file's credit such as language name and author
LOCALIZATION_CREDITS_LINE = 1
#line number location for storing localizatio split method, expecting word, or char, else = word
LOCALIZATION_METHOD_LINE = 2
#starting line number for searching dialogue pairs
LOCALIZATION_START_LINE = 3
#location of localization folder
LOCALIZATION_FOLDER = 'Localization'
#name of the configuration file, which stores the name of language currently used and other options
LOCALIZATION_CONFIG = 'config.txt'
#replacement for empty values
LOCALIZATION_TEXT_DEFAULT = 'empty'
#maximum lines allowed for each dialogue
LOCALIZATION_MAX_LINE = 4
#The maximum width of text in message windows, please refer to Window_Message class
MESSAGE_WINDOW_WIDTH = 380
#The maximum height of text in message windows, please refer to Window_Message class
MESSAGE_WINDOW_HEIGHT = 160
#cached means that the dialogue were cached upon game start into a hash
#--This is the recommended method to search dialogue files from pre-cached variable for faster performance.
#stream means everytime the game require specific dialogue, it search the file for its pair.
#--This is useful if the author want to look at the changes made with the dialogue file real time along with the game run
#--Warning, this method requires access to file every time its called, which means, can slow the game performance along the size of dialogue file.
@method = 'cached'
#strict = exit game upon receiving errors such as bad ID, empty value
#loose = errors were merely treated as warnings.
@behavior = 'loose'
#initialize container to store dialogues
@dialogues = Hash.new
#splitting method, define how automatic lining works, there are 2 type : word(default) and char
#word means splitting technique after specific word, very recommended for alphabets / most languages
#char means splitting technique after number of character, very recommended for alphabets / most languages
@split_method = 'word'
#initialization method declaration
@character_split = 1
#initialization method declaration
def self.init
@errors = {
:folder => nil,
:def_lang => nil,
:cur_lang => nil,
:save_lang => nil,
:change_lang => nil,
:bad_id => Array.new,
:duplicate => Array.new,
:empty => Array.new,
:line => Array.new,
:size => Array.new
}
#make sure localization folder exists
if !File.directory?(LOCALIZATION_FOLDER) then @errors[:folder] = 1 ; self.check end
#make sure default language file exists
if !File.exist?("#{LOCALIZATION_FOLDER}\\#{LOCALIZATION_DEFAULT}.txt") then @errors[:def_lang] = 1 ; self.check end
#set localization configuration file
@filename = "#{LOCALIZATION_FOLDER}\\#{LOCALIZATION_CONFIG}"
if !File.exists?(@filename)
begin
File.open(@filename, 'wb') {|file| file.write(LOCALIZATION_DEFAULT) }
rescue
@errors[:save_lang] = 1
self.check
end
end
change('language')
end
def self.read(id)
text = ''
case @method
when 'streaming'
#declare flag to determine whether the dialogue being searched was found or not
text = ''
found = false
#re-open the file
@file = File.new(@filename)
index = 1
@file.each { |line|
#ignore empty lines and lines without a delimiter
if !line.empty? && index >= LOCALIZATION_START_LINE && line.include?('=')
#get the text before delimiter as key or ID
key = line.split(LOCALIZATION_DELIMITER)[0]
#search for bad IDs
if !(key =~ /^(\w+)$/) then @errors[:bad_id].push(index) end
#search for duplicate keys
if @dialogues.has_key?(key.to_sym) then @errors[:duplicate].push(index) end
if key == id
#get the value for the rest of the string after delimiter
val = line.slice(key.size+LOCALIZATION_DELIMITER.size,line.size).squeeze(" ")
#adjusting any text which will exceed line length limit
if self.get('cur_width',val) < MESSAGE_WINDOW_WIDTH * LOCALIZATION_MAX_LINE
if self.get('cur_width',val) >= MESSAGE_WINDOW_WIDTH then
#prepare any local variables for splitting process
last = ''
adjusted_string = ''
run = 1
new_line = true
text_chunks = Array.new
line_chunks = Array.new
tolerance = 0
#set the character limit if splitting method was set to character.
if @split_method == 'char' then @character_split = self.get('max_line_width') end
val.split('\n').each { |fragment| text_chunks.push(fragment) }
text_chunks = text_chunks.reverse
while text_chunks.size > 0
last = text_chunks[text_chunks.size - 1] + "\n"
text_chunks.pop
if self.get('cur_width',last) >= MESSAGE_WINDOW_WIDTH
#commence automatic line adding by words and maximum length per line, work best for alphabets
if @split_method == 'word'
last.split(' ').each { |fragment2| line_chunks.push(fragment2) }
line_chunks = line_chunks.reverse
while line_chunks.size > 0
if self.get('cur_width',adjusted_string) + self.get('cur_width',line_chunks[line_chunks.size - 1]) - tolerance > MESSAGE_WINDOW_WIDTH * run then adjusted_string << "\n" ; run += 1 ; newline = true ; end
#if (adjusted_string + line_chunks[line_chunks.size - 1]).size - tolerance > 50 * run then adjusted_string += "\n" ; run += 1 ; newline = true ; end
if newline == true
adjusted_string << line_chunks[line_chunks.size - 1]
newline = false
else
adjusted_string << ' ' + line_chunks[line_chunks.size - 1]
end
line_chunks.pop
end
elsif @split_method == 'char'
#commence automatic line adding by number of characters and maximum length per line, intended for non-standard character
#for best result, use one of monospace fonts
adjusted_string = last.scan(/.{#{@character_split}}|.+/).join("\n")
end
else
tolerance += self.get('cur_width',last)
adjusted_string << last
newline = true
end
end
val = adjusted_string.lstrip
end
#make sure the value are present
if val.empty?
text = LOCALIZATION_TEXT_DEFAULT
elsif val.scan(/\n/).size - 1 > LOCALIZATION_MAX_LINE
text = "Warning, this text has too many lines, line #{index}"
else
text = val
end
else
text = "Warning, this text was too big, line #{index}"
end
#set the found flag
found = true
end
end
index += 1
}
if !found then text = "ID[#{id}] not found" end
else
if @dialogues[id.to_sym] != nil
text = String.new(@dialogues[id.to_sym])
else
text = "invalid ID specified"
end
end
return text.chomp
end
def self.check()
@errors.each { |key,val|
if val != nil && val.class != Array
case key
when :folder
print "Localization folder is missing,\nthis game will not run without its contents"
exit
when :def_lang
print "Default language file is missing,\nthis game will not run without it"
exit
when :save_lang
#this error is very rare, only happen in a very strictly set UAC or intentional file modification by user
print "Unable to modify localization file\nPlease make sure this game has elevated priveleges"
print "Run the game as administrator\nOr simply place the game folder in other than C:/ drive"
exit
when :change_lang
print "Language has been changed into : #{val}"
when :cur_lang
if @behavior == 'strict'
print "#{@current_language} was not found\nMake sure the specified language file is present in Localization folder"
exit
else
print "#{@current_language} was not found, switching to default language"
end
end
elsif val != nil && val.class == Array
if val.size > 0
text = ''
val.each { |v|
if text.empty?
text << (v.to_s)
else
text << (', ' + v.to_s)
end
}
case key
when :bad_id
print "Bad IDs detected on line :\n#{text}\nAlphabets, numbers & Underscores only\n"
when :empty
print "Empty values detected on line :\n#{text}\nEmpty values will return 'empty value'\n"
when :duplicate
print "Duplicate keys detected on line :\n#{text}\nDuplicate values are be ignored\n"
when :line
print "Too many lines detected on line :\n#{text}\nThis game allow a maximum of #{LOCALIZATION_MAX_LINE} lines per dialogue\n"
when :size
print "Big text detected on line :\n#{text}\nThis game allow a maximum of #{MESSAGE_WINDOW_WIDTH*LOCALIZATION_MAX_LINE} total dialogue width\n"
end
if @behavior == 'strict' then exit end
end
end
}
@errors = {
:folder => nil,
:def_lang => nil,
:cur_lang => nil,
:save_lang => nil,
:change_lang => nil,
:bad_id => Array.new,
:duplicate => Array.new,
:empty => Array.new,
:line => Array.new,
:size => Array.new
}
end
#change different variables which will change how localization works in general
def self.change(subject,value = nil)
case subject
when 'behavior'
@behavior = value
when 'method'
@method = value
when 'language'
different_language = false
@filename = "#{LOCALIZATION_FOLDER}\\#{LOCALIZATION_CONFIG}"
#clear dialogue files in case of changing a language that already cached
@dialogues.clear
#save the new language in configuration file
if value != nil && value != @current_language
begin
File.open(@filename, 'wb') {|file| file.write(value.upcase) }
rescue
@errors[:save_lang] = 1
self.check
end
different_language = true
end
#set the current language and filename being used based on the language specified in configuration file
@current_language = IO.readlines(@filename)[0].chomp.upcase
@filename = "#{LOCALIZATION_FOLDER}\\#{@current_language}.txt"
if !File.exists?(@filename)
@errors[:cur_lang] = 1
self.check
change('language',LOCALIZATION_DEFAULT)
return
end
#retrieve split method and language name from localization file
@split_method = IO.readlines(@filename)[LOCALIZATION_METHOD_LINE - 1].chomp
#set default value for splitting method if specified method is not recognized.
if @split_method != 'word' && @split_method != 'char' then @split_method = 'word' end
#set the character limit if splitting method was set to character.
if @split_method == 'char' then @character_split = self.get('max_line_width') end
#set language name from the first line of localization file
@language_name = IO.readlines(@filename)[LOCALIZATION_CREDITS_LINE - 1].split('-')[0]
if different_language then @errors[:change_lang] = @language_name ; self.check end
#caching dialogue files from localization file. Also, searching for any error in the file
if @method != 'streaming'
@file = File.new(@filename)
index = 1
@file.each { |line|
#ignore empty lines and lines without a delimiter
if !line.empty? && index >= LOCALIZATION_START_LINE && line.include?('=')
#get the text before delimiter as key or ID
key = line.split(LOCALIZATION_DELIMITER)[0]
#make sure the first part was a key or ID, formed by combination of aphabets, numbers and underscores only
if !(key =~ /^(\w+)$/)
@errors[:bad_id].push(index)
else
#make sure there are no duplicate keys
if @dialogues.has_key?(key.to_sym)
@errors[:duplicate].push(index)
else
#get the value for the rest of the string after delimiter
val = line.slice(key.size+LOCALIZATION_DELIMITER.size,line.size).squeeze(" ")
#adjusting any text which will exceed line length limit
if self.get('cur_width',val) < MESSAGE_WINDOW_WIDTH * LOCALIZATION_MAX_LINE
if self.get('cur_width',val) >= MESSAGE_WINDOW_WIDTH then
#prepare any local variables for splitting process
last = ''
adjusted_string = ''
run = 1
new_line = true
text_chunks = Array.new
line_chunks = Array.new
tolerance = 0
val.split('\n').each { |fragment| text_chunks.push(fragment) }
text_chunks = text_chunks.reverse
while text_chunks.size > 0
last = text_chunks[text_chunks.size - 1] + "\n"
text_chunks.pop
if self.get('cur_width',last) >= MESSAGE_WINDOW_WIDTH
#commence automatic line adding by words and maximum length per line, work best for alphabets
if @split_method == 'word'
last.split(' ').each { |fragment2| line_chunks.push(fragment2) }
line_chunks = line_chunks.reverse
while line_chunks.size > 0
if self.get('cur_width',adjusted_string) + self.get('cur_width',line_chunks[line_chunks.size - 1]) - tolerance > MESSAGE_WINDOW_WIDTH * run then adjusted_string << "\n" ; run += 1 ; newline = true ; end
#if (adjusted_string + line_chunks[line_chunks.size - 1]).size - tolerance > 50 * run then adjusted_string += "\n" ; run += 1 ; newline = true ; end
if newline == true
adjusted_string << line_chunks[line_chunks.size - 1]
newline = false
else
adjusted_string << ' ' + line_chunks[line_chunks.size - 1]
end
line_chunks.pop
end
elsif @split_method == 'char'
#commence automatic line adding by number of characters and maximum length per line, intended for non-standard character
#for best result, use one of monospace fonts
adjusted_string = last.scan(/.{#{@character_split}}|.+/).join("\n")
end
else
tolerance += self.get('cur_width',last)
adjusted_string << last
newline = true
end
end
val = adjusted_string.lstrip
end
#make sure the value are present
if val.empty?
@errors[:empty].push(index)
elsif val.scan(/\n/).size - 1 > LOCALIZATION_MAX_LINE
@errors[:line].push(index)
else
#insert each pair into the $dialogue hash
@dialogues[key.to_sym] = val
end
else
@errors[:size].push(index)
end
end
end
end
index += 1
}
end
self.check
end
end
#get various attributes
def self.get(subject,value = nil)
case subject
#get text height and width in pixel
when 'cur_width'; return Bitmap.new(1, 1).text_size(value).width
when 'cur_height'; return Bitmap.new(1, 1).text_size(value).height
when 'max_line_width'
multiplier = 1
text = 'あ' * multiplier
while Bitmap.new(1, 1).text_size(text).width < MESSAGE_WINDOW_WIDTH
multiplier += 1
text = 'あ' * multiplier
end
return multiplier
end
end
#scan any localization-related command and return its result to draw text method
def self.scan(command)
if command != nil
return command.gsub(/\\[Dd][Ll][Gg]\[(.+)\]/) { self.read($1) }
end
end
#call initialization method at the beginning of the game
Localization.init
end
#--------#
#OVERRIDE#
#--------#
class Game_Temp
def message_text=(string)
@message_text = Localization.scan(string)
end
end
class Interpreter
alias old_command_101 command_101
def command_101
# If other text has been set to message_text
if $game_temp.message_text != nil
# End
return false
end
# Set message end waiting flag and callback
@message_waiting = true
$game_temp.message_proc = Proc.new { @message_waiting = false }
# Set message text on first line
#----------------Make sure new lines treated properly----------------#
$game_temp.message_text = Localization.scan(@list[@index].parameters[0]) + "\n"
line_count = $game_temp.message_text.scan(/\n/).size
#----------------End of overriding----------------#
# Loop
loop do
# If next event command text is on the second line or after
if @list[@index+1].code == 401
# Add the second line or after to message_text
$game_temp.message_text += @list[@index+1].parameters[0] + "\n"
line_count += 1
# If event command is not on the second line or after
else
# If next event command is show choices
if @list[@index+1].code == 102
# If choices fit on screen
if @list[@index+1].parameters[0].size <= 4 - line_count
# Advance index
@index += 1
# Choices setup
$game_temp.choice_start = line_count
setup_choices(@list[@index].parameters)
end
# If next event command is input number
elsif @list[@index+1].code == 103
# If number input window fits on screen
if line_count < 4
# Advance index
@index += 1
# Number input setup
$game_temp.num_input_start = line_count
$game_temp.num_input_variable_id = @list[@index].parameters[0]
$game_temp.num_input_digits_max = @list[@index].parameters[1]
end
end
# Continue
return true
end
# Advance index
@index += 1
end
end
end
class Bitmap
alias old_draw_text draw_text
def draw_text(*args)
args.each_index {|i| if args[i].is_a?(String) then args[i] = Localization.scan(args[i]) end }
old_draw_text(*args)
end
end
Instructions1. Using the script :
The demo contains most of explanation that will help you use it
2. Creating Language file
-Download and install notepad++, very recommended
-Set the encoding as "UTF-8 without BOM"
-There are 3 different sections required for a language file :
Line 1 - Contains the language name and credits to the author, format : "(name of the language)-(credits)"
Line 2 - Contains the Line Splitting Method, which should be "word" or "char". Decide which suit the language best
Starting at line 3 - Contain the dialogue, which paired by ID and its text. format : "(ID)=(text)" ID can only consist of combination of alphabets, underscores and digits while text can be anything. If you want to add new line in the middle of the text, add "\n", that will tell the game to break the text into new line.
-Save the file as .txt file. The filename should be an obvious country small name such as English = ENG, Japan = JP, Indonesia = INA, etc. For example, english language file will be ENG.txt.
-Change the language, please refer to the demo on how to change the language
CompatibilityNot compatible with most other custom message script. For now, I cannot take any request for compatibility script as I got very busy working on my projects. Maybe later. Feel free to make your own compatibility script, I will link them here later
Credits and ThanksCredit to AzDesign for the script
Thanks to everyone who has answered my question in different topic I posted in the past.
Thanks to ForeverZer0 whose Localization script become my learning base.
LicenseCreative Commons - Attribution-NonCommercial-ShareAlike 3.0 Unported
(
http://creativecommons.org/licenses/by-nc-sa/3.0/ )
You are free:
to Share - to copy, distribute and transmit the work
to Remix - to adapt the work
Under the following conditions:
Attribution. You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work).
Noncommercial. You may not use this work for commercial purposes.
Share alike. If you alter, transform, or build upon this work, you may distribute the resulting work only under the same or similar license to this one.
- For any reuse or distribution, you must make clear to others the license terms of this work. The best way to do this is with a link to this web page.
- Any of the above conditions can be waived if you get permission from the copyright holder.
- Nothing in this license impairs or restricts the author's moral rights.
Author NotesI have spent several hours checking bugs but if you find any, please tell me