[XP] AzDesign Localization

Started by azdesign, September 06, 2012, 03:53:23 am

Previous topic - Next topic

azdesign

September 06, 2012, 03:53:23 am Last Edit: September 08, 2012, 06:02:34 am by azdesign
AzDesign Localization
Authors: AzDesign
Version: 1.0
Type: Multilingual Game Tool
Key Term: Game Utility



Introduction

Do 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



Features

1. 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

Spoiler: ShowHide





Demo

Please make sure to set the library to RGSS103J, otherwise, the script will hung up. The demo has already set with RGSS103J
Please make sure to set your system locale into japanese to display non-standard character properly

http://www.mediafire.com/?vscc64d5y1wqy5h



Script
Spoiler: ShowHide
#-----------------------------------------------------------#
#------------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




Instructions

1. 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"
Spoiler: ShowHide

-There are 3 different sections required for a language file :
Spoiler: ShowHide

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



Compatibility

Not 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 Thanks

Credit to AzDesign for the script
Thanks to everyone who has answered my question in different topic I posted in the past. :haha:
Thanks to ForeverZer0 whose Localization script become my learning base.  ;)



License

Creative 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 Notes

I have spent several hours checking bugs but if you find any, please tell me  ;)
~ Check out my deviantart http://my-az-design.deviantart.com/ ~

Blizzard

*fixes header*
*moves*
Make sure the next time to apply the header properly. This is important for the PHP script to display this topic in the script database index.
Check out Daygames and our games:

King of Booze 2      King of Booze: Never Ever
Drinking Game for Android      Never have I ever for Android
Drinking Game for iOS      Never have I ever for iOS


Quote from: winkioI do not speak to bricks, either as individuals or in wall form.

Quote from: Barney StinsonWhen I get sad, I stop being sad and be awesome instead. True story.

azdesign

 :^_^': Lol I didn't know there is header standard for this, is there is a reference for this I can read for the future I wrote another topic ?
~ Check out my deviantart http://my-az-design.deviantart.com/ ~

Blizzard

Either take any other topic or use the template directly from this topic: http://forum.chaos-project.com/index.php/topic,17.0.html
Check out Daygames and our games:

King of Booze 2      King of Booze: Never Ever
Drinking Game for Android      Never have I ever for Android
Drinking Game for iOS      Never have I ever for iOS


Quote from: winkioI do not speak to bricks, either as individuals or in wall form.

Quote from: Barney StinsonWhen I get sad, I stop being sad and be awesome instead. True story.

azdesign

September 08, 2012, 06:13:25 am #4 Last Edit: September 08, 2012, 06:16:41 am by azdesign
@Blizzard : Thanks, I'll use that for future reference.

ADDED : Spoilers and Script

For those who wondering, is this the same as ForeverZer0's Localization from  : http://forum.chaos-project.com/index.php/topic,12164.0.html,
No, we are using different method to get the text from the file. Also, mine has that text error finding feature and has automatic line splitting for text exceeding message window's width. I thought his script was very useful for my project, I was about to use it, but still, it doesn't have the features I wanted so I create one myself.  :)

Don't forget to change system locale into corresponding language for displaying non-standard characters such as japanese and chinese. Example, if you want to display chinese characters, set your computer system locale into chinese. For the rest of the language that using alphabets, you don't have to change anything.
~ Check out my deviantart http://my-az-design.deviantart.com/ ~