Language Handler Problems

I’m trying to get the language handler to work but for the life of me I can’t get it to translate.

My code that loads it up is here:


# First we pull in the standard API hooks.

require 'sketchup.rb'
require 'extensions.rb'
# require 'langhandler.rb'



# Define Module Hierarchy

module Medeek_Engineering_Inc_Extensions

	module MedeekTrussPlugin
	
	# require 'langhandler.rb'
	# LH = LanguageHandler.new("medeek_truss.strings")

##############################
#
# Class Methods of Plugin
#
##############################

class MedeekMethods
  	class << self

include Math

require 'langhandler.rb'
LH = LanguageHandler.new("medeek_truss.strings")

Then I call it up in my first prompt:

prompts1 = [LH["Truss Type: "], "Out-to-out Span (ft.): ", "Top chord Pitch (x/12): ", "Overhang Left (in.): ", "Overhang Right (in.): ", "TC Size (in.): ", "BC Size (in.): ", "Web Size (in.): ", "Ply Thickness (in.): ", "Raised Heel: ", "Heel Height (in.): "]
 			defaults1 = ["#{@Trusstype}", "#{@TrussSpan_ft.round(5)}", "#{@Pitch}", "#{@Overhangl}", "#{@Overhangr}", "#{@Tcd}", "#{@Bcd}", "#{@Webd}", "#{@Ply}", "#{@Raisedheel}", "#{@Usrhh}"]
 			list1 = ["King Post|Queen Post|Fink|Howe|Fan|Mod Queen|Double Fink|Double Howe|Mod Fan|Triple Fink|Triple Howe|Quad Fink", "", "2.5|3.0|3.5|4.0|4.5|5.0|5.5|6.0|6.5|7.0|7.5|8.0|8.5|9.0|9.5|10.0|10.5|11.0|11.5|12.0|12.5|13.0|13.5|14.0|14.5|15.0|15.5|16.0", "", "", "3.5|5.5|7.25|9.25|9.5|11.25|11.875|14.0|16.0", "3.5|5.5|7.25|9.25|9.5|11.25|11.875|14.0|16.0", "3.5|5.5|7.25|9.25|11.25", "", "NO|YES", ""]
 		

In my medeek_truss.strings file I have this line:

"Truss Type: "="Type de Treillis: ";

I’ve put this file into the Resources/en-US subfolder.

All that displays is the original phrase "Truss Type: ".

What am I missing? I’ve mucked around with this for over an hour now and nothing.

your own or SU’s…

LH = LanguageHandler.new("medeek_truss.strings")

SU will be looking for

 "YourExtensionFolder/Resources/en-US/medeek_truss.strings"

is that where it is?

john

for prompts it’s easiest to convert an array…

      prompts = [
        'Radius of Circle',
        'Circle Segments',
        'Wave Count',
        'Wave Height',
        'Create Single Wave',
        'with Circle',
        'with Surface',
        'with Cylinder',
        'with Castellation',
        'Face Thickness',
        'Explode Grouping'
        ]

      @prompts = prompts.each do |prompt|
       TRANSL8[prompt]
      end
``

From what I remember of the work Steve (@slbaumgartner) did helping me to redevelop his original Angular Dimension plugin, you need to put it into a folder for the language you are going to use - looks like French in this example.

Have a look at the Angular Dimension 2 plugin from Sketchucation to see how the language handler is used there.

Steve set up the code to use langhandler, and I did most of the work using Google Translate and a little feedback from users to create the data files for it, and to test them.

Here’s the file structure under Resources in the Angular Dimensions 2 plugin folder:
image

You need to put the .strings files into a separate subfolder for each language, I think, using the appropriate language code (two letters, except for en-US and pt_BR in our case).

We used a separate method to set up the HTML web-dialogue pages, using placeholder tokens where different languages need different words in the page. I wrote some code that took the original tokenised page, substituted the translation for the token, and generated the language specific web-dialogue HTML pages. They, and the translation strings used to create them, are also preserved in the individual language folders in Resources, but for that we used pipe separated strings, not quoted strings with = signs.

This isn’t the only way to do it, but it worked for us. Internally, if I recall correctly what Steve said, the language handler gets a language code back from the local regional settings (I don’t think it’s the SU installation), and uses it to decide which .strings file to use.

That makes it quite tricky to test, as it isn’t that easy to set up different language versions of your OS, whether Mac or Windows, but Steve wrote code to allow the plugin language to be changed by the user, regardless of OS or SU’s own settings.

PS.

If I as I assume you are using an en-US version of SU, I’m not quite sure what happens when you put a translation file in the en-US folder. IIRC if language handler doesn’t find a translation for a particular term in a particular language, it falls back to using the native ‘hard coded’ US terminology, so we don’t have any .strings file in the en-US Resources subfolder, just the conversions from tokens to English for the HTML pages.

Try putting the file in a fr sub folder and pointing Language Handler to that.

I have it precisely like this.

To test it I put the medeek_truss.strings file (french version) into the en-US folder but it won’t load the values.

I tried installing the French version of SketchUp on my same computer that I already have the english version installed, that does not seem to work, only the english version will load.

it should after a restart…

any line with any error will fail silently to english…

john

I’ve implemented the array technique, that will eventually save me a lot of time:


if @Roofslope_db == "PITCH"
			prompts1 = ['Truss Type', "Out-to-out Span (ft.): ", "Top chord Pitch (x/12): ", "Overhang Left (in.): ", "Overhang Right (in.): ", "TC Size (in.): ", "BC Size (in.): ", "Web Size (in.): ", "Ply Thickness (in.): ", "Raised Heel: ", "Heel Height (in.): "]
 			defaults1 = ["#{@Trusstype}", "#{@TrussSpan_ft.round(5)}", "#{@Pitch}", "#{@Overhangl}", "#{@Overhangr}", "#{@Tcd}", "#{@Bcd}", "#{@Webd}", "#{@Ply}", "#{@Raisedheel}", "#{@Usrhh}"]
 			list1 = ["King Post|Queen Post|Fink|Howe|Fan|Mod Queen|Double Fink|Double Howe|Mod Fan|Triple Fink|Triple Howe|Quad Fink", "", "2.5|3.0|3.5|4.0|4.5|5.0|5.5|6.0|6.5|7.0|7.5|8.0|8.5|9.0|9.5|10.0|10.5|11.0|11.5|12.0|12.5|13.0|13.5|14.0|14.5|15.0|15.5|16.0", "", "", "3.5|5.5|7.25|9.25|9.5|11.25|11.875|14.0|16.0", "3.5|5.5|7.25|9.25|9.5|11.25|11.875|14.0|16.0", "3.5|5.5|7.25|9.25|11.25", "", "NO|YES", ""]
 		elsif @Roofslope_db == "ANGLE"
			prompts1 = ['Truss Type', "Out-to-out Span (ft.): ", "Top chord Angle (deg): ", "Overhang Left (in.): ", "Overhang Right (in.): ", "TC Size (in.): ", "BC Size (in.): ", "Web Size (in.): ", "Ply Thickness (in.): ", "Raised Heel: ", "Heel Height (in.): "]
 			defaults1 = ["#{@Trusstype}", "#{@TrussSpan_ft.round(5)}", "#{@Pitch_deg}", "#{@Overhangl}", "#{@Overhangr}", "#{@Tcd}", "#{@Bcd}", "#{@Webd}", "#{@Ply}", "#{@Raisedheel}", "#{@Usrhh}"]
 			list1 = ["King Post|Queen Post|Fink|Howe|Fan|Mod Queen|Double Fink|Double Howe|Mod Fan|Triple Fink|Triple Howe|Quad Fink", "", "", "", "", "3.5|5.5|7.25|9.25|9.5|11.25|11.875|14.0|16.0", "3.5|5.5|7.25|9.25|9.5|11.25|11.875|14.0|16.0", "3.5|5.5|7.25|9.25|11.25", "", "NO|YES", ""]
 		
		else
			UI.messagebox("Non-valid roof slope type, action aborted.") 
			exit 0
		end

		@prompts1 = prompts1.each do |prompt|
			LH[prompt]
			puts LH[prompt]
		end
	
		@Inputs1 = UI.inputbox(@prompts1, defaults1, list1, "Medeek Truss Plugin - Geometry")

Still can’t get it to work though. Must be the way I have all these multiple nested modules and classes that is messing things up.

This language handler does not seem very robust, I might just have to build my own like Garry K. has done.

No you cannot have two editions of the same year release on the same computer. (Both will be overwriting each others registry entries also, possibly causing havoc.)

In related thread: Creating language resource files

So you can create a series of desktop icons for testing in the various supported languages, by editing the “Target:” field, of the “Shortcut” tab, of the icon’s “Properties”.

I think you would just add a space and then "-lang=fr" after the pathname to a copy of the standard icon made by the installer. Then of course you’d change the shortcut’s name to indicate French.

Repeat for other languages.

Only run one language test application instance at a time (as they’ll be sharing the same registry hive.)

Note what Thomas says concerning Sketchup::get_locale():


FYI:

Swedish has been added recently. NL was dropped after v8. Russian may have been dropped for a few years after v8.

Related posts elsewhere:

Related topic threads:

… and from those, other nuggets you should know:

As I mentioned in some of the other threads …

Keep in mind the interpreter parser is compiled C while the LanguageHandler class’ parser is interpreted Ruby.

I’m not sure if I have posted an example, but basically I just create a “lang” subfolder of my plugin folder, and populate it with .rb scripts that use a naming syntax like “#{LANG}.rb”, where the plugin constant LANG is either a specific language the user has selected in my plugin’s options dialog (which can be even languages the SketchUp doesn’t even ship using,) or (upon first run) the string returned by Sketchup::get_locale().

In the main plugin file, I’d define some lang constants used by all languages:

# encoding: UTF-8
module Author::SomePlugin

  KEY ||= Module.nesting.name[0].gsub('::','_') # qualified registry / plist key

  LANGCODES ||= Hash[
    :english,'en-US', :french,'fr', :italian,'it',
    :german,'de', :spanish,'es', :japan,'ja',
    :korean,'ko', :china,'zh-CN', :taiwan,'zh-TW',
    :portbraz,'pt-BR', :dutch,'nl', :russian,'ru'
  ]

  LANG ||= Sketchup.read_default(KEY,'lang') 
  if LANG.nil? # has not been set (1st run)
    LANG = Sketchup::get_locale # set it
    Sketchup.write_default(KEY,'lang',LANG) # store it
  end
  LANGFILE ||= File.join( File.dirname(__FILE__),"lang/#{LANG}.rb" )
  load(LANGFILE) rescue load( File.join( File.dirname(__FILE__),'lang/en-US.rb' ))

end

Then the contents of the language ruby scripts for "lang/en-US.rb" would be like:

# encoding: UTF-8
module Author::SomePlugin

  OPTIONS_PROMPTS ||= {
    :lang => 'Language',
    :data => 'Data Path',
    :save => 'Save Path:',
    # etc., etc., ...
  }

  OPTIONS_ITEMS ||= {
    :lang => LANGCODES.join('|'),
  }

  STATUSBAR ||= {
    :tool1 => 'This tool does such and so forth',
    :tool2 => 'This tool does more than any other'
    # etc., etc., ...
  }

  # ETC., ETC., ...

end

Then I copy the english RB file a number of times and edit it and rename it for other languages.

2 Likes

I really like how this approach uses identifiers different from the English string. A lot of the faulty translations in SketchUp is because things are translated out of context. E.g. Extension could both be a Ruby extension or the style setting of extending visually extending edges (View > Edges > Extension). Group could both be a verb and a noun (though newer SU versions call the action “Make Group in the context menu”).

Using identifiers such as tool_status_pick_entity, tool_status_modify, ro_extend_edges etc is safer, especially when the translators are sloppy.

1 Like

I like what I see here but I’m trying to figure out if I can implement the system you’ve described above and still somehow use the array technique given by John so that I don’t need to completely rewrite all of my prompts. Unfortunately, I have a lot of them so any time saver is huge.

I and many other people have rolled our own in the past…

it’s the main reason a generic translation tool has never been successfully developed…

The ‘biggest’ problem with LanguageHandler was addressed in SU v14…

The ‘small’ change was the ability to have our own ‘Resources’ folder structure, removing the need to store files in SU’s application folder…

LanguageHandler has a few ‘gotcha’s’, but they can be circumvented…

one is it uses a existing file format that doesn’t play well with ruby…

Associated file extensions: .strings
i18n type: STRINGS
Encoding: UTF-16
.strings files are used for localizing macOS and iOS apps.

I’m not sure if/how this effects windows…

it is an issue on mac [at least], but as LanguageHandler discounts any line containing / or */ you can add an encoding to the file and save to match…

# encoding: UTF-8 // langhandlers completely skips any line with // or */
another is it documentation implies the need for ‘one to one’ en-US strings, which makes updating in code text difficult…

Apples example uses keywords and comments to aid translation…

/* registration information: Username*/
“username” = “Username:”;
/* registration information: Password*/
“password” = “Password:”;
/* registration information: Email Address*/
“email_address” = “Email Address:”;
/* registration information: User Role*/
“user_role” = “User Role”;
/* registration information: Phone*/
“phone” = “Phone”;
/* registration information: Country*/
“country” = “Country”;

I should probably pushed the prompts in a ruby fashion…

      @prompts = []
      prompts.each do |prompt|
        @prompts << TRANSL8[prompt]
      end

may be why your test didn’t work…

john

Or

@promts = prompts.map { |p| TRANSL8[p] }
1 Like

I have a personal dislike for using |p| but :map is possibly a better approach…

EDIT: rubocop tells me this is ‘best’ for the strings…

    prompts = %w(
      radius
      segments
      frequency
      height
      line_only
      with_circle
      with_surface
      with_cylinder
      with_castellation
      surface_thickness
      explode_groups
    )

     @prompts = prompts.map { |prompt| TRANSL8[prompt] }

and then in the strings remembering the other 'LanguageHandler ‘gotcha’…

keyvalue = entryString.split('"="')

hardcoded to NOT accept whitespace around the =

# encoding: UTF-8 // langhandlers completely skips any line with // or */
"The string file can be translated"="The string file can be translated";
"Sine Circle Details"="Sine Circle Details";
"radius"="Radius of Circle";
"segments"="Circle Segments";
"frequency"="Wave Count";
"height"="Wave Height";
"line_only"="Create Single Wave";
"with_surface"="with Surface";
"with_circle"="with Circle";
"with_cylinder"="with Cylinder";
"with_castellation"="with Castellation";
"surface_thickness"="Face Thickness";
"explode_groups"="Explode Grouping";
"parram_legend"="Set parrameters for Wave";

one advantage this has is you can see if the en-US file works, before trying other languages…

john

I would really like to encourage everyone to using Langhandler to make translation of any extensions easier for the User base…

I started this in 2013 and it has been used by many developers and users since then…

but it only works on SU’s version of .string files…

if you don’t ‘understand’ a language, using phrases or word combinations gets better matches…

when a native speaker uses this and sends you the file, it’s even better…

it is also not limited to the ‘SU’ supported languages…

john

Yes that is nifty, but it can be used to translate strings, and then later still use hash or another author specific format, by using a Ruby utility to read .strings files and convert to something else.

There are two separate issues here. The translation of strings, and the workings of LanguageHandler class objects.


The latter is what is “clunky”. All example code shows creating and using perpetual LanguageHandler instance objects, which waste memory (2 copies of every string,) and are extremely slow (ie, Ruby String comparison.)

The examples encourage coders to look up the translated string in the LanguageHandler instance object each time it is needed. This is what I mainly take issue with.

Instead an extension (if it uses LanguageHandler instance objects,) … should do most it’s lookups when it first loads up, and store the translated strings in extension local objects (arrays, hashes, openstructs or sets, etc.) Then dispose of the LanguageHandler instance object(s).

One of my longest “pet peeves” are the perpetual global LanguageHandler instance objects created by all of the Trimble extensions. They sit there in memory even when the extensions are not being used !

this is the trace from loading Sine Circle…

"start"
[54, LanguageHandler, :[], "Sine Circle Details"]
[54, LanguageHandler, :[], "Click to Close"]
"HtmlDialog"
[54, LanguageHandler, :[], "Radius of Circle"]
[54, LanguageHandler, :[], "Circle Segments"]
[54, LanguageHandler, :[], "Wave Count"]
[54, LanguageHandler, :[], "Wave Height"]
[54, LanguageHandler, :[], "Create Single Wave"]
[54, LanguageHandler, :[], "with Circle"]
[54, LanguageHandler, :[], "with Surface"]
[54, LanguageHandler, :[], "with Cylinder"]
[54, LanguageHandler, :[], "with Castellation"]
[54, LanguageHandler, :[], "Face Thickness"]
[54, LanguageHandler, :[], "Explode Grouping"]
[54, LanguageHandler, :[], "Help"]
[50, Length, :initialize, 196.8503937007874]
[54, LanguageHandler, :[], "Set parrameters for Wave"]
[31, Length, :to_f, 196.8503937007874]
[54, LanguageHandler, :[], "Cancel"]
[54, LanguageHandler, :[], "OK"]

it appears to use the key, once…

john

If you refer to how you coded in this post …

… then I’m sorry, but that is “hard-wired” to use a single language (and a single unit of measure, which is a separate issue.)


You need to think about multilanguage support at the beginning, and code in a more flexible way.

Since you did not, then you’ll definitely need to do some code refactoring, but it will be educational, and perhaps a bit “painful”, but that’ll just provide more encouragement to do things better from the start, next time.

Build your prompt, default and choice arrays at the loadup of your extension (or sub-modules), and store them referenced by local constants, ie, PITCH_PROMPTS, PITCH_DEFAULTS, PITCH_CHOICES, ANGLE_PROMPTS, etc.

if @Roofslope_db == "PITCH"
  @Inputs1 = UI.inputbox( PITCH_PROMPTS, PITCH_DEFAULTS, PITCH_CHOICES, CAPTION )
elsif @Roofslope_db == "ANGLE"
  @Inputs1 = UI.inputbox( ANGLE_PROMPTS, ANGLE_DEFAULTS, ANGLE_CHOICES, CAPTION )
else
  UI.messagebox( ERROR_TEXT["Non-valid roof slope type, action aborted."] )
  raise InvaildRoofTypeError  # <-- local RuntimeError subclass
end

This makes code much more readable and hence easier to maintain.

(Notice that I replaced "exit 0" with a raising of a plugin specific runtime exception. SystemExit has no real use in an embedded Ruby interpreter, and used to cause a BugSplat in older SketchUp versions.)