Translating extension with minimal code changes

(1) Never, ever override Ruby core housekeeping methods without a non-override clause for normal exception handling when the scenario has nothing to do with why you are overriding the method.
You can simply use super, which calls the const_missing() method in the ancestor chain of superclasses. Or, alternatively, you can just raise a NameError exception right there within your override, ie:

      if module_name.const_defined?(name)
        return module_name.const_get(name)
      else
        fail(NameError, "uninitialized constant #{name}", caller)
      end

… or …

      if module_name.const_defined?(name)
        return module_name.const_get(name)
      else
        super # pass up to superclasses with all arguments
      end

(2) You must be careful so as not to get Ruby into a endless loop of calling const_missing. This line:

      module_name = (language == 0) ? English : French

… has the potential to cause Ruby to recursively call const_missing from itself, when one of the module identifiers has not yet been defined.

Also, the const_get() method will also call const_missing() when the constant is not defined, in order to decide whether a NameError should be raised. Calling const_get() method then can also set up a recursive loop (so you were correct in calling it conditionally by first checking if it was defined.)

(3) I probably would not have the get_current_language return an Integer. Instead, I would have it hold a string or symbol representing the language module name.
Or I might just set an extension wide LANG constant that points at the language module …

      begin
        load File.join(__dir__, 'lang', Sketchup.get_locale<<'.rb')
      rescue LoadError
        load File.join(__dir__, 'lang', 'en-US.rb')
      rescue
        raise
      end

Within each language file, at the level of the extension module I would set the LANG constant …

# encoding: UTF-8
# File: lang/en-US.rb

module MyName
  module Tool

    module English
      # translatable constants
    end

    LANG = English

  end
end

This could simplify your Tool.const_missing method somewhat …

module MyName
  module Tool
     
    # Load the language file:
    begin
      load File.join(__dir__, 'lang', Sketchup.get_locale<<'.rb')
    rescue LoadError
      load File.join(__dir__, 'lang', 'en-US.rb')
    rescue
      raise
    end
    # the LANG constant is now defined.

    def self.const_missing(name)
      if LANG.const_defined?(name)
        LANG.const_get(name)
      else
        super
      end
    end

    # ... other code ...

  end
end

I find it a bit “hacky”. Ruby is multi-paradigm so there are several ways of doing things.

The LanguageHandler class I find is memory intensive and uses String key lookups. String comparison in Ruby is slow compared to other equality tests. It’s use means there are 2 copies of every String object.

Since LanguageHandler is just a wrapper class around a Ruby hash, I find it easier to just define a LANG hash in the language files but I use short symbol keys. (Loading a .rb file that just defines a hash is fast because it uses Ruby’s compiled C interpreter instead of that which is coded in Ruby within the LanguageHandler class.)

So in my code LANG points at a hash loaded at the top of the extension module.
Then I often define a say() method that does the lookup in the hash and handles situations where the hash has no value for the key …

    def say(arg)
      if arg.is_a?(Symbol)
        text = LANG[sym]
        text.nil? arg.to_s : text
      else
        arg # return unchanged
      end
    end

Then in the code I use the method …

    nifty_command = UI:Command(say(:nifty_name)) { cmd_nifty() }
    UI.menu('Plugins').add_item(nifty_command)

But in practice, during testing I make sure all lookups into the LANG hash have keys and corresponding values. Only when I am done testing using an English hash do I copy the hash file and translate into other languages.


Anyway, back to the subject of using const_missing.

I think it is just another more complicated way of doing a lookup. Ie, causing the const_missing method to load a laguage file just adds complication to the code and testing it.

What I mean is that the normal behavior of const_missing is a valuable tool during testing that tells you your code has not loaded it’s resources properly. Changing how it works can work against you.

Yes, really I do not see the need for const_missing to be called many times during runtime. The extension’s language resource should be loaded once and then just used.

I’ve been doing it at the top of the extension module as one of the initial load tasks. But recently we’ve been discussing various ways to defer loading of large blocks of code or resources until the extension actually needs them as indicated by the user purposefully activating a feature of the extension. (ie, clicking a menu item or toolbar button, etc.)

As part of this, we have been talking about const_missing loading parts of the extension code “on demand”.
See: Extension Warehouse - set limit for number of startup files - Developers / Ruby API - SketchUp Community

So, it seems that your code is attempting to defer the loading and definition of a language module. When the translated constant is not accessible in the extension submodule (Tool in the example,) your const_missing will seek to load the file.

  • But, I do not think const_missing should be used continuously as a lookup method. Instead I would think it better to include the loaded language module into the parent extension module thereby making the needed constants available from that point forward.
      begin
        load File.join(__dir__, 'lang', Sketchup.get_locale<<'.rb')
      rescue LoadError
        load File.join(__dir__, 'lang', 'en-US.rb')
      rescue
        raise
      else
        # LANG set in the language file evaluation
        include LANG
        # The constants of the module pointed at by LANG are now available
      end
    
    Also, require and require_relative do not need to be used as they search $LOADED_FEATURES (which is unnecessary in your case as your code determines that the language module has not been loaded,) and then bloats the $LOADED_FEATURES array further by stuffing the load path into it. (There are some speed issues with lookups into the $LOADED_FEATURES array and it’s best not to bloat it when you know it will never need to be searched for a particular file path.)
    Note: load() does not use the $LOAD_PATH array to search for the file, so an absolute pathname must be passed to a valid existing file. This also means the filetype must be appended.

Regarding language files, one of the ways I’ve limited what was loaded, is to divide the strings into at least two files.
One I call the “info” file which only has the translated strings for the SketchupExtension instance’s properties that are displayed in the Extension Manager.
The other string file I’ve talked about above, which is the runtime translations that are only needed if the user has the extension set to load in the manager.

Now, regarding runtime translations. There will be a need for minimum translated text for the extension’s command menu texts, tooltips and statusbar texts and perhaps any messagebox error texts if some command is not evoked in the proper scenario.

In summation, I am not yet sure if I’ll use const_missing or just keep a hash containing flags for loaded resources that can be checked before a certain resource is needed, then loaded on demand if not and the flag in the hash then set true.

Just as the LanguageHandler class is a wrapper around a hash, extension authors could wrap or subclass Hash
and change the behavior when a key is not found to load that key/value pair “on demand”. (Just another idea in my head.)